Skip to content

Commit

Permalink
unittest: do not use TestCase.debug() with --pdb
Browse files Browse the repository at this point in the history
Fixes pytest-dev#5991
Fixes pytest-dev#3823

Ref: pytest-dev/pytest-django#772
Ref: pytest-dev#1890
Ref: pytest-dev/pytest-django#782

- inject wrapped testMethod

- adjust test_trial_error

- add test for `--trace` with unittests
  • Loading branch information
blueyed committed Nov 9, 2019
1 parent 245e1f1 commit 4b922e5
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 38 deletions.
1 change: 1 addition & 0 deletions changelog/3823.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``--trace`` now works with unittests.
1 change: 1 addition & 0 deletions changelog/5991.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix interaction with ``--pdb`` and unittests: do not use unittest's ``TestCase.debug()``.
11 changes: 0 additions & 11 deletions doc/en/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 40 additions & 20 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" discovery and running of std-library "unittest" style tests. """
import functools
import sys
import traceback

Expand Down Expand Up @@ -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)
Expand All @@ -115,6 +117,8 @@ def setup(self):
self._request._fillfixtures()

def teardown(self):
if self._need_tearDown:
self._testcase.tearDown()
self._testcase = None
self._obj = None

Expand Down Expand Up @@ -187,29 +191,45 @@ def addSuccess(self, testcase):
def stopTest(self, testcase):
pass

def _handle_skip(self):
# implements the skipping machinery (see #2137)
# analog to pythons Lib/unittest/case.py:run
def runtest(self):
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:
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)
Expand Down
47 changes: 40 additions & 7 deletions testing/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,24 +537,28 @@ def f(_):
)
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 *",
]
)

Expand Down Expand Up @@ -1096,3 +1100,32 @@ def test_should_not_run(self):
)
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

0 comments on commit 4b922e5

Please sign in to comment.