From bcd9aed0b1139165d99acbb67a508431eaa183e3 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 22 May 2009 19:57:21 +0200 Subject: [PATCH] * introduce pytest_pdb: plugin handling --pdb invocation * killing some unused/unneccessary hooks --HG-- branch : trunk --- py/test/custompdb.py | 77 -------------- py/test/plugin/api.py | 20 ++-- py/test/plugin/pytest_default.py | 29 +----- py/test/plugin/pytest_pdb.py | 162 +++++++++++++++++++++++++++-- py/test/plugin/pytest_pytester.py | 14 ++- py/test/pluginmanager.py | 4 +- py/test/runner.py | 15 +-- py/test/session.py | 11 +- py/test/testing/acceptance_test.py | 15 --- py/test/testing/test_runner.py | 20 ---- py/test/testing/test_session.py | 16 --- 11 files changed, 186 insertions(+), 197 deletions(-) delete mode 100644 py/test/custompdb.py diff --git a/py/test/custompdb.py b/py/test/custompdb.py deleted file mode 100644 index 4f221c54b..000000000 --- a/py/test/custompdb.py +++ /dev/null @@ -1,77 +0,0 @@ -import pdb, sys, linecache - -class Pdb(pdb.Pdb): - def do_list(self, arg): - self.lastcmd = 'list' - last = None - if arg: - try: - x = eval(arg, {}, {}) - if type(x) == type(()): - first, last = x - first = int(first) - last = int(last) - if last < first: - # Assume it's a count - last = first + last - else: - first = max(1, int(x) - 5) - except: - print '*** Error in argument:', repr(arg) - return - elif self.lineno is None: - first = max(1, self.curframe.f_lineno - 5) - else: - first = self.lineno + 1 - if last is None: - last = first + 10 - filename = self.curframe.f_code.co_filename - breaklist = self.get_file_breaks(filename) - try: - for lineno in range(first, last+1): - # start difference from normal do_line - line = self._getline(filename, lineno) - # end difference from normal do_line - if not line: - print '[EOF]' - break - else: - s = repr(lineno).rjust(3) - if len(s) < 4: s = s + ' ' - if lineno in breaklist: s = s + 'B' - else: s = s + ' ' - if lineno == self.curframe.f_lineno: - s = s + '->' - print s + '\t' + line, - self.lineno = lineno - except KeyboardInterrupt: - pass - do_l = do_list - - def _getline(self, filename, lineno): - if hasattr(filename, "__source__"): - try: - return filename.__source__.lines[lineno - 1] + "\n" - except IndexError: - return None - return linecache.getline(filename, lineno) - - def get_stack(self, f, t): - # Modified from bdb.py to be able to walk the stack beyond generators, - # which does not work in the normal pdb :-( - stack, i = pdb.Pdb.get_stack(self, f, t) - if f is None: - i = max(0, len(stack) - 1) - return stack, i - -def post_mortem(t): - # modified from pdb.py for the new get_stack() implementation - p = Pdb() - p.reset() - p.interaction(None, t) - -def set_trace(): - # again, a copy of the version in pdb.py - Pdb().set_trace(sys._getframe().f_back) - - diff --git a/py/test/plugin/api.py b/py/test/plugin/api.py index 6739d38f1..f8b931761 100644 --- a/py/test/plugin/api.py +++ b/py/test/plugin/api.py @@ -62,10 +62,14 @@ class PluginHooks: def pytest_collectreport(self, rep): """ collector finished collecting. """ + # XXX rename to item_collected()? meaning in distribution context? + def pytest_itemstart(self, item, node=None): + """ test item gets collected. """ + # ------------------------------------------------------------------------------ # runtest related hooks # ------------------------------------------------------------------------------ - # + def pytest_itemrun(self, item, pdb=None): """ run given test item and return test report. """ pytest_itemrun.firstresult = True @@ -75,21 +79,11 @@ class PluginHooks: pytest_pyfunc_call.firstresult = True def pytest_item_makereport(self, item, excinfo, when, outerr): - """ return ItemTestReport for the given test outcome. """ + """ make ItemTestReport for the specified test outcome. """ pytest_item_makereport.firstresult = True - def pytest_itemstart(self, item, node=None): - """ test item gets collected. """ - def pytest_itemtestreport(self, rep): - """ test has been run. """ - - # XXX pytest_runner reports - def pytest_item_runtest_finished(self, item, excinfo, outerr): - """ test has been run. """ - - def pytest_itemfixturereport(self, rep): - """ a report on running a fixture function. """ + """ process item test report. """ # ------------------------------------------------------------------------------ # reporting hooks (invoked from pytest_terminal.py) diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index 5253851b5..d42a886ac 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -2,13 +2,12 @@ import py -def pytest_itemrun(item, pdb=None): +def pytest_itemrun(item): from py.__.test.runner import basic_run_report, forked_run_report if item.config.option.boxed: - runner = forked_run_report + report = forked_run_report(item) else: - runner = basic_run_report - report = runner(item, pdb=pdb) + report = basic_run_report(item) item.config.hook.pytest_itemtestreport(rep=report) return True @@ -16,11 +15,6 @@ def pytest_item_makereport(item, excinfo, when, outerr): from py.__.test import runner return runner.ItemTestReport(item, excinfo, when, outerr) -def pytest_item_runtest_finished(item, excinfo, outerr): - from py.__.test import runner - rep = runner.ItemTestReport(item, excinfo, "execute", outerr) - item.config.hook.pytest_itemtestreport(rep=rep) - def pytest_pyfunc_call(pyfuncitem, args, kwargs): pyfuncitem.obj(*args, **kwargs) @@ -57,7 +51,7 @@ def pytest_report_iteminfo(item): return item.reportinfo() def pytest_addoption(parser): - group = parser.addgroup("general", "test collection and failure interaction options") + group = parser.getgroup("general", "test collection and failure interaction options") group._addoption('-v', '--verbose', action="count", dest="verbose", default=0, help="increase verbosity."), group._addoption('-x', '--exitfirst', @@ -75,9 +69,6 @@ def pytest_addoption(parser): #group._addoption('--showskipsummary', # action="store_true", dest="showskipsummary", default=False, # help="always show summary of skipped tests") - group._addoption('--pdb', - action="store_true", dest="usepdb", default=False, - help="start pdb (the Python debugger) on errors.") group._addoption('--tb', metavar="style", action="store", dest="tbstyle", default='long', type="choice", choices=['long', 'short', 'no'], @@ -89,9 +80,7 @@ def pytest_addoption(parser): action="store_true", dest="boxed", default=False, help="box each test run in a separate process") group._addoption('-p', action="append", dest="plugin", default = [], - help=("load the specified plugin after command line parsing. " - "Example: '-p hello' will trigger 'import pytest_hello' " - "and instantiate 'HelloPlugin' from the module.")) + help=("load the specified plugin after command line parsing. ")) group._addoption('-f', '--looponfail', action="store_true", dest="looponfail", default=False, help="run tests, re-run failing test set until all pass.") @@ -150,11 +139,6 @@ def fixoptions(config): config.option.tx = ['popen'] * int(config.option.numprocesses) if config.option.distload: config.option.dist = "load" - if config.getvalue("usepdb"): - if config.getvalue("looponfail"): - raise config.Error("--pdb incompatible with --looponfail.") - if config.option.dist != "no": - raise config.Error("--pdb incomptaible with distributing tests.") def loadplugins(config): for name in config.getvalue("plugin"): @@ -241,9 +225,6 @@ class TestDistOptions: assert testdir.tmpdir.join('x') in roots def test_dist_options(testdir): - py.test.raises(Exception, "testdir.parseconfigure('--pdb', '--looponfail')") - py.test.raises(Exception, "testdir.parseconfigure('--pdb', '-n 3')") - py.test.raises(Exception, "testdir.parseconfigure('--pdb', '-d')") config = testdir.parseconfigure("-n 2") assert config.option.dist == "load" assert config.option.tx == ['popen'] * 2 diff --git a/py/test/plugin/pytest_pdb.py b/py/test/plugin/pytest_pdb.py index 1f1c026c5..e1a128d17 100644 --- a/py/test/plugin/pytest_pdb.py +++ b/py/test/plugin/pytest_pdb.py @@ -1,9 +1,155 @@ -""" XXX should be used sometime. """ -from py.__.test.custompdb import post_mortem +""" +interactive debugging with a PDB prompt. + +""" +import py +import pdb, sys, linecache +from py.__.test.outcome import Skipped + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption('--pdb', + action="store_true", dest="usepdb", default=False, + help="start pdb (the Python debugger) on errors.") + + +def pytest_configure(config): + if config.option.usepdb: + if config.getvalue("looponfail"): + raise config.Error("--pdb incompatible with --looponfail.") + if config.option.dist != "no": + raise config.Error("--pdb incomptaible with distributing tests.") + config.pluginmanager.register(PdbInvoke()) + +class PdbInvoke: + def pytest_item_makereport(self, item, excinfo, when, outerr): + if excinfo and not excinfo.errisinstance(Skipped): + tw = py.io.TerminalWriter() + repr = excinfo.getrepr() + repr.toterminal(tw) + post_mortem(excinfo._excinfo[2]) + +class Pdb(py.std.pdb.Pdb): + def do_list(self, arg): + self.lastcmd = 'list' + last = None + if arg: + try: + x = eval(arg, {}, {}) + if type(x) == type(()): + first, last = x + first = int(first) + last = int(last) + if last < first: + # Assume it's a count + last = first + last + else: + first = max(1, int(x) - 5) + except: + print '*** Error in argument:', repr(arg) + return + elif self.lineno is None: + first = max(1, self.curframe.f_lineno - 5) + else: + first = self.lineno + 1 + if last is None: + last = first + 10 + filename = self.curframe.f_code.co_filename + breaklist = self.get_file_breaks(filename) + try: + for lineno in range(first, last+1): + # start difference from normal do_line + line = self._getline(filename, lineno) + # end difference from normal do_line + if not line: + print '[EOF]' + break + else: + s = repr(lineno).rjust(3) + if len(s) < 4: s = s + ' ' + if lineno in breaklist: s = s + 'B' + else: s = s + ' ' + if lineno == self.curframe.f_lineno: + s = s + '->' + print s + '\t' + line, + self.lineno = lineno + except KeyboardInterrupt: + pass + do_l = do_list + + def _getline(self, filename, lineno): + if hasattr(filename, "__source__"): + try: + return filename.__source__.lines[lineno - 1] + "\n" + except IndexError: + return None + return linecache.getline(filename, lineno) + + def get_stack(self, f, t): + # Modified from bdb.py to be able to walk the stack beyond generators, + # which does not work in the normal pdb :-( + stack, i = pdb.Pdb.get_stack(self, f, t) + if f is None: + i = max(0, len(stack) - 1) + return stack, i + +def post_mortem(t): + # modified from pdb.py for the new get_stack() implementation + p = Pdb() + p.reset() + p.interaction(None, t) + +def set_trace(): + # again, a copy of the version in pdb.py + Pdb().set_trace(sys._getframe().f_back) + + +class TestPDB: + def pytest_funcarg__pdblist(self, request): + monkeypatch = request.getfuncargvalue("monkeypatch") + pdblist = [] + def mypdb(*args): + pdblist.append(args) + monkeypatch.setitem(globals(), 'post_mortem', mypdb) + return pdblist + + def test_incompatibility_messages(self, testdir): + Error = py.test.config.Error + py.test.raises(Error, "testdir.parseconfigure('--pdb', '--looponfail')") + py.test.raises(Error, "testdir.parseconfigure('--pdb', '-n 3')") + py.test.raises(Error, "testdir.parseconfigure('--pdb', '-d')") + + def test_pdb_on_fail(self, testdir, pdblist): + rep = testdir.inline_runsource1('--pdb', """ + def test_func(): + assert 0 + """) + assert rep.failed + assert len(pdblist) == 1 + tb = py.code.Traceback(pdblist[0][0]) + assert tb[-1].name == "test_func" + + def test_pdb_on_skip(self, testdir, pdblist): + rep = testdir.inline_runsource1('--pdb', """ + import py + def test_func(): + py.test.skip("hello") + """) + assert rep.skipped + assert len(pdblist) == 0 + + def test_pdb_interaction(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + i = 0 + assert i == 1 + """) + child = testdir.spawn_pytest("--pdb %s" % p1) + #child.expect(".*def test_1.*") + child.expect(".*i = 0.*") + child.expect("(Pdb)") + child.sendeof() + child.expect("1 failed") + if child.isalive(): + child.wait() -def pytest_item_runtest_finished(item, excinfo, outerr): - if excinfo and item.config.option.usepdb: - tw = py.io.TerminalWriter() - repr = excinfo.getrepr() - repr.toterminal(tw) - post_mortem(excinfo._excinfo[2]) diff --git a/py/test/plugin/pytest_pytester.py b/py/test/plugin/pytest_pytester.py index 23406907a..b26b8e182 100644 --- a/py/test/plugin/pytest_pytester.py +++ b/py/test/plugin/pytest_pytester.py @@ -145,19 +145,29 @@ class TmpTestdir: items = list(session.genitems(colitems)) return items, rec - def runitem(self, source, **runnerargs): + def runitem(self, source): # used from runner functional tests item = self.getitem(source) # the test class where we are called from wants to provide the runner testclassinstance = self.request.function.im_self runner = testclassinstance.getrunner() - return runner(item, **runnerargs) + return runner(item) def inline_runsource(self, source, *cmdlineargs): p = self.makepyfile(source) l = list(cmdlineargs) + [p] return self.inline_run(*l) + def inline_runsource1(self, *args): + args = list(args) + source = args.pop() + p = self.makepyfile(source) + l = list(args) + [p] + reprec = self.inline_run(*l) + reports = reprec.getreports("pytest_itemtestreport") + assert len(reports) == 1, reports + return reports[0] + def inline_run(self, *args): config = self.parseconfig(*args) config.pluginmanager.do_configure(config) diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index 6b4083e7a..058cd6e1d 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -140,8 +140,8 @@ class PluginManager(object): config.hook.pytest_unconfigure(config=config) config.pluginmanager.unregister(self) - def do_itemrun(self, item, pdb=None): - res = self.hook.pytest_itemrun(item=item, pdb=pdb) + def do_itemrun(self, item): + res = self.hook.pytest_itemrun(item=item) if res is None: raise ValueError("could not run %r" %(item,)) diff --git a/py/test/runner.py b/py/test/runner.py index 0799360f0..470f3ccf8 100644 --- a/py/test/runner.py +++ b/py/test/runner.py @@ -9,7 +9,6 @@ import py from py.__.test.outcome import Skipped -from py.__.test.custompdb import post_mortem class Call: excinfo = None @@ -26,7 +25,7 @@ def runtest_with_deprecated_check(item): if not item._deprecated_testexecution(): item.runtest() -def basic_run_report(item, pdb=None): +def basic_run_report(item): """ return report about setting up and running a test item. """ setupstate = item.config._setupstate capture = item.config._getcapture() @@ -38,13 +37,9 @@ def basic_run_report(item, pdb=None): call = Call("teardown", lambda: setupstate.teardown_exact(item)) finally: outerr = capture.reset() - testrep = item.config.hook.pytest_item_makereport( - item=item, excinfo=call.excinfo, when=call.when, outerr=outerr) - if pdb and testrep.failed: - tw = py.io.TerminalWriter() - testrep.toterminal(tw) - pdb(call.excinfo) - return testrep + return item.config.hook.pytest_item_makereport( + item=item, excinfo=call.excinfo, + when=call.when, outerr=outerr) def basic_collect_report(collector): call = collector.config.guardedcall( @@ -55,7 +50,7 @@ def basic_collect_report(collector): result = call.result return CollectReport(collector, result, call.excinfo, call.outerr) -def forked_run_report(item, pdb=None): +def forked_run_report(item): EXITSTATUS_TESTEXIT = 4 from py.__.test.dist.mypickle import ImmutablePickler ipickle = ImmutablePickler(uneven=0) diff --git a/py/test/session.py b/py/test/session.py index 3cb0f4b36..495940bdd 100644 --- a/py/test/session.py +++ b/py/test/session.py @@ -112,8 +112,7 @@ class Session(object): if self.shouldstop: break if not self.config.option.collectonly: - self.runtest(item) - + item.config.pluginmanager.do_itemrun(item) self.config._setupstate.teardown_all() except KeyboardInterrupt: captured_excinfo = py.code.ExceptionInfo() @@ -126,11 +125,3 @@ class Session(object): exitstatus = outcome.EXIT_TESTSFAILED self.sessionfinishes(exitstatus=exitstatus, excinfo=captured_excinfo) return exitstatus - - def runpdb(self, excinfo): - from py.__.test.custompdb import post_mortem - post_mortem(excinfo._excinfo[2]) - - def runtest(self, item): - pdb = self.config.option.usepdb and self.runpdb or None - item.config.pluginmanager.do_itemrun(item, pdb=pdb) diff --git a/py/test/testing/acceptance_test.py b/py/test/testing/acceptance_test.py index 4a68a1233..03075d75a 100644 --- a/py/test/testing/acceptance_test.py +++ b/py/test/testing/acceptance_test.py @@ -441,21 +441,6 @@ class TestDistribution: class TestInteractive: - def test_pdb_interaction(self, testdir): - p1 = testdir.makepyfile(""" - def test_1(): - i = 0 - assert i == 1 - """) - child = testdir.spawn_pytest("--pdb %s" % p1) - #child.expect(".*def test_1.*") - child.expect(".*i = 0.*") - child.expect("(Pdb)") - child.sendeof() - child.expect("1 failed") - if child.isalive(): - child.wait() - def test_simple_looponfail_interaction(self, testdir): p1 = testdir.makepyfile(""" def test_1(): diff --git a/py/test/testing/test_runner.py b/py/test/testing/test_runner.py index 41bdc1433..839bf4186 100644 --- a/py/test/testing/test_runner.py +++ b/py/test/testing/test_runner.py @@ -222,26 +222,6 @@ class TestExecutionNonForked(BaseFunctionalTests): else: py.test.fail("did not raise") - def test_pdb_on_fail(self, testdir): - l = [] - rep = testdir.runitem(""" - def test_func(): - assert 0 - """, pdb=l.append) - assert rep.failed - assert rep.when == "runtest" - assert len(l) == 1 - - def test_pdb_on_skip(self, testdir): - l = [] - rep = testdir.runitem(""" - import py - def test_func(): - py.test.skip("hello") - """, pdb=l.append) - assert len(l) == 0 - assert rep.skipped - class TestExecutionForked(BaseFunctionalTests): def getrunner(self): if not hasattr(py.std.os, 'fork'): diff --git a/py/test/testing/test_session.py b/py/test/testing/test_session.py index 042c62976..612f6f528 100644 --- a/py/test/testing/test_session.py +++ b/py/test/testing/test_session.py @@ -135,22 +135,6 @@ class SessionTests: assert reports[0].skipped class TestNewSession(SessionTests): - def test_pdb_run(self, testdir, monkeypatch): - import py.__.test.custompdb - tfile = testdir.makepyfile(""" - def test_usepdb(): - assert 0 - """) - l = [] - def mypdb(*args): - l.append(args) - monkeypatch.setattr(py.__.test.custompdb, 'post_mortem', mypdb) - reprec = testdir.inline_run('--pdb', tfile) - rep = reprec.matchreport("test_usepdb") - assert rep.failed - assert len(l) == 1 - tb = py.code.Traceback(l[0][0]) - assert tb[-1].name == "test_usepdb" def test_order_of_execution(self, testdir): reprec = testdir.inline_runsource("""