@@ -84,6 +84,41 @@ def setUpModule():
8484@unittest .skipUnless (hasattr (sys , "_jit" ) and sys ._jit .is_enabled (),
8585 "requires a JIT-enabled build with JIT execution active" )
8686class JitBacktraceTests (DebuggerTests ):
87+ def _assert_jit_backtrace_shape (self , gdb_output , * , anchor_at_top ):
88+ # Shape assertions applied to every JIT backtrace we produce:
89+ # 1. The synthetic JIT symbol appears exactly once. A second
90+ # py::jit_entry:<jit> frame would mean the unwinder is
91+ # materializing two native frames for a single logical JIT
92+ # region, or failing to unwind out of the region entirely.
93+ # 2. At least one _PyEval_EvalFrameDefault / _PyEval_Vector
94+ # frame appears (the JIT is always reached from the eval
95+ # loop in these tests).
96+ # 3. The JIT frame is above (i.e. textually before, since GDB
97+ # prints innermost first) an eval frame.
98+ # 4. For tests that assert a specific entry PC, the JIT frame
99+ # is also at #0.
100+ jit_count = len (re .findall (r"py::jit_entry:<jit>" , gdb_output ))
101+ self .assertEqual (
102+ jit_count , 1 ,
103+ f"expected exactly 1 py::jit_entry:<jit> frame, got { jit_count } \n "
104+ f"backtrace:\n { gdb_output } " ,
105+ )
106+ eval_count = len (re .findall (EVAL_FRAME_RE , gdb_output ))
107+ self .assertGreaterEqual (
108+ eval_count , 1 ,
109+ f"expected at least one _PyEval_* frame, got { eval_count } \n "
110+ f"backtrace:\n { gdb_output } " ,
111+ )
112+ jit_before_eval = re .compile (
113+ rf"py::jit_entry:<jit>.*{ EVAL_FRAME_RE } " , re .DOTALL
114+ )
115+ self .assertRegex (gdb_output , jit_before_eval )
116+ if anchor_at_top :
117+ self .assertRegex (
118+ gdb_output ,
119+ re .compile (r"#0\s+py::jit_entry:<jit>" , re .DOTALL ),
120+ )
121+
87122 def test_bt_shows_compiled_jit_entry (self ):
88123 gdb_output = self .get_stack_trace (
89124 script = JIT_SAMPLE_SCRIPT ,
@@ -96,14 +131,9 @@ def test_bt_shows_compiled_jit_entry(self):
96131 PYTHON_JIT = "1" ,
97132 )
98133 # 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- )
134+ # the same synthetic symbol name; breaking at the entry PC pins the
135+ # JIT frame at #0.
136+ self ._assert_jit_backtrace_shape (gdb_output , anchor_at_top = True )
107137
108138 def test_bt_unwinds_through_jit_frames (self ):
109139 gdb_output = self .get_stack_trace (
@@ -114,13 +144,7 @@ def test_bt_unwinds_through_jit_frames(self):
114144 # The executor should appear as a named JIT frame and unwind back into
115145 # the eval loop. Whether GDB also materializes a separate shim frame is
116146 # 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- )
147+ self ._assert_jit_backtrace_shape (gdb_output , anchor_at_top = False )
124148
125149 def test_bt_unwinds_from_inside_jit_entry (self ):
126150 gdb_output = self .get_stack_trace (
@@ -132,12 +156,6 @@ def test_bt_unwinds_from_inside_jit_entry(self):
132156 ],
133157 PYTHON_JIT = "1" ,
134158 )
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- )
159+ # Once the selected PC is inside the JIT entry, we require that GDB
160+ # identifies the JIT frame at #0 and keeps unwinding into _PyEval_*.
161+ self ._assert_jit_backtrace_shape (gdb_output , anchor_at_top = True )
0 commit comments