Skip to content

Commit bdc28cc

Browse files
gpsheadclaude
andcommitted
Add regression test for subprocess timeout during large input write
Add test_communicate_timeout_large_input to verify that TimeoutExpired is raised promptly when communicate() is called with large input and a timeout, even when the subprocess doesn't consume stdin quickly. This test currently passes on POSIX (where select() is used) but is expected to fail on Windows where the stdin write blocks without checking the timeout. The test sends 128KB of input (larger than typical pipe buffers) to a subprocess that sleeps for 3 seconds before reading. With a 0.5s timeout, TimeoutExpired should be raised within a couple seconds, not after the subprocess finishes sleeping. After timeout, the test verifies that input continuation works by calling communicate() again and checking all data was received (testing the fix from gh-141473). Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 526d7a8 commit bdc28cc

File tree

1 file changed

+56
-0
lines changed

1 file changed

+56
-0
lines changed

Lib/test/test_subprocess.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,62 @@ def test_communicate_timeout_large_output(self):
992992
(stdout, _) = p.communicate()
993993
self.assertEqual(len(stdout), 4 * 64 * 1024)
994994

995+
def test_communicate_timeout_large_input(self):
996+
# Test that timeout is enforced when writing large input to a
997+
# slow-to-read subprocess, and that partial input is preserved
998+
# for continuation after timeout (gh-141473).
999+
#
1000+
# This is a regression test for Windows matching POSIX behavior.
1001+
# On POSIX, select() is used to multiplex I/O with timeout checking.
1002+
# On Windows, stdin writing must also honor the timeout rather than
1003+
# blocking indefinitely when the pipe buffer fills.
1004+
1005+
# Input larger than typical pipe buffer (4-64KB on Windows)
1006+
input_data = b"x" * (128 * 1024)
1007+
1008+
p = subprocess.Popen(
1009+
[sys.executable, "-c",
1010+
"import sys, time; "
1011+
"time.sleep(30); " # Don't read stdin for a long time
1012+
"sys.stdout.buffer.write(sys.stdin.buffer.read())"],
1013+
stdin=subprocess.PIPE,
1014+
stdout=subprocess.PIPE,
1015+
stderr=subprocess.PIPE)
1016+
1017+
try:
1018+
timeout = 0.5
1019+
start = time.monotonic()
1020+
try:
1021+
p.communicate(input_data, timeout=timeout)
1022+
# If we get here without TimeoutExpired, the timeout was ignored
1023+
elapsed = time.monotonic() - start
1024+
self.fail(
1025+
f"TimeoutExpired not raised. communicate() completed in "
1026+
f"{elapsed:.2f}s, but subprocess sleeps for 30s. "
1027+
"Stdin writing blocked without enforcing timeout.")
1028+
except subprocess.TimeoutExpired:
1029+
elapsed = time.monotonic() - start
1030+
1031+
# Timeout should occur close to the specified timeout value,
1032+
# not after waiting for the subprocess to finish sleeping.
1033+
# Allow generous margin for slow CI, but must be well under
1034+
# the subprocess sleep time.
1035+
self.assertLess(elapsed, 5.0,
1036+
f"TimeoutExpired raised after {elapsed:.2f}s; expected ~{timeout}s. "
1037+
"Stdin writing blocked without checking timeout.")
1038+
1039+
# After timeout, continue communication. The remaining input
1040+
# should be sent and we should receive all data back.
1041+
stdout, stderr = p.communicate()
1042+
1043+
# Verify all input was eventually received by the subprocess
1044+
self.assertEqual(len(stdout), len(input_data),
1045+
f"Expected {len(input_data)} bytes output but got {len(stdout)}")
1046+
self.assertEqual(stdout, input_data)
1047+
finally:
1048+
p.kill()
1049+
p.wait()
1050+
9951051
# Test for the fd leak reported in http://bugs.python.org/issue2791.
9961052
def test_communicate_pipe_fd_leak(self):
9971053
for stdin_pipe in (False, True):

0 commit comments

Comments
 (0)