diff --git a/changelog/3610.feature.rst b/changelog/3610.feature.rst new file mode 100644 index 000000000..fd044b4b2 --- /dev/null +++ b/changelog/3610.feature.rst @@ -0,0 +1 @@ +Added the `--trace` option to enter the debugger at the start of a test. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 25be54395..a5418df22 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -171,6 +171,18 @@ for example:: >>> sys.last_value AssertionError('assert result == "ok"',) +.. _trace-option: + +Dropping to PDB_ (Python Debugger) at the start of a test +---------------------------------------------------------- + + +``pytest`` allows one to drop into the PDB_ prompt immediately at the start of each test via a command line option:: + + pytest --trace + +This will invoke the Python debugger at the start of every test. + .. _breakpoints: Setting breakpoints diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3fc3eb552..9991307d0 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -5,6 +5,8 @@ import sys import os from doctest import UnexpectedException +from _pytest.config import hookimpl + try: from builtins import breakpoint # noqa @@ -28,6 +30,12 @@ def pytest_addoption(parser): help="start a custom interactive Python debugger on errors. " "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", ) + group._addoption( + "--trace", + dest="trace", + action="store_true", + help="Immediately break when running each test.", + ) def pytest_configure(config): @@ -38,6 +46,8 @@ def pytest_configure(config): else: pdb_cls = pdb.Pdb + if config.getvalue("trace"): + config.pluginmanager.register(PdbTrace(), "pdbtrace") if config.getvalue("usepdb"): config.pluginmanager.register(PdbInvoke(), "pdbinvoke") @@ -71,7 +81,7 @@ class pytestPDB(object): _pdb_cls = pdb.Pdb @classmethod - def set_trace(cls): + def set_trace(cls, set_break=True): """ invoke PDB set_trace debugging, dropping any IO capturing. """ import _pytest.config @@ -84,7 +94,8 @@ class pytestPDB(object): tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config) - cls._pdb_cls().set_trace(frame) + if set_break: + cls._pdb_cls().set_trace(frame) class PdbInvoke(object): @@ -104,6 +115,30 @@ class PdbInvoke(object): post_mortem(tb) +class PdbTrace(object): + @hookimpl(hookwrapper=True) + def pytest_pyfunc_call(self, pyfuncitem): + _test_pytest_function(pyfuncitem) + yield + + +def _test_pytest_function(pyfuncitem): + pytestPDB.set_trace(set_break=False) + testfunction = pyfuncitem.obj + pyfuncitem.obj = pdb.runcall + if pyfuncitem._isyieldedfunction(): + arg_list = list(pyfuncitem._args) + arg_list.insert(0, testfunction) + pyfuncitem._args = tuple(arg_list) + else: + if "func" in pyfuncitem._fixtureinfo.argnames: + raise ValueError("--trace can't be used with a fixture named func!") + pyfuncitem.funcargs["func"] = testfunction + new_list = list(pyfuncitem._fixtureinfo.argnames) + new_list.append("func") + pyfuncitem._fixtureinfo.argnames = tuple(new_list) + + def _enter_pdb(node, excinfo, rep): # XXX we re-use the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 08be812a2..43a78908c 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -696,3 +696,40 @@ class TestDebuggingBreakpoints(object): assert "1 failed" in rest assert "reading from stdin while output" not in rest TestPDB.flush(child) + + +class TestTraceOption: + def test_trace_sets_breakpoint(self, testdir): + p1 = testdir.makepyfile( + """ + def test_1(): + assert True + """ + ) + child = testdir.spawn_pytest("--trace " + str(p1)) + child.expect("test_1") + child.expect("(Pdb)") + child.sendeof() + rest = child.read().decode("utf8") + assert "1 passed" in rest + assert "reading from stdin while output" not in rest + TestPDB.flush(child) + + def test_trace_against_yield_test(self, testdir): + p1 = testdir.makepyfile( + """ + def is_equal(a, b): + assert a == b + + def test_1(): + yield is_equal, 1, 1 + """ + ) + child = testdir.spawn_pytest("--trace " + str(p1)) + child.expect("is_equal") + child.expect("(Pdb)") + child.sendeof() + rest = child.read().decode("utf8") + assert "1 passed" in rest + assert "reading from stdin while output" not in rest + TestPDB.flush(child)