diff --git a/CHANGELOG b/CHANGELOG index 5aa848852..075b693d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +Changes between 2.2.0 and 2.2.1.dev +---------------------------------------- + +- fix issue93 (in pytest and pytest-xdist) avoid "delayed teardowns": + the final test in a test node will now run its teardown directly + instead of waiting for the end of the session. Thanks Dave Hunt for + the good reporting and feedback. The pytest_runtest_protocol as well + as the pytest_runtest_teardown hooks now have "nextitem" available + which will be None indicating the end of the test run. + Changes between 2.1.3 and 2.2.0 ---------------------------------------- diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 6a64c3e88..33f303f5e 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.2.0' +__version__ = '2.2.1.dev1' diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index c13fa180c..ce128c744 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -121,16 +121,23 @@ def pytest_generate_tests(metafunc): def pytest_itemstart(item, node=None): """ (deprecated, use pytest_runtest_logstart). """ -def pytest_runtest_protocol(item): - """ implements the standard runtest_setup/call/teardown protocol including - capturing exceptions and calling reporting hooks on the results accordingly. +def pytest_runtest_protocol(item, nextitem): + """ implements the runtest_setup/call/teardown protocol for + the given test item, including capturing exceptions and calling + reporting hooks. + + :arg item: test item for which the runtest protocol is performed. + + :arg nexitem: the scheduled-to-be-next test item (or None if this + is the end my friend). This argument is passed on to + :py:func:`pytest_runtest_teardown`. :return boolean: True if no further hook implementations should be invoked. """ pytest_runtest_protocol.firstresult = True def pytest_runtest_logstart(nodeid, location): - """ signal the start of a test run. """ + """ signal the start of running a single test item. """ def pytest_runtest_setup(item): """ called before ``pytest_runtest_call(item)``. """ @@ -138,8 +145,14 @@ def pytest_runtest_setup(item): def pytest_runtest_call(item): """ called to execute the test ``item``. """ -def pytest_runtest_teardown(item): - """ called after ``pytest_runtest_call``. """ +def pytest_runtest_teardown(item, nextitem): + """ called after ``pytest_runtest_call``. + + :arg nexitem: the scheduled-to-be-next test item (None if no further + test item is scheduled). This argument can be used to + perform exact teardowns, i.e. calling just enough finalizers + so that nextitem only needs to call setup-functions. + """ def pytest_runtest_makereport(item, call): """ return a :py:class:`_pytest.runner.TestReport` object diff --git a/_pytest/main.py b/_pytest/main.py index 5d044021f..f6354205b 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -106,8 +106,12 @@ def pytest_collection(session): def pytest_runtestloop(session): if session.config.option.collectonly: return True - for item in session.items: - item.config.hook.pytest_runtest_protocol(item=item) + for i, item in enumerate(session.items): + try: + nextitem = session.items[i+1] + except IndexError: + nextitem = None + item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) if session.shouldstop: raise session.Interrupted(session.shouldstop) return True diff --git a/_pytest/runner.py b/_pytest/runner.py index b13a664ed..2ed278647 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -59,33 +59,20 @@ 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) +def pytest_runtest_protocol(item, nextitem): item.ihook.pytest_runtest_logstart( nodeid=item.nodeid, location=item.location, ) - runtestprotocol(item, teardowndelayed=True) + runtestprotocol(item, nextitem=nextitem) return True -def runtestprotocol(item, log=True, teardowndelayed=False): +def runtestprotocol(item, log=True, nextitem=None): rep = call_and_report(item, "setup", log) reports = [rep] if rep.passed: reports.append(call_and_report(item, "call", log)) - if teardowndelayed: - item.config._pendingteardown = item, log - else: - reports.append(call_and_report(item, "teardown", log)) + reports.append(call_and_report(item, "teardown", log, + nextitem=nextitem)) return reports def pytest_runtest_setup(item): @@ -94,17 +81,8 @@ def pytest_runtest_setup(item): def pytest_runtest_call(item): item.runtest() -def pytest_runtest_teardown(item): - item.session._setupstate.teardown_exact(item) - -def pytest__teardown_final(session): - 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_runtest_teardown(item, nextitem): + item.session._setupstate.teardown_exact(item, nextitem) def pytest_report_teststatus(report): if report.when in ("setup", "teardown"): @@ -120,18 +98,18 @@ def pytest_report_teststatus(report): # # Implementation -def call_and_report(item, when, log=True): - call = call_runtest_hook(item, when) +def call_and_report(item, when, log=True, **kwds): + call = call_runtest_hook(item, when, **kwds) hook = item.ihook report = hook.pytest_runtest_makereport(item=item, call=call) if log: hook.pytest_runtest_logreport(report=report) return report -def call_runtest_hook(item, when): +def call_runtest_hook(item, when, **kwds): hookname = "pytest_runtest_" + when ihook = getattr(item.ihook, hookname) - return CallInfo(lambda: ihook(item=item), when=when) + return CallInfo(lambda: ihook(item=item, **kwds), when=when) class CallInfo: """ Result/Exception info a function invocation. """ @@ -338,9 +316,8 @@ class SetupState(object): self._teardown_with_finalization(None) assert not self._finalizers - def teardown_exact(self, item): - colitem = item.nextitem - needed_collectors = colitem and colitem.listchain() or [] + def teardown_exact(self, item, nextitem): + needed_collectors = nextitem and nextitem.listchain() or [] self._teardown_towards(needed_collectors) def _teardown_towards(self, needed_collectors): diff --git a/setup.py b/setup.py index 3e198d62c..ddc5d1df2 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', + version='2.2.1.dev1', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -70,4 +70,4 @@ def make_entry_points(): return {'console_scripts': l} if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/testing/test_python.py b/testing/test_python.py index 5ce3e28f5..c59803334 100644 --- a/testing/test_python.py +++ b/testing/test_python.py @@ -689,7 +689,7 @@ class TestRequest: teardownlist = item.getparent(pytest.Module).obj.teardownlist ss = item.session._setupstate assert not teardownlist - ss.teardown_exact(item) + ss.teardown_exact(item, None) print(ss.stack) assert teardownlist == [1] diff --git a/testing/test_runner.py b/testing/test_runner.py index d1c832d03..a36e511f2 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -30,9 +30,9 @@ class TestSetupState: def test_teardown_exact_stack_empty(self, testdir): item = testdir.getitem("def test_func(): pass") ss = runner.SetupState() - ss.teardown_exact(item) - ss.teardown_exact(item) - ss.teardown_exact(item) + ss.teardown_exact(item, None) + ss.teardown_exact(item, None) + ss.teardown_exact(item, None) def test_setup_fails_and_failure_is_cached(self, testdir): item = testdir.getitem("""