Skip to content

Commit eda681a

Browse files
authored
Call Python 3.8 doClassCleanups (#8033)
1 parent b7ba766 commit eda681a

File tree

4 files changed

+210
-10
lines changed

4 files changed

+210
-10
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ Pauli Virtanen
232232
Pavel Karateev
233233
Paweł Adamczak
234234
Pedro Algarvio
235+
Petter Strandmark
235236
Philipp Loose
236237
Pieter Mulder
237238
Piotr Banaszkiewicz

changelog/8032.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`doClassCleanups` (introduced in `unittest` in Python and 3.8) is now called.

src/_pytest/unittest.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,26 +99,48 @@ def _inject_setup_teardown_fixtures(self, cls: type) -> None:
9999
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
100100
teardown functions (#517)."""
101101
class_fixture = _make_xunit_fixture(
102-
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
102+
cls,
103+
"setUpClass",
104+
"tearDownClass",
105+
"doClassCleanups",
106+
scope="class",
107+
pass_self=False,
103108
)
104109
if class_fixture:
105110
cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
106111

107112
method_fixture = _make_xunit_fixture(
108-
cls, "setup_method", "teardown_method", scope="function", pass_self=True
113+
cls,
114+
"setup_method",
115+
"teardown_method",
116+
None,
117+
scope="function",
118+
pass_self=True,
109119
)
110120
if method_fixture:
111121
cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
112122

113123

114124
def _make_xunit_fixture(
115-
obj: type, setup_name: str, teardown_name: str, scope: "_Scope", pass_self: bool
125+
obj: type,
126+
setup_name: str,
127+
teardown_name: str,
128+
cleanup_name: Optional[str],
129+
scope: "_Scope",
130+
pass_self: bool,
116131
):
117132
setup = getattr(obj, setup_name, None)
118133
teardown = getattr(obj, teardown_name, None)
119134
if setup is None and teardown is None:
120135
return None
121136

137+
if cleanup_name:
138+
cleanup = getattr(obj, cleanup_name, lambda *args: None)
139+
else:
140+
141+
def cleanup(*args):
142+
pass
143+
122144
@pytest.fixture(
123145
scope=scope,
124146
autouse=True,
@@ -130,16 +152,32 @@ def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
130152
reason = self.__unittest_skip_why__
131153
pytest.skip(reason)
132154
if setup is not None:
133-
if pass_self:
134-
setup(self, request.function)
135-
else:
136-
setup()
155+
try:
156+
if pass_self:
157+
setup(self, request.function)
158+
else:
159+
setup()
160+
# unittest does not call the cleanup function for every BaseException, so we
161+
# follow this here.
162+
except Exception:
163+
if pass_self:
164+
cleanup(self)
165+
else:
166+
cleanup()
167+
168+
raise
137169
yield
138-
if teardown is not None:
170+
try:
171+
if teardown is not None:
172+
if pass_self:
173+
teardown(self, request.function)
174+
else:
175+
teardown()
176+
finally:
139177
if pass_self:
140-
teardown(self, request.function)
178+
cleanup(self)
141179
else:
142-
teardown()
180+
cleanup()
143181

144182
return fixture
145183

testing/test_unittest.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,3 +1260,163 @@ def test_plain_unittest_does_not_support_async(testdir):
12601260
"*1 passed*",
12611261
]
12621262
result.stdout.fnmatch_lines(expected_lines)
1263+
1264+
1265+
@pytest.mark.skipif(
1266+
sys.version_info < (3, 8), reason="Feature introduced in Python 3.8"
1267+
)
1268+
def test_do_class_cleanups_on_success(testdir):
1269+
testpath = testdir.makepyfile(
1270+
"""
1271+
import unittest
1272+
class MyTestCase(unittest.TestCase):
1273+
values = []
1274+
@classmethod
1275+
def setUpClass(cls):
1276+
def cleanup():
1277+
cls.values.append(1)
1278+
cls.addClassCleanup(cleanup)
1279+
def test_one(self):
1280+
pass
1281+
def test_two(self):
1282+
pass
1283+
def test_cleanup_called_exactly_once():
1284+
assert MyTestCase.values == [1]
1285+
"""
1286+
)
1287+
reprec = testdir.inline_run(testpath)
1288+
passed, skipped, failed = reprec.countoutcomes()
1289+
assert failed == 0
1290+
assert passed == 3
1291+
1292+
1293+
@pytest.mark.skipif(
1294+
sys.version_info < (3, 8), reason="Feature introduced in Python 3.8"
1295+
)
1296+
def test_do_class_cleanups_on_setupclass_failure(testdir):
1297+
testpath = testdir.makepyfile(
1298+
"""
1299+
import unittest
1300+
class MyTestCase(unittest.TestCase):
1301+
values = []
1302+
@classmethod
1303+
def setUpClass(cls):
1304+
def cleanup():
1305+
cls.values.append(1)
1306+
cls.addClassCleanup(cleanup)
1307+
assert False
1308+
def test_one(self):
1309+
pass
1310+
def test_cleanup_called_exactly_once():
1311+
assert MyTestCase.values == [1]
1312+
"""
1313+
)
1314+
reprec = testdir.inline_run(testpath)
1315+
passed, skipped, failed = reprec.countoutcomes()
1316+
assert failed == 1
1317+
assert passed == 1
1318+
1319+
1320+
@pytest.mark.skipif(
1321+
sys.version_info < (3, 8), reason="Feature introduced in Python 3.8"
1322+
)
1323+
def test_do_class_cleanups_on_teardownclass_failure(testdir):
1324+
testpath = testdir.makepyfile(
1325+
"""
1326+
import unittest
1327+
class MyTestCase(unittest.TestCase):
1328+
values = []
1329+
@classmethod
1330+
def setUpClass(cls):
1331+
def cleanup():
1332+
cls.values.append(1)
1333+
cls.addClassCleanup(cleanup)
1334+
@classmethod
1335+
def tearDownClass(cls):
1336+
assert False
1337+
def test_one(self):
1338+
pass
1339+
def test_two(self):
1340+
pass
1341+
def test_cleanup_called_exactly_once():
1342+
assert MyTestCase.values == [1]
1343+
"""
1344+
)
1345+
reprec = testdir.inline_run(testpath)
1346+
passed, skipped, failed = reprec.countoutcomes()
1347+
assert passed == 3
1348+
1349+
1350+
def test_do_cleanups_on_success(testdir):
1351+
testpath = testdir.makepyfile(
1352+
"""
1353+
import unittest
1354+
class MyTestCase(unittest.TestCase):
1355+
values = []
1356+
def setUp(self):
1357+
def cleanup():
1358+
self.values.append(1)
1359+
self.addCleanup(cleanup)
1360+
def test_one(self):
1361+
pass
1362+
def test_two(self):
1363+
pass
1364+
def test_cleanup_called_the_right_number_of_times():
1365+
assert MyTestCase.values == [1, 1]
1366+
"""
1367+
)
1368+
reprec = testdir.inline_run(testpath)
1369+
passed, skipped, failed = reprec.countoutcomes()
1370+
assert failed == 0
1371+
assert passed == 3
1372+
1373+
1374+
def test_do_cleanups_on_setup_failure(testdir):
1375+
testpath = testdir.makepyfile(
1376+
"""
1377+
import unittest
1378+
class MyTestCase(unittest.TestCase):
1379+
values = []
1380+
def setUp(self):
1381+
def cleanup():
1382+
self.values.append(1)
1383+
self.addCleanup(cleanup)
1384+
assert False
1385+
def test_one(self):
1386+
pass
1387+
def test_two(self):
1388+
pass
1389+
def test_cleanup_called_the_right_number_of_times():
1390+
assert MyTestCase.values == [1, 1]
1391+
"""
1392+
)
1393+
reprec = testdir.inline_run(testpath)
1394+
passed, skipped, failed = reprec.countoutcomes()
1395+
assert failed == 2
1396+
assert passed == 1
1397+
1398+
1399+
def test_do_cleanups_on_teardown_failure(testdir):
1400+
testpath = testdir.makepyfile(
1401+
"""
1402+
import unittest
1403+
class MyTestCase(unittest.TestCase):
1404+
values = []
1405+
def setUp(self):
1406+
def cleanup():
1407+
self.values.append(1)
1408+
self.addCleanup(cleanup)
1409+
def tearDown(self):
1410+
assert False
1411+
def test_one(self):
1412+
pass
1413+
def test_two(self):
1414+
pass
1415+
def test_cleanup_called_the_right_number_of_times():
1416+
assert MyTestCase.values == [1, 1]
1417+
"""
1418+
)
1419+
reprec = testdir.inline_run(testpath)
1420+
passed, skipped, failed = reprec.countoutcomes()
1421+
assert failed == 2
1422+
assert passed == 1

0 commit comments

Comments
 (0)