Skip to content

Commit a9c6315

Browse files
committed
Assert backtrace shape in test_gdb.test_jit
Add a shared helper that asserts exactly one py::jit_entry frame above at least one eval frame, so regressions producing duplicate JIT frames or JIT-below-eval can't pass the old tolerant regex.
1 parent 3eeddb9 commit a9c6315

File tree

1 file changed

+64
-24
lines changed

1 file changed

+64
-24
lines changed

Lib/test/test_gdb/test_jit.py

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
# JIT region, instead of asserting against a misleading backtrace.
2929
MAX_JIT_ENTRY_STEPS = 4
3030
EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)"
31+
BACKTRACE_FRAME_RE = re.compile(r"^#\d+\s+.*$", re.MULTILINE)
3132

3233
FINISH_TO_JIT_ENTRY = (
3334
"python exec(\"import gdb\\n"
@@ -84,6 +85,62 @@ def setUpModule():
8485
@unittest.skipUnless(hasattr(sys, "_jit") and sys._jit.is_enabled(),
8586
"requires a JIT-enabled build with JIT execution active")
8687
class JitBacktraceTests(DebuggerTests):
88+
def _extract_backtrace_frames(self, gdb_output):
89+
frames = BACKTRACE_FRAME_RE.findall(gdb_output)
90+
self.assertGreater(
91+
len(frames), 0,
92+
f"expected at least one GDB backtrace frame in output:\n{gdb_output}",
93+
)
94+
return frames
95+
96+
def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
97+
# Shape assertions applied to every JIT backtrace we produce:
98+
# 1. The synthetic JIT symbol appears exactly once. A second
99+
# py::jit_entry:<jit> frame would mean the unwinder is
100+
# materializing two native frames for a single logical JIT
101+
# region, or failing to unwind out of the region entirely.
102+
# 2. At least one _PyEval_EvalFrameDefault / _PyEval_Vector
103+
# frame appears after the JIT frame, proving the unwinder
104+
# climbs back out of the JIT region into the eval loop.
105+
# Helper frames from inside the JITted region may still
106+
# appear above the synthetic JIT frame in the backtrace.
107+
# 4. For tests that assert a specific entry PC, the JIT frame
108+
# is also at #0.
109+
frames = self._extract_backtrace_frames(gdb_output)
110+
backtrace = "\n".join(frames)
111+
112+
jit_frames = [frame for frame in frames if "py::jit_entry:<jit>" in frame]
113+
jit_count = len(jit_frames)
114+
self.assertEqual(
115+
jit_count, 1,
116+
f"expected exactly 1 py::jit_entry:<jit> frame, got {jit_count}\n"
117+
f"backtrace:\n{backtrace}",
118+
)
119+
eval_frames = [frame for frame in frames if re.search(EVAL_FRAME_RE, frame)]
120+
eval_count = len(eval_frames)
121+
self.assertGreaterEqual(
122+
eval_count, 1,
123+
f"expected at least one _PyEval_* frame, got {eval_count}\n"
124+
f"backtrace:\n{backtrace}",
125+
)
126+
jit_frame_index = next(
127+
i for i, frame in enumerate(frames) if "py::jit_entry:<jit>" in frame
128+
)
129+
eval_after_jit = any(
130+
re.search(EVAL_FRAME_RE, frame)
131+
for frame in frames[jit_frame_index + 1:]
132+
)
133+
self.assertTrue(
134+
eval_after_jit,
135+
f"expected an eval frame after the JIT frame\n"
136+
f"backtrace:\n{backtrace}",
137+
)
138+
if anchor_at_top:
139+
self.assertRegex(
140+
frames[0],
141+
re.compile(r"^#0\s+py::jit_entry:<jit>"),
142+
)
143+
87144
def test_bt_shows_compiled_jit_entry(self):
88145
gdb_output = self.get_stack_trace(
89146
script=JIT_SAMPLE_SCRIPT,
@@ -96,14 +153,9 @@ def test_bt_shows_compiled_jit_entry(self):
96153
PYTHON_JIT="1",
97154
)
98155
# GDB registers the compiled JIT entry and per-trace JIT regions under
99-
# the same synthetic symbol name.
100-
self.assertRegex(
101-
gdb_output,
102-
re.compile(
103-
rf"#0\s+py::jit_entry:<jit>.*{EVAL_FRAME_RE}",
104-
re.DOTALL,
105-
),
106-
)
156+
# the same synthetic symbol name; breaking at the entry PC pins the
157+
# JIT frame at #0.
158+
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=True)
107159

108160
def test_bt_unwinds_through_jit_frames(self):
109161
gdb_output = self.get_stack_trace(
@@ -114,13 +166,7 @@ def test_bt_unwinds_through_jit_frames(self):
114166
# The executor should appear as a named JIT frame and unwind back into
115167
# the eval loop. Whether GDB also materializes a separate shim frame is
116168
# an implementation detail of the synthetic executor CFI.
117-
self.assertRegex(
118-
gdb_output,
119-
re.compile(
120-
rf"py::jit_entry:<jit>.*{EVAL_FRAME_RE}",
121-
re.DOTALL,
122-
),
123-
)
169+
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=False)
124170

125171
def test_bt_unwinds_from_inside_jit_entry(self):
126172
gdb_output = self.get_stack_trace(
@@ -132,12 +178,6 @@ def test_bt_unwinds_from_inside_jit_entry(self):
132178
],
133179
PYTHON_JIT="1",
134180
)
135-
# Once the selected PC is inside the JIT entry, we only require that
136-
# GDB can identify the JIT frame and keep unwinding into _PyEval_*.
137-
self.assertRegex(
138-
gdb_output,
139-
re.compile(
140-
rf"#0\s+py::jit_entry:<jit>.*{EVAL_FRAME_RE}",
141-
re.DOTALL,
142-
),
143-
)
181+
# Once the selected PC is inside the JIT entry, we require that GDB
182+
# identifies the JIT frame at #0 and keeps unwinding into _PyEval_*.
183+
self._assert_jit_backtrace_shape(gdb_output, anchor_at_top=True)

0 commit comments

Comments
 (0)