- 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
-----------------------------------
- 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
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),
style=style,
)
res = self.hook.pytest_internalerror(excrepr=excrepr)
res = self.hook.pytest_internalerror(excrepr=excrepr,
excinfo=excinfo)
if not py.builtin.any(res):
for line in str(excrepr).split("\n"):
sys.stderr.write("INTERNALERROR> %s\n" %line)

View File

@ -241,8 +241,18 @@ def pytest_plugin_registered(plugin, manager):
def pytest_plugin_unregistered(plugin):
""" a py lib plugin got unregistered. """
def pytest_internalerror(excrepr):
def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """
def pytest_keyboard_interrupt(excinfo):
""" 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 _pytest.mark import MarkInfo
import _pytest.runner
from _pytest.runner import collect_one_node, Skipped
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
# collection
skip_exceptions = (_pytest.runner.Skipped,)
skip_exceptions = (Skipped,)
class CollectError(Exception):
""" an error during collection, contains a custom message. """
@ -512,8 +512,7 @@ class Session(FSCollector):
parts = self._parsearg(arg)
self._initialparts.append(parts)
self._initialpaths.add(parts[0])
self.ihook.pytest_collectstart(collector=self)
rep = self.ihook.pytest_make_collect_report(collector=self)
rep = collect_one_node(self)
self.ihook.pytest_collectreport(report=rep)
self.trace.root.indent -= 1
if self._notfound:
@ -642,8 +641,7 @@ class Session(FSCollector):
resultnodes.append(node)
continue
assert isinstance(node, pytest.Collector)
node.ihook.pytest_collectstart(collector=node)
rep = node.ihook.pytest_make_collect_report(collector=node)
rep = collect_one_node(node)
if rep.passed:
has_matched = False
for x in rep.result:
@ -664,8 +662,7 @@ class Session(FSCollector):
yield node
else:
assert isinstance(node, pytest.Collector)
node.ihook.pytest_collectstart(collector=node)
rep = node.ihook.pytest_make_collect_report(collector=node)
rep = collect_one_node(node)
if rep.passed:
for subnode in rep.result:
for x in self.genitems(subnode):

View File

@ -52,31 +52,26 @@ def pytest_runtest_makereport():
pytestPDB.item = None
class PdbInvoke:
@pytest.mark.tryfirst
def pytest_runtest_makereport(self, item, call, __multicall__):
rep = __multicall__.execute()
if not call.excinfo or \
call.excinfo.errisinstance(pytest.skip.Exception) or \
call.excinfo.errisinstance(py.std.bdb.BdbQuit):
return rep
if hasattr(rep, "wasxfail"):
return rep
return _enter_pdb(item, call.excinfo, rep)
def pytest_exception_interact(self, node, call, report):
return _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)
def _enter_pdb(item, excinfo, rep):
# we assume that the above execute() suspended capturing
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 = item.config.pluginmanager.getplugin("terminalreporter")._tw
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
tw.line()
tw.sep(">", "traceback")
rep.toterminal(tw)
tw.sep(">", "entering PDB")
tb = _postmortem_traceback(excinfo)
post_mortem(tb)
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)
if log:
hook.pytest_runtest_logreport(report=report)
if check_interactive_exception(call, report):
hook.pytest_exception_interact(node=item, call=call, report=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):
hookname = "pytest_runtest_" + when
ihook = getattr(item.ihook, hookname)
@ -268,8 +276,11 @@ def pytest_make_collect_report(collector):
if not hasattr(errorinfo, "toterminal"):
errorinfo = CollectErrorRepr(errorinfo)
longrepr = errorinfo
return CollectReport(collector.nodeid, outcome, longrepr,
rep = CollectReport(collector.nodeid, outcome, longrepr,
getattr(call, 'result', None))
rep.call = call # see collect_one_node
return rep
class CollectReport(BaseReport):
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
@ -364,6 +375,16 @@ class SetupState(object):
col._prepare_exc = sys.exc_info()
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.

View File

@ -359,6 +359,17 @@ test execution:
.. 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
===========================================================

View File

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

View File

@ -1,4 +1,5 @@
import pytest, py, sys
from _pytest import runner
class TestOEJSKITSpecials:
def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage
@ -18,7 +19,7 @@ class TestOEJSKITSpecials:
pass
""")
# 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.obj = lambda arg1: None
clscol.funcargs = {}
@ -46,7 +47,7 @@ class TestOEJSKITSpecials:
pass
""")
# 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.obj = lambda: None
clscol.funcargs = {}

View File

@ -1,6 +1,8 @@
import py, pytest
import sys
from test_doctest import xfail_if_pdbpp_installed
class TestPDB:
def pytest_funcarg__pdblist(self, request):
monkeypatch = request.getfuncargvalue("monkeypatch")
@ -85,6 +87,32 @@ class TestPDB:
if child.isalive():
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):
p1 = testdir.makepyfile("""
import pytest
@ -122,6 +150,7 @@ class TestPDB:
if child.isalive():
child.wait()
@xfail_if_pdbpp_installed
def test_pdb_interaction_doctest(self, testdir):
p1 = testdir.makepyfile("""
import pytest

View File

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