Skip to content

Commit cff7843

Browse files
authored
Merge pull request #6412 from nicoddemus/remote-tb-5971
Fix serialization of 'None' reprcrashes
2 parents f46ad8d + 356d865 commit cff7843

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,69 @@ 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+
from concurrent.futures import ProcessPoolExecutor
329+
330+
def func():
331+
raise ValueError('value error')
332+
333+
def test_a():
334+
with ProcessPoolExecutor() as p:
335+
p.submit(func).result()
336+
"""
337+
)
338+
reprec = testdir.inline_run()
339+
340+
reports = reprec.getreports("pytest_runtest_logreport")
341+
342+
def check_longrepr(longrepr):
343+
assert isinstance(longrepr, ExceptionChainRepr)
344+
assert len(longrepr.chain) == 2
345+
entry1, entry2 = longrepr.chain
346+
tb1, fileloc1, desc1 = entry1
347+
tb2, fileloc2, desc2 = entry2
348+
349+
assert "RemoteTraceback" in str(tb1)
350+
assert "ValueError: value error" in str(tb2)
351+
352+
assert fileloc1 is None
353+
assert fileloc2.message == "ValueError: value error"
354+
355+
# 3 reports: setup/call/teardown: get the call report
356+
assert len(reports) == 3
357+
report = reports[1]
358+
359+
assert report.failed
360+
check_longrepr(report.longrepr)
361+
362+
data = report._to_json()
363+
loaded_report = TestReport._from_json(data)
364+
365+
assert loaded_report.failed
366+
check_longrepr(loaded_report.longrepr)
367+
368+
# for same reasons as previous test, ensure we don't blow up here
369+
loaded_report.longrepr.toterminal(tw_mock)
370+
315371

316372
class TestHooks:
317373
"""Test that the hooks are working correctly for plugins"""

0 commit comments

Comments
 (0)