fix issue93 - avoid "delayed" teardowns for distributed testing by

simplifying handling of teardowns.
This commit is contained in:
holger krekel 2011-12-02 21:00:19 +00:00
parent b28977fbaf
commit c4fe622b82
8 changed files with 55 additions and 51 deletions

View File

@ -1,3 +1,13 @@
Changes between 2.2.0 and 2.2.1.dev
----------------------------------------
- fix issue93 (in pytest and pytest-xdist) avoid "delayed teardowns":
the final test in a test node will now run its teardown directly
instead of waiting for the end of the session. Thanks Dave Hunt for
the good reporting and feedback. The pytest_runtest_protocol as well
as the pytest_runtest_teardown hooks now have "nextitem" available
which will be None indicating the end of the test run.
Changes between 2.1.3 and 2.2.0 Changes between 2.1.3 and 2.2.0
---------------------------------------- ----------------------------------------

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.2.0' __version__ = '2.2.1.dev1'

View File

@ -121,16 +121,23 @@ def pytest_generate_tests(metafunc):
def pytest_itemstart(item, node=None): def pytest_itemstart(item, node=None):
""" (deprecated, use pytest_runtest_logstart). """ """ (deprecated, use pytest_runtest_logstart). """
def pytest_runtest_protocol(item): def pytest_runtest_protocol(item, nextitem):
""" implements the standard runtest_setup/call/teardown protocol including """ implements the runtest_setup/call/teardown protocol for
capturing exceptions and calling reporting hooks on the results accordingly. the given test item, including capturing exceptions and calling
reporting hooks.
:arg item: test item for which the runtest protocol is performed.
:arg nexitem: the scheduled-to-be-next test item (or None if this
is the end my friend). This argument is passed on to
:py:func:`pytest_runtest_teardown`.
:return boolean: True if no further hook implementations should be invoked. :return boolean: True if no further hook implementations should be invoked.
""" """
pytest_runtest_protocol.firstresult = True pytest_runtest_protocol.firstresult = True
def pytest_runtest_logstart(nodeid, location): def pytest_runtest_logstart(nodeid, location):
""" signal the start of a test run. """ """ signal the start of running a single test item. """
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
""" called before ``pytest_runtest_call(item)``. """ """ called before ``pytest_runtest_call(item)``. """
@ -138,8 +145,14 @@ def pytest_runtest_setup(item):
def pytest_runtest_call(item): def pytest_runtest_call(item):
""" called to execute the test ``item``. """ """ called to execute the test ``item``. """
def pytest_runtest_teardown(item): def pytest_runtest_teardown(item, nextitem):
""" called after ``pytest_runtest_call``. """ """ called after ``pytest_runtest_call``.
:arg nexitem: the scheduled-to-be-next test item (None if no further
test item is scheduled). This argument can be used to
perform exact teardowns, i.e. calling just enough finalizers
so that nextitem only needs to call setup-functions.
"""
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object """ return a :py:class:`_pytest.runner.TestReport` object

View File

@ -106,8 +106,12 @@ 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.items: for i, item in enumerate(session.items):
item.config.hook.pytest_runtest_protocol(item=item) try:
nextitem = session.items[i+1]
except IndexError:
nextitem = None
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
if session.shouldstop: if session.shouldstop:
raise session.Interrupted(session.shouldstop) raise session.Interrupted(session.shouldstop)
return True return True

View File

@ -59,33 +59,20 @@ class NodeInfo:
def __init__(self, location): def __init__(self, location):
self.location = location self.location = location
def perform_pending_teardown(config, nextitem): def pytest_runtest_protocol(item, 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( item.ihook.pytest_runtest_logstart(
nodeid=item.nodeid, location=item.location, nodeid=item.nodeid, location=item.location,
) )
runtestprotocol(item, teardowndelayed=True) runtestprotocol(item, nextitem=nextitem)
return True return True
def runtestprotocol(item, log=True, teardowndelayed=False): def runtestprotocol(item, log=True, nextitem=None):
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))
if teardowndelayed: reports.append(call_and_report(item, "teardown", log,
item.config._pendingteardown = item, log nextitem=nextitem))
else:
reports.append(call_and_report(item, "teardown", log))
return reports return reports
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
@ -94,17 +81,8 @@ def pytest_runtest_setup(item):
def pytest_runtest_call(item): def pytest_runtest_call(item):
item.runtest() item.runtest()
def pytest_runtest_teardown(item): def pytest_runtest_teardown(item, nextitem):
item.session._setupstate.teardown_exact(item) item.session._setupstate.teardown_exact(item, nextitem)
def pytest__teardown_final(session):
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): def pytest_report_teststatus(report):
if report.when in ("setup", "teardown"): if report.when in ("setup", "teardown"):
@ -120,18 +98,18 @@ def pytest_report_teststatus(report):
# #
# Implementation # Implementation
def call_and_report(item, when, log=True): def call_and_report(item, when, log=True, **kwds):
call = call_runtest_hook(item, when) call = call_runtest_hook(item, when, **kwds)
hook = item.ihook hook = item.ihook
report = hook.pytest_runtest_makereport(item=item, call=call) report = hook.pytest_runtest_makereport(item=item, call=call)
if log: if log:
hook.pytest_runtest_logreport(report=report) hook.pytest_runtest_logreport(report=report)
return report return report
def call_runtest_hook(item, when): def call_runtest_hook(item, when, **kwds):
hookname = "pytest_runtest_" + when hookname = "pytest_runtest_" + when
ihook = getattr(item.ihook, hookname) ihook = getattr(item.ihook, hookname)
return CallInfo(lambda: ihook(item=item), when=when) return CallInfo(lambda: ihook(item=item, **kwds), when=when)
class CallInfo: class CallInfo:
""" Result/Exception info a function invocation. """ """ Result/Exception info a function invocation. """
@ -338,9 +316,8 @@ class SetupState(object):
self._teardown_with_finalization(None) self._teardown_with_finalization(None)
assert not self._finalizers assert not self._finalizers
def teardown_exact(self, item): def teardown_exact(self, item, nextitem):
colitem = item.nextitem needed_collectors = nextitem and nextitem.listchain() or []
needed_collectors = colitem and colitem.listchain() or []
self._teardown_towards(needed_collectors) self._teardown_towards(needed_collectors)
def _teardown_towards(self, needed_collectors): def _teardown_towards(self, needed_collectors):

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', version='2.2.1.dev1',
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

@ -689,7 +689,7 @@ class TestRequest:
teardownlist = item.getparent(pytest.Module).obj.teardownlist teardownlist = item.getparent(pytest.Module).obj.teardownlist
ss = item.session._setupstate ss = item.session._setupstate
assert not teardownlist assert not teardownlist
ss.teardown_exact(item) ss.teardown_exact(item, None)
print(ss.stack) print(ss.stack)
assert teardownlist == [1] assert teardownlist == [1]

View File

@ -30,9 +30,9 @@ class TestSetupState:
def test_teardown_exact_stack_empty(self, testdir): def test_teardown_exact_stack_empty(self, testdir):
item = testdir.getitem("def test_func(): pass") item = testdir.getitem("def test_func(): pass")
ss = runner.SetupState() ss = runner.SetupState()
ss.teardown_exact(item) ss.teardown_exact(item, None)
ss.teardown_exact(item) ss.teardown_exact(item, None)
ss.teardown_exact(item) ss.teardown_exact(item, None)
def test_setup_fails_and_failure_is_cached(self, testdir): def test_setup_fails_and_failure_is_cached(self, testdir):
item = testdir.getitem(""" item = testdir.getitem("""