Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cleanup functions not being invoked on test failures #7151

Merged
merged 2 commits into from May 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please review this bit @RonnyPfannschmidt? I noticed this was needed after one of the tests failed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm actually YOLO on that one, my insight to the implementation of unittest is limited

# 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)
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reverts the test to the state before 04f27d4, which introduced the breaking change about addCleanup not being called properly for failed tests.

"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