Skip to content

Commit 0e00069

Browse files
committed
Fix serialization of 'None' reprcrashes
Tracebacks coming from remote processes crated by the multiprocess module will contain "RemoteTracebacks" which don't have a 'reprcrash' attribute Fix #5971
1 parent 26a2e1a commit 0e00069

File tree

3 files changed

+74
-4
lines changed

3 files changed

+74
-4
lines changed

changelog/5971.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a ``pytest-xdist`` crash when dealing with exceptions raised in subprocesses created by the
2+
``multiprocessing`` module.

src/_pytest/reports.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,11 @@ def serialize_repr_traceback(reprtraceback):
374374
]
375375
return result
376376

377-
def serialize_repr_crash(reprcrash):
378-
return reprcrash.__dict__.copy()
377+
def serialize_repr_crash(reprcrash: Optional[ReprFileLocation]):
378+
if reprcrash is not None:
379+
return reprcrash.__dict__.copy()
380+
else:
381+
return None
379382

380383
def serialize_longrepr(rep):
381384
result = {
@@ -455,8 +458,11 @@ def deserialize_repr_traceback(repr_traceback_dict):
455458
]
456459
return ReprTraceback(**repr_traceback_dict)
457460

458-
def deserialize_repr_crash(repr_crash_dict):
459-
return ReprFileLocation(**repr_crash_dict)
461+
def deserialize_repr_crash(repr_crash_dict: Optional[dict]):
462+
if repr_crash_dict is not None:
463+
return ReprFileLocation(**repr_crash_dict)
464+
else:
465+
return None
460466

461467
if (
462468
reportdict["longrepr"]

testing/test_reports.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,75 @@ def check_longrepr(longrepr):
305305

306306
data = report._to_json()
307307
loaded_report = report_class._from_json(data)
308+
309+
assert loaded_report.failed
308310
check_longrepr(loaded_report.longrepr)
309311

310312
# make sure we don't blow up on ``toterminal`` call; we don't test the actual output because it is very
311313
# brittle and hard to maintain, but we can assume it is correct because ``toterminal`` is already tested
312314
# elsewhere and we do check the contents of the longrepr object after loading it.
313315
loaded_report.longrepr.toterminal(tw_mock)
314316

317+
def test_chained_exceptions_no_reprcrash(
318+
self, testdir, tw_mock,
319+
):
320+
"""Regression test for tracebacks without a reprcrash (#5971)
321+
322+
This happens notably on exceptions raised by multiprocess.pool: the exception transfer
323+
from subprocess to main process creates an artificial exception which, ExceptionInfo
324+
can't obtain the ReprFileLocation from.
325+
"""
326+
testdir.makepyfile(
327+
"""
328+
# equivalent of multiprocessing.pool.RemoteTraceback
329+
class RemoteTraceback(Exception):
330+
def __init__(self, tb):
331+
self.tb = tb
332+
def __str__(self):
333+
return self.tb
334+
335+
def test_a():
336+
try:
337+
raise ValueError('value error')
338+
except ValueError as e:
339+
# equivalent to how multiprocessing.pool.rebuild_exc does it
340+
e.__cause__ = RemoteTraceback('runtime error')
341+
raise e
342+
"""
343+
)
344+
reprec = testdir.inline_run()
345+
346+
reports = reprec.getreports("pytest_runtest_logreport")
347+
348+
def check_longrepr(longrepr):
349+
assert isinstance(longrepr, ExceptionChainRepr)
350+
assert len(longrepr.chain) == 2
351+
entry1, entry2 = longrepr.chain
352+
tb1, fileloc1, desc1 = entry1
353+
tb2, fileloc2, desc2 = entry2
354+
355+
assert "RemoteTraceback: runtime error" in str(tb1)
356+
assert "ValueError('value error')" in str(tb2)
357+
358+
assert fileloc1 is None
359+
assert fileloc2.message == "ValueError: value error"
360+
361+
# 3 reports: setup/call/teardown: get the call report
362+
assert len(reports) == 3
363+
report = reports[1]
364+
365+
assert report.failed
366+
check_longrepr(report.longrepr)
367+
368+
data = report._to_json()
369+
loaded_report = TestReport._from_json(data)
370+
371+
assert loaded_report.failed
372+
check_longrepr(loaded_report.longrepr)
373+
374+
# for same reasons as previous test, ensure we don't blow up here
375+
loaded_report.longrepr.toterminal(tw_mock)
376+
315377

316378
class TestHooks:
317379
"""Test that the hooks are working correctly for plugins"""

0 commit comments

Comments
 (0)