unittest: do not use TestCase.debug() with `--pdb` (#5996)

unittest: do not use TestCase.debug() with `--pdb`
This commit is contained in:
Bruno Oliveira 2019-12-03 11:15:06 -03:00 committed by GitHub
commit 1c4a672a52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 39 deletions

View File

@ -0,0 +1 @@
``--trace`` now works with unittests.

View File

@ -0,0 +1 @@
Fix interaction with ``--pdb`` and unittests: do not use unittest's ``TestCase.debug()``.

View File

@ -238,17 +238,6 @@ was executed ahead of the ``test_method``.
.. _pdb-unittest-note: .. _pdb-unittest-note:
.. note::
Running tests from ``unittest.TestCase`` subclasses with ``--pdb`` will
disable tearDown and cleanup methods for the case that an Exception
occurs. This allows proper post mortem debugging for all applications
which have significant logic in their tearDown machinery. However,
supporting this feature has the following side effect: If people
overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to
to overwrite ``debug`` in the same way (this is also true for standard
unittest).
.. note:: .. note::
Due to architectural differences between the two frameworks, setup and Due to architectural differences between the two frameworks, setup and

View File

@ -1,4 +1,5 @@
""" 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
@ -110,12 +111,15 @@ class TestCaseFunction(Function):
_testcase = None _testcase = None
def setup(self): def setup(self):
self._needs_explicit_tearDown = False
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:
self._testcase.tearDown()
self._testcase = None self._testcase = None
self._obj = None self._obj = None
@ -188,30 +192,46 @@ class TestCaseFunction(Function):
def stopTest(self, testcase): def stopTest(self, testcase):
pass pass
def _handle_skip(self): def _expecting_failure(self, test_method) -> bool:
# implements the skipping machinery (see #2137) """Return True if the given unittest method (or the entire class) is marked
# analog to pythons Lib/unittest/case.py:run with @expectedFailure"""
test_method = getattr(self._testcase, self._testcase._testMethodName) expecting_failure_method = getattr(
if getattr(self._testcase.__class__, "__unittest_skip__", False) or getattr( test_method, "__unittest_expecting_failure__", False
test_method, "__unittest_skip__", False )
): expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False)
# If the class or method was skipped. return bool(expecting_failure_class or expecting_failure_method)
skip_why = getattr(
self._testcase.__class__, "__unittest_skip_why__", ""
) or getattr(test_method, "__unittest_skip_why__", "")
self._testcase._addSkip(self, self._testcase, skip_why)
return True
return False
def runtest(self): def runtest(self):
if self.config.pluginmanager.get_plugin("pdbinvoke") is None:
# TODO: move testcase reporter into separate class, this shouldnt be on item # TODO: move testcase reporter into separate class, this shouldnt be on item
import unittest
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)
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
try:
self._testcase(result=self) self._testcase(result=self)
else: except _GetOutOf_testPartExecutor as exc:
# disables tearDown and cleanups for post mortem debugging (see #1890) raise exc.args[0] from exc.args[0]
if self._handle_skip(): finally:
return delattr(self._testcase, self._testcase._testMethodName)
self._testcase.debug()
def _prunetraceback(self, excinfo): def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo) Function._prunetraceback(self, excinfo)

View File

@ -537,24 +537,28 @@ class TestTrialUnittest:
) )
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"test_trial_error.py::TC::test_four FAILED", "test_trial_error.py::TC::test_four SKIPPED",
"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 FAILED", "test_trial_error.py::TC::test_two SKIPPED",
"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*",
"*_ TC.test_two _*", "*= 2 failed, 2 skipped, 2 errors in *",
"*NameError*crash*",
"*= 4 failed, 1 error in *",
] ]
) )
@ -1096,3 +1100,32 @@ def test_exit_outcome(testdir):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines(["*Exit: pytest_exit called*", "*= no tests ran in *"]) result.stdout.fnmatch_lines(["*Exit: pytest_exit called*", "*= no tests ran in *"])
def test_trace(testdir, monkeypatch):
calls = []
def check_call(*args, **kwargs):
calls.append((args, kwargs))
assert args == ("runcall",)
class _pdb:
def runcall(*args, **kwargs):
calls.append((args, kwargs))
return _pdb
monkeypatch.setattr("_pytest.debugging.pytestPDB._init_pdb", check_call)
p1 = testdir.makepyfile(
"""
import unittest
class MyTestCase(unittest.TestCase):
def test(self):
self.assertEqual('foo', 'foo')
"""
)
result = testdir.runpytest("--trace", str(p1))
assert len(calls) == 2
assert result.ret == 0