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.

This commit is contained in:
holger krekel 2011-11-18 16:01:29 +00:00
parent efe438d3e8
commit a5e7b2760d
10 changed files with 103 additions and 39 deletions

View File

@ -1,6 +1,8 @@
Changes between 2.1.3 and XXX 2.2.0 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 - add an all-powerful metafunc.parametrize function which allows to
parametrize test function arguments in multiple steps and therefore parametrize test function arguments in multiple steps and therefore
from indepdenent plugins and palces. from indepdenent plugins and palces.

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.2.0.dev8' __version__ = '2.2.0.dev9'

View File

@ -163,17 +163,6 @@ class CaptureManager:
def pytest_runtest_teardown(self, item): def pytest_runtest_teardown(self, item):
self.resumecapture_item(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): def pytest_keyboard_interrupt(self, excinfo):
if hasattr(self, '_capturing'): if hasattr(self, '_capturing'):
self.suspendcapture() self.suspendcapture()

View File

@ -82,11 +82,11 @@ def wrap_session(config, doit):
session.exitstatus = EXIT_INTERNALERROR session.exitstatus = EXIT_INTERNALERROR
if excinfo.errisinstance(SystemExit): if excinfo.errisinstance(SystemExit):
sys.stderr.write("mainloop: caught Spurious SystemExit!\n") sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
if not session.exitstatus and session._testsfailed:
session.exitstatus = EXIT_TESTSFAILED
if initstate >= 2: if initstate >= 2:
config.hook.pytest_sessionfinish(session=session, 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: if initstate >= 1:
config.pluginmanager.do_unconfigure(config) config.pluginmanager.do_unconfigure(config)
return session.exitstatus return session.exitstatus
@ -106,7 +106,7 @@ def pytest_collection(session):
def pytest_runtestloop(session): def pytest_runtestloop(session):
if session.config.option.collectonly: if session.config.option.collectonly:
return True return True
for item in session.session.items: for item in session.items:
item.config.hook.pytest_runtest_protocol(item=item) item.config.hook.pytest_runtest_protocol(item=item)
if session.shouldstop: if session.shouldstop:
raise session.Interrupted(session.shouldstop) raise session.Interrupted(session.shouldstop)

View File

@ -355,9 +355,11 @@ class TmpTestdir:
if not plugins: if not plugins:
plugins = [] plugins = []
plugins.append(Collect()) 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 assert len(rec) == 1
return items, rec[0] return items, reprec
def parseconfig(self, *args): def parseconfig(self, *args):
args = [str(x) for x in args] args = [str(x) for x in args]

View File

@ -387,6 +387,7 @@ class FuncargLookupErrorRepr(TerminalRepr):
tw.line() tw.line()
tw.line("%s:%d" % (self.filename, self.firstlineno+1)) tw.line("%s:%d" % (self.filename, self.firstlineno+1))
class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
def collect(self): def collect(self):
# test generators are seen as collectors but they also # test generators are seen as collectors but they also

View File

@ -60,19 +60,33 @@ class NodeInfo:
def __init__(self, location): def __init__(self, location):
self.location = 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): def pytest_runtest_protocol(item):
perform_pending_teardown(item.config, item)
item.ihook.pytest_runtest_logstart( item.ihook.pytest_runtest_logstart(
nodeid=item.nodeid, location=item.location, nodeid=item.nodeid, location=item.location,
) )
runtestprotocol(item) runtestprotocol(item, teardowndelayed=True)
return True return True
def runtestprotocol(item, log=True): def runtestprotocol(item, log=True, teardowndelayed=False):
rep = call_and_report(item, "setup", log) rep = call_and_report(item, "setup", log)
reports = [rep] reports = [rep]
if rep.passed: if rep.passed:
reports.append(call_and_report(item, "call", log)) 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 return reports
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
@ -85,12 +99,13 @@ def pytest_runtest_teardown(item):
item.session._setupstate.teardown_exact(item) item.session._setupstate.teardown_exact(item)
def pytest__teardown_final(session): def pytest__teardown_final(session):
call = CallInfo(session._setupstate.teardown_all, when="teardown") perform_pending_teardown(session.config, None)
if call.excinfo: #call = CallInfo(session._setupstate.teardown_all, when="teardown")
ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) #if call.excinfo:
call.excinfo.traceback = ntraceback.filter() # ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir)
longrepr = call.excinfo.getrepr(funcargs=True) # call.excinfo.traceback = ntraceback.filter()
return TeardownErrorReport(longrepr) # longrepr = call.excinfo.getrepr(funcargs=True)
# return TeardownErrorReport(longrepr)
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
if report.when in ("setup", "teardown"): if report.when in ("setup", "teardown"):
@ -325,19 +340,28 @@ class SetupState(object):
assert not self._finalizers assert not self._finalizers
def teardown_exact(self, item): 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() self._pop_and_teardown()
else:
self._callfinalizers(item)
def prepare(self, colitem): def prepare(self, colitem):
""" setup objects along the collector chain to the test-method """ setup objects along the collector chain to the test-method
and teardown previously setup objects.""" and teardown previously setup objects."""
needed_collectors = colitem.listchain() needed_collectors = colitem.listchain()
while self.stack: self._teardown_towards(needed_collectors)
if self.stack == needed_collectors[:len(self.stack)]:
break
self._pop_and_teardown()
# check if the last collection node has raised an error # check if the last collection node has raised an error
for col in self.stack: for col in self.stack:
if hasattr(col, '_prepare_exc'): if hasattr(col, '_prepare_exc'):

View File

@ -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 pytest-2.2.0 is a test-suite compatible release of the popular
py.test testing tool. There are a couple of new features: py.test testing tool. There are a couple of new features and improvements:
* "--duration=N" option showing the N slowest test execution * "--duration=N" option showing the N slowest test execution
or setup/teardown calls. 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" 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. 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 Usage of improved parametrize is documented in examples at
http://pytest.org/latest/example/parametrize.html http://pytest.org/latest/example/parametrize.html
Usages of the improved marking mechanism is illustrated by a couple Usages of the improved marking mechanism is illustrated by a couple
of initial examples, see http://pytest.org/latest/example/markers.html of initial examples, see http://pytest.org/latest/example/markers.html
@ -40,9 +45,11 @@ best,
holger krekel 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 * You need a new version of the pytest-xdist plugin (1.7) for distributing
test runs. test runs.

View File

@ -24,7 +24,7 @@ def main():
name='pytest', name='pytest',
description='py.test: simple powerful testing with Python', description='py.test: simple powerful testing with Python',
long_description = long_description, long_description = long_description,
version='2.2.0.dev8', version='2.2.0.dev9',
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -160,6 +160,45 @@ class BaseFunctionalTests:
#assert rep.failed.where.path.basename == "test_func.py" #assert rep.failed.where.path.basename == "test_func.py"
#assert rep.failed.failurerepr == "hello" #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): def test_failure_in_setup_function_ignores_custom_repr(self, testdir):
testdir.makepyfile(conftest=""" testdir.makepyfile(conftest="""
import pytest import pytest