diff --git a/CHANGELOG b/CHANGELOG index fe8910b5d..f8bfbf540 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 1.0.0b8 and 1.0.0b9 ===================================== +* cleanly handle and report final teardown of test setup + * fix svn-1.6 compat issue with py.path.svnwc().versioned() (thanks Wouter Vanden Hove) diff --git a/py/test/dist/txnode.py b/py/test/dist/txnode.py index 513772947..0d98aed90 100644 --- a/py/test/dist/txnode.py +++ b/py/test/dist/txnode.py @@ -115,6 +115,7 @@ class SlaveNode(object): self.config.basetemp = py.path.local(basetemp) self.config.pluginmanager.do_configure(self.config) self.config.pluginmanager.register(self) + self.runner = self.config.pluginmanager.getplugin("pytest_runner") self.sendevent("slaveready") try: while 1: @@ -135,26 +136,15 @@ class SlaveNode(object): raise def run_single(self, item): - call = CallInfo(item._checkcollectable, 'setup') + call = self.runner.CallInfo(item._checkcollectable, when='setup') if call.excinfo: # likely it is not collectable here because of # platform/import-dependency induced skips # XXX somewhat ugly shortcuts - also makes a collection # failure into an ItemTestReport - this might confuse # pytest_runtest_logreport hooks - runner = item.config.pluginmanager.getplugin("pytest_runner") - rep = runner.pytest_runtest_makereport(item=item, call=call) + rep = self.runner.pytest_runtest_makereport(item=item, call=call) self.pytest_runtest_logreport(rep) return item.config.hook.pytest_runtest_protocol(item=item) -class CallInfo: - excinfo = None - def __init__(self, func, when): - self.when = when - try: - self.result = func() - except KeyboardInterrupt: - raise - except: - self.excinfo = py.code.ExceptionInfo() diff --git a/py/test/plugin/hookspec.py b/py/test/plugin/hookspec.py index ac3260c41..15e506ba9 100644 --- a/py/test/plugin/hookspec.py +++ b/py/test/plugin/hookspec.py @@ -86,6 +86,14 @@ pytest_runtest_makereport.firstresult = True def pytest_runtest_logreport(rep): """ process item test report. """ +# special handling for final teardown - somewhat internal for now +def pytest__teardown_final(session): + """ called before test session finishes. """ +pytest__teardown_final.firstresult = True + +def pytest__teardown_final_logerror(rep): + """ called if runtest_teardown_final failed. """ + # ------------------------------------------------------------------------- # test session related hooks # ------------------------------------------------------------------------- diff --git a/py/test/plugin/pytest_iocapture.py b/py/test/plugin/pytest_iocapture.py index 5bf6657b1..33a53f586 100644 --- a/py/test/plugin/pytest_iocapture.py +++ b/py/test/plugin/pytest_iocapture.py @@ -188,6 +188,20 @@ class CaptureManager: def pytest_runtest_teardown(self, item): self.resumecapture_item(item) + def pytest_runtest_teardown(self, item): + self.resumecapture_item(item) + + def pytest__teardown_final(self, __call__, session): + method = self._getmethod(session.config, None) + self.resumecapture(method) + try: + rep = __call__.execute(firstresult=True) + finally: + outerr = self.suspendcapture() + if rep: + addouterr(rep, outerr) + return rep + def pytest_keyboard_interrupt(self, excinfo): if hasattr(self, '_capturing'): self.suspendcapture() diff --git a/py/test/plugin/pytest_runner.py b/py/test/plugin/pytest_runner.py index 6b8ed913a..bae86aa1b 100644 --- a/py/test/plugin/pytest_runner.py +++ b/py/test/plugin/pytest_runner.py @@ -22,7 +22,10 @@ def pytest_configure(config): def pytest_sessionfinish(session, exitstatus): # XXX see above if hasattr(session.config, '_setupstate'): - session.config._setupstate.teardown_all() + hook = session.config.hook + rep = hook.pytest__teardown_final(session=session) + if rep: + hook.pytest__teardown_final_logerror(rep=rep) # prevent logging module atexit handler from choking on # its attempt to close already closed streams # see http://bugs.python.org/issue6333 @@ -70,6 +73,12 @@ def pytest_runtest_makereport(item, call): def pytest_runtest_teardown(item): item.config._setupstate.teardown_exact(item) +def pytest__teardown_final(session): + call = CallInfo(session.config._setupstate.teardown_all, when="teardown") + if call.excinfo: + rep = TeardownErrorReport(call.excinfo) + return rep + def pytest_report_teststatus(rep): if rep.when in ("setup", "teardown"): if rep.failed: @@ -83,22 +92,24 @@ def pytest_report_teststatus(rep): # Implementation def call_and_report(item, when, log=True): - call = RuntestHookCall(item, when) + call = call_runtest_hook(item, when) hook = item.config.hook report = hook.pytest_runtest_makereport(item=item, call=call) if log and (when == "call" or not report.passed): hook.pytest_runtest_logreport(rep=report) return report -class RuntestHookCall: +def call_runtest_hook(item, when): + hookname = "pytest_runtest_" + when + hook = getattr(item.config.hook, hookname) + return CallInfo(lambda: hook(item=item), when=when) + +class CallInfo: excinfo = None - _prefix = "pytest_runtest_" - def __init__(self, item, when): + def __init__(self, func, when): self.when = when - hookname = self._prefix + when - hook = getattr(item.config.hook, hookname) try: - self.result = hook(item=item) + self.result = func() except KeyboardInterrupt: raise except: @@ -209,6 +220,13 @@ class CollectReport(BaseReport): def getnode(self): return self.collector +class TeardownErrorReport(BaseReport): + skipped = passed = False + failed = True + when = "teardown" + def __init__(self, excinfo): + self.longrepr = excinfo.getrepr(funcargs=True) + class SetupState(object): """ shared state for setting up/tearing down test items or collectors. """ def __init__(self): diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index 9dcfcff3d..9347aa63c 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -166,8 +166,10 @@ class TerminalReporter: fspath, lineno, msg = self._getreportinfo(item) self.write_fspath_result(fspath, "") + def pytest__teardown_final_logerror(self, rep): + self.stats.setdefault("error", []).append(rep) + def pytest_runtest_logreport(self, rep): - fspath = rep.item.fspath cat, letter, word = self.getcategoryletterword(rep) if not letter and not word: # probably passed setup/teardown @@ -290,9 +292,11 @@ class TerminalReporter: def _getfailureheadline(self, rep): if hasattr(rep, "collector"): return str(rep.collector.fspath) - else: + elif hasattr(rep, 'item'): fspath, lineno, msg = self._getreportinfo(rep.item) return msg + else: + return "test session" def _getreportinfo(self, item): try: diff --git a/py/test/plugin/test_pytest_iocapture.py b/py/test/plugin/test_pytest_iocapture.py index 1a7b3ff0b..5156f707c 100644 --- a/py/test/plugin/test_pytest_iocapture.py +++ b/py/test/plugin/test_pytest_iocapture.py @@ -123,7 +123,6 @@ class TestPerTestCapturing: #"*1 fixture failure*" ]) - @py.test.mark.xfail def test_teardown_final_capturing(self, testdir): p = testdir.makepyfile(""" def teardown_module(mod): @@ -134,8 +133,10 @@ class TestPerTestCapturing: """) result = testdir.runpytest(p) assert result.stdout.fnmatch_lines([ - "teardown module*", - #"*1 fixture failure*" + "*def teardown_module(mod):*", + "*Captured stdout*", + "*teardown module*", + "*1 error*", ]) def test_capturing_outerr(self, testdir):