Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code: handle repr'ing empty tracebacks gracefully #10907

Merged
merged 1 commit into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Erik M. Bray
Evan Kepner
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin
Expand Down
52 changes: 33 additions & 19 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,13 +411,14 @@ def filter(
"""
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
Expand Down Expand Up @@ -598,9 +599,11 @@ def errisinstance(
"""
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)

Expand Down Expand Up @@ -647,7 +650,9 @@ def getrepr(
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(),
Expand Down Expand Up @@ -803,12 +808,16 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:

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("???")
Expand Down Expand Up @@ -857,17 +866,21 @@ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTracebac
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(
Expand Down Expand Up @@ -924,6 +937,7 @@ def repr_excinfo(
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.
Expand All @@ -950,8 +964,8 @@ def repr_excinfo(
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_ = (
Expand Down Expand Up @@ -1042,7 +1056,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
@dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation"
reprcrash: Optional["ReprFileLocation"]

def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)
Expand Down Expand Up @@ -1147,8 +1161,8 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None:

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)
Expand Down
3 changes: 0 additions & 3 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,7 @@ def _repr_failure_py(
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?
Expand Down
3 changes: 3 additions & 0 deletions src/_pytest/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
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
Expand Down
23 changes: 10 additions & 13 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def f():
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
Expand All @@ -309,12 +310,7 @@ def f():
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():
Expand Down Expand Up @@ -1577,18 +1573,19 @@ 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():
__tracebackhide__ = True
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.*"])