diff --git a/CHANGELOG b/CHANGELOG index b0b6c15..56d576e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +1.4a1 +------------------------- + +- perform distributed testing related reporting in the plugin + rather than having dist-related code in the generic py.test + distribution + 1.3 ------------------------- diff --git a/setup.py b/setup.py index 8803b40..9c28eb8 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( packages = ['xdist'], entry_points = {'pytest11': ['xdist = xdist.plugin'],}, zip_safe=False, - install_requires = ['execnet>=1.0.6', 'py>=1.3.1'], + install_requires = ['execnet>=1.0.6', 'py>1.3.1'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index f086709..d4d9557 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,5 @@ import py +import sys class TestDistribution: def test_manytests_to_one_popen(self, testdir): @@ -173,3 +174,57 @@ class TestDistribution: assert result.ret assert 'SIGINT' in s assert 's2call' in s + + def test_keyboard_interrupt_dist(self, testdir): + # xxx could be refined to check for return code + p = testdir.makepyfile(""" + def test_sleep(): + import time + time.sleep(10) + """) + child = testdir.spawn_pytest("-n1") + child.expect(".*test session starts.*") + child.kill(2) # keyboard interrupt + child.expect(".*KeyboardInterrupt.*") + #child.expect(".*seconds.*") + child.close() + #assert ret == 2 + +class TestTerminalReporting: + def test_pass_skip_fail(self, testdir): + p = testdir.makepyfile(""" + import py + def test_ok(): + pass + def test_skip(): + py.test.skip("xx") + def test_func(): + assert 0 + """) + result = testdir.runpytest("-n1", "-v") + expected = [ + "*PASS*test_pass_skip_fail.py:2: *test_ok*", + "*SKIP*test_pass_skip_fail.py:4: *test_skip*", + "*FAIL*test_pass_skip_fail.py:6: *test_func*", + ] + for line in expected: + result.stdout.fnmatch_lines([line]) + result.stdout.fnmatch_lines([ + " def test_func():", + "> assert 0", + "E assert 0", + ]) + + def test_fail_platinfo(self, testdir): + p = testdir.makepyfile(""" + def test_func(): + assert 0 + """) + result = testdir.runpytest("-n1", "-v") + result.stdout.fnmatch_lines([ + "*FAIL*test_fail_platinfo.py:1: *test_func*", + "*popen*Python*", + " def test_func():", + "> assert 0", + "E assert 0", + ]) diff --git a/testing/conftest.py b/testing/conftest.py index 2f16fee..ea23fc9 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,7 +2,6 @@ import py import execnet pytest_plugins = "pytester" -option_report = 'skipped' #rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()] diff --git a/testing/test_remote.py b/testing/test_remote.py index 4510f73..330e221 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -132,7 +132,7 @@ class TestLooponFailing: x = 0 assert x == 1 """) - child = testdir.spawn_pytest("-f %s" % p) + child = testdir.spawn_pytest("-f %s --traceconfig" % p) child.expect("def test_one") child.expect("x == 1") child.expect("1 failed") diff --git a/tox.ini b/tox.ini index 6f87edc..f3be663 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] distshare={homedir}/.tox/distshare +envlist=py26,py31,py27,py25,py24 [tox:hudson] distshare={toxworkdir}/distshare sdistsrc={distshare}/pytest-xdist-**LATEST** @@ -9,15 +10,21 @@ changedir=testing deps= {distshare}/py-**LATEST** {distshare}/execnet-**LATEST** -commands=py.test -rsfxX --junitxml={envlogdir}/junit-{envname}.xml [] +commands= + py.test -rsfxX --tools-on-path \ + --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:py27] basepython=python2.7 [testenv:py26] basepython=python2.6 -[testenv:py26-py131] -basepython=python2.6 -deps= py==1.3.1 - execnet==1.0.6 +deps= + {distshare}/py-**LATEST** + {distshare}/execnet-**LATEST** + pexpect +#[testenv:py26-py132] +#basepython=python2.6 +#deps= py==1.3.2 +# execnet==1.0.6 [testenv:py25] basepython=python2.5 [testenv:py24] diff --git a/xdist/newhooks.py b/xdist/newhooks.py index fa52fa3..2d72fd6 100644 --- a/xdist/newhooks.py +++ b/xdist/newhooks.py @@ -20,6 +20,4 @@ def pytest_testnodedown(node, error): def pytest_rescheduleitems(items): """ reschedule Items from a node that went down. """ -def pytest_looponfailinfo(failreports, rootdirs): - """ info for repeating failing tests. """ diff --git a/xdist/plugin.py b/xdist/plugin.py index 7a22aa0..5e777f5 100644 --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -197,11 +197,23 @@ def pytest_configure(config): raise config.Error("--pdb incompatible with --looponfail.") from xdist.remote import LooponfailingSession config.setsessionclass(LooponfailingSession) + config._isdistsession = True elif val("dist") != "no": if usepdb: raise config.Error("--pdb incompatible with distributing tests.") from xdist.dsession import DSession config.setsessionclass(DSession) + config._isdistsession = True + + +def pytest_sessionstart(session): + config = session.config + if hasattr(config, '_isdistsession'): + if not config.pluginmanager.hasplugin("terminal") or \ + not config.pluginmanager.hasplugin("terminalreporter"): + return + trdist = TerminalDistReporter(config) + config.pluginmanager.register(trdist, "terminaldistreporter") def pytest_runtest_protocol(item): if item.config.getvalue("boxed"): @@ -244,3 +256,56 @@ def report_process_crash(item, result): from py._plugin.pytest_runner import ItemTestReport return ItemTestReport(item, excinfo=info, when="???") +class TerminalDistReporter: + def __init__(self, config): + self.gateway2info = {} + self.config = config + self.tplugin = config.pluginmanager.getplugin("terminal") + self.tr = config.pluginmanager.getplugin("terminalreporter") + + def write_line(self, msg): + self.tr.write_line(msg) + + def pytest_itemstart(self, __multicall__): + try: + __multicall__.methods.remove(self.tr.pytest_itemstart) + except KeyError: + pass + + def pytest_runtest_logreport(self, report): + if hasattr(report, 'node'): + report.headerlines.append(self.gateway2info.get( + report.node.gateway, + "node %r (platinfo not found? strange)")) + + def pytest_gwmanage_newgateway(self, gateway, platinfo): + #self.write_line("%s instantiated gateway from spec %r" %(gateway.id, gateway.spec._spec)) + d = {} + d['version'] = self.tplugin.repr_pythonversion(platinfo.version_info) + d['id'] = gateway.id + d['spec'] = gateway.spec._spec + d['platform'] = platinfo.platform + if self.config.option.verbose: + d['extra'] = "- " + platinfo.executable + else: + d['extra'] = "" + d['cwd'] = platinfo.cwd + infoline = ("[%(id)s] %(spec)s -- platform %(platform)s, " + "Python %(version)s " + "cwd: %(cwd)s" + "%(extra)s" % d) + self.write_line(infoline) + self.gateway2info[gateway] = infoline + + def pytest_testnodeready(self, node): + self.write_line("[%s] txnode ready to receive tests" %(node.gateway.id,)) + + def pytest_testnodedown(self, node, error): + if not error: + return + self.write_line("[%s] node down, error: %s" %(node.gateway.id, error)) + + def pytest_rescheduleitems(self, items): + if self.config.option.debug: + self.write_sep("!", "RESCHEDULING %s " %(items,)) + diff --git a/xdist/remote.py b/xdist/remote.py index 2df00c8..7cb0076 100644 --- a/xdist/remote.py +++ b/xdist/remote.py @@ -169,8 +169,21 @@ def slave_runsession(channel, config, fullwidth, hasmarkup): DEBUG("SLAVE: starting session.main()") session.main(colitems) - session.config.hook.pytest_looponfailinfo( + repr_pytest_looponfailinfo( failreports=list(failreports), rootdirs=[config.topdir]) rootcol = session.config._rootcol channel.send([rootcol.totrail(rep.getnode()) for rep in failreports]) + + +def repr_pytest_looponfailinfo(failreports, rootdirs): + tr = py.io.TerminalWriter() + if failreports: + tr.sep("#", "LOOPONFAILING", red=True) + for report in failreports: + loc = report._getcrashline() + if loc: + tr.line(loc, red=True) + tr.sep("#", "waiting for changes") + for rootdir in rootdirs: + tr.line("### Watching: %s" %(rootdir,), bold=True)