Commits

holger krekel committed e4b9018

fix issue93 - avoid "delayed" teardowns for distributed testing by
simplifying handling of teardowns.

Comments (0)

Files changed (8)

+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
 ----------------------------------------
 

_pytest/__init__.py

 #
-__version__ = '2.2.0'
+__version__ = '2.2.1.dev1'

_pytest/hookspec.py

 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)``. """
 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
 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

_pytest/runner.py

     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):
 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"):
 #
 # 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. """
         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):
         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'],
     return {'console_scripts': l}
 
 if __name__ == '__main__':
-    main()
+    main()

testing/test_python.py

         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]
 

testing/test_runner.py

     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("""