test-stdio.py
365 lines
| 11.1 KiB
| text/x-python
|
PythonLexer
/ tests / test-stdio.py
r48294 | #!/usr/bin/env python | |||
|
r45583 | """ | ||
Tests the buffering behavior of stdio streams in `mercurial.utils.procutil`. | ||||
""" | ||||
from __future__ import absolute_import | ||||
import contextlib | ||||
|
r45628 | import errno | ||
|
r45583 | import os | ||
|
r45655 | import signal | ||
|
r45583 | import subprocess | ||
import sys | ||||
|
r45657 | import tempfile | ||
|
r45583 | import unittest | ||
|
r45708 | from mercurial import pycompat, util | ||
|
r45583 | |||
|
r45707 | if pycompat.ispy3: | ||
def set_noninheritable(fd): | ||||
# On Python 3, file descriptors are non-inheritable by default. | ||||
pass | ||||
else: | ||||
if pycompat.iswindows: | ||||
# unused | ||||
set_noninheritable = None | ||||
else: | ||||
import fcntl | ||||
def set_noninheritable(fd): | ||||
old = fcntl.fcntl(fd, fcntl.F_GETFD) | ||||
fcntl.fcntl(fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC) | ||||
|
r45638 | TEST_BUFFERING_CHILD_SCRIPT = r''' | ||
|
r45583 | import os | ||
from mercurial import dispatch | ||||
from mercurial.utils import procutil | ||||
dispatch.initstdio() | ||||
|
r45589 | procutil.{stream}.write(b'aaa') | ||
os.write(procutil.{stream}.fileno(), b'[written aaa]') | ||||
procutil.{stream}.write(b'bbb\n') | ||||
os.write(procutil.{stream}.fileno(), b'[written bbb\\n]') | ||||
|
r45583 | ''' | ||
UNBUFFERED = b'aaa[written aaa]bbb\n[written bbb\\n]' | ||||
LINE_BUFFERED = b'[written aaa]aaabbb\n[written bbb\\n]' | ||||
FULLY_BUFFERED = b'[written aaa][written bbb\\n]aaabbb\n' | ||||
|
r45655 | TEST_LARGE_WRITE_CHILD_SCRIPT = r''' | ||
|
r45664 | import os | ||
|
r45655 | import signal | ||
import sys | ||||
from mercurial import dispatch | ||||
from mercurial.utils import procutil | ||||
signal.signal(signal.SIGINT, lambda *x: None) | ||||
dispatch.initstdio() | ||||
|
r45657 | write_result = procutil.{stream}.write(b'x' * 1048576) | ||
|
r45664 | with os.fdopen( | ||
os.open({write_result_fn!r}, os.O_WRONLY | getattr(os, 'O_TEMPORARY', 0)), | ||||
'w', | ||||
) as write_result_f: | ||||
|
r45657 | write_result_f.write(str(write_result)) | ||
|
r45655 | ''' | ||
|
r45708 | TEST_BROKEN_PIPE_CHILD_SCRIPT = r''' | ||
import os | ||||
import pickle | ||||
from mercurial import dispatch | ||||
from mercurial.utils import procutil | ||||
dispatch.initstdio() | ||||
procutil.stdin.read(1) # wait until parent process closed pipe | ||||
try: | ||||
procutil.{stream}.write(b'test') | ||||
procutil.{stream}.flush() | ||||
except EnvironmentError as e: | ||||
with os.fdopen( | ||||
os.open( | ||||
{err_fn!r}, | ||||
os.O_WRONLY | ||||
| getattr(os, 'O_BINARY', 0) | ||||
| getattr(os, 'O_TEMPORARY', 0), | ||||
), | ||||
'wb', | ||||
) as err_f: | ||||
pickle.dump(e, err_f) | ||||
# Exit early to suppress further broken pipe errors at interpreter shutdown. | ||||
os._exit(0) | ||||
''' | ||||
|
r45583 | @contextlib.contextmanager | ||
def _closing(fds): | ||||
try: | ||||
yield | ||||
finally: | ||||
for fd in fds: | ||||
try: | ||||
os.close(fd) | ||||
except EnvironmentError: | ||||
pass | ||||
|
r45707 | # In the following, we set the FDs non-inheritable mainly to make it possible | ||
# for tests to close the receiving end of the pipe / PTYs. | ||||
|
r45583 | @contextlib.contextmanager | ||
|
r45656 | def _devnull(): | ||
devnull = os.open(os.devnull, os.O_WRONLY) | ||||
|
r45707 | # We don't have a receiving end, so it's not worth the effort on Python 2 | ||
# on Windows to make the FD non-inheritable. | ||||
|
r45656 | with _closing([devnull]): | ||
yield (None, devnull) | ||||
@contextlib.contextmanager | ||||
|
r45583 | def _pipes(): | ||
rwpair = os.pipe() | ||||
|
r45707 | # Pipes are already non-inheritable on Windows. | ||
if not pycompat.iswindows: | ||||
set_noninheritable(rwpair[0]) | ||||
set_noninheritable(rwpair[1]) | ||||
|
r45583 | with _closing(rwpair): | ||
yield rwpair | ||||
@contextlib.contextmanager | ||||
def _ptys(): | ||||
if pycompat.iswindows: | ||||
raise unittest.SkipTest("PTYs are not supported on Windows") | ||||
import pty | ||||
import tty | ||||
rwpair = pty.openpty() | ||||
|
r45707 | set_noninheritable(rwpair[0]) | ||
set_noninheritable(rwpair[1]) | ||||
|
r45583 | with _closing(rwpair): | ||
tty.setraw(rwpair[0]) | ||||
yield rwpair | ||||
|
r45655 | def _readall(fd, buffer_size, initial_buf=None): | ||
buf = initial_buf or [] | ||||
|
r45628 | while True: | ||
try: | ||||
s = os.read(fd, buffer_size) | ||||
except OSError as e: | ||||
if e.errno == errno.EIO: | ||||
# If the child-facing PTY got closed, reading from the | ||||
# parent-facing PTY raises EIO. | ||||
break | ||||
raise | ||||
if not s: | ||||
break | ||||
buf.append(s) | ||||
return b''.join(buf) | ||||
|
r45589 | class TestStdio(unittest.TestCase): | ||
|
r45638 | def _test( | ||
self, | ||||
child_script, | ||||
stream, | ||||
rwpair_generator, | ||||
check_output, | ||||
python_args=[], | ||||
|
r45657 | post_child_check=None, | ||
|
r45708 | stdin_generator=None, | ||
|
r45638 | ): | ||
|
r45589 | assert stream in ('stdout', 'stderr') | ||
|
r45708 | if stdin_generator is None: | ||
stdin_generator = open(os.devnull, 'rb') | ||||
with rwpair_generator() as ( | ||||
stream_receiver, | ||||
child_stream, | ||||
), stdin_generator as child_stdin: | ||||
|
r45583 | proc = subprocess.Popen( | ||
|
r45638 | [sys.executable] + python_args + ['-c', child_script], | ||
|
r45583 | stdin=child_stdin, | ||
|
r45589 | stdout=child_stream if stream == 'stdout' else None, | ||
stderr=child_stream if stream == 'stderr' else None, | ||||
|
r45583 | ) | ||
|
r45628 | try: | ||
os.close(child_stream) | ||||
|
r45656 | if stream_receiver is not None: | ||
check_output(stream_receiver, proc) | ||||
|
r45629 | except: # re-raises | ||
proc.terminate() | ||||
raise | ||||
|
r45628 | finally: | ||
retcode = proc.wait() | ||||
|
r45583 | self.assertEqual(retcode, 0) | ||
|
r45657 | if post_child_check is not None: | ||
post_child_check() | ||||
|
r45583 | |||
|
r45638 | def _test_buffering( | ||
self, stream, rwpair_generator, expected_output, python_args=[] | ||||
): | ||||
|
r45655 | def check_output(stream_receiver, proc): | ||
|
r45638 | self.assertEqual(_readall(stream_receiver, 1024), expected_output) | ||
self._test( | ||||
TEST_BUFFERING_CHILD_SCRIPT.format(stream=stream), | ||||
stream, | ||||
rwpair_generator, | ||||
check_output, | ||||
python_args, | ||||
) | ||||
|
r45656 | def test_buffering_stdout_devnull(self): | ||
self._test_buffering('stdout', _devnull, None) | ||||
|
r45630 | def test_buffering_stdout_pipes(self): | ||
|
r45638 | self._test_buffering('stdout', _pipes, FULLY_BUFFERED) | ||
|
r45583 | |||
|
r45630 | def test_buffering_stdout_ptys(self): | ||
|
r45638 | self._test_buffering('stdout', _ptys, LINE_BUFFERED) | ||
|
r45583 | |||
|
r45656 | def test_buffering_stdout_devnull_unbuffered(self): | ||
self._test_buffering('stdout', _devnull, None, python_args=['-u']) | ||||
|
r45630 | def test_buffering_stdout_pipes_unbuffered(self): | ||
|
r45638 | self._test_buffering('stdout', _pipes, UNBUFFERED, python_args=['-u']) | ||
|
r45583 | |||
|
r45630 | def test_buffering_stdout_ptys_unbuffered(self): | ||
|
r45638 | self._test_buffering('stdout', _ptys, UNBUFFERED, python_args=['-u']) | ||
|
r45583 | |||
if not pycompat.ispy3 and not pycompat.iswindows: | ||||
# On Python 2 on non-Windows, we manually open stdout in line-buffered | ||||
# mode if connected to a TTY. We should check if Python was configured | ||||
# to use unbuffered stdout, but it's hard to do that. | ||||
|
r45630 | test_buffering_stdout_ptys_unbuffered = unittest.expectedFailure( | ||
test_buffering_stdout_ptys_unbuffered | ||||
|
r45583 | ) | ||
|
r45655 | def _test_large_write(self, stream, rwpair_generator, python_args=[]): | ||
if not pycompat.ispy3 and pycompat.isdarwin: | ||||
# Python 2 doesn't always retry on EINTR, but the libc might retry. | ||||
# So far, it was observed only on macOS that EINTR is raised at the | ||||
# Python level. As Python 2 support will be dropped soon-ish, we | ||||
# won't attempt to fix it. | ||||
raise unittest.SkipTest("raises EINTR on macOS") | ||||
def check_output(stream_receiver, proc): | ||||
if not pycompat.iswindows: | ||||
# On Unix, we can provoke a partial write() by interrupting it | ||||
# by a signal handler as soon as a bit of data was written. | ||||
# We test that write() is called until all data is written. | ||||
buf = [os.read(stream_receiver, 1)] | ||||
proc.send_signal(signal.SIGINT) | ||||
else: | ||||
# On Windows, there doesn't seem to be a way to cause partial | ||||
# writes. | ||||
buf = [] | ||||
self.assertEqual( | ||||
_readall(stream_receiver, 131072, buf), b'x' * 1048576 | ||||
) | ||||
|
r45657 | def post_child_check(): | ||
|
r45664 | write_result_str = write_result_f.read() | ||
|
r45657 | if pycompat.ispy3: | ||
# On Python 3, we test that the correct number of bytes is | ||||
# claimed to have been written. | ||||
expected_write_result_str = '1048576' | ||||
else: | ||||
# On Python 2, we only check that the large write does not | ||||
# crash. | ||||
expected_write_result_str = 'None' | ||||
self.assertEqual(write_result_str, expected_write_result_str) | ||||
|
r45664 | with tempfile.NamedTemporaryFile('r') as write_result_f: | ||
|
r45657 | self._test( | ||
TEST_LARGE_WRITE_CHILD_SCRIPT.format( | ||||
|
r45664 | stream=stream, write_result_fn=write_result_f.name | ||
|
r45657 | ), | ||
stream, | ||||
rwpair_generator, | ||||
check_output, | ||||
python_args, | ||||
post_child_check=post_child_check, | ||||
) | ||||
|
r45655 | |||
|
r45656 | def test_large_write_stdout_devnull(self): | ||
self._test_large_write('stdout', _devnull) | ||||
|
r45655 | def test_large_write_stdout_pipes(self): | ||
self._test_large_write('stdout', _pipes) | ||||
def test_large_write_stdout_ptys(self): | ||||
self._test_large_write('stdout', _ptys) | ||||
|
r45656 | def test_large_write_stdout_devnull_unbuffered(self): | ||
self._test_large_write('stdout', _devnull, python_args=['-u']) | ||||
|
r45655 | def test_large_write_stdout_pipes_unbuffered(self): | ||
self._test_large_write('stdout', _pipes, python_args=['-u']) | ||||
def test_large_write_stdout_ptys_unbuffered(self): | ||||
self._test_large_write('stdout', _ptys, python_args=['-u']) | ||||
|
r45656 | def test_large_write_stderr_devnull(self): | ||
self._test_large_write('stderr', _devnull) | ||||
|
r45655 | def test_large_write_stderr_pipes(self): | ||
self._test_large_write('stderr', _pipes) | ||||
def test_large_write_stderr_ptys(self): | ||||
self._test_large_write('stderr', _ptys) | ||||
|
r45656 | def test_large_write_stderr_devnull_unbuffered(self): | ||
self._test_large_write('stderr', _devnull, python_args=['-u']) | ||||
|
r45655 | def test_large_write_stderr_pipes_unbuffered(self): | ||
self._test_large_write('stderr', _pipes, python_args=['-u']) | ||||
def test_large_write_stderr_ptys_unbuffered(self): | ||||
self._test_large_write('stderr', _ptys, python_args=['-u']) | ||||
|
r45708 | def _test_broken_pipe(self, stream): | ||
assert stream in ('stdout', 'stderr') | ||||
def check_output(stream_receiver, proc): | ||||
os.close(stream_receiver) | ||||
proc.stdin.write(b'x') | ||||
proc.stdin.close() | ||||
def post_child_check(): | ||||
err = util.pickle.load(err_f) | ||||
self.assertEqual(err.errno, errno.EPIPE) | ||||
self.assertEqual(err.strerror, "Broken pipe") | ||||
with tempfile.NamedTemporaryFile('rb') as err_f: | ||||
self._test( | ||||
TEST_BROKEN_PIPE_CHILD_SCRIPT.format( | ||||
stream=stream, err_fn=err_f.name | ||||
), | ||||
stream, | ||||
_pipes, | ||||
check_output, | ||||
post_child_check=post_child_check, | ||||
stdin_generator=util.nullcontextmanager(subprocess.PIPE), | ||||
) | ||||
def test_broken_pipe_stdout(self): | ||||
self._test_broken_pipe('stdout') | ||||
def test_broken_pipe_stderr(self): | ||||
self._test_broken_pipe('stderr') | ||||
|
r45583 | |||
if __name__ == '__main__': | ||||
import silenttestrunner | ||||
silenttestrunner.main(__name__) | ||||