- 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:
parent
8360c1e687
commit
94ee37cdb3
|
@ -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.
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
#
|
||||
__version__ = '2.4.0.dev11'
|
||||
__version__ = '2.4.0.dev12'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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".
|
||||
"""
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
===========================================================
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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'],
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue