parent
7a69365486
commit
a511b98da9
|
@ -0,0 +1,2 @@
|
|||
Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like
|
||||
``pytest-xdist`` to display them properly.
|
|
@ -3,6 +3,7 @@ from typing import Optional
|
|||
|
||||
import py
|
||||
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ReprEntry
|
||||
from _pytest._code.code import ReprEntryNative
|
||||
|
@ -160,7 +161,7 @@ class BaseReport:
|
|||
|
||||
Experimental method.
|
||||
"""
|
||||
return _test_report_to_json(self)
|
||||
return _report_to_json(self)
|
||||
|
||||
@classmethod
|
||||
def _from_json(cls, reportdict):
|
||||
|
@ -172,7 +173,7 @@ class BaseReport:
|
|||
|
||||
Experimental method.
|
||||
"""
|
||||
kwargs = _test_report_kwargs_from_json(reportdict)
|
||||
kwargs = _report_kwargs_from_json(reportdict)
|
||||
return cls(**kwargs)
|
||||
|
||||
|
||||
|
@ -340,7 +341,7 @@ def pytest_report_from_serializable(data):
|
|||
)
|
||||
|
||||
|
||||
def _test_report_to_json(test_report):
|
||||
def _report_to_json(report):
|
||||
"""
|
||||
This was originally the serialize_report() function from xdist (ca03269).
|
||||
|
||||
|
@ -366,22 +367,35 @@ def _test_report_to_json(test_report):
|
|||
return reprcrash.__dict__.copy()
|
||||
|
||||
def serialize_longrepr(rep):
|
||||
return {
|
||||
result = {
|
||||
"reprcrash": serialize_repr_crash(rep.longrepr.reprcrash),
|
||||
"reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback),
|
||||
"sections": rep.longrepr.sections,
|
||||
}
|
||||
|
||||
d = test_report.__dict__.copy()
|
||||
if hasattr(test_report.longrepr, "toterminal"):
|
||||
if hasattr(test_report.longrepr, "reprtraceback") and hasattr(
|
||||
test_report.longrepr, "reprcrash"
|
||||
):
|
||||
d["longrepr"] = serialize_longrepr(test_report)
|
||||
if isinstance(rep.longrepr, ExceptionChainRepr):
|
||||
result["chain"] = []
|
||||
for repr_traceback, repr_crash, description in rep.longrepr.chain:
|
||||
result["chain"].append(
|
||||
(
|
||||
serialize_repr_traceback(repr_traceback),
|
||||
serialize_repr_crash(repr_crash),
|
||||
description,
|
||||
)
|
||||
)
|
||||
else:
|
||||
d["longrepr"] = str(test_report.longrepr)
|
||||
result["chain"] = None
|
||||
return result
|
||||
|
||||
d = report.__dict__.copy()
|
||||
if hasattr(report.longrepr, "toterminal"):
|
||||
if hasattr(report.longrepr, "reprtraceback") and hasattr(
|
||||
report.longrepr, "reprcrash"
|
||||
):
|
||||
d["longrepr"] = serialize_longrepr(report)
|
||||
else:
|
||||
d["longrepr"] = str(report.longrepr)
|
||||
else:
|
||||
d["longrepr"] = test_report.longrepr
|
||||
d["longrepr"] = report.longrepr
|
||||
for name in d:
|
||||
if isinstance(d[name], (py.path.local, Path)):
|
||||
d[name] = str(d[name])
|
||||
|
@ -390,12 +404,11 @@ def _test_report_to_json(test_report):
|
|||
return d
|
||||
|
||||
|
||||
def _test_report_kwargs_from_json(reportdict):
|
||||
def _report_kwargs_from_json(reportdict):
|
||||
"""
|
||||
This was originally the serialize_report() function from xdist (ca03269).
|
||||
|
||||
Factory method that returns either a TestReport or CollectReport, depending on the calling
|
||||
class. It's the callers responsibility to know which class to pass here.
|
||||
Returns **kwargs that can be used to construct a TestReport or CollectReport instance.
|
||||
"""
|
||||
|
||||
def deserialize_repr_entry(entry_data):
|
||||
|
@ -439,12 +452,26 @@ def _test_report_kwargs_from_json(reportdict):
|
|||
and "reprcrash" in reportdict["longrepr"]
|
||||
and "reprtraceback" in reportdict["longrepr"]
|
||||
):
|
||||
exception_info = ReprExceptionInfo(
|
||||
reprtraceback=deserialize_repr_traceback(
|
||||
reportdict["longrepr"]["reprtraceback"]
|
||||
),
|
||||
reprcrash=deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]),
|
||||
|
||||
reprtraceback = deserialize_repr_traceback(
|
||||
reportdict["longrepr"]["reprtraceback"]
|
||||
)
|
||||
reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
|
||||
if reportdict["longrepr"]["chain"]:
|
||||
chain = []
|
||||
for repr_traceback_data, repr_crash_data, description in reportdict[
|
||||
"longrepr"
|
||||
]["chain"]:
|
||||
chain.append(
|
||||
(
|
||||
deserialize_repr_traceback(repr_traceback_data),
|
||||
deserialize_repr_crash(repr_crash_data),
|
||||
description,
|
||||
)
|
||||
)
|
||||
exception_info = ExceptionChainRepr(chain)
|
||||
else:
|
||||
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
|
||||
|
||||
for section in reportdict["longrepr"]["sections"]:
|
||||
exception_info.addsection(*section)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
@ -220,8 +221,8 @@ class TestReportSerialization:
|
|||
assert data["path1"] == str(testdir.tmpdir)
|
||||
assert data["path2"] == str(testdir.tmpdir)
|
||||
|
||||
def test_unserialization_failure(self, testdir):
|
||||
"""Check handling of failure during unserialization of report types."""
|
||||
def test_deserialization_failure(self, testdir):
|
||||
"""Check handling of failure during deserialization of report types."""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test_a():
|
||||
|
@ -242,6 +243,75 @@ class TestReportSerialization:
|
|||
):
|
||||
TestReport._from_json(data)
|
||||
|
||||
@pytest.mark.parametrize("report_class", [TestReport, CollectReport])
|
||||
def test_chained_exceptions(self, testdir, tw_mock, report_class):
|
||||
"""Check serialization/deserialization of report objects containing chained exceptions (#5786)"""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def foo():
|
||||
raise ValueError('value error')
|
||||
def test_a():
|
||||
try:
|
||||
foo()
|
||||
except ValueError as e:
|
||||
raise RuntimeError('runtime error') from e
|
||||
if {error_during_import}:
|
||||
test_a()
|
||||
""".format(
|
||||
error_during_import=report_class is CollectReport
|
||||
)
|
||||
)
|
||||
|
||||
reprec = testdir.inline_run()
|
||||
if report_class is TestReport:
|
||||
reports = reprec.getreports("pytest_runtest_logreport")
|
||||
# we have 3 reports: setup/call/teardown
|
||||
assert len(reports) == 3
|
||||
# get the call report
|
||||
report = reports[1]
|
||||
else:
|
||||
assert report_class is CollectReport
|
||||
# two collection reports: session and test file
|
||||
reports = reprec.getreports("pytest_collectreport")
|
||||
assert len(reports) == 2
|
||||
report = reports[1]
|
||||
|
||||
def check_longrepr(longrepr):
|
||||
"""Check the attributes of the given longrepr object according to the test file.
|
||||
|
||||
We can get away with testing both CollectReport and TestReport with this function because
|
||||
the longrepr objects are very similar.
|
||||
"""
|
||||
assert isinstance(longrepr, ExceptionChainRepr)
|
||||
assert longrepr.sections == [("title", "contents", "=")]
|
||||
assert len(longrepr.chain) == 2
|
||||
entry1, entry2 = longrepr.chain
|
||||
tb1, fileloc1, desc1 = entry1
|
||||
tb2, fileloc2, desc2 = entry2
|
||||
|
||||
assert "ValueError('value error')" in str(tb1)
|
||||
assert "RuntimeError('runtime error')" in str(tb2)
|
||||
|
||||
assert (
|
||||
desc1
|
||||
== "The above exception was the direct cause of the following exception:"
|
||||
)
|
||||
assert desc2 is None
|
||||
|
||||
assert report.failed
|
||||
assert len(report.sections) == 0
|
||||
report.longrepr.addsection("title", "contents", "=")
|
||||
check_longrepr(report.longrepr)
|
||||
|
||||
data = report._to_json()
|
||||
loaded_report = report_class._from_json(data)
|
||||
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
|
||||
# brittle and hard to maintain, but we can assume it is correct because ``toterminal`` is already tested
|
||||
# elsewhere and we do check the contents of the longrepr object after loading it.
|
||||
loaded_report.longrepr.toterminal(tw_mock)
|
||||
|
||||
|
||||
class TestHooks:
|
||||
"""Test that the hooks are working correctly for plugins"""
|
||||
|
|
Loading…
Reference in New Issue