diff --git a/changelog/379.bugfix.rst b/changelog/379.bugfix.rst new file mode 100644 index 0000000..d569ce1 --- /dev/null +++ b/changelog/379.bugfix.rst @@ -0,0 +1,2 @@ +Warning attributes are checked to make sure they can be dumped prior to +serializing the warning for submission to the master node. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 44b2efd..e0d4770 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,5 +1,6 @@ import os import re +import sys import textwrap import py @@ -803,6 +804,39 @@ class TestWarnings: 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): diff --git a/xdist/remote.py b/xdist/remote.py index 58744d0..1cde135 100644 --- a/xdist/remote.py +++ b/xdist/remote.py @@ -218,7 +218,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