fix sessionstart/sessionfinish handling at the slave side, set "session.nodeid" to id of the slave and make sure "final" teardown failures are reported nicely. fixes issue66.
--HG-- branch : trunk
This commit is contained in:
parent
b6909e61a0
commit
3296939eda
|
@ -64,6 +64,10 @@ Changes between 1.X and 1.1.1
|
||||||
|
|
||||||
- fix assert reinterpreation that sees a call containing "keyword=..."
|
- fix assert reinterpreation that sees a call containing "keyword=..."
|
||||||
|
|
||||||
|
- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish
|
||||||
|
hooks on slaves during dist-testing, report module/session teardown
|
||||||
|
hooks correctly.
|
||||||
|
|
||||||
- fix issue65: properly handle dist-testing if no
|
- fix issue65: properly handle dist-testing if no
|
||||||
execnet/py lib installed remotely.
|
execnet/py lib installed remotely.
|
||||||
|
|
||||||
|
|
10
ISSUES.txt
10
ISSUES.txt
|
@ -42,16 +42,6 @@ test: testing/pytest/dist/test_dsession.py - test_terminate_on_hanging_node
|
||||||
Call gateway group termination with a small timeout if available.
|
Call gateway group termination with a small timeout if available.
|
||||||
Should make dist-testing less likely to leave lost processes.
|
Should make dist-testing less likely to leave lost processes.
|
||||||
|
|
||||||
dist-testing: fix session hook / setup calling
|
|
||||||
-----------------------------------------------------
|
|
||||||
tags: bug 1.2
|
|
||||||
|
|
||||||
Currently pytest_sessionstart and finish are called
|
|
||||||
on the master node and not on the slaves. Call
|
|
||||||
it on slaves and provide a session.nodeid which defaults
|
|
||||||
to None for the master and contains the gateway id
|
|
||||||
for slaves.
|
|
||||||
|
|
||||||
have --report=xfailed[-detail] report the actual tracebacks
|
have --report=xfailed[-detail] report the actual tracebacks
|
||||||
------------------------------------------------------------------
|
------------------------------------------------------------------
|
||||||
tags: feature
|
tags: feature
|
||||||
|
|
|
@ -127,6 +127,12 @@ class DSession(Session):
|
||||||
elif eventname == "pytest_runtest_logreport":
|
elif eventname == "pytest_runtest_logreport":
|
||||||
# might be some teardown report
|
# might be some teardown report
|
||||||
self.config.hook.pytest_runtest_logreport(**kwargs)
|
self.config.hook.pytest_runtest_logreport(**kwargs)
|
||||||
|
elif eventname == "pytest_internalerror":
|
||||||
|
self.config.hook.pytest_internalerror(**kwargs)
|
||||||
|
loopstate.exitstatus = outcome.EXIT_INTERNALERROR
|
||||||
|
elif eventname == "pytest__teardown_final_logerror":
|
||||||
|
self.config.hook.pytest__teardown_final_logerror(**kwargs)
|
||||||
|
loopstate.exitstatus = outcome.EXIT_TESTSFAILED
|
||||||
if not self.node2pending:
|
if not self.node2pending:
|
||||||
# finished
|
# finished
|
||||||
if loopstate.testsfailed:
|
if loopstate.testsfailed:
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"""
|
"""
|
||||||
import py
|
import py
|
||||||
from py.impl.test.dist.mypickle import PickleChannel
|
from py.impl.test.dist.mypickle import PickleChannel
|
||||||
|
from py.impl.test import outcome
|
||||||
|
|
||||||
class TXNode(object):
|
class TXNode(object):
|
||||||
""" Represents a Test Execution environment in the controlling process.
|
""" Represents a Test Execution environment in the controlling process.
|
||||||
|
@ -55,12 +56,12 @@ class TXNode(object):
|
||||||
elif eventname == "slavefinished":
|
elif eventname == "slavefinished":
|
||||||
self._down = True
|
self._down = True
|
||||||
self.notify("pytest_testnodedown", error=None, node=self)
|
self.notify("pytest_testnodedown", error=None, node=self)
|
||||||
elif eventname == "pytest_runtest_logreport":
|
elif eventname in ("pytest_runtest_logreport",
|
||||||
rep = kwargs['report']
|
"pytest__teardown_final_logerror"):
|
||||||
rep.node = self
|
kwargs['report'].node = self
|
||||||
self.notify("pytest_runtest_logreport", report=rep)
|
self.notify(eventname, **kwargs)
|
||||||
else:
|
else:
|
||||||
self.notify(eventname, *args, **kwargs)
|
self.notify(eventname, **kwargs)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# should not land in receiver-thread
|
# should not land in receiver-thread
|
||||||
raise
|
raise
|
||||||
|
@ -99,7 +100,7 @@ def install_slave(gateway, config):
|
||||||
basetemp = py.path.local.make_numbered_dir(prefix="slave-",
|
basetemp = py.path.local.make_numbered_dir(prefix="slave-",
|
||||||
keep=0, rootdir=popenbase)
|
keep=0, rootdir=popenbase)
|
||||||
basetemp = str(basetemp)
|
basetemp = str(basetemp)
|
||||||
channel.send((config, basetemp))
|
channel.send((config, basetemp, gateway.id))
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
class SlaveNode(object):
|
class SlaveNode(object):
|
||||||
|
@ -115,9 +116,12 @@ class SlaveNode(object):
|
||||||
def pytest_runtest_logreport(self, report):
|
def pytest_runtest_logreport(self, report):
|
||||||
self.sendevent("pytest_runtest_logreport", report=report)
|
self.sendevent("pytest_runtest_logreport", report=report)
|
||||||
|
|
||||||
|
def pytest__teardown_final_logerror(self, report):
|
||||||
|
self.sendevent("pytest__teardown_final_logerror", report=report)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
channel = self.channel
|
channel = self.channel
|
||||||
self.config, basetemp = channel.receive()
|
self.config, basetemp, self.nodeid = channel.receive()
|
||||||
if basetemp:
|
if basetemp:
|
||||||
self.config.basetemp = py.path.local(basetemp)
|
self.config.basetemp = py.path.local(basetemp)
|
||||||
self.config.pluginmanager.do_configure(self.config)
|
self.config.pluginmanager.do_configure(self.config)
|
||||||
|
@ -125,22 +129,27 @@ class SlaveNode(object):
|
||||||
self.runner = self.config.pluginmanager.getplugin("pytest_runner")
|
self.runner = self.config.pluginmanager.getplugin("pytest_runner")
|
||||||
self.sendevent("slaveready")
|
self.sendevent("slaveready")
|
||||||
try:
|
try:
|
||||||
|
self.config.hook.pytest_sessionstart(session=self)
|
||||||
while 1:
|
while 1:
|
||||||
task = channel.receive()
|
task = channel.receive()
|
||||||
if task is None:
|
if task is None:
|
||||||
self.sendevent("slavefinished")
|
|
||||||
break
|
break
|
||||||
if isinstance(task, list):
|
if isinstance(task, list):
|
||||||
for item in task:
|
for item in task:
|
||||||
self.run_single(item=item)
|
self.run_single(item=item)
|
||||||
else:
|
else:
|
||||||
self.run_single(item=task)
|
self.run_single(item=task)
|
||||||
|
self.config.hook.pytest_sessionfinish(
|
||||||
|
session=self,
|
||||||
|
exitstatus=outcome.EXIT_OK)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
er = py.code.ExceptionInfo().getrepr(funcargs=True, showlocals=True)
|
er = py.code.ExceptionInfo().getrepr(funcargs=True, showlocals=True)
|
||||||
self.sendevent("pytest_internalerror", excrepr=er)
|
self.sendevent("pytest_internalerror", excrepr=er)
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
self.sendevent("slavefinished")
|
||||||
|
|
||||||
def run_single(self, item):
|
def run_single(self, item):
|
||||||
call = self.runner.CallInfo(item._checkcollectable, when='setup')
|
call = self.runner.CallInfo(item._checkcollectable, when='setup')
|
||||||
|
|
|
@ -13,10 +13,7 @@ Item = py.test.collect.Item
|
||||||
Collector = py.test.collect.Collector
|
Collector = py.test.collect.Collector
|
||||||
|
|
||||||
class Session(object):
|
class Session(object):
|
||||||
"""
|
nodeid = ""
|
||||||
Session drives the collection and running of tests
|
|
||||||
and generates test events for reporters.
|
|
||||||
"""
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.pluginmanager = config.pluginmanager # shortcut
|
self.pluginmanager = config.pluginmanager # shortcut
|
||||||
|
|
|
@ -68,6 +68,8 @@ def pytest_runtest_teardown(item):
|
||||||
def pytest__teardown_final(session):
|
def pytest__teardown_final(session):
|
||||||
call = CallInfo(session.config._setupstate.teardown_all, when="teardown")
|
call = CallInfo(session.config._setupstate.teardown_all, when="teardown")
|
||||||
if call.excinfo:
|
if call.excinfo:
|
||||||
|
ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir)
|
||||||
|
call.excinfo.traceback = ntraceback.filter()
|
||||||
rep = TeardownErrorReport(call.excinfo)
|
rep = TeardownErrorReport(call.excinfo)
|
||||||
return rep
|
return rep
|
||||||
|
|
||||||
|
|
|
@ -444,15 +444,62 @@ def test_teardownfails_one_function(testdir):
|
||||||
def test_terminate_on_hangingnode(testdir):
|
def test_terminate_on_hangingnode(testdir):
|
||||||
p = testdir.makeconftest("""
|
p = testdir.makeconftest("""
|
||||||
def pytest__teardown_final(session):
|
def pytest__teardown_final(session):
|
||||||
if session.nodeid: # running on slave
|
if session.nodeid == "my": # running on slave
|
||||||
import time
|
import time
|
||||||
time.sleep(2)
|
time.sleep(3)
|
||||||
""")
|
""")
|
||||||
result = testdir.runpytest(p, '--dist=each', '--tx=popen')
|
result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my')
|
||||||
assert result.duration < 2.0
|
assert result.duration < 2.0
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*0 passed*",
|
"*killed*my*",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_hooks(testdir):
|
||||||
|
testdir.makeconftest("""
|
||||||
|
import sys
|
||||||
|
def pytest_sessionstart(session):
|
||||||
|
sys.pytestsessionhooks = session
|
||||||
|
def pytest_sessionfinish(session):
|
||||||
|
f = open(session.nodeid or "master", 'w')
|
||||||
|
f.write("xy")
|
||||||
|
f.close()
|
||||||
|
# let's fail on the slave
|
||||||
|
if session.nodeid:
|
||||||
|
raise ValueError(42)
|
||||||
|
""")
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
import sys
|
||||||
|
def test_hello():
|
||||||
|
assert hasattr(sys, 'pytestsessionhooks')
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p, "--dist=each", "--tx=popen//id=my1")
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*ValueError*",
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
assert result.ret
|
||||||
|
d = result.parseoutcomes()
|
||||||
|
assert d['passed'] == 1
|
||||||
|
assert testdir.tmpdir.join("my1").check()
|
||||||
|
assert testdir.tmpdir.join("master").check()
|
||||||
|
|
||||||
|
def test_funcarg_teardown_failure(testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
def pytest_funcarg__myarg(request):
|
||||||
|
def teardown(val):
|
||||||
|
raise ValueError(val)
|
||||||
|
return request.cached_setup(setup=lambda: 42, teardown=teardown,
|
||||||
|
scope="module")
|
||||||
|
def test_hello(myarg):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p, "-n1")
|
||||||
|
assert result.ret
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*ValueError*42*",
|
||||||
|
"*1 passed*1 error*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue