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 inaccd962c9f
). 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 from431ec6d34e
(which has been reverted). Helps towards fixing issue # 1904. Co-authored-by: Felix Hofstätter <Felhof1@hotmail.com>
This commit is contained in:
parent
eff54aece1
commit
e3b1799766
1
AUTHORS
1
AUTHORS
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.*"])
|
||||||
|
|
Loading…
Reference in New Issue