123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- """ interactive debugging with PDB, the Python Debugger. """
- from __future__ import absolute_import, division, print_function
- import pdb
- import sys
- import os
- from doctest import UnexpectedException
- from _pytest.config import hookimpl
- try:
- from builtins import breakpoint # noqa
- SUPPORTS_BREAKPOINT_BUILTIN = True
- except ImportError:
- SUPPORTS_BREAKPOINT_BUILTIN = False
- def pytest_addoption(parser):
- group = parser.getgroup("general")
- group._addoption(
- "--pdb",
- dest="usepdb",
- action="store_true",
- help="start the interactive Python debugger on errors or KeyboardInterrupt.",
- )
- group._addoption(
- "--pdbcls",
- dest="usepdb_cls",
- metavar="modulename:classname",
- 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):
- if config.getvalue("usepdb_cls"):
- modname, classname = config.getvalue("usepdb_cls").split(":")
- __import__(modname)
- pdb_cls = getattr(sys.modules[modname], classname)
- else:
- pdb_cls = pdb.Pdb
- if config.getvalue("trace"):
- config.pluginmanager.register(PdbTrace(), "pdbtrace")
- if config.getvalue("usepdb"):
- config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
- # Use custom Pdb class set_trace instead of default Pdb on breakpoint() call
- if SUPPORTS_BREAKPOINT_BUILTIN:
- _environ_pythonbreakpoint = os.environ.get("PYTHONBREAKPOINT", "")
- if _environ_pythonbreakpoint == "":
- sys.breakpointhook = pytestPDB.set_trace
- old = (pdb.set_trace, pytestPDB._pluginmanager)
- def fin():
- pdb.set_trace, pytestPDB._pluginmanager = old
- pytestPDB._config = None
- pytestPDB._pdb_cls = pdb.Pdb
- if SUPPORTS_BREAKPOINT_BUILTIN:
- sys.breakpointhook = sys.__breakpointhook__
- pdb.set_trace = pytestPDB.set_trace
- pytestPDB._pluginmanager = config.pluginmanager
- pytestPDB._config = config
- pytestPDB._pdb_cls = pdb_cls
- config._cleanup.append(fin)
- class pytestPDB(object):
- """ Pseudo PDB that defers to the real pdb. """
- _pluginmanager = None
- _config = None
- _pdb_cls = pdb.Pdb
- @classmethod
- def set_trace(cls, set_break=True):
- """ invoke PDB set_trace debugging, dropping any IO capturing. """
- import _pytest.config
- frame = sys._getframe().f_back
- if cls._pluginmanager is not None:
- capman = cls._pluginmanager.getplugin("capturemanager")
- if capman:
- capman.suspend_global_capture(in_=True)
- tw = _pytest.config.create_terminal_writer(cls._config)
- tw.line()
- tw.sep(">", "PDB set_trace (IO-capturing turned off)")
- cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
- if set_break:
- cls._pdb_cls().set_trace(frame)
- class PdbInvoke(object):
- def pytest_exception_interact(self, node, call, report):
- capman = node.config.pluginmanager.getplugin("capturemanager")
- if capman:
- capman.suspend_global_capture(in_=True)
- out, err = capman.read_global_capture()
- sys.stdout.write(out)
- sys.stdout.write(err)
- _enter_pdb(node, call.excinfo, report)
- def pytest_internalerror(self, excrepr, excinfo):
- for line in str(excrepr).split("\n"):
- sys.stderr.write("INTERNALERROR> %s\n" % line)
- sys.stderr.flush()
- tb = _postmortem_traceback(excinfo)
- 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
- # for not completely clear reasons.
- tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
- tw.line()
- showcapture = node.config.option.showcapture
- for sectionname, content in (
- ("stdout", rep.capstdout),
- ("stderr", rep.capstderr),
- ("log", rep.caplog),
- ):
- if showcapture in (sectionname, "all") and content:
- tw.sep(">", "captured " + sectionname)
- if content[-1:] == "\n":
- content = content[:-1]
- tw.line(content)
- tw.sep(">", "traceback")
- rep.toterminal(tw)
- tw.sep(">", "entering PDB")
- tb = _postmortem_traceback(excinfo)
- post_mortem(tb)
- rep._pdbshown = True
- return rep
- def _postmortem_traceback(excinfo):
- if isinstance(excinfo.value, UnexpectedException):
- # A doctest.UnexpectedException is not useful for post_mortem.
- # Use the underlying exception instead:
- return excinfo.value.exc_info[2]
- else:
- return excinfo._excinfo[2]
- def _find_last_non_hidden_frame(stack):
- i = max(0, len(stack) - 1)
- while i and stack[i][0].f_locals.get("__tracebackhide__", False):
- i -= 1
- return i
- def post_mortem(t):
- class Pdb(pytestPDB._pdb_cls):
- def get_stack(self, f, t):
- stack, i = pdb.Pdb.get_stack(self, f, t)
- if f is None:
- i = _find_last_non_hidden_frame(stack)
- return stack, i
- p = Pdb()
- p.reset()
- p.interaction(None, t)
|