Compare commits

...

43 Commits

Author SHA1 Message Date
Bruno Oliveira
9b2163f028 Update CHANGELOG with towncrier 2017-12-22 17:34:12 -02:00
Bruno Oliveira
3f09e158be Merge pull request #242 from Corni/loadfilescope
Load-Distribute test cases by filename
2017-12-22 16:27:20 -02:00
Bruno Oliveira
0d19e7f22b Merge remote-tracking branch 'upstream/master' into Corni/loadfilescope 2017-12-22 16:07:51 -02:00
Bruno Oliveira
aedad85e35 Add --dist=loadfile doc to README 2017-12-22 16:07:33 -02:00
Bruno Oliveira
a020c9509d Small adjustment to CHANGELOG 2017-12-22 16:07:26 -02:00
Ronny Pfannschmidt
efce50fe6a Merge pull request #261 from nicoddemus/pytest33
Fix tests in pytest >= 3.3 and add it to build matrix
2017-12-22 18:51:02 +01:00
Bruno Oliveira
db1fb683cf Add pytest 3.3 to tox build matrix 2017-12-22 15:38:07 -02:00
Bruno Oliveira
28d60f94f3 Fix load scope tests in pytest 3.3
Fix #260
2017-12-22 15:35:24 -02:00
Bruno Oliveira
ed9f25a0d2 Merge pull request #259 from hugovk/rm-eol
Drop support for EOL Python 2.6
2017-12-22 15:24:07 -02:00
Hugo
cb8b161d36 Add towncrier changelog entry 2017-12-21 11:31:38 +02:00
Hugo
cec33cca8a Add Python versions badge and alts to other badges 2017-12-21 11:26:59 +02:00
Hugo
c6f2fdcc31 Remove redundant parentheses 2017-12-21 11:19:58 +02:00
Hugo
8b79d909ea Rewrite unnecessary list literals as set literals 2017-12-21 11:04:19 +02:00
Hugo
2b0dc3f83a Remove redundant Python 3.2 code 2017-12-21 10:59:52 +02:00
Hugo
ed2db3d03a Drop support for EOL Python 2.6 2017-12-21 10:55:57 +02:00
Hugo
d3229af3da Ignore IDE metadata 2017-12-21 10:43:24 +02:00
Cornelius Riemenschneider
fad312fdb7 Add scheduling by test file. 2017-12-14 16:18:46 +01:00
Cornelius Riemenschneider
d4f977f3b7 Add PyCharm files to .gitignore. 2017-12-14 16:08:40 +01:00
Ronny Pfannschmidt
5889e4c229 Merge pull request #248 from timyhou/issue_241
Issue #241 Make copy during serialization of reprtraceback and reprcr…
2017-11-28 14:59:08 +01:00
Bruno Oliveira
239065810e Merge remote-tracking branch 'upstream/master' into timyhou/issue_241 2017-11-22 21:29:32 -02:00
Bruno Oliveira
bf0645ad35 Merge pull request #249 from nicoddemus/flakes
Fix E741 and E722 flake errors
2017-11-22 21:28:52 -02:00
Bruno Oliveira
cf067a86d7 Update pip on AppVeyor
py26-pytest30 is failing due to a setuptools error. Try to update
pip to see if this fixes the issue.
2017-11-22 20:58:06 -02:00
Bruno Oliveira
58f7291a99 Fix E741 and E722 flake errors 2017-11-22 20:00:39 -02:00
thou
2eb1002781 Issue #241 Make copy during serialization of reprtraceback and reprcrash to avoid mutating. 2017-11-21 12:33:07 -06:00
Bruno Oliveira
635d3d02cb Merge pull request #239 from nicoddemus/release-1.20.1
Release 1.20.1
2017-10-05 17:22:48 -03:00
Bruno Oliveira
adaa10f285 Release 1.20.1 2017-10-05 16:55:45 -03:00
Ronny Pfannschmidt
1cd7001647 Merge pull request #233 from blueyed/improve-pdb-usage-error
Improve UsageError when used with --pdb
2017-10-05 13:19:25 +02:00
Bruno Oliveira
95b660e26a Merge pull request #232 from verdesmarald/master
Don't create DSession in collect-only mode
2017-10-05 07:35:36 -03:00
Ronny Pfannschmidt
7a5efcdb76 Merge pull request #238 from timj/u/timj/exit-when-all-dead
Stop looping if all workers have died
2017-10-05 08:03:12 +02:00
Tim Jenness
9e59d07947 Stop looping if all workers have died
If the workers are crashing and the restart limit has been met,
we need to stop listening for events and trigger an internal
error.
2017-10-04 17:59:36 -07:00
Daniel Hahler
3799caa4fc Improve UsageError when used with --pdb
It might not be clear that pytest-xdist is used at all, and this should
help users to make --pdb work.
2017-09-23 21:07:39 +02:00
verdesmarald
1e66af6389 Fix flake8 formatting errors 2017-09-07 16:40:32 +10:00
verdesmarald
7674f6b186 Add tests for interactions between boxed, dist and collect-only.
Re-enable parsing of the boxed option with collect-only
2017-09-07 16:09:06 +10:00
verdesmarald
c83f73e19c Add news file for bugfix 2017-09-04 14:32:19 +10:00
verdesmarald
c874872190 Don't create DSession in collect-only mode 2017-09-04 14:25:42 +10:00
Ronny Pfannschmidt
e4f36b18f3 Merge pull request #228 from nicoddemus/ci-pytest
Add test environments using pytest master and features branches
2017-09-02 18:33:54 +02:00
Bruno Oliveira
0cdef59428 Add test environments using pytest master and features branches 2017-09-01 18:51:49 -03:00
Bruno Oliveira
4bb06e3f74 Merge pull request #227 from EdwardBetts/spelling
correct spelling mistake
2017-09-01 17:22:12 -03:00
Edward Betts
2c9b0b8894 correct spelling mistake 2017-09-01 20:45:18 +01:00
Bruno Oliveira
b464335c83 Fix link in CHANGELOG 2017-08-19 06:11:32 -03:00
Ronny Pfannschmidt
24b478446a Merge pull request #225 from pytest-dev/Ronny-patch-1
fix up travis deploy config
2017-08-18 15:47:25 +02:00
Ronny Pfannschmidt
d4d52dc933 fix up travis deploy config
move distributions key to the right place
install setuptools_scm for deploy
2017-08-18 14:00:44 +02:00
Bruno Oliveira
e5c2347934 Merge pull request #224 from nicoddemus/release-1.20.0
Release 1.20.0
2017-08-18 08:47:10 -03:00
21 changed files with 333 additions and 78 deletions

1
.gitignore vendored
View File

@@ -30,3 +30,4 @@ pytestdebug.log
.tox/ .tox/
.cache/ .cache/
.eggs/ .eggs/
.idea/

View File

@@ -12,7 +12,6 @@ notifications:
- pytest-commit@python.org - pytest-commit@python.org
python: python:
- '2.6'
- '2.7' - '2.7'
- '3.4' - '3.4'
- '3.5' - '3.5'
@@ -21,6 +20,7 @@ env:
- TOXENV=py-pytest30 - TOXENV=py-pytest30
- TOXENV=py-pytest31 - TOXENV=py-pytest31
- TOXENV=py-pytest32 - TOXENV=py-pytest32
- TOXENV=py-pytest33
install: pip install tox setuptools_scm install: pip install tox setuptools_scm
script: tox script: tox
@@ -29,6 +29,14 @@ jobs:
include: include:
- stage: test - stage: test
# python x env above are already included into this stage # python x env above are already included into this stage
- python: "2.7"
env: TOXENV=py27-pytestmaster
- python: "2.7"
env: TOXENV=py27-pytestfeatures
- python: "3.6"
env: TOXENV=py36-pytestmaster
- python: "3.6"
env: TOXENV=py36-pytestfeatures
- python: "3.6" - python: "3.6"
env: TOXENV=flakes env: TOXENV=flakes
- python: "3.6" - python: "3.6"
@@ -37,15 +45,15 @@ jobs:
- stage: deploy - stage: deploy
python: '3.6' python: '3.6'
env: env:
install: skip install: pip install -U setuptools setuptools_scm
script: skip script: skip
deploy: deploy:
provider: pypi provider: pypi
user: ronny user: ronny
distributions: sdist bdist_wheel
skip_upload_docs: true skip_upload_docs: true
password: password:
secure: cxmSDho5d+PYKEM4ZCg8ms1P4lzhYkrw6fEOm2HtTcsuCyY6aZMSgImWAnEYbJHSkdzgcxlXK9UKJ9B0YenXmBCkAr7UjdnpNXNmkySr0sYzlH/sfqt/dDATCHFaRKxnkOSOVywaDYhT9n8YudbXI77pXwD12i/CeSSJDbHhsu0JYUfAcb+D6YjRYoA2SEGCnzSzg+gDDfwXZx4ZiODCGLVwieNp1klCg88YROUE1BaYYNuUOONvfXX8+TWowbCF6ChH1WL/bZ49OStEYQNuYxZQZr4yClIqu9VJbchrU8j860K9ott2kkGTgfB/dDrQB/XncBubyIX9ikzCQAmmBXWAI3eyvWLPDk2Jz7kW2l2RT7syct80tCq3JhvQ1qdwr5ap7siocTLgnBW0tF4tkHSTFN3510fkc43npnp6FThebESQpnI24vqpwJ9hI/kW5mYi014Og2E/cpCXnz2XO8iZPDbqAMQpDsqEQoyhfGNgPTGp4K30TxRtwZBI5hHhDKnnR16fXtRgt1gYPvz/peUQvvpOm4JzIzGXPzluuutpnCBy75v5+oiwT3YRrLL/Meims9FtDDXL3qQubAE/ezIOOpm0N5XXV8DxIom8EN71yq5ab1tqhM+tBX7owRjy4FR4If2Q8feBdmTuh26DIQt/y+qSG8VkB9Sw/JCjc7c= secure: cxmSDho5d+PYKEM4ZCg8ms1P4lzhYkrw6fEOm2HtTcsuCyY6aZMSgImWAnEYbJHSkdzgcxlXK9UKJ9B0YenXmBCkAr7UjdnpNXNmkySr0sYzlH/sfqt/dDATCHFaRKxnkOSOVywaDYhT9n8YudbXI77pXwD12i/CeSSJDbHhsu0JYUfAcb+D6YjRYoA2SEGCnzSzg+gDDfwXZx4ZiODCGLVwieNp1klCg88YROUE1BaYYNuUOONvfXX8+TWowbCF6ChH1WL/bZ49OStEYQNuYxZQZr4yClIqu9VJbchrU8j860K9ott2kkGTgfB/dDrQB/XncBubyIX9ikzCQAmmBXWAI3eyvWLPDk2Jz7kW2l2RT7syct80tCq3JhvQ1qdwr5ap7siocTLgnBW0tF4tkHSTFN3510fkc43npnp6FThebESQpnI24vqpwJ9hI/kW5mYi014Og2E/cpCXnz2XO8iZPDbqAMQpDsqEQoyhfGNgPTGp4K30TxRtwZBI5hHhDKnnR16fXtRgt1gYPvz/peUQvvpOm4JzIzGXPzluuutpnCBy75v5+oiwT3YRrLL/Meims9FtDDXL3qQubAE/ezIOOpm0N5XXV8DxIom8EN71yq5ab1tqhM+tBX7owRjy4FR4If2Q8feBdmTuh26DIQt/y+qSG8VkB9Sw/JCjc7c=
on: on:
tags: true tags: true
distributions: sdist bdist_wheel
repo: pytest-dev/pytest-xdist repo: pytest-dev/pytest-xdist

View File

@@ -1,3 +1,43 @@
pytest-xdist 1.21.0 (2017-12-22)
================================
Deprecations and Removals
-------------------------
- Drop support for EOL Python 2.6. (`#259
<https://github.com/pytest-dev/pytest-xdist/issues/259>`_)
Features
--------
- New ``--dist=loadfile`` option which load-distributes test to workers grouped
by the file the tests live in. (`#242
<https://github.com/pytest-dev/pytest-xdist/issues/242>`_)
Bug Fixes
---------
- Fix accidental mutation of test report during serialization causing longrepr
string-ification to break. (`#241
<https://github.com/pytest-dev/pytest-xdist/issues/241>`_)
pytest-xdist 1.20.1 (2017-10-05)
================================
Bug Fixes
---------
- Fix hang when all worker nodes crash and restart limit is reached (`#45
<https://github.com/pytest-dev/pytest-xdist/issues/45>`_)
- Fix issue where the -n option would still run distributed tests when pytest
was run with the --collect-only option (`#5
<https://github.com/pytest-dev/pytest-xdist/issues/5>`_)
pytest-xdist 1.20.0 (2017-08-17) pytest-xdist 1.20.0 (2017-08-17)
================================ ================================
@@ -6,7 +46,7 @@ Features
- ``xdist`` now supports tests to log results multiple times, improving - ``xdist`` now supports tests to log results multiple times, improving
integration with plugins which require it like `pytest-rerunfailures integration with plugins which require it like `pytest-rerunfailures
<https://github.com/gocept/pytest-rerunfailures>_` and `flaky <https://github.com/gocept/pytest-rerunfailures>`_ and `flaky
<https://pypi.python.org/pypi/flaky>`_. (`#206 <https://github.com/pytest- <https://pypi.python.org/pypi/flaky>`_. (`#206 <https://github.com/pytest-
dev/pytest-xdist/issues/206>`_) dev/pytest-xdist/issues/206>`_)
@@ -250,7 +290,7 @@ Improved Documentation
in master. Thanks Aron Curzon. in master. Thanks Aron Curzon.
- fix pytest issue419 by sending/receiving indices into the test - fix pytest issue419 by sending/receiving indices into the test
collection instead of node ids (which are not neccessarily unique collection instead of node ids (which are not necessarily unique
for functions parametrized with duplicate values) for functions parametrized with duplicate values)
- send multiple "to test" indices in one network message to a slave - send multiple "to test" indices in one network message to a slave

View File

@@ -1,15 +1,23 @@
.. image:: http://img.shields.io/pypi/v/pytest-xdist.svg .. image:: http://img.shields.io/pypi/v/pytest-xdist.svg
:alt: PyPI version
:target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://img.shields.io/pypi/pyversions/pytest-xdist.svg
:alt: Python versions
:target: https://pypi.python.org/pypi/pytest-xdist :target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://anaconda.org/conda-forge/pytest-xdist/badges/version.svg .. image:: https://anaconda.org/conda-forge/pytest-xdist/badges/version.svg
:alt: Anaconda version
:target: https://anaconda.org/conda-forge/pytest-xdist :target: https://anaconda.org/conda-forge/pytest-xdist
.. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master .. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master
:alt: Travis CI build status
:target: https://travis-ci.org/pytest-dev/pytest-xdist :target: https://travis-ci.org/pytest-dev/pytest-xdist
.. image:: https://ci.appveyor.com/api/projects/status/56eq1a1avd4sdd7e/branch/master?svg=true .. image:: https://ci.appveyor.com/api/projects/status/56eq1a1avd4sdd7e/branch/master?svg=true
:alt: AppVeyor build status
:target: https://ci.appveyor.com/project/pytestbot/pytest-xdist :target: https://ci.appveyor.com/project/pytestbot/pytest-xdist
xdist: pytest distributed testing plugin xdist: pytest distributed testing plugin
@@ -229,7 +237,7 @@ You can also add default environments like this:
.. code-block:: ini .. code-block:: ini
[pytest] [pytest]
addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python3.6
and then just type:: and then just type::
@@ -313,6 +321,22 @@ the same worker ``gw0``, while the test methods from classes ``TestHDF`` and
Currently the groupings can't be customized, with grouping by class takes Currently the groupings can't be customized, with grouping by class takes
priority over grouping by module. priority over grouping by module.
Sending tests to the same worker based on their file
++++++++++++++++++++++++++++++++++++++++++++++++++++
*New in version 1.21.*
.. note::
This is an **experimental** feature: the actual functionality will
likely stay the same, but the CLI might change slightly in future versions.
You can send tests to the same worker grouped by their filename by using the
``--dist=loadfile`` option, so tests of the same file are guaranteed to run
in the same worker.
Using the example in the previous section, all tests from ``test_container.py`` will
run in the same worker, as well as the tests in ``test_io.py``.
Specifying "rsync" dirs in an ini-file Specifying "rsync" dirs in an ini-file
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

View File

@@ -1,18 +1,17 @@
environment: environment:
matrix: matrix:
# note: please use "tox --listenvs" to populate the build matrix # note: please use "tox --listenvs" to populate the build matrix
- TOXENV: "py26-pytest30" - TOXENV: "py27-pytest33"
- TOXENV: "py27-pytest30" - TOXENV: "py34-pytest33"
- TOXENV: "py34-pytest30" - TOXENV: "py35-pytest33"
- TOXENV: "py35-pytest30" - TOXENV: "py36-pytest33"
- TOXENV: "py36-pytest30" - TOXENV: "py27-pytest33-pexpect"
- TOXENV: "py27-pytest30-pexpect" - TOXENV: "py36-pytest33-pexpect"
- TOXENV: "py35-pytest30-pexpect"
- TOXENV: "flakes" - TOXENV: "flakes"
- TOXENV: "readme" - TOXENV: "readme"
install: install:
- C:\Python35\python -m pip install tox setuptools_scm - C:\Python35\python -m pip install -U tox setuptools_scm pip
build: false # Not a C# project, build stuff at the test step instead. build: false # Not a C# project, build stuff at the test step instead.

View File

@@ -1,12 +1,7 @@
from sys import version_info
from setuptools import setup, find_packages from setuptools import setup, find_packages
install_requires = ['execnet>=1.1', 'pytest>=3.0.0', 'pytest-forked'] install_requires = ['execnet>=1.1', 'pytest>=3.0.0', 'pytest-forked']
if version_info < (2, 7):
install_requires.append('ordereddict')
setup( setup(
name="pytest-xdist", name="pytest-xdist",
@@ -27,6 +22,7 @@ setup(
], ],
}, },
zip_safe=False, zip_safe=False,
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
install_requires=install_requires, install_requires=install_requires,
setup_requires=['setuptools_scm'], setup_requires=['setuptools_scm'],
classifiers=[ classifiers=[
@@ -41,6 +37,11 @@ setup(
'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Quality Assurance',
'Topic :: Utilities', 'Topic :: Utilities',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
], ],
) )

View File

@@ -262,6 +262,17 @@ class TestDistribution:
child.close() child.close()
# assert ret == 2 # assert ret == 2
def test_dist_with_collectonly(self, testdir):
p1 = testdir.makepyfile("""
def test_ok():
pass
""")
result = testdir.runpytest(p1, "-n1", "--collect-only")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*collected 1 item*",
])
class TestDistEach: class TestDistEach:
def test_simple(self, testdir): def test_simple(self, testdir):
@@ -701,6 +712,17 @@ class TestNodeFailure:
"*2 failed*2 passed*", "*2 failed*2 passed*",
]) ])
def test_max_slave_restart_die(self, testdir):
f = testdir.makepyfile("""
import os
os._exit(1)
""")
res = testdir.runpytest(f, '-n4', '--max-slave-restart=0')
res.stdout.fnmatch_lines([
"*Unexpectedly no active workers*",
"*INTERNALERROR*"
])
def test_disable_restart(self, testdir): def test_disable_restart(self, testdir):
f = testdir.makepyfile(""" f = testdir.makepyfile("""
import os import os
@@ -733,9 +755,9 @@ def test_worker_id_fixture(testdir, n):
with open(fname) as f: with open(fname) as f:
worker_ids.add(f.read().strip()) worker_ids.add(f.read().strip())
if n == 0: if n == 0:
assert worker_ids == set(['master']) assert worker_ids == {'master'}
else: else:
assert worker_ids == set(['gw0', 'gw1']) assert worker_ids == {'gw0', 'gw1'}
@pytest.mark.parametrize('tb', @pytest.mark.parametrize('tb',
@@ -834,11 +856,72 @@ class TestLoadScope:
'test_a.py::TestB', result.outlines) in ({'gw0': 10}, {'gw1': 10}) 'test_a.py::TestB', result.outlines) in ({'gw0': 10}, {'gw1': 10})
class TestFileScope:
def test_by_module(self, testdir):
test_file = """
import pytest
class TestA:
@pytest.mark.parametrize('i', range(10))
def test(self, i):
pass
class TestB:
@pytest.mark.parametrize('i', range(10))
def test(self, i):
pass
"""
testdir.makepyfile(
test_a=test_file,
test_b=test_file,
)
result = testdir.runpytest('-n2', '--dist=loadfile', '-v')
test_a_workers_and_test_count = get_workers_and_test_count_by_prefix(
'test_a.py::TestA', result.outlines)
test_b_workers_and_test_count = get_workers_and_test_count_by_prefix(
'test_b.py::TestB', result.outlines)
assert test_a_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \
test_a_workers_and_test_count in ({'gw0': 0}, {'gw1': 10})
assert test_b_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \
test_b_workers_and_test_count in ({'gw0': 0}, {'gw1': 10})
def test_by_class(self, testdir):
testdir.makepyfile(test_a="""
import pytest
class TestA:
@pytest.mark.parametrize('i', range(10))
def test(self, i):
pass
class TestB:
@pytest.mark.parametrize('i', range(10))
def test(self, i):
pass
""")
result = testdir.runpytest('-n2', '--dist=loadfile', '-v')
test_a_workers_and_test_count = get_workers_and_test_count_by_prefix(
'test_a.py::TestA', result.outlines)
test_b_workers_and_test_count = get_workers_and_test_count_by_prefix(
'test_a.py::TestB', result.outlines)
assert test_a_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \
test_a_workers_and_test_count in ({'gw0': 0}, {'gw1': 10})
assert test_b_workers_and_test_count in ({'gw0': 10}, {'gw1': 0}) or \
test_b_workers_and_test_count in ({'gw0': 0}, {'gw1': 10})
def parse_tests_and_workers_from_output(lines): def parse_tests_and_workers_from_output(lines):
result = [] result = []
for line in lines: for line in lines:
# example match: "[gw0] PASSED test_a.py::test[7]" # example match: "[gw0] PASSED test_a.py::test[7]"
m = re.match(r'\[(gw\d)\]\s(.*?)\s(.*::.*)', line.strip()) m = re.match(r'''
\[(gw\d)\] # worker
\s*
(?:\[\s*\d+%\])? # progress indicator (pytest >=3.3)
\s(.*?) # status string ("PASSED")
\s(.*::.*) # nodeid
''', line.strip(), re.VERBOSE)
if m: if m:
worker, status, nodeid = m.groups() worker, status, nodeid = m.groups()
result.append((worker, status, nodeid)) result.append((worker, status, nodeid))

View File

@@ -20,13 +20,13 @@ pytest_plugins = "pytester"
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _divert_atexit(request, monkeypatch): def _divert_atexit(request, monkeypatch):
import atexit import atexit
l = [] finalizers = []
def finish(): def finish():
while l: while finalizers:
l.pop()() finalizers.pop()()
monkeypatch.setattr(atexit, "register", l.append) monkeypatch.setattr(atexit, "register", finalizers.append)
request.addfinalizer(finish) request.addfinalizer(finish)
@@ -34,7 +34,7 @@ def pytest_addoption(parser):
parser.addoption('--gx', parser.addoption('--gx',
action="append", action="append",
dest="gspecs", dest="gspecs",
help=("add a global test environment, XSpec-syntax. ")) help="add a global test environment, XSpec-syntax. ")
@pytest.fixture @pytest.fixture

View File

@@ -126,7 +126,7 @@ class TestLoadScheduling:
sched.add_node(MockNode()) sched.add_node(MockNode())
sched.add_node(MockNode()) sched.add_node(MockNode())
node1, node2 = sched.nodes node1, node2 = sched.nodes
col = ["xyz"] * (6) col = ["xyz"] * 6
sched.add_node_collection(node1, col) sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col) sched.add_node_collection(node2, col)
sched.schedule() sched.schedule()

View File

@@ -83,10 +83,10 @@ class TestStatRecorder:
tmp = tmpdir tmp = tmpdir
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
l = [True, False] ret_values = [True, False]
monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop()) monkeypatch.setattr(StatRecorder, 'check', lambda self: ret_values.pop())
sd.waitonchange(checkinterval=0.2) sd.waitonchange(checkinterval=0.2)
assert not l assert not ret_values
class TestRemoteControl: class TestRemoteControl:

View File

@@ -44,6 +44,36 @@ def test_auto_detect_cpus(testdir, monkeypatch):
assert config.getoption('numprocesses') == 99 assert config.getoption('numprocesses') == 99
def test_boxed_with_collect_only(testdir):
from xdist.plugin import pytest_cmdline_main as check_options
config = testdir.parseconfigure("-n1", "--boxed")
check_options(config)
assert config.option.forked
config = testdir.parseconfigure("-n1", "--collect-only")
check_options(config)
assert not config.option.forked
config = testdir.parseconfigure("-n1", "--boxed", "--collect-only")
check_options(config)
assert config.option.forked
def test_dsession_with_collect_only(testdir):
from xdist.plugin import pytest_cmdline_main as check_options
from xdist.plugin import pytest_configure as configure
config = testdir.parseconfigure("-n1")
check_options(config)
configure(config)
assert config.pluginmanager.hasplugin("dsession")
config = testdir.parseconfigure("-n1", "--collect-only")
check_options(config)
configure(config)
assert not config.pluginmanager.hasplugin("dsession")
class TestDistOptions: class TestDistOptions:
def test_getxspecs(self, testdir): def test_getxspecs(self, testdir):
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")

View File

@@ -79,6 +79,21 @@ def test_remoteinitconfig(testdir):
class TestReportSerialization: class TestReportSerialization:
def test_xdist_longrepr_to_str_issue_241(self, testdir):
testdir.makepyfile("""
import os
def test_a(): assert False
def test_b(): pass
""")
testdir.makeconftest("""
def pytest_runtest_logreport(report):
print(report.longrepr)
""")
res = testdir.runpytest('-n1', '-s')
res.stdout.fnmatch_lines([
'*1 failed, 1 passed *'
])
def test_xdist_report_longrepr_reprcrash_130(self, testdir): def test_xdist_report_longrepr_reprcrash_130(self, testdir):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource("""
import py import py
@@ -107,8 +122,6 @@ class TestReportSerialization:
assert rep.longrepr.reprtraceback.style \ assert rep.longrepr.reprtraceback.style \
== a.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style
assert rep.longrepr.sections == a.longrepr.sections assert rep.longrepr.sections == a.longrepr.sections
assert rep.longrepr.reprtraceback.reprentries \
== a.longrepr.reprtraceback.reprentries
# Missing section attribute PR171 # Missing section attribute PR171
assert added_section in a.longrepr.sections assert added_section in a.longrepr.sections
@@ -127,14 +140,15 @@ class TestReportSerialization:
rep_entries = rep.longrepr.reprtraceback.reprentries rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries
assert rep_entries == a_entries
for i in range(len(a_entries)): for i in range(len(a_entries)):
assert isinstance(rep_entries[i], ReprEntry) assert isinstance(rep_entries[i], ReprEntry)
assert rep_entries[i].lines == a_entries[i].lines assert rep_entries[i].lines == a_entries[i].lines
assert rep_entries[i].localssep == a_entries[i].localssep assert rep_entries[i].localssep == a_entries[i].localssep
assert rep_entries[i].reprfileloc == a_entries[i].reprfileloc assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
assert rep_entries[i].reprfuncargs == a_entries[i].reprfuncargs assert rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
assert rep_entries[i].reprlocals == a_entries[i].reprlocals assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path
assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args
assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines
assert rep_entries[i].style == a_entries[i].style assert rep_entries[i].style == a_entries[i].style
def test_reprentries_serialization_196(self, testdir): def test_reprentries_serialization_196(self, testdir):
@@ -152,7 +166,6 @@ class TestReportSerialization:
rep_entries = rep.longrepr.reprtraceback.reprentries rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries
assert rep_entries == a_entries
for i in range(len(a_entries)): for i in range(len(a_entries)):
assert isinstance(rep_entries[i], ReprEntryNative) assert isinstance(rep_entries[i], ReprEntryNative)
assert rep_entries[i].lines == a_entries[i].lines assert rep_entries[i].lines == a_entries[i].lines

View File

@@ -50,10 +50,10 @@ class TestNodeManagerPopen:
assert gm.specs[0].chdir is None assert gm.specs[0].chdir is None
def test_default_chdir(self, config): def test_default_chdir(self, config):
l = ["ssh=noco", "socket=xyz"] specs = ["ssh=noco", "socket=xyz"]
for spec in NodeManager(config, l).specs: for spec in NodeManager(config, specs).specs:
assert spec.chdir == "pyexecnetcache" assert spec.chdir == "pyexecnetcache"
for spec in NodeManager(config, l, defaultchdir="abc").specs: for spec in NodeManager(config, specs, defaultchdir="abc").specs:
assert spec.chdir == "abc" assert spec.chdir == "abc"
def test_popen_makegateway_events(self, config, hookrecorder, def test_popen_makegateway_events(self, config, hookrecorder,
@@ -89,10 +89,10 @@ class TestNodeManagerPopen:
pass pass
gw.remote_exec = pseudoexec gw.remote_exec = pseudoexec
l = [] notifications = []
for gw in hm.group: for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: l.append(args)) hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert not l assert not notifications
hm.teardown_nodes() hm.teardown_nodes()
assert not len(hm.group) assert not len(hm.group)
assert "sys.path.insert" in gw.remote_exec.args[0] assert "sys.path.insert" in gw.remote_exec.args[0]
@@ -102,11 +102,11 @@ class TestNodeManagerPopen:
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1) hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
hm.setup_nodes(None) hm.setup_nodes(None)
source.ensure("dir1", "dir2", "hello") source.ensure("dir1", "dir2", "hello")
l = [] notifications = []
for gw in hm.group: for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: l.append(args)) hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert len(l) == 1 assert len(notifications) == 1
assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source) assert notifications[0] == ("rsyncrootready", hm.group['gw0'].spec, source)
hm.teardown_nodes() hm.teardown_nodes()
dest = dest.join(source.basename) dest = dest.join(source.basename)
assert dest.join("dir1").check() assert dest.join("dir1").check()
@@ -137,9 +137,9 @@ class TestHRSync:
source.ensure(".somedotfile", "moreentries") source.ensure(".somedotfile", "moreentries")
source.ensure("somedir", "editfile~") source.ensure("somedir", "editfile~")
syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES) syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES)
l = list(source.visit(rec=syncer.filter, fil=syncer.filter)) files = list(source.visit(rec=syncer.filter, fil=syncer.filter))
assert len(l) == 3 assert len(files) == 3
basenames = [x.basename for x in l] basenames = [x.basename for x in files]
assert 'dir' in basenames assert 'dir' in basenames
assert 'file.txt' in basenames assert 'file.txt' in basenames
assert 'somedir' in basenames assert 'somedir' in basenames

View File

@@ -1,8 +1,9 @@
[tox] [tox]
# if you change the envlist, please update .travis.yml file as well # if you change the envlist, please update .travis.yml file as well
envlist= envlist=
py{26,27,34,35,36}-pytest{30,31,32} py{27,34,35,36}-pytest{30,31,32,33}
py{27,36}-pytest{30,31,32}-pexpect py{27,36}-pytest{30,31,32,33}-pexpect
py{27,36}-pytest{master,features}
flakes flakes
readme readme
@@ -17,6 +18,9 @@ deps =
pytest30: pytest~=3.0.5 pytest30: pytest~=3.0.5
pytest31: pytest~=3.1.0 pytest31: pytest~=3.1.0
pytest32: pytest~=3.2.0 pytest32: pytest~=3.2.0
pytest33: pytest~=3.3.0
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
pexpect: pexpect pexpect: pexpect
platform= platform=
pexpect: linux|darwin pexpect: linux|darwin

View File

@@ -6,6 +6,7 @@ from xdist.scheduler import (
EachScheduling, EachScheduling,
LoadScheduling, LoadScheduling,
LoadScopeScheduling, LoadScopeScheduling,
LoadFileScheduling,
) )
@@ -99,6 +100,7 @@ class DSession:
'each': EachScheduling, 'each': EachScheduling,
'load': LoadScheduling, 'load': LoadScheduling,
'loadscope': LoadScopeScheduling, 'loadscope': LoadScopeScheduling,
'loadfile': LoadFileScheduling,
} }
return schedulers[dist](config, log) return schedulers[dist](config, log)
@@ -120,6 +122,10 @@ class DSession:
def loop_once(self): def loop_once(self):
"""Process one callback from one of the slaves.""" """Process one callback from one of the slaves."""
while 1: while 1:
if not self._active_nodes:
# If everything has died stop looping
self.triggershutdown()
raise RuntimeError("Unexpectedly no active workers available")
try: try:
eventcall = self.queue.get(timeout=2.0) eventcall = self.queue.get(timeout=2.0)
break break

View File

@@ -31,7 +31,7 @@ def pytest_addoption(parser):
"when crashed (set to zero to disable this feature)") "when crashed (set to zero to disable this feature)")
group.addoption( group.addoption(
'--dist', metavar="distmode", '--dist', metavar="distmode",
action="store", choices=['each', 'load', 'loadscope', 'no'], action="store", choices=['each', 'load', 'loadscope', 'loadfile', 'no'],
dest="dist", default="no", dest="dist", default="no",
help=("set mode for distributing tests to exec environments.\n\n" help=("set mode for distributing tests to exec environments.\n\n"
"each: send each test to all available environments.\n\n" "each: send each test to all available environments.\n\n"
@@ -39,6 +39,8 @@ def pytest_addoption(parser):
" available environment.\n\n" " available environment.\n\n"
"loadscope: load balance by sending pending groups of tests in" "loadscope: load balance by sending pending groups of tests in"
" the same scope to any available environment.\n\n" " the same scope to any available environment.\n\n"
"loadfile: load balance by sending test grouped by file"
" to any available environment.\n\n"
"(default) no: run tests inprocess, don't distribute.")) "(default) no: run tests inprocess, don't distribute."))
group.addoption( group.addoption(
'--tx', dest="tx", action="append", default=[], '--tx', dest="tx", action="append", default=[],
@@ -91,7 +93,7 @@ def pytest_addhooks(pluginmanager):
@pytest.mark.trylast @pytest.mark.trylast
def pytest_configure(config): def pytest_configure(config):
if config.getoption("dist") != "no": if config.getoption("dist") != "no" and not config.getvalue("collectonly"):
from xdist.dsession import DSession from xdist.dsession import DSession
session = DSession(config) session = DSession(config)
config.pluginmanager.register(session, "dsession") config.pluginmanager.register(session, "dsession")
@@ -115,7 +117,7 @@ def pytest_cmdline_main(config):
if val("dist") != "no": if val("dist") != "no":
if usepdb: if usepdb:
raise pytest.UsageError( raise pytest.UsageError(
"--pdb incompatible with distributing tests.") "--pdb is incompatible with distributing tests; try using -n0.") # noqa: E501
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# fixtures # fixtures

View File

@@ -111,18 +111,18 @@ class SlaveInteractor:
def serialize_report(rep): def serialize_report(rep):
def disassembled_report(rep): def disassembled_report(rep):
reprtraceback = rep.longrepr.reprtraceback.__dict__ reprtraceback = rep.longrepr.reprtraceback.__dict__.copy()
reprcrash = rep.longrepr.reprcrash.__dict__ reprcrash = rep.longrepr.reprcrash.__dict__.copy()
new_entries = [] new_entries = []
for entry in reprtraceback['reprentries']: for entry in reprtraceback['reprentries']:
entry_data = { entry_data = {
'type': type(entry).__name__, 'type': type(entry).__name__,
'data': entry.__dict__, 'data': entry.__dict__.copy(),
} }
for key, value in entry_data['data'].items(): for key, value in entry_data['data'].items():
if hasattr(value, '__dict__'): if hasattr(value, '__dict__'):
entry_data['data'][key] = value.__dict__ entry_data['data'][key] = value.__dict__.copy()
new_entries.append(entry_data) new_entries.append(entry_data)
reprtraceback['reprentries'] = new_entries reprtraceback['reprentries'] = new_entries
@@ -178,10 +178,6 @@ def remote_initconfig(option_dict, args):
if __name__ == '__channelexec__': if __name__ == '__channelexec__':
channel = channel # noqa channel = channel # noqa
# python3.2 is not concurrent import safe, so let's play it safe
# https://bitbucket.org/hpk42/pytest/issue/347/pytest-xdist-and-python-32
if sys.version_info[:2] == (3, 2):
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
slaveinput, args, option_dict = channel.receive() slaveinput, args, option_dict = channel.receive()
importpath = os.getcwd() importpath = os.getcwd()
sys.path.insert(0, importpath) # XXX only for remote situations sys.path.insert(0, importpath) # XXX only for remote situations

View File

@@ -1,3 +1,4 @@
from xdist.scheduler.each import EachScheduling # noqa from xdist.scheduler.each import EachScheduling # noqa
from xdist.scheduler.load import LoadScheduling # noqa from xdist.scheduler.load import LoadScheduling # noqa
from xdist.scheduler.loadscope import LoadScopeScheduling # noqa from xdist.scheduler.loadscope import LoadScopeScheduling # noqa
from xdist.scheduler.filescope import LoadFileScheduling # noqa

View File

@@ -0,0 +1,52 @@
from . import LoadScopeScheduling
from py.log import Producer
class LoadFileScheduling(LoadScopeScheduling):
"""Implement load scheduling across nodes, but grouping test test file.
This distributes the tests collected across all nodes so each test is run
just once. All nodes collect and submit the list of tests and when all
collections are received it is verified they are identical collections.
Then the collection gets divided up in work units, grouped by test file,
and those work units get submitted to nodes. Whenever a node finishes an
item, it calls ``.mark_test_complete()`` which will trigger the scheduler
to assign more work units if the number of pending tests for the node falls
below a low-watermark.
When created, ``numnodes`` defines how many nodes are expected to submit a
collection. This is used to know when all nodes have finished collection.
This class behaves very much like LoadScopeScheduling, but with a file-level scope.
"""
def __init(self, config, log=None):
super(LoadFileScheduling, self).__init__(config, log)
if log is None:
self.log = Producer('loadfilesched')
else:
self.log = log.loadfilesched
def _split_scope(self, nodeid):
"""Determine the scope (grouping) of a nodeid.
There are usually 3 cases for a nodeid::
example/loadsuite/test/test_beta.py::test_beta0
example/loadsuite/test/test_delta.py::Delta1::test_delta0
example/loadsuite/epsilon/__init__.py::epsilon.epsilon
#. Function in a test module.
#. Method of a class in a test module.
#. Doctest in a function in a package.
This function will group tests with the scope determined by splitting
the first ``::`` from the left. That is, test will be grouped in a
single work unit when they reside in the same file.
In the above example, scopes will be::
example/loadsuite/test/test_beta.py
example/loadsuite/test/test_delta.py
example/loadsuite/epsilon/__init__.py
"""
return nodeid.split('::', 1)[0]

View File

@@ -1,14 +1,9 @@
try:
from collections import OrderedDict from collections import OrderedDict
except ImportError:
# Support for Python 2.6
from ordereddict import OrderedDict
from py.log import Producer
from _pytest.runner import CollectReport from _pytest.runner import CollectReport
from py.log import Producer
from xdist.slavemanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.slavemanage import parse_spec_config
class LoadScopeScheduling: class LoadScopeScheduling:
@@ -373,12 +368,12 @@ class LoadScopeScheduling:
extra_nodes = len(self.nodes) - len(self.workqueue) extra_nodes = len(self.nodes) - len(self.workqueue)
if extra_nodes > 0: if extra_nodes > 0:
self.log('Shuting down {} nodes'.format(extra_nodes)) self.log('Shuting down {0} nodes'.format(extra_nodes))
for _ in range(extra_nodes): for _ in range(extra_nodes):
unused_node, assigned = self.assigned_work.popitem(last=True) unused_node, assigned = self.assigned_work.popitem(last=True)
self.log('Shuting down unused node {}'.format(unused_node)) self.log('Shuting down unused node {0}'.format(unused_node))
unused_node.shutdown() unused_node.shutdown()
# Assign initial workload # Assign initial workload

View File

@@ -186,7 +186,7 @@ class HostRSync(execnet.RSync):
def make_reltoroot(roots, args): def make_reltoroot(roots, args):
# XXX introduce/use public API for splitting py.test args # XXX introduce/use public API for splitting py.test args
splitcode = "::" splitcode = "::"
l = [] result = []
for arg in args: for arg in args:
parts = arg.split(splitcode) parts = arg.split(splitcode)
fspath = py.path.local(parts[0]) fspath = py.path.local(parts[0])
@@ -197,8 +197,8 @@ def make_reltoroot(roots, args):
break break
else: else:
raise ValueError("arg %s not relative to an rsync root" % (arg,)) raise ValueError("arg %s not relative to an rsync root" % (arg,))
l.append(splitcode.join(parts)) result.append(splitcode.join(parts))
return l return result
class SlaveController(object): class SlaveController(object):
@@ -326,7 +326,7 @@ class SlaveController(object):
except KeyboardInterrupt: except KeyboardInterrupt:
# should not land in receiver-thread # should not land in receiver-thread
raise raise
except: except: # noqa
excinfo = py.code.ExceptionInfo() excinfo = py.code.ExceptionInfo()
py.builtin.print_("!" * 20, excinfo) py.builtin.print_("!" * 20, excinfo)
self.config.notify_exception(excinfo) self.config.notify_exception(excinfo)