2828# JIT region, instead of asserting against a misleading backtrace.
2929MAX_JIT_ENTRY_STEPS = 4
3030EVAL_FRAME_RE = r"(_PyEval_EvalFrameDefault|_PyEval_Vector)"
31+ BACKTRACE_FRAME_RE = re .compile (r"^#\d+\s+.*$" , re .MULTILINE )
3132
3233FINISH_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" )
8687class 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