From aaa31b98d8ca74306ec95284ed2b07b821a0f047 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 1 Nov 2026 09:32:35 +0100 Subject: [PATCH 1/2] gh-160986: Fix flaky test_popping_python_frame_is_not_native The unwinder can attach before the target process enters its main loop, so on slow builds (e.g. UBSan) all 10,000 samples could be taken before leaf() runs. Wait until leaf() shows up in a sample before counting. Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_external_inspection.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index df0af14d9f75622..adef352c18ef3b7 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -2041,17 +2041,29 @@ def leaf(depth): except RuntimeError: continue break - samples = 0 - for _ in range(10_000): + + def sample_names(): try: threads = unwinder.get_stack_trace()[0].threads except TRANSIENT_ERRORS: - continue + return None if not threads: - continue - frames = threads[0].frame_info - names = [frame.funcname for frame in frames] + return None + names = [frame.funcname for frame in threads[0].frame_info] if "leaf" not in names: + return None + return names + + # Wait until the target is running leaf(): the unwinder can + # attach before the main loop starts on slow builds. + for _ in busy_retry(SHORT_TIMEOUT): + if sample_names() is not None: + break + + samples = 0 + for _ in range(10_000): + names = sample_names() + if names is None: continue samples += 1 self.assertNotIn(("leaf", ""), zip(names, names[1:])) From 2e577780b9ef757ceebc4e98585b859cac1fc22e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 2 Nov 2026 19:59:32 +0100 Subject: [PATCH 2/2] Reuse _get_stack_trace_with_retry() to wait for leaf() Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_external_inspection.py | 32 ++++++++++++---------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index adef352c18ef3b7..a3f2eabacf6e88f 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -2041,29 +2041,25 @@ def leaf(depth): except RuntimeError: continue break - - def sample_names(): + # The unwinder can attach before the main loop starts. + _get_stack_trace_with_retry( + unwinder, + condition=lambda trace: trace[0].threads and any( + frame.funcname == "leaf" + for frame in trace[0].threads[0].frame_info + ), + ) + samples = 0 + for _ in range(10_000): try: threads = unwinder.get_stack_trace()[0].threads except TRANSIENT_ERRORS: - return None + continue if not threads: - return None - names = [frame.funcname for frame in threads[0].frame_info] + continue + frames = threads[0].frame_info + names = [frame.funcname for frame in frames] if "leaf" not in names: - return None - return names - - # Wait until the target is running leaf(): the unwinder can - # attach before the main loop starts on slow builds. - for _ in busy_retry(SHORT_TIMEOUT): - if sample_names() is not None: - break - - samples = 0 - for _ in range(10_000): - names = sample_names() - if names is None: continue samples += 1 self.assertNotIn(("leaf", ""), zip(names, names[1:]))