1717 raise unittest .SkipTest ("test requires subprocess support" )
1818
1919
20+ STACK_DEPTH = 10
21+
22+
2023def _frame_pointers_expected (machine ):
2124 cflags = " " .join (
2225 value for value in (
@@ -70,7 +73,7 @@ def _frame_pointers_expected(machine):
7073 return None
7174
7275
73- def _build_stack_and_unwind ():
76+ def _build_stack_and_unwind (unwinder ):
7477 import operator
7578
7679 def build_stack (n , unwinder , warming_up_caller = False ):
@@ -89,7 +92,7 @@ def build_stack(n, unwinder, warming_up_caller=False):
8992 result = operator .call (build_stack , n - 1 , unwinder , warming_up )
9093 return result
9194
92- stack = build_stack (10 , _testinternalcapi . manual_frame_pointer_unwind )
95+ stack = build_stack (STACK_DEPTH , unwinder )
9396 return stack
9497
9598
@@ -112,8 +115,7 @@ def _classify_stack(stack, jit_enabled):
112115 return annotated , python_frames , jit_frames , other_frames
113116
114117
115- def _annotate_unwind ():
116- stack = _build_stack_and_unwind ()
118+ def _summarize_unwind (stack , unwinder_name ):
117119 jit_enabled = hasattr (sys , "_jit" ) and sys ._jit .is_enabled ()
118120 jit_backend = _testinternalcapi .get_jit_backend ()
119121 ranges = _testinternalcapi .get_jit_code_ranges () if jit_enabled else []
@@ -126,19 +128,44 @@ def _annotate_unwind():
126128 )
127129 for idx , addr , tag in annotated :
128130 print (f"#{ idx :02d} { addr :#x} -> { tag } " )
129- return json . dumps ( {
131+ return {
130132 "length" : len (stack ),
131133 "python_frames" : python_frames ,
132134 "jit_frames" : jit_frames ,
133135 "other_frames" : other_frames ,
134136 "jit_backend" : jit_backend ,
137+ "unwinder" : unwinder_name ,
138+ }
139+
140+
141+ def _annotate_unwind (unwinder_name = "manual_frame_pointer_unwind" ):
142+ unwinder = getattr (_testinternalcapi , unwinder_name )
143+ stack = _build_stack_and_unwind (unwinder )
144+ return json .dumps (_summarize_unwind (stack , unwinder_name ))
145+
146+
147+ def _annotate_unwind_after_executor_free (unwinder_name = "gnu_backtrace_unwind" ):
148+ # The first unwind runs at the bottom of _build_stack_and_unwind(), while
149+ # the recursive helper may be executing in JIT code. After it returns, this
150+ # helper is back in normal test code; clearing executor caches should remove
151+ # the old JIT ranges, so the second unwind must not report stale JIT frames.
152+ live = json .loads (_annotate_unwind (unwinder_name ))
153+
154+ sys ._clear_internal_caches ()
155+ _testinternalcapi .clear_executor_deletion_list ()
156+
157+ unwinder = getattr (_testinternalcapi , unwinder_name )
158+ after_free = _summarize_unwind (unwinder (), unwinder_name )
159+ return json .dumps ({
160+ "live" : live ,
161+ "after_free" : after_free ,
135162 })
136163
137164
138- def _manual_unwind_length ( ** env ):
165+ def _run_unwind_helper ( helper_name , unwinder_name , ** env ):
139166 code = (
140- "from test.test_frame_pointer_unwind import _annotate_unwind ; "
141- "print(_annotate_unwind( ));"
167+ f "from test.test_frame_pointer_unwind import { helper_name } ; "
168+ f "print({ helper_name } ( { unwinder_name !r } ));"
142169 )
143170 run_env = os .environ .copy ()
144171 run_env .update (env )
@@ -166,6 +193,15 @@ def _manual_unwind_length(**env):
166193 ) from exc
167194
168195
196+ def _unwind_result (unwinder_name , ** env ):
197+ return _run_unwind_helper ("_annotate_unwind" , unwinder_name , ** env )
198+
199+
200+ def _unwind_after_executor_free_result (unwinder_name , ** env ):
201+ return _run_unwind_helper (
202+ "_annotate_unwind_after_executor_free" , unwinder_name , ** env )
203+
204+
169205@support .requires_gil_enabled ("test requires the GIL enabled" )
170206@unittest .skipIf (support .is_wasi , "test not supported on WASI" )
171207class FramePointerUnwindTests (unittest .TestCase ):
@@ -197,14 +233,14 @@ def test_manual_unwind_respects_frame_pointers(self):
197233
198234 for env , using_jit in envs :
199235 with self .subTest (env = env ):
200- result = _manual_unwind_length ( ** env )
236+ result = _unwind_result ( "manual_frame_pointer_unwind" , ** env )
201237 jit_frames = result ["jit_frames" ]
202238 python_frames = result .get ("python_frames" , 0 )
203239 jit_backend = result .get ("jit_backend" )
204240 if self .frame_pointers_expected :
205- self .assertGreater (
241+ self .assertGreaterEqual (
206242 python_frames ,
207- 0 ,
243+ STACK_DEPTH ,
208244 f"expected to find Python frames on { self .machine } with env { env } " ,
209245 )
210246 if using_jit :
@@ -240,5 +276,84 @@ def test_manual_unwind_respects_frame_pointers(self):
240276 )
241277
242278
279+ @support .requires_gil_enabled ("test requires the GIL enabled" )
280+ @unittest .skipIf (support .is_wasi , "test not supported on WASI" )
281+ @unittest .skipUnless (sys .platform == "linux" , "GNU backtrace unwinding test requires Linux" )
282+ class GnuBacktraceUnwindTests (unittest .TestCase ):
283+
284+ def setUp (self ):
285+ super ().setUp ()
286+ try :
287+ _testinternalcapi .gnu_backtrace_unwind ()
288+ except RuntimeError as exc :
289+ if "not supported" in str (exc ):
290+ self .skipTest ("gnu backtrace unwinding not supported on this platform" )
291+ raise
292+
293+ def test_gnu_backtrace_unwinds_through_jit_frames (self ):
294+ jit_available = hasattr (sys , "_jit" ) and sys ._jit .is_available ()
295+ envs = [({"PYTHON_JIT" : "0" }, False )]
296+ if jit_available :
297+ envs .append (({"PYTHON_JIT" : "1" }, True ))
298+
299+ for env , using_jit in envs :
300+ with self .subTest (env = env ):
301+ result = _unwind_result ("gnu_backtrace_unwind" , ** env )
302+ python_frames = result .get ("python_frames" , 0 )
303+ jit_frames = result .get ("jit_frames" , 0 )
304+ jit_backend = result .get ("jit_backend" )
305+
306+ self .assertGreaterEqual (
307+ python_frames ,
308+ STACK_DEPTH ,
309+ f"expected to find Python frames in GNU backtrace with env { env } " ,
310+ )
311+ if using_jit and jit_backend == "jit" :
312+ self .assertGreater (
313+ jit_frames ,
314+ 0 ,
315+ f"expected GNU backtrace to include JIT frames with env { env } " ,
316+ )
317+ else :
318+ self .assertEqual (
319+ jit_frames ,
320+ 0 ,
321+ f"unexpected JIT frames counted in GNU backtrace with env { env } " ,
322+ )
323+
324+ def test_gnu_backtrace_jit_frames_disappear_after_executor_free (self ):
325+ if not (hasattr (sys , "_jit" ) and sys ._jit .is_available ()):
326+ self .skipTest ("JIT is not available" )
327+
328+ result = _unwind_after_executor_free_result (
329+ "gnu_backtrace_unwind" , PYTHON_JIT = "1" )
330+ live = result ["live" ]
331+ if live .get ("jit_backend" ) != "jit" :
332+ self .skipTest ("JIT backend is not active" )
333+
334+ self .assertGreaterEqual (
335+ live .get ("python_frames" , 0 ),
336+ STACK_DEPTH ,
337+ "expected live GNU backtrace to include recursive Python frames" ,
338+ )
339+ self .assertGreater (
340+ live .get ("jit_frames" , 0 ),
341+ 0 ,
342+ "expected live GNU backtrace to include JIT frames" ,
343+ )
344+
345+ after_free = result ["after_free" ]
346+ self .assertGreater (
347+ after_free .get ("python_frames" , 0 ),
348+ 0 ,
349+ "expected GNU backtrace after executor free to include Python frames" ,
350+ )
351+ self .assertEqual (
352+ after_free .get ("jit_frames" , 0 ),
353+ 0 ,
354+ "unexpected JIT frames in GNU backtrace after executor free" ,
355+ )
356+
357+
243358if __name__ == "__main__" :
244359 unittest .main ()
0 commit comments