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:
parent
26a2e1aba7
commit
0e00069340
|
@ -0,0 +1,2 @@
|
||||||
|
Fix a ``pytest-xdist`` crash when dealing with exceptions raised in subprocesses created by the
|
||||||
|
``multiprocessing`` module.
|
|
@ -374,8 +374,11 @@ def _report_to_json(report):
|
||||||
]
|
]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def serialize_repr_crash(reprcrash):
|
def serialize_repr_crash(reprcrash: Optional[ReprFileLocation]):
|
||||||
|
if reprcrash is not None:
|
||||||
return reprcrash.__dict__.copy()
|
return reprcrash.__dict__.copy()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def serialize_longrepr(rep):
|
def serialize_longrepr(rep):
|
||||||
result = {
|
result = {
|
||||||
|
@ -455,8 +458,11 @@ def _report_kwargs_from_json(reportdict):
|
||||||
]
|
]
|
||||||
return ReprTraceback(**repr_traceback_dict)
|
return ReprTraceback(**repr_traceback_dict)
|
||||||
|
|
||||||
def deserialize_repr_crash(repr_crash_dict):
|
def deserialize_repr_crash(repr_crash_dict: Optional[dict]):
|
||||||
|
if repr_crash_dict is not None:
|
||||||
return ReprFileLocation(**repr_crash_dict)
|
return ReprFileLocation(**repr_crash_dict)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
if (
|
if (
|
||||||
reportdict["longrepr"]
|
reportdict["longrepr"]
|
||||||
|
|
|
@ -305,6 +305,8 @@ class TestReportSerialization:
|
||||||
|
|
||||||
data = report._to_json()
|
data = report._to_json()
|
||||||
loaded_report = report_class._from_json(data)
|
loaded_report = report_class._from_json(data)
|
||||||
|
|
||||||
|
assert loaded_report.failed
|
||||||
check_longrepr(loaded_report.longrepr)
|
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
|
# 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.
|
# elsewhere and we do check the contents of the longrepr object after loading it.
|
||||||
loaded_report.longrepr.toterminal(tw_mock)
|
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:
|
class TestHooks:
|
||||||
"""Test that the hooks are working correctly for plugins"""
|
"""Test that the hooks are working correctly for plugins"""
|
||||||
|
|
Loading…
Reference in New Issue