Skip to content

Commit 04c0265

Browse files
authored
Merge pull request #426 from nicoddemus/serialization-hooks
Serialization hooks (requires pytest 4.4)
2 parents 911dfdb + 7ab9cf1 commit 04c0265

File tree

5 files changed

+31
-315
lines changed

5 files changed

+31
-315
lines changed

changelog/426.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``pytest-xdist`` now uses the new ``pytest_report_to_serializable`` and ``pytest_report_from_serializable``
2+
hooks from ``pytest 4.4`` (still experimental). This will make report serialization more reliable and
3+
extensible.
4+
5+
This also means that ``pytest-xdist`` now requires ``pytest>=4.4``.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from setuptools import setup, find_packages
22

3-
install_requires = ["execnet>=1.1", "pytest>=3.6.0", "pytest-forked", "six"]
3+
install_requires = ["execnet>=1.1", "pytest>=4.4.0", "pytest-forked", "six"]
44

55

66
with open("README.rst") as f:

testing/test_remote.py

Lines changed: 16 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import pytest
44
import sys
55

6-
from xdist.workermanage import WorkerController, unserialize_report
7-
from xdist.remote import serialize_report
6+
from xdist.workermanage import WorkerController
87
import execnet
98
import marshal
109

@@ -81,187 +80,17 @@ def test_remoteinitconfig(testdir):
8180
assert config2.pluginmanager.getplugin("terminal") in (-1, None)
8281

8382

84-
class TestReportSerialization:
85-
def test_xdist_longrepr_to_str_issue_241(self, testdir):
86-
testdir.makepyfile(
87-
"""
88-
import os
89-
def test_a(): assert False
90-
def test_b(): pass
91-
"""
92-
)
93-
testdir.makeconftest(
94-
"""
95-
def pytest_runtest_logreport(report):
96-
print(report.longrepr)
97-
"""
98-
)
99-
res = testdir.runpytest("-n1", "-s")
100-
res.stdout.fnmatch_lines(["*1 failed, 1 passed *"])
101-
102-
def test_xdist_report_longrepr_reprcrash_130(self, testdir):
103-
reprec = testdir.inline_runsource(
104-
"""
105-
import py
106-
def test_fail(): assert False, 'Expected Message'
107-
"""
108-
)
109-
reports = reprec.getreports("pytest_runtest_logreport")
110-
assert len(reports) == 3
111-
rep = reports[1]
112-
added_section = ("Failure Metadata", str("metadata metadata"), "*")
113-
rep.longrepr.sections.append(added_section)
114-
d = serialize_report(rep)
115-
check_marshallable(d)
116-
a = unserialize_report("testreport", d)
117-
# Check assembled == rep
118-
assert a.__dict__.keys() == rep.__dict__.keys()
119-
for key in rep.__dict__.keys():
120-
if key != "longrepr":
121-
assert getattr(a, key) == getattr(rep, key)
122-
assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
123-
assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
124-
assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
125-
assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep
126-
assert (
127-
rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline
128-
)
129-
assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style
130-
assert rep.longrepr.sections == a.longrepr.sections
131-
# Missing section attribute PR171
132-
assert added_section in a.longrepr.sections
133-
134-
def test_reprentries_serialization_170(self, testdir):
135-
from _pytest._code.code import ReprEntry
136-
137-
reprec = testdir.inline_runsource(
138-
"""
139-
def test_repr_entry():
140-
x = 0
141-
assert x
142-
""",
143-
"--showlocals",
144-
)
145-
reports = reprec.getreports("pytest_runtest_logreport")
146-
assert len(reports) == 3
147-
rep = reports[1]
148-
d = serialize_report(rep)
149-
a = unserialize_report("testreport", d)
150-
151-
rep_entries = rep.longrepr.reprtraceback.reprentries
152-
a_entries = a.longrepr.reprtraceback.reprentries
153-
for i in range(len(a_entries)):
154-
assert isinstance(rep_entries[i], ReprEntry)
155-
assert rep_entries[i].lines == a_entries[i].lines
156-
assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
157-
assert (
158-
rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
83+
class TestWorkerInteractor:
84+
@pytest.fixture
85+
def unserialize_report(self, pytestconfig):
86+
def unserialize(data):
87+
return pytestconfig.hook.pytest_report_from_serializable(
88+
config=pytestconfig, data=data
15989
)
160-
assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path
161-
assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args
162-
assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines
163-
assert rep_entries[i].style == a_entries[i].style
16490

165-
def test_reprentries_serialization_196(self, testdir):
166-
from _pytest._code.code import ReprEntryNative
91+
return unserialize
16792

168-
reprec = testdir.inline_runsource(
169-
"""
170-
def test_repr_entry_native():
171-
x = 0
172-
assert x
173-
""",
174-
"--tb=native",
175-
)
176-
reports = reprec.getreports("pytest_runtest_logreport")
177-
assert len(reports) == 3
178-
rep = reports[1]
179-
d = serialize_report(rep)
180-
a = unserialize_report("testreport", d)
181-
182-
rep_entries = rep.longrepr.reprtraceback.reprentries
183-
a_entries = a.longrepr.reprtraceback.reprentries
184-
for i in range(len(a_entries)):
185-
assert isinstance(rep_entries[i], ReprEntryNative)
186-
assert rep_entries[i].lines == a_entries[i].lines
187-
188-
def test_itemreport_outcomes(self, testdir):
189-
reprec = testdir.inline_runsource(
190-
"""
191-
import py
192-
def test_pass(): pass
193-
def test_fail(): 0/0
194-
@py.test.mark.skipif("True")
195-
def test_skip(): pass
196-
def test_skip_imperative():
197-
py.test.skip("hello")
198-
@py.test.mark.xfail("True")
199-
def test_xfail(): 0/0
200-
def test_xfail_imperative():
201-
py.test.xfail("hello")
202-
"""
203-
)
204-
reports = reprec.getreports("pytest_runtest_logreport")
205-
assert len(reports) == 17 # with setup/teardown "passed" reports
206-
for rep in reports:
207-
d = serialize_report(rep)
208-
check_marshallable(d)
209-
newrep = unserialize_report("testreport", d)
210-
assert newrep.passed == rep.passed
211-
assert newrep.failed == rep.failed
212-
assert newrep.skipped == rep.skipped
213-
if newrep.skipped and not hasattr(newrep, "wasxfail"):
214-
assert len(newrep.longrepr) == 3
215-
assert newrep.outcome == rep.outcome
216-
assert newrep.when == rep.when
217-
assert newrep.keywords == rep.keywords
218-
if rep.failed:
219-
assert newrep.longreprtext == rep.longreprtext
220-
221-
def test_collectreport_passed(self, testdir):
222-
reprec = testdir.inline_runsource("def test_func(): pass")
223-
reports = reprec.getreports("pytest_collectreport")
224-
for rep in reports:
225-
d = serialize_report(rep)
226-
check_marshallable(d)
227-
newrep = unserialize_report("collectreport", d)
228-
assert newrep.passed == rep.passed
229-
assert newrep.failed == rep.failed
230-
assert newrep.skipped == rep.skipped
231-
232-
def test_collectreport_fail(self, testdir):
233-
reprec = testdir.inline_runsource("qwe abc")
234-
reports = reprec.getreports("pytest_collectreport")
235-
assert reports
236-
for rep in reports:
237-
d = serialize_report(rep)
238-
check_marshallable(d)
239-
newrep = unserialize_report("collectreport", d)
240-
assert newrep.passed == rep.passed
241-
assert newrep.failed == rep.failed
242-
assert newrep.skipped == rep.skipped
243-
if rep.failed:
244-
assert newrep.longrepr == str(rep.longrepr)
245-
246-
def test_extended_report_deserialization(self, testdir):
247-
reprec = testdir.inline_runsource("qwe abc")
248-
reports = reprec.getreports("pytest_collectreport")
249-
assert reports
250-
for rep in reports:
251-
rep.extra = True
252-
d = serialize_report(rep)
253-
check_marshallable(d)
254-
newrep = unserialize_report("collectreport", d)
255-
assert newrep.extra
256-
assert newrep.passed == rep.passed
257-
assert newrep.failed == rep.failed
258-
assert newrep.skipped == rep.skipped
259-
if rep.failed:
260-
assert newrep.longrepr == str(rep.longrepr)
261-
262-
263-
class TestWorkerInteractor:
264-
def test_basic_collect_and_runtests(self, worker):
93+
def test_basic_collect_and_runtests(self, worker, unserialize_report):
26594
worker.testdir.makepyfile(
26695
"""
26796
def test_func():
@@ -286,14 +115,14 @@ def test_func():
286115
ev = worker.popevent("testreport") # setup
287116
ev = worker.popevent("testreport")
288117
assert ev.name == "testreport"
289-
rep = unserialize_report(ev.name, ev.kwargs["data"])
118+
rep = unserialize_report(ev.kwargs["data"])
290119
assert rep.nodeid.endswith("::test_func")
291120
assert rep.passed
292121
assert rep.when == "call"
293122
ev = worker.popevent("workerfinished")
294123
assert "workeroutput" in ev.kwargs
295124

296-
def test_remote_collect_skip(self, worker):
125+
def test_remote_collect_skip(self, worker, unserialize_report):
297126
worker.testdir.makepyfile(
298127
"""
299128
import pytest
@@ -305,25 +134,25 @@ def test_remote_collect_skip(self, worker):
305134
assert not ev.kwargs
306135
ev = worker.popevent()
307136
assert ev.name == "collectreport"
308-
rep = unserialize_report(ev.name, ev.kwargs["data"])
137+
rep = unserialize_report(ev.kwargs["data"])
309138
assert rep.skipped
310139
assert rep.longrepr[2] == "Skipped: hello"
311140
ev = worker.popevent("collectionfinish")
312141
assert not ev.kwargs["ids"]
313142

314-
def test_remote_collect_fail(self, worker):
143+
def test_remote_collect_fail(self, worker, unserialize_report):
315144
worker.testdir.makepyfile("""aasd qwe""")
316145
worker.setup()
317146
ev = worker.popevent("collectionstart")
318147
assert not ev.kwargs
319148
ev = worker.popevent()
320149
assert ev.name == "collectreport"
321-
rep = unserialize_report(ev.name, ev.kwargs["data"])
150+
rep = unserialize_report(ev.kwargs["data"])
322151
assert rep.failed
323152
ev = worker.popevent("collectionfinish")
324153
assert not ev.kwargs["ids"]
325154

326-
def test_runtests_all(self, worker):
155+
def test_runtests_all(self, worker, unserialize_report):
327156
worker.testdir.makepyfile(
328157
"""
329158
def test_func(): pass
@@ -345,7 +174,7 @@ def test_func2(): pass
345174
for i in range(3): # setup/call/teardown
346175
ev = worker.popevent("testreport")
347176
assert ev.name == "testreport"
348-
rep = unserialize_report(ev.name, ev.kwargs["data"])
177+
rep = unserialize_report(ev.kwargs["data"])
349178
assert rep.nodeid.endswith(func)
350179
ev = worker.popevent("workerfinished")
351180
assert "workeroutput" in ev.kwargs

xdist/remote.py

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ def pytest_runtest_logfinish(self, nodeid, location):
104104
self.sendevent("logfinish", nodeid=nodeid, location=location)
105105

106106
def pytest_runtest_logreport(self, report):
107-
data = serialize_report(report)
107+
data = self.config.hook.pytest_report_to_serializable(
108+
config=self.config, report=report
109+
)
108110
data["item_index"] = self.item_index
109111
data["worker_id"] = self.workerid
110112
assert self.session.items[self.item_index].nodeid == report.nodeid
@@ -113,7 +115,9 @@ def pytest_runtest_logreport(self, report):
113115
def pytest_collectreport(self, report):
114116
# send only reports that have not passed to master as optimization (#330)
115117
if not report.passed:
116-
data = serialize_report(report)
118+
data = self.config.hook.pytest_report_to_serializable(
119+
config=self.config, report=report
120+
)
117121
self.sendevent("collectreport", data=data)
118122

119123
# the pytest_logwarning hook was deprecated since pytest 4.0
@@ -143,47 +147,6 @@ def pytest_warning_captured(self, warning_message, when, item):
143147
)
144148

145149

146-
def serialize_report(rep):
147-
def disassembled_report(rep):
148-
reprtraceback = rep.longrepr.reprtraceback.__dict__.copy()
149-
reprcrash = rep.longrepr.reprcrash.__dict__.copy()
150-
151-
new_entries = []
152-
for entry in reprtraceback["reprentries"]:
153-
entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()}
154-
for key, value in entry_data["data"].items():
155-
if hasattr(value, "__dict__"):
156-
entry_data["data"][key] = value.__dict__.copy()
157-
new_entries.append(entry_data)
158-
159-
reprtraceback["reprentries"] = new_entries
160-
161-
return {
162-
"reprcrash": reprcrash,
163-
"reprtraceback": reprtraceback,
164-
"sections": rep.longrepr.sections,
165-
}
166-
167-
import py
168-
169-
d = rep.__dict__.copy()
170-
if hasattr(rep.longrepr, "toterminal"):
171-
if hasattr(rep.longrepr, "reprtraceback") and hasattr(
172-
rep.longrepr, "reprcrash"
173-
):
174-
d["longrepr"] = disassembled_report(rep)
175-
else:
176-
d["longrepr"] = str(rep.longrepr)
177-
else:
178-
d["longrepr"] = rep.longrepr
179-
for name in d:
180-
if isinstance(d[name], py.path.local):
181-
d[name] = str(d[name])
182-
elif name == "result":
183-
d[name] = None # for now
184-
return d
185-
186-
187150
def serialize_warning_message(warning_message):
188151
if isinstance(warning_message.message, Warning):
189152
message_module = type(warning_message.message).__module__

0 commit comments

Comments
 (0)