Skip to content

Commit 9b8425f

Browse files
committed
Fix cleanup functions not being invoked on test failures
Fix #6947
1 parent e364375 commit 9b8425f

File tree

4 files changed

+51
-28
lines changed

4 files changed

+51
-28
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: 6 additions & 26 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

@@ -205,40 +204,21 @@ def _expecting_failure(self, test_method) -> bool:
205204
return bool(expecting_failure_class or expecting_failure_method)
206205

207206
def runtest(self):
208-
# TODO: move testcase reporter into separate class, this shouldnt be on item
209-
import unittest
207+
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
210208

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)."""
215-
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)
209+
maybe_wrap_pytest_function_for_tracing(self)
230210

231211
# let the unittest framework handle async functions
232212
if is_async_function(self.obj):
233213
self._testcase(self)
234214
else:
235-
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
215+
# we need to update the actual bound method with self.obj, because
216+
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper
217+
setattr(self._testcase, self.name, self.obj)
236218
try:
237219
self._testcase(result=self)
238-
except _GetOutOf_testPartExecutor as exc:
239-
raise exc.args[0] from exc.args[0]
240220
finally:
241-
delattr(self._testcase, self._testcase._testMethodName)
221+
delattr(self._testcase, self.name)
242222

243223
def _prunetraceback(self, excinfo):
244224
Function._prunetraceback(self, excinfo)

testing/test_unittest.py

Lines changed: 31 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
"""

0 commit comments

Comments
 (0)