2121import sysconfig
2222import select
2323import shutil
24+ import socket
2425import threading
2526import gc
2627import textwrap
@@ -1043,19 +1044,49 @@ def test_communicate_timeout_large_input(self):
10431044 # On Windows, stdin writing must also honor the timeout rather than
10441045 # blocking indefinitely when the pipe buffer fills.
10451046
1046- # Input larger than typical pipe buffer (4-64KB on Windows)
1047- input_data = b"x" * (128 * 1024 )
1047+ input_data = b"x" * (128 * 1024 ) # > typical pipe buffer
1048+
1049+ # Cross-platform wake mechanism: the slow reader connects to a
1050+ # loopback TCP socket and blocks in select() on it (capped at 9s
1051+ # as a safety net we don't expect to hit). After phase 1 raises
1052+ # TimeoutExpired, the parent sends a byte to release the child so
1053+ # it drains stdin. A socket (rather than a raw pipe) is required
1054+ # because Windows select() only supports sockets, not arbitrary
1055+ # file descriptors.
1056+ server = socket .create_server (('127.0.0.1' , 0 ), backlog = 1 )
1057+ server .settimeout (10 ) # bound the accept() if the child fails to start
1058+ port = server .getsockname ()[1 ]
1059+ # The child sends one byte (low byte of its PID) first so the parent
1060+ # can detect the rare case of an unrelated process on the same host
1061+ # connecting to our ephemeral port before our child does. A single
1062+ # byte gives 1/256 collision odds, which is plenty for flake-prevention.
1063+ slow_reader = (
1064+ "import os, socket, sys, select; "
1065+ f"s = socket.create_connection(('127.0.0.1', { port } ), timeout=9); "
1066+ "s.sendall(bytes([os.getpid() & 0xff])); "
1067+ "select.select([s], [], [], 9); "
1068+ "sys.stdout.buffer.write(sys.stdin.buffer.read())"
1069+ )
10481070
10491071 p = subprocess .Popen (
1050- [sys .executable , "-c" ,
1051- "import sys, time; "
1052- "time.sleep(30); " # Don't read stdin for a long time
1053- "sys.stdout.buffer.write(sys.stdin.buffer.read())" ],
1072+ [sys .executable , "-c" , slow_reader ],
10541073 stdin = subprocess .PIPE ,
10551074 stdout = subprocess .PIPE ,
10561075 stderr = subprocess .PIPE )
10571076
1077+ conn = None
10581078 try :
1079+ conn , _ = server .accept ()
1080+ server .close ()
1081+ server = None
1082+
1083+ conn .settimeout (5 )
1084+ peer_byte = conn .recv (1 )
1085+ conn .settimeout (None )
1086+ self .assertEqual (peer_byte , bytes ([p .pid & 0xff ]),
1087+ f"loopback handshake byte { peer_byte !r} != "
1088+ f"low byte of child PID { p .pid } ({ p .pid & 0xff :#x} )" )
1089+
10591090 timeout = 0.2
10601091 start = time .monotonic ()
10611092 try :
@@ -1064,19 +1095,24 @@ def test_communicate_timeout_large_input(self):
10641095 elapsed = time .monotonic () - start
10651096 self .fail (
10661097 f"TimeoutExpired not raised. communicate() completed in "
1067- f"{ elapsed :.2f} s, but subprocess sleeps for 30s . "
1098+ f"{ elapsed :.2f} s, but slow reader stalls for up to 9s . "
10681099 "Stdin writing blocked without enforcing timeout." )
10691100 except subprocess .TimeoutExpired :
10701101 elapsed = time .monotonic () - start
10711102
10721103 # Timeout should occur close to the specified timeout value,
10731104 # not after waiting for the subprocess to finish sleeping.
10741105 # Allow generous margin for slow CI, but must be well under
1075- # the subprocess sleep time .
1106+ # the slow-reader's stall cap .
10761107 self .assertLess (elapsed , 5.0 ,
10771108 f"TimeoutExpired raised after { elapsed :.2f} s; expected ~{ timeout } s. "
10781109 "Stdin writing blocked without checking timeout." )
10791110
1111+ # Release the slow reader so it stops blocking and drains stdin.
1112+ conn .sendall (b'go' )
1113+ conn .close ()
1114+ conn = None
1115+
10801116 # After timeout, continue communication. The remaining input
10811117 # should be sent and we should receive all data back.
10821118 stdout , stderr = p .communicate ()
@@ -1086,6 +1122,10 @@ def test_communicate_timeout_large_input(self):
10861122 f"Expected { len (input_data )} bytes output but got { len (stdout )} " )
10871123 self .assertEqual (stdout , input_data )
10881124 finally :
1125+ if conn is not None :
1126+ conn .close ()
1127+ if server is not None :
1128+ server .close ()
10891129 p .kill ()
10901130 p .wait ()
10911131
0 commit comments