Compare commits

...

32 Commits

Author SHA1 Message Date
Bruno Oliveira
3579bfce7d Release 1.21.1 2018-11-09 14:29:21 -02:00
Ronny Pfannschmidt
365499eeaf Merge pull request #368 from nicoddemus/rename-filescope-file
Rename filescope to loadfile
2018-11-09 17:20:35 +01:00
Ronny Pfannschmidt
a518bd4e2a Merge pull request #362 from nicoddemus/serialize-fail-349
Handle warnings with unserializable arguments
2018-11-09 17:17:59 +01:00
Bruno Oliveira
d84ae73775 Rename filescope to loadfile
This makes it consistent with the option (--dist=loadfile) and the other
scheduling file (loadscope.py)
2018-11-09 13:00:40 -02:00
Bruno Oliveira
673fe9bd67 Create 'baseline' stage for fast-failure of builds, similar to pytest 2018-10-30 09:44:13 -03:00
Bruno Oliveira
9217b80fb1 Update CI to test against latest pytest release 2018-10-30 09:44:13 -03:00
Bruno Oliveira
b151e56fbd Handle unserializable warning arguments
Fix #349
2018-10-30 09:44:13 -03:00
Bruno Oliveira
70688b7986 Refactor warning acceptance tests into a class 2018-10-29 18:38:01 -03:00
Bruno Oliveira
adc385253c Merge pull request #357 from nicoddemus/release-1.24.0
Release 1.24.0
2018-10-29 18:23:37 -03:00
Bruno Oliveira
bfd6029102 Release 1.24.0 2018-10-18 07:55:54 -03:00
Bruno Oliveira
3f5a8eeb7f Merge pull request #356 from pristupa/maxprocessing-option
Add --maxprocesses option to use with --numprocesses=auto
2018-10-18 07:54:02 -03:00
Bruno Oliveira
d5b61e72d4 Merge pull request #351 from cybojanek/master
Issue node shutdown on empty workload
2018-10-18 07:53:52 -03:00
Bruno Oliveira
f224c4b00e Fix CHANGELOG formatting 2018-10-17 09:19:48 -03:00
Pavel Pristupa
ddefd3f453 Call group.addoption() instead of _addoption() when parsing command-line arguments
Fix #337
2018-10-17 17:48:47 +07:00
Pavel Pristupa
5ce8a6ec1a Add maxprocesses option to limit the maximum number of workers when using numprocesses=auto
Fix #337
2018-10-17 17:08:56 +07:00
Jan Kasiak
fe06f27b8b Issue node shutdown on empty workload
Remote pytest_runtestloop requires at least 2 tests, or a test
and a shutdown command. In case of inter test locking, for
example using a file lock, the tests could deadlock, since the
shutdown command would not be enqueued, and the last test of a
worker would never finish, and would not allow another worker
to make progress.
2018-09-30 15:39:04 -04:00
Bruno Oliveira
86e2fb5a5a Merge pull request #347 from nicoddemus/release-1.23.2
Release 1.23.2
2018-09-28 07:34:43 -03:00
Bruno Oliveira
ec6129639e Release 1.23.2 2018-09-28 06:38:50 -03:00
Bruno Oliveira
f6ac209cd5 Merge pull request #345 from BrandonHoffman/master
add exception handeling for warning serialization issues
2018-09-28 06:38:14 -03:00
Bruno Oliveira
b176b37606 Add a test which reproduces #344 and fix the handling code
Fix #344
2018-09-27 22:18:52 -03:00
Brandon Hoffman
22e36c9cb4 run black formatter 2018-09-27 19:32:10 -05:00
Brandon Hoffman
8825e7e234 use module name and class name for method instead of the reference to them 2018-09-27 19:27:49 -05:00
Brandon Hoffman
5d5f87b600 add moduel and class name to warning text 2018-09-27 19:24:58 -05:00
Brandon Hoffman
571d08fb8b remove unneeded lines 2018-09-26 21:05:30 -05:00
Brandon Hoffman
36c28ee154 add test case for invalid warning handeling 2018-09-26 20:44:01 -05:00
Brandon Hoffman
af3e975664 add changelog 2018-09-26 19:46:28 -05:00
Brandon Hoffman
bc2ba55f70 add exception handeling for warning serialization issues 2018-09-26 19:32:13 -05:00
Bruno Oliveira
8475876ce3 Merge pull request #343 from nicoddemus/release-1.23.1
Release 1.23.1
2018-09-25 17:05:40 -03:00
Bruno Oliveira
8ee6c0a4e6 Release 1.23.1 2018-09-25 12:11:08 -03:00
Bruno Oliveira
ec966d9800 Merge pull request #342 from nicoddemus/3.8-warnings
Fix warnings transfer between workers and master node with pytest >= 3.8
2018-09-25 12:10:10 -03:00
Bruno Oliveira
78030e3d32 Fix warnings transfer between workers and master node with pytest >= 3.8
Fix #341
2018-09-25 10:01:20 -03:00
Bruno Oliveira
ed2ab76b5c Merge pull request #334 from nicoddemus/release-1.23.0
Release 1.23.0
2018-08-25 01:49:04 -03:00
15 changed files with 316 additions and 40 deletions

View File

@@ -21,23 +21,26 @@ env:
- TOXENV=py-pytest31 - TOXENV=py-pytest31
- TOXENV=py-pytest32 - TOXENV=py-pytest32
- TOXENV=py-pytest33 - TOXENV=py-pytest33
- TOXENV=py-pytest36
install: pip install tox setuptools_scm install: pip install tox setuptools_scm
script: tox script: tox
stages: stages:
- linting - baseline
- test - test
- name: deploy - name: deploy
if: repo = pytest-dev/pytest-xdist AND tag IS present if: repo = pytest-dev/pytest-xdist AND tag IS present
jobs: jobs:
include: include:
- stage: linting - stage: baseline
python: '3.6' python: '3.6'
script: env: TOXENV=linting
- tox -e linting - python: '3.6'
env: TOXENV=py36-pytestlatest
- python: '2.7'
env: TOXENV=py27-pytestlatest
- 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" - python: "2.7"

View File

@@ -1,3 +1,45 @@
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)
================================
Bug Fixes
---------
- `#341 <https://github.com/pytest-dev/pytest-xdist/issues/341>`_: Fix warnings transfer between workers and master node with pytest >= 3.8.
pytest-xdist 1.23.0 (2018-08-23) pytest-xdist 1.23.0 (2018-08-23)
================================ ================================

View File

@@ -5,7 +5,7 @@ environment:
- TOXENV: "py34-pytest33" - TOXENV: "py34-pytest33"
- TOXENV: "py35-pytest33" - TOXENV: "py35-pytest33"
- TOXENV: "py36-pytest33" - TOXENV: "py36-pytest33"
- TOXENV: "py36-pytest36" - TOXENV: "py36-pytestlatest"
- TOXENV: "py27-pytest33-pexpect" - TOXENV: "py27-pytest33-pexpect"
- TOXENV: "py36-pytest33-pexpect" - TOXENV: "py36-pytest33-pexpect"

View File

@@ -400,33 +400,6 @@ class TestTerminalReporting:
] ]
) )
@pytest.mark.parametrize("n", ["-n0", "-n1"])
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
def test_logwarning(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
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): def test_logfinish_hook(self, testdir):
"""Ensure the pytest_runtest_logfinish hook is being properly handled""" """Ensure the pytest_runtest_logfinish hook is being properly handled"""
from _pytest import hookspec from _pytest import hookspec
@@ -734,6 +707,83 @@ def test_sub_plugins_disabled(testdir, plugin):
result.stdout.fnmatch_lines("*1 passed*") result.stdout.fnmatch_lines("*1 passed*")
class TestWarnings:
@pytest.fixture(autouse=True)
def skip_if_unsupported_pytest_version(self):
"""Skip tests of this class if we are running in a pytest version which does not
support warnings yet.
"""
from pkg_resources import parse_version
if parse_version(pytest.__version__) < parse_version("3.1"):
pytest.skip("pytest warnings requires >= 3.1")
@pytest.mark.parametrize("n", ["-n0", "-n1"])
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
def test_warnings(self, testdir, n, warn_type):
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*"])
@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*"])
class TestNodeFailure: class TestNodeFailure:
def test_load_single(self, testdir): def test_load_single(self, testdir):
f = testdir.makepyfile( f = testdir.makepyfile(
@@ -1101,6 +1151,56 @@ class TestFileScope:
assert c1 == c2 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): def parse_tests_and_workers_from_output(lines):
result = [] result = []
for line in lines: for line in lines:

View File

@@ -25,6 +25,10 @@ def test_dist_options(testdir):
check_options(config) 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("--numprocesses", "3", "--maxprocesses", "2")
check_options(config)
assert config.option.dist == "load"
assert config.option.tx == ["popen"] * 2
config = testdir.parseconfigure("-d") config = testdir.parseconfigure("-d")
check_options(config) check_options(config)
assert config.option.dist == "load" assert config.option.dist == "load"

View File

@@ -2,7 +2,7 @@
# 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=
linting linting
py{27,34,35,36}-pytest{30,31,32,33,36} py{27,34,35,36}-pytest{30,31,32,33,latest}
py{27,36}-pytest36-pexpect py{27,36}-pytest36-pexpect
py{27,36}-pytest{master,features} py{27,36}-pytest{master,features}
@@ -18,15 +18,14 @@ deps =
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 pytest33: pytest~=3.3.0
pytest36: pytest~=3.6.0 pytestlatest: pytest
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
pexpect: pexpect pexpect: pexpect
filelock
platform= platform=
pexpect: linux|darwin pexpect: linux|darwin
commands= commands=
# always clean to avoid code unmarshal mismatch on old python/pytest
py.cleanup -aq
pytest {posargs} pytest {posargs}
[testenv:linting] [testenv:linting]

View File

@@ -270,6 +270,11 @@ class DSession(object):
kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation) kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation)
self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs) self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs)
def worker_warning_captured(self, warning_message, when, item):
"""Emitted when a node calls the pytest_logwarning hook."""
kwargs = dict(warning_message=warning_message, when=when, item=item)
self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs)
def _clone_node(self, node): def _clone_node(self, node):
"""Return new node based on an existing one. """Return new node based on an existing one.

View File

@@ -47,6 +47,14 @@ def pytest_addoption(parser):
"you can use 'auto' here for auto detection CPUs number on " "you can use 'auto' here for auto detection CPUs number on "
"host system", "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( group.addoption(
"--max-worker-restart", "--max-worker-restart",
"--max-slave-restart", "--max-slave-restart",
@@ -172,7 +180,10 @@ def pytest_cmdline_main(config):
if config.option.numprocesses: if config.option.numprocesses:
if config.option.dist == "no": if config.option.dist == "no":
config.option.dist = "load" 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: if config.option.distload:
config.option.dist = "load" config.option.dist = "load"
val = config.getvalue val = config.getvalue

View File

@@ -12,6 +12,7 @@ import time
import _pytest.hookspec import _pytest.hookspec
import pytest import pytest
from execnet.gateway_base import dumps, DumpError
class WorkerInteractor(object): class WorkerInteractor(object):
@@ -123,6 +124,18 @@ class WorkerInteractor(object):
fslocation=str(fslocation), fslocation=str(fslocation),
) )
# the pytest_warning_captured hook was introduced in pytest 3.8
if hasattr(_pytest.hookspec, "pytest_warning_captured"):
def pytest_warning_captured(self, warning_message, when, item):
self.sendevent(
"warning_captured",
warning_message_data=serialize_warning_message(warning_message),
when=when,
# item cannot be serialized and will always be None when used with xdist
item=None,
)
def serialize_report(rep): def serialize_report(rep):
def disassembled_report(rep): def disassembled_report(rep):
@@ -165,6 +178,47 @@ def serialize_report(rep):
return d return d
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_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
message_class_name = None
message_args = None
if warning_message.category:
category_module = warning_message.category.__module__
category_class_name = warning_message.category.__name__
else:
category_module = None
category_class_name = None
result = {
"message_str": message_str,
"message_module": message_module,
"message_class_name": message_class_name,
"message_args": message_args,
"category_module": category_module,
"category_class_name": category_class_name,
}
# access private _WARNING_DETAILS because the attributes vary between Python versions
for attr_name in warning_message._WARNING_DETAILS:
if attr_name in ("message", "category"):
continue
result[attr_name] = getattr(warning_message, attr_name)
return result
def getinfodict(): def getinfodict():
import platform import platform
@@ -188,6 +242,7 @@ def remote_initconfig(option_dict, args):
config.option.dist = "no" config.option.dist = "no"
config.option.distload = False config.option.distload = False
config.option.numprocesses = None config.option.numprocesses = None
config.option.maxprocesses = None
config.args = args config.args = args
return config return config

View File

@@ -1,4 +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.loadfile import LoadFileScheduling # noqa
from xdist.scheduler.loadscope import LoadScopeScheduling # noqa from xdist.scheduler.loadscope import LoadScopeScheduling # noqa
from xdist.scheduler.filescope import LoadFileScheduling # noqa

View File

@@ -126,6 +126,7 @@ class EachScheduling(object):
if not pending: if not pending:
pending[:] = range(len(self.node2collection[node])) pending[:] = range(len(self.node2collection[node]))
node.send_runtest_all() node.send_runtest_all()
node.shutdown()
else: else:
node.send_runtest_some(pending) node.send_runtest_some(pending)
self._started.append(node) self._started.append(node)

View File

@@ -178,6 +178,9 @@ class LoadScheduling(object):
return return
num_send = items_per_node_max - len(node_pending) num_send = items_per_node_max - len(node_pending)
self._send_tests(node, num_send) self._send_tests(node, num_send)
else:
node.shutdown()
self.log("num items waiting for node:", len(self.pending)) self.log("num items waiting for node:", len(self.pending))
def remove_node(self, node): def remove_node(self, node):

View File

@@ -1,4 +1,4 @@
from . import LoadScopeScheduling from .loadscope import LoadScopeScheduling
from py.log import Producer from py.log import Producer

View File

@@ -306,6 +306,7 @@ class LoadScopeScheduling(object):
# Check that more work is available # Check that more work is available
if not self.workqueue: if not self.workqueue:
node.shutdown()
return return
self.log("Number of units waiting for node:", len(self.workqueue)) self.log("Number of units waiting for node:", len(self.workqueue))

View File

@@ -327,6 +327,16 @@ class WorkerController(object):
nodeid=kwargs["nodeid"], nodeid=kwargs["nodeid"],
fslocation=kwargs["nodeid"], fslocation=kwargs["nodeid"],
) )
elif eventname == "warning_captured":
warning_message = unserialize_warning_message(
kwargs["warning_message_data"]
)
self.notify_inproc(
eventname,
warning_message=warning_message,
when=kwargs["when"],
item=kwargs["item"],
)
else: else:
raise ValueError("unknown event: %s" % (eventname,)) raise ValueError("unknown event: %s" % (eventname,))
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -409,6 +419,48 @@ def unserialize_report(name, reportdict):
return runner.CollectReport(**assembled_report(reportdict)) return runner.CollectReport(**assembled_report(reportdict))
def unserialize_warning_message(data):
import warnings
import importlib
if data["message_module"]:
mod = importlib.import_module(data["message_module"])
cls = getattr(mod, data["message_class_name"])
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"]
if data["category_module"]:
mod = importlib.import_module(data["category_module"])
category = getattr(mod, data["category_class_name"])
else:
category = None
kwargs = {"message": message, "category": category}
# access private _WARNING_DETAILS because the attributes vary between Python versions
for attr_name in warnings.WarningMessage._WARNING_DETAILS:
if attr_name in ("message", "category"):
continue
kwargs[attr_name] = data[attr_name]
return warnings.WarningMessage(**kwargs)
def report_unserialization_failure(type_name, report_name, reportdict): def report_unserialization_failure(type_name, report_name, reportdict):
from pprint import pprint from pprint import pprint