- 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
|
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.
|
||||||
|
|
||||||
|
|
|
@ -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),
|
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)
|
||||||
|
|
|
@ -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".
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
===========================================================
|
===========================================================
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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'],
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue