From 3296939edaade233698e799eced591853b7fe725 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 11 Jan 2010 17:09:07 +0100 Subject: [PATCH] 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 --- CHANGELOG | 4 ++ ISSUES.txt | 10 ----- py/impl/test/dist/dsession.py | 6 +++ py/impl/test/dist/txnode.py | 25 ++++++++---- py/impl/test/session.py | 5 +-- py/plugin/pytest_runner.py | 2 + testing/pytest/dist/test_dsession.py | 57 +++++++++++++++++++++++++--- 7 files changed, 82 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d4126d1a3..224c6a215 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -64,6 +64,10 @@ Changes between 1.X and 1.1.1 - 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 execnet/py lib installed remotely. diff --git a/ISSUES.txt b/ISSUES.txt index 6a81bf05e..c0ee45d7c 100644 --- a/ISSUES.txt +++ b/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. 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 ------------------------------------------------------------------ tags: feature diff --git a/py/impl/test/dist/dsession.py b/py/impl/test/dist/dsession.py index 1ce0b52e4..c707e1b9e 100644 --- a/py/impl/test/dist/dsession.py +++ b/py/impl/test/dist/dsession.py @@ -127,6 +127,12 @@ class DSession(Session): elif eventname == "pytest_runtest_logreport": # might be some teardown report 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: # finished if loopstate.testsfailed: diff --git a/py/impl/test/dist/txnode.py b/py/impl/test/dist/txnode.py index ea76363bd..2c8b0441e 100644 --- a/py/impl/test/dist/txnode.py +++ b/py/impl/test/dist/txnode.py @@ -3,6 +3,7 @@ """ import py from py.impl.test.dist.mypickle import PickleChannel +from py.impl.test import outcome class TXNode(object): """ Represents a Test Execution environment in the controlling process. @@ -55,12 +56,12 @@ class TXNode(object): elif eventname == "slavefinished": self._down = True self.notify("pytest_testnodedown", error=None, node=self) - elif eventname == "pytest_runtest_logreport": - rep = kwargs['report'] - rep.node = self - self.notify("pytest_runtest_logreport", report=rep) + elif eventname in ("pytest_runtest_logreport", + "pytest__teardown_final_logerror"): + kwargs['report'].node = self + self.notify(eventname, **kwargs) else: - self.notify(eventname, *args, **kwargs) + self.notify(eventname, **kwargs) except KeyboardInterrupt: # should not land in receiver-thread raise @@ -99,7 +100,7 @@ def install_slave(gateway, config): basetemp = py.path.local.make_numbered_dir(prefix="slave-", keep=0, rootdir=popenbase) basetemp = str(basetemp) - channel.send((config, basetemp)) + channel.send((config, basetemp, gateway.id)) return channel class SlaveNode(object): @@ -115,9 +116,12 @@ class SlaveNode(object): def pytest_runtest_logreport(self, 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): channel = self.channel - self.config, basetemp = channel.receive() + self.config, basetemp, self.nodeid = channel.receive() if basetemp: self.config.basetemp = py.path.local(basetemp) self.config.pluginmanager.do_configure(self.config) @@ -125,22 +129,27 @@ class SlaveNode(object): 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: - self.sendevent("slavefinished") 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") def run_single(self, item): call = self.runner.CallInfo(item._checkcollectable, when='setup') diff --git a/py/impl/test/session.py b/py/impl/test/session.py index 6e417f5d0..4e649844c 100644 --- a/py/impl/test/session.py +++ b/py/impl/test/session.py @@ -13,10 +13,7 @@ Item = py.test.collect.Item Collector = py.test.collect.Collector class Session(object): - """ - Session drives the collection and running of tests - and generates test events for reporters. - """ + nodeid = "" def __init__(self, config): self.config = config self.pluginmanager = config.pluginmanager # shortcut diff --git a/py/plugin/pytest_runner.py b/py/plugin/pytest_runner.py index 8fadbf79a..78e6b5fb1 100644 --- a/py/plugin/pytest_runner.py +++ b/py/plugin/pytest_runner.py @@ -68,6 +68,8 @@ def pytest_runtest_teardown(item): def pytest__teardown_final(session): call = CallInfo(session.config._setupstate.teardown_all, when="teardown") if call.excinfo: + ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) + call.excinfo.traceback = ntraceback.filter() rep = TeardownErrorReport(call.excinfo) return rep diff --git a/testing/pytest/dist/test_dsession.py b/testing/pytest/dist/test_dsession.py index b013553bb..df135944f 100644 --- a/testing/pytest/dist/test_dsession.py +++ b/testing/pytest/dist/test_dsession.py @@ -440,19 +440,66 @@ def test_teardownfails_one_function(testdir): "*1 passed*1 error*" ]) -@py.test.mark.xfail +@py.test.mark.xfail def test_terminate_on_hangingnode(testdir): p = testdir.makeconftest(""" def pytest__teardown_final(session): - if session.nodeid: # running on slave + if session.nodeid == "my": # running on slave 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 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*", + ]) + +