From a5e7b2760db81c1dec6e68c9a74edca5381925fd Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 18 Nov 2011 16:01:29 +0000 Subject: [PATCH] fix issue90 - perform teardown after its actual test function/item. This is implemented by modifying the runtestprotocol to remember "pending" teardowns and call them before the setup of the next item. --- CHANGELOG | 2 ++ _pytest/__init__.py | 2 +- _pytest/capture.py | 11 ------- _pytest/main.py | 8 ++--- _pytest/pytester.py | 6 ++-- _pytest/python.py | 1 + _pytest/runner.py | 56 ++++++++++++++++++++++++---------- doc/announce/release-2.2.0.txt | 15 ++++++--- setup.py | 2 +- testing/test_runner.py | 39 +++++++++++++++++++++++ 10 files changed, 103 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 53690a8f4..72c652126 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 2.1.3 and XXX 2.2.0 ---------------------------------------- +- fix issue90: introduce eager tearing down of test items so that + teardown function are called earlier. - add an all-powerful metafunc.parametrize function which allows to parametrize test function arguments in multiple steps and therefore from indepdenent plugins and palces. diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 7a23d54b9..5c8bb36d2 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.2.0.dev8' +__version__ = '2.2.0.dev9' diff --git a/_pytest/capture.py b/_pytest/capture.py index 37e5611cc..dad9c73fe 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -163,17 +163,6 @@ class CaptureManager: def pytest_runtest_teardown(self, item): self.resumecapture_item(item) - def pytest__teardown_final(self, __multicall__, session): - method = self._getmethod(session.config, None) - self.resumecapture(method) - try: - rep = __multicall__.execute() - 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/_pytest/main.py b/_pytest/main.py index 819e3c124..4082b46b8 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -82,11 +82,11 @@ def wrap_session(config, doit): session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught Spurious SystemExit!\n") - if not session.exitstatus and session._testsfailed: - session.exitstatus = EXIT_TESTSFAILED if initstate >= 2: config.hook.pytest_sessionfinish(session=session, - exitstatus=session.exitstatus) + exitstatus=session.exitstatus or (session._testsfailed and 1)) + if not session.exitstatus and session._testsfailed: + session.exitstatus = EXIT_TESTSFAILED if initstate >= 1: config.pluginmanager.do_unconfigure(config) return session.exitstatus @@ -106,7 +106,7 @@ def pytest_collection(session): def pytest_runtestloop(session): if session.config.option.collectonly: return True - for item in session.session.items: + for item in session.items: item.config.hook.pytest_runtest_protocol(item=item) if session.shouldstop: raise session.Interrupted(session.shouldstop) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 3ec8b696e..a7aee998f 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -355,9 +355,11 @@ class TmpTestdir: if not plugins: plugins = [] plugins.append(Collect()) - self.pytestmain(list(args), plugins=[Collect()]) + ret = self.pytestmain(list(args), plugins=[Collect()]) + reprec = rec[0] + reprec.ret = ret assert len(rec) == 1 - return items, rec[0] + return items, reprec def parseconfig(self, *args): args = [str(x) for x in args] diff --git a/_pytest/python.py b/_pytest/python.py index 46cb2cbff..71bbebfae 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -387,6 +387,7 @@ class FuncargLookupErrorRepr(TerminalRepr): tw.line() tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): def collect(self): # test generators are seen as collectors but they also diff --git a/_pytest/runner.py b/_pytest/runner.py index c7dc4da06..305fb7fb9 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -60,19 +60,33 @@ class NodeInfo: def __init__(self, location): self.location = location +def perform_pending_teardown(config, nextitem): + try: + olditem, log = config._pendingteardown + except AttributeError: + pass + else: + del config._pendingteardown + olditem.nextitem = nextitem + call_and_report(olditem, "teardown", log) + def pytest_runtest_protocol(item): + perform_pending_teardown(item.config, item) item.ihook.pytest_runtest_logstart( nodeid=item.nodeid, location=item.location, ) - runtestprotocol(item) + runtestprotocol(item, teardowndelayed=True) return True -def runtestprotocol(item, log=True): +def runtestprotocol(item, log=True, teardowndelayed=False): rep = call_and_report(item, "setup", log) reports = [rep] if rep.passed: reports.append(call_and_report(item, "call", log)) - reports.append(call_and_report(item, "teardown", log)) + if teardowndelayed: + item.config._pendingteardown = item, log + else: + reports.append(call_and_report(item, "teardown", log)) return reports def pytest_runtest_setup(item): @@ -85,12 +99,13 @@ def pytest_runtest_teardown(item): item.session._setupstate.teardown_exact(item) def pytest__teardown_final(session): - call = CallInfo(session._setupstate.teardown_all, when="teardown") - if call.excinfo: - ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) - call.excinfo.traceback = ntraceback.filter() - longrepr = call.excinfo.getrepr(funcargs=True) - return TeardownErrorReport(longrepr) + perform_pending_teardown(session.config, None) + #call = CallInfo(session._setupstate.teardown_all, when="teardown") + #if call.excinfo: + # ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) + # call.excinfo.traceback = ntraceback.filter() + # longrepr = call.excinfo.getrepr(funcargs=True) + # return TeardownErrorReport(longrepr) def pytest_report_teststatus(report): if report.when in ("setup", "teardown"): @@ -325,19 +340,28 @@ class SetupState(object): assert not self._finalizers def teardown_exact(self, item): - if self.stack and item == self.stack[-1]: + try: + colitem = item.nextitem + except AttributeError: + # in distributed testing there might be no known nexitem + # and in this case we use the parent node to at least call + # teardown of the current item + colitem = item.parent + needed_collectors = colitem and colitem.listchain() or [] + self._teardown_towards(needed_collectors) + + def _teardown_towards(self, needed_collectors): + while self.stack: + if self.stack == needed_collectors[:len(self.stack)]: + break self._pop_and_teardown() - else: - self._callfinalizers(item) def prepare(self, colitem): """ setup objects along the collector chain to the test-method and teardown previously setup objects.""" needed_collectors = colitem.listchain() - while self.stack: - if self.stack == needed_collectors[:len(self.stack)]: - break - self._pop_and_teardown() + self._teardown_towards(needed_collectors) + # check if the last collection node has raised an error for col in self.stack: if hasattr(col, '_prepare_exc'): diff --git a/doc/announce/release-2.2.0.txt b/doc/announce/release-2.2.0.txt index 5f76b5cce..072c8882a 100644 --- a/doc/announce/release-2.2.0.txt +++ b/doc/announce/release-2.2.0.txt @@ -1,8 +1,8 @@ -py.test 2.2.0: improved test markers and duration profiling +py.test 2.2.0: test marking++, parametrization++ and duration profiling =========================================================================== -pytest-2.2.0 is a quite [1] backward compatible release of the popular -py.test testing tool. There are a couple of new features: +pytest-2.2.0 is a test-suite compatible release of the popular +py.test testing tool. There are a couple of new features and improvements: * "--duration=N" option showing the N slowest test execution or setup/teardown calls. @@ -16,8 +16,13 @@ py.test testing tool. There are a couple of new features: a new "markers" ini-variable for registering test markers. The new "--strict" option will bail out with an error if you are using unregistered markers. +* teardown functions are now more eagerly called so that they appear + more directly connected to the last test item that needed a particular + fixture/setup. + Usage of improved parametrize is documented in examples at http://pytest.org/latest/example/parametrize.html + Usages of the improved marking mechanism is illustrated by a couple of initial examples, see http://pytest.org/latest/example/markers.html @@ -40,9 +45,11 @@ best, holger krekel -[1] notes on incompatibility +notes on incompatibility ------------------------------ +While test suites should work unchanged you might need to upgrade plugins: + * You need a new version of the pytest-xdist plugin (1.7) for distributing test runs. diff --git a/setup.py b/setup.py index b80971beb..eb0a71e07 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.2.0.dev8', + version='2.2.0.dev9', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/test_runner.py b/testing/test_runner.py index 6ec9a66f1..d1c832d03 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -160,6 +160,45 @@ class BaseFunctionalTests: #assert rep.failed.where.path.basename == "test_func.py" #assert rep.failed.failurerepr == "hello" + def test_teardown_final_returncode(self, testdir): + rec = testdir.inline_runsource(""" + def test_func(): + pass + def teardown_function(func): + raise ValueError(42) + """) + assert rec.ret == 1 + + def test_exact_teardown_issue90(self, testdir): + rec = testdir.inline_runsource(""" + import pytest + + class TestClass: + def test_method(self): + pass + def teardown_class(cls): + raise Exception() + + def test_func(): + pass + def teardown_function(func): + raise ValueError(42) + """) + reps = rec.getreports("pytest_runtest_logreport") + print (reps) + for i in range(2): + assert reps[i].nodeid.endswith("test_method") + assert reps[i].passed + assert reps[2].when == "teardown" + assert reps[2].failed + assert len(reps) == 6 + for i in range(3,5): + assert reps[i].nodeid.endswith("test_func") + assert reps[i].passed + assert reps[5].when == "teardown" + assert reps[5].nodeid.endswith("test_func") + assert reps[5].failed + def test_failure_in_setup_function_ignores_custom_repr(self, testdir): testdir.makepyfile(conftest=""" import pytest