diff --git a/changelog/1316.breaking.rst b/changelog/1316.breaking.rst new file mode 100644 index 000000000..4c01de728 --- /dev/null +++ b/changelog/1316.breaking.rst @@ -0,0 +1 @@ +``TestReport.longrepr`` is now always an instance of ``ReprExceptionInfo``. Previously it was a ``str`` when a test failed with ``pytest.fail(..., pytrace=False)``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 7e8cff2ed..7b17d7612 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -46,7 +46,7 @@ if TYPE_CHECKING: from typing_extensions import Literal from weakref import ReferenceType - _TracebackStyle = Literal["long", "short", "line", "no", "native"] + _TracebackStyle = Literal["long", "short", "line", "no", "native", "value"] class Code: @@ -583,7 +583,7 @@ class ExceptionInfo(Generic[_E]): Show locals per traceback entry. Ignored if ``style=="native"``. - :param str style: long|short|no|native traceback style + :param str style: long|short|no|native|value traceback style :param bool abspath: If paths should be changed to absolute or left unchanged. @@ -758,16 +758,15 @@ class FormattedExcinfo: def repr_traceback_entry( self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None ) -> "ReprEntry": - source = self._getentrysource(entry) - if source is None: - source = Source("???") - line_index = 0 - else: - line_index = entry.lineno - entry.getfirstlinesource() - lines = [] # type: List[str] style = entry._repr_style if entry._repr_style is not None else self.style if style in ("short", "long"): + source = self._getentrysource(entry) + if source is None: + source = Source("???") + line_index = 0 + else: + line_index = entry.lineno - entry.getfirstlinesource() short = style == "short" reprargs = self.repr_args(entry) if not short else None s = self.get_source(source, line_index, excinfo, short=short) @@ -780,9 +779,14 @@ class FormattedExcinfo: reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) localsrepr = self.repr_locals(entry.locals) return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) - if excinfo: - lines.extend(self.get_exconly(excinfo, indent=4)) - return ReprEntry(lines, None, None, None, style) + elif style == "value": + if excinfo: + lines.extend(str(excinfo.value).split("\n")) + return ReprEntry(lines, None, None, None, style) + else: + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, style) def _makepath(self, path): if not self.abspath: @@ -806,6 +810,11 @@ class FormattedExcinfo: last = traceback[-1] entries = [] + if self.style == "value": + reprentry = self.repr_traceback_entry(last, excinfo) + entries.append(reprentry) + return ReprTraceback(entries, None, style=self.style) + for index, entry in enumerate(traceback): einfo = (last == entry) and excinfo or None reprentry = self.repr_traceback_entry(entry, einfo) @@ -865,7 +874,9 @@ class FormattedExcinfo: seen.add(id(e)) if excinfo_: reprtraceback = self.repr_traceback(excinfo_) - reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation] + reprcrash = ( + excinfo_._getreprcrash() if self.style != "value" else None + ) # type: Optional[ReprFileLocation] else: # fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work @@ -1048,8 +1059,11 @@ class ReprEntry(TerminalRepr): "Unexpected failure lines between source lines:\n" + "\n".join(self.lines) ) - indents.append(line[:indent_size]) - source_lines.append(line[indent_size:]) + if self.style == "value": + source_lines.append(line) + else: + indents.append(line[:indent_size]) + source_lines.append(line[indent_size:]) else: seeing_failures = True failure_lines.append(line) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 77e184312..4a1afc63e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -202,10 +202,8 @@ class _NodeReporter: if hasattr(report, "wasxfail"): self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly") else: - if hasattr(report.longrepr, "reprcrash"): + if getattr(report.longrepr, "reprcrash", None) is not None: message = report.longrepr.reprcrash.message - elif isinstance(report.longrepr, str): - message = report.longrepr else: message = str(report.longrepr) message = bin_xml_escape(message) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 2ed250610..4a79bc861 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -337,7 +337,7 @@ class Node(metaclass=NodeMeta): excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): if not excinfo.value.pytrace: - return str(excinfo.value) + style = "value" if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): diff --git a/testing/test_runner.py b/testing/test_runner.py index 00732d03b..7b0b27a4b 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1002,6 +1002,17 @@ class TestReportContents: assert rep.capstdout == "" assert rep.capstderr == "" + def test_longrepr_type(self, testdir) -> None: + reports = testdir.runitem( + """ + import pytest + def test_func(): + pytest.fail(pytrace=False) + """ + ) + rep = reports[1] + assert isinstance(rep.longrepr, _pytest._code.code.ExceptionRepr) + def test_outcome_exception_bad_msg() -> None: """Check that OutcomeExceptions validate their input to prevent confusing errors (#5578)""" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 32634d784..f48e78364 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -194,7 +194,7 @@ class TestXFail: assert len(reports) == 3 callreport = reports[1] assert callreport.failed - assert callreport.longrepr == "[XPASS(strict)] nope" + assert str(callreport.longrepr) == "[XPASS(strict)] nope" assert not hasattr(callreport, "wasxfail") def test_xfail_run_anyway(self, testdir):