Skip to content

Commit 01082fe

Browse files
authored
Serialize/deserialize chained exceptions (#5787)
Serialize/deserialize chained exceptions
2 parents 955e542 + a511b98 commit 01082fe

File tree

6 files changed

+411
-294
lines changed

6 files changed

+411
-294
lines changed

changelog/5786.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like
2+
``pytest-xdist`` to display them properly.

src/_pytest/reports.py

Lines changed: 143 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import py
55

6+
from _pytest._code.code import ExceptionChainRepr
67
from _pytest._code.code import ExceptionInfo
78
from _pytest._code.code import ReprEntry
89
from _pytest._code.code import ReprEntryNative
@@ -160,46 +161,7 @@ def _to_json(self):
160161
161162
Experimental method.
162163
"""
163-
164-
def disassembled_report(rep):
165-
reprtraceback = rep.longrepr.reprtraceback.__dict__.copy()
166-
reprcrash = rep.longrepr.reprcrash.__dict__.copy()
167-
168-
new_entries = []
169-
for entry in reprtraceback["reprentries"]:
170-
entry_data = {
171-
"type": type(entry).__name__,
172-
"data": entry.__dict__.copy(),
173-
}
174-
for key, value in entry_data["data"].items():
175-
if hasattr(value, "__dict__"):
176-
entry_data["data"][key] = value.__dict__.copy()
177-
new_entries.append(entry_data)
178-
179-
reprtraceback["reprentries"] = new_entries
180-
181-
return {
182-
"reprcrash": reprcrash,
183-
"reprtraceback": reprtraceback,
184-
"sections": rep.longrepr.sections,
185-
}
186-
187-
d = self.__dict__.copy()
188-
if hasattr(self.longrepr, "toterminal"):
189-
if hasattr(self.longrepr, "reprtraceback") and hasattr(
190-
self.longrepr, "reprcrash"
191-
):
192-
d["longrepr"] = disassembled_report(self)
193-
else:
194-
d["longrepr"] = str(self.longrepr)
195-
else:
196-
d["longrepr"] = self.longrepr
197-
for name in d:
198-
if isinstance(d[name], (py.path.local, Path)):
199-
d[name] = str(d[name])
200-
elif name == "result":
201-
d[name] = None # for now
202-
return d
164+
return _report_to_json(self)
203165

204166
@classmethod
205167
def _from_json(cls, reportdict):
@@ -211,55 +173,8 @@ def _from_json(cls, reportdict):
211173
212174
Experimental method.
213175
"""
214-
if reportdict["longrepr"]:
215-
if (
216-
"reprcrash" in reportdict["longrepr"]
217-
and "reprtraceback" in reportdict["longrepr"]
218-
):
219-
220-
reprtraceback = reportdict["longrepr"]["reprtraceback"]
221-
reprcrash = reportdict["longrepr"]["reprcrash"]
222-
223-
unserialized_entries = []
224-
reprentry = None
225-
for entry_data in reprtraceback["reprentries"]:
226-
data = entry_data["data"]
227-
entry_type = entry_data["type"]
228-
if entry_type == "ReprEntry":
229-
reprfuncargs = None
230-
reprfileloc = None
231-
reprlocals = None
232-
if data["reprfuncargs"]:
233-
reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
234-
if data["reprfileloc"]:
235-
reprfileloc = ReprFileLocation(**data["reprfileloc"])
236-
if data["reprlocals"]:
237-
reprlocals = ReprLocals(data["reprlocals"]["lines"])
238-
239-
reprentry = ReprEntry(
240-
lines=data["lines"],
241-
reprfuncargs=reprfuncargs,
242-
reprlocals=reprlocals,
243-
filelocrepr=reprfileloc,
244-
style=data["style"],
245-
)
246-
elif entry_type == "ReprEntryNative":
247-
reprentry = ReprEntryNative(data["lines"])
248-
else:
249-
_report_unserialization_failure(entry_type, cls, reportdict)
250-
unserialized_entries.append(reprentry)
251-
reprtraceback["reprentries"] = unserialized_entries
252-
253-
exception_info = ReprExceptionInfo(
254-
reprtraceback=ReprTraceback(**reprtraceback),
255-
reprcrash=ReprFileLocation(**reprcrash),
256-
)
257-
258-
for section in reportdict["longrepr"]["sections"]:
259-
exception_info.addsection(*section)
260-
reportdict["longrepr"] = exception_info
261-
262-
return cls(**reportdict)
176+
kwargs = _report_kwargs_from_json(reportdict)
177+
return cls(**kwargs)
263178

264179

265180
def _report_unserialization_failure(type_name, report_class, reportdict):
@@ -424,3 +339,142 @@ def pytest_report_from_serializable(data):
424339
assert False, "Unknown report_type unserialize data: {}".format(
425340
data["_report_type"]
426341
)
342+
343+
344+
def _report_to_json(report):
345+
"""
346+
This was originally the serialize_report() function from xdist (ca03269).
347+
348+
Returns the contents of this report as a dict of builtin entries, suitable for
349+
serialization.
350+
"""
351+
352+
def serialize_repr_entry(entry):
353+
entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()}
354+
for key, value in entry_data["data"].items():
355+
if hasattr(value, "__dict__"):
356+
entry_data["data"][key] = value.__dict__.copy()
357+
return entry_data
358+
359+
def serialize_repr_traceback(reprtraceback):
360+
result = reprtraceback.__dict__.copy()
361+
result["reprentries"] = [
362+
serialize_repr_entry(x) for x in reprtraceback.reprentries
363+
]
364+
return result
365+
366+
def serialize_repr_crash(reprcrash):
367+
return reprcrash.__dict__.copy()
368+
369+
def serialize_longrepr(rep):
370+
result = {
371+
"reprcrash": serialize_repr_crash(rep.longrepr.reprcrash),
372+
"reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback),
373+
"sections": rep.longrepr.sections,
374+
}
375+
if isinstance(rep.longrepr, ExceptionChainRepr):
376+
result["chain"] = []
377+
for repr_traceback, repr_crash, description in rep.longrepr.chain:
378+
result["chain"].append(
379+
(
380+
serialize_repr_traceback(repr_traceback),
381+
serialize_repr_crash(repr_crash),
382+
description,
383+
)
384+
)
385+
else:
386+
result["chain"] = None
387+
return result
388+
389+
d = report.__dict__.copy()
390+
if hasattr(report.longrepr, "toterminal"):
391+
if hasattr(report.longrepr, "reprtraceback") and hasattr(
392+
report.longrepr, "reprcrash"
393+
):
394+
d["longrepr"] = serialize_longrepr(report)
395+
else:
396+
d["longrepr"] = str(report.longrepr)
397+
else:
398+
d["longrepr"] = report.longrepr
399+
for name in d:
400+
if isinstance(d[name], (py.path.local, Path)):
401+
d[name] = str(d[name])
402+
elif name == "result":
403+
d[name] = None # for now
404+
return d
405+
406+
407+
def _report_kwargs_from_json(reportdict):
408+
"""
409+
This was originally the serialize_report() function from xdist (ca03269).
410+
411+
Returns **kwargs that can be used to construct a TestReport or CollectReport instance.
412+
"""
413+
414+
def deserialize_repr_entry(entry_data):
415+
data = entry_data["data"]
416+
entry_type = entry_data["type"]
417+
if entry_type == "ReprEntry":
418+
reprfuncargs = None
419+
reprfileloc = None
420+
reprlocals = None
421+
if data["reprfuncargs"]:
422+
reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
423+
if data["reprfileloc"]:
424+
reprfileloc = ReprFileLocation(**data["reprfileloc"])
425+
if data["reprlocals"]:
426+
reprlocals = ReprLocals(data["reprlocals"]["lines"])
427+
428+
reprentry = ReprEntry(
429+
lines=data["lines"],
430+
reprfuncargs=reprfuncargs,
431+
reprlocals=reprlocals,
432+
filelocrepr=reprfileloc,
433+
style=data["style"],
434+
)
435+
elif entry_type == "ReprEntryNative":
436+
reprentry = ReprEntryNative(data["lines"])
437+
else:
438+
_report_unserialization_failure(entry_type, TestReport, reportdict)
439+
return reprentry
440+
441+
def deserialize_repr_traceback(repr_traceback_dict):
442+
repr_traceback_dict["reprentries"] = [
443+
deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"]
444+
]
445+
return ReprTraceback(**repr_traceback_dict)
446+
447+
def deserialize_repr_crash(repr_crash_dict):
448+
return ReprFileLocation(**repr_crash_dict)
449+
450+
if (
451+
reportdict["longrepr"]
452+
and "reprcrash" in reportdict["longrepr"]
453+
and "reprtraceback" in reportdict["longrepr"]
454+
):
455+
456+
reprtraceback = deserialize_repr_traceback(
457+
reportdict["longrepr"]["reprtraceback"]
458+
)
459+
reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
460+
if reportdict["longrepr"]["chain"]:
461+
chain = []
462+
for repr_traceback_data, repr_crash_data, description in reportdict[
463+
"longrepr"
464+
]["chain"]:
465+
chain.append(
466+
(
467+
deserialize_repr_traceback(repr_traceback_data),
468+
deserialize_repr_crash(repr_crash_data),
469+
description,
470+
)
471+
)
472+
exception_info = ExceptionChainRepr(chain)
473+
else:
474+
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
475+
476+
for section in reportdict["longrepr"]["sections"]:
477+
exception_info.addsection(*section)
478+
reportdict["longrepr"] = exception_info
479+
480+
return reportdict

testing/code/test_code.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import sys
22
from unittest import mock
33

4-
from test_excinfo import TWMock
5-
64
import _pytest._code
75
import pytest
86

@@ -168,17 +166,15 @@ def test_getsource(self):
168166

169167

170168
class TestReprFuncArgs:
171-
def test_not_raise_exception_with_mixed_encoding(self):
169+
def test_not_raise_exception_with_mixed_encoding(self, tw_mock):
172170
from _pytest._code.code import ReprFuncArgs
173171

174-
tw = TWMock()
175-
176172
args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")]
177173

178174
r = ReprFuncArgs(args)
179-
r.toterminal(tw)
175+
r.toterminal(tw_mock)
180176

181177
assert (
182-
tw.lines[0]
178+
tw_mock.lines[0]
183179
== r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'"
184180
)

0 commit comments

Comments
 (0)