Skip to content

Commit 80936b6

Browse files
authored
Merge pull request #7156 from nicoddemus/backport-7151
2 parents ba2c49e + 5ca08e9 commit 80936b6

File tree

4 files changed

+108
-42
lines changed

4 files changed

+108
-42
lines changed

changelog/6947.bugfix.rst

+1
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

+13-2
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

+21-29
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,17 @@ class TestCaseFunction(Function):
114113
_testcase = None
115114

116115
def setup(self):
117-
self._needs_explicit_tearDown = False
116+
# a bound method to be called during teardown() if set (see 'runtest()')
117+
self._explicit_tearDown = None
118118
self._testcase = self.parent.obj(self.name)
119119
self._obj = getattr(self._testcase, self.name)
120120
if hasattr(self, "_request"):
121121
self._request._fillfixtures()
122122

123123
def teardown(self):
124-
if self._needs_explicit_tearDown:
125-
self._testcase.tearDown()
124+
if self._explicit_tearDown is not None:
125+
self._explicit_tearDown()
126+
self._explicit_tearDown = None
126127
self._testcase = None
127128
self._obj = None
128129

@@ -205,40 +206,31 @@ def _expecting_failure(self, test_method) -> bool:
205206
return bool(expecting_failure_class or expecting_failure_method)
206207

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

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)
211+
maybe_wrap_pytest_function_for_tracing(self)
230212

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

243235
def _prunetraceback(self, excinfo):
244236
Function._prunetraceback(self, excinfo)

testing/test_unittest.py

+73-11
Original file line numberDiff line numberDiff line change
@@ -537,28 +537,24 @@ def f(_):
537537
)
538538
result.stdout.fnmatch_lines(
539539
[
540-
"test_trial_error.py::TC::test_four SKIPPED",
540+
"test_trial_error.py::TC::test_four FAILED",
541541
"test_trial_error.py::TC::test_four ERROR",
542542
"test_trial_error.py::TC::test_one FAILED",
543543
"test_trial_error.py::TC::test_three FAILED",
544-
"test_trial_error.py::TC::test_two SKIPPED",
545-
"test_trial_error.py::TC::test_two ERROR",
544+
"test_trial_error.py::TC::test_two FAILED",
546545
"*ERRORS*",
547546
"*_ ERROR at teardown of TC.test_four _*",
548-
"NOTE: Incompatible Exception Representation, displaying natively:",
549-
"*DelayedCalls*",
550-
"*_ ERROR at teardown of TC.test_two _*",
551-
"NOTE: Incompatible Exception Representation, displaying natively:",
552547
"*DelayedCalls*",
553548
"*= FAILURES =*",
554-
# "*_ TC.test_four _*",
555-
# "*NameError*crash*",
549+
"*_ TC.test_four _*",
550+
"*NameError*crash*",
556551
"*_ TC.test_one _*",
557552
"*NameError*crash*",
558553
"*_ TC.test_three _*",
559-
"NOTE: Incompatible Exception Representation, displaying natively:",
560554
"*DelayedCalls*",
561-
"*= 2 failed, 2 skipped, 2 errors in *",
555+
"*_ TC.test_two _*",
556+
"*NameError*crash*",
557+
"*= 4 failed, 1 error in *",
562558
]
563559
)
564560

@@ -876,6 +872,37 @@ def test_notTornDown():
876872
reprec.assertoutcome(passed=1, failed=1)
877873

878874

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

11331160

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

0 commit comments

Comments
 (0)