Skip to content

Commit 24b415d

Browse files
committed
gh-109709: Fix asyncio test_stdin_broken_pipe()
Replace harcoded sleep of 500 ms with synchronization using a pipe. Fix also Process._feed_stdin(): catch also BrokenPipeError on stdin.write(input), not only on stdin.drain().
1 parent 1eb1b45 commit 24b415d

File tree

2 files changed

+28
-15
lines changed

2 files changed

+28
-15
lines changed

Lib/asyncio/subprocess.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,17 @@ def kill(self):
147147

148148
async def _feed_stdin(self, input):
149149
debug = self._loop.get_debug()
150-
if input is not None:
151-
self.stdin.write(input)
152-
if debug:
153-
logger.debug(
154-
'%r communicate: feed stdin (%s bytes)', self, len(input))
155150
try:
151+
if input is not None:
152+
self.stdin.write(input)
153+
if debug:
154+
logger.debug(
155+
'%r communicate: feed stdin (%s bytes)', self, len(input))
156+
156157
await self.stdin.drain()
157158
except (BrokenPipeError, ConnectionResetError) as exc:
158-
# communicate() ignores BrokenPipeError and ConnectionResetError
159+
# communicate() ignores BrokenPipeError and ConnectionResetError.
160+
# write() and drain() can raise these exceptions.
159161
if debug:
160162
logger.debug('%r communicate: stdin got %r', self, exc)
161163

Lib/test/test_asyncio/test_subprocess.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,26 +270,28 @@ async def send_signal(proc):
270270
finally:
271271
signal.signal(signal.SIGHUP, old_handler)
272272

273-
def prepare_broken_pipe_test(self):
273+
def test_stdin_broken_pipe(self):
274274
# buffer large enough to feed the whole pipe buffer
275275
large_data = b'x' * support.PIPE_MAX_SIZE
276276

277+
rfd, wfd = os.pipe()
278+
self.addCleanup(os.close, rfd)
279+
self.addCleanup(os.close, wfd)
280+
code = f'import os; fd = {rfd}; os.read(fd, 1)'
281+
277282
# the program ends before the stdin can be fed
278283
proc = self.loop.run_until_complete(
279284
asyncio.create_subprocess_exec(
280-
sys.executable, '-c', 'pass',
285+
sys.executable, '-c', code,
281286
stdin=subprocess.PIPE,
287+
pass_fds=(rfd,),
282288
)
283289
)
284290

285-
return (proc, large_data)
286-
287-
def test_stdin_broken_pipe(self):
288-
proc, large_data = self.prepare_broken_pipe_test()
289-
290291
async def write_stdin(proc, data):
291-
await asyncio.sleep(0.5)
292292
proc.stdin.write(data)
293+
# Only exit the child process once the write buffer is filled
294+
os.write(wfd, b'go')
293295
await proc.stdin.drain()
294296

295297
coro = write_stdin(proc, large_data)
@@ -300,7 +302,16 @@ async def write_stdin(proc, data):
300302
self.loop.run_until_complete(proc.wait())
301303

302304
def test_communicate_ignore_broken_pipe(self):
303-
proc, large_data = self.prepare_broken_pipe_test()
305+
# buffer large enough to feed the whole pipe buffer
306+
large_data = b'x' * support.PIPE_MAX_SIZE
307+
308+
# the program ends before the stdin can be fed
309+
proc = self.loop.run_until_complete(
310+
asyncio.create_subprocess_exec(
311+
sys.executable, '-c', 'pass',
312+
stdin=subprocess.PIPE,
313+
)
314+
)
304315

305316
# communicate() must ignore BrokenPipeError when feeding stdin
306317
self.loop.set_exception_handler(lambda loop, msg: None)

0 commit comments

Comments
 (0)