@@ -3854,5 +3854,155 @@ def test_get_stats_disabled_raises(self):
38543854 client_socket .sendall (b"done" )
38553855
38563856
3857+ @requires_remote_subprocess_debugging ()
3858+ @skip_if_not_supported
3859+ @unittest .skipIf (
3860+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3861+ "Test only runs on Linux with process_vm_readv support" ,
3862+ )
3863+ class TestMetadataDegradation (RemoteInspectionTestBase ):
3864+ """Tests for graceful degradation of oversized code-object metadata."""
3865+
3866+ @contextmanager
3867+ def _running_target (self , script_body ):
3868+ """Run a target script (socket handshake prepended), yield (process, socket)."""
3869+ port = find_unused_port ()
3870+ script = (
3871+ textwrap .dedent (
3872+ f"""\
3873+ import time, socket
3874+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3875+ sock.connect(('localhost', { port } ))
3876+ """
3877+ )
3878+ + textwrap .dedent (script_body )
3879+ )
3880+
3881+ with os_helper .temp_dir () as work_dir :
3882+ script_dir = os .path .join (work_dir , "script_pkg" )
3883+ os .mkdir (script_dir )
3884+
3885+ server_socket = _create_server_socket (port )
3886+ script_name = _make_test_script (script_dir , "script" , script )
3887+ client_socket = None
3888+
3889+ try :
3890+ with _managed_subprocess ([sys .executable , script_name ]) as p :
3891+ client_socket , _ = server_socket .accept ()
3892+ server_socket .close ()
3893+ server_socket = None
3894+ yield p , client_socket
3895+ finally :
3896+ _cleanup_sockets (client_socket , server_socket )
3897+
3898+ def _sample_until_frame (self , pid , predicate ):
3899+ """Sample until a frame matching predicate appears; return that frame."""
3900+ unwinder = RemoteUnwinder (pid , all_threads = True )
3901+ traces = _get_stack_trace_with_retry (
3902+ unwinder ,
3903+ condition = lambda t : self ._find_frame_in_trace (t , predicate )
3904+ is not None ,
3905+ )
3906+ return self ._find_frame_in_trace (traces , predicate )
3907+
3908+ def test_long_qualname_truncated_not_dropped (self ):
3909+ """A qualname longer than 1024 chars is truncated with a marker
3910+ instead of failing the whole sample."""
3911+ script_body = """\
3912+ src = ("def " + "f" * 1100 + "():\\ n"
3913+ " sock.sendall(b'ready')\\ n"
3914+ " time.sleep(10_000)\\ n")
3915+ ns = {"sock": sock, "time": time}
3916+ exec(src, ns)
3917+ ns["f" * 1100]()
3918+ """
3919+ with self ._running_target (script_body ) as (p , client_socket ):
3920+ _wait_for_signal (client_socket , b"ready" )
3921+ frame = self ._sample_until_frame (
3922+ p .pid , lambda f : f .funcname .startswith ("fff" )
3923+ )
3924+ self .assertEqual (
3925+ frame .funcname , "f" * 1024 + "(len=1100)"
3926+ )
3927+
3928+ def test_long_filename_truncated (self ):
3929+ """A filename longer than 1024 chars is truncated with a marker
3930+ instead of failing the whole sample."""
3931+ script_body = """\
3932+ src = ("def g():\\ n"
3933+ " sock.sendall(b'ready')\\ n"
3934+ " time.sleep(10_000)\\ n")
3935+ ns = {"sock": sock, "time": time}
3936+ exec(compile(src, "x" * 1500 + ".py", "exec"), ns)
3937+ ns["g"]()
3938+ """
3939+ with self ._running_target (script_body ) as (p , client_socket ):
3940+ _wait_for_signal (client_socket , b"ready" )
3941+ frame = self ._sample_until_frame (
3942+ p .pid , lambda f : f .funcname == "g"
3943+ )
3944+ self .assertEqual (
3945+ frame .filename , "x" * 1024 + "(len=1503)"
3946+ )
3947+
3948+ def test_large_linetable_keeps_line_numbers (self ):
3949+ """A linetable larger than the old 4096-byte cap keeps line numbers."""
3950+ script_body = """\
3951+ body = "\\ n".join(" x%d = %d" % (i, i) for i in range(4000))
3952+ src = ("def big():\\ n" + body + "\\ n"
3953+ " sock.sendall(b'ready')\\ n"
3954+ " time.sleep(10_000)\\ n")
3955+ ns = {"sock": sock, "time": time}
3956+ exec(src, ns)
3957+ sock.sendall(b"lt:%d\\ n" % len(ns["big"].__code__.co_linetable))
3958+ ns["big"]()
3959+ """
3960+ with self ._running_target (script_body ) as (p , client_socket ):
3961+ buffer = _wait_for_signal (client_socket , [b"lt:" , b"ready" ])
3962+ linetable_size = int (
3963+ buffer .partition (b"lt:" )[2 ].partition (b"\n " )[0 ]
3964+ )
3965+ self .assertGreater (linetable_size , 4096 )
3966+
3967+ frame = self ._sample_until_frame (
3968+ p .pid , lambda f : f .funcname == "big"
3969+ )
3970+ self .assertIsNotNone (
3971+ frame .location , "line info degraded for large linetable"
3972+ )
3973+ self .assertGreater (frame .location .lineno , 4000 )
3974+
3975+ @unittest .skipIf (
3976+ sys .platform == "win32" ,
3977+ "Process death maps to ProcessLookupError only on POSIX platforms" ,
3978+ )
3979+ def test_dead_process_raises_not_degrades (self ):
3980+ """Death of the target raises ProcessLookupError instead of
3981+ degrading to synthetic frames."""
3982+ script_body = """\
3983+ sock.sendall(b"ready")
3984+ time.sleep(10_000)
3985+ """
3986+ with self ._running_target (script_body ) as (p , client_socket ):
3987+ _wait_for_signal (client_socket , b"ready" )
3988+ unwinder = RemoteUnwinder (p .pid , all_threads = True )
3989+ _get_stack_trace_with_retry (unwinder )
3990+
3991+ p .kill ()
3992+ p .wait ()
3993+
3994+ for _ in busy_retry (SHORT_TIMEOUT , error = False ):
3995+ try :
3996+ unwinder .get_stack_trace ()
3997+ except ProcessLookupError :
3998+ break
3999+ except RuntimeError :
4000+ continue
4001+ else :
4002+ self .fail (
4003+ "ProcessLookupError never raised for dead process"
4004+ )
4005+
4006+
38574007if __name__ == "__main__" :
38584008 unittest .main ()
0 commit comments