diff --git a/.travis.yml b/.travis.yml index 1a5b531..4d6c3d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,24 +21,26 @@ env: - TOXENV=py-pytest31 - TOXENV=py-pytest32 - TOXENV=py-pytest33 -- TOXENV=py-pytest36 -- TOXENV=py-pytest38 install: 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: "2.7" diff --git a/appveyor.yml b/appveyor.yml index fef4d06..290ac16 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,7 @@ environment: - TOXENV: "py34-pytest33" - TOXENV: "py35-pytest33" - TOXENV: "py36-pytest33" - - TOXENV: "py36-pytest36" - - TOXENV: "py36-pytest38" + - TOXENV: "py36-pytestlatest" - TOXENV: "py27-pytest33-pexpect" - TOXENV: "py36-pytest33-pexpect" diff --git a/changelog/349.bugfix.rst b/changelog/349.bugfix.rst new file mode 100644 index 0000000..af0fa43 --- /dev/null +++ b/changelog/349.bugfix.rst @@ -0,0 +1 @@ +Correctly handle warnings created with arguments that can't be serialized during the transfer from workers to master node. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 93d8dd6..deb3c1d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -400,64 +400,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*"]) - - @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) - """ - from pkg_resources import parse_version - - if parse_version(pytest.__version__) < parse_version("3.1"): - pytest.skip("pytest warnings requires >= 3.1") - - 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*"]) - def test_logfinish_hook(self, testdir): """Ensure the pytest_runtest_logfinish hook is being properly handled""" from _pytest import hookspec @@ -765,6 +707,83 @@ def test_sub_plugins_disabled(testdir, plugin): 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: def test_load_single(self, testdir): f = testdir.makepyfile( diff --git a/tox.ini b/tox.ini index ebafc13..c3ba7b6 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # 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,34,35,36}-pytest{30,31,32,33,latest} py{27,36}-pytest36-pexpect py{27,36}-pytest{master,features} @@ -18,8 +18,7 @@ deps = 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 @@ -27,8 +26,6 @@ deps = platform= pexpect: linux|darwin commands= - # always clean to avoid code unmarshal mismatch on old python/pytest - py.cleanup -aq pytest {posargs} [testenv:linting] diff --git a/xdist/remote.py b/xdist/remote.py index 0c5aa30..66b270c 100644 --- a/xdist/remote.py +++ b/xdist/remote.py @@ -12,6 +12,7 @@ import time import _pytest.hookspec import pytest +from execnet.gateway_base import dumps, DumpError class WorkerInteractor(object): @@ -181,8 +182,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 = 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 diff --git a/xdist/workermanage.py b/xdist/workermanage.py index 9eb6418..3b35c14 100644 --- a/xdist/workermanage.py +++ b/xdist/workermanage.py @@ -426,9 +426,16 @@ def unserialize_warning_message(data): if data["message_module"]: mod = importlib.import_module(data["message_module"]) cls = getattr(mod, data["message_class_name"]) - try: - message = cls(*data["message_args"]) - except TypeError: + 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"],