Skip to content

Commit a518bd4

Browse files
Merge pull request #362 from nicoddemus/serialize-fail-349
Handle warnings with unserializable arguments
2 parents adc3852 + 673fe9b commit a518bd4

File tree

7 files changed

+108
-75
lines changed

7 files changed

+108
-75
lines changed

.travis.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,26 @@ env:
2121
- TOXENV=py-pytest31
2222
- TOXENV=py-pytest32
2323
- TOXENV=py-pytest33
24-
- TOXENV=py-pytest36
25-
- TOXENV=py-pytest38
2624

2725
install: pip install tox setuptools_scm
2826
script: tox
2927

3028
stages:
31-
- linting
29+
- baseline
3230
- test
3331
- name: deploy
3432
if: repo = pytest-dev/pytest-xdist AND tag IS present
3533

3634
jobs:
3735
include:
38-
- stage: linting
36+
- stage: baseline
3937
python: '3.6'
40-
script:
41-
- tox -e linting
38+
env: TOXENV=linting
39+
- python: '3.6'
40+
env: TOXENV=py36-pytestlatest
41+
- python: '2.7'
42+
env: TOXENV=py27-pytestlatest
43+
4244
- stage: test
4345
# python x env above are already included into this stage
4446
- python: "2.7"

appveyor.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ environment:
55
- TOXENV: "py34-pytest33"
66
- TOXENV: "py35-pytest33"
77
- TOXENV: "py36-pytest33"
8-
- TOXENV: "py36-pytest36"
9-
- TOXENV: "py36-pytest38"
8+
- TOXENV: "py36-pytestlatest"
109
- TOXENV: "py27-pytest33-pexpect"
1110
- TOXENV: "py36-pytest33-pexpect"
1211

changelog/349.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Correctly handle warnings created with arguments that can't be serialized during the transfer from workers to master node.

testing/acceptance_test.py

Lines changed: 77 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -400,64 +400,6 @@ def test_func():
400400
]
401401
)
402402

403-
@pytest.mark.parametrize("n", ["-n0", "-n1"])
404-
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
405-
def test_warnings(self, testdir, n, warn_type):
406-
from pkg_resources import parse_version
407-
408-
if parse_version(pytest.__version__) < parse_version("3.1"):
409-
pytest.skip("pytest warnings requires >= 3.1")
410-
411-
if warn_type == "builtin":
412-
warn_code = """warnings.warn(UserWarning('this is a warning'))"""
413-
elif warn_type == "pytest":
414-
warn_code = """request.config.warn('', 'this is a warning',
415-
fslocation=py.path.local())"""
416-
else:
417-
assert False
418-
testdir.makepyfile(
419-
"""
420-
import warnings, py, pytest
421-
422-
@pytest.mark.filterwarnings('ignore:config.warn has been deprecated')
423-
def test_func(request):
424-
{warn_code}
425-
""".format(
426-
warn_code=warn_code
427-
)
428-
)
429-
result = testdir.runpytest(n)
430-
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"])
431-
432-
@pytest.mark.parametrize("n", ["-n0", "-n1"])
433-
def test_custom_subclass(self, testdir, n):
434-
"""Check that warning subclasses that don't honor the args attribute don't break
435-
pytest-xdist (#344)
436-
"""
437-
from pkg_resources import parse_version
438-
439-
if parse_version(pytest.__version__) < parse_version("3.1"):
440-
pytest.skip("pytest warnings requires >= 3.1")
441-
442-
testdir.makepyfile(
443-
"""
444-
import warnings, py, pytest
445-
446-
class MyWarning(UserWarning):
447-
448-
def __init__(self, p1, p2):
449-
self.p1 = p1
450-
self.p2 = p2
451-
self.args = ()
452-
453-
def test_func(request):
454-
warnings.warn(MyWarning("foo", 1))
455-
"""
456-
)
457-
testdir.syspathinsert()
458-
result = testdir.runpytest(n)
459-
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warnings*"])
460-
461403
def test_logfinish_hook(self, testdir):
462404
"""Ensure the pytest_runtest_logfinish hook is being properly handled"""
463405
from _pytest import hookspec
@@ -765,6 +707,83 @@ def test_ok():
765707
result.stdout.fnmatch_lines("*1 passed*")
766708

767709

710+
class TestWarnings:
711+
@pytest.fixture(autouse=True)
712+
def skip_if_unsupported_pytest_version(self):
713+
"""Skip tests of this class if we are running in a pytest version which does not
714+
support warnings yet.
715+
"""
716+
from pkg_resources import parse_version
717+
718+
if parse_version(pytest.__version__) < parse_version("3.1"):
719+
pytest.skip("pytest warnings requires >= 3.1")
720+
721+
@pytest.mark.parametrize("n", ["-n0", "-n1"])
722+
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
723+
def test_warnings(self, testdir, n, warn_type):
724+
if warn_type == "builtin":
725+
warn_code = """warnings.warn(UserWarning('this is a warning'))"""
726+
elif warn_type == "pytest":
727+
warn_code = """request.config.warn('', 'this is a warning',
728+
fslocation=py.path.local())"""
729+
else:
730+
assert False
731+
testdir.makepyfile(
732+
"""
733+
import warnings, py, pytest
734+
735+
@pytest.mark.filterwarnings('ignore:config.warn has been deprecated')
736+
def test_func(request):
737+
{warn_code}
738+
""".format(
739+
warn_code=warn_code
740+
)
741+
)
742+
result = testdir.runpytest(n)
743+
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"])
744+
745+
@pytest.mark.parametrize("n", ["-n0", "-n1"])
746+
def test_custom_subclass(self, testdir, n):
747+
"""Check that warning subclasses that don't honor the args attribute don't break
748+
pytest-xdist (#344)
749+
"""
750+
testdir.makepyfile(
751+
"""
752+
import warnings, py, pytest
753+
754+
class MyWarning(UserWarning):
755+
756+
def __init__(self, p1, p2):
757+
self.p1 = p1
758+
self.p2 = p2
759+
self.args = ()
760+
761+
def test_func(request):
762+
warnings.warn(MyWarning("foo", 1))
763+
"""
764+
)
765+
testdir.syspathinsert()
766+
result = testdir.runpytest(n)
767+
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warnings*"])
768+
769+
@pytest.mark.parametrize("n", ["-n0", "-n1"])
770+
def test_unserializable_arguments(self, testdir, n):
771+
"""Check that warnings with unserializable arguments are handled correctly (#349)."""
772+
testdir.makepyfile(
773+
"""
774+
import warnings, pytest
775+
776+
def test_func(tmpdir):
777+
fn = (tmpdir / 'foo.txt').ensure(file=1)
778+
with fn.open('r') as f:
779+
warnings.warn(UserWarning("foo", f))
780+
"""
781+
)
782+
testdir.syspathinsert()
783+
result = testdir.runpytest(n)
784+
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warnings*"])
785+
786+
768787
class TestNodeFailure:
769788
def test_load_single(self, testdir):
770789
f = testdir.makepyfile(

tox.ini

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# if you change the envlist, please update .travis.yml file as well
33
envlist=
44
linting
5-
py{27,34,35,36}-pytest{30,31,32,33,36,38}
5+
py{27,34,35,36}-pytest{30,31,32,33,latest}
66
py{27,36}-pytest36-pexpect
77
py{27,36}-pytest{master,features}
88

@@ -18,17 +18,14 @@ deps =
1818
pytest31: pytest~=3.1.0
1919
pytest32: pytest~=3.2.0
2020
pytest33: pytest~=3.3.0
21-
pytest36: pytest~=3.6.0
22-
pytest38: pytest~=3.8.0
21+
pytestlatest: pytest
2322
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
2423
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
2524
pexpect: pexpect
2625
filelock
2726
platform=
2827
pexpect: linux|darwin
2928
commands=
30-
# always clean to avoid code unmarshal mismatch on old python/pytest
31-
py.cleanup -aq
3229
pytest {posargs}
3330

3431
[testenv:linting]

xdist/remote.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import _pytest.hookspec
1414
import pytest
15+
from execnet.gateway_base import dumps, DumpError
1516

1617

1718
class WorkerInteractor(object):
@@ -181,8 +182,15 @@ def serialize_warning_message(warning_message):
181182
if isinstance(warning_message.message, Warning):
182183
message_module = type(warning_message.message).__module__
183184
message_class_name = type(warning_message.message).__name__
184-
message_args = warning_message.message.args
185185
message_str = str(warning_message.message)
186+
# check now if we can serialize the warning arguments (#349)
187+
# if not, we will just use the exception message on the master node
188+
try:
189+
dumps(warning_message.message.args)
190+
except DumpError:
191+
message_args = None
192+
else:
193+
message_args = warning_message.message.args
186194
else:
187195
message_str = warning_message.message
188196
message_module = None

xdist/workermanage.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,9 +426,16 @@ def unserialize_warning_message(data):
426426
if data["message_module"]:
427427
mod = importlib.import_module(data["message_module"])
428428
cls = getattr(mod, data["message_class_name"])
429-
try:
430-
message = cls(*data["message_args"])
431-
except TypeError:
429+
message = None
430+
if data["message_args"] is not None:
431+
try:
432+
message = cls(*data["message_args"])
433+
except TypeError:
434+
pass
435+
if message is None:
436+
# could not recreate the original warning instance;
437+
# create a generic Warning instance with the original
438+
# message at least
432439
message_text = "{mod}.{cls}: {msg}".format(
433440
mod=data["message_module"],
434441
cls=data["message_class_name"],

0 commit comments

Comments
 (0)