Skip to content

Commit

Permalink
debug: more options for short_stack
Browse files Browse the repository at this point in the history
Now we have the option to show the full stack or an abbreviated stack.
But debug=premain should still show the complete stack.
  • Loading branch information
nedbat committed Nov 12, 2023
1 parent 2315fb7 commit 1662e0e
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 25 deletions.
2 changes: 1 addition & 1 deletion coverage/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ def do_debug(self, args: List[str]) -> int:
write_formatted_info(print, "config", self.coverage.config.debug_info())
elif args[0] == "premain":
print(info_header("premain"))
print(short_stack())
print(short_stack(full=True))
elif args[0] == "pybehave":
write_formatted_info(print, "pybehave", env.debug_info())
else:
Expand Down
2 changes: 1 addition & 1 deletion coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def _post_init(self) -> None:

# "[run] _crash" will raise an exception if the value is close by in
# the call stack, for testing error handling.
if self.config._crash and self.config._crash in short_stack(limit=4):
if self.config._crash and self.config._crash in short_stack():
raise RuntimeError(f"Crashing because called by {self.config._crash}")

def _write_startup_debug(self) -> None:
Expand Down
33 changes: 20 additions & 13 deletions coverage/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def exc_one_line(exc: Exception) -> str:
return "|".join(l.rstrip() for l in lines)


def short_stack(limit: Optional[int] = None, skip: int = 0) -> str:
def short_stack(skip: int = 0, full: bool = False, frame_ids: bool = False) -> str:
"""Return a string summarizing the call stack.
The string is multi-line, with one line per stack frame. Each line shows
Expand All @@ -193,12 +193,11 @@ def short_stack(limit: Optional[int] = None, skip: int = 0) -> str:
import_local_file : /Users/ned/coverage/trunk/coverage/backward.py:159
...
`limit` is the number of frames to include, defaulting to all of them.
`skip` is the number of frames to skip, so that debugging functions can
call this and not be included in the result.
Initial frames deemed uninteresting are automatically skipped.
If `full` is true, then include all frames. Otherwise, initial "boring"
frames (ones in site-packages and earlier) are omitted.
"""
# Regexes in initial frames that we don't care about.
Expand All @@ -208,23 +207,31 @@ def short_stack(limit: Optional[int] = None, skip: int = 0) -> str:
r"\bsite-packages\b", # pytest etc getting to our tests.
]

stack: Iterable[inspect.FrameInfo] = inspect.stack()[limit:skip:-1]
for pat in BORING_PRELUDE:
stack = itertools.dropwhile(
(lambda fi, pat=pat: re.search(pat, fi.filename)), # type: ignore[misc]
stack
)
return "\n".join(f"{fi.function:>30s} : {fi.filename}:{fi.lineno}" for fi in stack)
stack: Iterable[inspect.FrameInfo] = inspect.stack()[:skip:-1]
if not full:
for pat in BORING_PRELUDE:
stack = itertools.dropwhile(
(lambda fi, pat=pat: re.search(pat, fi.filename)), # type: ignore[misc]
stack
)
lines = []
for frame_info in stack:
line = f"{frame_info.function:>30s} : "
if frame_ids:
line += f"{id(frame_info.frame):#x} "
line += frame_info.filename
line += f":{frame_info.lineno}"
lines.append(line)
return "\n".join(lines)


def dump_stack_frames(
limit: Optional[int] = None,
out: Optional[TWritable] = None,
skip: int = 0
) -> None:
"""Print a summary of the stack to stdout, or someplace else."""
fout = out or sys.stdout
fout.write(short_stack(limit=limit, skip=skip+1))
fout.write(short_stack(skip=skip+1))
fout.write("\n")


Expand Down
2 changes: 1 addition & 1 deletion coverage/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ def add_arc(
"""Add an arc, including message fragments to use if it is missing."""
if self.debug: # pragma: debugging
print(f"\nAdding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}")
print(short_stack(limit=10))
print(short_stack())
self.arcs.add((start, end))

if smsg is not None or emsg is not None:
Expand Down
16 changes: 13 additions & 3 deletions tests/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import ast
import os
import pprint
import re
import sys
Expand Down Expand Up @@ -333,16 +334,25 @@ def test_debug_pybehave(self) -> None:
def test_debug_premain(self) -> None:
self.command_line("debug premain")
out = self.stdout()
# -- premain ---------------------------------------------------
# ... many lines ...
# _multicall : /Users/ned/cov/trunk/.tox/py39/site-packages/pluggy/_callers.py:77
# pytest_pyfunc_call : /Users/ned/cov/trunk/.tox/py39/site-packages/_pytest/python.py:183
# test_debug_premain : /Users/ned/cov/trunk/tests/test_cmdline.py:284
# command_line : /Users/ned/cov/trunk/tests/coveragetest.py:309
# command_line : /Users/ned/cov/trunk/tests/coveragetest.py:472
# command_line : /Users/ned/cov/trunk/coverage/cmdline.py:592
# do_debug : /Users/ned/cov/trunk/coverage/cmdline.py:804
assert re.search(r"(?m)^\s+test_debug_premain : .*[/\\]tests[/\\]test_cmdline.py:\d+$", out)
assert re.search(r"(?m)^\s+command_line : .*[/\\]coverage[/\\]cmdline.py:\d+$", out)
assert re.search(r"(?m)^\s+do_debug : .*[/\\]coverage[/\\]cmdline.py:\d+$", out)
lines = out.splitlines()
s = re.escape(os.sep)
assert lines[0].startswith("-- premain ----")
assert len(lines) > 25
assert re.search(fr"{s}site-packages{s}_pytest{s}", out)
assert re.search(fr"{s}site-packages{s}pluggy{s}", out)
assert re.search(fr"(?m)^\s+test_debug_premain : .*{s}tests{s}test_cmdline.py:\d+$", out)
assert re.search(fr"(?m)^\s+command_line : .*{s}coverage{s}cmdline.py:\d+$", out)
assert re.search(fr"(?m)^\s+do_debug : .*{s}coverage{s}cmdline.py:\d+$", out)
assert "do_debug : " in lines[-1]

def test_erase(self) -> None:
# coverage erase
Expand Down
33 changes: 27 additions & 6 deletions tests/test_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,19 +299,40 @@ def test_short_stack(self) -> None:
assert "f_two" in stack[2]
assert "f_three" in stack[3]

def test_short_stack_limit(self) -> None:
stack = f_one(limit=2).splitlines()
assert 2 == len(stack)
assert "f_two" in stack[0]
assert "f_three" in stack[1]

def test_short_stack_skip(self) -> None:
stack = f_one(skip=1).splitlines()
assert 3 == len(stack)
assert "test_short_stack" in stack[0]
assert "f_one" in stack[1]
assert "f_two" in stack[2]

def test_short_stack_full(self) -> None:
stack_text = f_one(full=True)
s = re.escape(os.sep)
if env.WINDOWS:
pylib = "[Ll]ib"
else:
py = "pypy" if env.PYPY else "python"
majv, minv = sys.version_info[:2]
pylib = f"lib{s}{py}{majv}.{minv}"
assert len(re_lines(fr"{s}{pylib}{s}site-packages{s}_pytest", stack_text)) > 3
assert len(re_lines(fr"{s}{pylib}{s}site-packages{s}pluggy", stack_text)) > 3
assert not re_lines(r" 0x[0-9a-fA-F]+", stack_text) # No frame ids
stack = stack_text.splitlines()
assert len(stack) > 25
assert "test_short_stack" in stack[-4]
assert "f_one" in stack[-3]
assert "f_two" in stack[-2]
assert "f_three" in stack[-1]

def test_short_stack_frame_ids(self) -> None:
stack = f_one(full=True, frame_ids=True).splitlines()
frame_ids = [re.search(r" 0x[0-9a-fA-F]+", line)[0] for line in stack]
# Every line has a frame id.
assert len(frame_ids) == len(stack)
# All the frame ids are different.
assert len(set(frame_ids)) == len(frame_ids)


def test_relevant_environment_display() -> None:
env_vars = {
Expand Down

0 comments on commit 1662e0e

Please sign in to comment.