- fix issue181: --pdb now also works on collect errors. This was

implemented by a slight internal refactoring and the introduction
  of a new hook ``pytest_exception_interact`` hook.

- fix issue341: introduce new experimental hook for IDEs/terminals to
  intercept debugging: ``pytest_exception_interact(node, call, report)``.
This commit is contained in:
holger krekel 2013-09-06 11:56:04 +02:00
parent 8360c1e687
commit 94ee37cdb3
12 changed files with 107 additions and 34 deletions

View File

@ -1,6 +1,14 @@
Changes between 2.3.5 and 2.4.DEV Changes between 2.3.5 and 2.4.DEV
----------------------------------- -----------------------------------
- fix issue181: --pdb now also works on collect errors (and
on internal errors) . This was implemented by a slight internal
refactoring and the introduction of a new hook
``pytest_exception_interact`` hook (see below).
- fix issue341: introduce new experimental hook for IDEs/terminals to
intercept debugging: ``pytest_exception_interact(node, call, report)``.
- PR27: correctly handle nose.SkipTest during collection. Thanks - PR27: correctly handle nose.SkipTest during collection. Thanks
Antonio Cuni, Ronny Pfannschmidt. Antonio Cuni, Ronny Pfannschmidt.

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.4.0.dev11' __version__ = '2.4.0.dev12'

View File

@ -298,7 +298,8 @@ class PluginManager(object):
showlocals=getattr(option, 'showlocals', False), showlocals=getattr(option, 'showlocals', False),
style=style, style=style,
) )
res = self.hook.pytest_internalerror(excrepr=excrepr) res = self.hook.pytest_internalerror(excrepr=excrepr,
excinfo=excinfo)
if not py.builtin.any(res): if not py.builtin.any(res):
for line in str(excrepr).split("\n"): for line in str(excrepr).split("\n"):
sys.stderr.write("INTERNALERROR> %s\n" %line) sys.stderr.write("INTERNALERROR> %s\n" %line)

View File

@ -241,8 +241,18 @@ def pytest_plugin_registered(plugin, manager):
def pytest_plugin_unregistered(plugin): def pytest_plugin_unregistered(plugin):
""" a py lib plugin got unregistered. """ """ a py lib plugin got unregistered. """
def pytest_internalerror(excrepr): def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """ """ called for internal errors. """
def pytest_keyboard_interrupt(excinfo): def pytest_keyboard_interrupt(excinfo):
""" called for keyboard interrupt. """ """ called for keyboard interrupt. """
def pytest_exception_interact(node, call, report):
""" (experimental, new in 2.4) called when
an exception was raised which can potentially be
interactively handled.
This hook is only called if an exception was raised
that is not an internal exception like "skip.Exception".
"""

View File

@ -10,7 +10,7 @@ except ImportError:
from UserDict import DictMixin as MappingMixin from UserDict import DictMixin as MappingMixin
from _pytest.mark import MarkInfo from _pytest.mark import MarkInfo
import _pytest.runner from _pytest.runner import collect_one_node, Skipped
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@ -372,7 +372,7 @@ class Collector(Node):
# the set of exceptions to interpret as "Skip the whole module" during # the set of exceptions to interpret as "Skip the whole module" during
# collection # collection
skip_exceptions = (_pytest.runner.Skipped,) skip_exceptions = (Skipped,)
class CollectError(Exception): class CollectError(Exception):
""" an error during collection, contains a custom message. """ """ an error during collection, contains a custom message. """
@ -512,8 +512,7 @@ class Session(FSCollector):
parts = self._parsearg(arg) parts = self._parsearg(arg)
self._initialparts.append(parts) self._initialparts.append(parts)
self._initialpaths.add(parts[0]) self._initialpaths.add(parts[0])
self.ihook.pytest_collectstart(collector=self) rep = collect_one_node(self)
rep = self.ihook.pytest_make_collect_report(collector=self)
self.ihook.pytest_collectreport(report=rep) self.ihook.pytest_collectreport(report=rep)
self.trace.root.indent -= 1 self.trace.root.indent -= 1
if self._notfound: if self._notfound:
@ -642,8 +641,7 @@ class Session(FSCollector):
resultnodes.append(node) resultnodes.append(node)
continue continue
assert isinstance(node, pytest.Collector) assert isinstance(node, pytest.Collector)
node.ihook.pytest_collectstart(collector=node) rep = collect_one_node(node)
rep = node.ihook.pytest_make_collect_report(collector=node)
if rep.passed: if rep.passed:
has_matched = False has_matched = False
for x in rep.result: for x in rep.result:
@ -664,8 +662,7 @@ class Session(FSCollector):
yield node yield node
else: else:
assert isinstance(node, pytest.Collector) assert isinstance(node, pytest.Collector)
node.ihook.pytest_collectstart(collector=node) rep = collect_one_node(node)
rep = node.ihook.pytest_make_collect_report(collector=node)
if rep.passed: if rep.passed:
for subnode in rep.result: for subnode in rep.result:
for x in self.genitems(subnode): for x in self.genitems(subnode):

View File

@ -52,31 +52,26 @@ def pytest_runtest_makereport():
pytestPDB.item = None pytestPDB.item = None
class PdbInvoke: class PdbInvoke:
@pytest.mark.tryfirst def pytest_exception_interact(self, node, call, report):
def pytest_runtest_makereport(self, item, call, __multicall__): return _enter_pdb(node, call.excinfo, report)
rep = __multicall__.execute()
if not call.excinfo or \ def pytest_internalerror(self, excrepr, excinfo):
call.excinfo.errisinstance(pytest.skip.Exception) or \ for line in str(excrepr).split("\n"):
call.excinfo.errisinstance(py.std.bdb.BdbQuit): sys.stderr.write("INTERNALERROR> %s\n" %line)
return rep sys.stderr.flush()
if hasattr(rep, "wasxfail"): tb = _postmortem_traceback(excinfo)
return rep post_mortem(tb)
return _enter_pdb(item, call.excinfo, rep)
def _enter_pdb(node, excinfo, rep):
def _enter_pdb(item, excinfo, rep):
# we assume that the above execute() suspended capturing
# XXX we re-use the TerminalReporter's terminalwriter # XXX we re-use the TerminalReporter's terminalwriter
# because this seems to avoid some encoding related troubles # because this seems to avoid some encoding related troubles
# for not completely clear reasons. # for not completely clear reasons.
tw = item.config.pluginmanager.getplugin("terminalreporter")._tw tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
tw.line() tw.line()
tw.sep(">", "traceback") tw.sep(">", "traceback")
rep.toterminal(tw) rep.toterminal(tw)
tw.sep(">", "entering PDB") tw.sep(">", "entering PDB")
tb = _postmortem_traceback(excinfo) tb = _postmortem_traceback(excinfo)
post_mortem(tb) post_mortem(tb)
rep._pdbshown = True rep._pdbshown = True

View File

@ -108,8 +108,16 @@ def call_and_report(item, when, log=True, **kwds):
report = hook.pytest_runtest_makereport(item=item, call=call) report = hook.pytest_runtest_makereport(item=item, call=call)
if log: if log:
hook.pytest_runtest_logreport(report=report) hook.pytest_runtest_logreport(report=report)
if check_interactive_exception(call, report):
hook.pytest_exception_interact(node=item, call=call, report=report)
return report return report
def check_interactive_exception(call, report):
return call.excinfo and not (
hasattr(report, "wasxfail") or
call.excinfo.errisinstance(skip.Exception) or
call.excinfo.errisinstance(py.std.bdb.BdbQuit))
def call_runtest_hook(item, when, **kwds): def call_runtest_hook(item, when, **kwds):
hookname = "pytest_runtest_" + when hookname = "pytest_runtest_" + when
ihook = getattr(item.ihook, hookname) ihook = getattr(item.ihook, hookname)
@ -268,8 +276,11 @@ def pytest_make_collect_report(collector):
if not hasattr(errorinfo, "toterminal"): if not hasattr(errorinfo, "toterminal"):
errorinfo = CollectErrorRepr(errorinfo) errorinfo = CollectErrorRepr(errorinfo)
longrepr = errorinfo longrepr = errorinfo
return CollectReport(collector.nodeid, outcome, longrepr, rep = CollectReport(collector.nodeid, outcome, longrepr,
getattr(call, 'result', None)) getattr(call, 'result', None))
rep.call = call # see collect_one_node
return rep
class CollectReport(BaseReport): class CollectReport(BaseReport):
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
@ -364,6 +375,16 @@ class SetupState(object):
col._prepare_exc = sys.exc_info() col._prepare_exc = sys.exc_info()
raise raise
def collect_one_node(collector):
ihook = collector.ihook
ihook.pytest_collectstart(collector=collector)
rep = ihook.pytest_make_collect_report(collector=collector)
call = rep.__dict__.pop("call", None)
if call and check_interactive_exception(call, rep):
ihook.pytest_exception_interact(node=collector, call=call, report=rep)
return rep
# ============================================================= # =============================================================
# Test OutcomeExceptions and helpers for creating them. # Test OutcomeExceptions and helpers for creating them.

View File

@ -359,6 +359,17 @@ test execution:
.. autofunction:: pytest_runtest_logreport .. autofunction:: pytest_runtest_logreport
Debugging/Interaction hooks
--------------------------------------
There are few hooks which can be used for special
reporting or interaction with exceptions:
.. autofunction:: pytest_internalerror
.. autofunction:: pytest_keyboard_interrupt
.. autofunction:: pytest_exception_interact
Reference of objects involved in hooks Reference of objects involved in hooks
=========================================================== ===========================================================

View File

@ -11,7 +11,7 @@ def main():
name='pytest', name='pytest',
description='py.test: simple powerful testing with Python', description='py.test: simple powerful testing with Python',
long_description = long_description, long_description = long_description,
version='2.4.0.dev11', version='2.4.0.dev12',
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -1,4 +1,5 @@
import pytest, py, sys import pytest, py, sys
from _pytest import runner
class TestOEJSKITSpecials: class TestOEJSKITSpecials:
def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage
@ -18,7 +19,7 @@ class TestOEJSKITSpecials:
pass pass
""") """)
# this hook finds funcarg factories # this hook finds funcarg factories
rep = modcol.ihook.pytest_make_collect_report(collector=modcol) rep = runner.collect_one_node(collector=modcol)
clscol = rep.result[0] clscol = rep.result[0]
clscol.obj = lambda arg1: None clscol.obj = lambda arg1: None
clscol.funcargs = {} clscol.funcargs = {}
@ -46,7 +47,7 @@ class TestOEJSKITSpecials:
pass pass
""") """)
# this hook finds funcarg factories # this hook finds funcarg factories
rep = modcol.ihook.pytest_make_collect_report(collector=modcol) rep = runner.collect_one_node(modcol)
clscol = rep.result[0] clscol = rep.result[0]
clscol.obj = lambda: None clscol.obj = lambda: None
clscol.funcargs = {} clscol.funcargs = {}

View File

@ -1,6 +1,8 @@
import py, pytest import py, pytest
import sys import sys
from test_doctest import xfail_if_pdbpp_installed
class TestPDB: class TestPDB:
def pytest_funcarg__pdblist(self, request): def pytest_funcarg__pdblist(self, request):
monkeypatch = request.getfuncargvalue("monkeypatch") monkeypatch = request.getfuncargvalue("monkeypatch")
@ -85,6 +87,32 @@ class TestPDB:
if child.isalive(): if child.isalive():
child.wait() child.wait()
def test_pdb_interaction_on_collection_issue181(self, testdir):
p1 = testdir.makepyfile("""
import pytest
xxx
""")
child = testdir.spawn_pytest("--pdb %s" % p1)
#child.expect(".*import pytest.*")
child.expect("(Pdb)")
child.sendeof()
child.expect("1 error")
if child.isalive():
child.wait()
def test_pdb_interaction_on_internal_error(self, testdir):
testdir.makeconftest("""
def pytest_runtest_protocol():
0/0
""")
p1 = testdir.makepyfile("def test_func(): pass")
child = testdir.spawn_pytest("--pdb %s" % p1)
#child.expect(".*import pytest.*")
child.expect("(Pdb)")
child.sendeof()
if child.isalive():
child.wait()
def test_pdb_interaction_capturing_simple(self, testdir): def test_pdb_interaction_capturing_simple(self, testdir):
p1 = testdir.makepyfile(""" p1 = testdir.makepyfile("""
import pytest import pytest
@ -122,6 +150,7 @@ class TestPDB:
if child.isalive(): if child.isalive():
child.wait() child.wait()
@xfail_if_pdbpp_installed
def test_pdb_interaction_doctest(self, testdir): def test_pdb_interaction_doctest(self, testdir):
p1 = testdir.makepyfile(""" p1 = testdir.makepyfile("""
import pytest import pytest

View File

@ -1,5 +1,5 @@
import pytest, py, sys, os import pytest, py, sys, os
from _pytest import runner from _pytest import runner, main
from py._code.code import ReprExceptionInfo from py._code.code import ReprExceptionInfo
class TestSetupState: class TestSetupState:
@ -298,7 +298,7 @@ class TestSessionReports:
class TestClass: class TestClass:
pass pass
""") """)
rep = runner.pytest_make_collect_report(col) rep = runner.collect_one_node(col)
assert not rep.failed assert not rep.failed
assert not rep.skipped assert not rep.skipped
assert rep.passed assert rep.passed
@ -318,7 +318,7 @@ class TestSessionReports:
def test_func(): def test_func():
pass pass
""") """)
rep = runner.pytest_make_collect_report(col) rep = main.collect_one_node(col)
assert not rep.failed assert not rep.failed
assert not rep.passed assert not rep.passed
assert rep.skipped assert rep.skipped