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
|
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.
|
||||||
|
|
|
@ -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):
|
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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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'],
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue