From 89d07a5cf22ba51622678ca02ca2042b21f88d20 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Mar 2019 20:27:25 -0300 Subject: [PATCH 1/2] Use serialization hooks from pytest 4.4 --- changelog/426.feature.rst | 5 + setup.py | 2 +- testing/test_remote.py | 207 ++++---------------------------------- xdist/remote.py | 49 ++------- xdist/workermanage.py | 87 +--------------- 5 files changed, 33 insertions(+), 317 deletions(-) create mode 100644 changelog/426.feature.rst diff --git a/changelog/426.feature.rst b/changelog/426.feature.rst new file mode 100644 index 0000000..ad85749 --- /dev/null +++ b/changelog/426.feature.rst @@ -0,0 +1,5 @@ +``pytest-xdist`` now uses the new ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` +from ``pytest 4.4`` (still experimental). This will make report serialization more reliable and +extensible. + +This also means that ``pytest-xdist`` now also requires ``pytest>=4.4``. diff --git a/setup.py b/setup.py index 03ee47a..17abe9a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -install_requires = ["execnet>=1.1", "pytest>=3.6.0", "pytest-forked", "six"] +install_requires = ["execnet>=1.1", "pytest>=4.4.0", "pytest-forked", "six"] with open("README.rst") as f: diff --git a/testing/test_remote.py b/testing/test_remote.py index c134a9e..c9c4e30 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -3,8 +3,7 @@ import pprint import pytest import sys -from xdist.workermanage import WorkerController, unserialize_report -from xdist.remote import serialize_report +from xdist.workermanage import WorkerController import execnet import marshal @@ -81,187 +80,17 @@ def test_remoteinitconfig(testdir): assert config2.pluginmanager.getplugin("terminal") in (-1, None) -class TestReportSerialization: - def test_xdist_longrepr_to_str_issue_241(self, testdir): - testdir.makepyfile( - """ - import os - def test_a(): assert False - def test_b(): pass - """ - ) - testdir.makeconftest( - """ - def pytest_runtest_logreport(report): - print(report.longrepr) - """ - ) - res = testdir.runpytest("-n1", "-s") - res.stdout.fnmatch_lines(["*1 failed, 1 passed *"]) - - def test_xdist_report_longrepr_reprcrash_130(self, testdir): - reprec = testdir.inline_runsource( - """ - import py - def test_fail(): assert False, 'Expected Message' - """ - ) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3 - rep = reports[1] - added_section = ("Failure Metadata", str("metadata metadata"), "*") - rep.longrepr.sections.append(added_section) - d = serialize_report(rep) - check_marshallable(d) - a = unserialize_report("testreport", d) - # Check assembled == rep - assert a.__dict__.keys() == rep.__dict__.keys() - for key in rep.__dict__.keys(): - if key != "longrepr": - assert getattr(a, key) == getattr(rep, key) - assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno - assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message - assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path - assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep - assert ( - rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline - ) - assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style - assert rep.longrepr.sections == a.longrepr.sections - # Missing section attribute PR171 - assert added_section in a.longrepr.sections - - def test_reprentries_serialization_170(self, testdir): - from _pytest._code.code import ReprEntry - - reprec = testdir.inline_runsource( - """ - def test_repr_entry(): - x = 0 - assert x - """, - "--showlocals", - ) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3 - rep = reports[1] - d = serialize_report(rep) - a = unserialize_report("testreport", d) - - rep_entries = rep.longrepr.reprtraceback.reprentries - a_entries = a.longrepr.reprtraceback.reprentries - for i in range(len(a_entries)): - assert isinstance(rep_entries[i], ReprEntry) - assert rep_entries[i].lines == a_entries[i].lines - assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno - assert ( - rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message - ) - assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path - assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args - assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines - assert rep_entries[i].style == a_entries[i].style - - def test_reprentries_serialization_196(self, testdir): - from _pytest._code.code import ReprEntryNative - - reprec = testdir.inline_runsource( - """ - def test_repr_entry_native(): - x = 0 - assert x - """, - "--tb=native", - ) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3 - rep = reports[1] - d = serialize_report(rep) - a = unserialize_report("testreport", d) - - rep_entries = rep.longrepr.reprtraceback.reprentries - a_entries = a.longrepr.reprtraceback.reprentries - for i in range(len(a_entries)): - assert isinstance(rep_entries[i], ReprEntryNative) - assert rep_entries[i].lines == a_entries[i].lines - - def test_itemreport_outcomes(self, testdir): - reprec = testdir.inline_runsource( - """ - import py - def test_pass(): pass - def test_fail(): 0/0 - @py.test.mark.skipif("True") - def test_skip(): pass - def test_skip_imperative(): - py.test.skip("hello") - @py.test.mark.xfail("True") - def test_xfail(): 0/0 - def test_xfail_imperative(): - py.test.xfail("hello") - """ - ) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 17 # with setup/teardown "passed" reports - for rep in reports: - d = serialize_report(rep) - check_marshallable(d) - newrep = unserialize_report("testreport", d) - assert newrep.passed == rep.passed - assert newrep.failed == rep.failed - assert newrep.skipped == rep.skipped - if newrep.skipped and not hasattr(newrep, "wasxfail"): - assert len(newrep.longrepr) == 3 - assert newrep.outcome == rep.outcome - assert newrep.when == rep.when - assert newrep.keywords == rep.keywords - if rep.failed: - assert newrep.longreprtext == rep.longreprtext - - def test_collectreport_passed(self, testdir): - reprec = testdir.inline_runsource("def test_func(): pass") - reports = reprec.getreports("pytest_collectreport") - for rep in reports: - d = serialize_report(rep) - check_marshallable(d) - newrep = unserialize_report("collectreport", d) - assert newrep.passed == rep.passed - assert newrep.failed == rep.failed - assert newrep.skipped == rep.skipped - - def test_collectreport_fail(self, testdir): - reprec = testdir.inline_runsource("qwe abc") - reports = reprec.getreports("pytest_collectreport") - assert reports - for rep in reports: - d = serialize_report(rep) - check_marshallable(d) - newrep = unserialize_report("collectreport", d) - assert newrep.passed == rep.passed - assert newrep.failed == rep.failed - assert newrep.skipped == rep.skipped - if rep.failed: - assert newrep.longrepr == str(rep.longrepr) - - def test_extended_report_deserialization(self, testdir): - reprec = testdir.inline_runsource("qwe abc") - reports = reprec.getreports("pytest_collectreport") - assert reports - for rep in reports: - rep.extra = True - d = serialize_report(rep) - check_marshallable(d) - newrep = unserialize_report("collectreport", d) - assert newrep.extra - assert newrep.passed == rep.passed - assert newrep.failed == rep.failed - assert newrep.skipped == rep.skipped - if rep.failed: - assert newrep.longrepr == str(rep.longrepr) - - class TestWorkerInteractor: - def test_basic_collect_and_runtests(self, worker): + @pytest.fixture + def unserialize_report(self, pytestconfig): + def unserialize(data): + return pytestconfig.hook.pytest_report_from_serializable( + config=pytestconfig, data=data + ) + + return unserialize + + def test_basic_collect_and_runtests(self, worker, unserialize_report): worker.testdir.makepyfile( """ def test_func(): @@ -286,14 +115,14 @@ class TestWorkerInteractor: ev = worker.popevent("testreport") # setup ev = worker.popevent("testreport") assert ev.name == "testreport" - rep = unserialize_report(ev.name, ev.kwargs["data"]) + rep = unserialize_report(ev.kwargs["data"]) assert rep.nodeid.endswith("::test_func") assert rep.passed assert rep.when == "call" ev = worker.popevent("workerfinished") assert "workeroutput" in ev.kwargs - def test_remote_collect_skip(self, worker): + def test_remote_collect_skip(self, worker, unserialize_report): worker.testdir.makepyfile( """ import pytest @@ -305,25 +134,25 @@ class TestWorkerInteractor: assert not ev.kwargs ev = worker.popevent() assert ev.name == "collectreport" - rep = unserialize_report(ev.name, ev.kwargs["data"]) + rep = unserialize_report(ev.kwargs["data"]) assert rep.skipped assert rep.longrepr[2] == "Skipped: hello" ev = worker.popevent("collectionfinish") assert not ev.kwargs["ids"] - def test_remote_collect_fail(self, worker): + def test_remote_collect_fail(self, worker, unserialize_report): worker.testdir.makepyfile("""aasd qwe""") worker.setup() ev = worker.popevent("collectionstart") assert not ev.kwargs ev = worker.popevent() assert ev.name == "collectreport" - rep = unserialize_report(ev.name, ev.kwargs["data"]) + rep = unserialize_report(ev.kwargs["data"]) assert rep.failed ev = worker.popevent("collectionfinish") assert not ev.kwargs["ids"] - def test_runtests_all(self, worker): + def test_runtests_all(self, worker, unserialize_report): worker.testdir.makepyfile( """ def test_func(): pass @@ -345,7 +174,7 @@ class TestWorkerInteractor: for i in range(3): # setup/call/teardown ev = worker.popevent("testreport") assert ev.name == "testreport" - rep = unserialize_report(ev.name, ev.kwargs["data"]) + rep = unserialize_report(ev.kwargs["data"]) assert rep.nodeid.endswith(func) ev = worker.popevent("workerfinished") assert "workeroutput" in ev.kwargs diff --git a/xdist/remote.py b/xdist/remote.py index c492f9e..d87ff4c 100644 --- a/xdist/remote.py +++ b/xdist/remote.py @@ -104,7 +104,9 @@ class WorkerInteractor(object): self.sendevent("logfinish", nodeid=nodeid, location=location) def pytest_runtest_logreport(self, report): - data = serialize_report(report) + data = self.config.hook.pytest_report_to_serializable( + config=self.config, report=report + ) data["item_index"] = self.item_index data["worker_id"] = self.workerid assert self.session.items[self.item_index].nodeid == report.nodeid @@ -113,7 +115,9 @@ class WorkerInteractor(object): def pytest_collectreport(self, report): # send only reports that have not passed to master as optimization (#330) if not report.passed: - data = serialize_report(report) + data = self.config.hook.pytest_report_to_serializable( + config=self.config, report=report + ) self.sendevent("collectreport", data=data) # the pytest_logwarning hook was deprecated since pytest 4.0 @@ -143,47 +147,6 @@ class WorkerInteractor(object): ) -def serialize_report(rep): - def disassembled_report(rep): - reprtraceback = rep.longrepr.reprtraceback.__dict__.copy() - reprcrash = rep.longrepr.reprcrash.__dict__.copy() - - new_entries = [] - for entry in reprtraceback["reprentries"]: - entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()} - for key, value in entry_data["data"].items(): - if hasattr(value, "__dict__"): - entry_data["data"][key] = value.__dict__.copy() - new_entries.append(entry_data) - - reprtraceback["reprentries"] = new_entries - - return { - "reprcrash": reprcrash, - "reprtraceback": reprtraceback, - "sections": rep.longrepr.sections, - } - - import py - - d = rep.__dict__.copy() - if hasattr(rep.longrepr, "toterminal"): - if hasattr(rep.longrepr, "reprtraceback") and hasattr( - rep.longrepr, "reprcrash" - ): - d["longrepr"] = disassembled_report(rep) - else: - d["longrepr"] = str(rep.longrepr) - else: - d["longrepr"] = rep.longrepr - for name in d: - if isinstance(d[name], py.path.local): - d[name] = str(d[name]) - elif name == "result": - d[name] = None # for now - return d - - def serialize_warning_message(warning_message): if isinstance(warning_message.message, Warning): message_module = type(warning_message.message).__module__ diff --git a/xdist/workermanage.py b/xdist/workermanage.py index dff6cad..daf2042 100644 --- a/xdist/workermanage.py +++ b/xdist/workermanage.py @@ -10,8 +10,6 @@ import execnet import xdist.remote -from _pytest import runner # XXX load dynamically - def parse_spec_config(config): xspeclist = [] @@ -322,7 +320,9 @@ class WorkerController(object): self.notify_inproc(eventname, node=self, **kwargs) elif eventname in ("testreport", "collectreport", "teardownreport"): item_index = kwargs.pop("item_index", None) - rep = unserialize_report(eventname, kwargs["data"]) + rep = self.config.hook.pytest_report_from_serializable( + config=self.config, data=kwargs["data"] + ) if item_index is not None: rep.item_index = item_index self.notify_inproc(eventname, node=self, rep=rep) @@ -367,73 +367,6 @@ class WorkerController(object): self.notify_inproc("errordown", node=self, error=excinfo) -def unserialize_report(name, reportdict): - def assembled_report(reportdict): - from _pytest._code.code import ( - ReprEntry, - ReprEntryNative, - ReprExceptionInfo, - ReprFileLocation, - ReprFuncArgs, - ReprLocals, - ReprTraceback, - ) - - if reportdict["longrepr"]: - if ( - "reprcrash" in reportdict["longrepr"] - and "reprtraceback" in reportdict["longrepr"] - ): - - reprtraceback = reportdict["longrepr"]["reprtraceback"] - reprcrash = reportdict["longrepr"]["reprcrash"] - - unserialized_entries = [] - reprentry = None - for entry_data in reprtraceback["reprentries"]: - data = entry_data["data"] - entry_type = entry_data["type"] - if entry_type == "ReprEntry": - reprfuncargs = None - reprfileloc = None - reprlocals = None - if data["reprfuncargs"]: - reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) - if data["reprfileloc"]: - reprfileloc = ReprFileLocation(**data["reprfileloc"]) - if data["reprlocals"]: - reprlocals = ReprLocals(data["reprlocals"]["lines"]) - - reprentry = ReprEntry( - lines=data["lines"], - reprfuncargs=reprfuncargs, - reprlocals=reprlocals, - filelocrepr=reprfileloc, - style=data["style"], - ) - elif entry_type == "ReprEntryNative": - reprentry = ReprEntryNative(data["lines"]) - else: - report_unserialization_failure(entry_type, name, reportdict) - unserialized_entries.append(reprentry) - reprtraceback["reprentries"] = unserialized_entries - - exception_info = ReprExceptionInfo( - reprtraceback=ReprTraceback(**reprtraceback), - reprcrash=ReprFileLocation(**reprcrash), - ) - - for section in reportdict["longrepr"]["sections"]: - exception_info.addsection(*section) - reportdict["longrepr"] = exception_info - return reportdict - - if name == "testreport": - return runner.TestReport(**assembled_report(reportdict)) - elif name == "collectreport": - return runner.CollectReport(**assembled_report(reportdict)) - - def unserialize_warning_message(data): import warnings import importlib @@ -474,17 +407,3 @@ def unserialize_warning_message(data): kwargs[attr_name] = data[attr_name] return warnings.WarningMessage(**kwargs) - - -def report_unserialization_failure(type_name, report_name, reportdict): - from pprint import pprint - - url = "https://github.com/pytest-dev/pytest-xdist/issues" - stream = py.io.TextIO() - pprint("-" * 100, stream=stream) - pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) - pprint("report_name: %s" % report_name, stream=stream) - pprint(reportdict, stream=stream) - pprint("Please report this bug at %s" % url, stream=stream) - pprint("-" * 100, stream=stream) - assert 0, stream.getvalue() From 7ab9cf1a7aa9c68f0206843851a804032d7f4aa9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 2 Apr 2019 19:20:32 -0300 Subject: [PATCH 2/2] Apply suggestions from code review Co-Authored-By: nicoddemus --- changelog/426.feature.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog/426.feature.rst b/changelog/426.feature.rst index ad85749..c29bd9e 100644 --- a/changelog/426.feature.rst +++ b/changelog/426.feature.rst @@ -1,5 +1,5 @@ -``pytest-xdist`` now uses the new ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` -from ``pytest 4.4`` (still experimental). This will make report serialization more reliable and +``pytest-xdist`` now uses the new ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` +hooks from ``pytest 4.4`` (still experimental). This will make report serialization more reliable and extensible. -This also means that ``pytest-xdist`` now also requires ``pytest>=4.4``. +This also means that ``pytest-xdist`` now requires ``pytest>=4.4``.