import json import os import platform import subprocess import sys import sysconfig import unittest from test import support from test.support import import_helper _testinternalcapi = import_helper.import_module("_testinternalcapi") if not support.has_subprocess_support: raise unittest.SkipTest("test requires subprocess support") def _frame_pointers_expected(machine): cflags = " ".join( value for value in ( sysconfig.get_config_var("PY_CORE_CFLAGS"), sysconfig.get_config_var("CFLAGS"), ) if value ) if "no-omit-frame-pointer" in cflags: return True if "omit-frame-pointer" in cflags: return False if sys.platform == "darwin": # macOS x86_64/ARM64 always have frame pointer by default. return True if sys.platform == "linux": if machine in {"aarch64", "arm64"}: # 32-bit Linux is not supported if sys.maxsize < 2**32: return None return True if machine == "x86_64": return False if sys.platform == "win32": # MSVC ignores /Oy and /Oy- on x64/ARM64. if machine == "arm64": # Windows ARM64 guidelines recommend frame pointers (x29) for stack walking. return True elif machine == "x86_64": # Windows x64 uses unwind metadata; frame pointers are not required. return None return None def _build_stack_and_unwind(): import operator def build_stack(n, unwinder, warming_up_caller=False): if warming_up_caller: return if n == 0: return unwinder() warming_up = True while warming_up: # Can't branch on JIT state inside JITted code, so compute here. warming_up = ( hasattr(sys, "_jit") and sys._jit.is_enabled() and not sys._jit.is_active() ) result = operator.call(build_stack, n - 1, unwinder, warming_up) return result stack = build_stack(10, _testinternalcapi.manual_frame_pointer_unwind) return stack def _classify_stack(stack, jit_enabled): labels = _testinternalcapi.classify_stack_addresses(stack, jit_enabled) annotated = [] jit_frames = 0 python_frames = 0 other_frames = 0 for idx, (frame, tag) in enumerate(zip(stack, labels)): addr = int(frame) if tag == "jit": jit_frames += 1 elif tag == "python": python_frames += 1 else: other_frames += 1 annotated.append((idx, addr, tag)) return annotated, python_frames, jit_frames, other_frames def _annotate_unwind(): stack = _build_stack_and_unwind() jit_enabled = hasattr(sys, "_jit") and sys._jit.is_enabled() jit_backend = _testinternalcapi.get_jit_backend() ranges = _testinternalcapi.get_jit_code_ranges() if jit_enabled else [] if jit_enabled and ranges: print("JIT ranges:") for start, end in ranges: print(f" {int(start):#x}-{int(end):#x}") annotated, python_frames, jit_frames, other_frames = _classify_stack( stack, jit_enabled ) for idx, addr, tag in annotated: print(f"#{idx:02d} {addr:#x} -> {tag}") return json.dumps({ "length": len(stack), "python_frames": python_frames, "jit_frames": jit_frames, "other_frames": other_frames, "jit_backend": jit_backend, }) def _manual_unwind_length(**env): code = ( "from test.test_frame_pointer_unwind import _annotate_unwind; " "print(_annotate_unwind());" ) run_env = os.environ.copy() run_env.update(env) proc = subprocess.run( [sys.executable, "-c", code], env=run_env, capture_output=True, text=True, ) # Surface the output for debugging/visibility when running this test if proc.stdout: print(proc.stdout, end="") if proc.returncode: raise RuntimeError( f"unwind helper failed (rc={proc.returncode}): {proc.stderr or proc.stdout}" ) stdout_lines = proc.stdout.strip().splitlines() if not stdout_lines: raise RuntimeError("unwind helper produced no output") try: return json.loads(stdout_lines[-1]) except ValueError as exc: raise RuntimeError( f"unexpected output from unwind helper: {proc.stdout!r}" ) from exc @support.requires_gil_enabled("test requires the GIL enabled") @unittest.skipIf(support.is_wasi, "test not supported on WASI") class FramePointerUnwindTests(unittest.TestCase): def setUp(self): super().setUp() machine = platform.machine().lower() expected = _frame_pointers_expected(machine) if expected is None: self.skipTest(f"unsupported architecture for frame pointer check: {machine}") try: _testinternalcapi.manual_frame_pointer_unwind() except RuntimeError as exc: if "not supported" in str(exc): self.skipTest("manual frame pointer unwinding not supported on this platform") raise self.machine = machine self.frame_pointers_expected = expected def test_manual_unwind_respects_frame_pointers(self): jit_available = hasattr(sys, "_jit") and sys._jit.is_available() envs = [({"PYTHON_JIT": "0"}, False)] if jit_available: envs.append(({"PYTHON_JIT": "1"}, True)) for env, using_jit in envs: with self.subTest(env=env): result = _manual_unwind_length(**env) jit_frames = result["jit_frames"] python_frames = result.get("python_frames", 0) jit_backend = result.get("jit_backend") if self.frame_pointers_expected: self.assertGreater( python_frames, 0, f"expected to find Python frames on {self.machine} with env {env}", ) if using_jit: if jit_backend == "jit": self.assertGreater( jit_frames, 0, f"expected to find JIT frames on {self.machine} with env {env}", ) else: # jit_backend is "interpreter" or not present self.assertEqual( jit_frames, 0, f"unexpected JIT frames counted on {self.machine} with env {env}", ) else: self.assertEqual( jit_frames, 0, f"unexpected JIT frames counted on {self.machine} with env {env}", ) else: self.assertLessEqual( python_frames, 1, f"unexpected Python frames counted on {self.machine} with env {env}", ) self.assertEqual( jit_frames, 0, f"unexpected JIT frames counted on {self.machine} with env {env}", ) if __name__ == "__main__": unittest.main()