code: handle repr'ing empty tracebacks gracefully

By "empty traceback" I mean a traceback all of whose entries have been
filtered/cut/pruned out.

Currently, if an empty traceback needs to be repr'ed, the last entry
before the filtering is used instead (added in
accd962c9f).

Showing a hidden frame is not so good IMO. This commit does the
following instead:

1. Shows details of the exception.
2. Shows a message about how the full trace can be seen.

Example:

```
_____________ test _____________

E   ZeroDivisionError: division by zero
All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.
```

Also handles `--tb=native`, though there the `--full-trace` bit is not
shown.

This commit contains some pieces from
431ec6d34e (which has been reverted).

Helps towards fixing issue # 1904.

Co-authored-by: Felix Hofstätter <Felhof1@hotmail.com>
This commit is contained in:
Ran Benita 2023-04-12 22:03:31 +03:00
parent eff54aece1
commit e3b1799766
5 changed files with 47 additions and 35 deletions

View File

@ -128,6 +128,7 @@ Erik M. Bray
Evan Kepner Evan Kepner
Fabien Zarifian Fabien Zarifian
Fabio Zadrozny Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen Felix Nieuwenhuizen
Feng Ma Feng Ma
Florian Bruhin Florian Bruhin

View File

@ -411,13 +411,14 @@ class Traceback(List[TracebackEntry]):
""" """
return Traceback(filter(fn, self), self._excinfo) return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> TracebackEntry: def getcrashentry(self) -> Optional[TracebackEntry]:
"""Return last non-hidden traceback entry that lead to the exception of a traceback.""" """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): for i in range(-1, -len(self) - 1, -1):
entry = self[i] entry = self[i]
if not entry.ishidden(): if not entry.ishidden():
return entry return entry
return self[-1] return None
def recursionindex(self) -> Optional[int]: def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if """Return the index of the frame/TracebackEntry where recursion originates if
@ -598,9 +599,11 @@ class ExceptionInfo(Generic[E]):
""" """
return isinstance(self.value, exc) return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation": def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True) exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry() entry = self.traceback.getcrashentry()
if entry is None:
return None
path, lineno = entry.frame.code.raw.co_filename, entry.lineno path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly) return ReprFileLocation(path, lineno + 1, exconly)
@ -647,7 +650,9 @@ class ExceptionInfo(Generic[E]):
return ReprExceptionInfo( return ReprExceptionInfo(
reprtraceback=ReprTracebackNative( reprtraceback=ReprTracebackNative(
traceback.format_exception( 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(), reprcrash=self._getreprcrash(),
@ -803,12 +808,16 @@ class FormattedExcinfo:
def repr_traceback_entry( def repr_traceback_entry(
self, self,
entry: TracebackEntry, entry: Optional[TracebackEntry],
excinfo: Optional[ExceptionInfo[BaseException]] = None, excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry": ) -> "ReprEntry":
lines: List[str] = [] lines: List[str] = []
style = entry._repr_style if entry._repr_style is not None else self.style style = (
if style in ("short", "long"): 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) source = self._getentrysource(entry)
if source is None: if source is None:
source = Source("???") source = Source("???")
@ -857,17 +866,21 @@ class FormattedExcinfo:
else: else:
extraline = None 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] last = traceback[-1]
entries = []
if self.style == "value": if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo) entries = [self.repr_traceback_entry(last, excinfo)]
entries.append(reprentry)
return ReprTraceback(entries, None, style=self.style) return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback): entries = [
einfo = (last == entry) and excinfo or None self.repr_traceback_entry(entry, excinfo if last == entry else None)
reprentry = self.repr_traceback_entry(entry, einfo) for entry in traceback
entries.append(reprentry) ]
return ReprTraceback(entries, extraline, style=self.style) return ReprTraceback(entries, extraline, style=self.style)
def _truncate_recursive_traceback( def _truncate_recursive_traceback(
@ -924,6 +937,7 @@ class FormattedExcinfo:
seen: Set[int] = set() seen: Set[int] = set()
while e is not None and id(e) not in seen: while e is not None and id(e) not in seen:
seen.add(id(e)) seen.add(id(e))
if excinfo_: if excinfo_:
# Fall back to native traceback as a temporary workaround until # Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo. # full support for exception groups added to ExceptionInfo.
@ -950,8 +964,8 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None) traceback.format_exception(type(e), e, None)
) )
reprcrash = None reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)] repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain: if e.__cause__ is not None and self.chain:
e = e.__cause__ e = e.__cause__
excinfo_ = ( excinfo_ = (
@ -1042,7 +1056,7 @@ class ExceptionChainRepr(ExceptionRepr):
@dataclasses.dataclass(eq=False) @dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback" reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation" reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw) self.reprtraceback.toterminal(tw)
@ -1147,8 +1161,8 @@ class ReprEntry(TerminalRepr):
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
if self.style == "short": if self.style == "short":
assert self.reprfileloc is not None if self.reprfileloc:
self.reprfileloc.toterminal(tw) self.reprfileloc.toterminal(tw)
self._write_entry_lines(tw) self._write_entry_lines(tw)
if self.reprlocals: if self.reprlocals:
self.reprlocals.toterminal(tw, indent=" " * 8) self.reprlocals.toterminal(tw, indent=" " * 8)

View File

@ -452,10 +452,7 @@ class Node(metaclass=NodeMeta):
if self.config.getoption("fulltrace", False): if self.config.getoption("fulltrace", False):
style = "long" style = "long"
else: else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
self._prunetraceback(excinfo) self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
if style == "auto": if style == "auto":
style = "long" style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it? # XXX should excinfo.getrepr record all data and toterminal() process it?

View File

@ -347,6 +347,9 @@ class TestReport(BaseReport):
elif isinstance(excinfo.value, skip.Exception): elif isinstance(excinfo.value, skip.Exception):
outcome = "skipped" outcome = "skipped"
r = excinfo._getreprcrash() 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: if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2] path, line = item.reportinfo()[:2]
assert line is not None assert line is not None

View File

@ -294,6 +294,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f) excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback tb = excinfo.traceback
entry = tb.getcrashentry() entry = tb.getcrashentry()
assert entry is not None
co = _pytest._code.Code.from_function(h) co = _pytest._code.Code.from_function(h)
assert entry.frame.code.path == co.path assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 1 assert entry.lineno == co.firstlineno + 1
@ -309,12 +310,7 @@ class TestTraceback_f_g_h:
g() g()
excinfo = pytest.raises(ValueError, f) excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback assert excinfo.traceback.getcrashentry() is None
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"
def test_excinfo_exconly(): def test_excinfo_exconly():
@ -1577,12 +1573,9 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False) _exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None: @pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
"""Regression test for #10903. def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> 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.
"""
pytester.makepyfile( pytester.makepyfile(
""" """
def test(): def test():
@ -1590,5 +1583,9 @@ def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None:
1 / 0 1 / 0
""" """
) )
result = pytester.runpytest() result = pytester.runpytest("--tb", tbstyle)
assert result.ret == 1 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.*"])