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
This commit is contained in:
Bruno Oliveira 2020-01-06 21:16:23 -03:00
parent 26a2e1aba7
commit 0e00069340
3 changed files with 74 additions and 4 deletions

View File

@ -0,0 +1,2 @@
Fix a ``pytest-xdist`` crash when dealing with exceptions raised in subprocesses created by the
``multiprocessing`` module.

View File

@ -374,8 +374,11 @@ def _report_to_json(report):
]
return result
def serialize_repr_crash(reprcrash):
return reprcrash.__dict__.copy()
def serialize_repr_crash(reprcrash: Optional[ReprFileLocation]):
if reprcrash is not None:
return reprcrash.__dict__.copy()
else:
return None
def serialize_longrepr(rep):
result = {
@ -455,8 +458,11 @@ def _report_kwargs_from_json(reportdict):
]
return ReprTraceback(**repr_traceback_dict)
def deserialize_repr_crash(repr_crash_dict):
return ReprFileLocation(**repr_crash_dict)
def deserialize_repr_crash(repr_crash_dict: Optional[dict]):
if repr_crash_dict is not None:
return ReprFileLocation(**repr_crash_dict)
else:
return None
if (
reportdict["longrepr"]

View File

@ -305,6 +305,8 @@ class TestReportSerialization:
data = report._to_json()
loaded_report = report_class._from_json(data)
assert loaded_report.failed
check_longrepr(loaded_report.longrepr)
# make sure we don't blow up on ``toterminal`` call; we don't test the actual output because it is very
@ -312,6 +314,66 @@ class TestReportSerialization:
# elsewhere and we do check the contents of the longrepr object after loading it.
loaded_report.longrepr.toterminal(tw_mock)
def test_chained_exceptions_no_reprcrash(
self, testdir, tw_mock,
):
"""Regression test for tracebacks without a reprcrash (#5971)
This happens notably on exceptions raised by multiprocess.pool: the exception transfer
from subprocess to main process creates an artificial exception which, ExceptionInfo
can't obtain the ReprFileLocation from.
"""
testdir.makepyfile(
"""
# equivalent of multiprocessing.pool.RemoteTraceback
class RemoteTraceback(Exception):
def __init__(self, tb):
self.tb = tb
def __str__(self):
return self.tb
def test_a():
try:
raise ValueError('value error')
except ValueError as e:
# equivalent to how multiprocessing.pool.rebuild_exc does it
e.__cause__ = RemoteTraceback('runtime error')
raise e
"""
)
reprec = testdir.inline_run()
reports = reprec.getreports("pytest_runtest_logreport")
def check_longrepr(longrepr):
assert isinstance(longrepr, ExceptionChainRepr)
assert len(longrepr.chain) == 2
entry1, entry2 = longrepr.chain
tb1, fileloc1, desc1 = entry1
tb2, fileloc2, desc2 = entry2
assert "RemoteTraceback: runtime error" in str(tb1)
assert "ValueError('value error')" in str(tb2)
assert fileloc1 is None
assert fileloc2.message == "ValueError: value error"
# 3 reports: setup/call/teardown: get the call report
assert len(reports) == 3
report = reports[1]
assert report.failed
check_longrepr(report.longrepr)
data = report._to_json()
loaded_report = TestReport._from_json(data)
assert loaded_report.failed
check_longrepr(loaded_report.longrepr)
# for same reasons as previous test, ensure we don't blow up here
loaded_report.longrepr.toterminal(tw_mock)
class TestHooks:
"""Test that the hooks are working correctly for plugins"""