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:
.. 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::
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. """
import functools
import sys
import traceback
@ -107,6 +108,7 @@ class TestCaseFunction(Function):
nofuncargs = True
_excinfo = None
_testcase = None
_need_tearDown = None
def setup(self):
self._testcase = self.parent.obj(self.name)
@ -115,6 +117,8 @@ class TestCaseFunction(Function):
self._request._fillfixtures()
def teardown(self):
if self._need_tearDown:
self._testcase.tearDown()
self._testcase = None
self._obj = None
@ -187,29 +191,45 @@ class TestCaseFunction(Function):
def stopTest(self, testcase):
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):
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)
else:
# disables tearDown and cleanups for post mortem debugging (see #1890)
if self._handle_skip():
return
self._testcase.debug()
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo)

View File

@ -537,24 +537,28 @@ class TestTrialUnittest:
)
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_one 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*",
"*_ 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*",
"*_ TC.test_two _*",
"*NameError*crash*",
"*= 4 failed, 1 error in *",
"*= 2 failed, 2 skipped, 2 errors in *",
]
)
@ -1096,3 +1100,32 @@ def test_exit_outcome(testdir):
)
result = testdir.runpytest()
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