Compare commits

..

47 Commits
1.0 ... 1.4

Author SHA1 Message Date
holger krekel
feaf840fab just use the released version fo execnet 2010-07-07 18:33:27 +02:00
holger krekel
f112ad6cfb typo 2010-07-07 18:14:28 +02:00
holger krekel
71771fbdc6 improved versioing 2010-07-07 18:12:43 +02:00
holger krekel
c0b2df1a77 show detailed gateway setup info only with "--verbose" or "-v" 2010-07-07 16:44:12 +02:00
holger krekel
9aa1df89ee bump to xdist 1.4 2010-07-07 14:52:40 +02:00
holger krekel
67e0cd463c add a test created from jgustak (but add the related env feature to execnet where it
is separately tested)
2010-07-07 14:33:24 +02:00
holger krekel
fb31aa882d integrate xdist related reporting into the plugin 2010-07-07 12:43:19 +02:00
holger krekel
51b0ba55e9 don't test deprecation warnings on python2.7 2010-07-06 15:01:40 +02:00
holger krekel
d0ccb5743d refining tests for jython and python2.7 2010-07-06 13:50:03 +02:00
holger krekel
fa7da18d9b using proper distshare dir for hudson 2010-07-06 12:56:15 +02:00
holger krekel
cf36bf8bf2 bumping version 2010-07-06 12:51:09 +02:00
holger krekel
55e3d27820 adding a tox.ini file 2010-07-06 12:50:06 +02:00
holger krekel
a17bc05c5a merge 2010-06-27 14:00:01 +02:00
holger krekel
e5e23fd278 add a test for issue57 which currently needs to be fixed on py-trunk though 2010-05-26 18:52:58 +02:00
holger krekel
43c0591c55 Added tag 1.3 for changeset e6c4ce20db4b 2010-05-25 21:26:09 +02:00
holger krekel
7bf3c7f029 bump version number 2010-05-25 21:26:06 +02:00
holger krekel
e48dcda834 Added tag 1.3 for changeset eaf8b1cb7c31 2010-05-25 21:23:29 +02:00
holger krekel
faa03ad601 merge 2010-05-25 21:23:23 +02:00
holger krekel
e0f61e4fa2 merge 2010-05-25 21:00:42 +02:00
holger krekel
f7be994848 adapt for py-trunk/1.3.1 changes wrt --exitfirst -> --maxfailures 2010-05-25 16:59:08 +02:00
holger krekel
7b19de5450 Added tag 1.2 for changeset 56d8e5280be2 2010-05-25 12:02:29 +02:00
holger krekel
0c4f5eced2 fix conftest related documentation 2010-05-18 18:40:48 +02:00
holger krekel
e593841a70 fix looponfailing issue to always run against the newest version of the source 2010-05-17 17:32:34 +02:00
holger krekel
43b693258f xdist is now somewhat stable after all the fixes, i'd say. 2010-05-05 21:21:39 +02:00
holger krekel
215e0e6149 modify CHANGELOG 2010-05-05 20:24:34 +02:00
holger krekel
a7ff9d751b require execnet-1.0.6 - it's release already 2010-05-05 20:19:30 +02:00
holger krekel
95a87e874d add python3 classifier 2010-05-05 14:25:59 +02:00
holger krekel
f18c78a118 adapt to new 1.3.0 hook registration 2010-05-02 16:44:59 +02:00
holger krekel
40277fbf7f require py-1.3.0 for xdist plugin 2010-05-02 15:07:00 +02:00
holger krekel
2b20e40805 update CHANGELOG 2010-04-30 16:28:10 +02:00
holger krekel
45f7787fe2 fixing a (likely) race condition when simultanous pickling/unpickling can leave
inconsistent memo states.  Rather than adding locks, the fix actually simplifies the code by
keeping the pickling/unpickling memo's in sync directly and more efficiently, also removing
a long outstanding XXX.
2010-04-30 15:56:48 +02:00
holger krekel
1bc7812dcc adapt for fnmatch_lines changes on trunk 2010-04-29 00:47:11 +02:00
holger krekel
bdc3f9bf53 fix python3 issues and add info to changelog 2010-04-27 21:26:35 +02:00
holger krekel
b6cdbf46a6 adapt for changed py-trunk, new outcome exceptions 2010-04-27 21:16:47 +02:00
holger krekel
bd254674f9 fix py3 print issue 2010-04-26 18:57:08 +02:00
holger krekel
9c74f59e55 fixes issue79 - call hooks more systematically on slave nodes and also in the case of SIGINT
reorganize internal session code to share more code with the "normal" non-distributed session
2010-04-26 18:35:53 +02:00
holger krekel
25cb514331 internally rename SlaveNode to SlaveSession which fits better 2010-04-26 15:48:31 +02:00
holger krekel
e993a6b079 add to changelog 2010-04-26 10:13:15 +02:00
Meme Dough
8eecf6e1d2 Fix description. 2010-04-26 14:54:55 +10:00
holger krekel
9abaae8778 * introduce explicit pytest_configure_node hook for node configuration,
simplifying implementation of slaveinput/slaveoutput data exchange
* only require the already released execnet-1.0.5
2010-04-22 17:00:03 +02:00
Meme Dough
8d6bd3ecde Slave data stored on config for slave and node for master. 2010-04-22 22:21:58 +10:00
holger krekel
a1ab548cb3 declare xdist-related hooks directly here (requires a recent py-trunk) 2010-04-22 11:58:13 +02:00
Meme Dough
e7a47ae911 Added support for exchanging data between master and slave. 2010-04-21 22:33:09 +10:00
holger krekel
3a8ead3c82 Added tag 1.1 for changeset eca7ce17eabf 2010-02-07 13:47:00 +01:00
holger krekel
8f2c3fb04e higher test-timeouts for slower nightly run machines 2010-01-21 20:07:41 +01:00
holger krekel
4a57dfb648 fix an indefinite hang which would wait for events although no events
are pending - this happened if items arrive very quickly while
the "reschedule-event" tried unconditionally avoiding a busy-loop
and not schedule new work.
2010-01-19 14:59:22 +01:00
holger krekel
bfefd6400b Added tag 1.0 for changeset 42c6503ee48f 2010-01-18 16:18:08 +01:00
21 changed files with 533 additions and 135 deletions

View File

@@ -19,3 +19,4 @@ dist/
pytest_xdist.egg-info pytest_xdist.egg-info
issue/ issue/
3rdparty/ 3rdparty/
.tox

6
.hgtags Normal file
View File

@@ -0,0 +1,6 @@
42c6503ee48fae9c4c96d406afb12bfc86f15803 1.0
eca7ce17eabf296983c36812c8b8be901e7055a3 1.1
56d8e5280be224a0ad3220a9deed55334710bd23 1.2
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
eaf8b1cb7c312883598677231be5bbeea3b5c127 1.3

View File

@@ -1,3 +1,46 @@
1.4
-------------------------
- perform distributed testing related reporting in the plugin
rather than having dist-related code in the generic py.test
distribution
- depend on execnet-1.0.7 which adds "env1:NAME=value" keys to
gateway specification strings.
- show detailed gateway setup and platform information only when
"-v" or "--verbose" is specified.
1.3
-------------------------
- fix --looponfailing - it would not actually run against the fully changed
source tree when initial conftest files load application state.
- adapt for py-1.3.1's new --maxfailure option
1.2
-------------------------
- fix issue79: sessionfinish/teardown hooks are now called 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.
- fix race condition in underlying pickling/unpickling handling
- use and require new register hooks facility of py.test>=1.3.0
- require improved execnet>=1.0.6 because of various race conditions
that can arise in xdist testing modes.
- fix some python3 related pickling related race conditions
- fix PyPI description
1.1
-------------------------
- fix an indefinite hang which would wait for events although no events
are pending - this happened if items arrive very quickly while
the "reschedule-event" tried unconditionally avoiding a busy-loop
and not schedule new work.
1.0 1.0
------------------------- -------------------------

View File

@@ -7,11 +7,12 @@ for the new options.
""" """
from setuptools import setup from setuptools import setup
from xdist import __version__
setup( setup(
name="pytest-xdist", name="pytest-xdist",
version="1.0", version=__version__,
description='py.test figleaf coverage plugin', description='py.test xdist plugin for distributed testing and loop-on-failing modes',
long_description=__doc__, long_description=__doc__,
license='GPLv2 or later', license='GPLv2 or later',
author='holger krekel and contributors', author='holger krekel and contributors',
@@ -21,9 +22,9 @@ setup(
packages = ['xdist'], packages = ['xdist'],
entry_points = {'pytest11': ['xdist = xdist.plugin'],}, entry_points = {'pytest11': ['xdist = xdist.plugin'],},
zip_safe=False, zip_safe=False,
install_requires = ['execnet>=1.0.3', 'py>=1.2.0'], install_requires = ['execnet>=1.0.7', 'py>1.3.1'],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License (GPL)',
'Operating System :: POSIX', 'Operating System :: POSIX',
@@ -33,5 +34,6 @@ setup(
'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Quality Assurance',
'Topic :: Utilities', 'Topic :: Utilities',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3',
], ],
) )

View File

@@ -1,4 +1,5 @@
import py import py
import sys
class TestDistribution: class TestDistribution:
def test_manytests_to_one_popen(self, testdir): def test_manytests_to_one_popen(self, testdir):
@@ -14,7 +15,7 @@ class TestDistribution:
py.test.skip("hello") py.test.skip("hello")
""", """,
) )
result = testdir.runpytest(p1, '-d', '--tx=popen', '--tx=popen') result = testdir.runpytest(p1, "-v", '-d', '--tx=popen', '--tx=popen')
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*0*popen*Python*", "*0*popen*Python*",
"*1*popen*Python*", "*1*popen*Python*",
@@ -38,7 +39,7 @@ class TestDistribution:
testdir.makeconftest(""" testdir.makeconftest("""
option_tx = 'popen popen popen'.split() option_tx = 'popen popen popen'.split()
""") """)
result = testdir.runpytest(p1, '-d') result = testdir.runpytest(p1, '-d', "-v")
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*0*popen*Python*", "*0*popen*Python*",
"*1*popen*Python*", "*1*popen*Python*",
@@ -47,6 +48,7 @@ class TestDistribution:
]) ])
assert result.ret == 1 assert result.ret == 1
@py.test.mark.xfail("sys.platform.startswith('java')")
def test_dist_tests_with_crash(self, testdir): def test_dist_tests_with_crash(self, testdir):
if not hasattr(py.std.os, 'kill'): if not hasattr(py.std.os, 'kill'):
py.test.skip("no os.kill") py.test.skip("no os.kill")
@@ -68,7 +70,7 @@ class TestDistribution:
os.kill(os.getpid(), 15) os.kill(os.getpid(), 15)
""" """
) )
result = testdir.runpytest(p1, '-d', '--tx=3*popen') result = testdir.runpytest(p1, "-v", '-d', '--tx=3*popen')
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*popen*Python*", "*popen*Python*",
"*popen*Python*", "*popen*Python*",
@@ -85,7 +87,7 @@ class TestDistribution:
subdir.ensure("__init__.py") subdir.ensure("__init__.py")
p = subdir.join("test_one.py") p = subdir.join("test_one.py")
p.write("def test_5(): assert not __file__.startswith(%r)" % str(p)) p.write("def test_5(): assert not __file__.startswith(%r)" % str(p))
result = testdir.runpytest("-d", "--rsyncdir=%(subdir)s" % locals(), result = testdir.runpytest("-v", "-d", "--rsyncdir=%(subdir)s" % locals(),
"--tx=popen//chdir=%(dest)s" % locals(), p) "--tx=popen//chdir=%(dest)s" % locals(), p)
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
@@ -110,10 +112,119 @@ class TestDistribution:
print("%s...%s" % sys.version_info[:2]) print("%s...%s" % sys.version_info[:2])
assert 0 assert 0
""") """)
args = ["--dist=each"] args = ["--dist=each", "-v"]
args += ["--tx", "popen//python=%s" % interpreters[0]] args += ["--tx", "popen//python=%s" % interpreters[0]]
args += ["--tx", "popen//python=%s" % interpreters[1]] args += ["--tx", "popen//python=%s" % interpreters[1]]
result = testdir.runpytest(*args) result = testdir.runpytest(*args)
s = result.stdout.str() s = result.stdout.str()
assert "2.4" in s assert "2.4" in s
assert "2.5" in s assert "2.5" in s
def test_data_exchange(self, testdir):
c1 = testdir.makeconftest("""
# This hook only called on master.
def pytest_configure_node(node):
node.slaveinput['a'] = 42
node.slaveinput['b'] = 7
# This hook only takes action on slave.
def pytest_configure(config):
if hasattr(config, 'slaveinput'):
a = config.slaveinput['a']
b = config.slaveinput['b']
r = a + b
config.slaveoutput['r'] = r
# This hook only called on master.
def pytest_testnodedown(node, error):
node.config.calc_result = node.slaveoutput['r']
# This hook only takes action on master.
def pytest_terminal_summary(terminalreporter):
if not hasattr(terminalreporter.config, 'slaveinput'):
calc_result = terminalreporter.config.calc_result
terminalreporter._tw.sep('-',
'calculated result is %s' % calc_result)
""")
p1 = testdir.makepyfile("def test_func(): pass")
result = testdir.runpytest("-v", p1, '-d', '--tx=popen')
result.stdout.fnmatch_lines([
"*popen*Python*",
"*calculated result is 49*",
"*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
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",
])

View File

@@ -2,7 +2,6 @@ import py
import execnet import execnet
pytest_plugins = "pytester" pytest_plugins = "pytester"
option_report = 'skipped'
#rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()] #rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()]

View File

@@ -8,7 +8,7 @@ def test_functional_boxed(testdir):
os.kill(os.getpid(), 15) os.kill(os.getpid(), 15)
""") """)
result = testdir.runpytest(p1, "--boxed") result = testdir.runpytest(p1, "--boxed")
assert result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*CRASHED*", "*CRASHED*",
"*1 failed*" "*1 failed*"
]) ])

View File

@@ -1,6 +1,7 @@
import py import py
def test_dist_conftest_options(testdir): @py.test.mark.xfail("sys.version_info[:2] == (2,7)")
def test_dist_conftest_options(testdir, recwarn):
p1 = testdir.tmpdir.ensure("dir", 'p1.py') p1 = testdir.tmpdir.ensure("dir", 'p1.py')
p1.dirpath("__init__.py").write("") p1.dirpath("__init__.py").write("")
p1.dirpath("conftest.py").write(py.code.Source(""" p1.dirpath("conftest.py").write(py.code.Source("""

View File

@@ -1,5 +1,5 @@
from xdist.dsession import DSession from xdist.dsession import DSession
from py._test import outcome from py._test import session as outcome
import py import py
import execnet import execnet
@@ -110,6 +110,7 @@ class TestDSession:
assert name == "pytest_rescheduleitems" assert name == "pytest_rescheduleitems"
assert kwargs['items'] == [item] assert kwargs['items'] == [item]
def test_keyboardinterrupt(self, testdir): def test_keyboardinterrupt(self, testdir):
item = testdir.getitem("def test_func(): pass") item = testdir.getitem("def test_func(): pass")
session = DSession(item.config) session = DSession(item.config)
@@ -134,14 +135,22 @@ class TestDSession:
loopstate = session._initloopstate([]) loopstate = session._initloopstate([])
session.queueevent("pytest_rescheduleitems", items=[item]) session.queueevent("pytest_rescheduleitems", items=[item])
session.loop_once(loopstate) session.loop_once(loopstate)
# check that RescheduleEvents are not immediately # we need to do work because nothing is pending / we would not wake up
# rescheduled if there are no nodes assert loopstate.dowork == True
session.node2pending[node].append(item)
session.queueevent("pytest_rescheduleitems", items=[item])
session.loop_once(loopstate)
# now we want to not directly trigger work again to avoid busy-wait
assert loopstate.dowork == False assert loopstate.dowork == False
session.queueevent(None) session.queueevent(None)
session.loop_once(loopstate) session.loop_once(loopstate)
session.queueevent(None) session.queueevent(None)
session.loop_once(loopstate) session.loop_once(loopstate)
assert node.sent == [item] assert node.sent == [item, item]
session.queueevent("pytest_runtest_logreport", report=run(item, node))
session.loop_once(loopstate)
session.queueevent("pytest_runtest_logreport", report=run(item, node)) session.queueevent("pytest_runtest_logreport", report=run(item, node))
session.loop_once(loopstate) session.loop_once(loopstate)
assert loopstate.shuttingdown assert loopstate.shuttingdown
@@ -250,7 +259,7 @@ class TestDSession:
def test_pass(): def test_pass():
pass pass
""") """)
modcol.config.option.exitfirst = True modcol.config.option.maxfail = 1
session = DSession(modcol.config) session = DSession(modcol.config)
node = MockNode() node = MockNode()
session.addnode(node) session.addnode(node)
@@ -262,12 +271,44 @@ class TestDSession:
# run tests ourselves and produce reports # run tests ourselves and produce reports
ev1 = run(items[0], node, "fail") ev1 = run(items[0], node, "fail")
ev2 = run(items[1], node, None) ev2 = run(items[1], node, None)
session.queueevent("pytest_runtest_logreport", report=ev1)
session.queueevent("pytest_runtest_logreport", report=ev2)
# now call the loop
loopstate = session._initloopstate(items)
py.test.raises(session.Interrupted, "session.loop_once(loopstate)")
assert loopstate.testsfailed
#assert loopstate.shuttingdown
def test_maxfail(self, testdir):
modcol = testdir.getmodulecol("""
def test_fail1():
assert 0
def test_fail2():
assert 0
def test_pass():
pass
""")
modcol.config.option.maxfail = 2
session = DSession(modcol.config)
node = MockNode()
session.addnode(node)
items = modcol.config.hook.pytest_make_collect_report(collector=modcol).result
# trigger testing - this sends tests to the node
session.triggertesting(items)
# run tests ourselves and produce reports
ev1 = run(items[0], node, "fail")
ev2 = run(items[1], node, "fail")
session.queueevent("pytest_runtest_logreport", report=ev1) # a failing one session.queueevent("pytest_runtest_logreport", report=ev1) # a failing one
session.queueevent("pytest_runtest_logreport", report=ev2) session.queueevent("pytest_runtest_logreport", report=ev2)
# now call the loop # now call the loop
loopstate = session._initloopstate(items) loopstate = session._initloopstate(items)
from xdist.dsession import ExitFirstInterrupt try:
py.test.raises(ExitFirstInterrupt, "session.loop_once(loopstate)") session.loop_once(loopstate)
except session.Interrupted:
py.test.fail("raised Interrupted but shouildn't")
py.test.raises(session.Interrupted, "session.loop_once(loopstate)")
assert loopstate.testsfailed assert loopstate.testsfailed
#assert loopstate.shuttingdown #assert loopstate.shuttingdown

View File

@@ -3,6 +3,7 @@ import os
from xdist.gwmanage import GatewayManager, HostRSync from xdist.gwmanage import GatewayManager, HostRSync
from py._test.pluginmanager import HookRelay, Registry from py._test.pluginmanager import HookRelay, Registry
from py._plugin import hookspec from py._plugin import hookspec
from xdist import newhooks
import execnet import execnet
def pytest_funcarg__hookrecorder(request): def pytest_funcarg__hookrecorder(request):
@@ -11,7 +12,7 @@ def pytest_funcarg__hookrecorder(request):
return _pytest.gethookrecorder(hook) return _pytest.gethookrecorder(hook)
def pytest_funcarg__hook(request): def pytest_funcarg__hook(request):
return HookRelay(hookspec, Registry()) return HookRelay([hookspec, newhooks], Registry())
class TestGatewayManagerPopen: class TestGatewayManagerPopen:
def test_popen_no_default_chdir(self, hook): def test_popen_no_default_chdir(self, hook):

View File

@@ -61,7 +61,6 @@ class TestLooponFailing:
""") """)
session = LooponfailingSession(modcol.config) session = LooponfailingSession(modcol.config)
loopstate = LoopState() loopstate = LoopState()
session.remotecontrol.setup()
session.loop_once(loopstate) session.loop_once(loopstate)
assert len(loopstate.colitems) == 1 assert len(loopstate.colitems) == 1
@@ -83,7 +82,6 @@ class TestLooponFailing:
""") """)
session = LooponfailingSession(modcol.config) session = LooponfailingSession(modcol.config)
loopstate = LoopState() loopstate = LoopState()
session.remotecontrol.setup()
loopstate.colitems = [] loopstate.colitems = []
session.loop_once(loopstate) session.loop_once(loopstate)
assert len(loopstate.colitems) == 1 assert len(loopstate.colitems) == 1
@@ -110,7 +108,6 @@ class TestLooponFailing:
""") """)
session = LooponfailingSession(modcol.config) session = LooponfailingSession(modcol.config)
loopstate = LoopState() loopstate = LoopState()
session.remotecontrol.setup()
loopstate.colitems = [] loopstate.colitems = []
session.loop_once(loopstate) session.loop_once(loopstate)
assert len(loopstate.colitems) == 2 assert len(loopstate.colitems) == 2
@@ -135,7 +132,7 @@ class TestLooponFailing:
x = 0 x = 0
assert x == 1 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("def test_one")
child.expect("x == 1") child.expect("x == 1")
child.expect("1 failed") child.expect("1 failed")
@@ -149,3 +146,16 @@ class TestLooponFailing:
child.expect(".*1 passed.*") child.expect(".*1 passed.*")
child.kill(15) child.kill(15)
def test_looponfail_xfail_passes(self, testdir):
p = testdir.makepyfile("""
import py
@py.test.mark.xfail
def test_one():
pass
""")
child = testdir.spawn_pytest("-f %s" % p)
child.expect("1 xpass")
child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes")
child.kill(15)

View File

@@ -12,7 +12,7 @@ class EventQueue:
self.queue = queue self.queue = queue
registry.register(self) registry.register(self)
def geteventargs(self, eventname, timeout=2.0): def geteventargs(self, eventname, timeout=10.0):
events = [] events = []
while 1: while 1:
try: try:
@@ -38,21 +38,22 @@ class MySetup:
self.id = 0 self.id = 0
self.request = request self.request = request
def geteventargs(self, eventname, timeout=2.0): def geteventargs(self, eventname, timeout=10.0):
eq = EventQueue(self.config.pluginmanager, self.queue) eq = EventQueue(self.config.pluginmanager, self.queue)
return eq.geteventargs(eventname, timeout=timeout) return eq.geteventargs(eventname, timeout=timeout)
def makenode(self, config=None): def makenode(self, config=None, xspec="popen"):
if config is None: if config is None:
testdir = self.request.getfuncargvalue("testdir") testdir = self.request.getfuncargvalue("testdir")
config = testdir.reparseconfig([]) config = testdir.reparseconfig([])
self.config = config self.config = config
self.queue = Queue() self.queue = Queue()
self.xspec = execnet.XSpec("popen") self.xspec = execnet.XSpec(xspec)
self.gateway = execnet.makegateway(self.xspec) self.gateway = execnet.makegateway(self.xspec)
self.id += 1 self.id += 1
self.gateway.id = str(self.id) self.gateway.id = str(self.id)
self.node = TXNode(self.gateway, self.config, putevent=self.queue.put) self.nodemanager = None
self.node = TXNode(self.nodemanager, self.gateway, self.config, putevent=self.queue.put)
assert not self.node.channel.isclosed() assert not self.node.channel.isclosed()
return self.node return self.node
@@ -80,7 +81,7 @@ class TestMasterSlaveConnection:
node.send(123) # invalid item node.send(123) # invalid item
kwargs = mysetup.geteventargs("pytest_testnodedown") kwargs = mysetup.geteventargs("pytest_testnodedown")
assert kwargs['node'] is node assert kwargs['node'] is node
assert isinstance(kwargs['error'], execnet.RemoteError) #assert isinstance(kwargs['error'], execnet.RemoteError)
def test_crash_killed(self, testdir, mysetup): def test_crash_killed(self, testdir, mysetup):
if not hasattr(py.std.os, 'kill'): if not hasattr(py.std.os, 'kill'):
@@ -146,3 +147,26 @@ class TestMasterSlaveConnection:
for outcome in "passed failed skipped".split(): for outcome in "passed failed skipped".split():
rep = mysetup.geteventargs("pytest_runtest_logreport")['report'] rep = mysetup.geteventargs("pytest_runtest_logreport")['report']
assert getattr(rep, outcome) assert getattr(rep, outcome)
def test_send_one_with_env(self, testdir, mysetup, monkeypatch):
if execnet.XSpec("popen").env is None:
py.test.skip("requires execnet 1.0.7 or above")
monkeypatch.delenv('ENV1', raising=False)
monkeypatch.delenv('ENV2', raising=False)
monkeypatch.setenv('ENV3', 'var3')
item = testdir.getitem("""
def test_func():
import os
# ENV1, ENV2 set by xspec; ENV3 inherited from parent process
assert os.getenv('ENV2') == 'var2'
assert os.getenv('ENV1') == 'var1'
assert os.getenv('ENV3') == 'var3'
""")
node = mysetup.makenode(item.config,
xspec="popen//env:ENV1=var1//env:ENV2=var2")
node.send(item)
kwargs = mysetup.geteventargs("pytest_runtest_logreport")
rep = kwargs['report']
assert rep.passed

35
tox.ini Normal file
View File

@@ -0,0 +1,35 @@
[tox]
distshare={homedir}/.tox/distshare
envlist=py26,py31,py27,py25,py24
[tox:hudson]
distshare={toxworkdir}/distshare
sdistsrc={distshare}/pytest-xdist-*
[testenv]
changedir=testing
deps=
{distshare}/py-*
commands=
py.test -rsfxX --tools-on-path \
--junitxml={envlogdir}/junit-{envname}.xml []
[testenv:py27]
basepython=python2.7
[testenv:py26]
basepython=python2.6
deps=
{distshare}/py-*
pexpect
#[testenv:py26-py132]
#basepython=python2.6
#deps= py==1.3.2
# execnet==1.0.6
[testenv:py25]
basepython=python2.5
[testenv:py24]
basepython=python2.4
[testenv:py31]
basepython=python3.1
#[testenv:pypy]
#basepython=pypy-c
[testenv:jython]
basepython=jython

View File

@@ -1,3 +1,3 @@
# #
__version__ = "1.0" __version__ = "1.4"

View File

@@ -1,6 +1,5 @@
import py import py
from py._test.session import Session from py._test import session
from py._test import outcome
from xdist.nodemanage import NodeManager from xdist.nodemanage import NodeManager
queue = py.builtin._tryimport('queue', 'Queue') queue = py.builtin._tryimport('queue', 'Queue')
@@ -21,7 +20,7 @@ class LoopState(object):
# waiting for a host to become ready. # waiting for a host to become ready.
self.dowork = True self.dowork = True
self.shuttingdown = False self.shuttingdown = False
self.testsfailed = False self.testsfailed = 0
def __repr__(self): def __repr__(self):
return "<LoopState exitstatus=%r shuttingdown=%r len(colitems)=%d>" % ( return "<LoopState exitstatus=%r shuttingdown=%r len(colitems)=%d>" % (
@@ -32,7 +31,7 @@ class LoopState(object):
if report.when != "teardown": # otherwise we already managed it if report.when != "teardown": # otherwise we already managed it
self.dsession.removeitem(report.item, report.node) self.dsession.removeitem(report.item, report.node)
if report.failed: if report.failed:
self.testsfailed = True self.testsfailed += 1
def pytest_collectreport(self, report): def pytest_collectreport(self, report):
if report.passed: if report.passed:
@@ -55,12 +54,11 @@ class LoopState(object):
def pytest_rescheduleitems(self, items): def pytest_rescheduleitems(self, items):
self.colitems[:] = items + self.colitems self.colitems[:] = items + self.colitems
self.dowork = False # avoid busywait for pending in self.dsession.node2pending.values():
if pending:
self.dowork = False # avoid busywait, nodes still have work
class ExitFirstInterrupt(KeyboardInterrupt): class DSession(session.Session):
pass
class DSession(Session):
""" """
Session drives the collection and running of tests Session drives the collection and running of tests
and generates test events for reporters. and generates test events for reporters.
@@ -102,9 +100,12 @@ class DSession(Session):
return exitstatus return exitstatus
def collect_all_items(self, colitems): def collect_all_items(self, colitems):
self.report_line("[master] starting full item collection ...") verbose = self.config.getvalue("verbose")
if verbose:
self.report_line("[master] starting full item collection ...")
allitems = list(self.collect(colitems)) allitems = list(self.collect(colitems))
self.report_line("[master] collected %d items" %(len(allitems))) if verbose:
self.report_line("[master] collected %d items" %(len(allitems)))
return allitems return allitems
def loop_once(self, loopstate): def loop_once(self, loopstate):
@@ -130,15 +131,19 @@ class DSession(Session):
call(**kwargs) call(**kwargs)
# termination conditions # termination conditions
if ((loopstate.testsfailed and self.config.option.exitfirst) or maxfail = self.config.getvalue("maxfail")
if (not self.node2pending or
(loopstate.testsfailed and maxfail and
loopstate.testsfailed >= maxfail) or
(not self.item2nodes and not colitems and not self.queue.qsize())): (not self.item2nodes and not colitems and not self.queue.qsize())):
if self.config.option.exitfirst: if maxfail and loopstate.testsfailed >= maxfail:
raise ExitFirstInterrupt() raise self.Interrupted("stopping after %d failures" % (
loopstate.testsfailed))
self.triggershutdown() self.triggershutdown()
loopstate.shuttingdown = True loopstate.shuttingdown = True
elif not self.node2pending: if not self.node2pending:
loopstate.exitstatus = outcome.EXIT_NOHOSTS loopstate.exitstatus = session.EXIT_NOHOSTS
def loop_once_shutdown(self, loopstate): def loop_once_shutdown(self, loopstate):
# once we are in shutdown mode we dont send # once we are in shutdown mode we dont send
# events other than HostDown upstream # events other than HostDown upstream
@@ -151,16 +156,16 @@ class DSession(Session):
self.config.hook.pytest_runtest_logreport(**kwargs) self.config.hook.pytest_runtest_logreport(**kwargs)
elif eventname == "pytest_internalerror": elif eventname == "pytest_internalerror":
self.config.hook.pytest_internalerror(**kwargs) self.config.hook.pytest_internalerror(**kwargs)
loopstate.exitstatus = outcome.EXIT_INTERNALERROR loopstate.exitstatus = session.EXIT_INTERNALERROR
elif eventname == "pytest__teardown_final_logerror": elif eventname == "pytest__teardown_final_logerror":
self.config.hook.pytest__teardown_final_logerror(**kwargs) self.config.hook.pytest__teardown_final_logerror(**kwargs)
loopstate.exitstatus = outcome.EXIT_TESTSFAILED loopstate.exitstatus = session.EXIT_TESTSFAILED
if not self.node2pending: if not self.node2pending:
# finished # finished
if loopstate.testsfailed: if loopstate.testsfailed:
loopstate.exitstatus = outcome.EXIT_TESTSFAILED loopstate.exitstatus = session.EXIT_TESTSFAILED
else: else:
loopstate.exitstatus = outcome.EXIT_OK loopstate.exitstatus = session.EXIT_OK
#self.config.pluginmanager.unregister(loopstate) #self.config.pluginmanager.unregister(loopstate)
def _initloopstate(self, colitems): def _initloopstate(self, colitems):
@@ -179,17 +184,14 @@ class DSession(Session):
break break
except KeyboardInterrupt: except KeyboardInterrupt:
excinfo = py.code.ExceptionInfo() excinfo = py.code.ExceptionInfo()
if excinfo.errisinstance(ExitFirstInterrupt): self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
exitstatus = outcome.EXIT_TESTSFAILED exitstatus = session.EXIT_INTERRUPTED
else:
self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
exitstatus = outcome.EXIT_INTERRUPTED
except: except:
self.config.pluginmanager.notify_exception() self.config.pluginmanager.notify_exception()
exitstatus = outcome.EXIT_INTERNALERROR exitstatus = session.EXIT_INTERNALERROR
self.config.pluginmanager.unregister(loopstate) self.config.pluginmanager.unregister(loopstate)
if exitstatus == 0 and self._testsfailed: if exitstatus == 0 and self._testsfailed:
exitstatus = outcome.EXIT_TESTSFAILED exitstatus = session.EXIT_TESTSFAILED
return exitstatus return exitstatus
def triggershutdown(self): def triggershutdown(self):
@@ -287,6 +289,9 @@ class DSession(Session):
def setup(self): def setup(self):
""" setup any neccessary resources ahead of the test run. """ """ setup any neccessary resources ahead of the test run. """
if not self.config.getvalue("verbose"):
self.report_line("instantiating gateways (use -v for details): %s" %
",".join(self.config.option.tx))
self.nodemanager = NodeManager(self.config) self.nodemanager = NodeManager(self.config)
self.nodemanager.setup_nodes(putevent=self.queue.put) self.nodemanager.setup_nodes(putevent=self.queue.put)

View File

@@ -31,13 +31,12 @@ class MyPickler(Pickler):
""" Pickler with a custom memoize() """ Pickler with a custom memoize()
to take care of unique ID creation. to take care of unique ID creation.
See the usage in ImmutablePickler See the usage in ImmutablePickler
XXX we could probably extend Pickler
and Unpickler classes to directly
update the other'S memos.
""" """
def __init__(self, file, protocol, uneven): def __init__(self, immo, file, protocol, uneven):
Pickler.__init__(self, file, protocol) Pickler.__init__(self, file, protocol)
self.uneven = uneven self.uneven = uneven
self._unpicklememo = immo._unpicklememo
self.memo = immo._picklememo
def memoize(self, obj): def memoize(self, obj):
if self.fast: if self.fast:
@@ -47,13 +46,26 @@ class MyPickler(Pickler):
key = memo_len * 2 + self.uneven key = memo_len * 2 + self.uneven
self.write(self.put(key)) self.write(self.put(key))
self.memo[id(obj)] = key, obj self.memo[id(obj)] = key, obj
key = makekey(key)
if key in self._unpicklememo:
assert self._unpicklememo[key] is obj
dict.__setitem__(self._unpicklememo, key, obj)
#if sys.version_info < (3,0): #if sys.version_info < (3,0):
# def save_string(self, obj, pack=struct.pack): # def save_string(self, obj, pack=struct.pack):
# obj = unicode(obj) # obj = unicode(obj)
# self.save_unicode(obj, pack=pack) # self.save_unicode(obj, pack=pack)
# Pickler.dispatch[str] = save_string # Pickler.dispatch[str] = save_string
class UnpicklingDict(dict):
def __init__(self, picklememo):
super(UnpicklingDict, self).__init__()
self._picklememo = picklememo
def __setitem__(self, key, obj):
super(UnpicklingDict, self).__setitem__(key, obj)
self._picklememo[id(obj)] = (fromkey(key), obj)
class ImmutablePickler: class ImmutablePickler:
def __init__(self, uneven, protocol=0): def __init__(self, uneven, protocol=0):
""" ImmutablePicklers are instantiated in Pairs. """ ImmutablePicklers are instantiated in Pairs.
@@ -64,7 +76,7 @@ class ImmutablePickler:
parameter. parameter.
""" """
self._picklememo = {} self._picklememo = {}
self._unpicklememo = {} self._unpicklememo = UnpicklingDict(self._picklememo)
self._protocol = protocol self._protocol = protocol
self.uneven = uneven and 1 or 0 self.uneven = uneven and 1 or 0
@@ -73,18 +85,13 @@ class ImmutablePickler:
# which be the case e.g. if you want to pickle # which be the case e.g. if you want to pickle
# from a forked process back to the original # from a forked process back to the original
f = py.io.BytesIO() f = py.io.BytesIO()
pickler = MyPickler(f, self._protocol, uneven=self.uneven) pickler = MyPickler(self, f, self._protocol, uneven=self.uneven)
pickler.memo = self._picklememo
pickler.memoize(obj) pickler.memoize(obj)
self._updateunpicklememo()
def dumps(self, obj): def dumps(self, obj):
f = py.io.BytesIO() f = py.io.BytesIO()
pickler = MyPickler(f, self._protocol, uneven=self.uneven) pickler = MyPickler(self, f, self._protocol, uneven=self.uneven)
pickler.memo = self._picklememo
pickler.dump(obj) pickler.dump(obj)
if obj is not None:
self._updateunpicklememo()
#print >>debug, "dumped", obj #print >>debug, "dumped", obj
#print >>debug, "picklememo", self._picklememo #print >>debug, "picklememo", self._picklememo
return f.getvalue() return f.getvalue()
@@ -94,21 +101,10 @@ class ImmutablePickler:
unpickler = Unpickler(f) unpickler = Unpickler(f)
unpickler.memo = self._unpicklememo unpickler.memo = self._unpicklememo
res = unpickler.load() res = unpickler.load()
self._updatepicklememo()
#print >>debug, "loaded", res #print >>debug, "loaded", res
#print >>debug, "unpicklememo", self._unpicklememo #print >>debug, "unpicklememo", self._unpicklememo
return res return res
def _updatepicklememo(self):
for x, obj in self._unpicklememo.items():
self._picklememo[id(obj)] = (fromkey(x), obj)
def _updateunpicklememo(self):
for key,obj in self._picklememo.values():
key = makekey(key)
if key in self._unpicklememo:
assert self._unpicklememo[key] is obj
self._unpicklememo[key] = obj
NO_ENDMARKER_WANTED = object() NO_ENDMARKER_WANTED = object()

23
xdist/newhooks.py Normal file
View File

@@ -0,0 +1,23 @@
def pytest_gwmanage_newgateway(gateway, platinfo):
""" called on new raw gateway creation. """
def pytest_gwmanage_rsyncstart(source, gateways):
""" called before rsyncing a directory to remote gateways takes place. """
def pytest_gwmanage_rsyncfinish(source, gateways):
""" called after rsyncing a directory to remote gateways takes place. """
def pytest_configure_node(node):
""" configure node information before it gets instantiated. """
def pytest_testnodeready(node):
""" Test Node is ready to operate. """
def pytest_testnodedown(node, error):
""" Test Node is down. """
def pytest_rescheduleitems(items):
""" reschedule Items from a node that went down. """

View File

@@ -58,7 +58,7 @@ class NodeManager(object):
self.rsync_roots() self.rsync_roots()
self.trace("setting up nodes") self.trace("setting up nodes")
for gateway in self.gwmanager.group: for gateway in self.gwmanager.group:
node = TXNode(gateway, self.config, putevent) node = TXNode(self, gateway, self.config, putevent)
gateway.node = node # to keep node alive gateway.node = node # to keep node alive
self.trace("started node %r" % node) self.trace("started node %r" % node)

View File

@@ -120,11 +120,11 @@ Specifying test exec environments in a conftest.py
Instead of specifying command line options, you can Instead of specifying command line options, you can
put options values in a ``conftest.py`` file like this:: put options values in a ``conftest.py`` file like this::
pytest_option_tx = ['ssh=myhost//python=python2.5', 'popen//python=python2.5'] option_tx = ['ssh=myhost//python=python2.5', 'popen//python=python2.5']
pytest_option_dist = True option_dist = True
Any commandline ``--tx`` specifictions will add to the list of available execution Any commandline ``--tx`` specifictions will add to the list of
environments. available execution environments.
Specifying "rsync" dirs in a conftest.py Specifying "rsync" dirs in a conftest.py
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -173,6 +173,16 @@ def pytest_addoption(parser):
group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", group.addoption('--rsyncdir', action="append", default=[], metavar="dir1",
help="add directory for rsyncing to remote tx nodes.") help="add directory for rsyncing to remote tx nodes.")
# -------------------------------------------------------------------------
# distributed testing hooks
# -------------------------------------------------------------------------
def pytest_addhooks(pluginmanager):
from xdist import newhooks
pluginmanager.addhooks(newhooks)
# -------------------------------------------------------------------------
# distributed testing initialization
# -------------------------------------------------------------------------
def pytest_configure(config): def pytest_configure(config):
if config.option.numprocesses: if config.option.numprocesses:
config.option.dist = "load" config.option.dist = "load"
@@ -187,11 +197,23 @@ def pytest_configure(config):
raise config.Error("--pdb incompatible with --looponfail.") raise config.Error("--pdb incompatible with --looponfail.")
from xdist.remote import LooponfailingSession from xdist.remote import LooponfailingSession
config.setsessionclass(LooponfailingSession) config.setsessionclass(LooponfailingSession)
config._isdistsession = True
elif val("dist") != "no": elif val("dist") != "no":
if usepdb: if usepdb:
raise config.Error("--pdb incompatible with distributing tests.") raise config.Error("--pdb incompatible with distributing tests.")
from xdist.dsession import DSession from xdist.dsession import DSession
config.setsessionclass(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): def pytest_runtest_protocol(item):
if item.config.getvalue("boxed"): if item.config.getvalue("boxed"):
@@ -234,3 +256,59 @@ def report_process_crash(item, result):
from py._plugin.pytest_runner import ItemTestReport from py._plugin.pytest_runner import ItemTestReport
return ItemTestReport(item, excinfo=info, when="???") 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)
if self.config.getvalue("verbose"):
self.write_line(infoline)
self.gateway2info[gateway] = infoline
def pytest_testnodeready(self, node):
if self.config.getvalue("verbose"):
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,))

View File

@@ -24,7 +24,6 @@ class LooponfailingSession(Session):
def main(self, initialitems): def main(self, initialitems):
try: try:
self.loopstate = loopstate = LoopState([]) self.loopstate = loopstate = LoopState([])
self.remotecontrol.setup()
while 1: while 1:
self.loop_once(loopstate) self.loop_once(loopstate)
if not loopstate.colitems and loopstate.wasfailing: if not loopstate.colitems and loopstate.wasfailing:
@@ -34,10 +33,10 @@ class LooponfailingSession(Session):
print print
def loop_once(self, loopstate): def loop_once(self, loopstate):
self.remotecontrol.setup()
colitems = loopstate.colitems colitems = loopstate.colitems
loopstate.wasfailing = colitems and len(colitems) loopstate.wasfailing = colitems and len(colitems)
loopstate.colitems = self.remotecontrol.runsession(colitems or ()) loopstate.colitems = self.remotecontrol.runsession(colitems or ())
self.remotecontrol.setup()
class LoopState: class LoopState:
def __init__(self, colitems=None): def __init__(self, colitems=None):
@@ -170,8 +169,21 @@ def slave_runsession(channel, config, fullwidth, hasmarkup):
DEBUG("SLAVE: starting session.main()") DEBUG("SLAVE: starting session.main()")
session.main(colitems) session.main(colitems)
session.config.hook.pytest_looponfailinfo( repr_pytest_looponfailinfo(
failreports=list(failreports), failreports=list(failreports),
rootdirs=[config.topdir]) rootdirs=[config.topdir])
rootcol = session.config._rootcol rootcol = session.config._rootcol
channel.send([rootcol.totrail(rep.getnode()) for rep in failreports]) 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)

View File

@@ -3,7 +3,7 @@
""" """
import py import py
from xdist.mypickle import PickleChannel from xdist.mypickle import PickleChannel
from py._test import outcome from py._test.session import Session
class TXNode(object): class TXNode(object):
""" Represents a Test Execution environment in the controlling process. """ Represents a Test Execution environment in the controlling process.
@@ -13,11 +13,13 @@ class TXNode(object):
""" """
ENDMARK = -1 ENDMARK = -1
def __init__(self, gateway, config, putevent): def __init__(self, nodemanager, gateway, config, putevent):
self.nodemanager = nodemanager
self.config = config self.config = config
self.putevent = putevent self.putevent = putevent
self.gateway = gateway self.gateway = gateway
self.channel = install_slave(gateway, config) self.slaveinput = {}
self.channel = install_slave(self)
self.channel.setcallback(self.callback, endmarker=self.ENDMARK) self.channel.setcallback(self.callback, endmarker=self.ENDMARK)
self._down = False self._down = False
@@ -43,7 +45,7 @@ class TXNode(object):
err = self.channel._getremoteerror() err = self.channel._getremoteerror()
if not self._down: if not self._down:
if not err or isinstance(err, EOFError): 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.notify("pytest_testnodedown", node=self, error=err)
self._down = True self._down = True
return return
@@ -52,7 +54,9 @@ class TXNode(object):
self.notify("pytest_testnodeready", node=self) self.notify("pytest_testnodeready", node=self)
elif eventname == "slavefinished": elif eventname == "slavefinished":
self._down = True self._down = True
self.notify("pytest_testnodedown", error=None, node=self) self.slaveoutput = kwargs['slaveoutput']
error = kwargs['error']
self.notify("pytest_testnodedown", error=error, node=self)
elif eventname in ("pytest_runtest_logreport", elif eventname in ("pytest_runtest_logreport",
"pytest__teardown_final_logerror"): "pytest__teardown_final_logerror"):
kwargs['report'].node = self kwargs['report'].node = self
@@ -80,32 +84,44 @@ class TXNode(object):
else: else:
self.channel.send(None) self.channel.send(None)
# setting up slave code # configuring and setting up slave node
def install_slave(gateway, config): def install_slave(node):
channel = gateway.remote_exec(source=""" channel = node.gateway.remote_exec(source="""
import os, sys import os, sys
sys.path.insert(0, os.getcwd()) sys.path.insert(0, os.getcwd())
from xdist.mypickle import PickleChannel from xdist.mypickle import PickleChannel
from xdist.txnode import SlaveNode from xdist.txnode import SlaveSession
channel.send("basicimport") channel.send("basicimport")
channel = PickleChannel(channel) channel = PickleChannel(channel)
slavenode = SlaveNode(channel) import py
slavenode.run() 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.receive()
channel = PickleChannel(channel) channel = PickleChannel(channel)
basetemp = None basetemp = None
if gateway.spec.popen: config = node.config
config.hook.pytest_configure_node(node=node)
if node.gateway.spec.popen:
popenbase = config.ensuretemp("popen") popenbase = config.ensuretemp("popen")
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, gateway.id)) channel.send((config, node.slaveinput, basetemp, node.gateway.id))
return channel return channel
class SlaveNode(object): class SlaveSession(Session):
def __init__(self, channel): def __init__(self, config, channel, nodeid):
self.channel = channel self.channel = channel
self.nodeid = nodeid
super(SlaveSession, self).__init__(config=config)
def __repr__(self): def __repr__(self):
return "<%s channel=%s>" %(self.__class__.__name__, self.channel) return "<%s channel=%s>" %(self.__class__.__name__, self.channel)
@@ -119,37 +135,31 @@ class SlaveNode(object):
def pytest__teardown_final_logerror(self, report): def pytest__teardown_final_logerror(self, report):
self.sendevent("pytest__teardown_final_logerror", report=report) self.sendevent("pytest__teardown_final_logerror", report=report)
def run(self): def pytest_keyboard_interrupt(self, excinfo):
channel = self.channel self._slaveerror = "SIGINT"
self.config, basetemp, self.nodeid = channel.receive()
if basetemp: def pytest_internalerror(self, excrepr):
self.config.basetemp = py.path.local(basetemp) self._slaveerror = "internal-error"
self.config.pluginmanager.do_configure(self.config) self.sendevent("pytest_internalerror", excrepr=excrepr)
self.config.pluginmanager.register(self)
def dist_main(self):
self.runner = self.config.pluginmanager.getplugin("pytest_runner") self.runner = self.config.pluginmanager.getplugin("pytest_runner")
self.sendevent("slaveready") self.sendevent("slaveready")
try: self.main(None)
self.config.hook.pytest_sessionstart(session=self) error = getattr(self, '_slaveerror', None)
while 1: self.sendevent("slavefinished", error=error,
task = channel.receive() slaveoutput=self.config.slaveoutput)
if task is None:
break def _mainloop(self, colitems):
if isinstance(task, list): while 1:
for item in task: task = self.channel.receive()
self.run_single(item=item) if task is None:
else: break
self.run_single(item=task) if isinstance(task, list):
self.config.hook.pytest_sessionfinish( for item in task:
session=self, self.run_single(item=item)
exitstatus=outcome.EXIT_OK) else:
except KeyboardInterrupt: self.run_single(item=task)
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): def run_single(self, item):
call = self.runner.CallInfo(item._reraiseunpicklingproblem, when='setup') call = self.runner.CallInfo(item._reraiseunpicklingproblem, when='setup')