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

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):
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()

View File

@ -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)

View File

@ -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]

View File

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

View File

@ -60,18 +60,32 @@ 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))
if teardowndelayed:
item.config._pendingteardown = item, log
else:
reports.append(call_and_report(item, "teardown", log))
return reports
@ -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'):

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

View File

@ -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'],

View File

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