From 81e38981ca64b55ff3cb540da12fb65ebf2518aa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 24 Sep 2018 14:58:13 -0300 Subject: [PATCH] Fix warnings transfer between workers and master node with pytest >= 3.8 Fix #341 --- .travis.yml | 1 + appveyor.yml | 1 + changelog/341.bugfix.rst | 1 + testing/acceptance_test.py | 6 +++-- tox.ini | 3 ++- xdist/dsession.py | 5 +++++ xdist/remote.py | 46 ++++++++++++++++++++++++++++++++++++++ xdist/workermanage.py | 37 ++++++++++++++++++++++++++++++ 8 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 changelog/341.bugfix.rst diff --git a/.travis.yml b/.travis.yml index 8952a4ed..1a5b5311 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ env: - TOXENV=py-pytest32 - TOXENV=py-pytest33 - TOXENV=py-pytest36 +- TOXENV=py-pytest38 install: pip install tox setuptools_scm script: tox diff --git a/appveyor.yml b/appveyor.yml index bc785971..fef4d068 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,7 @@ environment: - TOXENV: "py35-pytest33" - TOXENV: "py36-pytest33" - TOXENV: "py36-pytest36" + - TOXENV: "py36-pytest38" - TOXENV: "py27-pytest33-pexpect" - TOXENV: "py36-pytest33-pexpect" diff --git a/changelog/341.bugfix.rst b/changelog/341.bugfix.rst new file mode 100644 index 00000000..9e14585a --- /dev/null +++ b/changelog/341.bugfix.rst @@ -0,0 +1 @@ +Fix warnings transfer between workers and master node with pytest >= 3.8. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index e9864295..dcf717dd 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -402,7 +402,7 @@ def test_func(): @pytest.mark.parametrize("n", ["-n0", "-n1"]) @pytest.mark.parametrize("warn_type", ["pytest", "builtin"]) - def test_logwarning(self, testdir, n, warn_type): + def test_warnings(self, testdir, n, warn_type): from pkg_resources import parse_version if parse_version(pytest.__version__) < parse_version("3.1"): @@ -417,7 +417,9 @@ def test_logwarning(self, testdir, n, warn_type): assert False testdir.makepyfile( """ - import warnings, py + import warnings, py, pytest + + @pytest.mark.filterwarnings('ignore:config.warn has been deprecated') def test_func(request): {warn_code} """.format( diff --git a/tox.ini b/tox.ini index 11afb372..ffe74249 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} + py{27,34,35,36}-pytest{30,31,32,33,36,38} py{27,36}-pytest36-pexpect py{27,36}-pytest{master,features} @@ -19,6 +19,7 @@ deps = pytest32: pytest~=3.2.0 pytest33: pytest~=3.3.0 pytest36: pytest~=3.6.0 + pytest38: pytest~=3.8.0 pytestmaster: git+https://github.com/pytest-dev/pytest.git@master pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features pexpect: pexpect diff --git a/xdist/dsession.py b/xdist/dsession.py index 54925cfd..bc960d3f 100644 --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -270,6 +270,11 @@ def worker_logwarning(self, message, code, nodeid, fslocation): kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation) 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): """Return new node based on an existing one. diff --git a/xdist/remote.py b/xdist/remote.py index 74ac92ec..5d21039c 100644 --- a/xdist/remote.py +++ b/xdist/remote.py @@ -123,6 +123,18 @@ def pytest_logwarning(self, message, code, nodeid, 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, + # XXX what to do with "item" parameter? It can't be serialized + item=None, + ) + def serialize_report(rep): def disassembled_report(rep): @@ -165,6 +177,40 @@ def disassembled_report(rep): 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_args = warning_message.message.args + message_str = None + 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(): import platform diff --git a/xdist/workermanage.py b/xdist/workermanage.py index 54b6e2dc..b607d4e8 100644 --- a/xdist/workermanage.py +++ b/xdist/workermanage.py @@ -327,6 +327,16 @@ def process_from_remote(self, eventcall): # noqa too complex nodeid=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: raise ValueError("unknown event: %s" % (eventname,)) except KeyboardInterrupt: @@ -409,6 +419,33 @@ def 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 = cls(*data["message_args"]) + 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): from pprint import pprint