Skip to content

Commit

Permalink
Merge pull request #7151 from nicoddemus/unittest-cleanup-6947
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed May 2, 2020
2 parents 2b51ed4 + 82f584b commit 3312820
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 42 deletions.
1 change: 1 addition & 0 deletions changelog/6947.bugfix.rst
@@ -0,0 +1 @@
Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures.
15 changes: 13 additions & 2 deletions src/_pytest/debugging.py
Expand Up @@ -272,11 +272,15 @@ def pytest_internalerror(self, excrepr, excinfo):
class PdbTrace:
@hookimpl(hookwrapper=True)
def pytest_pyfunc_call(self, pyfuncitem):
_test_pytest_function(pyfuncitem)
wrap_pytest_function_for_tracing(pyfuncitem)
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")
testfunction = pyfuncitem.obj

Expand All @@ -291,6 +295,13 @@ def wrapper(*args, **kwargs):
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):
# XXX we re-use the TerminalReporter's terminalwriter
# because this seems to avoid some encoding related troubles
Expand Down
50 changes: 21 additions & 29 deletions src/_pytest/unittest.py
@@ -1,5 +1,4 @@
""" discovery and running of std-library "unittest" style tests. """
import functools
import sys
import traceback

Expand Down Expand Up @@ -114,15 +113,17 @@ class TestCaseFunction(Function):
_testcase = None

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._obj = getattr(self._testcase, self.name)
if hasattr(self, "_request"):
self._request._fillfixtures()

def teardown(self):
if self._needs_explicit_tearDown:
self._testcase.tearDown()
if self._explicit_tearDown is not None:
self._explicit_tearDown()
self._explicit_tearDown = None
self._testcase = None
self._obj = None

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

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

testMethod = getattr(self._testcase, self._testcase._testMethodName)

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

# let the unittest framework handle async functions
if is_async_function(self.obj):
self._testcase(self)
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:
self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally:
delattr(self._testcase, self._testcase._testMethodName)
delattr(self._testcase, self.name)

def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo)
Expand Down
84 changes: 73 additions & 11 deletions testing/test_unittest.py
Expand Up @@ -537,28 +537,24 @@ def f(_):
)
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_one FAILED",
"test_trial_error.py::TC::test_three FAILED",
"test_trial_error.py::TC::test_two SKIPPED",
"test_trial_error.py::TC::test_two ERROR",
"test_trial_error.py::TC::test_two FAILED",
"*ERRORS*",
"*_ 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*",
"*= FAILURES =*",
# "*_ TC.test_four _*",
# "*NameError*crash*",
"*_ TC.test_four _*",
"*NameError*crash*",
"*_ TC.test_one _*",
"*NameError*crash*",
"*_ TC.test_three _*",
"NOTE: Incompatible Exception Representation, displaying natively:",
"*DelayedCalls*",
"*= 2 failed, 2 skipped, 2 errors in *",
"*_ TC.test_two _*",
"*NameError*crash*",
"*= 4 failed, 1 error in *",
]
)

Expand Down Expand Up @@ -876,6 +872,37 @@ def test_notTornDown():
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):
testdir.makeconftest(
"""
Expand Down Expand Up @@ -1131,6 +1158,41 @@ def test(self):
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):
pytest.importorskip("unittest.async_case")

Expand Down

0 comments on commit 3312820

Please sign in to comment.