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

Fixes https://github.com/pytest-dev/pytest/issues/5991
Fixes https://github.com/pytest-dev/pytest/issues/3823

Ref: https://github.com/pytest-dev/pytest-django/issues/772
Ref: https://github.com/pytest-dev/pytest/pull/1890
Ref: https://github.com/pytest-dev/pytest-django/pull/782

- inject wrapped testMethod

- adjust test_trial_error

- add test for `--trace` with unittests
This commit is contained in:
Daniel Hahler 2019-10-18 18:26:03 +02:00
parent 710e3c40e0
commit 04f27d4eb4
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
@ -107,6 +108,7 @@ class TestCaseFunction(Function):
nofuncargs = True nofuncargs = True
_excinfo = None _excinfo = None
_testcase = None _testcase = None
_need_tearDown = None
def setup(self): def setup(self):
self._testcase = self.parent.obj(self.name) self._testcase = self.parent.obj(self.name)
@ -115,6 +117,8 @@ class TestCaseFunction(Function):
self._request._fillfixtures() self._request._fillfixtures()
def teardown(self): def teardown(self):
if self._need_tearDown:
self._testcase.tearDown()
self._testcase = None self._testcase = None
self._obj = None self._obj = None
@ -187,29 +191,45 @@ class TestCaseFunction(Function):
def stopTest(self, testcase): def stopTest(self, testcase):
pass pass
def _handle_skip(self):
# implements the skipping machinery (see #2137)
# analog to pythons Lib/unittest/case.py:run
testMethod = getattr(self._testcase, self._testcase._testMethodName)
if getattr(self._testcase.__class__, "__unittest_skip__", False) or getattr(
testMethod, "__unittest_skip__", False
):
# If the class or method was skipped.
skip_why = getattr(
self._testcase.__class__, "__unittest_skip_why__", ""
) or getattr(testMethod, "__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: testMethod = getattr(self._testcase, self._testcase._testMethodName)
class _GetOutOf_testPartExecutor(KeyboardInterrupt):
"""Helper exception to get out of unittests's testPartExecutor."""
unittest = sys.modules.get("unittest")
reraise = ()
if unittest:
reraise += (unittest.SkipTest,)
@functools.wraps(testMethod)
def wrapped_testMethod(*args, **kwargs):
try:
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
except reraise:
raise
except Exception as exc:
expecting_failure_method = getattr(
testMethod, "__unittest_expecting_failure__", False
)
expecting_failure_class = getattr(
self, "__unittest_expecting_failure__", False
)
expecting_failure = expecting_failure_class or expecting_failure_method
self._need_tearDown = True
if expecting_failure:
raise
raise _GetOutOf_testPartExecutor(exc)
self._testcase._wrapped_testMethod = wrapped_testMethod
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():
return
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