@@ -492,6 +492,22 @@ async def main():
492492 self .assertIn (main_name , names )
493493 self .assertEqual ([len (n ) for n in names if n .startswith ("x" )], [255 ])
494494
495+ @skip_if_not_supported
496+ def test_recursive_coroutine_stack_is_not_truncated (self ):
497+ # gh-158522
498+ async def rec (n ):
499+ if n :
500+ return await rec (n - 1 )
501+ return [
502+ frame .funcname .rpartition ("." )[2 ]
503+ for task in RemoteUnwinder (
504+ os .getpid ()).get_async_stack_trace ()[0 ].awaited_by
505+ for coro in task .coroutine_stack
506+ for frame in coro .call_stack
507+ ]
508+
509+ self .assertEqual (asyncio .run (rec (3 )), ["rec" ] * 4 )
510+
495511 @skip_if_not_supported
496512 @unittest .skipIf (
497513 sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
@@ -3293,6 +3309,242 @@ def test_finally_no_exception_no_flag(self):
32933309 self ._check_exception_status (p , thread_tid , expect_exception = False )
32943310
32953311
3312+ @skip_if_not_supported
3313+ class TestExceptionDetectionInProcess (RemoteInspectionTestBase ):
3314+ """gh-158539: HAS_EXCEPTION for handlers running in generators/coroutines.
3315+
3316+ ``TestExceptionDetectionScenarios`` samples a child process and therefore
3317+ needs subprocess debugging permissions. These tests inspect the current
3318+ process with ``RemoteUnwinder`` and only need self-inspection, so they also
3319+ run on macOS without special entitlements.
3320+ """
3321+
3322+ @classmethod
3323+ def setUpClass (cls ):
3324+ try :
3325+ RemoteUnwinder (os .getpid (), all_threads = True ).get_stack_trace ()
3326+ except PermissionError as exc :
3327+ raise unittest .SkipTest (f"self-inspection is unavailable: { exc } " )
3328+
3329+ def _check_running_handler (
3330+ self , target , expect_exception , * , mode = PROFILING_MODE_ALL ,
3331+ skip_non_matching_threads = False ,
3332+ ):
3333+ """Run *target* in a thread and check its HAS_EXCEPTION flag.
3334+
3335+ *target* receives ``(ready, stop)`` events and must signal ``ready``
3336+ only once it is executing inside the code region under test, then keep
3337+ running until ``stop`` is set.
3338+ """
3339+ stop = threading .Event ()
3340+ ready = threading .Event ()
3341+ failure = []
3342+
3343+ def runner ():
3344+ try :
3345+ target (ready , stop )
3346+ except BaseException as exc :
3347+ failure .append (exc )
3348+ ready .set ()
3349+
3350+ thread = threading .Thread (target = runner , daemon = True )
3351+ thread .start ()
3352+ try :
3353+ self .assertTrue (ready .wait (SHORT_TIMEOUT ), "handler never started" )
3354+ self .assertFalse (failure , f"handler raised { failure !r} " )
3355+
3356+ unwinder = RemoteUnwinder (
3357+ os .getpid (),
3358+ all_threads = True ,
3359+ mode = mode ,
3360+ skip_non_matching_threads = skip_non_matching_threads ,
3361+ )
3362+ observed = []
3363+ for _ in busy_retry (SHORT_TIMEOUT ):
3364+ with contextlib .suppress (* TRANSIENT_ERRORS ):
3365+ statuses = self ._get_thread_statuses (unwinder .get_stack_trace ())
3366+ status = statuses .get (thread .native_id )
3367+ if status is None :
3368+ continue
3369+ has_exception = bool (status & THREAD_STATUS_HAS_EXCEPTION )
3370+ observed .append (has_exception )
3371+ if has_exception == expect_exception :
3372+ break
3373+ self .assertTrue (
3374+ observed , "target thread status was never observed"
3375+ )
3376+ self .assertIn (
3377+ expect_exception ,
3378+ observed ,
3379+ f"HAS_EXCEPTION was never { expect_exception } while the "
3380+ f"handler was running (observed { observed } )" ,
3381+ )
3382+ finally :
3383+ stop .set ()
3384+ thread .join (SHORT_TIMEOUT )
3385+
3386+ def _busy_until_stopped (self , ready , stop ):
3387+ ready .set ()
3388+ while not stop .is_set ():
3389+ time .sleep (0.001 )
3390+
3391+ def test_handler_in_function (self ):
3392+ def target (ready , stop ):
3393+ try :
3394+ raise ValueError ("test" )
3395+ except ValueError :
3396+ self ._busy_until_stopped (ready , stop )
3397+
3398+ self ._check_running_handler (target , expect_exception = True )
3399+
3400+ def test_handler_in_generator (self ):
3401+ def target (ready , stop ):
3402+ def gen ():
3403+ try :
3404+ raise ValueError ("test" )
3405+ except ValueError :
3406+ self ._busy_until_stopped (ready , stop )
3407+ yield
3408+
3409+ for _ in gen ():
3410+ pass
3411+
3412+ self ._check_running_handler (target , expect_exception = True )
3413+
3414+ def test_handler_in_genexpr_callee (self ):
3415+ def target (ready , stop ):
3416+ def callee ():
3417+ try :
3418+ raise ValueError ("test" )
3419+ except ValueError :
3420+ self ._busy_until_stopped (ready , stop )
3421+
3422+ list (callee () for _ in range (1 ))
3423+
3424+ self ._check_running_handler (target , expect_exception = True )
3425+
3426+ def test_handler_in_coroutine (self ):
3427+ async def coro (ready , stop ):
3428+ try :
3429+ raise ValueError ("test" )
3430+ except ValueError :
3431+ self ._busy_until_stopped (ready , stop )
3432+
3433+ def target (ready , stop ):
3434+ asyncio .run (coro (ready , stop ))
3435+
3436+ self ._check_running_handler (target , expect_exception = True )
3437+
3438+ def test_handler_in_callee_from_coroutine (self ):
3439+ def callee (ready , stop ):
3440+ try :
3441+ raise ValueError ("test" )
3442+ except ValueError :
3443+ self ._busy_until_stopped (ready , stop )
3444+
3445+ async def coro (ready , stop ):
3446+ callee (ready , stop )
3447+
3448+ def target (ready , stop ):
3449+ asyncio .run (coro (ready , stop ))
3450+
3451+ self ._check_running_handler (target , expect_exception = True )
3452+
3453+ def test_outer_handler_while_generator_runs (self ):
3454+ """A generator with no handler of its own must not hide the outer one.
3455+
3456+ ``exc_info`` points at the generator's empty ``_PyErr_StackItem`` whose
3457+ ``previous_item`` is the thread's ``exc_state``, so the profiler has to
3458+ walk the chain to find the exception ``sys.exception()`` reports.
3459+ """
3460+ def target (ready , stop ):
3461+ def gen ():
3462+ self ._busy_until_stopped (ready , stop )
3463+ yield
3464+
3465+ try :
3466+ raise ValueError ("outer" )
3467+ except ValueError :
3468+ for _ in gen ():
3469+ pass
3470+
3471+ self ._check_running_handler (target , expect_exception = True )
3472+
3473+ def test_generator_without_exception (self ):
3474+ def target (ready , stop ):
3475+ def gen ():
3476+ self ._busy_until_stopped (ready , stop )
3477+ yield
3478+
3479+ for _ in gen ():
3480+ pass
3481+
3482+ self ._check_running_handler (target , expect_exception = False )
3483+
3484+ def test_outer_handler_while_nested_generators_run (self ):
3485+ def target (ready , stop ):
3486+ def gen (depth ):
3487+ if depth :
3488+ yield from gen (depth - 1 )
3489+ else :
3490+ self ._busy_until_stopped (ready , stop )
3491+ yield
3492+
3493+ try :
3494+ raise ValueError ("outer" )
3495+ except ValueError :
3496+ for _ in gen (32 ):
3497+ pass
3498+
3499+ self ._check_running_handler (
3500+ target ,
3501+ expect_exception = True ,
3502+ mode = PROFILING_MODE_EXCEPTION ,
3503+ skip_non_matching_threads = True ,
3504+ )
3505+
3506+ def test_generator_finally_after_except (self ):
3507+ """The handled exception is cleared before the generator's finally."""
3508+ def target (ready , stop ):
3509+ def gen ():
3510+ try :
3511+ raise ValueError ("test" )
3512+ except ValueError :
3513+ pass
3514+ finally :
3515+ self ._busy_until_stopped (ready , stop )
3516+ yield
3517+
3518+ for _ in gen ():
3519+ pass
3520+
3521+ self ._check_running_handler (target , expect_exception = False )
3522+
3523+ def test_exception_mode_filter_keeps_generator_handler (self ):
3524+ """The exception-mode thread filter must not drop a generator handler.
3525+
3526+ This mirrors what ``--mode=exception`` actually does: threads without
3527+ HAS_EXCEPTION are skipped before their stack is unwound.
3528+ """
3529+ def target (ready , stop ):
3530+ def gen ():
3531+ try :
3532+ raise ValueError ("test" )
3533+ except ValueError :
3534+ self ._busy_until_stopped (ready , stop )
3535+ yield
3536+
3537+ for _ in gen ():
3538+ pass
3539+
3540+ self ._check_running_handler (
3541+ target ,
3542+ expect_exception = True ,
3543+ mode = PROFILING_MODE_EXCEPTION ,
3544+ skip_non_matching_threads = True ,
3545+ )
3546+
3547+
32963548@requires_remote_subprocess_debugging ()
32973549class TestFrameCaching (RemoteInspectionTestBase ):
32983550 """Test that frame caching produces correct results.
0 commit comments