[py-svn] commit/pytest: hpk42: 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.

Bitbucket commits-noreply at bitbucket.org
Fri Nov 18 17:02:26 CET 2011


1 new commit in pytest:


https://bitbucket.org/hpk42/pytest/changeset/e776739a7800/
changeset:   e776739a7800
user:        hpk42
date:        2011-11-18 17:01:29
summary:     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.
affected #:  10 files

diff -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 CHANGELOG
--- 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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 _pytest/__init__.py
--- a/_pytest/__init__.py
+++ b/_pytest/__init__.py
@@ -1,2 +1,2 @@
 #
-__version__ = '2.2.0.dev8'
+__version__ = '2.2.0.dev9'


diff -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 _pytest/capture.py
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -163,17 +163,6 @@
     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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 _pytest/main.py
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -82,11 +82,11 @@
         session.exitstatus = EXIT_INTERNALERROR
         if excinfo.errisinstance(SystemExit):
             sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
+    if initstate >= 2:
+        config.hook.pytest_sessionfinish(session=session,
+            exitstatus=session.exitstatus or (session._testsfailed and 1))
     if not session.exitstatus and session._testsfailed:
         session.exitstatus = EXIT_TESTSFAILED
-    if initstate >= 2:
-        config.hook.pytest_sessionfinish(session=session,
-            exitstatus=session.exitstatus)
     if initstate >= 1:
         config.pluginmanager.do_unconfigure(config)
     return session.exitstatus
@@ -106,7 +106,7 @@
 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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 _pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -355,9 +355,11 @@
         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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 _pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -387,6 +387,7 @@
         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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 _pytest/runner.py
--- a/_pytest/runner.py
+++ b/_pytest/runner.py
@@ -60,19 +60,33 @@
     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 @@
     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 @@
         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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 doc/announce/release-2.2.0.txt
--- 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 @@
   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 @@
 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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 setup.py
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
         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 -r b22c3b301862b6cbd2593cc4933b87a190ee3fff -r e776739a7800d45ba9e55c91ad33827a16b90420 testing/test_runner.py
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -160,6 +160,45 @@
         #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

Repository URL: https://bitbucket.org/hpk42/pytest/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.



More information about the pytest-commit mailing list