diff --git a/CHANGELOG b/CHANGELOG index f2becd4..6471f8f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ 1.2 ------------------------- +- fix issue79: sessionfinish/teardown hooks more systematically + on the slave side + - introduce a new data input/output mechanism to allow the master side to send and receive data from a slave. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 441ae97..4e4a96b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -152,3 +152,23 @@ class TestDistribution: "*1 passed*" ]) assert result.ret == 0 + + def test_keyboardinterrupt_hooks_issue79(self, testdir): + testdir.makepyfile(__init__="", test_one=""" + def test_hello(): + raise KeyboardInterrupt() + """) + testdir.makeconftest(""" + def pytest_sessionfinish(session): + if hasattr(session.config, 'slaveoutput'): + session.config.slaveoutput['s2'] = 42 + def pytest_testnodedown(node, error): + assert node.slaveoutput['s2'] == 42 + print "s2call-finished" + """) + args = ["-n1"] + result = testdir.runpytest(*args) + s = result.stdout.str() + assert result.ret + assert 'SIGINT' in s + assert 's2call' in s diff --git a/testing/test_txnode.py b/testing/test_txnode.py index 0358804..51f5022 100644 --- a/testing/test_txnode.py +++ b/testing/test_txnode.py @@ -81,7 +81,7 @@ class TestMasterSlaveConnection: node.send(123) # invalid item kwargs = mysetup.geteventargs("pytest_testnodedown") assert kwargs['node'] is node - assert isinstance(kwargs['error'], execnet.RemoteError) + #assert isinstance(kwargs['error'], execnet.RemoteError) def test_crash_killed(self, testdir, mysetup): if not hasattr(py.std.os, 'kill'): diff --git a/xdist/dsession.py b/xdist/dsession.py index 4ffd0c2..6a89028 100644 --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -132,15 +132,16 @@ class DSession(Session): call(**kwargs) # termination conditions - if ((loopstate.testsfailed and self.config.option.exitfirst) or + if (not self.node2pending or + (loopstate.testsfailed and self.config.option.exitfirst) or (not self.item2nodes and not colitems and not self.queue.qsize())): if self.config.option.exitfirst: raise ExitFirstInterrupt() self.triggershutdown() loopstate.shuttingdown = True - elif not self.node2pending: - loopstate.exitstatus = outcome.EXIT_NOHOSTS - + if not self.node2pending: + loopstate.exitstatus = outcome.EXIT_NOHOSTS + def loop_once_shutdown(self, loopstate): # once we are in shutdown mode we dont send # events other than HostDown upstream diff --git a/xdist/txnode.py b/xdist/txnode.py index bd9a995..9aee27f 100644 --- a/xdist/txnode.py +++ b/xdist/txnode.py @@ -3,7 +3,7 @@ """ import py from xdist.mypickle import PickleChannel -from py._test import outcome +from py._test.session import Session class TXNode(object): """ Represents a Test Execution environment in the controlling process. @@ -45,7 +45,7 @@ class TXNode(object): err = self.channel._getremoteerror() if not self._down: if not err or isinstance(err, EOFError): - err = "Not properly terminated" + err = "Not properly terminated" # lost connection? self.notify("pytest_testnodedown", node=self, error=err) self._down = True return @@ -55,7 +55,8 @@ class TXNode(object): elif eventname == "slavefinished": self._down = True self.slaveoutput = kwargs['slaveoutput'] - self.notify("pytest_testnodedown", error=None, node=self) + error = kwargs['error'] + self.notify("pytest_testnodedown", error=error, node=self) elif eventname in ("pytest_runtest_logreport", "pytest__teardown_final_logerror"): kwargs['report'].node = self @@ -92,8 +93,16 @@ def install_slave(node): from xdist.txnode import SlaveSession channel.send("basicimport") channel = PickleChannel(channel) - session = SlaveSession(channel) - session.run() + import py + config, slaveinput, basetemp, nodeid = channel.receive() + config.slaveinput = slaveinput + config.slaveoutput = {} + if basetemp: + config.basetemp = py.path.local(basetemp) + config.nodeid = nodeid + config.pluginmanager.do_configure(config) + session = SlaveSession(config, channel, nodeid) + session.dist_main() """) channel.receive() channel = PickleChannel(channel) @@ -108,9 +117,11 @@ def install_slave(node): channel.send((config, node.slaveinput, basetemp, node.gateway.id)) return channel -class SlaveSession(object): - def __init__(self, channel): +class SlaveSession(Session): + def __init__(self, config, channel, nodeid): self.channel = channel + self.nodeid = nodeid + super(SlaveSession, self).__init__(config=config) def __repr__(self): return "<%s channel=%s>" %(self.__class__.__name__, self.channel) @@ -124,39 +135,31 @@ class SlaveSession(object): def pytest__teardown_final_logerror(self, report): self.sendevent("pytest__teardown_final_logerror", report=report) - def run(self): - channel = self.channel - self.config, slaveinput, basetemp, self.nodeid = channel.receive() - if basetemp: - self.config.basetemp = py.path.local(basetemp) - self.config.slaveinput = slaveinput - self.config.slaveoutput = {} - self.config.pluginmanager.do_configure(self.config) - self.config.pluginmanager.register(self) + def pytest_keyboard_interrupt(self, excinfo): + self._slaveerror = "SIGINT" + + def pytest_internalerror(self, excrepr): + self._slaveerror = "internal-error" + self.sendevent("pytest_internalerror", excrepr=excrepr) + + def dist_main(self): self.runner = self.config.pluginmanager.getplugin("pytest_runner") self.sendevent("slaveready") - try: - self.config.hook.pytest_sessionstart(session=self) - while 1: - task = channel.receive() - if task is None: - break - if isinstance(task, list): - for item in task: - self.run_single(item=item) - else: - self.run_single(item=task) - self.config.hook.pytest_sessionfinish( - session=self, - exitstatus=outcome.EXIT_OK) - except KeyboardInterrupt: - raise - except: - er = py.code.ExceptionInfo().getrepr(funcargs=True, showlocals=True) - self.sendevent("pytest_internalerror", excrepr=er) - raise - else: - self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput) + self.main(None) + error = getattr(self, '_slaveerror', None) + self.sendevent("slavefinished", error=error, + slaveoutput=self.config.slaveoutput) + + def _mainloop(self, colitems): + while 1: + task = self.channel.receive() + if task is None: + break + if isinstance(task, list): + for item in task: + self.run_single(item=item) + else: + self.run_single(item=task) def run_single(self, item): call = self.runner.CallInfo(item._reraiseunpicklingproblem, when='setup')