@@ -3804,5 +3804,170 @@ def test_get_stats_disabled_raises(self):
38043804 client_socket .sendall (b"done" )
38053805
38063806
3807+ @requires_remote_subprocess_debugging ()
3808+ class TestMetadataDegradation (RemoteInspectionTestBase ):
3809+ """Tests for graceful degradation of oversized code-object metadata."""
3810+
3811+ @contextmanager
3812+ def _running_target (self , script_body ):
3813+ """Run a target script (socket handshake prepended), yield (process, socket)."""
3814+ port = find_unused_port ()
3815+ script = (
3816+ textwrap .dedent (
3817+ f"""\
3818+ import time, socket
3819+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3820+ sock.connect(('localhost', { port } ))
3821+ """
3822+ )
3823+ + textwrap .dedent (script_body )
3824+ )
3825+
3826+ with os_helper .temp_dir () as work_dir :
3827+ script_dir = os .path .join (work_dir , "script_pkg" )
3828+ os .mkdir (script_dir )
3829+
3830+ server_socket = _create_server_socket (port )
3831+ script_name = _make_test_script (script_dir , "script" , script )
3832+ client_socket = None
3833+
3834+ try :
3835+ with _managed_subprocess ([sys .executable , script_name ]) as p :
3836+ client_socket , _ = server_socket .accept ()
3837+ server_socket .close ()
3838+ server_socket = None
3839+ yield p , client_socket
3840+ finally :
3841+ _cleanup_sockets (client_socket , server_socket )
3842+
3843+ def _sample_until_frame (self , pid , predicate ):
3844+ """Sample until a frame matching predicate appears; return that frame."""
3845+ unwinder = RemoteUnwinder (pid , all_threads = True )
3846+ traces = _get_stack_trace_with_retry (
3847+ unwinder ,
3848+ condition = lambda t : self ._find_frame_in_trace (t , predicate )
3849+ is not None ,
3850+ )
3851+ return self ._find_frame_in_trace (traces , predicate )
3852+
3853+ @skip_if_not_supported
3854+ @unittest .skipIf (
3855+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3856+ "Test only runs on Linux with process_vm_readv support" ,
3857+ )
3858+ def test_long_qualname_truncated_not_dropped (self ):
3859+ """A qualname longer than 1024 chars is truncated with a marker
3860+ instead of failing the whole sample."""
3861+ script_body = """\
3862+ src = ("def " + "f" * 1100 + "():\\ n"
3863+ " sock.sendall(b'ready')\\ n"
3864+ " time.sleep(10_000)\\ n")
3865+ ns = {"sock": sock, "time": time}
3866+ exec(src, ns)
3867+ ns["f" * 1100]()
3868+ """
3869+ with self ._running_target (script_body ) as (p , client_socket ):
3870+ _wait_for_signal (client_socket , b"ready" )
3871+ frame = self ._sample_until_frame (
3872+ p .pid , lambda f : f .funcname .startswith ("fff" )
3873+ )
3874+ self .assertEqual (
3875+ frame .funcname , "f" * 1024 + "…(len=1100)"
3876+ )
3877+
3878+ @skip_if_not_supported
3879+ @unittest .skipIf (
3880+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3881+ "Test only runs on Linux with process_vm_readv support" ,
3882+ )
3883+ def test_long_filename_truncated (self ):
3884+ """A filename longer than 1024 chars is truncated with a marker
3885+ instead of failing the whole sample."""
3886+ script_body = """\
3887+ src = ("def g():\\ n"
3888+ " sock.sendall(b'ready')\\ n"
3889+ " time.sleep(10_000)\\ n")
3890+ ns = {"sock": sock, "time": time}
3891+ exec(compile(src, "x" * 1500 + ".py", "exec"), ns)
3892+ ns["g"]()
3893+ """
3894+ with self ._running_target (script_body ) as (p , client_socket ):
3895+ _wait_for_signal (client_socket , b"ready" )
3896+ frame = self ._sample_until_frame (
3897+ p .pid , lambda f : f .funcname == "g"
3898+ )
3899+ self .assertEqual (
3900+ frame .filename , "x" * 1024 + "…(len=1503)"
3901+ )
3902+
3903+ @skip_if_not_supported
3904+ @unittest .skipIf (
3905+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3906+ "Test only runs on Linux with process_vm_readv support" ,
3907+ )
3908+ def test_large_linetable_keeps_line_numbers (self ):
3909+ """A linetable larger than the old 4096-byte cap keeps line numbers."""
3910+ script_body = """\
3911+ body = "\\ n".join(" x%d = %d" % (i, i) for i in range(4000))
3912+ src = ("def big():\\ n" + body + "\\ n"
3913+ " sock.sendall(b'ready')\\ n"
3914+ " time.sleep(10_000)\\ n")
3915+ ns = {"sock": sock, "time": time}
3916+ exec(src, ns)
3917+ sock.sendall(b"lt:%d\\ n" % len(ns["big"].__code__.co_linetable))
3918+ ns["big"]()
3919+ """
3920+ with self ._running_target (script_body ) as (p , client_socket ):
3921+ buffer = _wait_for_signal (client_socket , [b"lt:" , b"ready" ])
3922+ linetable_size = int (
3923+ buffer .partition (b"lt:" )[2 ].partition (b"\n " )[0 ]
3924+ )
3925+ self .assertGreater (linetable_size , 4096 )
3926+
3927+ frame = self ._sample_until_frame (
3928+ p .pid , lambda f : f .funcname == "big"
3929+ )
3930+ self .assertIsNotNone (
3931+ frame .location , "line info degraded for large linetable"
3932+ )
3933+ self .assertGreater (frame .location .lineno , 4000 )
3934+
3935+ @skip_if_not_supported
3936+ @unittest .skipIf (
3937+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3938+ "Test only runs on Linux with process_vm_readv support" ,
3939+ )
3940+ @unittest .skipIf (
3941+ sys .platform == "win32" ,
3942+ "Process death maps to ProcessLookupError only on POSIX platforms" ,
3943+ )
3944+ def test_dead_process_raises_not_degrades (self ):
3945+ """Death of the target raises ProcessLookupError instead of
3946+ degrading to synthetic frames."""
3947+ script_body = """\
3948+ sock.sendall(b"ready")
3949+ time.sleep(10_000)
3950+ """
3951+ with self ._running_target (script_body ) as (p , client_socket ):
3952+ _wait_for_signal (client_socket , b"ready" )
3953+ unwinder = RemoteUnwinder (p .pid , all_threads = True )
3954+ _get_stack_trace_with_retry (unwinder )
3955+
3956+ p .kill ()
3957+ p .wait ()
3958+
3959+ for _ in busy_retry (SHORT_TIMEOUT , error = False ):
3960+ try :
3961+ unwinder .get_stack_trace ()
3962+ except ProcessLookupError :
3963+ break
3964+ except RuntimeError :
3965+ continue
3966+ else :
3967+ self .fail (
3968+ "ProcessLookupError never raised for dead process"
3969+ )
3970+
3971+
38073972if __name__ == "__main__" :
38083973 unittest .main ()
0 commit comments