2026-05-08 14:58:38 +01:00
|
|
|
"""Test in-process C stack unwinders against Python and JIT frames.
|
|
|
|
|
|
|
|
|
|
The tests build a recursive Python call stack, ask each _testinternalcapi
|
|
|
|
|
unwinder for return addresses, and classify those addresses as Python, JIT, or
|
|
|
|
|
other frames. The backends include CPython's manual stack-chain unwinder and
|
|
|
|
|
GNU backtrace(), so this module is about in-process C stack unwinding rather
|
|
|
|
|
than a single unwind mechanism. GDB integration tests live in test_gdb.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
STACK_DEPTH = 10
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 14:58:38 +01:00
|
|
|
def _manual_unwind_expected(machine):
|
gh-149202: Fix frame pointer unwinding on s390x and ARM (GH-149362)
-fno-omit-frame-pointer is not enough to make every target walkable by the
simple manual frame pointer unwinder.
The helper used by test_frame_pointer_unwind used to assume the frame pointer
named a two-word record where fp[0] was the previous frame pointer and fp[1]
was the return address. That is only the generic layout used by some targets.
This patch keeps that default, but moves the slots behind named offsets so
architecture-specific layouts can describe where the backchain and return
address really live.
On s390x, GCC and Clang do not emit a usable backchain unless -mbackchain is
enabled. Without it, the unwinder stops at the current C frame and the test
reports no Python frames. Once backchains are present, the helper must also
stop at the current thread's known C stack bounds; otherwise it can follow the
final backchain far enough to dereference an invalid frame and segfault.
For Linux s390x backchain frames, the documented z/Architecture stack-frame
layout saves r14, the return-address register, at byte offset 112 from the
frame pointer, so read the return address from that named slot instead of fp[1].
The 112-byte offset comes from Linux's s390 debugging documentation: its Stack
Frame Layout table shows z/Architecture backchain frames with the backchain at
offset 0 and saved r14 of the caller function at offset 112:
https://www.kernel.org/doc/html/v5.3/s390/debugging390.html#stack-frame-layout
This helper remains scoped to Linux s390x backchain frames. GNU SFrame's s390x
notes state that the s390x ELF ABI does not generally mandate where RA and FP
are saved, or whether they are saved at all:
https://sourceware.org/binutils/docs/sframe-spec.html#s390x
As Jens Remus noted, -fno-omit-frame-pointer is not needed when -mbackchain is
present.
On 32-bit ARM, GCC defaults to Thumb mode on common armhf toolchains. The Thumb
prologue keeps the saved frame pointer and link register at offsets that depend
on the generated frame, which breaks the fp[0]/fp[1] walk used by the helper.
Use -marm when it is supported for frame-pointer builds, and teach the helper
the GCC ARM-mode slots where the previous frame pointer is at fp[-1] and the
saved LR return address is at fp[0].
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
2026-05-06 16:03:37 +01:00
|
|
|
_Py_WITH_FRAME_POINTERS = getattr(
|
|
|
|
|
_testinternalcapi,
|
|
|
|
|
"_Py_WITH_FRAME_POINTERS",
|
|
|
|
|
-1,
|
|
|
|
|
)
|
|
|
|
|
if _Py_WITH_FRAME_POINTERS > 0:
|
|
|
|
|
return True
|
|
|
|
|
if _Py_WITH_FRAME_POINTERS == 0:
|
|
|
|
|
return False
|
|
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
cflags = " ".join(
|
|
|
|
|
value for value in (
|
|
|
|
|
sysconfig.get_config_var("PY_CORE_CFLAGS"),
|
|
|
|
|
sysconfig.get_config_var("CFLAGS"),
|
|
|
|
|
)
|
|
|
|
|
if value
|
|
|
|
|
)
|
2026-04-01 12:35:58 +02:00
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
if "no-omit-frame-pointer" in cflags:
|
2026-05-01 21:16:11 +01:00
|
|
|
# For example, configure adds -fno-omit-frame-pointer by default on
|
|
|
|
|
# supported GCC-compatible builds.
|
2026-01-27 13:17:40 +00:00
|
|
|
return True
|
|
|
|
|
if "omit-frame-pointer" in cflags:
|
|
|
|
|
return False
|
2026-04-01 12:35:58 +02:00
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
if sys.platform == "darwin":
|
|
|
|
|
# macOS x86_64/ARM64 always have frame pointer by default.
|
|
|
|
|
return True
|
2026-04-01 12:35:58 +02:00
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
if sys.platform == "linux":
|
|
|
|
|
if machine in {"aarch64", "arm64"}:
|
|
|
|
|
# 32-bit Linux is not supported
|
|
|
|
|
if sys.maxsize < 2**32:
|
|
|
|
|
return None
|
|
|
|
|
return True
|
2026-05-07 11:47:08 +02:00
|
|
|
if machine == "ppc64le":
|
|
|
|
|
# The power ABI specification requires that compilers maintain a
|
|
|
|
|
# back chain by default, so unwinding already works without a
|
|
|
|
|
# dedicated frame pointer.
|
|
|
|
|
# https://openpowerfoundation.org/specifications/64bitelfabi/
|
|
|
|
|
return True
|
2026-01-27 13:17:40 +00:00
|
|
|
if machine == "x86_64":
|
2026-04-01 12:35:58 +02:00
|
|
|
final_opt = ""
|
|
|
|
|
for opt in cflags.split():
|
|
|
|
|
if opt.startswith('-O'):
|
|
|
|
|
final_opt = opt
|
|
|
|
|
if final_opt in ("-O0", "-Og", "-O1"):
|
|
|
|
|
# Unwinding works if the optimization level is low
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
Py_ENABLE_SHARED = int(sysconfig.get_config_var('Py_ENABLE_SHARED') or '0')
|
|
|
|
|
if Py_ENABLE_SHARED:
|
|
|
|
|
# Unwinding does crash using gcc -O2 or gcc -O3
|
|
|
|
|
# when Python is built with --enable-shared
|
|
|
|
|
return "crash"
|
2026-01-27 13:17:40 +00:00
|
|
|
return False
|
2026-04-01 12:35:58 +02:00
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 14:03:05 +01:00
|
|
|
def _is_arm32_build():
|
|
|
|
|
if sys.maxsize >= 2**32:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
abi = " ".join(
|
|
|
|
|
value for value in (
|
|
|
|
|
sysconfig.get_config_var("MULTIARCH"),
|
|
|
|
|
sysconfig.get_config_var("HOST_GNU_TYPE"),
|
|
|
|
|
sysconfig.get_config_var("SOABI"),
|
|
|
|
|
)
|
|
|
|
|
if value
|
|
|
|
|
).lower()
|
|
|
|
|
return "arm" in abi
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
def _build_stack_and_unwind(unwinder):
|
2026-01-27 13:17:40 +00:00
|
|
|
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
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
stack = build_stack(STACK_DEPTH, unwinder)
|
2026-01-27 13:17:40 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
def _summarize_unwind(stack, unwinder_name):
|
2026-01-27 13:17:40 +00:00
|
|
|
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}")
|
2026-05-05 09:29:07 +01:00
|
|
|
return {
|
2026-01-27 13:17:40 +00:00
|
|
|
"length": len(stack),
|
|
|
|
|
"python_frames": python_frames,
|
|
|
|
|
"jit_frames": jit_frames,
|
|
|
|
|
"other_frames": other_frames,
|
|
|
|
|
"jit_backend": jit_backend,
|
2026-05-05 09:29:07 +01:00
|
|
|
"unwinder": unwinder_name,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _annotate_unwind(unwinder_name="manual_frame_pointer_unwind"):
|
|
|
|
|
unwinder = getattr(_testinternalcapi, unwinder_name)
|
|
|
|
|
stack = _build_stack_and_unwind(unwinder)
|
|
|
|
|
return json.dumps(_summarize_unwind(stack, unwinder_name))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _annotate_unwind_after_executor_free(unwinder_name="gnu_backtrace_unwind"):
|
|
|
|
|
# The first unwind runs at the bottom of _build_stack_and_unwind(), while
|
|
|
|
|
# the recursive helper may be executing in JIT code. After it returns, this
|
|
|
|
|
# helper is back in normal test code; clearing executor caches should remove
|
|
|
|
|
# the old JIT ranges, so the second unwind must not report stale JIT frames.
|
|
|
|
|
live = json.loads(_annotate_unwind(unwinder_name))
|
|
|
|
|
|
|
|
|
|
sys._clear_internal_caches()
|
|
|
|
|
_testinternalcapi.clear_executor_deletion_list()
|
|
|
|
|
|
|
|
|
|
unwinder = getattr(_testinternalcapi, unwinder_name)
|
|
|
|
|
after_free = _summarize_unwind(unwinder(), unwinder_name)
|
|
|
|
|
return json.dumps({
|
|
|
|
|
"live": live,
|
|
|
|
|
"after_free": after_free,
|
2026-01-27 13:17:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
def _run_unwind_helper(helper_name, unwinder_name, **env):
|
2026-01-27 13:17:40 +00:00
|
|
|
code = (
|
2026-05-08 14:58:38 +01:00
|
|
|
f"from test.test_c_stack_unwind import {helper_name}; "
|
2026-05-05 09:29:07 +01:00
|
|
|
f"print({helper_name}({unwinder_name!r}));"
|
2026-01-27 13:17:40 +00:00
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
def _unwind_result(unwinder_name, **env):
|
|
|
|
|
return _run_unwind_helper("_annotate_unwind", unwinder_name, **env)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _unwind_after_executor_free_result(unwinder_name, **env):
|
|
|
|
|
return _run_unwind_helper(
|
|
|
|
|
"_annotate_unwind_after_executor_free", unwinder_name, **env)
|
|
|
|
|
|
|
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
@support.requires_gil_enabled("test requires the GIL enabled")
|
|
|
|
|
@unittest.skipIf(support.is_wasi, "test not supported on WASI")
|
2026-05-08 14:58:38 +01:00
|
|
|
class ManualStackUnwindTests(unittest.TestCase):
|
2026-01-27 13:17:40 +00:00
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
super().setUp()
|
2026-04-01 12:35:58 +02:00
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
machine = platform.machine().lower()
|
2026-05-08 14:58:38 +01:00
|
|
|
expected = _manual_unwind_expected(machine)
|
2026-01-27 13:17:40 +00:00
|
|
|
if expected is None:
|
2026-05-08 14:58:38 +01:00
|
|
|
self.skipTest(
|
|
|
|
|
f"unsupported architecture for manual stack unwind check: {machine}"
|
|
|
|
|
)
|
2026-04-01 12:35:58 +02:00
|
|
|
if expected == "crash":
|
|
|
|
|
self.skipTest(f"test does crash on {machine}")
|
|
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
try:
|
|
|
|
|
_testinternalcapi.manual_frame_pointer_unwind()
|
|
|
|
|
except RuntimeError as exc:
|
|
|
|
|
if "not supported" in str(exc):
|
2026-05-08 14:58:38 +01:00
|
|
|
self.skipTest(
|
|
|
|
|
"manual stack unwinding not supported on this platform"
|
|
|
|
|
)
|
2026-01-27 13:17:40 +00:00
|
|
|
raise
|
|
|
|
|
self.machine = machine
|
2026-05-08 14:58:38 +01:00
|
|
|
self.manual_unwind_expected = expected
|
2026-01-27 13:17:40 +00:00
|
|
|
|
2026-05-08 14:58:38 +01:00
|
|
|
def test_manual_unwind_finds_expected_frames(self):
|
2026-01-27 13:17:40 +00:00
|
|
|
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):
|
2026-05-05 09:29:07 +01:00
|
|
|
result = _unwind_result("manual_frame_pointer_unwind", **env)
|
2026-01-27 13:17:40 +00:00
|
|
|
jit_frames = result["jit_frames"]
|
|
|
|
|
python_frames = result.get("python_frames", 0)
|
|
|
|
|
jit_backend = result.get("jit_backend")
|
2026-05-08 14:58:38 +01:00
|
|
|
if self.manual_unwind_expected:
|
2026-05-05 09:29:07 +01:00
|
|
|
self.assertGreaterEqual(
|
2026-01-27 13:17:40 +00:00
|
|
|
python_frames,
|
2026-05-05 09:29:07 +01:00
|
|
|
STACK_DEPTH,
|
2026-01-27 13:17:40 +00:00
|
|
|
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}",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 09:29:07 +01:00
|
|
|
@support.requires_gil_enabled("test requires the GIL enabled")
|
|
|
|
|
@unittest.skipIf(support.is_wasi, "test not supported on WASI")
|
|
|
|
|
@unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux")
|
2026-05-08 14:03:05 +01:00
|
|
|
@unittest.skipIf(
|
|
|
|
|
_is_arm32_build(),
|
|
|
|
|
"GNU backtrace unwinding skipped on Arm 32-bit",
|
|
|
|
|
)
|
2026-05-05 09:29:07 +01:00
|
|
|
class GnuBacktraceUnwindTests(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
super().setUp()
|
|
|
|
|
try:
|
|
|
|
|
_testinternalcapi.gnu_backtrace_unwind()
|
|
|
|
|
except RuntimeError as exc:
|
|
|
|
|
if "not supported" in str(exc):
|
|
|
|
|
self.skipTest("gnu backtrace unwinding not supported on this platform")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
def test_gnu_backtrace_unwinds_through_jit_frames(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 = _unwind_result("gnu_backtrace_unwind", **env)
|
|
|
|
|
python_frames = result.get("python_frames", 0)
|
|
|
|
|
jit_frames = result.get("jit_frames", 0)
|
|
|
|
|
jit_backend = result.get("jit_backend")
|
|
|
|
|
|
|
|
|
|
self.assertGreaterEqual(
|
|
|
|
|
python_frames,
|
|
|
|
|
STACK_DEPTH,
|
|
|
|
|
f"expected to find Python frames in GNU backtrace with env {env}",
|
|
|
|
|
)
|
|
|
|
|
if using_jit and jit_backend == "jit":
|
|
|
|
|
self.assertGreater(
|
|
|
|
|
jit_frames,
|
|
|
|
|
0,
|
|
|
|
|
f"expected GNU backtrace to include JIT frames with env {env}",
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
jit_frames,
|
|
|
|
|
0,
|
|
|
|
|
f"unexpected JIT frames counted in GNU backtrace with env {env}",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_gnu_backtrace_jit_frames_disappear_after_executor_free(self):
|
|
|
|
|
if not (hasattr(sys, "_jit") and sys._jit.is_available()):
|
|
|
|
|
self.skipTest("JIT is not available")
|
|
|
|
|
|
|
|
|
|
result = _unwind_after_executor_free_result(
|
|
|
|
|
"gnu_backtrace_unwind", PYTHON_JIT="1")
|
|
|
|
|
live = result["live"]
|
|
|
|
|
if live.get("jit_backend") != "jit":
|
|
|
|
|
self.skipTest("JIT backend is not active")
|
|
|
|
|
|
|
|
|
|
self.assertGreaterEqual(
|
|
|
|
|
live.get("python_frames", 0),
|
|
|
|
|
STACK_DEPTH,
|
|
|
|
|
"expected live GNU backtrace to include recursive Python frames",
|
|
|
|
|
)
|
|
|
|
|
self.assertGreater(
|
|
|
|
|
live.get("jit_frames", 0),
|
|
|
|
|
0,
|
|
|
|
|
"expected live GNU backtrace to include JIT frames",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
after_free = result["after_free"]
|
|
|
|
|
self.assertGreater(
|
|
|
|
|
after_free.get("python_frames", 0),
|
|
|
|
|
0,
|
|
|
|
|
"expected GNU backtrace after executor free to include Python frames",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
after_free.get("jit_frames", 0),
|
|
|
|
|
0,
|
|
|
|
|
"unexpected JIT frames in GNU backtrace after executor free",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-01-27 13:17:40 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|