diff --git a/AUTHORS b/AUTHORS index a4c7f8568..1aa5265e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -128,6 +128,7 @@ Erik M. Bray Evan Kepner Fabien Zarifian Fabio Zadrozny +Felix Hofstätter Felix Nieuwenhuizen Feng Ma Florian Bruhin diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 328ab55f3..67d1062ff 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -411,13 +411,14 @@ class Traceback(List[TracebackEntry]): """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self) -> TracebackEntry: - """Return last non-hidden traceback entry that lead to the exception of a traceback.""" + def getcrashentry(self) -> Optional[TracebackEntry]: + """Return last non-hidden traceback entry that lead to the exception of + a traceback, or None if all hidden.""" for i in range(-1, -len(self) - 1, -1): entry = self[i] if not entry.ishidden(): return entry - return self[-1] + return None def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if @@ -621,9 +622,11 @@ class ExceptionInfo(Generic[E]): """ return isinstance(self.value, exc) - def _getreprcrash(self) -> "ReprFileLocation": + def _getreprcrash(self) -> Optional["ReprFileLocation"]: exconly = self.exconly(tryshort=True) entry = self.traceback.getcrashentry() + if entry is None: + return None path, lineno = entry.frame.code.raw.co_filename, entry.lineno return ReprFileLocation(path, lineno + 1, exconly) @@ -670,7 +673,9 @@ class ExceptionInfo(Generic[E]): return ReprExceptionInfo( reprtraceback=ReprTracebackNative( traceback.format_exception( - self.type, self.value, self.traceback[0]._rawentry + self.type, + self.value, + self.traceback[0]._rawentry if self.traceback else None, ) ), reprcrash=self._getreprcrash(), @@ -826,12 +831,16 @@ class FormattedExcinfo: def repr_traceback_entry( self, - entry: TracebackEntry, + entry: Optional[TracebackEntry], excinfo: Optional[ExceptionInfo[BaseException]] = None, ) -> "ReprEntry": lines: List[str] = [] - style = entry._repr_style if entry._repr_style is not None else self.style - if style in ("short", "long"): + style = ( + entry._repr_style + if entry is not None and entry._repr_style is not None + else self.style + ) + if style in ("short", "long") and entry is not None: source = self._getentrysource(entry) if source is None: source = Source("???") @@ -880,17 +889,21 @@ class FormattedExcinfo: else: extraline = None + if not traceback: + if extraline is None: + extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." + entries = [self.repr_traceback_entry(None, excinfo)] + return ReprTraceback(entries, extraline, style=self.style) + last = traceback[-1] - entries = [] if self.style == "value": - reprentry = self.repr_traceback_entry(last, excinfo) - entries.append(reprentry) + entries = [self.repr_traceback_entry(last, excinfo)] 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) - entries.append(reprentry) + entries = [ + self.repr_traceback_entry(entry, excinfo if last == entry else None) + for entry in traceback + ] return ReprTraceback(entries, extraline, style=self.style) def _truncate_recursive_traceback( @@ -947,6 +960,7 @@ class FormattedExcinfo: seen: Set[int] = set() while e is not None and id(e) not in seen: seen.add(id(e)) + if excinfo_: # Fall back to native traceback as a temporary workaround until # full support for exception groups added to ExceptionInfo. @@ -973,8 +987,8 @@ class FormattedExcinfo: traceback.format_exception(type(e), e, None) ) reprcrash = None - repr_chain += [(reprtraceback, reprcrash, descr)] + if e.__cause__ is not None and self.chain: e = e.__cause__ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None @@ -1057,7 +1071,7 @@ class ExceptionChainRepr(ExceptionRepr): @dataclasses.dataclass(eq=False) class ReprExceptionInfo(ExceptionRepr): reprtraceback: "ReprTraceback" - reprcrash: "ReprFileLocation" + reprcrash: Optional["ReprFileLocation"] def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) @@ -1162,8 +1176,8 @@ class ReprEntry(TerminalRepr): def toterminal(self, tw: TerminalWriter) -> None: if self.style == "short": - assert self.reprfileloc is not None - self.reprfileloc.toterminal(tw) + if self.reprfileloc: + self.reprfileloc.toterminal(tw) self._write_entry_lines(tw) if self.reprlocals: self.reprlocals.toterminal(tw, indent=" " * 8) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c74740dbc..ea016786e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -452,10 +452,7 @@ class Node(metaclass=NodeMeta): if self.config.getoption("fulltrace", False): style = "long" else: - tb = _pytest._code.Traceback([excinfo.traceback[-1]]) self._prunetraceback(excinfo) - if len(excinfo.traceback) == 0: - excinfo.traceback = tb if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 2e36514ea..74e8794b2 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -347,6 +347,9 @@ class TestReport(BaseReport): elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() + assert ( + r is not None + ), "There should always be a traceback entry for skipping a test." if excinfo.value._use_item_location: path, line = item.reportinfo()[:2] assert line is not None diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 5b83d2ebf..6a720e64c 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -308,6 +308,7 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback entry = tb.getcrashentry() + assert entry is not None co = _pytest._code.Code.from_function(h) assert entry.frame.code.path == co.path assert entry.lineno == co.firstlineno + 1 @@ -323,12 +324,7 @@ class TestTraceback_f_g_h: g() excinfo = pytest.raises(ValueError, f) - tb = excinfo.traceback - entry = tb.getcrashentry() - co = _pytest._code.Code.from_function(g) - assert entry.frame.code.path == co.path - assert entry.lineno == co.firstlineno + 2 - assert entry.frame.code.name == "g" + assert excinfo.traceback.getcrashentry() is None def test_excinfo_exconly(): @@ -1591,12 +1587,9 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None: _exceptiongroup_common(pytester, outer_chain, inner_chain, native=False) -def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None: - """Regression test for #10903. - - We're not really sure what should be *displayed* here, so this test - just verified that at least it doesn't crash. - """ +@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native")) +def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None: + """Regression test for #10903.""" pytester.makepyfile( """ def test(): @@ -1604,5 +1597,9 @@ def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None: 1 / 0 """ ) - result = pytester.runpytest() + result = pytester.runpytest("--tb", tbstyle) assert result.ret == 1 + if tbstyle != "line": + result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"]) + if tbstyle not in ("line", "native"): + result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])