@@ -4070,6 +4070,93 @@ def test_get_stats_disabled_raises(self):
40704070 client_socket .sendall (b"done" )
40714071
40724072
4073+ @requires_remote_subprocess_debugging ()
4074+ @skip_if_not_supported
4075+ @unittest .skipIf (
4076+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
4077+ "Test only runs on Linux with process_vm_readv support" ,
4078+ )
4079+ class TestMetadataDegradation (RemoteInspectionTestBase ):
4080+ """Tests for graceful degradation of oversized code-object metadata."""
4081+
4082+ def test_long_qualname_truncated_not_dropped (self ):
4083+ """A qualname longer than 1024 chars is truncated instead of
4084+ failing the whole sample."""
4085+ name = "f" * 1100
4086+ src = f"def { name } (sample):\n return sample()\n "
4087+ ns = {}
4088+ exec (src , ns )
4089+
4090+ trace = ns [name ](RemoteUnwinder (os .getpid ()).get_stack_trace )
4091+ frame = self ._find_frame_in_trace (
4092+ trace , lambda f : f .funcname .startswith ("fff" )
4093+ )
4094+ self .assertIsNotNone (frame )
4095+ self .assertEqual (frame .funcname , "f" * 1024 )
4096+
4097+ def test_long_filename_truncated (self ):
4098+ """A filename longer than 1024 chars is truncated instead of
4099+ failing the whole sample."""
4100+ src = "def g(sample):\n return sample()\n "
4101+ ns = {}
4102+ exec (compile (src , "x" * 1500 + ".py" , "exec" ), ns )
4103+
4104+ trace = ns ["g" ](RemoteUnwinder (os .getpid ()).get_stack_trace )
4105+ frame = self ._find_frame_in_trace (trace , lambda f : f .funcname == "g" )
4106+ self .assertIsNotNone (frame )
4107+ self .assertEqual (frame .filename , "x" * 1024 )
4108+
4109+ def test_oversized_linetable_degrades_to_no_location (self ):
4110+ """A linetable over MAX_LINETABLE_SIZE degrades to a frame without
4111+ location instead of failing the whole sample."""
4112+ src = (
4113+ "def big(sample):\n "
4114+ + " x = 1\n " * 20_000
4115+ + " return sample()\n "
4116+ )
4117+ ns = {}
4118+ exec (compile (src , "big_linetable.py" , "exec" ), ns )
4119+ big = ns ["big" ]
4120+ self .assertGreater (len (big .__code__ .co_linetable ), 64 * 1024 )
4121+
4122+ trace = big (RemoteUnwinder (os .getpid ()).get_stack_trace )
4123+ frame = self ._find_frame_in_trace (
4124+ trace , lambda f : f .funcname == "big"
4125+ )
4126+ self .assertIsNone (frame .location )
4127+ self .assertEqual (frame .filename , "big_linetable.py" )
4128+
4129+ @unittest .skipIf (
4130+ sys .platform == "win32" ,
4131+ "Process death maps to ProcessLookupError only on POSIX platforms" ,
4132+ )
4133+ def test_dead_process_raises_not_degrades (self ):
4134+ """Death of the target raises ProcessLookupError instead of
4135+ degrading to synthetic frames."""
4136+ script_body = """\
4137+ import time
4138+ sock.sendall(b"ready")
4139+ time.sleep(10_000)
4140+ """
4141+ with self ._target_process (script_body ) as (p , client_socket , make_unwinder ):
4142+ _wait_for_signal (client_socket , b"ready" )
4143+ unwinder = make_unwinder ()
4144+ _get_stack_trace_with_retry (unwinder )
4145+
4146+ p .kill ()
4147+ p .wait ()
4148+
4149+ for _ in busy_retry (SHORT_TIMEOUT , error = False ):
4150+ try :
4151+ unwinder .get_stack_trace ()
4152+ except ProcessLookupError :
4153+ break
4154+ except RuntimeError :
4155+ continue
4156+ else :
4157+ self .fail ("ProcessLookupError never raised for dead process" )
4158+
4159+
40734160@requires_remote_subprocess_debugging ()
40744161class TestFrameChainLimits (RemoteInspectionTestBase ):
40754162 """Frame chain walks abort instead of looping/overflowing on deep chains."""
0 commit comments