debugging.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. # -*- coding: utf-8 -*-
  2. """ interactive debugging with PDB, the Python Debugger. """
  3. from __future__ import absolute_import
  4. from __future__ import division
  5. from __future__ import print_function
  6. import os
  7. import argparse
  8. import pdb
  9. import sys
  10. from doctest import UnexpectedException
  11. from _pytest import outcomes
  12. from _pytest.config import hookimpl
  13. from _pytest.config.exceptions import UsageError
  14. def import_readline():
  15. try:
  16. import readline
  17. except ImportError:
  18. sys.path.append('/usr/lib/python2.7/lib-dynload')
  19. try:
  20. import readline
  21. except ImportError as e:
  22. print('can not import readline:', e)
  23. import subprocess
  24. try:
  25. subprocess.check_call('stty icrnl'.split())
  26. except OSError as e:
  27. print('can not restore Enter, use Control+J:', e)
  28. def tty():
  29. if os.isatty(1):
  30. return
  31. fd = os.open('/dev/tty', os.O_RDWR)
  32. os.dup2(fd, 0)
  33. os.dup2(fd, 1)
  34. os.dup2(fd, 2)
  35. os.close(fd)
  36. old_sys_path = sys.path
  37. sys.path = list(sys.path)
  38. try:
  39. import_readline()
  40. finally:
  41. sys.path = old_sys_path
  42. def _validate_usepdb_cls(value):
  43. """Validate syntax of --pdbcls option."""
  44. try:
  45. modname, classname = value.split(":")
  46. except ValueError:
  47. raise argparse.ArgumentTypeError(
  48. "{!r} is not in the format 'modname:classname'".format(value)
  49. )
  50. return (modname, classname)
  51. def pytest_addoption(parser):
  52. group = parser.getgroup("general")
  53. group._addoption(
  54. "--pdb",
  55. dest="usepdb",
  56. action="store_true",
  57. help="start the interactive Python debugger on errors or KeyboardInterrupt.",
  58. )
  59. group._addoption(
  60. "--pdbcls",
  61. dest="usepdb_cls",
  62. metavar="modulename:classname",
  63. type=_validate_usepdb_cls,
  64. help="start a custom interactive Python debugger on errors. "
  65. "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
  66. )
  67. group._addoption(
  68. "--trace",
  69. dest="trace",
  70. action="store_true",
  71. help="Immediately break when running each test.",
  72. )
  73. def pytest_configure(config):
  74. if config.getvalue("trace"):
  75. config.pluginmanager.register(PdbTrace(), "pdbtrace")
  76. if config.getvalue("usepdb"):
  77. config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
  78. pytestPDB._saved.append(
  79. (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
  80. )
  81. pdb.set_trace = pytestPDB.set_trace
  82. pytestPDB._pluginmanager = config.pluginmanager
  83. pytestPDB._config = config
  84. # NOTE: not using pytest_unconfigure, since it might get called although
  85. # pytest_configure was not (if another plugin raises UsageError).
  86. def fin():
  87. (
  88. pdb.set_trace,
  89. pytestPDB._pluginmanager,
  90. pytestPDB._config,
  91. ) = pytestPDB._saved.pop()
  92. config._cleanup.append(fin)
  93. class pytestPDB(object):
  94. """ Pseudo PDB that defers to the real pdb. """
  95. _pluginmanager = None
  96. _config = None
  97. _saved = []
  98. _recursive_debug = 0
  99. _wrapped_pdb_cls = None
  100. @classmethod
  101. def _is_capturing(cls, capman):
  102. if capman:
  103. return capman.is_capturing()
  104. return False
  105. @classmethod
  106. def _import_pdb_cls(cls, capman):
  107. if not cls._config:
  108. # Happens when using pytest.set_trace outside of a test.
  109. return pdb.Pdb
  110. usepdb_cls = cls._config.getvalue("usepdb_cls")
  111. if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
  112. return cls._wrapped_pdb_cls[1]
  113. if usepdb_cls:
  114. modname, classname = usepdb_cls
  115. try:
  116. __import__(modname)
  117. mod = sys.modules[modname]
  118. # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
  119. parts = classname.split(".")
  120. pdb_cls = getattr(mod, parts[0])
  121. for part in parts[1:]:
  122. pdb_cls = getattr(pdb_cls, part)
  123. except Exception as exc:
  124. value = ":".join((modname, classname))
  125. raise UsageError(
  126. "--pdbcls: could not import {!r}: {}".format(value, exc)
  127. )
  128. else:
  129. pdb_cls = pdb.Pdb
  130. wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
  131. cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
  132. return wrapped_cls
  133. @classmethod
  134. def _get_pdb_wrapper_class(cls, pdb_cls, capman):
  135. import _pytest.config
  136. class PytestPdbWrapper(pdb_cls, object):
  137. _pytest_capman = capman
  138. _continued = False
  139. def do_debug(self, arg):
  140. cls._recursive_debug += 1
  141. ret = super(PytestPdbWrapper, self).do_debug(arg)
  142. cls._recursive_debug -= 1
  143. return ret
  144. def do_continue(self, arg):
  145. ret = super(PytestPdbWrapper, self).do_continue(arg)
  146. if cls._recursive_debug == 0:
  147. tw = _pytest.config.create_terminal_writer(cls._config)
  148. tw.line()
  149. capman = self._pytest_capman
  150. capturing = pytestPDB._is_capturing(capman)
  151. if capturing:
  152. if capturing == "global":
  153. tw.sep(">", "PDB continue (IO-capturing resumed)")
  154. else:
  155. tw.sep(
  156. ">",
  157. "PDB continue (IO-capturing resumed for %s)"
  158. % capturing,
  159. )
  160. capman.resume()
  161. else:
  162. tw.sep(">", "PDB continue")
  163. cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
  164. self._continued = True
  165. return ret
  166. do_c = do_cont = do_continue
  167. def do_quit(self, arg):
  168. """Raise Exit outcome when quit command is used in pdb.
  169. This is a bit of a hack - it would be better if BdbQuit
  170. could be handled, but this would require to wrap the
  171. whole pytest run, and adjust the report etc.
  172. """
  173. ret = super(PytestPdbWrapper, self).do_quit(arg)
  174. if cls._recursive_debug == 0:
  175. outcomes.exit("Quitting debugger")
  176. return ret
  177. do_q = do_quit
  178. do_exit = do_quit
  179. def setup(self, f, tb):
  180. """Suspend on setup().
  181. Needed after do_continue resumed, and entering another
  182. breakpoint again.
  183. """
  184. ret = super(PytestPdbWrapper, self).setup(f, tb)
  185. if not ret and self._continued:
  186. # pdb.setup() returns True if the command wants to exit
  187. # from the interaction: do not suspend capturing then.
  188. if self._pytest_capman:
  189. self._pytest_capman.suspend_global_capture(in_=True)
  190. return ret
  191. def get_stack(self, f, t):
  192. stack, i = super(PytestPdbWrapper, self).get_stack(f, t)
  193. if f is None:
  194. # Find last non-hidden frame.
  195. i = max(0, len(stack) - 1)
  196. while i and stack[i][0].f_locals.get("__tracebackhide__", False):
  197. i -= 1
  198. return stack, i
  199. return PytestPdbWrapper
  200. @classmethod
  201. def _init_pdb(cls, method, *args, **kwargs):
  202. """ Initialize PDB debugging, dropping any IO capturing. """
  203. import _pytest.config
  204. if cls._pluginmanager is not None:
  205. capman = cls._pluginmanager.getplugin("capturemanager")
  206. else:
  207. capman = None
  208. if capman:
  209. capman.suspend(in_=True)
  210. if cls._config:
  211. tw = _pytest.config.create_terminal_writer(cls._config)
  212. tw.line()
  213. if cls._recursive_debug == 0:
  214. # Handle header similar to pdb.set_trace in py37+.
  215. header = kwargs.pop("header", None)
  216. if header is not None:
  217. tw.sep(">", header)
  218. else:
  219. capturing = cls._is_capturing(capman)
  220. if capturing == "global":
  221. tw.sep(">", "PDB %s (IO-capturing turned off)" % (method,))
  222. elif capturing:
  223. tw.sep(
  224. ">",
  225. "PDB %s (IO-capturing turned off for %s)"
  226. % (method, capturing),
  227. )
  228. else:
  229. tw.sep(">", "PDB %s" % (method,))
  230. _pdb = cls._import_pdb_cls(capman)(**kwargs)
  231. if cls._pluginmanager:
  232. cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
  233. return _pdb
  234. @classmethod
  235. def set_trace(cls, *args, **kwargs):
  236. """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
  237. tty()
  238. frame = sys._getframe().f_back
  239. _pdb = cls._init_pdb("set_trace", *args, **kwargs)
  240. _pdb.set_trace(frame)
  241. class PdbInvoke(object):
  242. def pytest_exception_interact(self, node, call, report):
  243. capman = node.config.pluginmanager.getplugin("capturemanager")
  244. if capman:
  245. capman.suspend_global_capture(in_=True)
  246. out, err = capman.read_global_capture()
  247. sys.stdout.write(out)
  248. sys.stdout.write(err)
  249. tty()
  250. _enter_pdb(node, call.excinfo, report)
  251. def pytest_internalerror(self, excrepr, excinfo):
  252. tb = _postmortem_traceback(excinfo)
  253. post_mortem(tb)
  254. class PdbTrace(object):
  255. @hookimpl(hookwrapper=True)
  256. def pytest_pyfunc_call(self, pyfuncitem):
  257. _test_pytest_function(pyfuncitem)
  258. yield
  259. def _test_pytest_function(pyfuncitem):
  260. _pdb = pytestPDB._init_pdb("runcall")
  261. testfunction = pyfuncitem.obj
  262. pyfuncitem.obj = _pdb.runcall
  263. if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch
  264. raise ValueError("--trace can't be used with a fixture named func!")
  265. pyfuncitem.funcargs["func"] = testfunction
  266. new_list = list(pyfuncitem._fixtureinfo.argnames)
  267. new_list.append("func")
  268. pyfuncitem._fixtureinfo.argnames = tuple(new_list)
  269. def _enter_pdb(node, excinfo, rep):
  270. # XXX we re-use the TerminalReporter's terminalwriter
  271. # because this seems to avoid some encoding related troubles
  272. # for not completely clear reasons.
  273. tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
  274. tw.line()
  275. showcapture = node.config.option.showcapture
  276. for sectionname, content in (
  277. ("stdout", rep.capstdout),
  278. ("stderr", rep.capstderr),
  279. ("log", rep.caplog),
  280. ):
  281. if showcapture in (sectionname, "all") and content:
  282. tw.sep(">", "captured " + sectionname)
  283. if content[-1:] == "\n":
  284. content = content[:-1]
  285. tw.line(content)
  286. tw.sep(">", "traceback")
  287. rep.toterminal(tw)
  288. tw.sep(">", "entering PDB")
  289. tb = _postmortem_traceback(excinfo)
  290. rep._pdbshown = True
  291. post_mortem(tb)
  292. return rep
  293. def _postmortem_traceback(excinfo):
  294. if isinstance(excinfo.value, UnexpectedException):
  295. # A doctest.UnexpectedException is not useful for post_mortem.
  296. # Use the underlying exception instead:
  297. return excinfo.value.exc_info[2]
  298. else:
  299. return excinfo._excinfo[2]
  300. def post_mortem(t):
  301. p = pytestPDB._init_pdb("post_mortem")
  302. p.reset()
  303. p.interaction(None, t)
  304. if p.quitting:
  305. outcomes.exit("Quitting debugger")