Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f27bf87e | ||
|
|
c19f5e4bfd | ||
|
|
8a923ea2f4 | ||
|
|
e8ae2f9090 | ||
|
|
f946ab1835 | ||
|
|
1c2563d864 | ||
|
|
b04616419e | ||
|
|
722006f7f6 | ||
|
|
d9c5f18478 | ||
|
|
bd30e96ab0 | ||
|
|
eb09e22b62 | ||
|
|
319e247de5 | ||
|
|
1c712fe6a0 | ||
|
|
a6a4158b78 | ||
|
|
1167186f65 | ||
|
|
125991f180 | ||
|
|
fc1f29db68 | ||
|
|
9108790801 | ||
|
|
3fc03c8c8e | ||
|
|
c924dda30f | ||
|
|
9e982f0f73 | ||
|
|
705fe5962b | ||
|
|
d1f799064b | ||
|
|
47d3e99083 | ||
|
|
d81ffaa980 | ||
|
|
a0652e9d99 | ||
|
|
8272a669a4 | ||
|
|
9d55d3c831 | ||
|
|
985c9163b7 | ||
|
|
4661db0fef | ||
|
|
4cd76363f6 | ||
|
|
f1a77e80e8 | ||
|
|
e4bd57b670 | ||
|
|
516b5cfe6a | ||
|
|
9675d754db | ||
|
|
7b73e081fe | ||
|
|
d83af5fdcd | ||
|
|
025f1d577e | ||
|
|
dc67e46d59 | ||
|
|
51815940c2 | ||
|
|
9df55b1c60 | ||
|
|
3767ce3269 | ||
|
|
f7923c692e | ||
|
|
3ad58ae21f | ||
|
|
749faf5e7c | ||
|
|
c90c4a254c | ||
|
|
2efb059d75 | ||
|
|
e03f6d76b6 | ||
|
|
4d0422a548 | ||
|
|
198a2c2d38 | ||
|
|
518fae1f21 | ||
|
|
feaf840fab | ||
|
|
f112ad6cfb | ||
|
|
71771fbdc6 | ||
|
|
c0b2df1a77 | ||
|
|
9aa1df89ee | ||
|
|
67e0cd463c | ||
|
|
fb31aa882d | ||
|
|
51b0ba55e9 | ||
|
|
d0ccb5743d | ||
|
|
fa7da18d9b | ||
|
|
cf36bf8bf2 | ||
|
|
55e3d27820 | ||
|
|
a17bc05c5a | ||
|
|
e5e23fd278 | ||
|
|
43c0591c55 | ||
|
|
7bf3c7f029 | ||
|
|
e48dcda834 | ||
|
|
faa03ad601 | ||
|
|
e0f61e4fa2 | ||
|
|
f7be994848 | ||
|
|
7b19de5450 | ||
|
|
0c4f5eced2 | ||
|
|
e593841a70 | ||
|
|
43b693258f |
@@ -19,3 +19,4 @@ dist/
|
|||||||
pytest_xdist.egg-info
|
pytest_xdist.egg-info
|
||||||
issue/
|
issue/
|
||||||
3rdparty/
|
3rdparty/
|
||||||
|
.tox
|
||||||
|
|||||||
8
.hgtags
8
.hgtags
@@ -1,2 +1,10 @@
|
|||||||
42c6503ee48fae9c4c96d406afb12bfc86f15803 1.0
|
42c6503ee48fae9c4c96d406afb12bfc86f15803 1.0
|
||||||
eca7ce17eabf296983c36812c8b8be901e7055a3 1.1
|
eca7ce17eabf296983c36812c8b8be901e7055a3 1.1
|
||||||
|
56d8e5280be224a0ad3220a9deed55334710bd23 1.2
|
||||||
|
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
|
||||||
|
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
|
||||||
|
eaf8b1cb7c312883598677231be5bbeea3b5c127 1.3
|
||||||
|
a423748bf17ee778a37853225210257699cad9c1 1.4
|
||||||
|
cd44a941c833c098e4899fe3d42a96703754d0d5 1.5
|
||||||
|
4815040bdad8f182a5487f57a9da385483836e75 1.6
|
||||||
|
20875fed94e7f3dff50bdf762df91153b15ceca6 1.7
|
||||||
|
|||||||
49
CHANGELOG
49
CHANGELOG
@@ -1,3 +1,52 @@
|
|||||||
|
1.7
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
- fix incompatibilities with pytest-2.2.0 (allow multiple
|
||||||
|
pytest_runtest_logreport reports for a test item)
|
||||||
|
|
||||||
|
1.6
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
- terser collection reporting
|
||||||
|
|
||||||
|
- fix issue34 - distributed testing with -p plugin now works correctly
|
||||||
|
|
||||||
|
- fix race condition in looponfail mode where a concurrent file removal
|
||||||
|
could cause a crash
|
||||||
|
|
||||||
|
1.5
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
- adapt to and require pytest-2.0 changes, rsyncdirs and rsyncignore can now
|
||||||
|
only be specified in [pytest] sections of ini files, see "py.test -h"
|
||||||
|
for details.
|
||||||
|
- major internal refactoring to match the pytest-2.0 event refactoring
|
||||||
|
- perform test collection always at slave side instead of at the master
|
||||||
|
- make python2/python3 bridging work, remove usage of pickling
|
||||||
|
- improve initial reporting by using line-rewriting
|
||||||
|
- remove all trailing whitespace from source
|
||||||
|
|
||||||
|
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
|
1.2
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|||||||
23
ISSUES.txt
23
ISSUES.txt
@@ -1,5 +1,19 @@
|
|||||||
|
next release critical
|
||||||
|
-----------------------------------------------
|
||||||
|
tag: bug
|
||||||
|
|
||||||
allow to run xdist tests with xdist
|
miserably fails: --dist=each --tx popen --tx socket=...
|
||||||
|
|
||||||
|
|
||||||
|
rename / hooks
|
||||||
|
-----------------------------------------------
|
||||||
|
tag: bug
|
||||||
|
|
||||||
|
node -> slave
|
||||||
|
transition for hooks?
|
||||||
|
configure_node -> configure_slave
|
||||||
|
|
||||||
|
allow to remotely run xdist tests with xdist
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
tag: feature
|
tag: feature
|
||||||
|
|
||||||
@@ -8,3 +22,10 @@ currently this doesn't work because the remote side
|
|||||||
has no py.test plugin. How to configure/do
|
has no py.test plugin. How to configure/do
|
||||||
register "xdist.plugin" on the remote side?
|
register "xdist.plugin" on the remote side?
|
||||||
|
|
||||||
|
see to avoid any "from _pytest" internal imports
|
||||||
|
-----------------------------------------------
|
||||||
|
tag: feature
|
||||||
|
|
||||||
|
currently tests and even xdist core code imports
|
||||||
|
names from the internal _pytest namespace.
|
||||||
|
See to avoid it.
|
||||||
|
|||||||
171
README.txt
171
README.txt
@@ -1,29 +1,170 @@
|
|||||||
py.test xdist plugin: distributed testing and loop failure
|
xdist: pytest distributed testing plugin
|
||||||
===============================================================
|
===============================================================
|
||||||
|
|
||||||
.. _`pytest-xdist respository`: http://bitbucket.org/hpk42/pytest-xdist
|
The `pytest-xdist`_ plugin extends py.test with some unique
|
||||||
.. _`pytest`: http://pytest.org
|
test execution modes:
|
||||||
|
|
||||||
The pytest-xdist plugin extends `py.test`_ to ad-hoc distribute test
|
* Looponfail: run your tests repeatedly in a subprocess. After each run py.test
|
||||||
runs to multiple CPUs or remote machines. It requires setuptools
|
waits until a file in your project changes and then re-runs the previously
|
||||||
or distribute which help to pull in the neccessary execnet and
|
failing tests. This is repeated until all tests pass after which again
|
||||||
pytest-core dependencies.
|
a full run is performed.
|
||||||
|
|
||||||
Install the plugin locally with::
|
* multiprocess Load-balancing: if you have multiple CPUs or hosts you can use
|
||||||
|
those for a combined test run. This allows to speed up
|
||||||
|
development or to use special resources of remote machines.
|
||||||
|
|
||||||
python setup.py install
|
* Multi-Platform coverage: you can specify different Python interpreters
|
||||||
|
or different platforms and run tests in parallel on all of them.
|
||||||
|
|
||||||
or use the package in develope/in-place mode, particularly
|
Before running tests remotely, ``py.test`` efficiently "rsyncs" your
|
||||||
useful with a checkout of the `pytest-xdist repository`_::
|
program source code to the remote place. All test results
|
||||||
|
are reported back and displayed to your local terminal.
|
||||||
|
You may specify different Python versions and interpreters.
|
||||||
|
|
||||||
python setup.py develop
|
|
||||||
|
|
||||||
or use one of::
|
Installation
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Install the plugin with::
|
||||||
|
|
||||||
easy_install pytest-xdist
|
easy_install pytest-xdist
|
||||||
|
|
||||||
|
# or
|
||||||
|
|
||||||
pip install pytest-xdist
|
pip install pytest-xdist
|
||||||
|
|
||||||
for downloading and installing it in one go.
|
or use the package in develope/in-place mode with
|
||||||
|
a checkout of the `pytest-xdist repository`_ ::
|
||||||
|
|
||||||
|
python setup.py develop
|
||||||
|
|
||||||
|
Usage examples
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Speed up test runs by sending tests to multiple CPUs
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
To send tests to multiple CPUs, type::
|
||||||
|
|
||||||
|
py.test -n NUM
|
||||||
|
|
||||||
|
Especially for longer running tests or tests requiring
|
||||||
|
a lot of IO this can lead to considerable speed ups.
|
||||||
|
|
||||||
|
|
||||||
|
Running tests in a Python subprocess
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
To instantiate a python2.4 sub process and send tests to it, you may type::
|
||||||
|
|
||||||
|
py.test -d --tx popen//python=python2.4
|
||||||
|
|
||||||
|
This will start a subprocess which is run with the "python2.4"
|
||||||
|
Python interpreter, found in your system binary lookup path.
|
||||||
|
|
||||||
|
If you prefix the --tx option value like this::
|
||||||
|
|
||||||
|
--tx 3*popen//python=python2.4
|
||||||
|
|
||||||
|
then three subprocesses would be created and tests
|
||||||
|
will be load-balanced across these three processes.
|
||||||
|
|
||||||
|
|
||||||
|
Sending tests to remote SSH accounts
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
Suppose you have a package ``mypkg`` which contains some
|
||||||
|
tests that you can successfully run locally. And you
|
||||||
|
have a ssh-reachable machine ``myhost``. Then
|
||||||
|
you can ad-hoc distribute your tests by typing::
|
||||||
|
|
||||||
|
py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
|
||||||
|
|
||||||
|
This will synchronize your ``mypkg`` package directory
|
||||||
|
to an remote ssh account and then locally collect tests
|
||||||
|
and send them to remote places for execution.
|
||||||
|
|
||||||
|
You can specify multiple ``--rsyncdir`` directories
|
||||||
|
to be sent to the remote side.
|
||||||
|
|
||||||
|
**NOTE:** For py.test to collect and send tests correctly
|
||||||
|
you not only need to make sure all code and tests
|
||||||
|
directories are rsynced, but that any test (sub) directory
|
||||||
|
also has an ``__init__.py`` file because internally
|
||||||
|
py.test references tests as a fully qualified python
|
||||||
|
module path. **You will otherwise get strange errors**
|
||||||
|
during setup of the remote side.
|
||||||
|
|
||||||
|
Sending tests to remote Socket Servers
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
Download the single-module `socketserver.py`_ Python program
|
||||||
|
and run it like this::
|
||||||
|
|
||||||
|
python socketserver.py
|
||||||
|
|
||||||
|
It will tell you that it starts listening on the default
|
||||||
|
port. You can now on your home machine specify this
|
||||||
|
new socket host with something like this::
|
||||||
|
|
||||||
|
py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg
|
||||||
|
|
||||||
|
|
||||||
|
.. _`atonce`:
|
||||||
|
|
||||||
|
Running tests on many platforms at once
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
The basic command to run tests on multiple platforms is::
|
||||||
|
|
||||||
|
py.test --dist=each --tx=spec1 --tx=spec2
|
||||||
|
|
||||||
|
If you specify a windows host, an OSX host and a Linux
|
||||||
|
environment this command will send each tests to all
|
||||||
|
platforms - and report back failures from all platforms
|
||||||
|
at once. The specifications strings use the `xspec syntax`_.
|
||||||
|
|
||||||
|
.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec
|
||||||
|
|
||||||
|
.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py
|
||||||
|
|
||||||
|
.. _`execnet`: http://codespeak.net/execnet
|
||||||
|
|
||||||
|
Specifying test exec environments in an ini file
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
pytest (since version 2.0) supports ini-style cofiguration.
|
||||||
|
You can for example make running with three subprocesses
|
||||||
|
your default like this::
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
addopts = -n3
|
||||||
|
|
||||||
|
You can also add default environments like this::
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6
|
||||||
|
|
||||||
|
and then just type::
|
||||||
|
|
||||||
|
py.test --dist=each
|
||||||
|
|
||||||
|
to run tests in each of the environments.
|
||||||
|
|
||||||
|
Specifying "rsync" dirs in an ini-file
|
||||||
|
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
|
||||||
|
you may specify directories to include or to exclude in synchronisation::
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
rsyncdirs = . mypkg helperpkg
|
||||||
|
rsyncignore = .hg
|
||||||
|
|
||||||
|
These directory specifications are relative to the directory
|
||||||
|
where the configuration file was found.
|
||||||
|
|
||||||
|
.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist
|
||||||
|
.. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist
|
||||||
|
.. _`pytest`: http://pytest.org
|
||||||
|
|
||||||
holger krekel <holger at merlinux eu>
|
|
||||||
|
|||||||
17
setup.py
17
setup.py
@@ -1,19 +1,10 @@
|
|||||||
"""
|
|
||||||
py.test 'xdist' plugin for distributed testing and loop-on-failing modes.
|
|
||||||
|
|
||||||
See http://pytest.org/plugin/xdist.html for documentation and, after
|
|
||||||
installation of the ``pytest-xdist`` PyPI package, ``py.test -h``
|
|
||||||
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=__version__,
|
version='1.7.dev1',
|
||||||
description='py.test xdist plugin for distributed testing and loop-on-failing modes',
|
description='py.test xdist plugin for distributed testing and loop-on-failing modes',
|
||||||
long_description=__doc__,
|
long_description=open('README.txt').read(),
|
||||||
license='GPLv2 or later',
|
license='GPLv2 or later',
|
||||||
author='holger krekel and contributors',
|
author='holger krekel and contributors',
|
||||||
author_email='py-dev@codespeak.net,holger@merlinux.eu',
|
author_email='py-dev@codespeak.net,holger@merlinux.eu',
|
||||||
@@ -22,9 +13,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.6', 'py>=1.3.0'],
|
install_requires = ['execnet>=1.0.8', 'pytest>=2.2.0'],
|
||||||
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',
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
#
|
|
||||||
@@ -1,6 +1,41 @@
|
|||||||
import py
|
import py
|
||||||
|
import sys
|
||||||
|
|
||||||
class TestDistribution:
|
class TestDistribution:
|
||||||
|
def test_n1_pass(self, testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_ok():
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, "-n1")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_n1_fail(self, testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_fail():
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, "-n1")
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 failed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_n1_skip(self, testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_skip():
|
||||||
|
import py
|
||||||
|
py.test.skip("myreason")
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, "-n1")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 skipped*",
|
||||||
|
])
|
||||||
|
|
||||||
def test_manytests_to_one_popen(self, testdir):
|
def test_manytests_to_one_popen(self, testdir):
|
||||||
p1 = testdir.makepyfile("""
|
p1 = testdir.makepyfile("""
|
||||||
import py
|
import py
|
||||||
@@ -14,15 +49,40 @@ 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*",
|
"*1*Python*",
|
||||||
"*1*popen*Python*",
|
|
||||||
"*2 failed, 1 passed, 1 skipped*",
|
"*2 failed, 1 passed, 1 skipped*",
|
||||||
])
|
])
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
|
|
||||||
def test_dist_conftest_specified(self, testdir):
|
def test_n1_fail_minus_x(self, testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_fail1():
|
||||||
|
assert 0
|
||||||
|
def test_fail2():
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, "-x", "-v", "-n1")
|
||||||
|
assert result.ret == 2
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*Interrupted: stopping*1*",
|
||||||
|
"*1 failed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_basetemp_in_subprocesses(self, testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
def test_send(tmpdir):
|
||||||
|
import py
|
||||||
|
assert tmpdir.relto(py.path.local(%r)), tmpdir
|
||||||
|
""" % str(testdir.tmpdir))
|
||||||
|
result = testdir.runpytest(p1, "-n1")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_dist_ini_specified(self, testdir):
|
||||||
p1 = testdir.makepyfile("""
|
p1 = testdir.makepyfile("""
|
||||||
import py
|
import py
|
||||||
def test_fail0():
|
def test_fail0():
|
||||||
@@ -35,18 +95,18 @@ class TestDistribution:
|
|||||||
py.test.skip("hello")
|
py.test.skip("hello")
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
testdir.makeconftest("""
|
testdir.makeini("""
|
||||||
option_tx = 'popen popen popen'.split()
|
[pytest]
|
||||||
|
addopts = --tx=3*popen
|
||||||
""")
|
""")
|
||||||
result = testdir.runpytest(p1, '-d')
|
result = testdir.runpytest(p1, '-d', "-v")
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*0*popen*Python*",
|
"*2*Python*",
|
||||||
"*1*popen*Python*",
|
|
||||||
"*2*popen*Python*",
|
|
||||||
"*2 failed, 1 passed, 1 skipped*",
|
"*2 failed, 1 passed, 1 skipped*",
|
||||||
])
|
])
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
|
|
||||||
|
@py.test.mark.xfail("sys.platform.startswith('java')", run=False)
|
||||||
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,12 +128,11 @@ 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', '-n1')
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*popen*Python*",
|
"*Python*",
|
||||||
"*popen*Python*",
|
"*PASS**test_ok*",
|
||||||
"*popen*Python*",
|
"*node*down*",
|
||||||
"*node down*",
|
|
||||||
"*3 failed, 1 passed, 1 skipped*"
|
"*3 failed, 1 passed, 1 skipped*"
|
||||||
])
|
])
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
@@ -84,40 +143,19 @@ class TestDistribution:
|
|||||||
subdir = source.mkdir("example_pkg")
|
subdir = source.mkdir("example_pkg")
|
||||||
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():\n 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([
|
||||||
"*0* *popen*platform*",
|
"*0* *cwd*",
|
||||||
#"RSyncStart: [G1]",
|
#"RSyncStart: [G1]",
|
||||||
#"RSyncFinished: [G1]",
|
#"RSyncFinished: [G1]",
|
||||||
"*1 passed*"
|
"*1 passed*"
|
||||||
])
|
])
|
||||||
assert dest.join(subdir.basename).check(dir=1)
|
assert dest.join(subdir.basename).check(dir=1)
|
||||||
|
|
||||||
def test_dist_each(self, testdir):
|
|
||||||
interpreters = []
|
|
||||||
for name in ("python2.4", "python2.5"):
|
|
||||||
interp = py.path.local.sysfind(name)
|
|
||||||
if interp is None:
|
|
||||||
py.test.skip("%s not found" % name)
|
|
||||||
interpreters.append(interp)
|
|
||||||
|
|
||||||
testdir.makepyfile(__init__="", test_one="""
|
|
||||||
import sys
|
|
||||||
def test_hello():
|
|
||||||
print("%s...%s" % sys.version_info[:2])
|
|
||||||
assert 0
|
|
||||||
""")
|
|
||||||
args = ["--dist=each"]
|
|
||||||
args += ["--tx", "popen//python=%s" % interpreters[0]]
|
|
||||||
args += ["--tx", "popen//python=%s" % interpreters[1]]
|
|
||||||
result = testdir.runpytest(*args)
|
|
||||||
s = result.stdout.str()
|
|
||||||
assert "2.4" in s
|
|
||||||
assert "2.5" in s
|
|
||||||
|
|
||||||
def test_data_exchange(self, testdir):
|
def test_data_exchange(self, testdir):
|
||||||
c1 = testdir.makeconftest("""
|
c1 = testdir.makeconftest("""
|
||||||
# This hook only called on master.
|
# This hook only called on master.
|
||||||
@@ -125,8 +163,8 @@ class TestDistribution:
|
|||||||
node.slaveinput['a'] = 42
|
node.slaveinput['a'] = 42
|
||||||
node.slaveinput['b'] = 7
|
node.slaveinput['b'] = 7
|
||||||
|
|
||||||
# This hook only takes action on slave.
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
# this attribute is only set on slaves
|
||||||
if hasattr(config, 'slaveinput'):
|
if hasattr(config, 'slaveinput'):
|
||||||
a = config.slaveinput['a']
|
a = config.slaveinput['a']
|
||||||
b = config.slaveinput['b']
|
b = config.slaveinput['b']
|
||||||
@@ -137,7 +175,6 @@ class TestDistribution:
|
|||||||
def pytest_testnodedown(node, error):
|
def pytest_testnodedown(node, error):
|
||||||
node.config.calc_result = node.slaveoutput['r']
|
node.config.calc_result = node.slaveoutput['r']
|
||||||
|
|
||||||
# This hook only takes action on master.
|
|
||||||
def pytest_terminal_summary(terminalreporter):
|
def pytest_terminal_summary(terminalreporter):
|
||||||
if not hasattr(terminalreporter.config, 'slaveinput'):
|
if not hasattr(terminalreporter.config, 'slaveinput'):
|
||||||
calc_result = terminalreporter.config.calc_result
|
calc_result = terminalreporter.config.calc_result
|
||||||
@@ -145,9 +182,9 @@ class TestDistribution:
|
|||||||
'calculated result is %s' % calc_result)
|
'calculated result is %s' % calc_result)
|
||||||
""")
|
""")
|
||||||
p1 = testdir.makepyfile("def test_func(): pass")
|
p1 = testdir.makepyfile("def test_func(): pass")
|
||||||
result = testdir.runpytest(p1, '-d', '--tx=popen')
|
result = testdir.runpytest("-v", p1, '-d', '--tx=popen')
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*popen*Python*",
|
"*0*Python*",
|
||||||
"*calculated result is 49*",
|
"*calculated result is 49*",
|
||||||
"*1 passed*"
|
"*1 passed*"
|
||||||
])
|
])
|
||||||
@@ -160,15 +197,228 @@ class TestDistribution:
|
|||||||
""")
|
""")
|
||||||
testdir.makeconftest("""
|
testdir.makeconftest("""
|
||||||
def pytest_sessionfinish(session):
|
def pytest_sessionfinish(session):
|
||||||
|
# on the slave
|
||||||
if hasattr(session.config, 'slaveoutput'):
|
if hasattr(session.config, 'slaveoutput'):
|
||||||
session.config.slaveoutput['s2'] = 42
|
session.config.slaveoutput['s2'] = 42
|
||||||
|
# on the master
|
||||||
def pytest_testnodedown(node, error):
|
def pytest_testnodedown(node, error):
|
||||||
assert node.slaveoutput['s2'] == 42
|
assert node.slaveoutput['s2'] == 42
|
||||||
print ("s2call-finished")
|
print ("s2call-finished")
|
||||||
""")
|
""")
|
||||||
args = ["-n1"]
|
args = ["-n1", "--debug"]
|
||||||
result = testdir.runpytest(*args)
|
result = testdir.runpytest(*args)
|
||||||
s = result.stdout.str()
|
s = result.stdout.str()
|
||||||
assert result.ret
|
assert result.ret == 2
|
||||||
assert 'SIGINT' in s
|
|
||||||
assert 's2call' in s
|
assert 's2call' in s
|
||||||
|
assert "Interrupted" 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 TestDistEach:
|
||||||
|
def test_simple(self, testdir):
|
||||||
|
testdir.makepyfile("""
|
||||||
|
def test_hello():
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest("--debug", "--dist=each", "--tx=2*popen")
|
||||||
|
assert not result.ret
|
||||||
|
result.stdout.fnmatch_lines(["*2 pass*"])
|
||||||
|
|
||||||
|
@py.test.mark.xfail(run=False,
|
||||||
|
reason="other python versions might not have py.test installed")
|
||||||
|
def test_simple_diffoutput(self, testdir):
|
||||||
|
interpreters = []
|
||||||
|
for name in ("python2.5", "python2.6"):
|
||||||
|
interp = py.path.local.sysfind(name)
|
||||||
|
if interp is None:
|
||||||
|
py.test.skip("%s not found" % name)
|
||||||
|
interpreters.append(interp)
|
||||||
|
|
||||||
|
testdir.makepyfile(__init__="", test_one="""
|
||||||
|
import sys
|
||||||
|
def test_hello():
|
||||||
|
print("%s...%s" % sys.version_info[:2])
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
args = ["--dist=each", "-v"]
|
||||||
|
args += ["--tx", "popen//python=%s" % interpreters[0]]
|
||||||
|
args += ["--tx", "popen//python=%s" % interpreters[1]]
|
||||||
|
result = testdir.runpytest(*args)
|
||||||
|
s = result.stdout.str()
|
||||||
|
assert "2...5" in s
|
||||||
|
assert "2...6" in s
|
||||||
|
|
||||||
|
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")
|
||||||
|
result.stdout.fnmatch_lines_random([
|
||||||
|
"*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*",
|
||||||
|
])
|
||||||
|
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*",
|
||||||
|
"*0*Python*",
|
||||||
|
"*def test_func():",
|
||||||
|
"> assert 0",
|
||||||
|
"E assert 0",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_teardownfails_one_function(testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
def test_func():
|
||||||
|
pass
|
||||||
|
def teardown_function(function):
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p, '-n1', '--tx=popen')
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*def teardown_function(function):*",
|
||||||
|
"*1 passed*1 error*"
|
||||||
|
])
|
||||||
|
|
||||||
|
@py.test.mark.xfail
|
||||||
|
def test_terminate_on_hangingnode(testdir):
|
||||||
|
p = testdir.makeconftest("""
|
||||||
|
def pytest__teardown_final(session):
|
||||||
|
if session.nodeid == "my": # running on slave
|
||||||
|
import time
|
||||||
|
time.sleep(3)
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my')
|
||||||
|
assert result.duration < 2.0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*killed*my*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_hooks(testdir):
|
||||||
|
testdir.makeconftest("""
|
||||||
|
import sys
|
||||||
|
def pytest_sessionstart(session):
|
||||||
|
sys.pytestsessionhooks = session
|
||||||
|
def pytest_sessionfinish(session):
|
||||||
|
if hasattr(session.config, 'slaveinput'):
|
||||||
|
name = "slave"
|
||||||
|
else:
|
||||||
|
name = "master"
|
||||||
|
f = open(name, "w")
|
||||||
|
f.write("xy")
|
||||||
|
f.close()
|
||||||
|
# let's fail on the slave
|
||||||
|
if name == "slave":
|
||||||
|
raise ValueError(42)
|
||||||
|
""")
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
import sys
|
||||||
|
def test_hello():
|
||||||
|
assert hasattr(sys, 'pytestsessionhooks')
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p, "--dist=each", "--tx=popen")
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*ValueError*",
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
assert not result.ret
|
||||||
|
d = result.parseoutcomes()
|
||||||
|
assert d['passed'] == 1
|
||||||
|
assert testdir.tmpdir.join("slave").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("--debug", p) # , "-n1")
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*ValueError*42*",
|
||||||
|
"*1 passed*1 error*",
|
||||||
|
])
|
||||||
|
assert result.ret
|
||||||
|
|
||||||
|
def test_crashing_item(testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
import py
|
||||||
|
import os
|
||||||
|
def test_crash():
|
||||||
|
py.process.kill(os.getpid())
|
||||||
|
def test_noncrash():
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest("-n2", p)
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*crashed*test_crash*",
|
||||||
|
"*1 failed*1 passed*"
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_skipping(testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
def test_crash():
|
||||||
|
pytest.skip("hello")
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest("-n1", '-rs', p)
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*hello*",
|
||||||
|
"*1 skipped*"
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_issue34_pluginloading_in_subprocess(testdir):
|
||||||
|
testdir.tmpdir.join("plugin123.py").write(py.code.Source("""
|
||||||
|
def pytest_namespace():
|
||||||
|
return {'sample_variable': 'testing'}
|
||||||
|
"""))
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
def test_hello():
|
||||||
|
assert pytest.sample_variable == "testing"
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest("-n1", "-p", "plugin123")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
|||||||
@@ -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()]
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ def getspecssh(config):
|
|||||||
if spec.ssh:
|
if spec.ssh:
|
||||||
if not py.path.local.sysfind("ssh"):
|
if not py.path.local.sysfind("ssh"):
|
||||||
py.test.skip("command not found: ssh")
|
py.test.skip("command not found: ssh")
|
||||||
return spec
|
return str(spec)
|
||||||
py.test.skip("need '--gx ssh=...'")
|
py.test.skip("need '--gx ssh=...'")
|
||||||
|
|
||||||
def getsocketspec(config):
|
def getsocketspec(config):
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ def test_functional_boxed(testdir):
|
|||||||
class TestOptionEffects:
|
class TestOptionEffects:
|
||||||
def test_boxed_option_default(self, testdir):
|
def test_boxed_option_default(self, testdir):
|
||||||
tmpdir = testdir.tmpdir.ensure("subdir", dir=1)
|
tmpdir = testdir.tmpdir.ensure("subdir", dir=1)
|
||||||
config = testdir.reparseconfig()
|
config = testdir.parseconfig()
|
||||||
config.initsession()
|
|
||||||
assert not config.option.boxed
|
assert not config.option.boxed
|
||||||
py.test.importorskip("execnet")
|
py.test.importorskip("execnet")
|
||||||
config = testdir.reparseconfig(['-d', tmpdir])
|
config = testdir.parseconfig('-d', tmpdir)
|
||||||
config.initsession()
|
|
||||||
assert not config.option.boxed
|
assert not config.option.boxed
|
||||||
|
|
||||||
def test_is_not_boxed_by_default(self, testdir):
|
def test_is_not_boxed_by_default(self, testdir):
|
||||||
config = testdir.reparseconfig([testdir.tmpdir])
|
config = testdir.parseconfig(testdir.tmpdir)
|
||||||
assert not config.option.boxed
|
assert not config.option.boxed
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import py
|
|
||||||
|
|
||||||
def test_dist_conftest_options(testdir):
|
|
||||||
p1 = testdir.tmpdir.ensure("dir", 'p1.py')
|
|
||||||
p1.dirpath("__init__.py").write("")
|
|
||||||
p1.dirpath("conftest.py").write(py.code.Source("""
|
|
||||||
import py
|
|
||||||
from py.builtin import print_
|
|
||||||
print_("importing conftest", __file__)
|
|
||||||
Option = py.test.config.Option
|
|
||||||
option = py.test.config.addoptions("someopt",
|
|
||||||
Option('--someopt', action="store_true",
|
|
||||||
dest="someopt", default=False))
|
|
||||||
dist_rsync_roots = ['../dir']
|
|
||||||
print_("added options", option)
|
|
||||||
print_("config file seen from conftest", py.test.config)
|
|
||||||
"""))
|
|
||||||
p1.write(py.code.Source("""
|
|
||||||
import py
|
|
||||||
from %s import conftest
|
|
||||||
from py.builtin import print_
|
|
||||||
def test_1():
|
|
||||||
print_("config from test_1", py.test.config)
|
|
||||||
print_("conftest from test_1", conftest.__file__)
|
|
||||||
print_("test_1: py.test.config.option.someopt", py.test.config.option.someopt)
|
|
||||||
print_("test_1: conftest", conftest)
|
|
||||||
print_("test_1: conftest.option.someopt", conftest.option.someopt)
|
|
||||||
assert conftest.option.someopt
|
|
||||||
""" % p1.dirpath().purebasename ))
|
|
||||||
result = testdir.runpytest('-d', '--tx=popen', p1, '--someopt')
|
|
||||||
assert result.ret == 0
|
|
||||||
result.stderr.fnmatch_lines([
|
|
||||||
"*Deprecation*pytest_addoptions*",
|
|
||||||
])
|
|
||||||
result.stdout.fnmatch_lines([
|
|
||||||
"*1 passed*",
|
|
||||||
])
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from xdist.dsession import DSession
|
from xdist.dsession import DSession, LoadScheduling, EachScheduling
|
||||||
from py._test import session as outcome
|
from _pytest import main as outcome
|
||||||
import py
|
import py
|
||||||
import execnet
|
import execnet
|
||||||
|
|
||||||
@@ -16,8 +16,11 @@ class MockNode:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sent = []
|
self.sent = []
|
||||||
|
|
||||||
def send(self, item):
|
def send_runtest(self, nodeid):
|
||||||
self.sent.append(item)
|
self.sent.append(nodeid)
|
||||||
|
|
||||||
|
def send_runtest_all(self):
|
||||||
|
self.sent.append("ALL")
|
||||||
|
|
||||||
def sendlist(self, items):
|
def sendlist(self, items):
|
||||||
self.sent.extend(items)
|
self.sent.extend(items)
|
||||||
@@ -29,373 +32,115 @@ def dumpqueue(queue):
|
|||||||
while queue.qsize():
|
while queue.qsize():
|
||||||
print(queue.get())
|
print(queue.get())
|
||||||
|
|
||||||
class TestDSession:
|
class TestEachScheduling:
|
||||||
def test_add_remove_node(self, testdir):
|
def test_schedule_load_simple(self):
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
node = MockNode()
|
|
||||||
rep = run(item, node)
|
|
||||||
session = DSession(item.config)
|
|
||||||
assert not session.node2pending
|
|
||||||
session.addnode(node)
|
|
||||||
assert len(session.node2pending) == 1
|
|
||||||
session.senditems_load([item])
|
|
||||||
pending = session.removenode(node)
|
|
||||||
assert pending == [item]
|
|
||||||
assert item not in session.item2nodes
|
|
||||||
l = session.removenode(node)
|
|
||||||
assert not l
|
|
||||||
|
|
||||||
def test_senditems_each_and_receive_with_two_nodes(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
node1 = MockNode()
|
node1 = MockNode()
|
||||||
node2 = MockNode()
|
node2 = MockNode()
|
||||||
session = DSession(item.config)
|
sched = EachScheduling(2)
|
||||||
session.addnode(node1)
|
sched.addnode(node1)
|
||||||
session.addnode(node2)
|
sched.addnode(node2)
|
||||||
session.senditems_each([item])
|
collection = ["a.py::test_1", ]
|
||||||
assert session.node2pending[node1] == [item]
|
assert not sched.collection_is_completed
|
||||||
assert session.node2pending[node2] == [item]
|
sched.addnode_collection(node1, collection)
|
||||||
assert node1 in session.item2nodes[item]
|
assert not sched.collection_is_completed
|
||||||
assert node2 in session.item2nodes[item]
|
sched.addnode_collection(node2, collection)
|
||||||
session.removeitem(item, node1)
|
assert sched.collection_is_completed
|
||||||
assert session.item2nodes[item] == [node2]
|
assert sched.node2collection[node1] == collection
|
||||||
session.removeitem(item, node2)
|
assert sched.node2collection[node2] == collection
|
||||||
assert not session.node2pending[node1]
|
sched.init_distribute()
|
||||||
assert not session.item2nodes
|
assert not sched.tests_finished()
|
||||||
|
assert node1.sent == ['ALL']
|
||||||
|
assert node2.sent == ['ALL']
|
||||||
|
sched.remove_item(node1, collection[0])
|
||||||
|
assert not sched.tests_finished()
|
||||||
|
sched.remove_item(node2, collection[0])
|
||||||
|
assert sched.tests_finished()
|
||||||
|
|
||||||
def test_senditems_load_and_receive_one_node(self, testdir):
|
def test_schedule_remove_node(self):
|
||||||
item = testdir.getitem("def test_func(): pass")
|
node1 = MockNode()
|
||||||
node = MockNode()
|
sched = EachScheduling(1)
|
||||||
rep = run(item, node)
|
sched.addnode(node1)
|
||||||
session = DSession(item.config)
|
collection = ["a.py::test_1", ]
|
||||||
session.addnode(node)
|
assert not sched.collection_is_completed
|
||||||
session.senditems_load([item])
|
sched.addnode_collection(node1, collection)
|
||||||
assert session.node2pending[node] == [item]
|
assert sched.collection_is_completed
|
||||||
assert session.item2nodes[item] == [node]
|
assert sched.node2collection[node1] == collection
|
||||||
session.removeitem(item, node)
|
sched.init_distribute()
|
||||||
assert not session.node2pending[node]
|
assert not sched.tests_finished()
|
||||||
assert not session.item2nodes
|
crashitem = sched.remove_node(node1)
|
||||||
|
assert crashitem
|
||||||
|
assert sched.tests_finished()
|
||||||
|
assert not sched.hasnodes()
|
||||||
|
|
||||||
def test_triggertesting_collect(self, testdir):
|
class TestLoadScheduling:
|
||||||
modcol = testdir.getmodulecol("""
|
def test_schedule_load_simple(self):
|
||||||
def test_func():
|
|
||||||
pass
|
|
||||||
""")
|
|
||||||
session = DSession(modcol.config)
|
|
||||||
reprec = testdir.getreportrecorder(session)
|
|
||||||
items = session.collect_all_items([modcol])
|
|
||||||
assert len(items) == 1
|
|
||||||
calls= reprec.getcalls("pytest_collectreport")
|
|
||||||
assert len(calls) == 1
|
|
||||||
call = calls[0]
|
|
||||||
assert len(call.report.result) == 1
|
|
||||||
|
|
||||||
def test_senditems_load(self, testdir, monkeypatch):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session = DSession(item.config)
|
|
||||||
node1 = MockNode()
|
node1 = MockNode()
|
||||||
node2 = MockNode()
|
node2 = MockNode()
|
||||||
session.addnode(node1)
|
sched = LoadScheduling(2)
|
||||||
session.addnode(node2)
|
sched.addnode(node1)
|
||||||
monkeypatch.setattr(session, 'ITEM_CHUNKSIZE', 3)
|
sched.addnode(node2)
|
||||||
session.senditems_load([item] * (2*session.ITEM_CHUNKSIZE +1))
|
collection = ["a.py::test_1", "a.py::test_2"]
|
||||||
|
assert not sched.collection_is_completed
|
||||||
|
sched.addnode_collection(node1, collection)
|
||||||
|
assert not sched.collection_is_completed
|
||||||
|
sched.addnode_collection(node2, collection)
|
||||||
|
assert sched.collection_is_completed
|
||||||
|
assert sched.node2collection[node1] == collection
|
||||||
|
assert sched.node2collection[node2] == collection
|
||||||
|
sched.init_distribute()
|
||||||
|
assert not sched.tests_finished()
|
||||||
|
assert len(node1.sent) == 1
|
||||||
|
assert len(node2.sent) == 1
|
||||||
|
x = sorted(node1.sent + node2.sent)
|
||||||
|
assert x == collection
|
||||||
|
sched.remove_item(node1, node1.sent[0])
|
||||||
|
sched.remove_item(node2, node2.sent[0])
|
||||||
|
assert sched.tests_finished()
|
||||||
|
assert not sched.pending
|
||||||
|
|
||||||
|
def test_init_distribute_chunksize(self):
|
||||||
|
sched = LoadScheduling(2)
|
||||||
|
node1 = MockNode()
|
||||||
|
node2 = MockNode()
|
||||||
|
sched.addnode(node1)
|
||||||
|
sched.addnode(node2)
|
||||||
|
sched.ITEM_CHUNKSIZE = 2
|
||||||
|
col = ["xyz"] * (2*sched.ITEM_CHUNKSIZE +1)
|
||||||
|
sched.addnode_collection(node1, col)
|
||||||
|
sched.addnode_collection(node2, col)
|
||||||
|
sched.init_distribute()
|
||||||
sent1 = node1.sent
|
sent1 = node1.sent
|
||||||
sent2 = node2.sent
|
sent2 = node2.sent
|
||||||
chunkitems = [item] * session.ITEM_CHUNKSIZE
|
chunkitems = col[:sched.ITEM_CHUNKSIZE]
|
||||||
assert sent1 == chunkitems
|
assert sent1 == chunkitems
|
||||||
assert sent2 == chunkitems
|
assert sent2 == chunkitems
|
||||||
assert session.node2pending[node1] == sent1
|
assert sched.node2pending[node1] == sent1
|
||||||
assert session.node2pending[node2] == sent2
|
assert sched.node2pending[node2] == sent2
|
||||||
name, args, kwargs = session.queue.get(block=False)
|
assert len(sched.pending) == 1
|
||||||
assert name == "pytest_rescheduleitems"
|
for node in (node1, node2):
|
||||||
assert kwargs['items'] == [item]
|
for i in range(sched.ITEM_CHUNKSIZE):
|
||||||
|
sched.remove_item(node, "xyz")
|
||||||
|
assert not sched.pending
|
||||||
|
|
||||||
|
def test_add_remove_node(self):
|
||||||
def test_keyboardinterrupt(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session = DSession(item.config)
|
|
||||||
def raise_(timeout=None): raise KeyboardInterrupt()
|
|
||||||
session.queue.get = raise_
|
|
||||||
exitstatus = session.loop([])
|
|
||||||
assert exitstatus == outcome.EXIT_INTERRUPTED
|
|
||||||
|
|
||||||
def test_internalerror(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session = DSession(item.config)
|
|
||||||
def raise_(): raise ValueError()
|
|
||||||
session.queue.get = raise_
|
|
||||||
exitstatus = session.loop([])
|
|
||||||
assert exitstatus == outcome.EXIT_INTERNALERROR
|
|
||||||
|
|
||||||
def test_rescheduleevent(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session = DSession(item.config)
|
|
||||||
node = MockNode()
|
node = MockNode()
|
||||||
session.addnode(node)
|
sched = LoadScheduling(1)
|
||||||
loopstate = session._initloopstate([])
|
sched.addnode(node)
|
||||||
session.queueevent("pytest_rescheduleitems", items=[item])
|
collection = ["test_file.py::test_func"]
|
||||||
session.loop_once(loopstate)
|
sched.addnode_collection(node, collection)
|
||||||
# we need to do work because nothing is pending / we would not wake up
|
assert sched.collection_is_completed
|
||||||
assert loopstate.dowork == True
|
sched.init_distribute()
|
||||||
|
assert not sched.pending
|
||||||
|
crashitem = sched.remove_node(node)
|
||||||
|
assert crashitem == collection[0]
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
session.queueevent(None)
|
class TestDistReporter:
|
||||||
session.loop_once(loopstate)
|
|
||||||
session.queueevent(None)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
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.loop_once(loopstate)
|
|
||||||
assert loopstate.shuttingdown
|
|
||||||
assert not loopstate.testsfailed
|
|
||||||
|
|
||||||
def test_no_node_remaining_for_tests(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
# setup a session with one node
|
|
||||||
session = DSession(item.config)
|
|
||||||
node = MockNode()
|
|
||||||
session.addnode(node)
|
|
||||||
|
|
||||||
# setup a HostDown event
|
|
||||||
session.queueevent("pytest_testnodedown", node=node, error=None)
|
|
||||||
|
|
||||||
loopstate = session._initloopstate([item])
|
|
||||||
loopstate.dowork = False
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
dumpqueue(session.queue)
|
|
||||||
assert loopstate.exitstatus == outcome.EXIT_NOHOSTS
|
|
||||||
|
|
||||||
def test_removeitem_from_failing_teardown(self, testdir):
|
|
||||||
# teardown reports only come in when they signal a failure
|
|
||||||
# internal session-management should basically ignore them
|
|
||||||
# XXX probably it'S best to invent a new error hook for
|
|
||||||
# teardown/setup related failures
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_one():
|
|
||||||
pass
|
|
||||||
def teardown_function(function):
|
|
||||||
assert 0
|
|
||||||
""")
|
|
||||||
item1, = modcol.collect()
|
|
||||||
|
|
||||||
# setup a session with two nodes
|
|
||||||
session = DSession(item1.config)
|
|
||||||
node1, node2 = MockNode(), MockNode()
|
|
||||||
session.addnode(node1)
|
|
||||||
session.addnode(node2)
|
|
||||||
|
|
||||||
# have one test pending for a node that goes down
|
|
||||||
session.senditems_each([item1])
|
|
||||||
nodes = session.item2nodes[item1]
|
|
||||||
class rep:
|
|
||||||
failed = True
|
|
||||||
item = item1
|
|
||||||
node = nodes[0]
|
|
||||||
when = "call"
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=rep)
|
|
||||||
reprec = testdir.getreportrecorder(session)
|
|
||||||
print(session.item2nodes)
|
|
||||||
loopstate = session._initloopstate([])
|
|
||||||
assert len(session.item2nodes[item1]) == 2
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(session.item2nodes[item1]) == 1
|
|
||||||
rep.when = "teardown"
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=rep)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(session.item2nodes[item1]) == 1
|
|
||||||
|
|
||||||
def test_testnodeready_adds_to_available(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
# setup a session with two nodes
|
|
||||||
session = DSession(item.config)
|
|
||||||
node1 = MockNode()
|
|
||||||
session.queueevent("pytest_testnodeready", node=node1)
|
|
||||||
loopstate = session._initloopstate([item])
|
|
||||||
loopstate.dowork = False
|
|
||||||
assert len(session.node2pending) == 0
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(session.node2pending) == 1
|
|
||||||
|
|
||||||
def runthrough(self, item, excinfo=None):
|
|
||||||
session = DSession(item.config)
|
|
||||||
node = MockNode()
|
|
||||||
session.addnode(node)
|
|
||||||
loopstate = session._initloopstate([item])
|
|
||||||
|
|
||||||
session.queueevent(None)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
|
|
||||||
assert node.sent == [item]
|
|
||||||
ev = run(item, node, excinfo=excinfo)
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=ev)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert loopstate.shuttingdown
|
|
||||||
session.queueevent("pytest_testnodedown", node=node, error=None)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
dumpqueue(session.queue)
|
|
||||||
return session, loopstate.exitstatus
|
|
||||||
|
|
||||||
def test_exit_completed_tests_ok(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session, exitstatus = self.runthrough(item)
|
|
||||||
assert exitstatus == outcome.EXIT_OK
|
|
||||||
|
|
||||||
def test_exit_completed_tests_fail(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): 0/0")
|
|
||||||
session, exitstatus = self.runthrough(item, excinfo="fail")
|
|
||||||
assert exitstatus == outcome.EXIT_TESTSFAILED
|
|
||||||
|
|
||||||
def test_exit_on_first_failing(self, testdir):
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_fail():
|
|
||||||
assert 0
|
|
||||||
def test_pass():
|
|
||||||
pass
|
|
||||||
""")
|
|
||||||
modcol.config.option.exitfirst = True
|
|
||||||
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, None)
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=ev1) # a failing one
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=ev2)
|
|
||||||
# now call the loop
|
|
||||||
loopstate = session._initloopstate(items)
|
|
||||||
from xdist.dsession import ExitFirstInterrupt
|
|
||||||
py.test.raises(ExitFirstInterrupt, "session.loop_once(loopstate)")
|
|
||||||
assert loopstate.testsfailed
|
|
||||||
#assert loopstate.shuttingdown
|
|
||||||
|
|
||||||
def test_shuttingdown_filters(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session = DSession(item.config)
|
|
||||||
node = MockNode()
|
|
||||||
session.addnode(node)
|
|
||||||
loopstate = session._initloopstate([])
|
|
||||||
loopstate.shuttingdown = True
|
|
||||||
reprec = testdir.getreportrecorder(session)
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=run(item, node))
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert not reprec.getcalls("pytest_testnodedown")
|
|
||||||
session.queueevent("pytest_testnodedown", node=node, error=None)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert reprec.getcall('pytest_testnodedown').node == node
|
|
||||||
|
|
||||||
def test_filteritems(self, testdir):
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_fail():
|
|
||||||
assert 0
|
|
||||||
def test_pass():
|
|
||||||
pass
|
|
||||||
""")
|
|
||||||
session = DSession(modcol.config)
|
|
||||||
|
|
||||||
modcol.config.option.keyword = "nothing"
|
|
||||||
dsel = session.filteritems([modcol])
|
|
||||||
assert dsel == [modcol]
|
|
||||||
items = modcol.collect()
|
|
||||||
hookrecorder = testdir.getreportrecorder(session).hookrecorder
|
|
||||||
remaining = session.filteritems(items)
|
|
||||||
assert remaining == []
|
|
||||||
|
|
||||||
event = hookrecorder.getcalls("pytest_deselected")[-1]
|
|
||||||
assert event.items == items
|
|
||||||
|
|
||||||
modcol.config.option.keyword = "test_fail"
|
|
||||||
remaining = session.filteritems(items)
|
|
||||||
assert remaining == [items[0]]
|
|
||||||
|
|
||||||
event = hookrecorder.getcalls("pytest_deselected")[-1]
|
|
||||||
assert event.items == [items[1]]
|
|
||||||
|
|
||||||
def test_testnodedown_shutdown_after_completion(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
session = DSession(item.config)
|
|
||||||
|
|
||||||
node = MockNode()
|
|
||||||
session.addnode(node)
|
|
||||||
session.senditems_load([item])
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=run(item, node))
|
|
||||||
loopstate = session._initloopstate([])
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert node._shutdown is True
|
|
||||||
assert loopstate.exitstatus is None, "loop did not wait for testnodedown"
|
|
||||||
assert loopstate.shuttingdown
|
|
||||||
session.queueevent("pytest_testnodedown", node=node, error=None)
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert loopstate.exitstatus == 0
|
|
||||||
|
|
||||||
def test_nopending_but_collection_remains(self, testdir):
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_fail():
|
|
||||||
assert 0
|
|
||||||
def test_pass():
|
|
||||||
pass
|
|
||||||
""")
|
|
||||||
session = DSession(modcol.config)
|
|
||||||
node = MockNode()
|
|
||||||
session.addnode(node)
|
|
||||||
|
|
||||||
colreport = modcol.config.hook.pytest_make_collect_report(collector=modcol)
|
|
||||||
item1, item2 = colreport.result
|
|
||||||
session.senditems_load([item1])
|
|
||||||
# node2pending will become empty when the loop sees the report
|
|
||||||
rep = run(item1, node)
|
|
||||||
session.queueevent("pytest_runtest_logreport", report=run(item1, node))
|
|
||||||
|
|
||||||
# but we have a collection pending
|
|
||||||
session.queueevent("pytest_collectreport", report=colreport)
|
|
||||||
|
|
||||||
loopstate = session._initloopstate([])
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert loopstate.exitstatus is None, "loop did not care for collection report"
|
|
||||||
assert not loopstate.colitems
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert loopstate.colitems == colreport.result
|
|
||||||
assert loopstate.exitstatus is None, "loop did not care for colitems"
|
|
||||||
|
|
||||||
def test_dist_some_tests(self, testdir):
|
|
||||||
p1 = testdir.makepyfile(test_one="""
|
|
||||||
def test_1():
|
|
||||||
pass
|
|
||||||
def test_x():
|
|
||||||
import py
|
|
||||||
py.test.skip("aaa")
|
|
||||||
def test_fail():
|
|
||||||
assert 0
|
|
||||||
""")
|
|
||||||
config = testdir.parseconfig('-d', p1, '--tx=popen')
|
|
||||||
dsession = DSession(config)
|
|
||||||
hookrecorder = testdir.getreportrecorder(config).hookrecorder
|
|
||||||
dsession.main([config.getnode(p1)])
|
|
||||||
rep = hookrecorder.popcall("pytest_runtest_logreport").report
|
|
||||||
assert rep.passed
|
|
||||||
rep = hookrecorder.popcall("pytest_runtest_logreport").report
|
|
||||||
assert rep.skipped
|
|
||||||
rep = hookrecorder.popcall("pytest_runtest_logreport").report
|
|
||||||
assert rep.failed
|
|
||||||
# see that the node is really down
|
|
||||||
node = hookrecorder.popcall("pytest_testnodedown").node
|
|
||||||
assert node.gateway.spec.popen
|
|
||||||
#XXX eq.geteventargs("pytest_sessionfinish")
|
|
||||||
|
|
||||||
|
@py.test.mark.xfail
|
||||||
def test_rsync_printing(self, testdir, linecomp):
|
def test_rsync_printing(self, testdir, linecomp):
|
||||||
config = testdir.parseconfig()
|
config = testdir.parseconfig()
|
||||||
from py._plugin.pytest_terminal import TerminalReporter
|
from _pytest.pytest_terminal import TerminalReporter
|
||||||
rep = TerminalReporter(config, file=linecomp.stringio)
|
rep = TerminalReporter(config, file=linecomp.stringio)
|
||||||
config.pluginmanager.register(rep, "terminalreporter")
|
config.pluginmanager.register(rep, "terminalreporter")
|
||||||
dsession = DSession(config)
|
dsession = DSession(config)
|
||||||
@@ -411,123 +156,11 @@ class TestDSession:
|
|||||||
# platform = "xyz"
|
# platform = "xyz"
|
||||||
# cwd = "qwe"
|
# cwd = "qwe"
|
||||||
|
|
||||||
#dsession.pytest_gwmanage_newgateway(gw1, rinfo)
|
#dsession.pytest_xdist_newgateway(gw1, rinfo)
|
||||||
#linecomp.assert_contains_lines([
|
#linecomp.assert_contains_lines([
|
||||||
# "*X1*popen*xyz*2.5*"
|
# "*X1*popen*xyz*2.5*"
|
||||||
#])
|
#])
|
||||||
dsession.pytest_gwmanage_rsyncstart(source="hello", gateways=[gw1, gw2])
|
dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2])
|
||||||
linecomp.assert_contains_lines([
|
linecomp.assert_contains_lines([
|
||||||
"[X1,X2] rsyncing: hello",
|
"[X1,X2] rsyncing: hello",
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_collected_function_causes_remote_skip(testdir):
|
|
||||||
sub = testdir.mkpydir("testing")
|
|
||||||
sub.join("test_module.py").write(py.code.Source("""
|
|
||||||
import py
|
|
||||||
path = py.path.local(%r)
|
|
||||||
if path.check():
|
|
||||||
path.remove()
|
|
||||||
else:
|
|
||||||
py.test.skip("remote skip")
|
|
||||||
def test_func():
|
|
||||||
pass
|
|
||||||
def test_func2():
|
|
||||||
pass
|
|
||||||
""" % str(sub.ensure("somefile"))))
|
|
||||||
result = testdir.runpytest('-v', '--dist=each', '--tx=popen')
|
|
||||||
result.stdout.fnmatch_lines([
|
|
||||||
"*2 skipped*"
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_teardownfails_one_function(testdir):
|
|
||||||
p = testdir.makepyfile("""
|
|
||||||
def test_func():
|
|
||||||
pass
|
|
||||||
def teardown_function(function):
|
|
||||||
assert 0
|
|
||||||
""")
|
|
||||||
result = testdir.runpytest(p, '--dist=each', '--tx=popen')
|
|
||||||
result.stdout.fnmatch_lines([
|
|
||||||
"*def teardown_function(function):*",
|
|
||||||
"*1 passed*1 error*"
|
|
||||||
])
|
|
||||||
|
|
||||||
@py.test.mark.xfail
|
|
||||||
def test_terminate_on_hangingnode(testdir):
|
|
||||||
p = testdir.makeconftest("""
|
|
||||||
def pytest__teardown_final(session):
|
|
||||||
if session.nodeid == "my": # running on slave
|
|
||||||
import time
|
|
||||||
time.sleep(3)
|
|
||||||
""")
|
|
||||||
result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my')
|
|
||||||
assert result.duration < 2.0
|
|
||||||
result.stdout.fnmatch_lines([
|
|
||||||
"*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*",
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_crashing_item(testdir):
|
|
||||||
p = testdir.makepyfile("""
|
|
||||||
import py
|
|
||||||
import os
|
|
||||||
def test_crash():
|
|
||||||
py.process.kill(os.getpid())
|
|
||||||
def test_noncrash():
|
|
||||||
pass
|
|
||||||
""")
|
|
||||||
result = testdir.runpytest("-n2", p)
|
|
||||||
result.stdout.fnmatch_lines([
|
|
||||||
"*crashed*test_crash*",
|
|
||||||
"*1 failed*1 passed*"
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import py
|
|
||||||
import os
|
|
||||||
from xdist.gwmanage import GatewayManager, HostRSync
|
|
||||||
from py._test.pluginmanager import HookRelay, Registry
|
|
||||||
from py._plugin import hookspec
|
|
||||||
from xdist import newhooks
|
|
||||||
import execnet
|
|
||||||
|
|
||||||
def pytest_funcarg__hookrecorder(request):
|
|
||||||
_pytest = request.getfuncargvalue('_pytest')
|
|
||||||
hook = request.getfuncargvalue('hook')
|
|
||||||
return _pytest.gethookrecorder(hook)
|
|
||||||
|
|
||||||
def pytest_funcarg__hook(request):
|
|
||||||
return HookRelay([hookspec, newhooks], Registry())
|
|
||||||
|
|
||||||
class TestGatewayManagerPopen:
|
|
||||||
def test_popen_no_default_chdir(self, hook):
|
|
||||||
gm = GatewayManager(["popen"], hook)
|
|
||||||
assert gm.specs[0].chdir is None
|
|
||||||
|
|
||||||
def test_default_chdir(self, hook):
|
|
||||||
l = ["ssh=noco", "socket=xyz"]
|
|
||||||
for spec in GatewayManager(l, hook).specs:
|
|
||||||
assert spec.chdir == "pyexecnetcache"
|
|
||||||
for spec in GatewayManager(l, hook, defaultchdir="abc").specs:
|
|
||||||
assert spec.chdir == "abc"
|
|
||||||
|
|
||||||
def test_popen_makegateway_events(self, hook, hookrecorder, _pytest):
|
|
||||||
hm = GatewayManager(["popen"] * 2, hook)
|
|
||||||
hm.makegateways()
|
|
||||||
call = hookrecorder.popcall("pytest_gwmanage_newgateway")
|
|
||||||
assert call.gateway.spec == execnet.XSpec("popen")
|
|
||||||
assert call.gateway.id == "gw0"
|
|
||||||
assert call.platinfo.executable == call.gateway._rinfo().executable
|
|
||||||
call = hookrecorder.popcall("pytest_gwmanage_newgateway")
|
|
||||||
assert call.gateway.id == "gw1"
|
|
||||||
assert len(hm.group) == 2
|
|
||||||
hm.exit()
|
|
||||||
assert not len(hm.group)
|
|
||||||
|
|
||||||
def test_popens_rsync(self, hook, mysetup):
|
|
||||||
source = mysetup.source
|
|
||||||
hm = GatewayManager(["popen"] * 2, hook)
|
|
||||||
hm.makegateways()
|
|
||||||
assert len(hm.group) == 2
|
|
||||||
for gw in hm.group:
|
|
||||||
class pseudoexec:
|
|
||||||
args = []
|
|
||||||
def __init__(self, *args):
|
|
||||||
self.args.extend(args)
|
|
||||||
def waitclose(self):
|
|
||||||
pass
|
|
||||||
gw.remote_exec = pseudoexec
|
|
||||||
l = []
|
|
||||||
hm.rsync(source, notify=lambda *args: l.append(args))
|
|
||||||
assert not l
|
|
||||||
hm.exit()
|
|
||||||
assert not len(hm.group)
|
|
||||||
assert "sys.path.insert" in gw.remote_exec.args[0]
|
|
||||||
|
|
||||||
def test_rsync_popen_with_path(self, hook, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
hm = GatewayManager(["popen//chdir=%s" %dest] * 1, hook)
|
|
||||||
hm.makegateways()
|
|
||||||
source.ensure("dir1", "dir2", "hello")
|
|
||||||
l = []
|
|
||||||
hm.rsync(source, notify=lambda *args: l.append(args))
|
|
||||||
assert len(l) == 1
|
|
||||||
assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source)
|
|
||||||
hm.exit()
|
|
||||||
dest = dest.join(source.basename)
|
|
||||||
assert dest.join("dir1").check()
|
|
||||||
assert dest.join("dir1", "dir2").check()
|
|
||||||
assert dest.join("dir1", "dir2", 'hello').check()
|
|
||||||
|
|
||||||
def test_rsync_same_popen_twice(self, hook, mysetup, hookrecorder):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
hm = GatewayManager(["popen//chdir=%s" %dest] * 2, hook)
|
|
||||||
hm.makegateways()
|
|
||||||
source.ensure("dir1", "dir2", "hello")
|
|
||||||
hm.rsync(source)
|
|
||||||
call = hookrecorder.popcall("pytest_gwmanage_rsyncstart")
|
|
||||||
assert call.source == source
|
|
||||||
assert len(call.gateways) == 1
|
|
||||||
assert call.gateways[0] in hm.group
|
|
||||||
call = hookrecorder.popcall("pytest_gwmanage_rsyncfinish")
|
|
||||||
|
|
||||||
class pytest_funcarg__mysetup:
|
|
||||||
def __init__(self, request):
|
|
||||||
tmp = request.getfuncargvalue('tmpdir')
|
|
||||||
self.source = tmp.mkdir("source")
|
|
||||||
self.dest = tmp.mkdir("dest")
|
|
||||||
|
|
||||||
class TestHRSync:
|
|
||||||
def test_hrsync_filter(self, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
source.ensure("dir", "file.txt")
|
|
||||||
source.ensure(".svn", "entries")
|
|
||||||
source.ensure(".somedotfile", "moreentries")
|
|
||||||
source.ensure("somedir", "editfile~")
|
|
||||||
syncer = HostRSync(source)
|
|
||||||
l = list(source.visit(rec=syncer.filter,
|
|
||||||
fil=syncer.filter))
|
|
||||||
assert len(l) == 3
|
|
||||||
basenames = [x.basename for x in l]
|
|
||||||
assert 'dir' in basenames
|
|
||||||
assert 'file.txt' in basenames
|
|
||||||
assert 'somedir' in basenames
|
|
||||||
|
|
||||||
def test_hrsync_one_host(self, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
gw = execnet.makegateway("popen//chdir=%s" % dest)
|
|
||||||
finished = []
|
|
||||||
rsync = HostRSync(source)
|
|
||||||
rsync.add_target_host(gw, finished=lambda: finished.append(1))
|
|
||||||
source.join("hello.py").write("world")
|
|
||||||
rsync.send()
|
|
||||||
gw.exit()
|
|
||||||
assert dest.join(source.basename, "hello.py").check()
|
|
||||||
assert len(finished) == 1
|
|
||||||
255
testing/test_looponfail.py
Normal file
255
testing/test_looponfail.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import py
|
||||||
|
from xdist.looponfail import RemoteControl
|
||||||
|
from xdist.looponfail import StatRecorder
|
||||||
|
|
||||||
|
class TestStatRecorder:
|
||||||
|
def test_filechange(self, tmpdir):
|
||||||
|
tmp = tmpdir
|
||||||
|
hello = tmp.ensure("hello.py")
|
||||||
|
sd = StatRecorder([tmp])
|
||||||
|
changed = sd.check()
|
||||||
|
assert not changed
|
||||||
|
|
||||||
|
hello.write("world")
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
p = tmp.ensure("new.py")
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
p.remove()
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
tmp.join("a", "b", "c.py").ensure()
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
tmp.join("a", "c.txt").ensure()
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
changed = sd.check()
|
||||||
|
assert not changed
|
||||||
|
|
||||||
|
tmp.join("a").remove()
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
def test_filechange_deletion_race(self, tmpdir, monkeypatch):
|
||||||
|
tmp = tmpdir
|
||||||
|
sd = StatRecorder([tmp])
|
||||||
|
changed = sd.check()
|
||||||
|
assert not changed
|
||||||
|
|
||||||
|
p = tmp.ensure("new.py")
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
p.remove()
|
||||||
|
# make check()'s visit() call return our just removed
|
||||||
|
# path as if we were in a race condition
|
||||||
|
monkeypatch.setattr(tmp, 'visit', lambda *args: [p])
|
||||||
|
|
||||||
|
changed = sd.check()
|
||||||
|
assert changed
|
||||||
|
|
||||||
|
def test_pycremoval(self, tmpdir):
|
||||||
|
tmp = tmpdir
|
||||||
|
hello = tmp.ensure("hello.py")
|
||||||
|
sd = StatRecorder([tmp])
|
||||||
|
changed = sd.check()
|
||||||
|
assert not changed
|
||||||
|
|
||||||
|
pycfile = hello + "c"
|
||||||
|
pycfile.ensure()
|
||||||
|
changed = sd.check()
|
||||||
|
assert not changed
|
||||||
|
|
||||||
|
hello.write("world")
|
||||||
|
changed = sd.check()
|
||||||
|
assert not pycfile.check()
|
||||||
|
|
||||||
|
def test_waitonchange(self, tmpdir, monkeypatch):
|
||||||
|
tmp = tmpdir
|
||||||
|
sd = StatRecorder([tmp])
|
||||||
|
|
||||||
|
l = [True, False]
|
||||||
|
monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop())
|
||||||
|
sd.waitonchange(checkinterval=0.2)
|
||||||
|
assert not l
|
||||||
|
|
||||||
|
class TestRemoteControl:
|
||||||
|
def test_nofailures(self, testdir):
|
||||||
|
item = testdir.getitem("def test_func(): pass\n")
|
||||||
|
control = RemoteControl(item.config)
|
||||||
|
control.setup()
|
||||||
|
topdir, failures = control.runsession()[:2]
|
||||||
|
assert not failures
|
||||||
|
|
||||||
|
def test_failures_somewhere(self, testdir):
|
||||||
|
item = testdir.getitem("def test_func(): assert 0\n")
|
||||||
|
control = RemoteControl(item.config)
|
||||||
|
control.setup()
|
||||||
|
failures = control.runsession()
|
||||||
|
assert failures
|
||||||
|
control.setup()
|
||||||
|
item.fspath.write("def test_func(): assert 1\n")
|
||||||
|
pyc = item.fspath.new(ext=".pyc")
|
||||||
|
if pyc.check():
|
||||||
|
pyc.remove()
|
||||||
|
topdir, failures = control.runsession()[:2]
|
||||||
|
assert not failures
|
||||||
|
|
||||||
|
def test_failure_change(self, testdir):
|
||||||
|
modcol = testdir.getitem("""
|
||||||
|
def test_func():
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
control = RemoteControl(modcol.config)
|
||||||
|
control.loop_once()
|
||||||
|
assert control.failures
|
||||||
|
modcol.fspath.write(py.code.Source("""
|
||||||
|
def test_func():
|
||||||
|
assert 1
|
||||||
|
def test_new():
|
||||||
|
assert 0
|
||||||
|
"""))
|
||||||
|
pyc = modcol.fspath.new(ext=".pyc")
|
||||||
|
if pyc.check():
|
||||||
|
pyc.remove()
|
||||||
|
control.loop_once()
|
||||||
|
assert not control.failures
|
||||||
|
control.loop_once()
|
||||||
|
assert control.failures
|
||||||
|
assert str(control.failures).find("test_new") != -1
|
||||||
|
|
||||||
|
def test_failure_subdir_no_init(self, testdir):
|
||||||
|
modcol = testdir.getitem("""
|
||||||
|
def test_func():
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
parent = modcol.fspath.dirpath().dirpath()
|
||||||
|
parent.chdir()
|
||||||
|
modcol.config.args = [py.path.local(x).relto(parent)
|
||||||
|
for x in modcol.config.args]
|
||||||
|
control = RemoteControl(modcol.config)
|
||||||
|
control.loop_once()
|
||||||
|
assert control.failures
|
||||||
|
control.loop_once()
|
||||||
|
assert control.failures
|
||||||
|
|
||||||
|
class TestLooponFailing:
|
||||||
|
def test_looponfail_from_fail_to_ok(self, testdir):
|
||||||
|
modcol = testdir.getmodulecol("""
|
||||||
|
def test_one():
|
||||||
|
x = 0
|
||||||
|
assert x == 1
|
||||||
|
def test_two():
|
||||||
|
assert 1
|
||||||
|
""")
|
||||||
|
remotecontrol = RemoteControl(modcol.config)
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 1
|
||||||
|
|
||||||
|
modcol.fspath.write(py.code.Source("""
|
||||||
|
def test_one():
|
||||||
|
assert 1
|
||||||
|
def test_two():
|
||||||
|
assert 1
|
||||||
|
"""))
|
||||||
|
removepyc(modcol.fspath)
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert not remotecontrol.failures
|
||||||
|
|
||||||
|
def test_looponfail_from_one_to_two_tests(self, testdir):
|
||||||
|
modcol = testdir.getmodulecol("""
|
||||||
|
def test_one():
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
remotecontrol = RemoteControl(modcol.config)
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 1
|
||||||
|
assert 'test_one' in remotecontrol.failures[0]
|
||||||
|
|
||||||
|
modcol.fspath.write(py.code.Source("""
|
||||||
|
def test_one():
|
||||||
|
assert 1 # passes now
|
||||||
|
def test_two():
|
||||||
|
assert 0 # new and fails
|
||||||
|
"""))
|
||||||
|
removepyc(modcol.fspath)
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 0
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 1
|
||||||
|
assert 'test_one' not in remotecontrol.failures[0]
|
||||||
|
assert 'test_two' in remotecontrol.failures[0]
|
||||||
|
|
||||||
|
def test_looponfail_removed_test(self, testdir):
|
||||||
|
modcol = testdir.getmodulecol("""
|
||||||
|
def test_one():
|
||||||
|
assert 0
|
||||||
|
def test_two():
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
remotecontrol = RemoteControl(modcol.config)
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 2
|
||||||
|
|
||||||
|
modcol.fspath.write(py.code.Source("""
|
||||||
|
def test_xxx(): # renamed test
|
||||||
|
assert 0
|
||||||
|
def test_two():
|
||||||
|
assert 1 # pass now
|
||||||
|
"""))
|
||||||
|
removepyc(modcol.fspath)
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 0
|
||||||
|
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
assert len(remotecontrol.failures) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestFunctional:
|
||||||
|
def test_fail_to_ok(self, testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
def test_one():
|
||||||
|
x = 0
|
||||||
|
assert x == 1
|
||||||
|
""")
|
||||||
|
#p = testdir.mkdir("sub").join(p1.basename)
|
||||||
|
#p1.move(p)
|
||||||
|
child = testdir.spawn_pytest("-f %s --traceconfig" % p)
|
||||||
|
child.expect("def test_one")
|
||||||
|
child.expect("x == 1")
|
||||||
|
child.expect("1 failed")
|
||||||
|
child.expect("### LOOPONFAILING ####")
|
||||||
|
child.expect("waiting for changes")
|
||||||
|
p.write(py.code.Source("""
|
||||||
|
def test_one():
|
||||||
|
x = 1
|
||||||
|
assert x == 1
|
||||||
|
"""))
|
||||||
|
child.expect(".*1 passed.*")
|
||||||
|
child.kill(15)
|
||||||
|
|
||||||
|
def test_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)
|
||||||
|
|
||||||
|
def removepyc(path):
|
||||||
|
# XXX damn those pyc files
|
||||||
|
pyc = path + "c"
|
||||||
|
if pyc.check():
|
||||||
|
pyc.remove()
|
||||||
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
|
|
||||||
import py
|
|
||||||
import sys
|
|
||||||
import execnet
|
|
||||||
|
|
||||||
Queue = py.builtin._tryimport('queue', 'Queue').Queue
|
|
||||||
|
|
||||||
from xdist.mypickle import ImmutablePickler, PickleChannel
|
|
||||||
from xdist.mypickle import UnpickleError, makekey
|
|
||||||
# first let's test some basic functionality
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
|
||||||
if 'picklemod' in metafunc.funcargnames:
|
|
||||||
import pickle
|
|
||||||
metafunc.addcall(funcargs={'picklemod': pickle})
|
|
||||||
try:
|
|
||||||
import cPickle
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
metafunc.addcall(funcargs={'picklemod': cPickle})
|
|
||||||
elif "obj" in metafunc.funcargnames and "proto" in metafunc.funcargnames:
|
|
||||||
a1 = A()
|
|
||||||
a2 = A()
|
|
||||||
a2.a1 = a1
|
|
||||||
for proto in (0,1,2, -1):
|
|
||||||
for obj in {1:2}, [1,2,3], a1, a2:
|
|
||||||
metafunc.addcall(funcargs=dict(obj=obj, proto=proto))
|
|
||||||
|
|
||||||
def test_underlying_basic_pickling_mechanisms(picklemod):
|
|
||||||
f1 = py.io.BytesIO()
|
|
||||||
f2 = py.io.BytesIO()
|
|
||||||
|
|
||||||
pickler1 = picklemod.Pickler(f1)
|
|
||||||
unpickler1 = picklemod.Unpickler(f2)
|
|
||||||
|
|
||||||
pickler2 = picklemod.Pickler(f2)
|
|
||||||
unpickler2 = picklemod.Unpickler(f1)
|
|
||||||
|
|
||||||
#pickler1.memo = unpickler1.memo = {}
|
|
||||||
#pickler2.memo = unpickler2.memo = {}
|
|
||||||
|
|
||||||
d = {}
|
|
||||||
|
|
||||||
pickler1.dump(d)
|
|
||||||
f1.seek(0)
|
|
||||||
d_other = unpickler2.load()
|
|
||||||
|
|
||||||
# translate unpickler2 memo to pickler2
|
|
||||||
pickler2.memo = dict([(id(obj), (int(x), obj))
|
|
||||||
for x, obj in unpickler2.memo.items()])
|
|
||||||
|
|
||||||
pickler2.dump(d_other)
|
|
||||||
f2.seek(0)
|
|
||||||
|
|
||||||
unpickler1.memo = dict([(makekey(x), y)
|
|
||||||
for x, y in pickler1.memo.values()])
|
|
||||||
d_back = unpickler1.load()
|
|
||||||
assert d is d_back
|
|
||||||
|
|
||||||
|
|
||||||
class A:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_pickle_and_back_IS_same(obj, proto):
|
|
||||||
p1 = ImmutablePickler(uneven=False, protocol=proto)
|
|
||||||
p2 = ImmutablePickler(uneven=True, protocol=proto)
|
|
||||||
s1 = p1.dumps(obj)
|
|
||||||
d2 = p2.loads(s1)
|
|
||||||
s2 = p2.dumps(d2)
|
|
||||||
obj_back = p1.loads(s2)
|
|
||||||
assert obj is obj_back
|
|
||||||
|
|
||||||
def test_pickling_twice_before_unpickling():
|
|
||||||
p1 = ImmutablePickler(uneven=False)
|
|
||||||
p2 = ImmutablePickler(uneven=True)
|
|
||||||
|
|
||||||
a1 = A()
|
|
||||||
a2 = A()
|
|
||||||
a3 = A()
|
|
||||||
a3.a1 = a1
|
|
||||||
a2.a1 = a1
|
|
||||||
s1 = p1.dumps(a1)
|
|
||||||
a1.a3 = a3
|
|
||||||
s2 = p1.dumps(a2)
|
|
||||||
other_a1 = p2.loads(s1)
|
|
||||||
other_a2 = p2.loads(s2)
|
|
||||||
back_a1 = p1.loads(p2.dumps(other_a1))
|
|
||||||
other_a3 = p2.loads(p1.dumps(a3))
|
|
||||||
back_a3 = p1.loads(p2.dumps(other_a3))
|
|
||||||
back_a2 = p1.loads(p2.dumps(other_a2))
|
|
||||||
back_a1 = p1.loads(p2.dumps(other_a1))
|
|
||||||
assert back_a1 is a1
|
|
||||||
assert back_a2 is a2
|
|
||||||
|
|
||||||
def test_pickling_concurrently():
|
|
||||||
p1 = ImmutablePickler(uneven=False)
|
|
||||||
p2 = ImmutablePickler(uneven=True)
|
|
||||||
|
|
||||||
a1 = A()
|
|
||||||
a1.hasattr = 42
|
|
||||||
a2 = A()
|
|
||||||
|
|
||||||
s1 = p1.dumps(a1)
|
|
||||||
s2 = p2.dumps(a2)
|
|
||||||
other_a1 = p2.loads(s1)
|
|
||||||
other_a2 = p1.loads(s2)
|
|
||||||
a1_back = p1.loads(p2.dumps(other_a1))
|
|
||||||
|
|
||||||
def test_self_memoize():
|
|
||||||
p1 = ImmutablePickler(uneven=False)
|
|
||||||
a1 = A()
|
|
||||||
p1.selfmemoize(a1)
|
|
||||||
x = p1.loads(p1.dumps(a1))
|
|
||||||
assert x is a1
|
|
||||||
|
|
||||||
TESTTIMEOUT = 2.0
|
|
||||||
class TestPickleChannelFunctional:
|
|
||||||
def setup_class(cls):
|
|
||||||
cls.gw = execnet.PopenGateway()
|
|
||||||
cls.gw.remote_exec(
|
|
||||||
"import py ; py.path.local(%r).pyimport()" %(__file__)
|
|
||||||
)
|
|
||||||
cls.gw.remote_init_threads(5)
|
|
||||||
# we need the remote test code to import
|
|
||||||
# the same test module here
|
|
||||||
|
|
||||||
def test_popen_send_instance(self):
|
|
||||||
channel = self.gw.remote_exec("""
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
from testing.test_mypickle import A
|
|
||||||
a1 = A()
|
|
||||||
a1.hello = 10
|
|
||||||
channel.send(a1)
|
|
||||||
a2 = channel.receive()
|
|
||||||
channel.send(a2 is a1)
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
a_received = channel.receive()
|
|
||||||
assert isinstance(a_received, A)
|
|
||||||
assert a_received.hello == 10
|
|
||||||
channel.send(a_received)
|
|
||||||
remote_a2_is_a1 = channel.receive()
|
|
||||||
assert remote_a2_is_a1
|
|
||||||
|
|
||||||
def test_send_concurrent(self):
|
|
||||||
channel = self.gw.remote_exec("""
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
from testing.test_mypickle import A
|
|
||||||
l = [A() for i in range(10)]
|
|
||||||
channel.send(l)
|
|
||||||
other_l = channel.receive()
|
|
||||||
channel.send((l, other_l))
|
|
||||||
channel.send(channel.receive())
|
|
||||||
channel.receive()
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
l = [A() for i in range(10)]
|
|
||||||
channel.send(l)
|
|
||||||
other_l = channel.receive()
|
|
||||||
channel.send(other_l)
|
|
||||||
ret = channel.receive()
|
|
||||||
assert ret[0] is other_l
|
|
||||||
assert ret[1] is l
|
|
||||||
back = channel.receive()
|
|
||||||
assert other_l is other_l
|
|
||||||
channel.send(None)
|
|
||||||
|
|
||||||
#s1 = p1.dumps(a1)
|
|
||||||
#s2 = p2.dumps(a2)
|
|
||||||
#other_a1 = p2.loads(s1)
|
|
||||||
#other_a2 = p1.loads(s2)
|
|
||||||
#a1_back = p1.loads(p2.dumps(other_a1))
|
|
||||||
|
|
||||||
def test_popen_with_callback(self):
|
|
||||||
channel = self.gw.remote_exec("""
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
from testing.test_mypickle import A
|
|
||||||
a1 = A()
|
|
||||||
a1.hello = 10
|
|
||||||
channel.send(a1)
|
|
||||||
a2 = channel.receive()
|
|
||||||
channel.send(a2 is a1)
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
queue = Queue()
|
|
||||||
channel.setcallback(queue.put)
|
|
||||||
a_received = queue.get(timeout=TESTTIMEOUT)
|
|
||||||
assert isinstance(a_received, A)
|
|
||||||
assert a_received.hello == 10
|
|
||||||
channel.send(a_received)
|
|
||||||
#remote_a2_is_a1 = queue.get(timeout=TESTTIMEOUT)
|
|
||||||
#assert remote_a2_is_a1
|
|
||||||
|
|
||||||
def test_popen_with_callback_with_endmarker(self):
|
|
||||||
channel = self.gw.remote_exec("""
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
from testing.test_mypickle import A
|
|
||||||
a1 = A()
|
|
||||||
a1.hello = 10
|
|
||||||
channel.send(a1)
|
|
||||||
a2 = channel.receive()
|
|
||||||
channel.send(a2 is a1)
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
queue = Queue()
|
|
||||||
channel.setcallback(queue.put, endmarker=-1)
|
|
||||||
|
|
||||||
a_received = queue.get(timeout=TESTTIMEOUT)
|
|
||||||
assert isinstance(a_received, A)
|
|
||||||
assert a_received.hello == 10
|
|
||||||
channel.send(a_received)
|
|
||||||
remote_a2_is_a1 = queue.get(timeout=TESTTIMEOUT)
|
|
||||||
assert remote_a2_is_a1
|
|
||||||
endmarker = queue.get(timeout=TESTTIMEOUT)
|
|
||||||
assert endmarker == -1
|
|
||||||
|
|
||||||
def test_popen_with_callback_with_endmarker_and_unpickling_error(self):
|
|
||||||
channel = self.gw.remote_exec("""
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
from testing.test_mypickle import A
|
|
||||||
a1 = A()
|
|
||||||
channel.send(a1)
|
|
||||||
channel.send(a1)
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
queue = Queue()
|
|
||||||
a = channel.receive()
|
|
||||||
channel._ipickle._unpicklememo.clear()
|
|
||||||
channel.setcallback(queue.put, endmarker=-1)
|
|
||||||
next = queue.get(timeout=TESTTIMEOUT)
|
|
||||||
assert next == -1
|
|
||||||
error = channel._getremoteerror()
|
|
||||||
assert isinstance(error, UnpickleError)
|
|
||||||
|
|
||||||
def test_popen_with_various_methods(self):
|
|
||||||
channel = self.gw.remote_exec("""
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
channel.receive()
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
assert not channel.isclosed()
|
|
||||||
assert not channel._getremoteerror()
|
|
||||||
channel.send(2)
|
|
||||||
channel.waitclose(timeout=2)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import py
|
|
||||||
from xdist.nodemanage import NodeManager
|
|
||||||
|
|
||||||
class pytest_funcarg__mysetup:
|
|
||||||
def __init__(self, request):
|
|
||||||
basetemp = request.config.mktemp(
|
|
||||||
"mysetup-%s" % request.function.__name__,
|
|
||||||
numbered=True)
|
|
||||||
self.source = basetemp.mkdir("source")
|
|
||||||
self.dest = basetemp.mkdir("dest")
|
|
||||||
request.getfuncargvalue("_pytest")
|
|
||||||
|
|
||||||
class TestNodeManager:
|
|
||||||
@py.test.mark.xfail
|
|
||||||
def test_rsync_roots_no_roots(self, testdir, mysetup):
|
|
||||||
mysetup.source.ensure("dir1", "file1").write("hello")
|
|
||||||
config = testdir.reparseconfig([source])
|
|
||||||
nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest])
|
|
||||||
assert nodemanager.config.topdir == source == config.topdir
|
|
||||||
nodemanager.rsync_roots()
|
|
||||||
p, = nodemanager.gwmanager.multi_exec("import os ; channel.send(os.getcwd())").receive_each()
|
|
||||||
p = py.path.local(p)
|
|
||||||
py.builtin.print_("remote curdir", p)
|
|
||||||
assert p == mysetup.dest.join(config.topdir.basename)
|
|
||||||
assert p.join("dir1").check()
|
|
||||||
assert p.join("dir1", "file1").check()
|
|
||||||
|
|
||||||
def test_popen_rsync_subdir(self, testdir, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
dir1 = mysetup.source.mkdir("dir1")
|
|
||||||
dir2 = dir1.mkdir("dir2")
|
|
||||||
dir2.ensure("hello")
|
|
||||||
for rsyncroot in (dir1, source):
|
|
||||||
dest.remove()
|
|
||||||
nodemanager = NodeManager(testdir.parseconfig(
|
|
||||||
"--tx", "popen//chdir=%s" % dest,
|
|
||||||
"--rsyncdir", rsyncroot,
|
|
||||||
source,
|
|
||||||
))
|
|
||||||
assert nodemanager.config.topdir == source
|
|
||||||
nodemanager.rsync_roots()
|
|
||||||
if rsyncroot == source:
|
|
||||||
dest = dest.join("source")
|
|
||||||
assert dest.join("dir1").check()
|
|
||||||
assert dest.join("dir1", "dir2").check()
|
|
||||||
assert dest.join("dir1", "dir2", 'hello').check()
|
|
||||||
nodemanager.gwmanager.exit()
|
|
||||||
|
|
||||||
def test_init_rsync_roots(self, testdir, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
dir2 = source.ensure("dir1", "dir2", dir=1)
|
|
||||||
source.ensure("dir1", "somefile", dir=1)
|
|
||||||
dir2.ensure("hello")
|
|
||||||
source.ensure("bogusdir", "file")
|
|
||||||
source.join("conftest.py").write(py.code.Source("""
|
|
||||||
rsyncdirs = ['dir1/dir2']
|
|
||||||
"""))
|
|
||||||
session = testdir.reparseconfig([source]).initsession()
|
|
||||||
nodemanager = NodeManager(session.config, ["popen//chdir=%s" % dest])
|
|
||||||
nodemanager.rsync_roots()
|
|
||||||
assert dest.join("dir2").check()
|
|
||||||
assert not dest.join("dir1").check()
|
|
||||||
assert not dest.join("bogus").check()
|
|
||||||
|
|
||||||
def test_rsyncignore(self, testdir, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
dir2 = source.ensure("dir1", "dir2", dir=1)
|
|
||||||
dir5 = source.ensure("dir5", "dir6", "bogus")
|
|
||||||
dirf = source.ensure("dir5", "file")
|
|
||||||
dir2.ensure("hello")
|
|
||||||
source.join("conftest.py").write(py.code.Source("""
|
|
||||||
rsyncdirs = ['dir1', 'dir5']
|
|
||||||
rsyncignore = ['dir1/dir2', 'dir5/dir6']
|
|
||||||
"""))
|
|
||||||
session = testdir.reparseconfig([source]).initsession()
|
|
||||||
nodemanager = NodeManager(session.config,
|
|
||||||
["popen//chdir=%s" % dest])
|
|
||||||
nodemanager.rsync_roots()
|
|
||||||
assert dest.join("dir1").check()
|
|
||||||
assert not dest.join("dir1", "dir2").check()
|
|
||||||
assert dest.join("dir5","file").check()
|
|
||||||
assert not dest.join("dir6").check()
|
|
||||||
|
|
||||||
def test_optimise_popen(self, testdir, mysetup):
|
|
||||||
source, dest = mysetup.source, mysetup.dest
|
|
||||||
specs = ["popen"] * 3
|
|
||||||
source.join("conftest.py").write("rsyncdirs = ['a']")
|
|
||||||
source.ensure('a', dir=1)
|
|
||||||
config = testdir.reparseconfig([source])
|
|
||||||
nodemanager = NodeManager(config, specs)
|
|
||||||
nodemanager.rsync_roots()
|
|
||||||
for gwspec in nodemanager.gwmanager.specs:
|
|
||||||
assert gwspec._samefilesystem()
|
|
||||||
assert not gwspec.chdir
|
|
||||||
|
|
||||||
def test_setup_DEBUG(self, mysetup, testdir):
|
|
||||||
source = mysetup.source
|
|
||||||
specs = ["popen"] * 2
|
|
||||||
source.join("conftest.py").write("rsyncdirs = ['a']")
|
|
||||||
source.ensure('a', dir=1)
|
|
||||||
config = testdir.reparseconfig([source, '--debug'])
|
|
||||||
assert config.option.debug
|
|
||||||
nodemanager = NodeManager(config, specs)
|
|
||||||
reprec = testdir.getreportrecorder(config).hookrecorder
|
|
||||||
nodemanager.setup_nodes(putevent=[].append)
|
|
||||||
for spec in nodemanager.gwmanager.specs:
|
|
||||||
l = reprec.getcalls("pytest_trace")
|
|
||||||
assert l
|
|
||||||
nodemanager.teardown_nodes()
|
|
||||||
|
|
||||||
def test_ssh_setup_nodes(self, specssh, testdir):
|
|
||||||
testdir.makepyfile(__init__="", test_x="""
|
|
||||||
def test_one():
|
|
||||||
pass
|
|
||||||
""")
|
|
||||||
reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir,
|
|
||||||
"--tx", specssh, testdir.tmpdir)
|
|
||||||
rep, = reprec.getreports("pytest_runtest_logreport")
|
|
||||||
assert rep.passed
|
|
||||||
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import py
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
def setglobals(request):
|
|
||||||
oldconfig = py.test.config
|
|
||||||
print("setting py.test.config to None")
|
|
||||||
py.test.config = None
|
|
||||||
def resetglobals():
|
|
||||||
py.builtin.print_("setting py.test.config to", oldconfig)
|
|
||||||
py.test.config = oldconfig
|
|
||||||
request.addfinalizer(resetglobals)
|
|
||||||
|
|
||||||
def pytest_funcarg__testdir(request):
|
|
||||||
setglobals(request)
|
|
||||||
return request.getfuncargvalue("testdir")
|
|
||||||
|
|
||||||
class ImmutablePickleTransport:
|
|
||||||
def __init__(self, request):
|
|
||||||
from xdist.mypickle import ImmutablePickler
|
|
||||||
self.p1 = ImmutablePickler(uneven=0)
|
|
||||||
self.p2 = ImmutablePickler(uneven=1)
|
|
||||||
setglobals(request)
|
|
||||||
|
|
||||||
def p1_to_p2(self, obj):
|
|
||||||
return self.p2.loads(self.p1.dumps(obj))
|
|
||||||
|
|
||||||
def p2_to_p1(self, obj):
|
|
||||||
return self.p1.loads(self.p2.dumps(obj))
|
|
||||||
|
|
||||||
def unifyconfig(self, config):
|
|
||||||
p2config = self.p1_to_p2(config)
|
|
||||||
p2config._initafterpickle(config.topdir)
|
|
||||||
return p2config
|
|
||||||
|
|
||||||
pytest_funcarg__pickletransport = ImmutablePickleTransport
|
|
||||||
|
|
||||||
class TestImmutablePickling:
|
|
||||||
def test_pickle_config(self, testdir, pickletransport):
|
|
||||||
config1 = testdir.parseconfig()
|
|
||||||
assert config1.topdir == testdir.tmpdir
|
|
||||||
testdir.chdir()
|
|
||||||
p2config = pickletransport.p1_to_p2(config1)
|
|
||||||
assert p2config.topdir.realpath() == config1.topdir.realpath()
|
|
||||||
config_back = pickletransport.p2_to_p1(p2config)
|
|
||||||
assert config_back is config1
|
|
||||||
|
|
||||||
def test_pickle_modcol(self, testdir, pickletransport):
|
|
||||||
modcol1 = testdir.getmodulecol("def test_one(): pass")
|
|
||||||
modcol2a = pickletransport.p1_to_p2(modcol1)
|
|
||||||
modcol2b = pickletransport.p1_to_p2(modcol1)
|
|
||||||
assert modcol2a is modcol2b
|
|
||||||
|
|
||||||
modcol1_back = pickletransport.p2_to_p1(modcol2a)
|
|
||||||
assert modcol1_back
|
|
||||||
|
|
||||||
def test_pickle_func(self, testdir, pickletransport):
|
|
||||||
modcol1 = testdir.getmodulecol("def test_one(): pass")
|
|
||||||
item = modcol1.collect_by_name("test_one")
|
|
||||||
testdir.chdir()
|
|
||||||
item2a = pickletransport.p1_to_p2(item)
|
|
||||||
assert item is not item2a # of course
|
|
||||||
assert item2a.name == item.name
|
|
||||||
modback = pickletransport.p2_to_p1(item2a.parent)
|
|
||||||
assert modback is modcol1
|
|
||||||
|
|
||||||
|
|
||||||
def test_config__setstate__wired_correctly_in_childprocess(testdir):
|
|
||||||
execnet = py.test.importorskip("execnet")
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
gw = execnet.makegateway()
|
|
||||||
channel = gw.remote_exec("""
|
|
||||||
import py
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
config = channel.receive()
|
|
||||||
assert py.test.config == config
|
|
||||||
""")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
config = testdir.parseconfig()
|
|
||||||
channel.send(config)
|
|
||||||
channel.waitclose() # this will potentially raise
|
|
||||||
gw.exit()
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import py
|
import py
|
||||||
|
|
||||||
import execnet
|
import execnet
|
||||||
from xdist.nodemanage import NodeManager
|
from xdist.slavemanage import NodeManager
|
||||||
|
|
||||||
def test_dist_incompatibility_messages(testdir):
|
def test_dist_incompatibility_messages(testdir):
|
||||||
Error = py.test.config.Error
|
result = testdir.runpytest("--pdb", "--looponfail")
|
||||||
py.test.raises(Error, "testdir.parseconfigure('--pdb', '--looponfail')")
|
assert result.ret != 0
|
||||||
result = testdir.runpytest("--pdb", "-n", "3")
|
result = testdir.runpytest("--pdb", "-n", "3")
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
assert "incompatible" in result.stderr.str()
|
assert "incompatible" in result.stderr.str()
|
||||||
@@ -14,10 +13,13 @@ def test_dist_incompatibility_messages(testdir):
|
|||||||
assert "incompatible" in result.stderr.str()
|
assert "incompatible" in result.stderr.str()
|
||||||
|
|
||||||
def test_dist_options(testdir):
|
def test_dist_options(testdir):
|
||||||
|
from xdist.plugin import check_options
|
||||||
config = testdir.parseconfigure("-n 2")
|
config = testdir.parseconfigure("-n 2")
|
||||||
|
check_options(config)
|
||||||
assert config.option.dist == "load"
|
assert config.option.dist == "load"
|
||||||
assert config.option.tx == ['popen'] * 2
|
assert config.option.tx == ['popen'] * 2
|
||||||
config = testdir.parseconfigure("-d")
|
config = testdir.parseconfigure("-d")
|
||||||
|
check_options(config)
|
||||||
assert config.option.dist == "load"
|
assert config.option.dist == "load"
|
||||||
|
|
||||||
class TestDistOptions:
|
class TestDistOptions:
|
||||||
@@ -39,22 +41,24 @@ class TestDistOptions:
|
|||||||
def test_getrsyncdirs(self, testdir):
|
def test_getrsyncdirs(self, testdir):
|
||||||
config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir))
|
config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir))
|
||||||
nm = NodeManager(config, specs=[execnet.XSpec("popen")])
|
nm = NodeManager(config, specs=[execnet.XSpec("popen")])
|
||||||
roots = nm._getrsyncdirs()
|
assert not nm._getrsyncdirs()
|
||||||
assert len(roots) == 1 + 2 # pylib + xdist
|
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")])
|
||||||
assert testdir.tmpdir in roots
|
assert nm.roots
|
||||||
|
assert testdir.tmpdir in nm.roots
|
||||||
|
|
||||||
def test_getrsyncdirs_with_conftest(self, testdir):
|
def test_getrsyncdirs_with_conftest(self, testdir):
|
||||||
p = py.path.local()
|
p = py.path.local()
|
||||||
for bn in 'x y z'.split():
|
for bn in 'x y z'.split():
|
||||||
p.mkdir(bn)
|
p.mkdir(bn)
|
||||||
testdir.makeconftest("""
|
testdir.makeini("""
|
||||||
rsyncdirs= 'x',
|
[pytest]
|
||||||
|
rsyncdirs= x
|
||||||
""")
|
""")
|
||||||
config = testdir.parseconfigure(
|
config = testdir.parseconfigure(
|
||||||
testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z')
|
testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z')
|
||||||
nm = NodeManager(config, specs=[execnet.XSpec("popen")])
|
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")])
|
||||||
roots = nm._getrsyncdirs()
|
roots = nm._getrsyncdirs()
|
||||||
assert len(roots) == 3 + 2 # pylib + xdist
|
#assert len(roots) == 3 + 1 # pylib
|
||||||
assert py.path.local('y') in roots
|
assert py.path.local('y') in roots
|
||||||
assert py.path.local('z') in roots
|
assert py.path.local('z') in roots
|
||||||
assert testdir.tmpdir.join('x') in roots
|
assert testdir.tmpdir.join('x') in roots
|
||||||
|
|||||||
@@ -1,151 +1,229 @@
|
|||||||
import py
|
import py
|
||||||
py.test.importorskip("execnet")
|
from xdist.slavemanage import SlaveController, unserialize_report
|
||||||
from xdist.remote import LooponfailingSession, LoopState, RemoteControl
|
from xdist.remote import serialize_report
|
||||||
|
import execnet
|
||||||
|
queue = py.builtin._tryimport("queue", "Queue")
|
||||||
|
from py.builtin import print_
|
||||||
|
import marshal
|
||||||
|
|
||||||
class TestRemoteControl:
|
WAIT_TIMEOUT = 10.0
|
||||||
def test_nofailures(self, testdir):
|
|
||||||
item = testdir.getitem("def test_func(): pass\n")
|
|
||||||
control = RemoteControl(item.config)
|
|
||||||
control.setup()
|
|
||||||
failures = control.runsession()
|
|
||||||
assert not failures
|
|
||||||
|
|
||||||
def test_failures_somewhere(self, testdir):
|
def check_marshallable(d):
|
||||||
item = testdir.getitem("def test_func(): assert 0\n")
|
try:
|
||||||
control = RemoteControl(item.config)
|
marshal.dumps(d)
|
||||||
control.setup()
|
except ValueError:
|
||||||
failures = control.runsession()
|
py.std.pprint.pprint(d)
|
||||||
assert failures
|
raise ValueError("not marshallable")
|
||||||
control.setup()
|
|
||||||
item.fspath.write("def test_func(): assert 1\n")
|
|
||||||
pyc = item.fspath.new(ext=".pyc")
|
|
||||||
if pyc.check():
|
|
||||||
pyc.remove()
|
|
||||||
failures = control.runsession(failures)
|
|
||||||
assert not failures
|
|
||||||
|
|
||||||
def test_failure_change(self, testdir):
|
class EventCall:
|
||||||
modcol = testdir.getitem("""
|
def __init__(self, eventcall):
|
||||||
|
self.name, self.kwargs = eventcall
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "<EventCall %s(**%s)>" %(self.name, self.kwargs)
|
||||||
|
|
||||||
|
class SlaveSetup:
|
||||||
|
use_callback = False
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
self.testdir = testdir = request.getfuncargvalue("testdir")
|
||||||
|
self.request = request
|
||||||
|
self.events = queue.Queue()
|
||||||
|
|
||||||
|
def setup(self, ):
|
||||||
|
self.testdir.chdir()
|
||||||
|
#import os ; os.environ['EXECNET_DEBUG'] = "2"
|
||||||
|
self.gateway = execnet.makegateway()
|
||||||
|
self.config = config = self.testdir.parseconfigure()
|
||||||
|
putevent = self.use_callback and self.events.put or None
|
||||||
|
self.slp = SlaveController(None, self.gateway, config, putevent)
|
||||||
|
self.request.addfinalizer(self.slp.ensure_teardown)
|
||||||
|
self.slp.setup()
|
||||||
|
|
||||||
|
def popevent(self, name=None):
|
||||||
|
while 1:
|
||||||
|
if self.use_callback:
|
||||||
|
data = self.events.get(timeout=WAIT_TIMEOUT)
|
||||||
|
else:
|
||||||
|
data = self.slp.channel.receive(timeout=WAIT_TIMEOUT)
|
||||||
|
ev = EventCall(data)
|
||||||
|
if name is None or ev.name == name:
|
||||||
|
return ev
|
||||||
|
print("skipping %s" % (ev,))
|
||||||
|
|
||||||
|
def sendcommand(self, name, **kwargs):
|
||||||
|
self.slp.sendcommand(name, **kwargs)
|
||||||
|
|
||||||
|
def pytest_funcarg__slave(request):
|
||||||
|
return SlaveSetup(request)
|
||||||
|
|
||||||
|
def test_remoteinitconfig(testdir):
|
||||||
|
from xdist.remote import remote_initconfig
|
||||||
|
config1 = testdir.parseconfig()
|
||||||
|
config2 = remote_initconfig(config1.option.__dict__, config1.args)
|
||||||
|
assert config2.option.__dict__ == config1.option.__dict__
|
||||||
|
assert config2.pluginmanager.getplugin("terminal") in (-1, None)
|
||||||
|
|
||||||
|
class TestReportSerialization:
|
||||||
|
def test_itemreport_outcomes(self, testdir):
|
||||||
|
reprec = testdir.inline_runsource("""
|
||||||
|
import py
|
||||||
|
def test_pass(): pass
|
||||||
|
def test_fail(): 0/0
|
||||||
|
@py.test.mark.skipif("True")
|
||||||
|
def test_skip(): pass
|
||||||
|
def test_skip_imperative():
|
||||||
|
py.test.skip("hello")
|
||||||
|
@py.test.mark.xfail("True")
|
||||||
|
def test_xfail(): 0/0
|
||||||
|
def test_xfail_imperative():
|
||||||
|
py.test.xfail("hello")
|
||||||
|
""")
|
||||||
|
reports = reprec.getreports("pytest_runtest_logreport")
|
||||||
|
assert len(reports) == 17 # with setup/teardown "passed" reports
|
||||||
|
for rep in reports:
|
||||||
|
d = serialize_report(rep)
|
||||||
|
check_marshallable(d)
|
||||||
|
newrep = unserialize_report("testreport", d)
|
||||||
|
assert newrep.passed == rep.passed
|
||||||
|
assert newrep.failed == rep.failed
|
||||||
|
assert newrep.skipped == rep.skipped
|
||||||
|
if newrep.skipped and 'xfail' not in newrep.keywords:
|
||||||
|
assert len(newrep.longrepr) == 3
|
||||||
|
assert newrep.outcome == rep.outcome
|
||||||
|
assert newrep.when == rep.when
|
||||||
|
assert newrep.keywords == rep.keywords
|
||||||
|
if rep.failed:
|
||||||
|
assert newrep.longrepr == str(rep.longrepr)
|
||||||
|
|
||||||
|
def test_collectreport_passed(self, testdir):
|
||||||
|
reprec = testdir.inline_runsource("def test_func(): pass")
|
||||||
|
reports = reprec.getreports("pytest_collectreport")
|
||||||
|
for rep in reports:
|
||||||
|
d = serialize_report(rep)
|
||||||
|
check_marshallable(d)
|
||||||
|
newrep = unserialize_report("collectreport", d)
|
||||||
|
assert newrep.passed == rep.passed
|
||||||
|
assert newrep.failed == rep.failed
|
||||||
|
assert newrep.skipped == rep.skipped
|
||||||
|
|
||||||
|
def test_collectreport_fail(self, testdir):
|
||||||
|
reprec = testdir.inline_runsource("qwe abc")
|
||||||
|
reports = reprec.getreports("pytest_collectreport")
|
||||||
|
assert reports
|
||||||
|
for rep in reports:
|
||||||
|
d = serialize_report(rep)
|
||||||
|
check_marshallable(d)
|
||||||
|
newrep = unserialize_report("collectreport", d)
|
||||||
|
assert newrep.passed == rep.passed
|
||||||
|
assert newrep.failed == rep.failed
|
||||||
|
assert newrep.skipped == rep.skipped
|
||||||
|
if rep.failed:
|
||||||
|
assert newrep.longrepr == str(rep.longrepr)
|
||||||
|
|
||||||
|
class TestSlaveInteractor:
|
||||||
|
def test_basic_collect_and_runtests(self, slave):
|
||||||
|
p = slave.testdir.makepyfile("""
|
||||||
def test_func():
|
def test_func():
|
||||||
assert 0
|
pass
|
||||||
""")
|
""")
|
||||||
control = RemoteControl(modcol.config)
|
slave.setup()
|
||||||
control.setup()
|
ev = slave.popevent()
|
||||||
failures = control.runsession()
|
assert ev.name == "slaveready"
|
||||||
assert failures
|
ev = slave.popevent()
|
||||||
control.setup()
|
assert ev.name == "collectionstart"
|
||||||
modcol.fspath.write(py.code.Source("""
|
assert not ev.kwargs
|
||||||
|
ev = slave.popevent("collectionfinish")
|
||||||
|
assert ev.kwargs['topdir'] == slave.testdir.tmpdir
|
||||||
|
ids = ev.kwargs['ids']
|
||||||
|
assert len(ids) == 1
|
||||||
|
slave.sendcommand("runtests", ids=ids)
|
||||||
|
ev = slave.popevent("testreport") # setup
|
||||||
|
ev = slave.popevent("testreport")
|
||||||
|
assert ev.name == "testreport"
|
||||||
|
rep = unserialize_report(ev.name, ev.kwargs['data'])
|
||||||
|
assert rep.nodeid.endswith("::test_func")
|
||||||
|
assert rep.passed
|
||||||
|
assert rep.when == "call"
|
||||||
|
slave.sendcommand("shutdown")
|
||||||
|
ev = slave.popevent("slavefinished")
|
||||||
|
assert 'slaveoutput' in ev.kwargs
|
||||||
|
|
||||||
|
def test_remote_collect_skip(self, slave):
|
||||||
|
p = slave.testdir.makepyfile("""
|
||||||
|
import py
|
||||||
|
py.test.skip("hello")
|
||||||
|
""")
|
||||||
|
slave.setup()
|
||||||
|
ev = slave.popevent("collectionstart")
|
||||||
|
assert not ev.kwargs
|
||||||
|
ev = slave.popevent()
|
||||||
|
assert ev.name == "collectreport"
|
||||||
|
ev = slave.popevent()
|
||||||
|
assert ev.name == "collectreport"
|
||||||
|
rep = unserialize_report(ev.name, ev.kwargs['data'])
|
||||||
|
assert rep.skipped
|
||||||
|
ev = slave.popevent("collectionfinish")
|
||||||
|
assert not ev.kwargs['ids']
|
||||||
|
|
||||||
|
def test_remote_collect_fail(self, slave):
|
||||||
|
p = slave.testdir.makepyfile("""aasd qwe""")
|
||||||
|
slave.setup()
|
||||||
|
ev = slave.popevent("collectionstart")
|
||||||
|
assert not ev.kwargs
|
||||||
|
ev = slave.popevent()
|
||||||
|
assert ev.name == "collectreport"
|
||||||
|
ev = slave.popevent()
|
||||||
|
assert ev.name == "collectreport"
|
||||||
|
rep = unserialize_report(ev.name, ev.kwargs['data'])
|
||||||
|
assert rep.failed
|
||||||
|
ev = slave.popevent("collectionfinish")
|
||||||
|
assert not ev.kwargs['ids']
|
||||||
|
|
||||||
|
def test_runtests_all(self, slave):
|
||||||
|
p = slave.testdir.makepyfile("""
|
||||||
|
def test_func(): pass
|
||||||
|
def test_func2(): pass
|
||||||
|
""")
|
||||||
|
slave.setup()
|
||||||
|
ev = slave.popevent()
|
||||||
|
assert ev.name == "slaveready"
|
||||||
|
ev = slave.popevent()
|
||||||
|
assert ev.name == "collectionstart"
|
||||||
|
assert not ev.kwargs
|
||||||
|
ev = slave.popevent("collectionfinish")
|
||||||
|
ids = ev.kwargs['ids']
|
||||||
|
assert len(ids) == 2
|
||||||
|
slave.sendcommand("runtests_all", )
|
||||||
|
slave.sendcommand("shutdown", )
|
||||||
|
for func in "::test_func", "::test_func2":
|
||||||
|
for i in range(3): # setup/call/teardown
|
||||||
|
ev = slave.popevent("testreport")
|
||||||
|
assert ev.name == "testreport"
|
||||||
|
rep = unserialize_report(ev.name, ev.kwargs['data'])
|
||||||
|
assert rep.nodeid.endswith(func)
|
||||||
|
ev = slave.popevent("slavefinished")
|
||||||
|
assert 'slaveoutput' in ev.kwargs
|
||||||
|
|
||||||
|
def test_happy_run_events_converted(self, testdir, slave):
|
||||||
|
py.test.xfail("implement a simple test for event production")
|
||||||
|
assert not slave.use_callback
|
||||||
|
p = slave.testdir.makepyfile("""
|
||||||
def test_func():
|
def test_func():
|
||||||
assert 1
|
pass
|
||||||
def test_new():
|
|
||||||
assert 0
|
|
||||||
"""))
|
|
||||||
pyc = modcol.fspath.new(ext=".pyc")
|
|
||||||
if pyc.check():
|
|
||||||
pyc.remove()
|
|
||||||
failures = control.runsession(failures)
|
|
||||||
assert not failures
|
|
||||||
control.setup()
|
|
||||||
failures = control.runsession()
|
|
||||||
assert failures
|
|
||||||
assert str(failures).find("test_new") != -1
|
|
||||||
|
|
||||||
class TestLooponFailing:
|
|
||||||
def test_looponfail_from_fail_to_ok(self, testdir):
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_one():
|
|
||||||
x = 0
|
|
||||||
assert x == 1
|
|
||||||
def test_two():
|
|
||||||
assert 1
|
|
||||||
""")
|
""")
|
||||||
session = LooponfailingSession(modcol.config)
|
slave.setup()
|
||||||
loopstate = LoopState()
|
hookrec = testdir.getreportrecorder(slave.config)
|
||||||
session.remotecontrol.setup()
|
for data in slave.slp.channel:
|
||||||
session.loop_once(loopstate)
|
slave.slp.process_from_remote(data)
|
||||||
assert len(loopstate.colitems) == 1
|
slave.slp.process_from_remote(slave.slp.ENDMARK)
|
||||||
|
py.std.pprint.pprint(hookrec.hookrecorder.calls)
|
||||||
modcol.fspath.write(py.code.Source("""
|
hookrec.hookrecorder.contains([
|
||||||
def test_one():
|
("pytest_collectstart", "collector.fspath == aaa"),
|
||||||
x = 15
|
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
||||||
assert x == 15
|
("pytest_collectreport", "report.collector.fspath == aaa"),
|
||||||
def test_two():
|
("pytest_collectstart", "collector.fspath == bbb"),
|
||||||
assert 1
|
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
||||||
"""))
|
("pytest_collectreport", "report.collector.fspath == bbb"),
|
||||||
assert session.statrecorder.check()
|
])
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert not loopstate.colitems
|
|
||||||
|
|
||||||
def test_looponfail_from_one_to_two_tests(self, testdir):
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_one():
|
|
||||||
assert 0
|
|
||||||
""")
|
|
||||||
session = LooponfailingSession(modcol.config)
|
|
||||||
loopstate = LoopState()
|
|
||||||
session.remotecontrol.setup()
|
|
||||||
loopstate.colitems = []
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(loopstate.colitems) == 1
|
|
||||||
|
|
||||||
modcol.fspath.write(py.code.Source("""
|
|
||||||
def test_one():
|
|
||||||
assert 1 # passes now
|
|
||||||
def test_two():
|
|
||||||
assert 0 # new and fails
|
|
||||||
"""))
|
|
||||||
assert session.statrecorder.check()
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(loopstate.colitems) == 0
|
|
||||||
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(loopstate.colitems) == 1
|
|
||||||
|
|
||||||
def test_looponfail_removed_test(self, testdir):
|
|
||||||
modcol = testdir.getmodulecol("""
|
|
||||||
def test_one():
|
|
||||||
assert 0
|
|
||||||
def test_two():
|
|
||||||
assert 0
|
|
||||||
""")
|
|
||||||
session = LooponfailingSession(modcol.config)
|
|
||||||
loopstate = LoopState()
|
|
||||||
session.remotecontrol.setup()
|
|
||||||
loopstate.colitems = []
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(loopstate.colitems) == 2
|
|
||||||
|
|
||||||
modcol.fspath.write(py.code.Source("""
|
|
||||||
def test_xxx(): # renamed test
|
|
||||||
assert 0
|
|
||||||
def test_two():
|
|
||||||
assert 1 # pass now
|
|
||||||
"""))
|
|
||||||
assert session.statrecorder.check()
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(loopstate.colitems) == 0
|
|
||||||
|
|
||||||
session.loop_once(loopstate)
|
|
||||||
assert len(loopstate.colitems) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_looponfail_functional_fail_to_ok(self, testdir):
|
|
||||||
p = testdir.makepyfile("""
|
|
||||||
def test_one():
|
|
||||||
x = 0
|
|
||||||
assert x == 1
|
|
||||||
""")
|
|
||||||
child = testdir.spawn_pytest("-f %s" % p)
|
|
||||||
child.expect("def test_one")
|
|
||||||
child.expect("x == 1")
|
|
||||||
child.expect("1 failed")
|
|
||||||
child.expect("### LOOPONFAILING ####")
|
|
||||||
child.expect("waiting for changes")
|
|
||||||
p.write(py.code.Source("""
|
|
||||||
def test_one():
|
|
||||||
x = 1
|
|
||||||
assert x == 1
|
|
||||||
"""))
|
|
||||||
child.expect(".*1 passed.*")
|
|
||||||
child.kill(15)
|
|
||||||
|
|
||||||
|
|||||||
233
testing/test_slavemanage.py
Normal file
233
testing/test_slavemanage.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import py
|
||||||
|
import os
|
||||||
|
import execnet
|
||||||
|
from xdist.slavemanage import HostRSync, NodeManager
|
||||||
|
|
||||||
|
pytest_plugins = "pytester",
|
||||||
|
|
||||||
|
def pytest_funcarg__hookrecorder(request):
|
||||||
|
_pytest = request.getfuncargvalue('_pytest')
|
||||||
|
config = request.getfuncargvalue('config')
|
||||||
|
return _pytest.gethookrecorder(config.hook)
|
||||||
|
|
||||||
|
def pytest_funcarg__config(request):
|
||||||
|
testdir = request.getfuncargvalue("testdir")
|
||||||
|
config = testdir.parseconfig()
|
||||||
|
return config
|
||||||
|
|
||||||
|
class pytest_funcarg__mysetup:
|
||||||
|
def __init__(self, request):
|
||||||
|
temp = request.getfuncargvalue("tmpdir")
|
||||||
|
self.source = temp.mkdir("source")
|
||||||
|
self.dest = temp.mkdir("dest")
|
||||||
|
request.getfuncargvalue("_pytest")
|
||||||
|
|
||||||
|
class TestNodeManagerPopen:
|
||||||
|
def test_popen_no_default_chdir(self, config):
|
||||||
|
gm = NodeManager(config, ["popen"])
|
||||||
|
assert gm.specs[0].chdir is None
|
||||||
|
|
||||||
|
def test_default_chdir(self, config):
|
||||||
|
l = ["ssh=noco", "socket=xyz"]
|
||||||
|
for spec in NodeManager(config, l).specs:
|
||||||
|
assert spec.chdir == "pyexecnetcache"
|
||||||
|
for spec in NodeManager(config, l, defaultchdir="abc").specs:
|
||||||
|
assert spec.chdir == "abc"
|
||||||
|
|
||||||
|
def test_popen_makegateway_events(self, config, hookrecorder, _pytest):
|
||||||
|
hm = NodeManager(config, ["popen"] * 2)
|
||||||
|
hm.makegateways()
|
||||||
|
call = hookrecorder.popcall("pytest_xdist_setupnodes")
|
||||||
|
assert len(call.specs) == 2
|
||||||
|
|
||||||
|
call = hookrecorder.popcall("pytest_xdist_newgateway")
|
||||||
|
assert call.gateway.spec == execnet.XSpec("popen")
|
||||||
|
assert call.gateway.id == "gw0"
|
||||||
|
call = hookrecorder.popcall("pytest_xdist_newgateway")
|
||||||
|
assert call.gateway.id == "gw1"
|
||||||
|
assert len(hm.group) == 2
|
||||||
|
hm.teardown_nodes()
|
||||||
|
assert not len(hm.group)
|
||||||
|
|
||||||
|
def test_popens_rsync(self, config, mysetup):
|
||||||
|
source = mysetup.source
|
||||||
|
hm = NodeManager(config, ["popen"] * 2)
|
||||||
|
hm.makegateways()
|
||||||
|
assert len(hm.group) == 2
|
||||||
|
for gw in hm.group:
|
||||||
|
class pseudoexec:
|
||||||
|
args = []
|
||||||
|
def __init__(self, *args):
|
||||||
|
self.args.extend(args)
|
||||||
|
def waitclose(self):
|
||||||
|
pass
|
||||||
|
gw.remote_exec = pseudoexec
|
||||||
|
l = []
|
||||||
|
hm.rsync(source, notify=lambda *args: l.append(args))
|
||||||
|
assert not l
|
||||||
|
hm.teardown_nodes()
|
||||||
|
assert not len(hm.group)
|
||||||
|
assert "sys.path.insert" in gw.remote_exec.args[0]
|
||||||
|
|
||||||
|
def test_rsync_popen_with_path(self, config, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
hm = NodeManager(config, ["popen//chdir=%s" %dest] * 1)
|
||||||
|
hm.makegateways()
|
||||||
|
source.ensure("dir1", "dir2", "hello")
|
||||||
|
l = []
|
||||||
|
hm.rsync(source, notify=lambda *args: l.append(args))
|
||||||
|
assert len(l) == 1
|
||||||
|
assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source)
|
||||||
|
hm.teardown_nodes()
|
||||||
|
dest = dest.join(source.basename)
|
||||||
|
assert dest.join("dir1").check()
|
||||||
|
assert dest.join("dir1", "dir2").check()
|
||||||
|
assert dest.join("dir1", "dir2", 'hello').check()
|
||||||
|
|
||||||
|
def test_rsync_same_popen_twice(self, config, mysetup, hookrecorder):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
hm = NodeManager(config, ["popen//chdir=%s" %dest] * 2)
|
||||||
|
hm.makegateways()
|
||||||
|
source.ensure("dir1", "dir2", "hello")
|
||||||
|
hm.rsync(source)
|
||||||
|
call = hookrecorder.popcall("pytest_xdist_rsyncstart")
|
||||||
|
assert call.source == source
|
||||||
|
assert len(call.gateways) == 1
|
||||||
|
assert call.gateways[0] in hm.group
|
||||||
|
call = hookrecorder.popcall("pytest_xdist_rsyncfinish")
|
||||||
|
|
||||||
|
class TestHRSync:
|
||||||
|
class pytest_funcarg__mysetup:
|
||||||
|
def __init__(self, request):
|
||||||
|
tmp = request.getfuncargvalue('tmpdir')
|
||||||
|
self.source = tmp.mkdir("source")
|
||||||
|
self.dest = tmp.mkdir("dest")
|
||||||
|
|
||||||
|
def test_hrsync_filter(self, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
source.ensure("dir", "file.txt")
|
||||||
|
source.ensure(".svn", "entries")
|
||||||
|
source.ensure(".somedotfile", "moreentries")
|
||||||
|
source.ensure("somedir", "editfile~")
|
||||||
|
syncer = HostRSync(source)
|
||||||
|
l = list(source.visit(rec=syncer.filter,
|
||||||
|
fil=syncer.filter))
|
||||||
|
assert len(l) == 3
|
||||||
|
basenames = [x.basename for x in l]
|
||||||
|
assert 'dir' in basenames
|
||||||
|
assert 'file.txt' in basenames
|
||||||
|
assert 'somedir' in basenames
|
||||||
|
|
||||||
|
def test_hrsync_one_host(self, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
gw = execnet.makegateway("popen//chdir=%s" % dest)
|
||||||
|
finished = []
|
||||||
|
rsync = HostRSync(source)
|
||||||
|
rsync.add_target_host(gw, finished=lambda: finished.append(1))
|
||||||
|
source.join("hello.py").write("world")
|
||||||
|
rsync.send()
|
||||||
|
gw.exit()
|
||||||
|
assert dest.join(source.basename, "hello.py").check()
|
||||||
|
assert len(finished) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestNodeManager:
|
||||||
|
@py.test.mark.xfail
|
||||||
|
def test_rsync_roots_no_roots(self, testdir, mysetup):
|
||||||
|
mysetup.source.ensure("dir1", "file1").write("hello")
|
||||||
|
config = testdir.parseconfig(source)
|
||||||
|
nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest])
|
||||||
|
#assert nodemanager.config.topdir == source == config.topdir
|
||||||
|
nodemanager.makegateways()
|
||||||
|
nodemanager.rsync_roots()
|
||||||
|
p, = nodemanager.gwmanager.multi_exec(
|
||||||
|
"import os ; channel.send(os.getcwd())").receive_each()
|
||||||
|
p = py.path.local(p)
|
||||||
|
py.builtin.print_("remote curdir", p)
|
||||||
|
assert p == mysetup.dest.join(config.topdir.basename)
|
||||||
|
assert p.join("dir1").check()
|
||||||
|
assert p.join("dir1", "file1").check()
|
||||||
|
|
||||||
|
def test_popen_rsync_subdir(self, testdir, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
dir1 = mysetup.source.mkdir("dir1")
|
||||||
|
dir2 = dir1.mkdir("dir2")
|
||||||
|
dir2.ensure("hello")
|
||||||
|
for rsyncroot in (dir1, source):
|
||||||
|
dest.remove()
|
||||||
|
nodemanager = NodeManager(testdir.parseconfig(
|
||||||
|
"--tx", "popen//chdir=%s" % dest,
|
||||||
|
"--rsyncdir", rsyncroot,
|
||||||
|
source,
|
||||||
|
))
|
||||||
|
nodemanager.makegateways()
|
||||||
|
nodemanager.rsync_roots()
|
||||||
|
if rsyncroot == source:
|
||||||
|
dest = dest.join("source")
|
||||||
|
assert dest.join("dir1").check()
|
||||||
|
assert dest.join("dir1", "dir2").check()
|
||||||
|
assert dest.join("dir1", "dir2", 'hello').check()
|
||||||
|
nodemanager.teardown_nodes()
|
||||||
|
|
||||||
|
def test_init_rsync_roots(self, testdir, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
dir2 = source.ensure("dir1", "dir2", dir=1)
|
||||||
|
source.ensure("dir1", "somefile", dir=1)
|
||||||
|
dir2.ensure("hello")
|
||||||
|
source.ensure("bogusdir", "file")
|
||||||
|
source.join("tox.ini").write(py.std.textwrap.dedent("""
|
||||||
|
[pytest]
|
||||||
|
rsyncdirs=dir1/dir2
|
||||||
|
"""))
|
||||||
|
config = testdir.parseconfig(source)
|
||||||
|
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
|
||||||
|
nodemanager.makegateways()
|
||||||
|
nodemanager.rsync_roots()
|
||||||
|
assert dest.join("dir2").check()
|
||||||
|
assert not dest.join("dir1").check()
|
||||||
|
assert not dest.join("bogus").check()
|
||||||
|
|
||||||
|
def test_rsyncignore(self, testdir, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
dir2 = source.ensure("dir1", "dir2", dir=1)
|
||||||
|
dir5 = source.ensure("dir5", "dir6", "bogus")
|
||||||
|
dirf = source.ensure("dir5", "file")
|
||||||
|
dir2.ensure("hello")
|
||||||
|
source.join("tox.ini").write(py.std.textwrap.dedent("""
|
||||||
|
[pytest]
|
||||||
|
rsyncdirs = dir1 dir5
|
||||||
|
rsyncignore = dir1/dir2 dir5/dir6
|
||||||
|
"""))
|
||||||
|
config = testdir.parseconfig(source)
|
||||||
|
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
|
||||||
|
nodemanager.makegateways()
|
||||||
|
nodemanager.rsync_roots()
|
||||||
|
assert dest.join("dir1").check()
|
||||||
|
assert not dest.join("dir1", "dir2").check()
|
||||||
|
assert dest.join("dir5","file").check()
|
||||||
|
assert not dest.join("dir6").check()
|
||||||
|
|
||||||
|
def test_optimise_popen(self, testdir, mysetup):
|
||||||
|
source, dest = mysetup.source, mysetup.dest
|
||||||
|
specs = ["popen"] * 3
|
||||||
|
source.join("conftest.py").write("rsyncdirs = ['a']")
|
||||||
|
source.ensure('a', dir=1)
|
||||||
|
config = testdir.parseconfig(source)
|
||||||
|
nodemanager = NodeManager(config, specs)
|
||||||
|
nodemanager.makegateways()
|
||||||
|
nodemanager.rsync_roots()
|
||||||
|
for gwspec in nodemanager.specs:
|
||||||
|
assert gwspec._samefilesystem()
|
||||||
|
assert not gwspec.chdir
|
||||||
|
|
||||||
|
def test_ssh_setup_nodes(self, specssh, testdir):
|
||||||
|
testdir.makepyfile(__init__="", test_x="""
|
||||||
|
def test_one():
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir,
|
||||||
|
"--tx", specssh, testdir.tmpdir)
|
||||||
|
rep, = reprec.getreports("pytest_runtest_logreport")
|
||||||
|
assert rep.passed
|
||||||
|
|
||||||
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
|
|
||||||
import py
|
|
||||||
import execnet
|
|
||||||
from xdist.txnode import TXNode
|
|
||||||
queue = py.builtin._tryimport("queue", "Queue")
|
|
||||||
Queue = queue.Queue
|
|
||||||
|
|
||||||
class EventQueue:
|
|
||||||
def __init__(self, registry, queue=None):
|
|
||||||
if queue is None:
|
|
||||||
queue = Queue()
|
|
||||||
self.queue = queue
|
|
||||||
registry.register(self)
|
|
||||||
|
|
||||||
def geteventargs(self, eventname, timeout=10.0):
|
|
||||||
events = []
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
eventcall = self.queue.get(timeout=timeout)
|
|
||||||
except queue.Empty:
|
|
||||||
#print "node channel", self.node.channel
|
|
||||||
#print "remoteerror", self.node.channel._getremoteerror()
|
|
||||||
py.builtin.print_("seen events", events)
|
|
||||||
raise IOError("did not see %r events" % (eventname))
|
|
||||||
else:
|
|
||||||
name, args, kwargs = eventcall
|
|
||||||
assert isinstance(name, str)
|
|
||||||
if name == eventname:
|
|
||||||
if args:
|
|
||||||
return args
|
|
||||||
return kwargs
|
|
||||||
events.append(name)
|
|
||||||
if name == "pytest_internalerror":
|
|
||||||
py.builtin.print_(str(kwargs["excrepr"]))
|
|
||||||
|
|
||||||
class MySetup:
|
|
||||||
def __init__(self, request):
|
|
||||||
self.id = 0
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
def geteventargs(self, eventname, timeout=10.0):
|
|
||||||
eq = EventQueue(self.config.pluginmanager, self.queue)
|
|
||||||
return eq.geteventargs(eventname, timeout=timeout)
|
|
||||||
|
|
||||||
def makenode(self, config=None):
|
|
||||||
if config is None:
|
|
||||||
testdir = self.request.getfuncargvalue("testdir")
|
|
||||||
config = testdir.reparseconfig([])
|
|
||||||
self.config = config
|
|
||||||
self.queue = Queue()
|
|
||||||
self.xspec = execnet.XSpec("popen")
|
|
||||||
self.gateway = execnet.makegateway(self.xspec)
|
|
||||||
self.id += 1
|
|
||||||
self.gateway.id = str(self.id)
|
|
||||||
self.nodemanager = None
|
|
||||||
self.node = TXNode(self.nodemanager, self.gateway, self.config, putevent=self.queue.put)
|
|
||||||
assert not self.node.channel.isclosed()
|
|
||||||
return self.node
|
|
||||||
|
|
||||||
def xfinalize(self):
|
|
||||||
if hasattr(self, 'node'):
|
|
||||||
gw = self.node.gateway
|
|
||||||
py.builtin.print_("exiting:", gw)
|
|
||||||
gw.exit()
|
|
||||||
|
|
||||||
def pytest_funcarg__mysetup(request):
|
|
||||||
mysetup = MySetup(request)
|
|
||||||
#pyfuncitem.addfinalizer(mysetup.finalize)
|
|
||||||
return mysetup
|
|
||||||
|
|
||||||
def test_node_hash_equality(mysetup):
|
|
||||||
node = mysetup.makenode()
|
|
||||||
node2 = mysetup.makenode()
|
|
||||||
assert node != node2
|
|
||||||
assert node == node
|
|
||||||
assert not (node != node)
|
|
||||||
|
|
||||||
class TestMasterSlaveConnection:
|
|
||||||
def test_crash_invalid_item(self, mysetup):
|
|
||||||
node = mysetup.makenode()
|
|
||||||
node.send(123) # invalid item
|
|
||||||
kwargs = mysetup.geteventargs("pytest_testnodedown")
|
|
||||||
assert kwargs['node'] is node
|
|
||||||
#assert isinstance(kwargs['error'], execnet.RemoteError)
|
|
||||||
|
|
||||||
def test_crash_killed(self, testdir, mysetup):
|
|
||||||
if not hasattr(py.std.os, 'kill'):
|
|
||||||
py.test.skip("no os.kill")
|
|
||||||
item = testdir.getitem("""
|
|
||||||
def test_func():
|
|
||||||
import os
|
|
||||||
os.kill(os.getpid(), 9)
|
|
||||||
""")
|
|
||||||
node = mysetup.makenode(item.config)
|
|
||||||
node.send(item)
|
|
||||||
kwargs = mysetup.geteventargs("pytest_testnodedown")
|
|
||||||
assert kwargs['node'] is node
|
|
||||||
assert "Not properly terminated" in str(kwargs['error'])
|
|
||||||
|
|
||||||
def test_node_down(self, mysetup):
|
|
||||||
node = mysetup.makenode()
|
|
||||||
node.shutdown()
|
|
||||||
kwargs = mysetup.geteventargs("pytest_testnodedown")
|
|
||||||
assert kwargs['node'] is node
|
|
||||||
assert not kwargs['error']
|
|
||||||
node.callback(node.ENDMARK)
|
|
||||||
excinfo = py.test.raises(IOError,
|
|
||||||
"mysetup.geteventargs('testnodedown', timeout=0.01)")
|
|
||||||
|
|
||||||
def test_send_on_closed_channel(self, testdir, mysetup):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
node = mysetup.makenode(item.config)
|
|
||||||
node.channel.close()
|
|
||||||
py.test.raises(IOError, "node.send(item)")
|
|
||||||
#ev = self.getcalls(pytest_internalerror)
|
|
||||||
#assert ev.excinfo.errisinstance(IOError)
|
|
||||||
|
|
||||||
def test_send_one(self, testdir, mysetup):
|
|
||||||
item = testdir.getitem("def test_func(): pass")
|
|
||||||
node = mysetup.makenode(item.config)
|
|
||||||
node.send(item)
|
|
||||||
kwargs = mysetup.geteventargs("pytest_runtest_logreport")
|
|
||||||
rep = kwargs['report']
|
|
||||||
assert rep.passed
|
|
||||||
py.builtin.print_(rep)
|
|
||||||
assert rep.item == item
|
|
||||||
|
|
||||||
def test_send_some(self, testdir, mysetup):
|
|
||||||
items = testdir.getitems("""
|
|
||||||
def test_pass():
|
|
||||||
pass
|
|
||||||
def test_fail():
|
|
||||||
assert 0
|
|
||||||
def test_skip():
|
|
||||||
import py
|
|
||||||
py.test.skip("x")
|
|
||||||
""")
|
|
||||||
node = mysetup.makenode(items[0].config)
|
|
||||||
for item in items:
|
|
||||||
node.send(item)
|
|
||||||
for outcome in "passed failed skipped".split():
|
|
||||||
kwargs = mysetup.geteventargs("pytest_runtest_logreport")
|
|
||||||
report = kwargs['report']
|
|
||||||
assert getattr(report, outcome)
|
|
||||||
|
|
||||||
node.sendlist(items)
|
|
||||||
for outcome in "passed failed skipped".split():
|
|
||||||
rep = mysetup.geteventargs("pytest_runtest_logreport")['report']
|
|
||||||
assert getattr(rep, outcome)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import py
|
|
||||||
from xdist.util import StatRecorder
|
|
||||||
|
|
||||||
def test_filechange(tmpdir):
|
|
||||||
tmp = tmpdir
|
|
||||||
hello = tmp.ensure("hello.py")
|
|
||||||
sd = StatRecorder([tmp])
|
|
||||||
changed = sd.check()
|
|
||||||
assert not changed
|
|
||||||
|
|
||||||
hello.write("world")
|
|
||||||
changed = sd.check()
|
|
||||||
assert changed
|
|
||||||
|
|
||||||
tmp.ensure("new.py")
|
|
||||||
changed = sd.check()
|
|
||||||
assert changed
|
|
||||||
|
|
||||||
tmp.join("new.py").remove()
|
|
||||||
changed = sd.check()
|
|
||||||
assert changed
|
|
||||||
|
|
||||||
tmp.join("a", "b", "c.py").ensure()
|
|
||||||
changed = sd.check()
|
|
||||||
assert changed
|
|
||||||
|
|
||||||
tmp.join("a", "c.txt").ensure()
|
|
||||||
changed = sd.check()
|
|
||||||
assert changed
|
|
||||||
changed = sd.check()
|
|
||||||
assert not changed
|
|
||||||
|
|
||||||
tmp.join("a").remove()
|
|
||||||
changed = sd.check()
|
|
||||||
assert changed
|
|
||||||
|
|
||||||
def test_pycremoval(tmpdir):
|
|
||||||
tmp = tmpdir
|
|
||||||
hello = tmp.ensure("hello.py")
|
|
||||||
sd = StatRecorder([tmp])
|
|
||||||
changed = sd.check()
|
|
||||||
assert not changed
|
|
||||||
|
|
||||||
pycfile = hello + "c"
|
|
||||||
pycfile.ensure()
|
|
||||||
changed = sd.check()
|
|
||||||
assert not changed
|
|
||||||
|
|
||||||
hello.write("world")
|
|
||||||
changed = sd.check()
|
|
||||||
assert not pycfile.check()
|
|
||||||
|
|
||||||
|
|
||||||
def test_waitonchange(tmpdir, monkeypatch):
|
|
||||||
tmp = tmpdir
|
|
||||||
sd = StatRecorder([tmp])
|
|
||||||
|
|
||||||
l = [True, False]
|
|
||||||
monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop())
|
|
||||||
sd.waitonchange(checkinterval=0.2)
|
|
||||||
assert not l
|
|
||||||
23
tox.ini
Normal file
23
tox.ini
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[tox]
|
||||||
|
envlist=py26,py31,py27,py25,py24
|
||||||
|
indexserver=
|
||||||
|
testrun = http://pypi.testrun.org
|
||||||
|
pypi = http://pypi.python.org/simple
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
changedir=testing
|
||||||
|
deps=:testrun:pytest>=2.2.0.dev2
|
||||||
|
commands= py.test --junitxml={envlogdir}/junit-{envname}.xml []
|
||||||
|
|
||||||
|
[testenv:py31]
|
||||||
|
deps=:pypi:pytest # XXX needed because ClueRelease/pip broken
|
||||||
|
[testenv:py32]
|
||||||
|
deps=:pypi:pytest # XXX needed because ClueRelease/pip broken
|
||||||
|
|
||||||
|
[testenv:py26]
|
||||||
|
deps=
|
||||||
|
:testrun:pytest
|
||||||
|
:pypi:pexpect
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
addopts = -rsfxX
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
#
|
#
|
||||||
__version__ = "1.2"
|
__version__ = '1.7.dev1'
|
||||||
|
|
||||||
|
|||||||
@@ -1,297 +1,392 @@
|
|||||||
import py
|
import pytest, py
|
||||||
from py._test import session
|
import sys
|
||||||
from xdist.nodemanage import NodeManager
|
from xdist.slavemanage import NodeManager
|
||||||
queue = py.builtin._tryimport('queue', 'Queue')
|
queue = py.builtin._tryimport('queue', 'Queue')
|
||||||
|
|
||||||
debug_file = None # open('/tmp/loop.log', 'w')
|
class EachScheduling:
|
||||||
def debug(*args):
|
|
||||||
if debug_file is not None:
|
|
||||||
s = " ".join(map(str, args))
|
|
||||||
debug_file.write(s+"\n")
|
|
||||||
debug_file.flush()
|
|
||||||
|
|
||||||
class LoopState(object):
|
def __init__(self, numnodes, log=None):
|
||||||
def __init__(self, dsession, colitems):
|
self.numnodes = numnodes
|
||||||
self.dsession = dsession
|
self.node2collection = {}
|
||||||
self.colitems = colitems
|
self.node2pending = {}
|
||||||
self.exitstatus = None
|
if log is None:
|
||||||
# loopstate.dowork is False after reschedule events
|
self.log = py.log.Producer("eachsched")
|
||||||
# because otherwise we might very busily loop
|
else:
|
||||||
# waiting for a host to become ready.
|
self.log = log.loadsched
|
||||||
self.dowork = True
|
self.collection_is_completed = False
|
||||||
self.shuttingdown = False
|
|
||||||
self.testsfailed = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
def hasnodes(self):
|
||||||
return "<LoopState exitstatus=%r shuttingdown=%r len(colitems)=%d>" % (
|
return bool(self.node2pending)
|
||||||
self.exitstatus, self.shuttingdown, len(self.colitems))
|
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report):
|
def addnode(self, node):
|
||||||
if report.item in self.dsession.item2nodes:
|
self.node2collection[node] = None
|
||||||
if report.when != "teardown": # otherwise we already managed it
|
|
||||||
self.dsession.removeitem(report.item, report.node)
|
|
||||||
if report.failed:
|
|
||||||
self.testsfailed = True
|
|
||||||
|
|
||||||
def pytest_collectreport(self, report):
|
def tests_finished(self):
|
||||||
if report.passed:
|
if not self.collection_is_completed:
|
||||||
self.colitems.extend(report.result)
|
return False
|
||||||
|
for items in self.node2pending.values():
|
||||||
|
if items:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def pytest_testnodeready(self, node):
|
def addnode_collection(self, node, collection):
|
||||||
self.dsession.addnode(node)
|
assert not self.collection_is_completed
|
||||||
|
assert self.node2collection[node] is None
|
||||||
|
self.node2collection[node] = list(collection)
|
||||||
|
self.node2pending[node] = []
|
||||||
|
if len(self.node2pending) >= self.numnodes:
|
||||||
|
self.collection_is_completed = True
|
||||||
|
|
||||||
def pytest_testnodedown(self, node, error=None):
|
def remove_item(self, node, item):
|
||||||
pending = self.dsession.removenode(node)
|
self.node2pending[node].remove(item)
|
||||||
if pending:
|
|
||||||
if error:
|
|
||||||
crashitem = pending[0]
|
|
||||||
debug("determined crashitem", crashitem)
|
|
||||||
self.dsession.handle_crashitem(crashitem, node)
|
|
||||||
# XXX recovery handling for "each"?
|
|
||||||
# currently pending items are not retried
|
|
||||||
if self.dsession.config.option.dist == "load":
|
|
||||||
self.colitems.extend(pending[1:])
|
|
||||||
|
|
||||||
def pytest_rescheduleitems(self, items):
|
def remove_node(self, node):
|
||||||
self.colitems[:] = items + self.colitems
|
# KeyError if we didn't get an addnode() yet
|
||||||
for pending in self.dsession.node2pending.values():
|
pending = self.node2pending.pop(node)
|
||||||
if pending:
|
if not pending:
|
||||||
self.dowork = False # avoid busywait, nodes still have work
|
return
|
||||||
|
crashitem = pending.pop(0)
|
||||||
|
# XXX what about the rest of pending?
|
||||||
|
return crashitem
|
||||||
|
|
||||||
class ExitFirstInterrupt(KeyboardInterrupt):
|
def init_distribute(self):
|
||||||
pass
|
assert self.collection_is_completed
|
||||||
|
for node, pending in self.node2pending.items():
|
||||||
|
node.send_runtest_all()
|
||||||
|
pending[:] = self.node2collection[node]
|
||||||
|
|
||||||
class DSession(session.Session):
|
class LoadScheduling:
|
||||||
"""
|
|
||||||
Session drives the collection and running of tests
|
|
||||||
and generates test events for reporters.
|
|
||||||
"""
|
|
||||||
LOAD_THRESHOLD_NEWITEMS = 5
|
LOAD_THRESHOLD_NEWITEMS = 5
|
||||||
ITEM_CHUNKSIZE = 10
|
ITEM_CHUNKSIZE = 10
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, numnodes, log=None):
|
||||||
self.queue = queue.Queue()
|
self.numnodes = numnodes
|
||||||
self.node2pending = {}
|
self.node2pending = {}
|
||||||
|
self.node2collection = {}
|
||||||
|
self.pending = []
|
||||||
|
if log is None:
|
||||||
|
self.log = py.log.Producer("loadsched")
|
||||||
|
else:
|
||||||
|
self.log = log.loadsched
|
||||||
|
self.collection_is_completed = False
|
||||||
|
|
||||||
|
def hasnodes(self):
|
||||||
|
return bool(self.node2pending)
|
||||||
|
|
||||||
|
def addnode(self, node):
|
||||||
|
self.node2pending[node] = []
|
||||||
|
|
||||||
|
def tests_finished(self):
|
||||||
|
if not self.collection_is_completed or self.pending:
|
||||||
|
return False
|
||||||
|
for items in self.node2pending.values():
|
||||||
|
if items:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def addnode_collection(self, node, collection):
|
||||||
|
assert not self.collection_is_completed
|
||||||
|
assert node in self.node2pending
|
||||||
|
self.node2collection[node] = list(collection)
|
||||||
|
if len(self.node2collection) >= self.numnodes:
|
||||||
|
self.collection_is_completed = True
|
||||||
|
|
||||||
|
def remove_item(self, node, item):
|
||||||
|
if item not in self.item2nodes:
|
||||||
|
raise AssertionError(item, self.item2nodes)
|
||||||
|
nodes = self.item2nodes[item]
|
||||||
|
if node in nodes: # the node might have gone down already
|
||||||
|
nodes.remove(node)
|
||||||
|
#if not nodes:
|
||||||
|
# del self.item2nodes[item]
|
||||||
|
pending = self.node2pending[node]
|
||||||
|
pending.remove(item)
|
||||||
|
# pre-load items-to-test if the node may become ready
|
||||||
|
if self.pending and len(pending) < self.LOAD_THRESHOLD_NEWITEMS:
|
||||||
|
item = self.pending.pop(0)
|
||||||
|
pending.append(item)
|
||||||
|
self.item2nodes.setdefault(item, []).append(node)
|
||||||
|
node.send_runtest(item)
|
||||||
|
#self.log("items waiting for node: %d" %(len(self.pending)))
|
||||||
|
#self.log("item2pending still executing: %s" %(self.item2nodes,))
|
||||||
|
#self.log("node2pending: %s" %(self.node2pending,))
|
||||||
|
|
||||||
|
def remove_node(self, node):
|
||||||
|
pending = self.node2pending.pop(node)
|
||||||
|
# KeyError if we didn't get an addnode() yet
|
||||||
|
for item in pending:
|
||||||
|
l = self.item2nodes[item]
|
||||||
|
l.remove(node)
|
||||||
|
if not l:
|
||||||
|
del self.item2nodes[item]
|
||||||
|
if not pending:
|
||||||
|
return
|
||||||
|
crashitem = pending.pop(0)
|
||||||
|
self.pending.extend(pending)
|
||||||
|
return crashitem
|
||||||
|
|
||||||
|
def init_distribute(self):
|
||||||
|
assert self.collection_is_completed
|
||||||
|
assert not hasattr(self, 'item2nodes')
|
||||||
self.item2nodes = {}
|
self.item2nodes = {}
|
||||||
super(DSession, self).__init__(config=config)
|
# XXX allow nodes to have different collections
|
||||||
|
col = list(self.node2collection.values())[0]
|
||||||
|
for node, collection in self.node2collection.items():
|
||||||
|
assert collection == col
|
||||||
|
self.pending = col
|
||||||
|
if not col:
|
||||||
|
return
|
||||||
|
available = list(self.node2pending.items())
|
||||||
|
num_available = self.numnodes
|
||||||
|
max_one_round = num_available * self.ITEM_CHUNKSIZE -1
|
||||||
|
for i, item in enumerate(self.pending):
|
||||||
|
nodeindex = i % num_available
|
||||||
|
node, pending = available[nodeindex]
|
||||||
|
node.send_runtest(item)
|
||||||
|
self.item2nodes.setdefault(item, []).append(node)
|
||||||
|
pending.append(item)
|
||||||
|
if i >= max_one_round:
|
||||||
|
break
|
||||||
|
del self.pending[:i+1]
|
||||||
|
|
||||||
|
class Interrupted(KeyboardInterrupt):
|
||||||
|
""" signals an immediate interruption. """
|
||||||
|
|
||||||
|
class DSession:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.log = py.log.Producer("dsession")
|
||||||
|
if not config.option.debug:
|
||||||
|
py.log.setconsumer(self.log._keywords, None)
|
||||||
|
self.shuttingdown = False
|
||||||
|
self.countfailures = 0
|
||||||
|
self.maxfail = config.getvalue("maxfail")
|
||||||
|
self.queue = queue.Queue()
|
||||||
try:
|
try:
|
||||||
self.terminal = config.pluginmanager.getplugin("terminalreporter")
|
self.terminal = config.pluginmanager.getplugin("terminalreporter")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.terminal = None
|
self.terminal = None
|
||||||
self._nodesready = py.std.threading.Event()
|
else:
|
||||||
|
self.trdist = TerminalDistReporter(config)
|
||||||
|
config.pluginmanager.register(self.trdist, "terminaldistreporter")
|
||||||
|
|
||||||
def report_line(self, line):
|
def report_line(self, line):
|
||||||
if self.terminal:
|
if self.terminal and self.config.option.verbose >= 0:
|
||||||
self.terminal.write_line(line)
|
self.terminal.write_line(line)
|
||||||
|
|
||||||
def pytest_gwmanage_rsyncstart(self, source, gateways):
|
@pytest.mark.trylast
|
||||||
targets = ",".join([gw.id for gw in gateways])
|
def pytest_sessionstart(self, session):
|
||||||
msg = "[%s] rsyncing: %s" %(targets, source)
|
self.nodemanager = NodeManager(self.config)
|
||||||
self.report_line(msg)
|
self.nodemanager.setup_nodes(putevent=self.queue.put)
|
||||||
|
|
||||||
#def pytest_gwmanage_rsyncfinish(self, source, gateways):
|
def pytest_sessionfinish(self, session):
|
||||||
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
|
""" teardown any resources after a test run. """
|
||||||
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
|
nm = getattr(self, 'nodemanager', None) # if not fully initialized
|
||||||
|
if nm is not None:
|
||||||
|
nm.teardown_nodes()
|
||||||
|
|
||||||
def main(self, colitems):
|
def pytest_collection(self):
|
||||||
self.sessionstarts()
|
# prohibit collection of test items in master process
|
||||||
self.setup()
|
return True
|
||||||
allitems = self.collect_all_items(colitems)
|
|
||||||
exitstatus = self.loop(allitems)
|
|
||||||
self.teardown()
|
|
||||||
self.sessionfinishes(exitstatus=exitstatus)
|
|
||||||
return exitstatus
|
|
||||||
|
|
||||||
def collect_all_items(self, colitems):
|
def pytest_runtestloop(self):
|
||||||
self.report_line("[master] starting full item collection ...")
|
numnodes = len(self.nodemanager.specs)
|
||||||
allitems = list(self.collect(colitems))
|
dist = self.config.getvalue("dist")
|
||||||
self.report_line("[master] collected %d items" %(len(allitems)))
|
if dist == "load":
|
||||||
return allitems
|
self.sched = LoadScheduling(numnodes, log=self.log)
|
||||||
|
elif dist == "each":
|
||||||
|
self.sched = EachScheduling(numnodes, log=self.log)
|
||||||
|
else:
|
||||||
|
assert 0, dist
|
||||||
|
self.shouldstop = False
|
||||||
|
self.session_finished = False
|
||||||
|
while not self.session_finished:
|
||||||
|
self.loop_once()
|
||||||
|
if self.shouldstop:
|
||||||
|
raise Interrupted(str(self.shouldstop))
|
||||||
|
return True
|
||||||
|
|
||||||
def loop_once(self, loopstate):
|
def loop_once(self):
|
||||||
if loopstate.shuttingdown:
|
""" process one callback from one of the slaves. """
|
||||||
return self.loop_once_shutdown(loopstate)
|
|
||||||
colitems = loopstate.colitems
|
|
||||||
if self._nodesready.isSet() and loopstate.dowork and colitems:
|
|
||||||
self.triggertesting(loopstate.colitems)
|
|
||||||
colitems[:] = []
|
|
||||||
# we use a timeout here so that control-C gets through
|
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
eventcall = self.queue.get(timeout=2.0)
|
eventcall = self.queue.get(timeout=2.0)
|
||||||
break
|
break
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
loopstate.dowork = True
|
callname, kwargs = eventcall
|
||||||
|
assert callname, kwargs
|
||||||
callname, args, kwargs = eventcall
|
method = "slave_" + callname
|
||||||
if callname is not None:
|
call = getattr(self, method)
|
||||||
call = getattr(self.config.hook, callname)
|
self.log("calling method: %s(**%s)" % (method, kwargs))
|
||||||
assert not args
|
call(**kwargs)
|
||||||
call(**kwargs)
|
if self.sched.tests_finished():
|
||||||
|
|
||||||
# termination conditions
|
|
||||||
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()
|
self.triggershutdown()
|
||||||
loopstate.shuttingdown = True
|
|
||||||
if not self.node2pending:
|
|
||||||
loopstate.exitstatus = session.EXIT_NOHOSTS
|
|
||||||
|
|
||||||
def loop_once_shutdown(self, loopstate):
|
#
|
||||||
# once we are in shutdown mode we dont send
|
# callbacks for processing events from slaves
|
||||||
# events other than HostDown upstream
|
#
|
||||||
eventname, args, kwargs = self.queue.get()
|
|
||||||
if eventname == "pytest_testnodedown":
|
|
||||||
self.config.hook.pytest_testnodedown(**kwargs)
|
|
||||||
self.removenode(kwargs['node'])
|
|
||||||
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 = session.EXIT_INTERNALERROR
|
|
||||||
elif eventname == "pytest__teardown_final_logerror":
|
|
||||||
self.config.hook.pytest__teardown_final_logerror(**kwargs)
|
|
||||||
loopstate.exitstatus = session.EXIT_TESTSFAILED
|
|
||||||
if not self.node2pending:
|
|
||||||
# finished
|
|
||||||
if loopstate.testsfailed:
|
|
||||||
loopstate.exitstatus = session.EXIT_TESTSFAILED
|
|
||||||
else:
|
|
||||||
loopstate.exitstatus = session.EXIT_OK
|
|
||||||
#self.config.pluginmanager.unregister(loopstate)
|
|
||||||
|
|
||||||
def _initloopstate(self, colitems):
|
def slave_slaveready(self, node, slaveinfo):
|
||||||
loopstate = LoopState(self, colitems)
|
node.slaveinfo = slaveinfo
|
||||||
self.config.pluginmanager.register(loopstate)
|
node.slaveinfo['id'] = node.gateway.id
|
||||||
return loopstate
|
node.slaveinfo['spec'] = node.gateway.spec
|
||||||
|
self.config.hook.pytest_testnodeready(node=node)
|
||||||
def loop(self, colitems):
|
self.sched.addnode(node)
|
||||||
try:
|
if self.shuttingdown:
|
||||||
loopstate = self._initloopstate(colitems)
|
|
||||||
loopstate.dowork = False # first receive at least one HostUp events
|
|
||||||
while 1:
|
|
||||||
self.loop_once(loopstate)
|
|
||||||
if loopstate.exitstatus is not None:
|
|
||||||
exitstatus = loopstate.exitstatus
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
excinfo = py.code.ExceptionInfo()
|
|
||||||
if excinfo.errisinstance(ExitFirstInterrupt):
|
|
||||||
exitstatus = session.EXIT_TESTSFAILED
|
|
||||||
else:
|
|
||||||
self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
|
||||||
exitstatus = session.EXIT_INTERRUPTED
|
|
||||||
except:
|
|
||||||
self.config.pluginmanager.notify_exception()
|
|
||||||
exitstatus = session.EXIT_INTERNALERROR
|
|
||||||
self.config.pluginmanager.unregister(loopstate)
|
|
||||||
if exitstatus == 0 and self._testsfailed:
|
|
||||||
exitstatus = session.EXIT_TESTSFAILED
|
|
||||||
return exitstatus
|
|
||||||
|
|
||||||
def triggershutdown(self):
|
|
||||||
for node in self.node2pending:
|
|
||||||
node.shutdown()
|
node.shutdown()
|
||||||
|
|
||||||
def addnode(self, node):
|
def slave_slavefinished(self, node):
|
||||||
assert node not in self.node2pending
|
self.config.hook.pytest_testnodedown(node=node, error=None)
|
||||||
self.node2pending[node] = []
|
if node.slaveoutput['exitstatus'] == 2: # keyboard-interrupt
|
||||||
if (not hasattr(self, 'nodemanager') or
|
self.shouldstop = "%s received keyboard-interrupt" % (node,)
|
||||||
len(self.node2pending) == len(self.nodemanager.gwmanager.group)):
|
self.slave_errordown(node, "keyboard-interrupt")
|
||||||
self._nodesready.set()
|
return
|
||||||
|
crashitem = self.sched.remove_node(node)
|
||||||
|
#assert not crashitem, (crashitem, node)
|
||||||
|
if self.shuttingdown and not self.sched.hasnodes():
|
||||||
|
self.session_finished = True
|
||||||
|
|
||||||
def removenode(self, node):
|
def slave_errordown(self, node, error):
|
||||||
|
self.config.hook.pytest_testnodedown(node=node, error=error)
|
||||||
try:
|
try:
|
||||||
pending = self.node2pending.pop(node)
|
crashitem = self.sched.remove_node(node)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# this happens if we didn't receive a testnodeready event yet
|
pass
|
||||||
return []
|
|
||||||
for item in pending:
|
|
||||||
l = self.item2nodes[item]
|
|
||||||
l.remove(node)
|
|
||||||
if not l:
|
|
||||||
del self.item2nodes[item]
|
|
||||||
return pending
|
|
||||||
|
|
||||||
def triggertesting(self, colitems):
|
|
||||||
# for now we don't allow sending collectors
|
|
||||||
for next in colitems:
|
|
||||||
assert isinstance(next, py.test.collect.Item), next
|
|
||||||
senditems = list(colitems)
|
|
||||||
if self.config.option.dist == "each":
|
|
||||||
self.senditems_each(senditems)
|
|
||||||
else:
|
else:
|
||||||
# XXX assert self.config.option.dist == "load"
|
if crashitem:
|
||||||
self.senditems_load(senditems)
|
self.handle_crashitem(crashitem, node)
|
||||||
|
#self.report_line("item crashed on node: %s" % crashitem)
|
||||||
|
if not self.sched.hasnodes():
|
||||||
|
self.session_finished = True
|
||||||
|
|
||||||
def queueevent(self, eventname, **kwargs):
|
def slave_collectionfinish(self, node, ids):
|
||||||
self.queue.put((eventname, (), kwargs))
|
self.sched.addnode_collection(node, ids)
|
||||||
|
if self.terminal:
|
||||||
|
self.trdist.setstatus(node.gateway.spec, "[%d]" %(len(ids)))
|
||||||
|
|
||||||
def senditems_each(self, tosend):
|
if self.sched.collection_is_completed:
|
||||||
if not tosend:
|
if self.terminal:
|
||||||
return
|
self.trdist.ensure_show_status()
|
||||||
for node, pending in self.node2pending.items():
|
self.terminal.write_line("")
|
||||||
node.sendlist(tosend)
|
self.terminal.write_line("scheduling tests via %s" %(
|
||||||
pending.extend(tosend)
|
self.sched.__class__.__name__))
|
||||||
for item in tosend:
|
|
||||||
nodes = self.item2nodes.setdefault(item, [])
|
|
||||||
assert node not in nodes
|
|
||||||
nodes.append(node)
|
|
||||||
item.ihook.pytest_itemstart(item=item, node=node)
|
|
||||||
tosend[:] = []
|
|
||||||
|
|
||||||
def senditems_load(self, tosend):
|
self.sched.init_distribute()
|
||||||
if not tosend:
|
|
||||||
return
|
|
||||||
available = []
|
|
||||||
for node, pending in self.node2pending.items():
|
|
||||||
if len(pending) < self.LOAD_THRESHOLD_NEWITEMS:
|
|
||||||
available.append((node, pending))
|
|
||||||
num_available = len(available)
|
|
||||||
max_one_round = num_available * self.ITEM_CHUNKSIZE -1
|
|
||||||
if num_available:
|
|
||||||
for i, item in enumerate(tosend):
|
|
||||||
nodeindex = i % num_available
|
|
||||||
node, pending = available[nodeindex]
|
|
||||||
node.send(item)
|
|
||||||
self.item2nodes.setdefault(item, []).append(node)
|
|
||||||
item.ihook.pytest_itemstart(item=item, node=node)
|
|
||||||
pending.append(item)
|
|
||||||
if i >= max_one_round:
|
|
||||||
break
|
|
||||||
del tosend[:i+1]
|
|
||||||
if tosend:
|
|
||||||
# we have some left, give it to the main loop
|
|
||||||
self.queueevent("pytest_rescheduleitems", items=tosend)
|
|
||||||
|
|
||||||
def removeitem(self, item, node):
|
def slave_logstart(self, node, nodeid, location):
|
||||||
if item not in self.item2nodes:
|
self.config.hook.pytest_runtest_logstart(
|
||||||
raise AssertionError(item, self.item2nodes)
|
nodeid=nodeid, location=location)
|
||||||
nodes = self.item2nodes[item]
|
|
||||||
if node in nodes: # the node might have gone down already
|
|
||||||
nodes.remove(node)
|
|
||||||
if not nodes:
|
|
||||||
del self.item2nodes[item]
|
|
||||||
pending = self.node2pending[node]
|
|
||||||
pending.remove(item)
|
|
||||||
|
|
||||||
def handle_crashitem(self, item, node):
|
def slave_testreport(self, node, rep):
|
||||||
runner = item.config.pluginmanager.getplugin("runner")
|
if not (rep.passed and rep.when != "call"):
|
||||||
info = "!!! Node %r crashed during running of test %r" %(node, item)
|
if rep.when in ("setup", "call"):
|
||||||
rep = runner.ItemTestReport(item=item, excinfo=info, when="???")
|
self.sched.remove_item(node, rep.nodeid)
|
||||||
|
#self.report_line("testreport %s: %s" %(rep.id, rep.status))
|
||||||
rep.node = node
|
rep.node = node
|
||||||
item.ihook.pytest_runtest_logreport(report=rep)
|
self.config.hook.pytest_runtest_logreport(report=rep)
|
||||||
|
self._handlefailures(rep)
|
||||||
|
|
||||||
def setup(self):
|
def slave_teardownreport(self, node, rep):
|
||||||
""" setup any neccessary resources ahead of the test run. """
|
rep.node = node
|
||||||
self.nodemanager = NodeManager(self.config)
|
self.config.hook.pytest__teardown_final_logerror(report=rep)
|
||||||
self.nodemanager.setup_nodes(putevent=self.queue.put)
|
|
||||||
|
def slave_collectreport(self, node, rep):
|
||||||
|
#self.report_line("collectreport %s: %s" %(rep.id, rep.status))
|
||||||
|
#rep.node = node
|
||||||
|
self._handlefailures(rep)
|
||||||
|
|
||||||
|
def _handlefailures(self, rep):
|
||||||
|
if rep.failed:
|
||||||
|
self.countfailures += 1
|
||||||
|
if self.maxfail and self.countfailures >= self.maxfail:
|
||||||
|
self.shouldstop = "stopping after %d failures" % (
|
||||||
|
self.countfailures)
|
||||||
|
|
||||||
|
def triggershutdown(self):
|
||||||
|
self.log("triggering shutdown")
|
||||||
|
self.shuttingdown = True
|
||||||
|
for node in self.sched.node2pending:
|
||||||
|
node.shutdown()
|
||||||
|
|
||||||
|
def handle_crashitem(self, nodeid, slave):
|
||||||
|
# XXX get more reporting info by recording pytest_runtest_logstart?
|
||||||
|
runner = self.config.pluginmanager.getplugin("runner")
|
||||||
|
fspath = nodeid.split("::")[0]
|
||||||
|
msg = "Slave %r crashed while running %r" %(slave.gateway.id, nodeid)
|
||||||
|
rep = runner.TestReport(nodeid, (fspath, None, fspath), (),
|
||||||
|
"failed", msg, "???")
|
||||||
|
rep.node = slave
|
||||||
|
self.config.hook.pytest_runtest_logreport(report=rep)
|
||||||
|
|
||||||
|
class TerminalDistReporter:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.tr = config.pluginmanager.getplugin("terminalreporter")
|
||||||
|
self._status = {}
|
||||||
|
self._lastlen = 0
|
||||||
|
|
||||||
|
def write_line(self, msg):
|
||||||
|
self.tr.write_line(msg)
|
||||||
|
|
||||||
|
def ensure_show_status(self):
|
||||||
|
if not self.tr.hasmarkup:
|
||||||
|
self.write_line(self.getstatus())
|
||||||
|
|
||||||
|
def setstatus(self, spec, status, show=True):
|
||||||
|
self._status[spec.id] = status
|
||||||
|
if show and self.tr.hasmarkup:
|
||||||
|
self.rewrite(self.getstatus())
|
||||||
|
|
||||||
|
def getstatus(self):
|
||||||
|
parts = ["%s %s" %(spec.id, self._status[spec.id])
|
||||||
|
for spec in self._specs]
|
||||||
|
return " / ".join(parts)
|
||||||
|
|
||||||
|
def rewrite(self, line, newline=False):
|
||||||
|
pline = line + " " * max(self._lastlen-len(line), 0)
|
||||||
|
if newline:
|
||||||
|
self._lastlen = 0
|
||||||
|
pline += "\n"
|
||||||
|
else:
|
||||||
|
self._lastlen = len(line)
|
||||||
|
self.tr.rewrite(pline, bold=True)
|
||||||
|
|
||||||
|
def pytest_xdist_setupnodes(self, specs):
|
||||||
|
self._specs = specs
|
||||||
|
for spec in specs:
|
||||||
|
self.setstatus(spec, "I", show=False)
|
||||||
|
self.setstatus(spec, "I", show=True)
|
||||||
|
self.ensure_show_status()
|
||||||
|
|
||||||
|
def pytest_xdist_newgateway(self, gateway):
|
||||||
|
if self.config.option.verbose > 0:
|
||||||
|
rinfo = gateway._rinfo()
|
||||||
|
version = "%s.%s.%s" % rinfo.version_info[:3]
|
||||||
|
self.rewrite("[%s] %s Python %s cwd: %s" % (
|
||||||
|
gateway.id, rinfo.platform, version, rinfo.cwd),
|
||||||
|
newline=True)
|
||||||
|
self.setstatus(gateway.spec, "C")
|
||||||
|
|
||||||
|
def pytest_testnodeready(self, node):
|
||||||
|
if self.config.option.verbose > 0:
|
||||||
|
d = node.slaveinfo
|
||||||
|
infoline = "[%s] Python %s" %(
|
||||||
|
d['id'],
|
||||||
|
d['version'].replace('\n', ' -- '),)
|
||||||
|
self.rewrite(infoline, newline=True)
|
||||||
|
self.setstatus(node.gateway.spec, "ok")
|
||||||
|
|
||||||
|
def pytest_testnodedown(self, node, error):
|
||||||
|
if not error:
|
||||||
|
return
|
||||||
|
self.write_line("[%s] node down: %s" %(node.gateway.id, error))
|
||||||
|
|
||||||
|
#def pytest_xdist_rsyncstart(self, source, gateways):
|
||||||
|
# targets = ",".join([gw.id for gw in gateways])
|
||||||
|
# msg = "[%s] rsyncing: %s" %(targets, source)
|
||||||
|
# self.write_line(msg)
|
||||||
|
#def pytest_xdist_rsyncfinish(self, source, gateways):
|
||||||
|
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
|
||||||
|
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
|
||||||
|
|
||||||
def teardown(self):
|
|
||||||
""" teardown any resources after a test run. """
|
|
||||||
self.nodemanager.teardown_nodes()
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
"""
|
|
||||||
instantiating, managing and rsyncing to test hosts
|
|
||||||
"""
|
|
||||||
|
|
||||||
import py
|
|
||||||
import sys, os.path
|
|
||||||
import execnet
|
|
||||||
from execnet.gateway_base import RemoteError
|
|
||||||
|
|
||||||
class GatewayManager:
|
|
||||||
EXIT_TIMEOUT = 10
|
|
||||||
RemoteError = RemoteError
|
|
||||||
def __init__(self, specs, hook, defaultchdir="pyexecnetcache"):
|
|
||||||
self.specs = []
|
|
||||||
self.hook = hook
|
|
||||||
self.group = execnet.Group()
|
|
||||||
for spec in specs:
|
|
||||||
if not isinstance(spec, execnet.XSpec):
|
|
||||||
spec = execnet.XSpec(spec)
|
|
||||||
if not spec.chdir and not spec.popen:
|
|
||||||
spec.chdir = defaultchdir
|
|
||||||
self.specs.append(spec)
|
|
||||||
|
|
||||||
def makegateways(self):
|
|
||||||
assert not list(self.group)
|
|
||||||
for spec in self.specs:
|
|
||||||
gw = self.group.makegateway(spec)
|
|
||||||
self.hook.pytest_gwmanage_newgateway(
|
|
||||||
gateway=gw, platinfo=gw._rinfo())
|
|
||||||
|
|
||||||
def rsync(self, source, notify=None, verbose=False, ignores=None):
|
|
||||||
""" perform rsync to all remote hosts.
|
|
||||||
"""
|
|
||||||
rsync = HostRSync(source, verbose=verbose, ignores=ignores)
|
|
||||||
seen = py.builtin.set()
|
|
||||||
gateways = []
|
|
||||||
for gateway in self.group:
|
|
||||||
spec = gateway.spec
|
|
||||||
if spec.popen and not spec.chdir:
|
|
||||||
# XXX this assumes that sources are python-packages
|
|
||||||
# and that adding the basedir does not hurt
|
|
||||||
gateway.remote_exec("""
|
|
||||||
import sys ; sys.path.insert(0, %r)
|
|
||||||
""" % os.path.dirname(str(source))).waitclose()
|
|
||||||
continue
|
|
||||||
if spec not in seen:
|
|
||||||
def finished():
|
|
||||||
if notify:
|
|
||||||
notify("rsyncrootready", spec, source)
|
|
||||||
rsync.add_target_host(gateway, finished=finished)
|
|
||||||
seen.add(spec)
|
|
||||||
gateways.append(gateway)
|
|
||||||
if seen:
|
|
||||||
self.hook.pytest_gwmanage_rsyncstart(
|
|
||||||
source=source,
|
|
||||||
gateways=gateways,
|
|
||||||
)
|
|
||||||
rsync.send()
|
|
||||||
self.hook.pytest_gwmanage_rsyncfinish(
|
|
||||||
source=source,
|
|
||||||
gateways=gateways,
|
|
||||||
)
|
|
||||||
|
|
||||||
def exit(self):
|
|
||||||
self.group.terminate(self.EXIT_TIMEOUT)
|
|
||||||
|
|
||||||
class HostRSync(execnet.RSync):
|
|
||||||
""" RSyncer that filters out common files
|
|
||||||
"""
|
|
||||||
def __init__(self, sourcedir, *args, **kwargs):
|
|
||||||
self._synced = {}
|
|
||||||
ignores= None
|
|
||||||
if 'ignores' in kwargs:
|
|
||||||
ignores = kwargs.pop('ignores')
|
|
||||||
self._ignores = ignores or []
|
|
||||||
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
|
|
||||||
|
|
||||||
def filter(self, path):
|
|
||||||
path = py.path.local(path)
|
|
||||||
if not path.ext in ('.pyc', '.pyo'):
|
|
||||||
if not path.basename.endswith('~'):
|
|
||||||
if path.check(dotfile=0):
|
|
||||||
for x in self._ignores:
|
|
||||||
if path == x:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_target_host(self, gateway, finished=None):
|
|
||||||
remotepath = os.path.basename(self._sourcedir)
|
|
||||||
super(HostRSync, self).add_target(gateway, remotepath,
|
|
||||||
finishedcallback=finished,
|
|
||||||
delete=True,)
|
|
||||||
|
|
||||||
def _report_send_file(self, gateway, modified_rel_path):
|
|
||||||
if self._verbose:
|
|
||||||
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
|
|
||||||
remotepath = gateway.spec.chdir
|
|
||||||
py.builtin.print_('%s:%s <= %s' %
|
|
||||||
(gateway.spec, remotepath, path))
|
|
||||||
227
xdist/looponfail.py
Normal file
227
xdist/looponfail.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Implement -f aka looponfailing for py.test.
|
||||||
|
|
||||||
|
NOTE that we try to avoid loading and depending on application modules
|
||||||
|
within the controlling process (the one that starts repeatedly test
|
||||||
|
processes) otherwise changes to source code can crash
|
||||||
|
the controlling process which should best never happen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import py, pytest
|
||||||
|
import sys
|
||||||
|
import execnet
|
||||||
|
|
||||||
|
def looponfail_main(config):
|
||||||
|
remotecontrol = RemoteControl(config)
|
||||||
|
rootdirs = config.getini("looponfailroots")
|
||||||
|
statrecorder = StatRecorder(rootdirs)
|
||||||
|
try:
|
||||||
|
while 1:
|
||||||
|
remotecontrol.loop_once()
|
||||||
|
if not remotecontrol.failures and remotecontrol.wasfailing:
|
||||||
|
continue # the last failures passed, let's immediately rerun all
|
||||||
|
repr_pytest_looponfailinfo(
|
||||||
|
failreports=remotecontrol.failures,
|
||||||
|
rootdirs=rootdirs)
|
||||||
|
statrecorder.waitonchange(checkinterval=2.0)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
|
||||||
|
class RemoteControl(object):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.failures = []
|
||||||
|
|
||||||
|
def trace(self, *args):
|
||||||
|
if self.config.option.debug:
|
||||||
|
msg = " ".join([str(x) for x in args])
|
||||||
|
py.builtin.print_("RemoteControl:", msg)
|
||||||
|
|
||||||
|
def initgateway(self):
|
||||||
|
return execnet.makegateway("popen")
|
||||||
|
|
||||||
|
def setup(self, out=None):
|
||||||
|
if out is None:
|
||||||
|
out = py.io.TerminalWriter()
|
||||||
|
if hasattr(self, 'gateway'):
|
||||||
|
raise ValueError("already have gateway %r" % self.gateway)
|
||||||
|
self.trace("setting up slave session")
|
||||||
|
self.gateway = self.initgateway()
|
||||||
|
self.channel = channel = self.gateway.remote_exec(init_slave_session,
|
||||||
|
args=self.config.args,
|
||||||
|
option_dict=vars(self.config.option),
|
||||||
|
)
|
||||||
|
remote_outchannel = channel.receive()
|
||||||
|
def write(s):
|
||||||
|
out._file.write(s)
|
||||||
|
out._file.flush()
|
||||||
|
remote_outchannel.setcallback(write)
|
||||||
|
|
||||||
|
def ensure_teardown(self):
|
||||||
|
if hasattr(self, 'channel'):
|
||||||
|
if not self.channel.isclosed():
|
||||||
|
self.trace("closing", self.channel)
|
||||||
|
self.channel.close()
|
||||||
|
del self.channel
|
||||||
|
if hasattr(self, 'gateway'):
|
||||||
|
self.trace("exiting", self.gateway)
|
||||||
|
self.gateway.exit()
|
||||||
|
del self.gateway
|
||||||
|
|
||||||
|
def runsession(self):
|
||||||
|
try:
|
||||||
|
self.trace("sending", self.failures)
|
||||||
|
self.channel.send(self.failures)
|
||||||
|
try:
|
||||||
|
return self.channel.receive()
|
||||||
|
except self.channel.RemoteError:
|
||||||
|
e = sys.exc_info()[1]
|
||||||
|
self.trace("ERROR", e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self.ensure_teardown()
|
||||||
|
|
||||||
|
def loop_once(self):
|
||||||
|
self.setup()
|
||||||
|
self.wasfailing = self.failures and len(self.failures)
|
||||||
|
result = self.runsession()
|
||||||
|
failures, reports, collection_failed = result
|
||||||
|
if collection_failed:
|
||||||
|
reports = ["Collection failed, keeping previous failure set"]
|
||||||
|
else:
|
||||||
|
self.failures = failures
|
||||||
|
|
||||||
|
def repr_pytest_looponfailinfo(failreports, rootdirs):
|
||||||
|
tr = py.io.TerminalWriter()
|
||||||
|
if failreports:
|
||||||
|
tr.sep("#", "LOOPONFAILING", bold=True)
|
||||||
|
for report in failreports:
|
||||||
|
if report:
|
||||||
|
tr.line(report, red=True)
|
||||||
|
tr.sep("#", "waiting for changes", bold=True)
|
||||||
|
for rootdir in rootdirs:
|
||||||
|
tr.line("### Watching: %s" %(rootdir,), bold=True)
|
||||||
|
|
||||||
|
|
||||||
|
def init_slave_session(channel, args, option_dict):
|
||||||
|
import os, sys
|
||||||
|
import py
|
||||||
|
outchannel = channel.gateway.newchannel()
|
||||||
|
sys.stdout = sys.stderr = outchannel.makefile('w')
|
||||||
|
channel.send(outchannel)
|
||||||
|
# prune sys.path to not contain relative paths
|
||||||
|
newpaths = []
|
||||||
|
for p in sys.path:
|
||||||
|
if p:
|
||||||
|
if not os.path.isabs(p):
|
||||||
|
p = os.path.abspath(p)
|
||||||
|
newpaths.append(p)
|
||||||
|
sys.path[:] = newpaths
|
||||||
|
|
||||||
|
#fullwidth, hasmarkup = channel.receive()
|
||||||
|
from _pytest.config import Config
|
||||||
|
config = Config.fromdictargs(option_dict, list(args))
|
||||||
|
config.args = args
|
||||||
|
from xdist.looponfail import SlaveFailSession
|
||||||
|
SlaveFailSession(config, channel).main()
|
||||||
|
|
||||||
|
class SlaveFailSession:
|
||||||
|
def __init__(self, config, channel):
|
||||||
|
self.config = config
|
||||||
|
self.channel = channel
|
||||||
|
self.recorded_failures = []
|
||||||
|
self.collection_failed = False
|
||||||
|
config.pluginmanager.register(self)
|
||||||
|
config.option.looponfail = False
|
||||||
|
config.option.usepdb = False
|
||||||
|
|
||||||
|
def DEBUG(self, *args):
|
||||||
|
if self.config.option.debug:
|
||||||
|
print(" ".join(map(str, args)))
|
||||||
|
|
||||||
|
def pytest_collection(self, session):
|
||||||
|
self.session = session
|
||||||
|
self.trails = self.current_command
|
||||||
|
hook = self.session.ihook
|
||||||
|
try:
|
||||||
|
items = session.perform_collect(self.trails or None)
|
||||||
|
except pytest.UsageError:
|
||||||
|
items = session.perform_collect(None)
|
||||||
|
hook.pytest_collection_modifyitems(session=session, config=session.config, items=items)
|
||||||
|
hook.pytest_collection_finish(session=session)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def pytest_runtest_logreport(self, report):
|
||||||
|
if report.failed:
|
||||||
|
self.recorded_failures.append(report)
|
||||||
|
|
||||||
|
def pytest_collectreport(self, report):
|
||||||
|
if report.failed:
|
||||||
|
self.recorded_failures.append(report)
|
||||||
|
self.collection_failed = True
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
self.DEBUG("SLAVE: received configuration, waiting for command trails")
|
||||||
|
try:
|
||||||
|
command = self.channel.receive()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return # in the slave we can't do much about this
|
||||||
|
self.DEBUG("received", command)
|
||||||
|
self.current_command = command
|
||||||
|
self.config.hook.pytest_cmdline_main(config=self.config)
|
||||||
|
trails, failreports = [], []
|
||||||
|
for rep in self.recorded_failures:
|
||||||
|
trails.append(rep.nodeid)
|
||||||
|
loc = rep.longrepr
|
||||||
|
loc = str(getattr(loc, 'reprcrash', loc))
|
||||||
|
failreports.append(loc)
|
||||||
|
self.channel.send((trails, failreports, self.collection_failed))
|
||||||
|
|
||||||
|
class StatRecorder:
|
||||||
|
def __init__(self, rootdirlist):
|
||||||
|
self.rootdirlist = rootdirlist
|
||||||
|
self.statcache = {}
|
||||||
|
self.check() # snapshot state
|
||||||
|
|
||||||
|
def fil(self, p):
|
||||||
|
return p.ext in ('.py', '.txt', '.c', '.h')
|
||||||
|
def rec(self, p):
|
||||||
|
return p.check(dotfile=0)
|
||||||
|
|
||||||
|
def waitonchange(self, checkinterval=1.0):
|
||||||
|
while 1:
|
||||||
|
changed = self.check()
|
||||||
|
if changed:
|
||||||
|
return
|
||||||
|
py.std.time.sleep(checkinterval)
|
||||||
|
|
||||||
|
def check(self, removepycfiles=True):
|
||||||
|
changed = False
|
||||||
|
statcache = self.statcache
|
||||||
|
newstat = {}
|
||||||
|
for rootdir in self.rootdirlist:
|
||||||
|
for path in rootdir.visit(self.fil, self.rec):
|
||||||
|
oldstat = statcache.pop(path, None)
|
||||||
|
try:
|
||||||
|
newstat[path] = curstat = path.stat()
|
||||||
|
except py.error.ENOENT:
|
||||||
|
if oldstat:
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
if oldstat:
|
||||||
|
if oldstat.mtime != curstat.mtime or \
|
||||||
|
oldstat.size != curstat.size:
|
||||||
|
changed = True
|
||||||
|
py.builtin.print_("# MODIFIED", path)
|
||||||
|
if removepycfiles and path.ext == ".py":
|
||||||
|
pycfile = path + "c"
|
||||||
|
if pycfile.check():
|
||||||
|
pycfile.remove()
|
||||||
|
|
||||||
|
else:
|
||||||
|
changed = True
|
||||||
|
if statcache:
|
||||||
|
changed = True
|
||||||
|
self.statcache = newstat
|
||||||
|
return changed
|
||||||
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
"""
|
|
||||||
|
|
||||||
Pickling support for two processes that want to exchange
|
|
||||||
*immutable* object instances. Immutable in the sense
|
|
||||||
that the receiving side of an object can modify its
|
|
||||||
copy but when it sends it back the original sending
|
|
||||||
side will continue to see its unmodified version
|
|
||||||
(and no actual state will go over the wire).
|
|
||||||
|
|
||||||
This module also implements an experimental
|
|
||||||
execnet pickling channel using this idea.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import py
|
|
||||||
import sys, os, struct
|
|
||||||
#debug = open("log-mypickle-%d" % os.getpid(), 'w')
|
|
||||||
|
|
||||||
if sys.version_info >= (3,0):
|
|
||||||
makekey = lambda x: x
|
|
||||||
fromkey = lambda x: x
|
|
||||||
from pickle import _Pickler as Pickler
|
|
||||||
from pickle import _Unpickler as Unpickler
|
|
||||||
else:
|
|
||||||
makekey = str
|
|
||||||
fromkey = int
|
|
||||||
from pickle import Pickler, Unpickler
|
|
||||||
|
|
||||||
|
|
||||||
class MyPickler(Pickler):
|
|
||||||
""" Pickler with a custom memoize()
|
|
||||||
to take care of unique ID creation.
|
|
||||||
See the usage in ImmutablePickler
|
|
||||||
"""
|
|
||||||
def __init__(self, immo, file, protocol, uneven):
|
|
||||||
Pickler.__init__(self, file, protocol)
|
|
||||||
self.uneven = uneven
|
|
||||||
self._unpicklememo = immo._unpicklememo
|
|
||||||
self.memo = immo._picklememo
|
|
||||||
|
|
||||||
def memoize(self, obj):
|
|
||||||
if self.fast:
|
|
||||||
return
|
|
||||||
assert id(obj) not in self.memo
|
|
||||||
memo_len = len(self.memo)
|
|
||||||
key = memo_len * 2 + self.uneven
|
|
||||||
self.write(self.put(key))
|
|
||||||
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):
|
|
||||||
# def save_string(self, obj, pack=struct.pack):
|
|
||||||
# obj = unicode(obj)
|
|
||||||
# self.save_unicode(obj, pack=pack)
|
|
||||||
# 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:
|
|
||||||
def __init__(self, uneven, protocol=0):
|
|
||||||
""" ImmutablePicklers are instantiated in Pairs.
|
|
||||||
The two sides need to create unique IDs
|
|
||||||
while pickling their objects. This is
|
|
||||||
done by using either even or uneven
|
|
||||||
numbers, depending on the instantiation
|
|
||||||
parameter.
|
|
||||||
"""
|
|
||||||
self._picklememo = {}
|
|
||||||
self._unpicklememo = UnpicklingDict(self._picklememo)
|
|
||||||
self._protocol = protocol
|
|
||||||
self.uneven = uneven and 1 or 0
|
|
||||||
|
|
||||||
def selfmemoize(self, obj):
|
|
||||||
# this is for feeding objects to ourselfes
|
|
||||||
# which be the case e.g. if you want to pickle
|
|
||||||
# from a forked process back to the original
|
|
||||||
f = py.io.BytesIO()
|
|
||||||
pickler = MyPickler(self, f, self._protocol, uneven=self.uneven)
|
|
||||||
pickler.memoize(obj)
|
|
||||||
|
|
||||||
def dumps(self, obj):
|
|
||||||
f = py.io.BytesIO()
|
|
||||||
pickler = MyPickler(self, f, self._protocol, uneven=self.uneven)
|
|
||||||
pickler.dump(obj)
|
|
||||||
#print >>debug, "dumped", obj
|
|
||||||
#print >>debug, "picklememo", self._picklememo
|
|
||||||
return f.getvalue()
|
|
||||||
|
|
||||||
def loads(self, string):
|
|
||||||
f = py.io.BytesIO(string)
|
|
||||||
unpickler = Unpickler(f)
|
|
||||||
unpickler.memo = self._unpicklememo
|
|
||||||
res = unpickler.load()
|
|
||||||
#print >>debug, "loaded", res
|
|
||||||
#print >>debug, "unpicklememo", self._unpicklememo
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
NO_ENDMARKER_WANTED = object()
|
|
||||||
|
|
||||||
class UnpickleError(Exception):
|
|
||||||
""" Problems while unpickling. """
|
|
||||||
def __init__(self, formatted):
|
|
||||||
self.formatted = formatted
|
|
||||||
Exception.__init__(self, formatted)
|
|
||||||
def __str__(self):
|
|
||||||
return self.formatted
|
|
||||||
|
|
||||||
class PickleChannel(object):
|
|
||||||
""" PickleChannels wrap execnet channels
|
|
||||||
and allow to send/receive by using
|
|
||||||
"immutable pickling".
|
|
||||||
"""
|
|
||||||
_unpicklingerror = None
|
|
||||||
def __init__(self, channel):
|
|
||||||
self._channel = channel
|
|
||||||
# we use the fact that each side of a
|
|
||||||
# gateway connection counts with uneven
|
|
||||||
# or even numbers depending on which
|
|
||||||
# side it is (for the purpose of creating
|
|
||||||
# unique ids - which is what we need it here for)
|
|
||||||
uneven = channel.gateway._channelfactory.count % 2
|
|
||||||
self._ipickle = ImmutablePickler(uneven=uneven)
|
|
||||||
self.RemoteError = channel.RemoteError
|
|
||||||
|
|
||||||
def send(self, obj):
|
|
||||||
pickled_obj = self._ipickle.dumps(obj)
|
|
||||||
self._channel.send(pickled_obj)
|
|
||||||
|
|
||||||
def receive(self):
|
|
||||||
pickled_obj = self._channel.receive()
|
|
||||||
return self._unpickle(pickled_obj)
|
|
||||||
|
|
||||||
def _unpickle(self, pickled_obj):
|
|
||||||
if isinstance(pickled_obj, self._channel.__class__):
|
|
||||||
return pickled_obj
|
|
||||||
return self._ipickle.loads(pickled_obj)
|
|
||||||
|
|
||||||
def _getremoteerror(self):
|
|
||||||
return self._unpicklingerror or self._channel._getremoteerror()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
return self._channel.close()
|
|
||||||
|
|
||||||
def isclosed(self):
|
|
||||||
return self._channel.isclosed()
|
|
||||||
|
|
||||||
def waitclose(self, timeout=None):
|
|
||||||
return self._channel.waitclose(timeout=timeout)
|
|
||||||
|
|
||||||
def setcallback(self, callback, endmarker=NO_ENDMARKER_WANTED):
|
|
||||||
if endmarker is NO_ENDMARKER_WANTED:
|
|
||||||
def unpickle_callback(pickled_obj):
|
|
||||||
obj = self._unpickle(pickled_obj)
|
|
||||||
callback(obj)
|
|
||||||
self._channel.setcallback(unpickle_callback)
|
|
||||||
return
|
|
||||||
uniqueendmarker = object()
|
|
||||||
def unpickle_callback(pickled_obj):
|
|
||||||
if pickled_obj is uniqueendmarker:
|
|
||||||
return callback(endmarker)
|
|
||||||
try:
|
|
||||||
obj = self._unpickle(pickled_obj)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
raise
|
|
||||||
except:
|
|
||||||
excinfo = py.code.ExceptionInfo()
|
|
||||||
formatted = str(excinfo.getrepr(showlocals=True,funcargs=True))
|
|
||||||
self._unpicklingerror = UnpickleError(formatted)
|
|
||||||
callback(endmarker)
|
|
||||||
else:
|
|
||||||
callback(obj)
|
|
||||||
self._channel.setcallback(unpickle_callback, uniqueendmarker)
|
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
def pytest_gwmanage_newgateway(gateway, platinfo):
|
def pytest_xdist_setupnodes(config, specs):
|
||||||
|
""" called before any remote node is set up. """
|
||||||
|
|
||||||
|
def pytest_xdist_newgateway(gateway):
|
||||||
""" called on new raw gateway creation. """
|
""" called on new raw gateway creation. """
|
||||||
|
|
||||||
def pytest_gwmanage_rsyncstart(source, gateways):
|
def pytest_xdist_rsyncstart(source, gateways):
|
||||||
""" called before rsyncing a directory to remote gateways takes place. """
|
""" called before rsyncing a directory to remote gateways takes place. """
|
||||||
|
|
||||||
def pytest_gwmanage_rsyncfinish(source, gateways):
|
def pytest_xdist_rsyncfinish(source, gateways):
|
||||||
""" called after rsyncing a directory to remote gateways takes place. """
|
""" called after rsyncing a directory to remote gateways takes place. """
|
||||||
|
|
||||||
def pytest_configure_node(node):
|
def pytest_configure_node(node):
|
||||||
@@ -16,10 +19,3 @@ def pytest_testnodeready(node):
|
|||||||
|
|
||||||
def pytest_testnodedown(node, error):
|
def pytest_testnodedown(node, error):
|
||||||
""" Test Node is down. """
|
""" Test Node is down. """
|
||||||
|
|
||||||
def pytest_rescheduleitems(items):
|
|
||||||
""" reschedule Items from a node that went down. """
|
|
||||||
|
|
||||||
def pytest_looponfailinfo(failreports, rootdirs):
|
|
||||||
""" info for repeating failing tests. """
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import py
|
|
||||||
import sys, os
|
|
||||||
import xdist
|
|
||||||
from xdist.txnode import TXNode
|
|
||||||
from xdist.gwmanage import GatewayManager
|
|
||||||
import execnet
|
|
||||||
|
|
||||||
class NodeManager(object):
|
|
||||||
def __init__(self, config, specs=None):
|
|
||||||
self.config = config
|
|
||||||
if specs is None:
|
|
||||||
specs = self._getxspecs()
|
|
||||||
self.roots = self._getrsyncdirs()
|
|
||||||
self.gwmanager = GatewayManager(specs, config.hook)
|
|
||||||
self._nodesready = py.std.threading.Event()
|
|
||||||
|
|
||||||
def trace(self, msg):
|
|
||||||
self.config.hook.pytest_trace(category="nodemanage", msg=msg)
|
|
||||||
|
|
||||||
def config_getignores(self):
|
|
||||||
return self.config.getconftest_pathlist("rsyncignore")
|
|
||||||
|
|
||||||
def rsync_roots(self):
|
|
||||||
""" make sure that all remote gateways
|
|
||||||
have the same set of roots in their
|
|
||||||
current directory.
|
|
||||||
"""
|
|
||||||
self.makegateways()
|
|
||||||
options = {
|
|
||||||
'ignores': self.config_getignores(),
|
|
||||||
'verbose': self.config.option.verbose,
|
|
||||||
}
|
|
||||||
if self.roots:
|
|
||||||
# send each rsync root
|
|
||||||
for root in self.roots:
|
|
||||||
self.gwmanager.rsync(root, **options)
|
|
||||||
else:
|
|
||||||
XXX # do we want to care for situations without explicit rsyncdirs?
|
|
||||||
# we transfer our topdir as the root
|
|
||||||
self.gwmanager.rsync(self.config.topdir, **options)
|
|
||||||
# and cd into it
|
|
||||||
self.gwmanager.multi_chdir(self.config.topdir.basename, inplacelocal=False)
|
|
||||||
|
|
||||||
def makegateways(self):
|
|
||||||
# we change to the topdir sot that
|
|
||||||
# PopenGateways will have their cwd
|
|
||||||
# such that unpickling configs will
|
|
||||||
# pick it up as the right topdir
|
|
||||||
# (for other gateways this chdir is irrelevant)
|
|
||||||
self.trace("making gateways")
|
|
||||||
old = self.config.topdir.chdir()
|
|
||||||
try:
|
|
||||||
self.gwmanager.makegateways()
|
|
||||||
finally:
|
|
||||||
old.chdir()
|
|
||||||
|
|
||||||
def setup_nodes(self, putevent):
|
|
||||||
self.rsync_roots()
|
|
||||||
self.trace("setting up nodes")
|
|
||||||
for gateway in self.gwmanager.group:
|
|
||||||
node = TXNode(self, gateway, self.config, putevent)
|
|
||||||
gateway.node = node # to keep node alive
|
|
||||||
self.trace("started node %r" % node)
|
|
||||||
|
|
||||||
def teardown_nodes(self):
|
|
||||||
self.gwmanager.exit()
|
|
||||||
|
|
||||||
def _getxspecs(self):
|
|
||||||
config = self.config
|
|
||||||
xspeclist = []
|
|
||||||
for xspec in config.getvalue("tx"):
|
|
||||||
i = xspec.find("*")
|
|
||||||
try:
|
|
||||||
num = int(xspec[:i])
|
|
||||||
except ValueError:
|
|
||||||
xspeclist.append(xspec)
|
|
||||||
else:
|
|
||||||
xspeclist.extend([xspec[i+1:]] * num)
|
|
||||||
if not xspeclist:
|
|
||||||
raise config.Error(
|
|
||||||
"MISSING test execution (tx) nodes: please specify --tx")
|
|
||||||
return [execnet.XSpec(x) for x in xspeclist]
|
|
||||||
|
|
||||||
def _getrsyncdirs(self):
|
|
||||||
config = self.config
|
|
||||||
candidates = [py._pydir]
|
|
||||||
candidates += [py.path.local(xdist.__file__).dirpath()]
|
|
||||||
candidates += config.option.rsyncdir
|
|
||||||
conftestroots = config.getconftest_pathlist("rsyncdirs")
|
|
||||||
if conftestroots:
|
|
||||||
candidates.extend(conftestroots)
|
|
||||||
roots = []
|
|
||||||
for root in candidates:
|
|
||||||
root = py.path.local(root).realpath()
|
|
||||||
if not root.check():
|
|
||||||
raise config.Error("rsyncdir doesn't exist: %r" %(root,))
|
|
||||||
if root not in roots:
|
|
||||||
roots.append(root)
|
|
||||||
return roots
|
|
||||||
201
xdist/plugin.py
201
xdist/plugin.py
@@ -1,147 +1,5 @@
|
|||||||
"""loop on failing tests, distribute test runs to CPUs and hosts.
|
|
||||||
|
|
||||||
The `pytest-xdist`_ plugin extends py.test with some unique
|
|
||||||
test execution modes:
|
|
||||||
|
|
||||||
* Looponfail: run your tests repeatedly in a subprocess. After each run py.test
|
|
||||||
waits until a file in your project changes and then re-runs the previously
|
|
||||||
failing tests. This is repeated until all tests pass after which again
|
|
||||||
a full run is performed.
|
|
||||||
|
|
||||||
* Load-balancing: if you have multiple CPUs or hosts you can use
|
|
||||||
those for a combined test run. This allows to speed up
|
|
||||||
development or to use special resources of remote machines.
|
|
||||||
|
|
||||||
* Multi-Platform coverage: you can specify different Python interpreters
|
|
||||||
or different platforms and run tests in parallel on all of them.
|
|
||||||
|
|
||||||
Before running tests remotely, ``py.test`` efficiently synchronizes your
|
|
||||||
program source code to the remote place. All test results
|
|
||||||
are reported back and displayed to your local test session.
|
|
||||||
You may specify different Python versions and interpreters.
|
|
||||||
|
|
||||||
.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist
|
|
||||||
|
|
||||||
Usage examples
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
Speed up test runs by sending tests to multiple CPUs
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
To send tests to multiple CPUs, type::
|
|
||||||
|
|
||||||
py.test -n NUM
|
|
||||||
|
|
||||||
Especially for longer running tests or tests requiring
|
|
||||||
a lot of IO this can lead to considerable speed ups.
|
|
||||||
|
|
||||||
|
|
||||||
Running tests in a Python subprocess
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
To instantiate a python2.4 sub process and send tests to it, you may type::
|
|
||||||
|
|
||||||
py.test -d --tx popen//python=python2.4
|
|
||||||
|
|
||||||
This will start a subprocess which is run with the "python2.4"
|
|
||||||
Python interpreter, found in your system binary lookup path.
|
|
||||||
|
|
||||||
If you prefix the --tx option value like this::
|
|
||||||
|
|
||||||
--tx 3*popen//python=python2.4
|
|
||||||
|
|
||||||
then three subprocesses would be created and tests
|
|
||||||
will be load-balanced across these three processes.
|
|
||||||
|
|
||||||
|
|
||||||
Sending tests to remote SSH accounts
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
Suppose you have a package ``mypkg`` which contains some
|
|
||||||
tests that you can successfully run locally. And you
|
|
||||||
have a ssh-reachable machine ``myhost``. Then
|
|
||||||
you can ad-hoc distribute your tests by typing::
|
|
||||||
|
|
||||||
py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
|
|
||||||
|
|
||||||
This will synchronize your ``mypkg`` package directory
|
|
||||||
to an remote ssh account and then locally collect tests
|
|
||||||
and send them to remote places for execution.
|
|
||||||
|
|
||||||
You can specify multiple ``--rsyncdir`` directories
|
|
||||||
to be sent to the remote side.
|
|
||||||
|
|
||||||
**NOTE:** For py.test to collect and send tests correctly
|
|
||||||
you not only need to make sure all code and tests
|
|
||||||
directories are rsynced, but that any test (sub) directory
|
|
||||||
also has an ``__init__.py`` file because internally
|
|
||||||
py.test references tests as a fully qualified python
|
|
||||||
module path. **You will otherwise get strange errors**
|
|
||||||
during setup of the remote side.
|
|
||||||
|
|
||||||
Sending tests to remote Socket Servers
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
Download the single-module `socketserver.py`_ Python program
|
|
||||||
and run it like this::
|
|
||||||
|
|
||||||
python socketserver.py
|
|
||||||
|
|
||||||
It will tell you that it starts listening on the default
|
|
||||||
port. You can now on your home machine specify this
|
|
||||||
new socket host with something like this::
|
|
||||||
|
|
||||||
py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg
|
|
||||||
|
|
||||||
|
|
||||||
.. _`atonce`:
|
|
||||||
|
|
||||||
Running tests on many platforms at once
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
The basic command to run tests on multiple platforms is::
|
|
||||||
|
|
||||||
py.test --dist=each --tx=spec1 --tx=spec2
|
|
||||||
|
|
||||||
If you specify a windows host, an OSX host and a Linux
|
|
||||||
environment this command will send each tests to all
|
|
||||||
platforms - and report back failures from all platforms
|
|
||||||
at once. The specifications strings use the `xspec syntax`_.
|
|
||||||
|
|
||||||
.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec
|
|
||||||
|
|
||||||
.. _`socketserver.py`: http://codespeak.net/svn/py/dist/py/execnet/script/socketserver.py
|
|
||||||
|
|
||||||
.. _`execnet`: http://codespeak.net/execnet
|
|
||||||
|
|
||||||
Specifying test exec environments in a conftest.py
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
Instead of specifying command line options, you can
|
|
||||||
put options values in a ``conftest.py`` file like this::
|
|
||||||
|
|
||||||
pytest_option_tx = ['ssh=myhost//python=python2.5', 'popen//python=python2.5']
|
|
||||||
pytest_option_dist = True
|
|
||||||
|
|
||||||
Any commandline ``--tx`` specifictions will add to the list of available execution
|
|
||||||
environments.
|
|
||||||
|
|
||||||
Specifying "rsync" dirs in a conftest.py
|
|
||||||
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
In your ``mypkg/conftest.py`` you may specify directories to synchronise
|
|
||||||
or to exclude::
|
|
||||||
|
|
||||||
rsyncdirs = ['.', '../plugins']
|
|
||||||
rsyncignore = ['_cache']
|
|
||||||
|
|
||||||
These directory specifications are relative to the directory
|
|
||||||
where the ``conftest.py`` is found.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import py
|
import py, pytest
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("xdist", "distributed and subprocess testing")
|
group = parser.getgroup("xdist", "distributed and subprocess testing")
|
||||||
@@ -173,6 +31,13 @@ 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.")
|
||||||
|
|
||||||
|
parser.addini('rsyncdirs', 'list of (relative) paths to be rsynced for'
|
||||||
|
' remote distributed testing.', type="pathlist")
|
||||||
|
parser.addini('rsyncignore', 'list of (relative) paths to be ignored '
|
||||||
|
'for rsyncing.', type="pathlist")
|
||||||
|
parser.addini("looponfailroots", type="pathlist",
|
||||||
|
help="directories to check for changes", default=[py.path.local()])
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# distributed testing hooks
|
# distributed testing hooks
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -183,7 +48,22 @@ def pytest_addhooks(pluginmanager):
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# distributed testing initialization
|
# distributed testing initialization
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
def pytest_configure(config):
|
|
||||||
|
def pytest_cmdline_main(config):
|
||||||
|
check_options(config)
|
||||||
|
if config.getvalue("looponfail"):
|
||||||
|
from xdist.looponfail import looponfail_main
|
||||||
|
looponfail_main(config)
|
||||||
|
return 2 # looponfail only can get stop with ctrl-C anyway
|
||||||
|
|
||||||
|
def pytest_configure(config, __multicall__):
|
||||||
|
__multicall__.execute()
|
||||||
|
if config.getvalue("dist") != "no":
|
||||||
|
from xdist.dsession import DSession
|
||||||
|
session = DSession(config)
|
||||||
|
config.pluginmanager.register(session, "dsession")
|
||||||
|
|
||||||
|
def check_options(config):
|
||||||
if config.option.numprocesses:
|
if config.option.numprocesses:
|
||||||
config.option.dist = "load"
|
config.option.dist = "load"
|
||||||
config.option.tx = ['popen'] * int(config.option.numprocesses)
|
config.option.tx = ['popen'] * int(config.option.numprocesses)
|
||||||
@@ -194,14 +74,11 @@ def pytest_configure(config):
|
|||||||
usepdb = config.option.usepdb # a core option
|
usepdb = config.option.usepdb # a core option
|
||||||
if val("looponfail"):
|
if val("looponfail"):
|
||||||
if usepdb:
|
if usepdb:
|
||||||
raise config.Error("--pdb incompatible with --looponfail.")
|
raise pytest.UsageError("--pdb incompatible with --looponfail.")
|
||||||
from xdist.remote import LooponfailingSession
|
|
||||||
config.setsessionclass(LooponfailingSession)
|
|
||||||
elif val("dist") != "no":
|
elif val("dist") != "no":
|
||||||
if usepdb:
|
if usepdb:
|
||||||
raise config.Error("--pdb incompatible with distributing tests.")
|
raise pytest.UsageError("--pdb incompatible with distributing tests.")
|
||||||
from xdist.dsession import DSession
|
|
||||||
config.setsessionclass(DSession)
|
|
||||||
|
|
||||||
def pytest_runtest_protocol(item):
|
def pytest_runtest_protocol(item):
|
||||||
if item.config.getvalue("boxed"):
|
if item.config.getvalue("boxed"):
|
||||||
@@ -213,25 +90,23 @@ def pytest_runtest_protocol(item):
|
|||||||
def forked_run_report(item):
|
def forked_run_report(item):
|
||||||
# for now, we run setup/teardown in the subprocess
|
# for now, we run setup/teardown in the subprocess
|
||||||
# XXX optionally allow sharing of setup/teardown
|
# XXX optionally allow sharing of setup/teardown
|
||||||
from py._plugin.pytest_runner import runtestprotocol
|
from _pytest.runner import runtestprotocol
|
||||||
EXITSTATUS_TESTEXIT = 4
|
EXITSTATUS_TESTEXIT = 4
|
||||||
from xdist.mypickle import ImmutablePickler
|
import marshal
|
||||||
ipickle = ImmutablePickler(uneven=0)
|
from xdist.remote import serialize_report
|
||||||
ipickle.selfmemoize(item.config)
|
from xdist.slavemanage import unserialize_report
|
||||||
# XXX workaround the issue that 2.6 cannot pickle
|
|
||||||
# instances of classes defined in global conftest.py files
|
|
||||||
ipickle.selfmemoize(item)
|
|
||||||
def runforked():
|
def runforked():
|
||||||
try:
|
try:
|
||||||
reports = runtestprotocol(item, log=False)
|
reports = runtestprotocol(item, log=False)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
py.std.os._exit(EXITSTATUS_TESTEXIT)
|
py.std.os._exit(EXITSTATUS_TESTEXIT)
|
||||||
return ipickle.dumps(reports)
|
return marshal.dumps([serialize_report(x) for x in reports])
|
||||||
|
|
||||||
ff = py.process.ForkedFunc(runforked)
|
ff = py.process.ForkedFunc(runforked)
|
||||||
result = ff.waitfinish()
|
result = ff.waitfinish()
|
||||||
if result.retval is not None:
|
if result.retval is not None:
|
||||||
return ipickle.loads(result.retval)
|
report_dumps = marshal.loads(result.retval)
|
||||||
|
return [unserialize_report("testreport", x) for x in report_dumps]
|
||||||
else:
|
else:
|
||||||
if result.exitstatus == EXITSTATUS_TESTEXIT:
|
if result.exitstatus == EXITSTATUS_TESTEXIT:
|
||||||
py.test.exit("forked test item %s raised Exit" %(item,))
|
py.test.exit("forked test item %s raised Exit" %(item,))
|
||||||
@@ -241,6 +116,8 @@ def report_process_crash(item, result):
|
|||||||
path, lineno = item._getfslineno()
|
path, lineno = item._getfslineno()
|
||||||
info = "%s:%s: running the test CRASHED with signal %d" %(
|
info = "%s:%s: running the test CRASHED with signal %d" %(
|
||||||
path, lineno, result.signal)
|
path, lineno, result.signal)
|
||||||
from py._plugin.pytest_runner import ItemTestReport
|
from _pytest import runner
|
||||||
return ItemTestReport(item, excinfo=info, when="???")
|
call = runner.CallInfo(lambda: 0/0, "???")
|
||||||
|
call.excinfo = info
|
||||||
|
rep = runner.pytest_runtest_makereport(item, call)
|
||||||
|
return rep
|
||||||
|
|||||||
272
xdist/remote.py
272
xdist/remote.py
@@ -1,177 +1,135 @@
|
|||||||
"""
|
"""
|
||||||
LooponfailingSession and Helpers.
|
This module is executed in remote subprocesses and helps to
|
||||||
|
control a remote testing session and relay back information.
|
||||||
NOTE that one really has to avoid loading and depending on
|
It assumes that 'py' is importable and does not have dependencies
|
||||||
application modules within the controlling process
|
on the rest of the xdist code. This means that the xdist-plugin
|
||||||
(the one that starts repeatedly test processes)
|
needs not to be installed in remote environments.
|
||||||
otherwise changes to source code can crash
|
|
||||||
the controlling process which should never happen.
|
|
||||||
"""
|
"""
|
||||||
import py
|
|
||||||
import sys
|
|
||||||
import execnet
|
|
||||||
from py._test.session import Session
|
|
||||||
from xdist import util
|
|
||||||
|
|
||||||
class LooponfailingSession(Session):
|
import sys, os
|
||||||
def __init__(self, config):
|
|
||||||
super(LooponfailingSession, self).__init__(config=config)
|
|
||||||
self.rootdirs = [self.config.topdir] # xxx dist_rsync_roots?
|
|
||||||
self.statrecorder = util.StatRecorder(self.rootdirs)
|
|
||||||
self.remotecontrol = RemoteControl(self.config)
|
|
||||||
self.out = py.io.TerminalWriter()
|
|
||||||
|
|
||||||
def main(self, initialitems):
|
class SlaveInteractor:
|
||||||
try:
|
def __init__(self, config, channel):
|
||||||
self.loopstate = loopstate = LoopState([])
|
|
||||||
self.remotecontrol.setup()
|
|
||||||
while 1:
|
|
||||||
self.loop_once(loopstate)
|
|
||||||
if not loopstate.colitems and loopstate.wasfailing:
|
|
||||||
continue # the last failures passed, let's rerun all
|
|
||||||
self.statrecorder.waitonchange(checkinterval=2.0)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print
|
|
||||||
|
|
||||||
def loop_once(self, loopstate):
|
|
||||||
colitems = loopstate.colitems
|
|
||||||
loopstate.wasfailing = colitems and len(colitems)
|
|
||||||
loopstate.colitems = self.remotecontrol.runsession(colitems or ())
|
|
||||||
self.remotecontrol.setup()
|
|
||||||
|
|
||||||
class LoopState:
|
|
||||||
def __init__(self, colitems=None):
|
|
||||||
self.colitems = colitems
|
|
||||||
|
|
||||||
class RemoteControl(object):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.slaveid = config.slaveinput.get('slaveid', "?")
|
||||||
|
self.log = py.log.Producer("slave-%s" % self.slaveid)
|
||||||
|
if not config.option.debug:
|
||||||
|
py.log.setconsumer(self.log._keywords, None)
|
||||||
|
self.channel = channel
|
||||||
|
config.pluginmanager.register(self)
|
||||||
|
|
||||||
def trace(self, *args):
|
def sendevent(self, name, **kwargs):
|
||||||
if self.config.option.debug:
|
self.log("sending", name, kwargs)
|
||||||
msg = " ".join([str(x) for x in args])
|
self.channel.send((name, kwargs))
|
||||||
py.builtin.print_("RemoteControl:", msg)
|
|
||||||
|
|
||||||
def initgateway(self):
|
def pytest_internalerror(self, excrepr):
|
||||||
return execnet.makegateway("popen")
|
for line in str(excrepr).split("\n"):
|
||||||
|
self.log("IERROR> " + line)
|
||||||
|
|
||||||
def setup(self, out=None):
|
def pytest__teardown_final_logerror(self, report):
|
||||||
if out is None:
|
rep = serialize_report(report)
|
||||||
out = py.io.TerminalWriter()
|
self.sendevent("teardownreport", data=rep)
|
||||||
if hasattr(self, 'gateway'):
|
|
||||||
raise ValueError("already have gateway %r" % self.gateway)
|
|
||||||
self.trace("setting up slave session")
|
|
||||||
self.gateway = self.initgateway()
|
|
||||||
self.channel = channel = self.gateway.remote_exec("""
|
|
||||||
import os, sys
|
|
||||||
import py
|
|
||||||
chdir = channel.receive()
|
|
||||||
outchannel = channel.gateway.newchannel()
|
|
||||||
channel.send(outchannel)
|
|
||||||
# prune sys.path to not contain relative paths
|
|
||||||
newpaths = []
|
|
||||||
for p in sys.path:
|
|
||||||
if p:
|
|
||||||
if not os.path.isabs(p):
|
|
||||||
p = os.path.abspath(p)
|
|
||||||
newpaths.append(p)
|
|
||||||
sys.path[:] = newpaths
|
|
||||||
os.chdir(chdir) # unpickling config uses cwd as topdir
|
|
||||||
|
|
||||||
config_state = channel.receive()
|
def pytest_sessionstart(self, session):
|
||||||
fullwidth, hasmarkup = channel.receive()
|
self.session = session
|
||||||
py.test.config.__setstate__(config_state)
|
slaveinfo = getinfodict()
|
||||||
|
self.sendevent("slaveready", slaveinfo=slaveinfo)
|
||||||
|
|
||||||
import sys
|
def pytest_sessionfinish(self, __multicall__, exitstatus):
|
||||||
sys.stdout = sys.stderr = outchannel.makefile('w')
|
self.config.slaveoutput['exitstatus'] = exitstatus
|
||||||
|
res = __multicall__.execute()
|
||||||
|
self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput)
|
||||||
|
return res
|
||||||
|
|
||||||
from xdist.remote import slave_runsession
|
def pytest_collection(self, session):
|
||||||
slave_runsession(channel, py.test.config, fullwidth, hasmarkup)
|
self.sendevent("collectionstart")
|
||||||
""")
|
|
||||||
channel.send(str(self.config.topdir))
|
|
||||||
remote_outchannel = channel.receive()
|
|
||||||
def write(s):
|
|
||||||
out._file.write(s)
|
|
||||||
out._file.flush()
|
|
||||||
remote_outchannel.setcallback(write)
|
|
||||||
channel.send(self.config.__getstate__())
|
|
||||||
channel.send((out.fullwidth, out.hasmarkup))
|
|
||||||
self.trace("set up of slave session complete")
|
|
||||||
|
|
||||||
def ensure_teardown(self):
|
def pytest_runtestloop(self, session):
|
||||||
if hasattr(self, 'channel'):
|
self.log("entering main loop")
|
||||||
if not self.channel.isclosed():
|
while 1:
|
||||||
self.trace("closing", self.channel)
|
name, kwargs = self.channel.receive()
|
||||||
self.channel.close()
|
self.log("received command %s(**%s)" % (name, kwargs))
|
||||||
del self.channel
|
if name == "runtests":
|
||||||
if hasattr(self, 'gateway'):
|
ids = kwargs['ids']
|
||||||
self.trace("exiting", self.gateway)
|
for nodeid in ids:
|
||||||
self.gateway.exit()
|
item = self._id2item[nodeid]
|
||||||
del self.gateway
|
self.config.hook.pytest_runtest_protocol(item=item)
|
||||||
|
elif name == "runtests_all":
|
||||||
|
for item in session.items:
|
||||||
|
self.config.hook.pytest_runtest_protocol(item=item)
|
||||||
|
elif name == "shutdown":
|
||||||
|
break
|
||||||
|
return True
|
||||||
|
|
||||||
def runsession(self, colitems=()):
|
def pytest_collection_finish(self, session):
|
||||||
try:
|
self._id2item = {}
|
||||||
self.trace("sending", colitems)
|
ids = []
|
||||||
trails = colitems
|
for item in session.items:
|
||||||
self.channel.send(trails)
|
self._id2item[item.nodeid] = item
|
||||||
try:
|
ids.append(item.nodeid)
|
||||||
return self.channel.receive()
|
self.sendevent("collectionfinish",
|
||||||
except self.channel.RemoteError:
|
topdir=str(session.fspath),
|
||||||
e = sys.exc_info()[1]
|
ids=ids)
|
||||||
self.trace("ERROR", e)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.ensure_teardown()
|
|
||||||
|
|
||||||
def slave_runsession(channel, config, fullwidth, hasmarkup):
|
#def pytest_runtest_logstart(self, nodeid, location, fspath):
|
||||||
""" we run this on the other side. """
|
# self.sendevent("logstart", nodeid=nodeid, location=location)
|
||||||
if config.option.debug:
|
|
||||||
def DEBUG(*args):
|
def pytest_runtest_logreport(self, report):
|
||||||
print(" ".join(map(str, args)))
|
data = serialize_report(report)
|
||||||
|
self.sendevent("testreport", data=data)
|
||||||
|
|
||||||
|
def pytest_collectreport(self, report):
|
||||||
|
data = serialize_report(report)
|
||||||
|
self.sendevent("collectreport", data=data)
|
||||||
|
|
||||||
|
def serialize_report(rep):
|
||||||
|
import py
|
||||||
|
d = rep.__dict__.copy()
|
||||||
|
if hasattr(rep.longrepr, 'toterminal'):
|
||||||
|
d['longrepr'] = str(rep.longrepr)
|
||||||
else:
|
else:
|
||||||
def DEBUG(*args): pass
|
d['longrepr'] = rep.longrepr
|
||||||
|
for name in d:
|
||||||
|
if isinstance(d[name], py.path.local):
|
||||||
|
d[name] = str(d[name])
|
||||||
|
elif name == "result":
|
||||||
|
d[name] = None # for now
|
||||||
|
return d
|
||||||
|
|
||||||
DEBUG("SLAVE: received configuration, using topdir:", config.topdir)
|
def getinfodict():
|
||||||
#config.option.session = None
|
import platform
|
||||||
|
return dict(
|
||||||
|
version = sys.version,
|
||||||
|
version_info = tuple(sys.version_info),
|
||||||
|
sysplatform = sys.platform,
|
||||||
|
platform = platform.platform(),
|
||||||
|
executable = sys.executable,
|
||||||
|
cwd = os.getcwd(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def remote_initconfig(option_dict, args):
|
||||||
|
from _pytest.config import Config
|
||||||
|
option_dict['plugins'].append("no:terminal")
|
||||||
|
config = Config.fromdictargs(option_dict, args)
|
||||||
config.option.looponfail = False
|
config.option.looponfail = False
|
||||||
config.option.usepdb = False
|
config.option.usepdb = False
|
||||||
try:
|
config.option.dist = "no"
|
||||||
trails = channel.receive()
|
config.option.distload = False
|
||||||
except KeyboardInterrupt:
|
config.option.numprocesses = None
|
||||||
return # in the slave we can't do much about this
|
config.args = args
|
||||||
config.pluginmanager.do_configure(config)
|
return config
|
||||||
DEBUG("SLAVE: initsession()")
|
|
||||||
session = config.initsession()
|
|
||||||
# XXX configure the reporter object's terminal writer more directly
|
|
||||||
# XXX and write a test for this remote-terminal setting logic
|
|
||||||
config.pytest_terminal_hasmarkup = hasmarkup
|
|
||||||
config.pytest_terminal_fullwidth = fullwidth
|
|
||||||
if trails:
|
|
||||||
colitems = []
|
|
||||||
for trail in trails:
|
|
||||||
try:
|
|
||||||
colitem = config._rootcol.fromtrail(trail)
|
|
||||||
except ValueError:
|
|
||||||
#XXX send info for "test disappeared" or so
|
|
||||||
continue
|
|
||||||
colitems.append(colitem)
|
|
||||||
else:
|
|
||||||
colitems = config.getinitialnodes()
|
|
||||||
session.shouldclose = channel.isclosed
|
|
||||||
|
|
||||||
class Failures(list):
|
|
||||||
def pytest_runtest_logreport(self, report):
|
|
||||||
if report.failed:
|
|
||||||
self.append(report)
|
|
||||||
pytest_collectreport = pytest_runtest_logreport
|
|
||||||
|
|
||||||
failreports = Failures()
|
if __name__ == '__channelexec__':
|
||||||
session.pluginmanager.register(failreports)
|
slaveinput,args,option_dict = channel.receive()
|
||||||
|
importpath = os.getcwd()
|
||||||
DEBUG("SLAVE: starting session.main()")
|
sys.path.insert(0, importpath) # XXX only for remote situations
|
||||||
session.main(colitems)
|
os.environ['PYTHONPATH'] = (importpath + os.pathsep +
|
||||||
session.config.hook.pytest_looponfailinfo(
|
os.environ.get('PYTHONPATH', ''))
|
||||||
failreports=list(failreports),
|
#os.environ['PYTHONPATH'] = importpath
|
||||||
rootdirs=[config.topdir])
|
import py
|
||||||
rootcol = session.config._rootcol
|
config = remote_initconfig(option_dict, args)
|
||||||
channel.send([rootcol.totrail(rep.getnode()) for rep in failreports])
|
config.slaveinput = slaveinput
|
||||||
|
config.slaveoutput = {}
|
||||||
|
interactor = SlaveInteractor(config, channel)
|
||||||
|
config.hook.pytest_cmdline_main(config=config)
|
||||||
|
|||||||
302
xdist/slavemanage.py
Normal file
302
xdist/slavemanage.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import py, pytest
|
||||||
|
import sys, os
|
||||||
|
import execnet
|
||||||
|
import xdist.remote
|
||||||
|
|
||||||
|
from _pytest import runner # XXX load dynamically
|
||||||
|
|
||||||
|
class NodeManager(object):
|
||||||
|
EXIT_TIMEOUT = 10
|
||||||
|
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
|
||||||
|
self.config = config
|
||||||
|
self._nodesready = py.std.threading.Event()
|
||||||
|
self.trace = self.config.trace.get("nodemanager")
|
||||||
|
self.group = execnet.Group()
|
||||||
|
if specs is None:
|
||||||
|
specs = self._getxspecs()
|
||||||
|
self.specs = []
|
||||||
|
for spec in specs:
|
||||||
|
if not isinstance(spec, execnet.XSpec):
|
||||||
|
spec = execnet.XSpec(spec)
|
||||||
|
if not spec.chdir and not spec.popen:
|
||||||
|
spec.chdir = defaultchdir
|
||||||
|
self.group.allocate_id(spec)
|
||||||
|
self.specs.append(spec)
|
||||||
|
self.roots = self._getrsyncdirs()
|
||||||
|
|
||||||
|
def rsync_roots(self):
|
||||||
|
""" make sure that all remote gateways
|
||||||
|
have the same set of roots in their
|
||||||
|
current directory.
|
||||||
|
"""
|
||||||
|
options = {
|
||||||
|
'ignores': self.config.getini("rsyncignore"),
|
||||||
|
'verbose': self.config.option.verbose,
|
||||||
|
}
|
||||||
|
if self.roots:
|
||||||
|
# send each rsync root
|
||||||
|
for root in self.roots:
|
||||||
|
self.rsync(root, **options)
|
||||||
|
|
||||||
|
def makegateways(self):
|
||||||
|
assert not list(self.group)
|
||||||
|
self.config.hook.pytest_xdist_setupnodes(config=self.config,
|
||||||
|
specs=self.specs)
|
||||||
|
for spec in self.specs:
|
||||||
|
gw = self.group.makegateway(spec)
|
||||||
|
self.config.hook.pytest_xdist_newgateway(gateway=gw)
|
||||||
|
|
||||||
|
def setup_nodes(self, putevent):
|
||||||
|
self.makegateways()
|
||||||
|
self.rsync_roots()
|
||||||
|
self.trace("setting up nodes")
|
||||||
|
for gateway in self.group:
|
||||||
|
node = SlaveController(self, gateway, self.config, putevent)
|
||||||
|
gateway.node = node # to keep node alive
|
||||||
|
node.setup()
|
||||||
|
self.trace("started node %r" % node)
|
||||||
|
|
||||||
|
def teardown_nodes(self):
|
||||||
|
self.group.terminate(self.EXIT_TIMEOUT)
|
||||||
|
|
||||||
|
def _getxspecs(self):
|
||||||
|
xspeclist = []
|
||||||
|
for xspec in self.config.getvalue("tx"):
|
||||||
|
i = xspec.find("*")
|
||||||
|
try:
|
||||||
|
num = int(xspec[:i])
|
||||||
|
except ValueError:
|
||||||
|
xspeclist.append(xspec)
|
||||||
|
else:
|
||||||
|
xspeclist.extend([xspec[i+1:]] * num)
|
||||||
|
if not xspeclist:
|
||||||
|
raise pytest.UsageError(
|
||||||
|
"MISSING test execution (tx) nodes: please specify --tx")
|
||||||
|
return [execnet.XSpec(x) for x in xspeclist]
|
||||||
|
|
||||||
|
def _getrsyncdirs(self):
|
||||||
|
for spec in self.specs:
|
||||||
|
if not spec.popen or spec.chdir:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
import pytest, _pytest
|
||||||
|
pytestpath = pytest.__file__.rstrip("co")
|
||||||
|
pytestdir = py.path.local(_pytest.__file__).dirpath()
|
||||||
|
config = self.config
|
||||||
|
candidates = [py._pydir,pytestpath,pytestdir]
|
||||||
|
candidates += config.option.rsyncdir
|
||||||
|
rsyncroots = config.getini("rsyncdirs")
|
||||||
|
if rsyncroots:
|
||||||
|
candidates.extend(rsyncroots)
|
||||||
|
roots = []
|
||||||
|
for root in candidates:
|
||||||
|
root = py.path.local(root).realpath()
|
||||||
|
if not root.check():
|
||||||
|
raise pytest.UsageError("rsyncdir doesn't exist: %r" %(root,))
|
||||||
|
if root not in roots:
|
||||||
|
roots.append(root)
|
||||||
|
return roots
|
||||||
|
|
||||||
|
def rsync(self, source, notify=None, verbose=False, ignores=None):
|
||||||
|
""" perform rsync to all remote hosts.
|
||||||
|
"""
|
||||||
|
rsync = HostRSync(source, verbose=verbose, ignores=ignores)
|
||||||
|
seen = py.builtin.set()
|
||||||
|
gateways = []
|
||||||
|
for gateway in self.group:
|
||||||
|
spec = gateway.spec
|
||||||
|
if spec.popen and not spec.chdir:
|
||||||
|
# XXX this assumes that sources are python-packages
|
||||||
|
# and that adding the basedir does not hurt
|
||||||
|
gateway.remote_exec("""
|
||||||
|
import sys ; sys.path.insert(0, %r)
|
||||||
|
""" % os.path.dirname(str(source))).waitclose()
|
||||||
|
continue
|
||||||
|
if spec not in seen:
|
||||||
|
def finished():
|
||||||
|
if notify:
|
||||||
|
notify("rsyncrootready", spec, source)
|
||||||
|
rsync.add_target_host(gateway, finished=finished)
|
||||||
|
seen.add(spec)
|
||||||
|
gateways.append(gateway)
|
||||||
|
if seen:
|
||||||
|
self.config.hook.pytest_xdist_rsyncstart(
|
||||||
|
source=source,
|
||||||
|
gateways=gateways,
|
||||||
|
)
|
||||||
|
rsync.send()
|
||||||
|
self.config.hook.pytest_xdist_rsyncfinish(
|
||||||
|
source=source,
|
||||||
|
gateways=gateways,
|
||||||
|
)
|
||||||
|
|
||||||
|
class HostRSync(execnet.RSync):
|
||||||
|
""" RSyncer that filters out common files
|
||||||
|
"""
|
||||||
|
def __init__(self, sourcedir, *args, **kwargs):
|
||||||
|
self._synced = {}
|
||||||
|
ignores= None
|
||||||
|
if 'ignores' in kwargs:
|
||||||
|
ignores = kwargs.pop('ignores')
|
||||||
|
self._ignores = ignores or []
|
||||||
|
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
|
||||||
|
|
||||||
|
def filter(self, path):
|
||||||
|
path = py.path.local(path)
|
||||||
|
if not path.ext in ('.pyc', '.pyo'):
|
||||||
|
if not path.basename.endswith('~'):
|
||||||
|
if path.check(dotfile=0):
|
||||||
|
for x in self._ignores:
|
||||||
|
if path == x:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_target_host(self, gateway, finished=None):
|
||||||
|
remotepath = os.path.basename(self._sourcedir)
|
||||||
|
super(HostRSync, self).add_target(gateway, remotepath,
|
||||||
|
finishedcallback=finished,
|
||||||
|
delete=True,)
|
||||||
|
|
||||||
|
def _report_send_file(self, gateway, modified_rel_path):
|
||||||
|
if self._verbose:
|
||||||
|
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
|
||||||
|
remotepath = gateway.spec.chdir
|
||||||
|
py.builtin.print_('%s:%s <= %s' %
|
||||||
|
(gateway.spec, remotepath, path))
|
||||||
|
|
||||||
|
|
||||||
|
def make_reltoroot(roots, args):
|
||||||
|
# XXX introduce/use public API for splitting py.test args
|
||||||
|
splitcode = "::"
|
||||||
|
l = []
|
||||||
|
for arg in args:
|
||||||
|
parts = arg.split(splitcode)
|
||||||
|
fspath = py.path.local(parts[0])
|
||||||
|
for root in roots:
|
||||||
|
x = fspath.relto(root)
|
||||||
|
if x or fspath == root:
|
||||||
|
parts[0] = root.basename + "/" + x
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("arg %s not relative to an rsync root" % (arg,))
|
||||||
|
l.append(splitcode.join(parts))
|
||||||
|
return l
|
||||||
|
|
||||||
|
class SlaveController(object):
|
||||||
|
ENDMARK = -1
|
||||||
|
|
||||||
|
def __init__(self, nodemanager, gateway, config, putevent):
|
||||||
|
self.nodemanager = nodemanager
|
||||||
|
self.putevent = putevent
|
||||||
|
self.gateway = gateway
|
||||||
|
self.config = config
|
||||||
|
self.slaveinput = {'slaveid': gateway.id}
|
||||||
|
self._down = False
|
||||||
|
self.log = py.log.Producer("slavectl-%s" % gateway.id)
|
||||||
|
if not self.config.option.debug:
|
||||||
|
py.log.setconsumer(self.log._keywords, None)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s %s>" %(self.__class__.__name__, self.gateway.id,)
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.log("setting up slave session")
|
||||||
|
spec = self.gateway.spec
|
||||||
|
args = self.config.args
|
||||||
|
if not spec.popen or spec.chdir:
|
||||||
|
args = make_reltoroot(self.nodemanager.roots, args)
|
||||||
|
option_dict = vars(self.config.option)
|
||||||
|
if spec.popen:
|
||||||
|
name = "popen-%s" % self.gateway.id
|
||||||
|
basetemp = self.config._tmpdirhandler.getbasetemp()
|
||||||
|
option_dict['basetemp'] = str(basetemp.join(name))
|
||||||
|
self.config.hook.pytest_configure_node(node=self)
|
||||||
|
self.channel = self.gateway.remote_exec(xdist.remote)
|
||||||
|
self.channel.send((self.slaveinput, args, option_dict))
|
||||||
|
if self.putevent:
|
||||||
|
self.channel.setcallback(self.process_from_remote,
|
||||||
|
endmarker=self.ENDMARK)
|
||||||
|
|
||||||
|
def ensure_teardown(self):
|
||||||
|
if hasattr(self, 'channel'):
|
||||||
|
if not self.channel.isclosed():
|
||||||
|
self.log("closing", self.channel)
|
||||||
|
self.channel.close()
|
||||||
|
#del self.channel
|
||||||
|
if hasattr(self, 'gateway'):
|
||||||
|
self.log("exiting", self.gateway)
|
||||||
|
self.gateway.exit()
|
||||||
|
#del self.gateway
|
||||||
|
|
||||||
|
def send_runtest(self, nodeid):
|
||||||
|
self.sendcommand("runtests", ids=[nodeid])
|
||||||
|
|
||||||
|
def send_runtest_all(self):
|
||||||
|
self.sendcommand("runtests_all",)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if not self._down and not self.channel.isclosed():
|
||||||
|
self.sendcommand("shutdown")
|
||||||
|
|
||||||
|
def sendcommand(self, name, **kwargs):
|
||||||
|
""" send a named parametrized command to the other side. """
|
||||||
|
self.log("sending command %s(**%s)" % (name, kwargs))
|
||||||
|
self.channel.send((name, kwargs))
|
||||||
|
|
||||||
|
def notify_inproc(self, eventname, **kwargs):
|
||||||
|
self.log("queuing %s(**%s)" % (eventname, kwargs))
|
||||||
|
self.putevent((eventname, kwargs))
|
||||||
|
|
||||||
|
def process_from_remote(self, eventcall):
|
||||||
|
""" this gets called for each object we receive from
|
||||||
|
the other side and if the channel closes.
|
||||||
|
|
||||||
|
Note that channel callbacks run in the receiver
|
||||||
|
thread of execnet gateways - we need to
|
||||||
|
avoid raising exceptions or doing heavy work.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if eventcall == self.ENDMARK:
|
||||||
|
err = self.channel._getremoteerror()
|
||||||
|
if not self._down:
|
||||||
|
if not err or isinstance(err, EOFError):
|
||||||
|
err = "Not properly terminated" # lost connection?
|
||||||
|
self.notify_inproc("errordown", node=self, error=err)
|
||||||
|
self._down = True
|
||||||
|
return
|
||||||
|
eventname, kwargs = eventcall
|
||||||
|
if eventname in ("collectionstart"):
|
||||||
|
self.log("ignoring %s(%s)" %(eventname, kwargs))
|
||||||
|
elif eventname == "slaveready":
|
||||||
|
self.notify_inproc(eventname, node=self, **kwargs)
|
||||||
|
elif eventname == "slavefinished":
|
||||||
|
self._down = True
|
||||||
|
self.slaveoutput = kwargs['slaveoutput']
|
||||||
|
self.notify_inproc("slavefinished", node=self)
|
||||||
|
#elif eventname == "logstart":
|
||||||
|
# self.notify_inproc(eventname, node=self, **kwargs)
|
||||||
|
elif eventname in ("testreport", "collectreport", "teardownreport"):
|
||||||
|
rep = unserialize_report(eventname, kwargs['data'])
|
||||||
|
self.notify_inproc(eventname, node=self, rep=rep)
|
||||||
|
elif eventname == "collectionfinish":
|
||||||
|
self.notify_inproc(eventname, node=self, ids=kwargs['ids'])
|
||||||
|
else:
|
||||||
|
raise ValueError("unknown event: %s" %(eventname,))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# should not land in receiver-thread
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
excinfo = py.code.ExceptionInfo()
|
||||||
|
py.builtin.print_("!" * 20, excinfo)
|
||||||
|
self.config.pluginmanager.notify_exception(excinfo)
|
||||||
|
|
||||||
|
def unserialize_report(name, reportdict):
|
||||||
|
d = reportdict
|
||||||
|
if name == "testreport":
|
||||||
|
return runner.TestReport(**d)
|
||||||
|
elif name == "collectreport":
|
||||||
|
return runner.CollectReport(**d)
|
||||||
|
elif name == "teardownreport":
|
||||||
|
return runner.TeardownErrorReport(**d)
|
||||||
174
xdist/txnode.py
174
xdist/txnode.py
@@ -1,174 +0,0 @@
|
|||||||
"""
|
|
||||||
Manage setup, running and local representation of remote nodes/processes.
|
|
||||||
"""
|
|
||||||
import py
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
from py._test.session import Session
|
|
||||||
|
|
||||||
class TXNode(object):
|
|
||||||
""" Represents a Test Execution environment in the controlling process.
|
|
||||||
- sets up a slave node through an execnet gateway
|
|
||||||
- manages sending of test-items and receival of results and events
|
|
||||||
- creates events when the remote side crashes
|
|
||||||
"""
|
|
||||||
ENDMARK = -1
|
|
||||||
|
|
||||||
def __init__(self, nodemanager, gateway, config, putevent):
|
|
||||||
self.nodemanager = nodemanager
|
|
||||||
self.config = config
|
|
||||||
self.putevent = putevent
|
|
||||||
self.gateway = gateway
|
|
||||||
self.slaveinput = {}
|
|
||||||
self.channel = install_slave(self)
|
|
||||||
self.channel.setcallback(self.callback, endmarker=self.ENDMARK)
|
|
||||||
self._down = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
id = self.gateway.id
|
|
||||||
status = self._down and 'true' or 'false'
|
|
||||||
return "<TXNode %r down=%s>" %(id, status)
|
|
||||||
|
|
||||||
def notify(self, eventname, *args, **kwargs):
|
|
||||||
assert not args
|
|
||||||
self.putevent((eventname, args, kwargs))
|
|
||||||
|
|
||||||
def callback(self, eventcall):
|
|
||||||
""" this gets called for each object we receive from
|
|
||||||
the other side and if the channel closes.
|
|
||||||
|
|
||||||
Note that channel callbacks run in the receiver
|
|
||||||
thread of execnet gateways - we need to
|
|
||||||
avoid raising exceptions or doing heavy work.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if eventcall == self.ENDMARK:
|
|
||||||
err = self.channel._getremoteerror()
|
|
||||||
if not self._down:
|
|
||||||
if not err or isinstance(err, EOFError):
|
|
||||||
err = "Not properly terminated" # lost connection?
|
|
||||||
self.notify("pytest_testnodedown", node=self, error=err)
|
|
||||||
self._down = True
|
|
||||||
return
|
|
||||||
eventname, args, kwargs = eventcall
|
|
||||||
if eventname == "slaveready":
|
|
||||||
self.notify("pytest_testnodeready", node=self)
|
|
||||||
elif eventname == "slavefinished":
|
|
||||||
self._down = True
|
|
||||||
self.slaveoutput = kwargs['slaveoutput']
|
|
||||||
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
|
|
||||||
self.notify(eventname, **kwargs)
|
|
||||||
else:
|
|
||||||
self.notify(eventname, **kwargs)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# should not land in receiver-thread
|
|
||||||
raise
|
|
||||||
except:
|
|
||||||
excinfo = py.code.ExceptionInfo()
|
|
||||||
py.builtin.print_("!" * 20, excinfo)
|
|
||||||
self.config.pluginmanager.notify_exception(excinfo)
|
|
||||||
|
|
||||||
def send(self, item):
|
|
||||||
assert item is not None
|
|
||||||
self.channel.send(item)
|
|
||||||
|
|
||||||
def sendlist(self, itemlist):
|
|
||||||
self.channel.send(itemlist)
|
|
||||||
|
|
||||||
def shutdown(self, kill=False):
|
|
||||||
if kill:
|
|
||||||
self.gateway.exit()
|
|
||||||
else:
|
|
||||||
self.channel.send(None)
|
|
||||||
|
|
||||||
# configuring and setting up slave node
|
|
||||||
def install_slave(node):
|
|
||||||
channel = node.gateway.remote_exec(source="""
|
|
||||||
import os, sys
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
from xdist.mypickle import PickleChannel
|
|
||||||
from xdist.txnode import SlaveSession
|
|
||||||
channel.send("basicimport")
|
|
||||||
channel = PickleChannel(channel)
|
|
||||||
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)
|
|
||||||
basetemp = None
|
|
||||||
config = node.config
|
|
||||||
config.hook.pytest_configure_node(node=node)
|
|
||||||
if node.gateway.spec.popen:
|
|
||||||
popenbase = config.ensuretemp("popen")
|
|
||||||
basetemp = py.path.local.make_numbered_dir(prefix="slave-",
|
|
||||||
keep=0, rootdir=popenbase)
|
|
||||||
basetemp = str(basetemp)
|
|
||||||
channel.send((config, node.slaveinput, basetemp, node.gateway.id))
|
|
||||||
return 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)
|
|
||||||
|
|
||||||
def sendevent(self, eventname, *args, **kwargs):
|
|
||||||
self.channel.send((eventname, args, kwargs))
|
|
||||||
|
|
||||||
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 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")
|
|
||||||
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')
|
|
||||||
if call.excinfo:
|
|
||||||
# likely it is not collectable here because of
|
|
||||||
# platform/import-dependency induced skips
|
|
||||||
# we fake a setup-error report with the obtained exception
|
|
||||||
# and do not care about capturing or non-runner hooks
|
|
||||||
rep = self.runner.pytest_runtest_makereport(item=item, call=call)
|
|
||||||
self.pytest_runtest_logreport(rep)
|
|
||||||
return
|
|
||||||
item.config.hook.pytest_runtest_protocol(item=item)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import py
|
|
||||||
|
|
||||||
class StatRecorder:
|
|
||||||
def __init__(self, rootdirlist):
|
|
||||||
self.rootdirlist = rootdirlist
|
|
||||||
self.statcache = {}
|
|
||||||
self.check() # snapshot state
|
|
||||||
|
|
||||||
def fil(self, p):
|
|
||||||
return p.ext in ('.py', '.txt', '.c', '.h')
|
|
||||||
def rec(self, p):
|
|
||||||
return p.check(dotfile=0)
|
|
||||||
|
|
||||||
def waitonchange(self, checkinterval=1.0):
|
|
||||||
while 1:
|
|
||||||
changed = self.check()
|
|
||||||
if changed:
|
|
||||||
return
|
|
||||||
py.std.time.sleep(checkinterval)
|
|
||||||
|
|
||||||
def check(self, removepycfiles=True):
|
|
||||||
changed = False
|
|
||||||
statcache = self.statcache
|
|
||||||
newstat = {}
|
|
||||||
for rootdir in self.rootdirlist:
|
|
||||||
for path in rootdir.visit(self.fil, self.rec):
|
|
||||||
oldstat = statcache.get(path, None)
|
|
||||||
if oldstat is not None:
|
|
||||||
del statcache[path]
|
|
||||||
try:
|
|
||||||
newstat[path] = curstat = path.stat()
|
|
||||||
except py.error.ENOENT:
|
|
||||||
if oldstat:
|
|
||||||
del statcache[path]
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
if oldstat:
|
|
||||||
if oldstat.mtime != curstat.mtime or \
|
|
||||||
oldstat.size != curstat.size:
|
|
||||||
changed = True
|
|
||||||
py.builtin.print_("# MODIFIED", path)
|
|
||||||
if removepycfiles and path.ext == ".py":
|
|
||||||
pycfile = path + "c"
|
|
||||||
if pycfile.check():
|
|
||||||
pycfile.remove()
|
|
||||||
|
|
||||||
else:
|
|
||||||
changed = True
|
|
||||||
if statcache:
|
|
||||||
changed = True
|
|
||||||
self.statcache = newstat
|
|
||||||
return changed
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user