Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc02bb04a | ||
|
|
c5f65b39ec | ||
|
|
f00f9ecf58 | ||
|
|
48b7014530 | ||
|
|
1dec9dc246 | ||
|
|
f6ecaa9b7f | ||
|
|
1276df4cfa | ||
|
|
ccdb36a477 | ||
|
|
45ddc221ef | ||
|
|
c3dbd05bd1 | ||
|
|
b9423d894c | ||
|
|
242c009866 | ||
|
|
0ee219dc48 | ||
|
|
76075f5de1 | ||
|
|
f8d51aab57 | ||
|
|
ac2bf10fe2 | ||
|
|
fa85371c73 | ||
|
|
d55fab3aa7 | ||
|
|
4632df37a6 | ||
|
|
ca94bda1b7 | ||
|
|
261d4189f0 | ||
|
|
f283dddf36 | ||
|
|
6d74bab518 | ||
|
|
c306287a4e | ||
|
|
2b65800738 | ||
|
|
86bf3cb8b3 | ||
|
|
95aaaec6c5 | ||
|
|
33d5441258 | ||
|
|
b5c09f8e84 | ||
|
|
a30124e858 | ||
|
|
c5d8635294 | ||
|
|
5f0abe3cb8 | ||
|
|
3332c99e1e | ||
|
|
3329bbbae2 | ||
|
|
dd63d0b667 | ||
|
|
dd6eac12a4 | ||
|
|
a70dace69c | ||
|
|
6a21a5aa81 | ||
|
|
49c09117ed | ||
|
|
566480fdd3 | ||
|
|
5ecbf25450 | ||
|
|
646eb868ab | ||
|
|
50b27d2735 | ||
|
|
16db6eddd7 | ||
|
|
f2061875e1 | ||
|
|
c4b365b082 | ||
|
|
5465d936f7 | ||
|
|
b819657dac | ||
|
|
d8c6c22989 | ||
|
|
1efec5a153 | ||
|
|
f9a13fe53b | ||
|
|
74a5e0ca03 | ||
|
|
3292e0ede0 | ||
|
|
d40d47acd9 | ||
|
|
3579bfce7d | ||
|
|
365499eeaf | ||
|
|
a518bd4e2a | ||
|
|
d84ae73775 | ||
|
|
673fe9bd67 | ||
|
|
9217b80fb1 | ||
|
|
b151e56fbd | ||
|
|
70688b7986 | ||
|
|
adc385253c | ||
|
|
bfd6029102 | ||
|
|
3f5a8eeb7f | ||
|
|
d5b61e72d4 | ||
|
|
f224c4b00e | ||
|
|
ddefd3f453 | ||
|
|
5ce8a6ec1a | ||
|
|
fe06f27b8b | ||
|
|
86e2fb5a5a | ||
|
|
ec6129639e | ||
|
|
f6ac209cd5 | ||
|
|
b176b37606 | ||
|
|
22e36c9cb4 | ||
|
|
8825e7e234 | ||
|
|
5d5f87b600 | ||
|
|
571d08fb8b | ||
|
|
36c28ee154 | ||
|
|
af3e975664 | ||
|
|
bc2ba55f70 | ||
|
|
8475876ce3 |
38
.travis.yml
38
.travis.yml
@@ -11,36 +11,36 @@ notifications:
|
||||
email:
|
||||
- pytest-commit@python.org
|
||||
|
||||
python:
|
||||
- '2.7'
|
||||
- '3.4'
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
env:
|
||||
- TOXENV=py-pytest30
|
||||
- TOXENV=py-pytest31
|
||||
- TOXENV=py-pytest32
|
||||
- TOXENV=py-pytest33
|
||||
- TOXENV=py-pytest36
|
||||
- TOXENV=py-pytest38
|
||||
|
||||
install: pip install tox setuptools_scm
|
||||
install:
|
||||
- pip install -U pip
|
||||
- pip install tox setuptools_scm
|
||||
script: tox
|
||||
|
||||
stages:
|
||||
- linting
|
||||
- baseline
|
||||
- test
|
||||
- name: deploy
|
||||
if: repo = pytest-dev/pytest-xdist AND tag IS present
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: linting
|
||||
- stage: baseline
|
||||
python: '3.6'
|
||||
script:
|
||||
- tox -e linting
|
||||
env: TOXENV=linting
|
||||
- python: '3.6'
|
||||
env: TOXENV=py36-pytestlatest
|
||||
- python: '2.7'
|
||||
env: TOXENV=py27-pytestlatest
|
||||
|
||||
- stage: test
|
||||
# python x env above are already included into this stage
|
||||
python: "3.4"
|
||||
env: TOXENV=py34-pytestlatest
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35-pytestlatest
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37-pytestlatest
|
||||
sudo: required
|
||||
dist: xenial
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27-pytestmaster
|
||||
- python: "2.7"
|
||||
|
||||
@@ -1,3 +1,94 @@
|
||||
pytest-xdist 1.26.1 (2019-01-28)
|
||||
================================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#406 <https://github.com/pytest-dev/pytest-xdist/issues/406>`_: Do not implement deprecated ``pytest_logwarning`` hook in pytest versions where it is deprecated.
|
||||
|
||||
|
||||
pytest-xdist 1.26.0 (2019-01-11)
|
||||
================================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#376 <https://github.com/pytest-dev/pytest-xdist/issues/376>`_: The current directory is no longer added ``sys.path`` for local workers, only for remote connections.
|
||||
|
||||
This behavior is surprising because it makes xdist runs and non-xdist runs to potentially behave differently.
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#379 <https://github.com/pytest-dev/pytest-xdist/issues/379>`_: Warning attributes are checked to make sure they can be dumped prior to
|
||||
serializing the warning for submission to the master node.
|
||||
|
||||
|
||||
pytest-xdist 1.25.0 (2018-12-12)
|
||||
================================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#372 <https://github.com/pytest-dev/pytest-xdist/issues/372>`_: Pytest versions older than 3.6 are no longer supported.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#373 <https://github.com/pytest-dev/pytest-xdist/issues/373>`_: Node setup information is hidden when pytest is run in quiet mode to reduce noise on many-core machines.
|
||||
|
||||
- `#388 <https://github.com/pytest-dev/pytest-xdist/issues/388>`_: ``mainargv`` is made available in ``workerinput`` from the host's ``sys.argv``.
|
||||
|
||||
This can be used via ``request.config.workerinput["mainargv"]``.
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#332 <https://github.com/pytest-dev/pytest-xdist/issues/332>`_: Fix report of module-level skips (``pytest.skip(reason, allow_module_level=True)``).
|
||||
|
||||
- `#378 <https://github.com/pytest-dev/pytest-xdist/issues/378>`_: Fix support for gevent monkeypatching
|
||||
|
||||
- `#384 <https://github.com/pytest-dev/pytest-xdist/issues/384>`_: pytest 4.1 support: ``ExceptionInfo`` API changes.
|
||||
|
||||
- `#390 <https://github.com/pytest-dev/pytest-xdist/issues/390>`_: pytest 4.1 support: ``pytest_logwarning`` hook removed.
|
||||
|
||||
|
||||
pytest-xdist 1.24.1 (2018-11-09)
|
||||
================================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#349 <https://github.com/pytest-dev/pytest-xdist/issues/349>`_: Correctly handle warnings created with arguments that can't be serialized during the transfer from workers to master node.
|
||||
|
||||
|
||||
pytest-xdist 1.24.0 (2018-10-18)
|
||||
================================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#337 <https://github.com/pytest-dev/pytest-xdist/issues/337>`_: New ``--maxprocesses`` command-line option that limits the maximum number of workers when using ``--numprocesses=auto``.
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#351 <https://github.com/pytest-dev/pytest-xdist/issues/351>`_: Fix scheduling deadlock in case of inter-test locking.
|
||||
|
||||
|
||||
pytest-xdist 1.23.2 (2018-09-28)
|
||||
================================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#344 <https://github.com/pytest-dev/pytest-xdist/issues/344>`_: Fix issue where Warnings could cause pytest to fail if they do not set the args attribute correctly.
|
||||
|
||||
|
||||
pytest-xdist 1.23.1 (2018-09-25)
|
||||
================================
|
||||
|
||||
|
||||
@@ -223,6 +223,12 @@ defined:
|
||||
The information about the worker_id in a test is stored in the ``TestReport`` as
|
||||
well, under the ``worker_id`` attribute.
|
||||
|
||||
Acessing ``sys.argv`` from the master node in workers
|
||||
-----------------------------------------------------
|
||||
|
||||
To access the ``sys.argv`` passed to the command-line of the master node, use
|
||||
``request.config.workerinput["mainargv"]``.
|
||||
|
||||
|
||||
Specifying test exec environments in an ini file
|
||||
------------------------------------------------
|
||||
|
||||
23
appveyor.yml
23
appveyor.yml
@@ -1,22 +1,23 @@
|
||||
environment:
|
||||
matrix:
|
||||
# note: please use "tox --listenvs" to populate the build matrix
|
||||
- TOXENV: "py27-pytest33"
|
||||
- TOXENV: "py34-pytest33"
|
||||
- TOXENV: "py35-pytest33"
|
||||
- TOXENV: "py36-pytest33"
|
||||
- TOXENV: "py36-pytest36"
|
||||
- TOXENV: "py36-pytest38"
|
||||
- TOXENV: "py27-pytest33-pexpect"
|
||||
- TOXENV: "py36-pytest33-pexpect"
|
||||
- TOXENV: "py27-pytestlatest"
|
||||
- TOXENV: "py34-pytestlatest"
|
||||
- TOXENV: "py35-pytestlatest"
|
||||
- TOXENV: "py36-pytestlatest"
|
||||
- TOXENV: "py37-pytestlatest"
|
||||
- TOXENV: "py27-pytestmaster"
|
||||
- TOXENV: "py36-pytestmaster"
|
||||
- TOXENV: "py27-pytestfeatures"
|
||||
- TOXENV: "py36-pytestfeatures"
|
||||
|
||||
install:
|
||||
- C:\Python36\python -m pip install -U tox setuptools_scm pip
|
||||
- C:\Python37\python -m pip install -U pip
|
||||
- C:\Python37\python -m pip install -U tox setuptools_scm
|
||||
|
||||
build: false # Not a C# project, build stuff at the test step instead.
|
||||
|
||||
test_script:
|
||||
- C:\Python36\python -m tox
|
||||
- C:\Python37\python -m tox
|
||||
|
||||
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
|
||||
# might as well save resources
|
||||
|
||||
8
setup.py
8
setup.py
@@ -1,14 +1,17 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
install_requires = ["execnet>=1.1", "pytest>=3.0.0", "pytest-forked", "six"]
|
||||
install_requires = ["execnet>=1.1", "pytest>=3.6.0", "pytest-forked", "six"]
|
||||
|
||||
|
||||
with open("README.rst") as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name="pytest-xdist",
|
||||
use_scm_version={"write_to": "xdist/_version.py"},
|
||||
description="pytest xdist plugin for distributed testing"
|
||||
" and loop-on-failing modes",
|
||||
long_description=open("README.rst").read(),
|
||||
long_description=long_description,
|
||||
license="MIT",
|
||||
author="holger krekel and contributors",
|
||||
author_email="pytest-dev@python.org,holger@merlinux.eu",
|
||||
@@ -40,5 +43,6 @@ setup(
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import py
|
||||
@@ -358,6 +359,30 @@ class TestDistEach:
|
||||
|
||||
|
||||
class TestTerminalReporting:
|
||||
@pytest.mark.parametrize("verbosity", ["", "-q", "-v"])
|
||||
def test_output_verbosity(self, testdir, verbosity):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test_ok():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
args = ["-n1"]
|
||||
if verbosity:
|
||||
args.append(verbosity)
|
||||
result = testdir.runpytest(*args)
|
||||
out = result.stdout.str()
|
||||
if verbosity == "-v":
|
||||
assert "scheduling tests" in out
|
||||
assert "gw" in out
|
||||
elif verbosity == "-q":
|
||||
assert "scheduling tests" not in out
|
||||
assert "gw" not in out
|
||||
assert "bringing up nodes..." in out
|
||||
else:
|
||||
assert "scheduling tests" not in out
|
||||
assert "gw" in out
|
||||
|
||||
def test_pass_skip_fail(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
@@ -400,35 +425,6 @@ class TestTerminalReporting:
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
|
||||
def test_warnings(self, testdir, n, warn_type):
|
||||
from pkg_resources import parse_version
|
||||
|
||||
if parse_version(pytest.__version__) < parse_version("3.1"):
|
||||
pytest.skip("pytest warnings requires >= 3.1")
|
||||
|
||||
if warn_type == "builtin":
|
||||
warn_code = """warnings.warn(UserWarning('this is a warning'))"""
|
||||
elif warn_type == "pytest":
|
||||
warn_code = """request.config.warn('', 'this is a warning',
|
||||
fslocation=py.path.local())"""
|
||||
else:
|
||||
assert False
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import warnings, py, pytest
|
||||
|
||||
@pytest.mark.filterwarnings('ignore:config.warn has been deprecated')
|
||||
def test_func(request):
|
||||
{warn_code}
|
||||
""".format(
|
||||
warn_code=warn_code
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest(n)
|
||||
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"])
|
||||
|
||||
def test_logfinish_hook(self, testdir):
|
||||
"""Ensure the pytest_runtest_logfinish hook is being properly handled"""
|
||||
from _pytest import hookspec
|
||||
@@ -494,9 +490,8 @@ def test_session_hooks(testdir):
|
||||
name = "worker"
|
||||
else:
|
||||
name = "master"
|
||||
f = open(name, "w")
|
||||
f.write("xy")
|
||||
f.close()
|
||||
with open(name, "w") as f:
|
||||
f.write("xy")
|
||||
# let's fail on the worker
|
||||
if name == "worker":
|
||||
raise ValueError(42)
|
||||
@@ -546,16 +541,15 @@ def test_session_testscollected(testdir):
|
||||
assert collected_file.read() == "collected = 3"
|
||||
|
||||
|
||||
def test_funcarg_teardown_failure(testdir):
|
||||
def test_fixture_teardown_failure(testdir):
|
||||
p = testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
@pytest.fixture
|
||||
@pytest.fixture(scope="module")
|
||||
def myarg(request):
|
||||
def teardown(val):
|
||||
raise ValueError(val)
|
||||
return request.cached_setup(setup=lambda: 42, teardown=teardown,
|
||||
scope="module")
|
||||
yield 42
|
||||
raise ValueError(42)
|
||||
|
||||
def test_hello(myarg):
|
||||
pass
|
||||
"""
|
||||
@@ -641,6 +635,11 @@ def test_skipping(testdir):
|
||||
|
||||
|
||||
def test_issue34_pluginloading_in_subprocess(testdir):
|
||||
import _pytest.hookspec
|
||||
|
||||
if not hasattr(_pytest.hookspec, "pytest_namespace"):
|
||||
pytest.skip("this pytest version no longer supports pytest_namespace()")
|
||||
|
||||
testdir.tmpdir.join("plugin123.py").write(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
@@ -736,6 +735,108 @@ def test_sub_plugins_disabled(testdir, plugin):
|
||||
result.stdout.fnmatch_lines("*1 passed*")
|
||||
|
||||
|
||||
class TestWarnings:
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
|
||||
def test_warnings(self, testdir, n, request, warn_type):
|
||||
if warn_type == "builtin":
|
||||
warn_code = """warnings.warn(UserWarning('this is a warning'))"""
|
||||
elif warn_type == "pytest":
|
||||
if not hasattr(request.config, "warn"):
|
||||
pytest.skip("config.warn has been removed in pytest 4.1")
|
||||
warn_code = """request.config.warn('', 'this is a warning',
|
||||
fslocation=py.path.local())"""
|
||||
else:
|
||||
assert False
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import warnings, py, pytest
|
||||
|
||||
@pytest.mark.filterwarnings('ignore:config.warn has been deprecated')
|
||||
def test_func(request):
|
||||
{warn_code}
|
||||
""".format(
|
||||
warn_code=warn_code
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest(n)
|
||||
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"])
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_custom_subclass(self, testdir, n):
|
||||
"""Check that warning subclasses that don't honor the args attribute don't break
|
||||
pytest-xdist (#344)
|
||||
"""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import warnings, py, pytest
|
||||
|
||||
class MyWarning(UserWarning):
|
||||
|
||||
def __init__(self, p1, p2):
|
||||
self.p1 = p1
|
||||
self.p2 = p2
|
||||
self.args = ()
|
||||
|
||||
def test_func(request):
|
||||
warnings.warn(MyWarning("foo", 1))
|
||||
"""
|
||||
)
|
||||
testdir.syspathinsert()
|
||||
result = testdir.runpytest(n)
|
||||
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warnings*"])
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_unserializable_arguments(self, testdir, n):
|
||||
"""Check that warnings with unserializable arguments are handled correctly (#349)."""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import warnings, pytest
|
||||
|
||||
def test_func(tmpdir):
|
||||
fn = (tmpdir / 'foo.txt').ensure(file=1)
|
||||
with fn.open('r') as f:
|
||||
warnings.warn(UserWarning("foo", f))
|
||||
"""
|
||||
)
|
||||
testdir.syspathinsert()
|
||||
result = testdir.runpytest(n)
|
||||
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warnings*"])
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_unserializable_warning_details(self, testdir, n):
|
||||
"""Check that warnings with unserializable _WARNING_DETAILS are
|
||||
handled correctly (#379).
|
||||
"""
|
||||
if sys.version_info[0] < 3:
|
||||
# The issue is only present in Python 3 warnings
|
||||
return
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import warnings, pytest
|
||||
import socket
|
||||
import gc
|
||||
def abuse_socket():
|
||||
s = socket.socket()
|
||||
del s
|
||||
|
||||
# Deliberately provoke a ResourceWarning for an unclosed socket.
|
||||
# The socket itself will end up attached as a value in
|
||||
# _WARNING_DETAIL. We need to test that it is not serialized
|
||||
# (it can't be, so the test will fail if we try to).
|
||||
@pytest.mark.filterwarnings('always')
|
||||
def test_func(tmpdir):
|
||||
abuse_socket()
|
||||
gc.collect()
|
||||
"""
|
||||
)
|
||||
testdir.syspathinsert()
|
||||
result = testdir.runpytest(n)
|
||||
result.stdout.fnmatch_lines(
|
||||
["*ResourceWarning*unclosed*", "*1 passed, 1 warnings*"]
|
||||
)
|
||||
|
||||
|
||||
class TestNodeFailure:
|
||||
def test_load_single(self, testdir):
|
||||
f = testdir.makepyfile(
|
||||
@@ -1103,6 +1204,56 @@ class TestFileScope:
|
||||
assert c1 == c2
|
||||
|
||||
|
||||
class TestLocking:
|
||||
_test_content = """
|
||||
class TestClassName%s(object):
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
FILE_LOCK.acquire()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
FILE_LOCK.release()
|
||||
|
||||
def test_a(self):
|
||||
pass
|
||||
|
||||
def test_b(self):
|
||||
pass
|
||||
|
||||
def test_c(self):
|
||||
pass
|
||||
|
||||
"""
|
||||
|
||||
test_file1 = """
|
||||
import filelock
|
||||
|
||||
FILE_LOCK = filelock.FileLock("test.lock")
|
||||
|
||||
""" + (
|
||||
(_test_content * 4) % ("A", "B", "C", "D")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"])
|
||||
def test_single_file(self, testdir, scope):
|
||||
testdir.makepyfile(test_a=self.test_file1)
|
||||
result = testdir.runpytest("-n2", "--dist=%s" % scope, "-v")
|
||||
result.assert_outcomes(passed=(12 if scope != "each" else 12 * 2))
|
||||
|
||||
@pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"])
|
||||
def test_multi_file(self, testdir, scope):
|
||||
testdir.makepyfile(
|
||||
test_a=self.test_file1,
|
||||
test_b=self.test_file1,
|
||||
test_c=self.test_file1,
|
||||
test_d=self.test_file1,
|
||||
)
|
||||
result = testdir.runpytest("-n2", "--dist=%s" % scope, "-v")
|
||||
result.assert_outcomes(passed=(48 if scope != "each" else 48 * 2))
|
||||
|
||||
|
||||
def parse_tests_and_workers_from_output(lines):
|
||||
result = []
|
||||
for line in lines:
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import py
|
||||
import pytest
|
||||
from pkg_resources import parse_version
|
||||
|
||||
from xdist.looponfail import RemoteControl
|
||||
from xdist.looponfail import StatRecorder
|
||||
|
||||
@@ -214,7 +217,11 @@ class TestLooponFailing:
|
||||
assert "test_one" not in remotecontrol.failures[0]
|
||||
assert "test_two" in remotecontrol.failures[0]
|
||||
|
||||
@py.test.mark.xfail(py.test.__version__ >= "3.1", reason="broken by pytest 3.1+")
|
||||
@pytest.mark.xfail(
|
||||
parse_version(pytest.__version__) >= parse_version("3.1"),
|
||||
reason="broken by pytest 3.1+",
|
||||
strict=True,
|
||||
)
|
||||
def test_looponfail_removed_test(self, testdir):
|
||||
modcol = testdir.getmodulecol(
|
||||
"""
|
||||
|
||||
@@ -25,6 +25,10 @@ def test_dist_options(testdir):
|
||||
check_options(config)
|
||||
assert config.option.dist == "load"
|
||||
assert config.option.tx == ["popen"] * 2
|
||||
config = testdir.parseconfigure("--numprocesses", "3", "--maxprocesses", "2")
|
||||
check_options(config)
|
||||
assert config.option.dist == "load"
|
||||
assert config.option.tx == ["popen"] * 2
|
||||
config = testdir.parseconfigure("-d")
|
||||
check_options(config)
|
||||
assert config.option.dist == "load"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import py
|
||||
import pprint
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
from xdist.workermanage import WorkerController, unserialize_report
|
||||
from xdist.remote import serialize_report
|
||||
import execnet
|
||||
@@ -151,7 +153,6 @@ class TestReportSerialization:
|
||||
for i in range(len(a_entries)):
|
||||
assert isinstance(rep_entries[i], ReprEntry)
|
||||
assert rep_entries[i].lines == a_entries[i].lines
|
||||
assert rep_entries[i].localssep == a_entries[i].localssep
|
||||
assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
|
||||
assert (
|
||||
rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
|
||||
@@ -292,14 +293,11 @@ class TestWorkerInteractor:
|
||||
ev = worker.popevent("workerfinished")
|
||||
assert "workeroutput" in ev.kwargs
|
||||
|
||||
@pytest.mark.skipif(
|
||||
pytest.__version__ >= "3.0", reason="skip at module level illegal in pytest 3.0"
|
||||
)
|
||||
def test_remote_collect_skip(self, worker):
|
||||
worker.testdir.makepyfile(
|
||||
"""
|
||||
import py
|
||||
py.test.skip("hello")
|
||||
import pytest
|
||||
pytest.skip("hello", allow_module_level=True)
|
||||
"""
|
||||
)
|
||||
worker.setup()
|
||||
@@ -307,10 +305,9 @@ class TestWorkerInteractor:
|
||||
assert not ev.kwargs
|
||||
ev = worker.popevent()
|
||||
assert ev.name == "collectreport"
|
||||
ev = worker.popevent()
|
||||
assert ev.name == "collectreport"
|
||||
rep = unserialize_report(ev.name, ev.kwargs["data"])
|
||||
assert rep.skipped
|
||||
assert rep.longrepr[2] == "Skipped: hello"
|
||||
ev = worker.popevent("collectionfinish")
|
||||
assert not ev.kwargs["ids"]
|
||||
|
||||
@@ -400,3 +397,66 @@ def test_remote_env_vars(testdir):
|
||||
)
|
||||
result = testdir.runpytest("-n2", "--max-worker-restart=0")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_remote_inner_argv(testdir):
|
||||
"""Test/document the behavior due to execnet using `python -c`."""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import sys
|
||||
|
||||
def test_argv():
|
||||
assert sys.argv == ["-c"]
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("-n1")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_remote_mainargv(testdir):
|
||||
outer_argv = sys.argv
|
||||
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test_mainargv(request):
|
||||
assert request.config.workerinput["mainargv"] == {!r}
|
||||
""".format(
|
||||
outer_argv
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest("-n1")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_remote_usage_prog(testdir, request):
|
||||
if not hasattr(request.config._parser, "prog"):
|
||||
pytest.skip("prog not available in config parser")
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
config_parser = None
|
||||
|
||||
@pytest.fixture
|
||||
def get_config_parser():
|
||||
return config_parser
|
||||
|
||||
def pytest_configure(config):
|
||||
global config_parser
|
||||
config_parser = config._parser
|
||||
"""
|
||||
)
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import sys
|
||||
|
||||
def test(get_config_parser, request):
|
||||
get_config_parser._getparser().error("my_usage_error")
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest_subprocess("-n1")
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(
|
||||
["usage: pytest.py *", "pytest.py: error: my_usage_error"]
|
||||
)
|
||||
|
||||
23
tox.ini
23
tox.ini
@@ -1,33 +1,18 @@
|
||||
[tox]
|
||||
# if you change the envlist, please update .travis.yml file as well
|
||||
envlist=
|
||||
linting
|
||||
py{27,34,35,36}-pytest{30,31,32,33,36,38}
|
||||
py{27,36}-pytest36-pexpect
|
||||
py{27,34,35,36,37}-pytestlatest
|
||||
py{27,36}-pytest{master,features}
|
||||
|
||||
|
||||
[testenv]
|
||||
changedir=testing
|
||||
passenv = USER USERNAME
|
||||
deps =
|
||||
pycmd
|
||||
# to avoid .eggs
|
||||
setuptools_scm
|
||||
pytest30: pytest~=3.0.5
|
||||
pytest31: pytest~=3.1.0
|
||||
pytest32: pytest~=3.2.0
|
||||
pytest33: pytest~=3.3.0
|
||||
pytest36: pytest~=3.6.0
|
||||
pytest38: pytest~=3.8.0
|
||||
pytestlatest: pytest
|
||||
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
|
||||
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
|
||||
pexpect: pexpect
|
||||
platform=
|
||||
pexpect: linux|darwin
|
||||
filelock
|
||||
commands=
|
||||
# always clean to avoid code unmarshal mismatch on old python/pytest
|
||||
py.cleanup -aq
|
||||
pytest {posargs}
|
||||
|
||||
[testenv:linting]
|
||||
@@ -50,7 +35,7 @@ commands =
|
||||
towncrier --version {posargs} --yes
|
||||
|
||||
[pytest]
|
||||
addopts = -rsfxX
|
||||
addopts = -ra
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
|
||||
@@ -229,9 +229,10 @@ class DSession(object):
|
||||
if self.terminal and not self.sched.has_pending:
|
||||
self.trdist.ensure_show_status()
|
||||
self.terminal.write_line("")
|
||||
self.terminal.write_line(
|
||||
"scheduling tests via %s" % (self.sched.__class__.__name__)
|
||||
)
|
||||
if self.config.option.verbose > 0:
|
||||
self.terminal.write_line(
|
||||
"scheduling tests via %s" % (self.sched.__class__.__name__)
|
||||
)
|
||||
self.sched.schedule()
|
||||
|
||||
def worker_logstart(self, node, nodeid, location):
|
||||
@@ -259,10 +260,10 @@ class DSession(object):
|
||||
def worker_collectreport(self, node, rep):
|
||||
"""Emitted when a node calls the pytest_collectreport hook.
|
||||
|
||||
Because we only need the report when there's a failure, as optimization
|
||||
we only expect to receive failed reports from workers (#330).
|
||||
Because we only need the report when there's a failure/skip, as optimization
|
||||
we only expect to receive failed/skipped reports from workers (#330).
|
||||
"""
|
||||
assert rep.failed
|
||||
assert not rep.passed
|
||||
self._failed_worker_collectreport(node, rep)
|
||||
|
||||
def worker_logwarning(self, message, code, nodeid, fslocation):
|
||||
@@ -344,8 +345,11 @@ class TerminalDistReporter(object):
|
||||
self.rewrite(self.getstatus())
|
||||
|
||||
def getstatus(self):
|
||||
parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs]
|
||||
return " / ".join(parts)
|
||||
if self.config.option.verbose >= 0:
|
||||
parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs]
|
||||
return " / ".join(parts)
|
||||
else:
|
||||
return "bringing up nodes..."
|
||||
|
||||
def rewrite(self, line, newline=False):
|
||||
pline = line + " " * max(self._lastlen - len(line), 0)
|
||||
|
||||
@@ -32,7 +32,7 @@ def pytest_cmdline_main(config):
|
||||
if config.getoption("looponfail"):
|
||||
usepdb = config.getoption("usepdb") # a core option
|
||||
if usepdb:
|
||||
raise pytest.UsageError("--pdb incompatible with --looponfail.")
|
||||
raise pytest.UsageError("--pdb is incompatible with --looponfail.")
|
||||
looponfail_main(config)
|
||||
return 2 # looponfail only can get stop with ctrl-C anyway
|
||||
|
||||
|
||||
@@ -47,6 +47,14 @@ def pytest_addoption(parser):
|
||||
"you can use 'auto' here for auto detection CPUs number on "
|
||||
"host system",
|
||||
)
|
||||
group.addoption(
|
||||
"--maxprocesses",
|
||||
dest="maxprocesses",
|
||||
metavar="maxprocesses",
|
||||
action="store",
|
||||
type=int,
|
||||
help="limit the maximum number of workers to process the tests when using --numprocesses=auto",
|
||||
)
|
||||
group.addoption(
|
||||
"--max-worker-restart",
|
||||
"--max-slave-restart",
|
||||
@@ -118,12 +126,12 @@ def pytest_addoption(parser):
|
||||
)
|
||||
parser.addini(
|
||||
"rsyncdirs",
|
||||
"list of (relative) paths to be rsynced for" " remote distributed testing.",
|
||||
"list of (relative) paths to be rsynced for remote distributed testing.",
|
||||
type="pathlist",
|
||||
)
|
||||
parser.addini(
|
||||
"rsyncignore",
|
||||
"list of (relative) glob-style paths to be ignored " "for rsyncing.",
|
||||
"list of (relative) glob-style paths to be ignored for rsyncing.",
|
||||
type="pathlist",
|
||||
)
|
||||
parser.addini(
|
||||
@@ -172,7 +180,10 @@ def pytest_cmdline_main(config):
|
||||
if config.option.numprocesses:
|
||||
if config.option.dist == "no":
|
||||
config.option.dist = "load"
|
||||
config.option.tx = ["popen"] * config.option.numprocesses
|
||||
numprocesses = config.option.numprocesses
|
||||
if config.option.maxprocesses:
|
||||
numprocesses = min(numprocesses, config.option.maxprocesses)
|
||||
config.option.tx = ["popen"] * numprocesses
|
||||
if config.option.distload:
|
||||
config.option.dist = "load"
|
||||
val = config.getvalue
|
||||
|
||||
@@ -12,6 +12,7 @@ import time
|
||||
|
||||
import _pytest.hookspec
|
||||
import pytest
|
||||
from execnet.gateway_base import dumps, DumpError
|
||||
|
||||
|
||||
class WorkerInteractor(object):
|
||||
@@ -109,19 +110,24 @@ class WorkerInteractor(object):
|
||||
self.sendevent("testreport", data=data)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
# master only needs reports that failed, as optimization send only them instead (#330)
|
||||
if report.failed:
|
||||
# send only reports that have not passed to master as optimization (#330)
|
||||
if not report.passed:
|
||||
data = serialize_report(report)
|
||||
self.sendevent("collectreport", data=data)
|
||||
|
||||
def pytest_logwarning(self, message, code, nodeid, fslocation):
|
||||
self.sendevent(
|
||||
"logwarning",
|
||||
message=message,
|
||||
code=code,
|
||||
nodeid=nodeid,
|
||||
fslocation=str(fslocation),
|
||||
)
|
||||
# the pytest_logwarning hook was deprecated since pytest 4.0
|
||||
if hasattr(
|
||||
_pytest.hookspec, "pytest_logwarning"
|
||||
) and not _pytest.hookspec.pytest_logwarning.pytest_spec.get("warn_on_impl"):
|
||||
|
||||
def pytest_logwarning(self, message, code, nodeid, fslocation):
|
||||
self.sendevent(
|
||||
"logwarning",
|
||||
message=message,
|
||||
code=code,
|
||||
nodeid=nodeid,
|
||||
fslocation=str(fslocation),
|
||||
)
|
||||
|
||||
# the pytest_warning_captured hook was introduced in pytest 3.8
|
||||
if hasattr(_pytest.hookspec, "pytest_warning_captured"):
|
||||
@@ -181,8 +187,15 @@ def serialize_warning_message(warning_message):
|
||||
if isinstance(warning_message.message, Warning):
|
||||
message_module = type(warning_message.message).__module__
|
||||
message_class_name = type(warning_message.message).__name__
|
||||
message_args = warning_message.message.args
|
||||
message_str = None
|
||||
message_str = str(warning_message.message)
|
||||
# check now if we can serialize the warning arguments (#349)
|
||||
# if not, we will just use the exception message on the master node
|
||||
try:
|
||||
dumps(warning_message.message.args)
|
||||
except DumpError:
|
||||
message_args = None
|
||||
else:
|
||||
message_args = warning_message.message.args
|
||||
else:
|
||||
message_str = warning_message.message
|
||||
message_module = None
|
||||
@@ -207,7 +220,15 @@ def serialize_warning_message(warning_message):
|
||||
for attr_name in warning_message._WARNING_DETAILS:
|
||||
if attr_name in ("message", "category"):
|
||||
continue
|
||||
result[attr_name] = getattr(warning_message, attr_name)
|
||||
attr = getattr(warning_message, attr_name)
|
||||
# Check if we can serialize the warning detail, marking `None` otherwise
|
||||
# Note that we need to define the attr (even as `None`) to allow deserializing
|
||||
try:
|
||||
dumps(attr)
|
||||
except DumpError:
|
||||
result[attr_name] = repr(attr)
|
||||
else:
|
||||
result[attr_name] = attr
|
||||
return result
|
||||
|
||||
|
||||
@@ -234,24 +255,29 @@ def remote_initconfig(option_dict, args):
|
||||
config.option.dist = "no"
|
||||
config.option.distload = False
|
||||
config.option.numprocesses = None
|
||||
config.option.maxprocesses = None
|
||||
config.args = args
|
||||
return config
|
||||
|
||||
|
||||
if __name__ == "__channelexec__":
|
||||
channel = channel # noqa
|
||||
workerinput, args, option_dict = channel.receive()
|
||||
importpath = os.getcwd()
|
||||
sys.path.insert(0, importpath) # XXX only for remote situations
|
||||
os.environ["PYTHONPATH"] = (
|
||||
importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
|
||||
)
|
||||
os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
|
||||
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
|
||||
# os.environ['PYTHONPATH'] = importpath
|
||||
import py
|
||||
|
||||
channel = channel # noqa
|
||||
workerinput, args, option_dict, change_sys_path = channel.receive()
|
||||
|
||||
if change_sys_path:
|
||||
importpath = os.getcwd()
|
||||
sys.path.insert(0, importpath)
|
||||
os.environ["PYTHONPATH"] = (
|
||||
importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
|
||||
)
|
||||
|
||||
os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
|
||||
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
|
||||
|
||||
config = remote_initconfig(option_dict, args)
|
||||
config._parser.prog = os.path.basename(workerinput["mainargv"][0])
|
||||
config.workerinput = workerinput
|
||||
config.workeroutput = {}
|
||||
# TODO: deprecated name, backward compatibility only. Remove it in future
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from xdist.scheduler.each import EachScheduling # noqa
|
||||
from xdist.scheduler.load import LoadScheduling # noqa
|
||||
from xdist.scheduler.loadfile import LoadFileScheduling # noqa
|
||||
from xdist.scheduler.loadscope import LoadScopeScheduling # noqa
|
||||
from xdist.scheduler.filescope import LoadFileScheduling # noqa
|
||||
|
||||
@@ -126,6 +126,7 @@ class EachScheduling(object):
|
||||
if not pending:
|
||||
pending[:] = range(len(self.node2collection[node]))
|
||||
node.send_runtest_all()
|
||||
node.shutdown()
|
||||
else:
|
||||
node.send_runtest_some(pending)
|
||||
self._started.append(node)
|
||||
|
||||
@@ -178,6 +178,9 @@ class LoadScheduling(object):
|
||||
return
|
||||
num_send = items_per_node_max - len(node_pending)
|
||||
self._send_tests(node, num_send)
|
||||
else:
|
||||
node.shutdown()
|
||||
|
||||
self.log("num items waiting for node:", len(self.pending))
|
||||
|
||||
def remove_node(self, node):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from . import LoadScopeScheduling
|
||||
from .loadscope import LoadScopeScheduling
|
||||
from py.log import Producer
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ class LoadScopeScheduling(object):
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Unable to identify crashitem on a workload with " "pending items"
|
||||
"Unable to identify crashitem on a workload with pending items"
|
||||
)
|
||||
|
||||
# Made uncompleted work unit available again
|
||||
@@ -306,6 +306,7 @@ class LoadScopeScheduling(object):
|
||||
|
||||
# Check that more work is available
|
||||
if not self.workqueue:
|
||||
node.shutdown()
|
||||
return
|
||||
|
||||
self.log("Number of units waiting for node:", len(self.workqueue))
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import print_function
|
||||
import fnmatch
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import py
|
||||
@@ -213,6 +214,7 @@ class WorkerController(object):
|
||||
"workercount": len(nodemanager.specs),
|
||||
"slaveid": gateway.id,
|
||||
"slavecount": len(nodemanager.specs),
|
||||
"mainargv": sys.argv,
|
||||
}
|
||||
# TODO: deprecated name, backward compatibility only. Remove it in future
|
||||
self.slaveinput = self.workerinput
|
||||
@@ -243,7 +245,9 @@ class WorkerController(object):
|
||||
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.workerinput, args, option_dict))
|
||||
# change sys.path only for remote workers
|
||||
change_sys_path = not self.gateway.spec.popen
|
||||
self.channel.send((self.workerinput, args, option_dict, change_sys_path))
|
||||
if self.putevent:
|
||||
self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK)
|
||||
|
||||
@@ -268,7 +272,7 @@ class WorkerController(object):
|
||||
if not self._down:
|
||||
try:
|
||||
self.sendcommand("shutdown")
|
||||
except IOError:
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
self._shutdown_sent = True
|
||||
|
||||
@@ -345,7 +349,11 @@ class WorkerController(object):
|
||||
except: # noqa
|
||||
from _pytest._code import ExceptionInfo
|
||||
|
||||
excinfo = ExceptionInfo()
|
||||
# ExceptionInfo API changed in pytest 4.1
|
||||
if hasattr(ExceptionInfo, "from_current"):
|
||||
excinfo = ExceptionInfo.from_current()
|
||||
else:
|
||||
excinfo = ExceptionInfo()
|
||||
print("!" * 20, excinfo)
|
||||
self.config.notify_exception(excinfo)
|
||||
self.shutdown()
|
||||
@@ -426,7 +434,22 @@ def unserialize_warning_message(data):
|
||||
if data["message_module"]:
|
||||
mod = importlib.import_module(data["message_module"])
|
||||
cls = getattr(mod, data["message_class_name"])
|
||||
message = cls(*data["message_args"])
|
||||
message = None
|
||||
if data["message_args"] is not None:
|
||||
try:
|
||||
message = cls(*data["message_args"])
|
||||
except TypeError:
|
||||
pass
|
||||
if message is None:
|
||||
# could not recreate the original warning instance;
|
||||
# create a generic Warning instance with the original
|
||||
# message at least
|
||||
message_text = "{mod}.{cls}: {msg}".format(
|
||||
mod=data["message_module"],
|
||||
cls=data["message_class_name"],
|
||||
msg=data["message_str"],
|
||||
)
|
||||
message = Warning(message_text)
|
||||
else:
|
||||
message = data["message_str"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user