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):
 | 
				
			||||||
 | 
					            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:
 | 
				
			||||||
 | 
					            result["chain"] = None
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    d = test_report.__dict__.copy()
 | 
					    d = report.__dict__.copy()
 | 
				
			||||||
    if hasattr(test_report.longrepr, "toterminal"):
 | 
					    if hasattr(report.longrepr, "toterminal"):
 | 
				
			||||||
        if hasattr(test_report.longrepr, "reprtraceback") and hasattr(
 | 
					        if hasattr(report.longrepr, "reprtraceback") and hasattr(
 | 
				
			||||||
            test_report.longrepr, "reprcrash"
 | 
					            report.longrepr, "reprcrash"
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            d["longrepr"] = serialize_longrepr(test_report)
 | 
					            d["longrepr"] = serialize_longrepr(report)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            d["longrepr"] = str(test_report.longrepr)
 | 
					            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