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:
parent
efe438d3e8
commit
a5e7b2760d
|
@ -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.
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
#
|
||||
__version__ = '2.2.0.dev8'
|
||||
__version__ = '2.2.0.dev9'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
2
setup.py
2
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.dev8',
|
||||
version='2.2.0.dev9',
|
||||
url='http://pytest.org',
|
||||
license='MIT license',
|
||||
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue