import sys import os import errno import unittest import time import tempfile import gevent.testing as greentest import gevent from gevent.testing import mock from gevent import subprocess if not hasattr(subprocess, 'mswindows'): # PyPy3, native python subprocess subprocess.mswindows = False PYPY = hasattr(sys, 'pypy_version_info') PY3 = sys.version_info[0] >= 3 if subprocess.mswindows: SETBINARY = 'import msvcrt; msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY);' else: SETBINARY = '' python_universal_newlines = hasattr(sys.stdout, 'newlines') # The stdlib of Python 3 on Windows doesn't properly handle universal newlines # (it produces broken results compared to Python 2) # See gevent.subprocess for more details. python_universal_newlines_broken = PY3 and subprocess.mswindows @greentest.skipWithoutResource('subprocess') class TestPopen(greentest.TestCase): # Use the normal error handling. Make sure that any background greenlets # subprocess spawns propagate errors as expected. error_fatal = False def test_exit(self): popen = subprocess.Popen([sys.executable, '-c', 'import sys; sys.exit(10)']) self.assertEqual(popen.wait(), 10) def test_wait(self): popen = subprocess.Popen([sys.executable, '-c', 'import sys; sys.exit(11)']) gevent.wait([popen]) self.assertEqual(popen.poll(), 11) def test_child_exception(self): with self.assertRaises(OSError) as exc: subprocess.Popen(['*']).wait() self.assertEqual(exc.exception.errno, 2) def test_leak(self): num_before = greentest.get_number_open_files() p = subprocess.Popen([sys.executable, "-c", "print()"], stdout=subprocess.PIPE) p.wait() p.stdout.close() del p num_after = greentest.get_number_open_files() self.assertEqual(num_before, num_after) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_communicate(self): p = subprocess.Popen([sys.executable, "-W", "ignore", "-c", 'import sys,os;' 'sys.stderr.write("pineapple");' 'sys.stdout.write(sys.stdin.read())'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = p.communicate(b"banana") self.assertEqual(stdout, b"banana") if sys.executable.endswith('-dbg'): assert stderr.startswith(b'pineapple') else: self.assertEqual(stderr, b"pineapple") @greentest.skipIf(subprocess.mswindows, "Windows does weird things here") @greentest.skipOnLibuvOnCIOnPyPy("Sometimes segfaults") def test_communicate_universal(self): # Native string all the things. See https://github.com/gevent/gevent/issues/1039 p = subprocess.Popen( [ sys.executable, "-W", "ignore", "-c", 'import sys,os;' 'sys.stderr.write("pineapple\\r\\n\\xff\\xff\\xf2\\xf9\\r\\n");' 'sys.stdout.write(sys.stdin.read())' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True ) (stdout, stderr) = p.communicate('banana\r\n\xff\xff\xf2\xf9\r\n') self.assertIsInstance(stdout, str) self.assertIsInstance(stderr, str) self.assertEqual(stdout, 'banana\n\xff\xff\xf2\xf9\n') self.assertEqual(stderr, 'pineapple\n\xff\xff\xf2\xf9\n') @greentest.skipOnWindows("Windows IO is weird; this doesn't raise") @greentest.skipOnPy2("Only Python 2 decodes") def test_communicate_undecodable(self): # If the subprocess writes non-decodable data, `communicate` raises the # same UnicodeDecodeError that the stdlib does, instead of # printing it to the hub. This only applies to Python 3, because only it # will actually use text mode. # See https://github.com/gevent/gevent/issues/1510 with subprocess.Popen( [ sys.executable, '-W', 'ignore', '-c', "import os, sys; " r'os.write(sys.stdout.fileno(), b"\xff")' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, universal_newlines=True ) as p: with self.assertRaises(UnicodeDecodeError): p.communicate() @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_universal1(self): with subprocess.Popen( [ sys.executable, "-c", 'import sys,os;' + SETBINARY + 'sys.stdout.write("line1\\n");' 'sys.stdout.flush();' 'sys.stdout.write("line2\\r");' 'sys.stdout.flush();' 'sys.stdout.write("line3\\r\\n");' 'sys.stdout.flush();' 'sys.stdout.write("line4\\r");' 'sys.stdout.flush();' 'sys.stdout.write("\\nline5");' 'sys.stdout.flush();' 'sys.stdout.write("\\nline6");' ], stdout=subprocess.PIPE, universal_newlines=1, bufsize=1 ) as p: stdout = p.stdout.read() if python_universal_newlines: # Interpreter with universal newline support if not python_universal_newlines_broken: self.assertEqual(stdout, "line1\nline2\nline3\nline4\nline5\nline6") else: # Note the extra newline after line 3 self.assertEqual(stdout, 'line1\nline2\nline3\n\nline4\n\nline5\nline6') else: # Interpreter without universal newline support self.assertEqual(stdout, "line1\nline2\rline3\r\nline4\r\nline5\nline6") @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_universal2(self): with subprocess.Popen( [ sys.executable, "-c", 'import sys,os;' + SETBINARY + 'sys.stdout.write("line1\\n");' 'sys.stdout.flush();' 'sys.stdout.write("line2\\r");' 'sys.stdout.flush();' 'sys.stdout.write("line3\\r\\n");' 'sys.stdout.flush();' 'sys.stdout.write("line4\\r\\nline5");' 'sys.stdout.flush();' 'sys.stdout.write("\\nline6");' ], stdout=subprocess.PIPE, universal_newlines=1, bufsize=1 ) as p: stdout = p.stdout.read() if python_universal_newlines: # Interpreter with universal newline support if not python_universal_newlines_broken: self.assertEqual(stdout, "line1\nline2\nline3\nline4\nline5\nline6") else: # Note the extra newline after line 3 self.assertEqual(stdout, 'line1\nline2\nline3\n\nline4\n\nline5\nline6') else: # Interpreter without universal newline support self.assertEqual(stdout, "line1\nline2\rline3\r\nline4\r\nline5\nline6") @greentest.skipOnWindows("Uses 'grep' command") def test_nonblock_removed(self): # see issue #134 r, w = os.pipe() stdin = subprocess.FileObject(r) with subprocess.Popen(['grep', 'text'], stdin=stdin) as p: try: # Closing one half of the pipe causes Python 3 on OS X to terminate the # child process; it exits with code 1 and the assert that p.poll is None # fails. Removing the close lets it pass under both Python 3 and 2.7. # If subprocess.Popen._remove_nonblock_flag is changed to a noop, then # the test fails (as expected) even with the close removed #os.close(w) time.sleep(0.1) self.assertEqual(p.poll(), None) finally: if p.poll() is None: p.kill() stdin.close() os.close(w) def test_issue148(self): for _ in range(7): with self.assertRaises(OSError) as exc: with subprocess.Popen('this_name_must_not_exist'): pass self.assertEqual(exc.exception.errno, errno.ENOENT) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_check_output_keyword_error(self): with self.assertRaises(subprocess.CalledProcessError) as exc: # pylint:disable=no-member subprocess.check_output([sys.executable, '-c', 'import sys; sys.exit(44)']) self.assertEqual(exc.exception.returncode, 44) @greentest.skipOnPy3("The default buffer changed in Py3") def test_popen_bufsize(self): # Test that subprocess has unbuffered output by default # (as the vanilla subprocess module) with subprocess.Popen( [sys.executable, '-u', '-c', 'import sys; sys.stdout.write(sys.stdin.readline())'], stdin=subprocess.PIPE, stdout=subprocess.PIPE ) as p: p.stdin.write(b'foobar\n') r = p.stdout.readline() self.assertEqual(r, b'foobar\n') @greentest.ignores_leakcheck @greentest.skipOnWindows("Not sure why?") def test_subprocess_in_native_thread(self): # gevent.subprocess doesn't work from a background # native thread. See #688 from gevent import monkey # must be a native thread; defend against monkey-patching ex = [] Thread = monkey.get_original('threading', 'Thread') def fn(): with self.assertRaises(TypeError) as exc: gevent.subprocess.Popen('echo 123', shell=True) ex.append(exc.exception) thread = Thread(target=fn) thread.start() thread.join() self.assertEqual(len(ex), 1) self.assertTrue(isinstance(ex[0], TypeError), ex) self.assertEqual(ex[0].args[0], 'child watchers are only available on the default loop') @greentest.skipOnLibuvOnPyPyOnWin("hangs") def __test_no_output(self, kwargs, kind): with subprocess.Popen( [sys.executable, '-c', 'pass'], stdout=subprocess.PIPE, **kwargs ) as proc: stdout, stderr = proc.communicate() self.assertIsInstance(stdout, kind) self.assertIsNone(stderr) @greentest.skipOnLibuvOnCIOnPyPy("Sometimes segfaults; " "https://travis-ci.org/gevent/gevent/jobs/327357682") def test_universal_newlines_text_mode_no_output_is_always_str(self): # If the file is in universal_newlines mode, we should always get a str when # there is no output. # https://github.com/gevent/gevent/pull/939 self.__test_no_output({'universal_newlines': True}, str) @greentest.skipIf(sys.version_info[:2] < (3, 6), "Need encoding argument") def test_encoded_text_mode_no_output_is_str(self): # If the file is in universal_newlines mode, we should always get a str when # there is no output. # https://github.com/gevent/gevent/pull/939 self.__test_no_output({'encoding': 'utf-8'}, str) def test_default_mode_no_output_is_always_str(self): # If the file is in default mode, we should always get a str when # there is no output. # https://github.com/gevent/gevent/pull/939 self.__test_no_output({}, bytes) @greentest.skipOnWindows("Testing POSIX fd closing") class TestFDs(unittest.TestCase): @mock.patch('os.closerange') @mock.patch('gevent.subprocess._set_inheritable') @mock.patch('os.close') def test_close_fds_brute_force(self, close, set_inheritable, closerange): keep = ( 4, 5, # Leave a hole # 6, 7, ) subprocess.Popen._close_fds_brute_force(keep, None) closerange.assert_has_calls([ mock.call(3, 4), mock.call(8, subprocess.MAXFD), ]) set_inheritable.assert_has_calls([ mock.call(4, True), mock.call(5, True), ]) close.assert_called_once_with(6) @mock.patch('gevent.subprocess.Popen._close_fds_brute_force') @mock.patch('os.listdir') def test_close_fds_from_path_bad_values(self, listdir, brute_force): listdir.return_value = 'Not an Integer' subprocess.Popen._close_fds_from_path('path', [], 42) brute_force.assert_called_once_with([], 42) @mock.patch('os.listdir') @mock.patch('os.closerange') @mock.patch('gevent.subprocess._set_inheritable') @mock.patch('os.close') def test_close_fds_from_path(self, close, set_inheritable, closerange, listdir): keep = ( 4, 5, # Leave a hole # 6, 7, ) listdir.return_value = ['1', '6', '37'] subprocess.Popen._close_fds_from_path('path', keep, 5) self.assertEqual([], closerange.mock_calls) set_inheritable.assert_has_calls([ mock.call(4, True), mock.call(7, True), ]) close.assert_has_calls([ mock.call(6), mock.call(37), ]) @mock.patch('gevent.subprocess.Popen._close_fds_brute_force') @mock.patch('os.path.isdir') def test_close_fds_no_dir(self, isdir, brute_force): isdir.return_value = False subprocess.Popen._close_fds([], 42) brute_force.assert_called_once_with([], 42) isdir.assert_has_calls([ mock.call('/proc/self/fd'), mock.call('/dev/fd'), ]) @mock.patch('gevent.subprocess.Popen._close_fds_from_path') @mock.patch('gevent.subprocess.Popen._close_fds_brute_force') @mock.patch('os.path.isdir') def test_close_fds_with_dir(self, isdir, brute_force, from_path): isdir.return_value = True subprocess.Popen._close_fds([7], 42) self.assertEqual([], brute_force.mock_calls) from_path.assert_called_once_with('/proc/self/fd', [7], 42) class RunFuncTestCase(greentest.TestCase): # Based on code from python 3.6+ __timeout__ = greentest.LARGE_TIMEOUT @greentest.skipWithoutResource('subprocess') def run_python(self, code, **kwargs): """Run Python code in a subprocess using subprocess.run""" argv = [sys.executable, "-c", code] return subprocess.run(argv, **kwargs) def test_returncode(self): # call() function with sequence argument cp = self.run_python("import sys; sys.exit(47)") self.assertEqual(cp.returncode, 47) with self.assertRaises(subprocess.CalledProcessError): # pylint:disable=no-member cp.check_returncode() def test_check(self): with self.assertRaises(subprocess.CalledProcessError) as c: # pylint:disable=no-member self.run_python("import sys; sys.exit(47)", check=True) self.assertEqual(c.exception.returncode, 47) def test_check_zero(self): # check_returncode shouldn't raise when returncode is zero cp = self.run_python("import sys; sys.exit(0)", check=True) self.assertEqual(cp.returncode, 0) def test_timeout(self): # run() function with timeout argument; we want to test that the child # process gets killed when the timeout expires. If the child isn't # killed, this call will deadlock since subprocess.run waits for the # child. with self.assertRaises(subprocess.TimeoutExpired): self.run_python("while True: pass", timeout=0.0001) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_capture_stdout(self): # capture stdout with zero return code cp = self.run_python("print('BDFL')", stdout=subprocess.PIPE) self.assertIn(b'BDFL', cp.stdout) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_capture_stderr(self): cp = self.run_python("import sys; sys.stderr.write('BDFL')", stderr=subprocess.PIPE) self.assertIn(b'BDFL', cp.stderr) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_check_output_stdin_arg(self): # run() can be called with stdin set to a file with tempfile.TemporaryFile() as tf: tf.write(b'pear') tf.seek(0) cp = self.run_python( "import sys; sys.stdout.write(sys.stdin.read().upper())", stdin=tf, stdout=subprocess.PIPE) self.assertIn(b'PEAR', cp.stdout) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_check_output_input_arg(self): # check_output() can be called with input set to a string cp = self.run_python( "import sys; sys.stdout.write(sys.stdin.read().upper())", input=b'pear', stdout=subprocess.PIPE) self.assertIn(b'PEAR', cp.stdout) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_check_output_stdin_with_input_arg(self): # run() refuses to accept 'stdin' with 'input' with tempfile.TemporaryFile() as tf: tf.write(b'pear') tf.seek(0) with self.assertRaises(ValueError, msg="Expected ValueError when stdin and input args supplied.") as c: self.run_python("print('will not be run')", stdin=tf, input=b'hare') self.assertIn('stdin', c.exception.args[0]) self.assertIn('input', c.exception.args[0]) @greentest.skipOnLibuvOnPyPyOnWin("hangs") def test_check_output_timeout(self): with self.assertRaises(subprocess.TimeoutExpired) as c: self.run_python( ( "import sys, time\n" "sys.stdout.write('BDFL')\n" "sys.stdout.flush()\n" "time.sleep(3600)" ), # Some heavily loaded buildbots (sparc Debian 3.x) require # this much time to start and print. timeout=3, stdout=subprocess.PIPE) self.assertEqual(c.exception.output, b'BDFL') # output is aliased to stdout self.assertEqual(c.exception.stdout, b'BDFL') def test_run_kwargs(self): newenv = os.environ.copy() newenv["FRUIT"] = "banana" cp = self.run_python(('import sys, os;' 'sys.exit(33 if os.getenv("FRUIT")=="banana" else 31)'), env=newenv) self.assertEqual(cp.returncode, 33) # This test _might_ wind up a bit fragile on loaded build+test machines # as it depends on the timing with wide enough margins for normal situations # but does assert that it happened "soon enough" to believe the right thing # happened. @greentest.skipOnWindows("requires posix like 'sleep' shell command") def test_run_with_shell_timeout_and_capture_output(self): #Output capturing after a timeout mustn't hang forever on open filehandles with self.runs_in_given_time(0.1): with self.assertRaises(subprocess.TimeoutExpired): subprocess.run('sleep 3', shell=True, timeout=0.1, capture_output=True) # New session unspecified. if __name__ == '__main__': greentest.main()