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
|
import py
|
||||||
|
|
||||||
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import ReprEntry
|
from _pytest._code.code import ReprEntry
|
||||||
from _pytest._code.code import ReprEntryNative
|
from _pytest._code.code import ReprEntryNative
|
||||||
|
@ -160,7 +161,7 @@ class BaseReport:
|
||||||
|
|
||||||
Experimental method.
|
Experimental method.
|
||||||
"""
|
"""
|
||||||
return _test_report_to_json(self)
|
return _report_to_json(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_json(cls, reportdict):
|
def _from_json(cls, reportdict):
|
||||||
|
@ -172,7 +173,7 @@ class BaseReport:
|
||||||
|
|
||||||
Experimental method.
|
Experimental method.
|
||||||
"""
|
"""
|
||||||
kwargs = _test_report_kwargs_from_json(reportdict)
|
kwargs = _report_kwargs_from_json(reportdict)
|
||||||
return cls(**kwargs)
|
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).
|
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()
|
return reprcrash.__dict__.copy()
|
||||||
|
|
||||||
def serialize_longrepr(rep):
|
def serialize_longrepr(rep):
|
||||||
return {
|
result = {
|
||||||
"reprcrash": serialize_repr_crash(rep.longrepr.reprcrash),
|
"reprcrash": serialize_repr_crash(rep.longrepr.reprcrash),
|
||||||
"reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback),
|
"reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback),
|
||||||
"sections": rep.longrepr.sections,
|
"sections": rep.longrepr.sections,
|
||||||
}
|
}
|
||||||
|
if isinstance(rep.longrepr, ExceptionChainRepr):
|
||||||
d = test_report.__dict__.copy()
|
result["chain"] = []
|
||||||
if hasattr(test_report.longrepr, "toterminal"):
|
for repr_traceback, repr_crash, description in rep.longrepr.chain:
|
||||||
if hasattr(test_report.longrepr, "reprtraceback") and hasattr(
|
result["chain"].append(
|
||||||
test_report.longrepr, "reprcrash"
|
(
|
||||||
):
|
serialize_repr_traceback(repr_traceback),
|
||||||
d["longrepr"] = serialize_longrepr(test_report)
|
serialize_repr_crash(repr_crash),
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
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:
|
else:
|
||||||
d["longrepr"] = test_report.longrepr
|
d["longrepr"] = report.longrepr
|
||||||
for name in d:
|
for name in d:
|
||||||
if isinstance(d[name], (py.path.local, Path)):
|
if isinstance(d[name], (py.path.local, Path)):
|
||||||
d[name] = str(d[name])
|
d[name] = str(d[name])
|
||||||
|
@ -390,12 +404,11 @@ def _test_report_to_json(test_report):
|
||||||
return d
|
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).
|
This was originally the serialize_report() function from xdist (ca03269).
|
||||||
|
|
||||||
Factory method that returns either a TestReport or CollectReport, depending on the calling
|
Returns **kwargs that can be used to construct a TestReport or CollectReport instance.
|
||||||
class. It's the callers responsibility to know which class to pass here.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def deserialize_repr_entry(entry_data):
|
def deserialize_repr_entry(entry_data):
|
||||||
|
@ -439,12 +452,26 @@ def _test_report_kwargs_from_json(reportdict):
|
||||||
and "reprcrash" in reportdict["longrepr"]
|
and "reprcrash" in reportdict["longrepr"]
|
||||||
and "reprtraceback" in reportdict["longrepr"]
|
and "reprtraceback" in reportdict["longrepr"]
|
||||||
):
|
):
|
||||||
exception_info = ReprExceptionInfo(
|
|
||||||
reprtraceback=deserialize_repr_traceback(
|
reprtraceback = deserialize_repr_traceback(
|
||||||
reportdict["longrepr"]["reprtraceback"]
|
reportdict["longrepr"]["reprtraceback"]
|
||||||
),
|
|
||||||
reprcrash=deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]),
|
|
||||||
)
|
)
|
||||||
|
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"]:
|
for section in reportdict["longrepr"]["sections"]:
|
||||||
exception_info.addsection(*section)
|
exception_info.addsection(*section)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
from _pytest.reports import CollectReport
|
from _pytest.reports import CollectReport
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
@ -220,8 +221,8 @@ class TestReportSerialization:
|
||||||
assert data["path1"] == str(testdir.tmpdir)
|
assert data["path1"] == str(testdir.tmpdir)
|
||||||
assert data["path2"] == str(testdir.tmpdir)
|
assert data["path2"] == str(testdir.tmpdir)
|
||||||
|
|
||||||
def test_unserialization_failure(self, testdir):
|
def test_deserialization_failure(self, testdir):
|
||||||
"""Check handling of failure during unserialization of report types."""
|
"""Check handling of failure during deserialization of report types."""
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_a():
|
def test_a():
|
||||||
|
@ -242,6 +243,75 @@ class TestReportSerialization:
|
||||||
):
|
):
|
||||||
TestReport._from_json(data)
|
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:
|
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