Skip to content

Commit 0972e00

Browse files
committed
test
1 parent dd6e19a commit 0972e00

1 file changed

Lines changed: 154 additions & 34 deletions

File tree

Lib/test/test_external_inspection.py

Lines changed: 154 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,40 @@ def _run_script_and_get_trace(
339339
finally:
340340
_cleanup_sockets(client_socket, server_socket)
341341

342+
@contextmanager
343+
def _target_process(self, script_body):
344+
"""Context manager for running a target process with socket sync."""
345+
port = find_unused_port()
346+
script = f"""\
347+
import socket
348+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
349+
sock.connect(('localhost', {port}))
350+
{textwrap.dedent(script_body)}
351+
"""
352+
353+
with os_helper.temp_dir() as work_dir:
354+
script_dir = os.path.join(work_dir, "script_pkg")
355+
os.mkdir(script_dir)
356+
357+
server_socket = _create_server_socket(port)
358+
script_name = _make_test_script(script_dir, "script", script)
359+
client_socket = None
360+
361+
try:
362+
with _managed_subprocess([sys.executable, script_name]) as p:
363+
client_socket, _ = server_socket.accept()
364+
server_socket.close()
365+
server_socket = None
366+
367+
def make_unwinder(cache_frames=True):
368+
return RemoteUnwinder(
369+
p.pid, all_threads=True, cache_frames=cache_frames
370+
)
371+
372+
yield p, client_socket, make_unwinder
373+
finally:
374+
_cleanup_sockets(client_socket, server_socket)
375+
342376
def _find_frame_in_trace(self, stack_trace, predicate):
343377
"""
344378
Find a frame matching predicate in stack trace.
@@ -2927,40 +2961,6 @@ class TestFrameCaching(RemoteInspectionTestBase):
29272961
All tests verify cache reuse via object identity checks (assertIs).
29282962
"""
29292963

2930-
@contextmanager
2931-
def _target_process(self, script_body):
2932-
"""Context manager for running a target process with socket sync."""
2933-
port = find_unused_port()
2934-
script = f"""\
2935-
import socket
2936-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2937-
sock.connect(('localhost', {port}))
2938-
{textwrap.dedent(script_body)}
2939-
"""
2940-
2941-
with os_helper.temp_dir() as work_dir:
2942-
script_dir = os.path.join(work_dir, "script_pkg")
2943-
os.mkdir(script_dir)
2944-
2945-
server_socket = _create_server_socket(port)
2946-
script_name = _make_test_script(script_dir, "script", script)
2947-
client_socket = None
2948-
2949-
try:
2950-
with _managed_subprocess([sys.executable, script_name]) as p:
2951-
client_socket, _ = server_socket.accept()
2952-
server_socket.close()
2953-
server_socket = None
2954-
2955-
def make_unwinder(cache_frames=True):
2956-
return RemoteUnwinder(
2957-
p.pid, all_threads=True, cache_frames=cache_frames
2958-
)
2959-
2960-
yield p, client_socket, make_unwinder
2961-
finally:
2962-
_cleanup_sockets(client_socket, server_socket)
2963-
29642964
def _get_frames_with_retry(self, unwinder, required_funcs):
29652965
"""Get frames containing required_funcs, with retry for transient errors."""
29662966
for _ in range(MAX_TRIES):
@@ -3804,5 +3804,125 @@ def test_get_stats_disabled_raises(self):
38043804
client_socket.sendall(b"done")
38053805

38063806

3807+
@requires_remote_subprocess_debugging()
3808+
class TestFrameChainLimits(RemoteInspectionTestBase):
3809+
"""Frame chain walks abort instead of looping/overflowing on deep chains."""
3810+
3811+
CHAIN_DEPTH = 1024 + 512 + 1
3812+
3813+
def _assert_unwinder_limit_error(self, unwind, expected_substring):
3814+
"""Call unwind() until it raises the frame chain limit error.
3815+
3816+
unwind must construct the RemoteUnwinder and call it, so that
3817+
transient RuntimeErrors from either step are retried; a successful
3818+
call means the limit never triggered and fails immediately.
3819+
"""
3820+
last_error = None
3821+
for _ in busy_retry(SHORT_TIMEOUT, error=False):
3822+
try:
3823+
unwind()
3824+
except TRANSIENT_ERRORS as e:
3825+
if expected_substring in str(e):
3826+
return
3827+
last_error = e
3828+
continue
3829+
self.fail(
3830+
"frame chain limit did not trigger; call returned a result"
3831+
)
3832+
self.fail(
3833+
f"frame chain limit never raised; last transient error: "
3834+
f"{last_error!r}"
3835+
)
3836+
3837+
@skip_if_not_supported
3838+
@unittest.skipIf(
3839+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3840+
"Test only runs on Linux with process_vm_readv support",
3841+
)
3842+
def test_get_stack_trace_deep_frame_chain_aborts(self):
3843+
"""Test that a frame chain deeper than the limit aborts the
3844+
synchronous stack walk instead of walking it indefinitely."""
3845+
script_body = f"""\
3846+
import sys
3847+
sys.setrecursionlimit({self.CHAIN_DEPTH * 2})
3848+
3849+
def recurse(n):
3850+
if n <= 0:
3851+
sock.sendall(b"ready")
3852+
sock.recv(16)
3853+
return
3854+
recurse(n - 1)
3855+
3856+
recurse({self.CHAIN_DEPTH})
3857+
"""
3858+
with self._target_process(script_body) as (p, client_socket, _):
3859+
_wait_for_signal(client_socket, b"ready")
3860+
self._assert_unwinder_limit_error(
3861+
lambda: RemoteUnwinder(p.pid).get_stack_trace(),
3862+
"Too many stack frames",
3863+
)
3864+
client_socket.sendall(b"done")
3865+
3866+
@skip_if_not_supported
3867+
@unittest.skipIf(
3868+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3869+
"Test only runs on Linux with process_vm_readv support",
3870+
)
3871+
def test_get_async_stack_trace_deep_frame_chain_aborts(self):
3872+
"""Test that a frame chain deeper than the limit aborts the async
3873+
stack walk instead of walking it indefinitely."""
3874+
script_body = f"""\
3875+
import sys, asyncio
3876+
sys.setrecursionlimit({self.CHAIN_DEPTH * 2})
3877+
3878+
def recurse(n):
3879+
if n <= 0:
3880+
sock.sendall(b"ready")
3881+
sock.recv(16)
3882+
return
3883+
recurse(n - 1)
3884+
3885+
async def deep():
3886+
recurse({self.CHAIN_DEPTH})
3887+
3888+
asyncio.run(deep())
3889+
"""
3890+
with self._target_process(script_body) as (p, client_socket, _):
3891+
_wait_for_signal(client_socket, b"ready")
3892+
self._assert_unwinder_limit_error(
3893+
lambda: RemoteUnwinder(p.pid).get_async_stack_trace(),
3894+
"Too many async stack frames",
3895+
)
3896+
client_socket.sendall(b"done")
3897+
3898+
@skip_if_not_supported
3899+
@unittest.skipIf(
3900+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3901+
"Test only runs on Linux with process_vm_readv support",
3902+
)
3903+
def test_get_all_awaited_by_deep_coro_chain_aborts(self):
3904+
"""Test that a coroutine await chain deeper than the limit aborts
3905+
the walk instead of overflowing the C stack."""
3906+
script_body = f"""\
3907+
import sys, asyncio
3908+
sys.setrecursionlimit({self.CHAIN_DEPTH * 2})
3909+
3910+
async def chain(n):
3911+
if n <= 0:
3912+
sock.sendall(b"ready")
3913+
await asyncio.sleep(10_000)
3914+
return
3915+
await chain(n - 1)
3916+
3917+
asyncio.run(chain({self.CHAIN_DEPTH}))
3918+
"""
3919+
with self._target_process(script_body) as (p, client_socket, _):
3920+
_wait_for_signal(client_socket, b"ready")
3921+
self._assert_unwinder_limit_error(
3922+
lambda: RemoteUnwinder(p.pid).get_all_awaited_by(),
3923+
"Too many coroutine frames",
3924+
)
3925+
3926+
38073927
if __name__ == "__main__":
38083928
unittest.main()

0 commit comments

Comments
 (0)