Skip to content

Commit 0c63f99

Browse files
committed
Add experimental _to_json and _from_json to TestReport and CollectReport
This methods were moved from xdist (ca03269). Our intention is to keep this code closer to the core, given that it might break easily due to refactorings. Having it in the core might also allow to improve the code by moving some responsibility to the "code" objects (ReprEntry, etc) which are often found in the reports. Finally pytest-xdist and pytest-subtests can use those functions instead of coding it themselves.
1 parent 2df9d05 commit 0c63f99

File tree

2 files changed

+315
-0
lines changed

2 files changed

+315
-0
lines changed

src/_pytest/reports.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import py
2+
import six
23

34
from _pytest._code.code import ExceptionInfo
5+
from _pytest._code.code import ReprEntry
6+
from _pytest._code.code import ReprEntryNative
7+
from _pytest._code.code import ReprExceptionInfo
8+
from _pytest._code.code import ReprFileLocation
9+
from _pytest._code.code import ReprFuncArgs
10+
from _pytest._code.code import ReprLocals
11+
from _pytest._code.code import ReprTraceback
412
from _pytest._code.code import TerminalRepr
513
from _pytest.outcomes import skip
614

@@ -137,6 +145,130 @@ def head_line(self):
137145
fspath, lineno, domain = self.location
138146
return domain
139147

148+
def _to_json(self):
149+
"""
150+
This was originally the serialize_report() function from xdist (ca03269).
151+
152+
Returns the contents of this report as a dict of builtin entries, suitable for
153+
serialization.
154+
155+
Experimental method.
156+
"""
157+
158+
def disassembled_report(rep):
159+
reprtraceback = rep.longrepr.reprtraceback.__dict__.copy()
160+
reprcrash = rep.longrepr.reprcrash.__dict__.copy()
161+
162+
new_entries = []
163+
for entry in reprtraceback["reprentries"]:
164+
entry_data = {
165+
"type": type(entry).__name__,
166+
"data": entry.__dict__.copy(),
167+
}
168+
for key, value in entry_data["data"].items():
169+
if hasattr(value, "__dict__"):
170+
entry_data["data"][key] = value.__dict__.copy()
171+
new_entries.append(entry_data)
172+
173+
reprtraceback["reprentries"] = new_entries
174+
175+
return {
176+
"reprcrash": reprcrash,
177+
"reprtraceback": reprtraceback,
178+
"sections": rep.longrepr.sections,
179+
}
180+
181+
d = self.__dict__.copy()
182+
if hasattr(self.longrepr, "toterminal"):
183+
if hasattr(self.longrepr, "reprtraceback") and hasattr(
184+
self.longrepr, "reprcrash"
185+
):
186+
d["longrepr"] = disassembled_report(self)
187+
else:
188+
d["longrepr"] = six.text_type(self.longrepr)
189+
else:
190+
d["longrepr"] = self.longrepr
191+
for name in d:
192+
if isinstance(d[name], py.path.local):
193+
d[name] = str(d[name])
194+
elif name == "result":
195+
d[name] = None # for now
196+
return d
197+
198+
@classmethod
199+
def _from_json(cls, reportdict):
200+
"""
201+
This was originally the serialize_report() function from xdist (ca03269).
202+
203+
Factory method that returns either a TestReport or CollectReport, depending on the calling
204+
class. It's the callers responsibility to know which class to pass here.
205+
206+
Experimental method.
207+
"""
208+
if reportdict["longrepr"]:
209+
if (
210+
"reprcrash" in reportdict["longrepr"]
211+
and "reprtraceback" in reportdict["longrepr"]
212+
):
213+
214+
reprtraceback = reportdict["longrepr"]["reprtraceback"]
215+
reprcrash = reportdict["longrepr"]["reprcrash"]
216+
217+
unserialized_entries = []
218+
reprentry = None
219+
for entry_data in reprtraceback["reprentries"]:
220+
data = entry_data["data"]
221+
entry_type = entry_data["type"]
222+
if entry_type == "ReprEntry":
223+
reprfuncargs = None
224+
reprfileloc = None
225+
reprlocals = None
226+
if data["reprfuncargs"]:
227+
reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
228+
if data["reprfileloc"]:
229+
reprfileloc = ReprFileLocation(**data["reprfileloc"])
230+
if data["reprlocals"]:
231+
reprlocals = ReprLocals(data["reprlocals"]["lines"])
232+
233+
reprentry = ReprEntry(
234+
lines=data["lines"],
235+
reprfuncargs=reprfuncargs,
236+
reprlocals=reprlocals,
237+
filelocrepr=reprfileloc,
238+
style=data["style"],
239+
)
240+
elif entry_type == "ReprEntryNative":
241+
reprentry = ReprEntryNative(data["lines"])
242+
else:
243+
_report_unserialization_failure(entry_type, cls, reportdict)
244+
unserialized_entries.append(reprentry)
245+
reprtraceback["reprentries"] = unserialized_entries
246+
247+
exception_info = ReprExceptionInfo(
248+
reprtraceback=ReprTraceback(**reprtraceback),
249+
reprcrash=ReprFileLocation(**reprcrash),
250+
)
251+
252+
for section in reportdict["longrepr"]["sections"]:
253+
exception_info.addsection(*section)
254+
reportdict["longrepr"] = exception_info
255+
256+
return cls(**reportdict)
257+
258+
259+
def _report_unserialization_failure(type_name, report_class, reportdict):
260+
from pprint import pprint
261+
262+
url = "https://github.com/pytest-dev/pytest/issues"
263+
stream = py.io.TextIO()
264+
pprint("-" * 100, stream=stream)
265+
pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
266+
pprint("report_name: %s" % report_class, stream=stream)
267+
pprint(reportdict, stream=stream)
268+
pprint("Please report this bug at %s" % url, stream=stream)
269+
pprint("-" * 100, stream=stream)
270+
assert 0, stream.getvalue()
271+
140272

141273
class TestReport(BaseReport):
142274
""" Basic test report object (also used for setup and teardown calls if

testing/test_reports.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from _pytest.reports import CollectReport
2+
from _pytest.reports import TestReport
3+
4+
5+
class TestReportSerialization(object):
6+
"""
7+
All the tests in this class came originally from test_remote.py in xdist (ca03269).
8+
"""
9+
10+
def test_xdist_longrepr_to_str_issue_241(self, testdir):
11+
testdir.makepyfile(
12+
"""
13+
import os
14+
def test_a(): assert False
15+
def test_b(): pass
16+
"""
17+
)
18+
reprec = testdir.inline_run()
19+
reports = reprec.getreports("pytest_runtest_logreport")
20+
assert len(reports) == 6
21+
test_a_call = reports[1]
22+
assert test_a_call.when == "call"
23+
assert test_a_call.outcome == "failed"
24+
assert test_a_call._to_json()["longrepr"]["reprtraceback"]["style"] == "long"
25+
test_b_call = reports[4]
26+
assert test_b_call.when == "call"
27+
assert test_b_call.outcome == "passed"
28+
assert test_b_call._to_json()["longrepr"] is None
29+
30+
def test_xdist_report_longrepr_reprcrash_130(self, testdir):
31+
reprec = testdir.inline_runsource(
32+
"""
33+
import py
34+
def test_fail(): assert False, 'Expected Message'
35+
"""
36+
)
37+
reports = reprec.getreports("pytest_runtest_logreport")
38+
assert len(reports) == 3
39+
rep = reports[1]
40+
added_section = ("Failure Metadata", str("metadata metadata"), "*")
41+
rep.longrepr.sections.append(added_section)
42+
d = rep._to_json()
43+
a = TestReport._from_json(d)
44+
# Check assembled == rep
45+
assert a.__dict__.keys() == rep.__dict__.keys()
46+
for key in rep.__dict__.keys():
47+
if key != "longrepr":
48+
assert getattr(a, key) == getattr(rep, key)
49+
assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
50+
assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
51+
assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
52+
assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep
53+
assert (
54+
rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline
55+
)
56+
assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style
57+
assert rep.longrepr.sections == a.longrepr.sections
58+
# Missing section attribute PR171
59+
assert added_section in a.longrepr.sections
60+
61+
def test_reprentries_serialization_170(self, testdir):
62+
from _pytest._code.code import ReprEntry
63+
64+
reprec = testdir.inline_runsource(
65+
"""
66+
def test_repr_entry():
67+
x = 0
68+
assert x
69+
""",
70+
"--showlocals",
71+
)
72+
reports = reprec.getreports("pytest_runtest_logreport")
73+
assert len(reports) == 3
74+
rep = reports[1]
75+
d = rep._to_json()
76+
a = TestReport._from_json(d)
77+
78+
rep_entries = rep.longrepr.reprtraceback.reprentries
79+
a_entries = a.longrepr.reprtraceback.reprentries
80+
for i in range(len(a_entries)):
81+
assert isinstance(rep_entries[i], ReprEntry)
82+
assert rep_entries[i].lines == a_entries[i].lines
83+
assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
84+
assert (
85+
rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
86+
)
87+
assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path
88+
assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args
89+
assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines
90+
assert rep_entries[i].style == a_entries[i].style
91+
92+
def test_reprentries_serialization_196(self, testdir):
93+
from _pytest._code.code import ReprEntryNative
94+
95+
reprec = testdir.inline_runsource(
96+
"""
97+
def test_repr_entry_native():
98+
x = 0
99+
assert x
100+
""",
101+
"--tb=native",
102+
)
103+
reports = reprec.getreports("pytest_runtest_logreport")
104+
assert len(reports) == 3
105+
rep = reports[1]
106+
d = rep._to_json()
107+
a = TestReport._from_json(d)
108+
109+
rep_entries = rep.longrepr.reprtraceback.reprentries
110+
a_entries = a.longrepr.reprtraceback.reprentries
111+
for i in range(len(a_entries)):
112+
assert isinstance(rep_entries[i], ReprEntryNative)
113+
assert rep_entries[i].lines == a_entries[i].lines
114+
115+
def test_itemreport_outcomes(self, testdir):
116+
reprec = testdir.inline_runsource(
117+
"""
118+
import py
119+
def test_pass(): pass
120+
def test_fail(): 0/0
121+
@py.test.mark.skipif("True")
122+
def test_skip(): pass
123+
def test_skip_imperative():
124+
py.test.skip("hello")
125+
@py.test.mark.xfail("True")
126+
def test_xfail(): 0/0
127+
def test_xfail_imperative():
128+
py.test.xfail("hello")
129+
"""
130+
)
131+
reports = reprec.getreports("pytest_runtest_logreport")
132+
assert len(reports) == 17 # with setup/teardown "passed" reports
133+
for rep in reports:
134+
d = rep._to_json()
135+
newrep = TestReport._from_json(d)
136+
assert newrep.passed == rep.passed
137+
assert newrep.failed == rep.failed
138+
assert newrep.skipped == rep.skipped
139+
if newrep.skipped and not hasattr(newrep, "wasxfail"):
140+
assert len(newrep.longrepr) == 3
141+
assert newrep.outcome == rep.outcome
142+
assert newrep.when == rep.when
143+
assert newrep.keywords == rep.keywords
144+
if rep.failed:
145+
assert newrep.longreprtext == rep.longreprtext
146+
147+
def test_collectreport_passed(self, testdir):
148+
reprec = testdir.inline_runsource("def test_func(): pass")
149+
reports = reprec.getreports("pytest_collectreport")
150+
for rep in reports:
151+
d = rep._to_json()
152+
newrep = CollectReport._from_json(d)
153+
assert newrep.passed == rep.passed
154+
assert newrep.failed == rep.failed
155+
assert newrep.skipped == rep.skipped
156+
157+
def test_collectreport_fail(self, testdir):
158+
reprec = testdir.inline_runsource("qwe abc")
159+
reports = reprec.getreports("pytest_collectreport")
160+
assert reports
161+
for rep in reports:
162+
d = rep._to_json()
163+
newrep = CollectReport._from_json(d)
164+
assert newrep.passed == rep.passed
165+
assert newrep.failed == rep.failed
166+
assert newrep.skipped == rep.skipped
167+
if rep.failed:
168+
assert newrep.longrepr == str(rep.longrepr)
169+
170+
def test_extended_report_deserialization(self, testdir):
171+
reprec = testdir.inline_runsource("qwe abc")
172+
reports = reprec.getreports("pytest_collectreport")
173+
assert reports
174+
for rep in reports:
175+
rep.extra = True
176+
d = rep._to_json()
177+
newrep = CollectReport._from_json(d)
178+
assert newrep.extra
179+
assert newrep.passed == rep.passed
180+
assert newrep.failed == rep.failed
181+
assert newrep.skipped == rep.skipped
182+
if rep.failed:
183+
assert newrep.longrepr == str(rep.longrepr)

0 commit comments

Comments
 (0)