@@ -4132,6 +4132,93 @@ def test_get_stats_disabled_raises(self):
41324132 client_socket .sendall (b"done" )
41334133
41344134
4135+ @requires_remote_subprocess_debugging ()
4136+ @skip_if_not_supported
4137+ @unittest .skipIf (
4138+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
4139+ "Test only runs on Linux with process_vm_readv support" ,
4140+ )
4141+ class TestMetadataDegradation (RemoteInspectionTestBase ):
4142+ """Tests for graceful degradation of oversized code-object metadata."""
4143+
4144+ def test_long_qualname_truncated_not_dropped (self ):
4145+ """A qualname longer than 1024 chars is truncated instead of
4146+ failing the whole sample."""
4147+ name = "f" * 1100
4148+ src = f"def { name } (sample):\n return sample()\n "
4149+ ns = {}
4150+ exec (src , ns )
4151+
4152+ trace = ns [name ](RemoteUnwinder (os .getpid ()).get_stack_trace )
4153+ frame = self ._find_frame_in_trace (
4154+ trace , lambda f : f .funcname .startswith ("fff" )
4155+ )
4156+ self .assertIsNotNone (frame )
4157+ self .assertEqual (frame .funcname , "f" * 1024 )
4158+
4159+ def test_long_filename_truncated (self ):
4160+ """A filename longer than 1024 chars is truncated instead of
4161+ failing the whole sample."""
4162+ src = "def g(sample):\n return sample()\n "
4163+ ns = {}
4164+ exec (compile (src , "x" * 1500 + ".py" , "exec" ), ns )
4165+
4166+ trace = ns ["g" ](RemoteUnwinder (os .getpid ()).get_stack_trace )
4167+ frame = self ._find_frame_in_trace (trace , lambda f : f .funcname == "g" )
4168+ self .assertIsNotNone (frame )
4169+ self .assertEqual (frame .filename , "x" * 1024 )
4170+
4171+ def test_oversized_linetable_degrades_to_no_location (self ):
4172+ """A linetable over MAX_LINETABLE_SIZE degrades to a frame without
4173+ location instead of failing the whole sample."""
4174+ src = (
4175+ "def big(sample):\n "
4176+ + " x = 1\n " * 20_000
4177+ + " return sample()\n "
4178+ )
4179+ ns = {}
4180+ exec (compile (src , "big_linetable.py" , "exec" ), ns )
4181+ big = ns ["big" ]
4182+ self .assertGreater (len (big .__code__ .co_linetable ), 64 * 1024 )
4183+
4184+ trace = big (RemoteUnwinder (os .getpid ()).get_stack_trace )
4185+ frame = self ._find_frame_in_trace (
4186+ trace , lambda f : f .funcname == "big"
4187+ )
4188+ self .assertIsNone (frame .location )
4189+ self .assertEqual (frame .filename , "big_linetable.py" )
4190+
4191+ @unittest .skipIf (
4192+ sys .platform == "win32" ,
4193+ "Process death maps to ProcessLookupError only on POSIX platforms" ,
4194+ )
4195+ def test_dead_process_raises_not_degrades (self ):
4196+ """Death of the target raises ProcessLookupError instead of
4197+ degrading to synthetic frames."""
4198+ script_body = """\
4199+ import time
4200+ sock.sendall(b"ready")
4201+ time.sleep(10_000)
4202+ """
4203+ with self ._target_process (script_body ) as (p , client_socket , make_unwinder ):
4204+ _wait_for_signal (client_socket , b"ready" )
4205+ unwinder = make_unwinder ()
4206+ _get_stack_trace_with_retry (unwinder )
4207+
4208+ p .kill ()
4209+ p .wait ()
4210+
4211+ for _ in busy_retry (SHORT_TIMEOUT , error = False ):
4212+ try :
4213+ unwinder .get_stack_trace ()
4214+ except ProcessLookupError :
4215+ break
4216+ except RuntimeError :
4217+ continue
4218+ else :
4219+ self .fail ("ProcessLookupError never raised for dead process" )
4220+
4221+
41354222@requires_remote_subprocess_debugging ()
41364223class TestFrameChainLimits (RemoteInspectionTestBase ):
41374224 """Frame chain walks abort instead of looping/overflowing on deep chains."""
0 commit comments