Skip to content

Commit b5cc897

Browse files
committed
Fix cleanup functions not being invoked on test failures
Also delay calling tearDown() when --pdb is given, so users still have access to the instance variables (which are usually cleaned up during tearDown()) when debugging. Fix #6947
1 parent 0b78983 commit b5cc897

File tree

4 files changed

+103
-32
lines changed

4 files changed

+103
-32
lines changed

changelog/6947.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures.

src/_pytest/debugging.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,15 @@ def pytest_internalerror(self, excrepr, excinfo):
272272
class PdbTrace:
273273
@hookimpl(hookwrapper=True)
274274
def pytest_pyfunc_call(self, pyfuncitem):
275-
_test_pytest_function(pyfuncitem)
275+
wrap_pytest_function_for_tracing(pyfuncitem)
276276
yield
277277

278278

279-
def _test_pytest_function(pyfuncitem):
279+
def wrap_pytest_function_for_tracing(pyfuncitem):
280+
"""Changes the python function object of the given Function item by a wrapper which actually
281+
enters pdb before calling the python function itself, effectively leaving the user
282+
in the pdb prompt in the first statement of the function.
283+
"""
280284
_pdb = pytestPDB._init_pdb("runcall")
281285
testfunction = pyfuncitem.obj
282286

@@ -291,6 +295,13 @@ def wrapper(*args, **kwargs):
291295
pyfuncitem.obj = wrapper
292296

293297

298+
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
299+
"""Wrap the given pytestfunct item for tracing support if --trace was given in
300+
the command line"""
301+
if pyfuncitem.config.getvalue("trace"):
302+
wrap_pytest_function_for_tracing(pyfuncitem)
303+
304+
294305
def _enter_pdb(node, excinfo, rep):
295306
# XXX we re-use the TerminalReporter's terminalwriter
296307
# because this seems to avoid some encoding related troubles

src/_pytest/unittest.py

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
""" discovery and running of std-library "unittest" style tests. """
2-
import functools
32
import sys
43
import traceback
54

@@ -114,15 +113,18 @@ class TestCaseFunction(Function):
114113
_testcase = None
115114

116115
def setup(self):
117-
self._needs_explicit_tearDown = False
118116
self._testcase = self.parent.obj(self.name)
119117
self._obj = getattr(self._testcase, self.name)
120118
if hasattr(self, "_request"):
121119
self._request._fillfixtures()
122120

121+
# a bound method to be called during teardown() if set (see 'runtest()')
122+
self._explicit_tearDown = None
123+
123124
def teardown(self):
124-
if self._needs_explicit_tearDown:
125-
self._testcase.tearDown()
125+
if self._explicit_tearDown:
126+
self._explicit_tearDown()
127+
self._explicit_tearDown = None
126128
self._testcase = None
127129
self._obj = None
128130

@@ -205,40 +207,31 @@ def _expecting_failure(self, test_method) -> bool:
205207
return bool(expecting_failure_class or expecting_failure_method)
206208

207209
def runtest(self):
208-
# TODO: move testcase reporter into separate class, this shouldnt be on item
209-
import unittest
210-
211-
testMethod = getattr(self._testcase, self._testcase._testMethodName)
212-
213-
class _GetOutOf_testPartExecutor(KeyboardInterrupt):
214-
"""Helper exception to get out of unittests's testPartExecutor (see TestCase.run)."""
210+
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
215211

216-
@functools.wraps(testMethod)
217-
def wrapped_testMethod(*args, **kwargs):
218-
"""Wrap the original method to call into pytest's machinery, so other pytest
219-
features can have a chance to kick in (notably --pdb)"""
220-
try:
221-
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
222-
except unittest.SkipTest:
223-
raise
224-
except Exception as exc:
225-
expecting_failure = self._expecting_failure(testMethod)
226-
if expecting_failure:
227-
raise
228-
self._needs_explicit_tearDown = True
229-
raise _GetOutOf_testPartExecutor(exc)
212+
maybe_wrap_pytest_function_for_tracing(self)
230213

231214
# let the unittest framework handle async functions
232215
if is_async_function(self.obj):
233216
self._testcase(self)
234217
else:
235-
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
218+
# when --pdb is given, we want to postpone calling tearDown() otherwise
219+
# when entering the pdb prompt, tearDown() would have probably cleaned up
220+
# instance variables, which makes it difficult to debug
221+
# arguably we could always postpone tearDown(), but this changes the moment where the
222+
# TestCase instance interacts with the results object, so better to only do it
223+
# when absolutely needed
224+
if self.config.getoption("usepdb"):
225+
self._explicit_tearDown = self._testcase.tearDown
226+
setattr(self._testcase, "tearDown", lambda *args: None)
227+
228+
# we need to update the actual bound method with self.obj, because
229+
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper
230+
setattr(self._testcase, self.name, self.obj)
236231
try:
237-
self._testcase(result=self)
238-
except _GetOutOf_testPartExecutor as exc:
239-
raise exc.args[0] from exc.args[0]
232+
self._testcase.run(result=self)
240233
finally:
241-
delattr(self._testcase, self._testcase._testMethodName)
234+
delattr(self._testcase, self.name)
242235

243236
def _prunetraceback(self, excinfo):
244237
Function._prunetraceback(self, excinfo)

testing/test_unittest.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,37 @@ def test_notTornDown():
876876
reprec.assertoutcome(passed=1, failed=1)
877877

878878

879+
def test_cleanup_functions(testdir):
880+
"""Ensure functions added with addCleanup are always called after each test ends (#6947)"""
881+
testdir.makepyfile(
882+
"""
883+
import unittest
884+
885+
cleanups = []
886+
887+
class Test(unittest.TestCase):
888+
889+
def test_func_1(self):
890+
self.addCleanup(cleanups.append, "test_func_1")
891+
892+
def test_func_2(self):
893+
self.addCleanup(cleanups.append, "test_func_2")
894+
assert 0
895+
896+
def test_func_3_check_cleanups(self):
897+
assert cleanups == ["test_func_1", "test_func_2"]
898+
"""
899+
)
900+
result = testdir.runpytest("-v")
901+
result.stdout.fnmatch_lines(
902+
[
903+
"*::test_func_1 PASSED *",
904+
"*::test_func_2 FAILED *",
905+
"*::test_func_3_check_cleanups PASSED *",
906+
]
907+
)
908+
909+
879910
def test_issue333_result_clearing(testdir):
880911
testdir.makeconftest(
881912
"""
@@ -1131,6 +1162,41 @@ def test(self):
11311162
assert result.ret == 0
11321163

11331164

1165+
def test_pdb_teardown_called(testdir, monkeypatch):
1166+
"""Ensure tearDown() is always called when --pdb is given in the command-line.
1167+
1168+
We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling
1169+
tearDown() eventually to avoid memory leaks when using --pdb.
1170+
"""
1171+
teardowns = []
1172+
monkeypatch.setattr(
1173+
pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False
1174+
)
1175+
1176+
testdir.makepyfile(
1177+
"""
1178+
import unittest
1179+
import pytest
1180+
1181+
class MyTestCase(unittest.TestCase):
1182+
1183+
def tearDown(self):
1184+
pytest.test_pdb_teardown_called_teardowns.append(self.id())
1185+
1186+
def test_1(self):
1187+
pass
1188+
def test_2(self):
1189+
pass
1190+
"""
1191+
)
1192+
result = testdir.runpytest_inprocess("--pdb")
1193+
result.stdout.fnmatch_lines("* 2 passed in *")
1194+
assert teardowns == [
1195+
"test_pdb_teardown_called.MyTestCase.test_1",
1196+
"test_pdb_teardown_called.MyTestCase.test_2",
1197+
]
1198+
1199+
11341200
def test_async_support(testdir):
11351201
pytest.importorskip("unittest.async_case")
11361202

0 commit comments

Comments
 (0)