Merge pull request #7151 from nicoddemus/unittest-cleanup-6947

This commit is contained in:
Bruno Oliveira 2020-05-02 15:43:54 -03:00 committed by GitHub
commit 3312820051
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 108 additions and 42 deletions

View File

@ -0,0 +1 @@
Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures.

View File

@ -272,11 +272,15 @@ class PdbInvoke:
class PdbTrace: class PdbTrace:
@hookimpl(hookwrapper=True) @hookimpl(hookwrapper=True)
def pytest_pyfunc_call(self, pyfuncitem): def pytest_pyfunc_call(self, pyfuncitem):
_test_pytest_function(pyfuncitem) wrap_pytest_function_for_tracing(pyfuncitem)
yield yield
def _test_pytest_function(pyfuncitem): def wrap_pytest_function_for_tracing(pyfuncitem):
"""Changes the python function object of the given Function item by a wrapper which actually
enters pdb before calling the python function itself, effectively leaving the user
in the pdb prompt in the first statement of the function.
"""
_pdb = pytestPDB._init_pdb("runcall") _pdb = pytestPDB._init_pdb("runcall")
testfunction = pyfuncitem.obj testfunction = pyfuncitem.obj
@ -291,6 +295,13 @@ def _test_pytest_function(pyfuncitem):
pyfuncitem.obj = wrapper pyfuncitem.obj = wrapper
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
"""Wrap the given pytestfunct item for tracing support if --trace was given in
the command line"""
if pyfuncitem.config.getvalue("trace"):
wrap_pytest_function_for_tracing(pyfuncitem)
def _enter_pdb(node, excinfo, rep): def _enter_pdb(node, excinfo, rep):
# XXX we re-use the TerminalReporter's terminalwriter # XXX we re-use the TerminalReporter's terminalwriter
# because this seems to avoid some encoding related troubles # because this seems to avoid some encoding related troubles

View File

@ -1,5 +1,4 @@
""" discovery and running of std-library "unittest" style tests. """ """ discovery and running of std-library "unittest" style tests. """
import functools
import sys import sys
import traceback import traceback
@ -114,15 +113,17 @@ class TestCaseFunction(Function):
_testcase = None _testcase = None
def setup(self): def setup(self):
self._needs_explicit_tearDown = False # a bound method to be called during teardown() if set (see 'runtest()')
self._explicit_tearDown = None
self._testcase = self.parent.obj(self.name) self._testcase = self.parent.obj(self.name)
self._obj = getattr(self._testcase, self.name) self._obj = getattr(self._testcase, self.name)
if hasattr(self, "_request"): if hasattr(self, "_request"):
self._request._fillfixtures() self._request._fillfixtures()
def teardown(self): def teardown(self):
if self._needs_explicit_tearDown: if self._explicit_tearDown is not None:
self._testcase.tearDown() self._explicit_tearDown()
self._explicit_tearDown = None
self._testcase = None self._testcase = None
self._obj = None self._obj = None
@ -205,40 +206,31 @@ class TestCaseFunction(Function):
return bool(expecting_failure_class or expecting_failure_method) return bool(expecting_failure_class or expecting_failure_method)
def runtest(self): def runtest(self):
# TODO: move testcase reporter into separate class, this shouldnt be on item from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
import unittest
testMethod = getattr(self._testcase, self._testcase._testMethodName) maybe_wrap_pytest_function_for_tracing(self)
class _GetOutOf_testPartExecutor(KeyboardInterrupt):
"""Helper exception to get out of unittests's testPartExecutor (see TestCase.run)."""
@functools.wraps(testMethod)
def wrapped_testMethod(*args, **kwargs):
"""Wrap the original method to call into pytest's machinery, so other pytest
features can have a chance to kick in (notably --pdb)"""
try:
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
except unittest.SkipTest:
raise
except Exception as exc:
expecting_failure = self._expecting_failure(testMethod)
if expecting_failure:
raise
self._needs_explicit_tearDown = True
raise _GetOutOf_testPartExecutor(exc)
# let the unittest framework handle async functions # let the unittest framework handle async functions
if is_async_function(self.obj): if is_async_function(self.obj):
self._testcase(self) self._testcase(self)
else: else:
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod) # when --pdb is given, we want to postpone calling tearDown() otherwise
# when entering the pdb prompt, tearDown() would have probably cleaned up
# instance variables, which makes it difficult to debug
# arguably we could always postpone tearDown(), but this changes the moment where the
# TestCase instance interacts with the results object, so better to only do it
# when absolutely needed
if self.config.getoption("usepdb"):
self._explicit_tearDown = self._testcase.tearDown
setattr(self._testcase, "tearDown", lambda *args: None)
# we need to update the actual bound method with self.obj, because
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper
setattr(self._testcase, self.name, self.obj)
try: try:
self._testcase(result=self) self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally: finally:
delattr(self._testcase, self._testcase._testMethodName) delattr(self._testcase, self.name)
def _prunetraceback(self, excinfo): def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo) Function._prunetraceback(self, excinfo)

View File

@ -537,28 +537,24 @@ class TestTrialUnittest:
) )
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"test_trial_error.py::TC::test_four SKIPPED", "test_trial_error.py::TC::test_four FAILED",
"test_trial_error.py::TC::test_four ERROR", "test_trial_error.py::TC::test_four ERROR",
"test_trial_error.py::TC::test_one FAILED", "test_trial_error.py::TC::test_one FAILED",
"test_trial_error.py::TC::test_three FAILED", "test_trial_error.py::TC::test_three FAILED",
"test_trial_error.py::TC::test_two SKIPPED", "test_trial_error.py::TC::test_two FAILED",
"test_trial_error.py::TC::test_two ERROR",
"*ERRORS*", "*ERRORS*",
"*_ ERROR at teardown of TC.test_four _*", "*_ ERROR at teardown of TC.test_four _*",
"NOTE: Incompatible Exception Representation, displaying natively:",
"*DelayedCalls*",
"*_ ERROR at teardown of TC.test_two _*",
"NOTE: Incompatible Exception Representation, displaying natively:",
"*DelayedCalls*", "*DelayedCalls*",
"*= FAILURES =*", "*= FAILURES =*",
# "*_ TC.test_four _*", "*_ TC.test_four _*",
# "*NameError*crash*", "*NameError*crash*",
"*_ TC.test_one _*", "*_ TC.test_one _*",
"*NameError*crash*", "*NameError*crash*",
"*_ TC.test_three _*", "*_ TC.test_three _*",
"NOTE: Incompatible Exception Representation, displaying natively:",
"*DelayedCalls*", "*DelayedCalls*",
"*= 2 failed, 2 skipped, 2 errors in *", "*_ TC.test_two _*",
"*NameError*crash*",
"*= 4 failed, 1 error in *",
] ]
) )
@ -876,6 +872,37 @@ def test_no_teardown_if_setupclass_failed(testdir):
reprec.assertoutcome(passed=1, failed=1) reprec.assertoutcome(passed=1, failed=1)
def test_cleanup_functions(testdir):
"""Ensure functions added with addCleanup are always called after each test ends (#6947)"""
testdir.makepyfile(
"""
import unittest
cleanups = []
class Test(unittest.TestCase):
def test_func_1(self):
self.addCleanup(cleanups.append, "test_func_1")
def test_func_2(self):
self.addCleanup(cleanups.append, "test_func_2")
assert 0
def test_func_3_check_cleanups(self):
assert cleanups == ["test_func_1", "test_func_2"]
"""
)
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines(
[
"*::test_func_1 PASSED *",
"*::test_func_2 FAILED *",
"*::test_func_3_check_cleanups PASSED *",
]
)
def test_issue333_result_clearing(testdir): def test_issue333_result_clearing(testdir):
testdir.makeconftest( testdir.makeconftest(
""" """
@ -1131,6 +1158,41 @@ def test_trace(testdir, monkeypatch):
assert result.ret == 0 assert result.ret == 0
def test_pdb_teardown_called(testdir, monkeypatch):
"""Ensure tearDown() is always called when --pdb is given in the command-line.
We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling
tearDown() eventually to avoid memory leaks when using --pdb.
"""
teardowns = []
monkeypatch.setattr(
pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False
)
testdir.makepyfile(
"""
import unittest
import pytest
class MyTestCase(unittest.TestCase):
def tearDown(self):
pytest.test_pdb_teardown_called_teardowns.append(self.id())
def test_1(self):
pass
def test_2(self):
pass
"""
)
result = testdir.runpytest_inprocess("--pdb")
result.stdout.fnmatch_lines("* 2 passed in *")
assert teardowns == [
"test_pdb_teardown_called.MyTestCase.test_1",
"test_pdb_teardown_called.MyTestCase.test_2",
]
def test_async_support(testdir): def test_async_support(testdir):
pytest.importorskip("unittest.async_case") pytest.importorskip("unittest.async_case")