Skip to content

Commit 1629b1d

Browse files
authored
[3.11] gh-109709: Fix asyncio test_stdin_broken_pipe() (#109710) (#109735)
gh-109709: Fix asyncio test_stdin_broken_pipe() (#109710) 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(). (cherry picked from commit cbbdf2c)
1 parent f45ef5e commit 1629b1d

File tree

2 files changed

+49
-15
lines changed

2 files changed

+49
-15
lines changed

Lib/asyncio/subprocess.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,16 @@ def kill(self):
147147

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

Lib/test/test_asyncio/test_subprocess.py

+42-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import shutil
33
import signal
44
import sys
5+
import textwrap
56
import unittest
67
import warnings
78
from unittest import mock
@@ -13,9 +14,14 @@
1314
from test import support
1415
from test.support import os_helper
1516

16-
if sys.platform != 'win32':
17+
18+
MS_WINDOWS = (sys.platform == 'win32')
19+
if MS_WINDOWS:
20+
import msvcrt
21+
else:
1722
from asyncio import unix_events
1823

24+
1925
if support.check_sanitizer(address=True):
2026
raise unittest.SkipTest("Exposes ASAN flakiness in GitHub CI")
2127

@@ -253,26 +259,43 @@ async def send_signal(proc):
253259
finally:
254260
signal.signal(signal.SIGHUP, old_handler)
255261

256-
def prepare_broken_pipe_test(self):
262+
def test_stdin_broken_pipe(self):
257263
# buffer large enough to feed the whole pipe buffer
258264
large_data = b'x' * support.PIPE_MAX_SIZE
259265

266+
rfd, wfd = os.pipe()
267+
self.addCleanup(os.close, rfd)
268+
self.addCleanup(os.close, wfd)
269+
if MS_WINDOWS:
270+
handle = msvcrt.get_osfhandle(rfd)
271+
os.set_handle_inheritable(handle, True)
272+
code = textwrap.dedent(f'''
273+
import os, msvcrt
274+
handle = {handle}
275+
fd = msvcrt.open_osfhandle(handle, os.O_RDONLY)
276+
os.read(fd, 1)
277+
''')
278+
from subprocess import STARTUPINFO
279+
startupinfo = STARTUPINFO()
280+
startupinfo.lpAttributeList = {"handle_list": [handle]}
281+
kwargs = dict(startupinfo=startupinfo)
282+
else:
283+
code = f'import os; fd = {rfd}; os.read(fd, 1)'
284+
kwargs = dict(pass_fds=(rfd,))
285+
260286
# the program ends before the stdin can be fed
261287
proc = self.loop.run_until_complete(
262288
asyncio.create_subprocess_exec(
263-
sys.executable, '-c', 'pass',
289+
sys.executable, '-c', code,
264290
stdin=subprocess.PIPE,
291+
**kwargs
265292
)
266293
)
267294

268-
return (proc, large_data)
269-
270-
def test_stdin_broken_pipe(self):
271-
proc, large_data = self.prepare_broken_pipe_test()
272-
273295
async def write_stdin(proc, data):
274-
await asyncio.sleep(0.5)
275296
proc.stdin.write(data)
297+
# Only exit the child process once the write buffer is filled
298+
os.write(wfd, b'go')
276299
await proc.stdin.drain()
277300

278301
coro = write_stdin(proc, large_data)
@@ -283,7 +306,16 @@ async def write_stdin(proc, data):
283306
self.loop.run_until_complete(proc.wait())
284307

285308
def test_communicate_ignore_broken_pipe(self):
286-
proc, large_data = self.prepare_broken_pipe_test()
309+
# buffer large enough to feed the whole pipe buffer
310+
large_data = b'x' * support.PIPE_MAX_SIZE
311+
312+
# the program ends before the stdin can be fed
313+
proc = self.loop.run_until_complete(
314+
asyncio.create_subprocess_exec(
315+
sys.executable, '-c', 'pass',
316+
stdin=subprocess.PIPE,
317+
)
318+
)
287319

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

0 commit comments

Comments
 (0)