| #!/usr/bin/env vpython3 |
| # Copyright 2013 The LUCI Authors. All rights reserved. |
| # Use of this source code is governed under the Apache License, Version 2.0 |
| # that can be found in the LICENSE file. |
| |
| from __future__ import print_function |
| |
| import ctypes |
| import errno |
| import itertools |
| import os |
| import platform |
| import signal |
| import sys |
| import tempfile |
| import textwrap |
| import time |
| import unittest |
| |
| from nose2.tools import params |
| |
| # Mutates sys.path. |
| import test_env |
| |
| from utils import subprocess42 |
| |
| # Disable pre-set unbuffered output to not interfere with the testing being done |
| # here. Otherwise everything would test with unbuffered; which is fine but |
| # that's not what we specifically want to test here. |
| ENV = os.environ.copy() |
| ENV.pop("PYTHONUNBUFFERED", None) |
| |
| SCRIPT_OUT = ( |
| "import signal, sys, time;\n" |
| "l = [];\n" |
| "def handler(signum, _):\n" |
| " l.append(signum);\n" |
| " sys.stdout.write('got signal %%d\\n' %% signum);\n" |
| " sys.stdout.flush();\n" |
| "signal.signal(%s, handler);\n" |
| "sys.stdout.write('hi\\n');\n" |
| "sys.stdout.flush();\n" |
| "while not l:\n" |
| " try:\n" |
| " time.sleep(0.01);\n" |
| " except IOError:\n" |
| " sys.stdout.write('ioerror\\n');\n" |
| " sys.stdout.flush();\n" |
| "sys.stdout.write('bye\\n');\n" |
| "sys.stdout.flush();\n" |
| ) % ("signal.SIGBREAK" if sys.platform == "win32" else "signal.SIGTERM") |
| |
| SCRIPT_ERR = ( |
| "import signal, sys, time;\n" |
| "l = [];\n" |
| "def handler(signum, _):\n" |
| " l.append(signum);\n" |
| " sys.stderr.write('got signal %%d\\n' %% signum);\n" |
| " sys.stderr.flush();\n" |
| "signal.signal(%s, handler);\n" |
| "sys.stderr.write('hi\\n');\n" |
| "sys.stderr.flush();\n" |
| "while not l:\n" |
| " try:\n" |
| " time.sleep(0.01);\n" |
| " except IOError:\n" |
| " sys.stderr.write('ioerror\\n');\n" |
| " sys.stderr.flush();\n" |
| "sys.stderr.write('bye\\n');\n" |
| "sys.stderr.flush();\n" |
| ) % ("signal.SIGBREAK" if sys.platform == "win32" else "signal.SIGTERM") |
| |
| OUTPUT_SCRIPT = rb""" |
| import os |
| import re |
| import sys |
| import time |
| |
| def main(): |
| try: |
| for command in sys.argv[1:]: |
| if re.match(r'^[0-9\.]+$', command): |
| time.sleep(float(command)) |
| continue |
| |
| if command.startswith('out_'): |
| pipe, other = sys.stdout, sys.stderr |
| elif command.startswith('err_'): |
| pipe, other = sys.stderr, sys.stdout |
| else: |
| return 1 |
| |
| command = command[4:] |
| if command == 'print': |
| pipe.write('printing') |
| elif command == 'sleeping': |
| pipe.write('Sleeping.\n') |
| elif command == 'slept': |
| pipe.write('Slept.\n') |
| elif command == 'lf': |
| pipe.write('\n') |
| elif command == 'flush': |
| pipe.flush() |
| elif command == 'leak': |
| pid = os.fork() |
| if pid > 0: |
| return 0 |
| |
| other.write("leaked child is %s %s\n" % (os.getpid(), os.getpgid(0))) |
| other.write("sleeping\n") |
| time.sleep(30) |
| other.write("woke up\n") |
| return 1 |
| else: |
| return 1 |
| return 0 |
| except OSError: |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |
| """ |
| |
| |
| def to_native_eol(string): |
| if string is None: |
| return string |
| if sys.platform == "win32": |
| return string.replace(b"\n", b"\r\n") |
| return string |
| |
| |
| def get_output_sleep_proc(flush, unbuffered, sleep_duration): |
| """Returns process with universal_newlines=True that prints to stdout before |
| after a sleep. |
| |
| It also optionally sys.stdout.flush() before the sleep and optionally enable |
| unbuffered output in python. |
| """ |
| command = [ |
| "import sys,time", |
| "print('A')", |
| ] |
| if flush: |
| # Sadly, this doesn't work otherwise in some combination. |
| command.append("sys.stdout.flush()") |
| command.extend( |
| ( |
| "time.sleep(%s)" % sleep_duration, |
| "print('B')", |
| ) |
| ) |
| cmd = [sys.executable, "-c", ";".join(command)] |
| if unbuffered: |
| cmd.append("-u") |
| return subprocess42.Popen( |
| cmd, env=ENV, stdout=subprocess42.PIPE, universal_newlines=True |
| ) |
| |
| |
| def get_output_sleep_proc_err(sleep_duration): |
| """Returns process with universal_newlines=True that prints to stderr before |
| and after a sleep. |
| """ |
| command = [ |
| "import sys,time", |
| "sys.stderr.write('A\\n')", |
| ] |
| command.extend( |
| ( |
| "time.sleep(%s)" % sleep_duration, |
| "sys.stderr.write('B\\n')", |
| ) |
| ) |
| cmd = [sys.executable, "-c", ";".join(command)] |
| return subprocess42.Popen( |
| cmd, env=ENV, stderr=subprocess42.PIPE, universal_newlines=True |
| ) |
| |
| |
| class Subprocess42Test(unittest.TestCase): |
| def setUp(self): |
| self._output_script = None |
| super(Subprocess42Test, self).setUp() |
| |
| def tearDown(self): |
| try: |
| if self._output_script: |
| os.remove(self._output_script) |
| finally: |
| super(Subprocess42Test, self).tearDown() |
| |
| @property |
| def output_script(self): |
| if not self._output_script: |
| handle, self._output_script = tempfile.mkstemp( |
| prefix="subprocess42", suffix=".py" |
| ) |
| os.write(handle, OUTPUT_SCRIPT) |
| os.close(handle) |
| return self._output_script |
| |
| def test_communicate_timeout(self): |
| timedout = 1 if sys.platform == "win32" else -9 |
| # Format is: |
| # ( (cmd, stderr_pipe, timeout), (stdout, stderr, returncode) ), ... |
| # See OUTPUT script for the meaning of the commands. |
| test_data = [ |
| # 0 means no timeout, like None. |
| ( |
| (["out_sleeping", "0.001", "out_slept", "err_print"], None, 0), |
| (b"Sleeping.\nSlept.\n", None, 0), |
| ), |
| ( |
| (["err_print"], subprocess42.STDOUT, 0), |
| (b"printing", None, 0), |
| ), |
| ( |
| (["err_print"], subprocess42.PIPE, 0), |
| (b"", b"printing", 0), |
| ), |
| # On a loaded system, this can be tight. |
| ( |
| (["out_sleeping", "out_flush", "60", "out_slept"], None, 1), |
| (b"Sleeping.\n", None, timedout), |
| ), |
| ( |
| ( |
| # Note that err_flush is necessary on Windows but not on the |
| # other OSes. This means the likelihood of missing stderr output |
| # from a killed child process on Windows is much higher than on |
| # other OSes. |
| [ |
| "out_sleeping", |
| "out_flush", |
| "err_print", |
| "err_flush", |
| "60", |
| "out_slept", |
| ], |
| subprocess42.PIPE, |
| 1, |
| ), |
| (b"Sleeping.\n", b"printing", timedout), |
| ), |
| ( |
| (["out_sleeping", "0.001", "out_slept"], None, 60), |
| (b"Sleeping.\nSlept.\n", None, 0), |
| ), |
| ( |
| ([], None, 60), |
| (b"", None, 0), |
| ), |
| ( |
| ([], subprocess42.PIPE, 60), |
| (b"", b"", 0), |
| ), |
| ] |
| for i, ((args, errpipe, timeout), expected) in enumerate(test_data): |
| proc = subprocess42.Popen( |
| [sys.executable, self.output_script] + args, |
| env=ENV, |
| stdout=subprocess42.PIPE, |
| stderr=errpipe, |
| ) |
| try: |
| stdout, stderr = proc.communicate(timeout=timeout) |
| code = proc.returncode |
| except subprocess42.TimeoutExpired as e: |
| stdout = e.output |
| stderr = e.stderr |
| self.assertTrue(proc.kill()) |
| code = proc.wait() |
| finally: |
| duration = proc.duration() |
| expected_duration = 0.0001 if not timeout or timeout == 60 else timeout |
| self.assertTrue(duration >= expected_duration, (i, expected_duration)) |
| self.assertEqual( |
| (i, stdout, stderr, code), |
| ( |
| i, |
| to_native_eol(expected[0]), |
| to_native_eol(expected[1]), |
| expected[2], |
| ), |
| ) |
| |
| # Try again with universal_newlines=True. |
| proc = subprocess42.Popen( |
| [sys.executable, self.output_script] + args, |
| env=ENV, |
| stdout=subprocess42.PIPE, |
| stderr=errpipe, |
| universal_newlines=True, |
| ) |
| try: |
| stdout, stderr = proc.communicate(timeout=timeout) |
| code = proc.returncode |
| except subprocess42.TimeoutExpired as e: |
| # With communicate() in the native subprocess.py, output/stderr becomes |
| # bytes even if universal_newlines = True in Python3. |
| # They are str on Windows because the communicate() in subprocess42.py |
| # is used. |
| stdout = e.output |
| stderr = e.stderr |
| if sys.platform != "win32": |
| stdout = stdout.decode() if stdout else None |
| stderr = stderr.decode() if stderr else None |
| self.assertTrue(proc.kill()) |
| code = proc.wait() |
| finally: |
| duration = proc.duration() |
| self.assertTrue(duration >= expected_duration, (i, expected_duration)) |
| self.assertEqual( |
| ( |
| i, |
| None if stdout is None else stdout.encode(), |
| None if stderr is None else stderr.encode(), |
| code, |
| ), |
| (i,) + expected, |
| ) |
| |
| def test_communicate_input(self): |
| cmd = [ |
| sys.executable, |
| "-u", |
| "-c", |
| "import sys; sys.stdout.write(sys.stdin.read(5))", |
| ] |
| proc = subprocess42.Popen( |
| cmd, stdin=subprocess42.PIPE, stdout=subprocess42.PIPE |
| ) |
| out, err = proc.communicate(input=b"12345") |
| self.assertEqual(b"12345", out) |
| self.assertEqual(None, err) |
| |
| def test_communicate_input_timeout(self): |
| cmd = [sys.executable, "-u", "-c", "import time; time.sleep(60)"] |
| proc = subprocess42.Popen(cmd, stdin=subprocess42.PIPE) |
| try: |
| proc.communicate(input=b"12345", timeout=0.5) |
| self.fail() |
| except subprocess42.TimeoutExpired as e: |
| self.assertEqual(None, e.output) |
| self.assertEqual(None, e.stderr) |
| self.assertTrue(proc.kill()) |
| proc.wait() |
| self.assertLessEqual(0.5, proc.duration()) |
| |
| def test_communicate_input_stdout_timeout(self): |
| cmd = [ |
| sys.executable, |
| "-u", |
| "-c", |
| """ |
| import sys, time |
| sys.stdout.write(sys.stdin.read(5)) |
| sys.stdout.flush() |
| time.sleep(60) |
| """, |
| ] |
| proc = subprocess42.Popen( |
| cmd, stdin=subprocess42.PIPE, stdout=subprocess42.PIPE |
| ) |
| try: |
| proc.communicate(input=b"12345", timeout=2) |
| self.fail() |
| except subprocess42.TimeoutExpired as e: |
| self.assertEqual(b"12345", e.output) |
| self.assertEqual(None, e.stderr) |
| self.assertTrue(proc.kill()) |
| proc.wait() |
| self.assertLessEqual(0.5, proc.duration()) |
| |
| def test_communicate_timeout_no_pipe(self): |
| # In this case, it's effectively a wait() call. |
| cmd = [sys.executable, "-u", "-c", "import time; time.sleep(60)"] |
| proc = subprocess42.Popen(cmd) |
| try: |
| proc.communicate(timeout=0.5) |
| self.fail() |
| except subprocess42.TimeoutExpired as e: |
| self.assertEqual(None, e.output) |
| self.assertEqual(None, e.stderr) |
| self.assertTrue(proc.kill()) |
| proc.wait() |
| self.assertLessEqual(0.5, proc.duration()) |
| |
| def _test_lower_priority(self, lower_priority): |
| if sys.platform == "win32": |
| cmd = [ |
| sys.executable, |
| "-u", |
| "-c", |
| "import ctypes,sys; v=ctypes.windll.kernel32.GetPriorityClass(-1);" |
| "sys.stdout.write(hex(v))", |
| ] |
| else: |
| cmd = [ |
| sys.executable, |
| "-u", |
| "-c", |
| "import os,sys;sys.stdout.write(str(os.nice(0)))", |
| ] |
| proc = subprocess42.Popen( |
| cmd, stdout=subprocess42.PIPE, lower_priority=lower_priority |
| ) |
| out, err = proc.communicate() |
| self.assertEqual(None, err) |
| return out |
| |
| @unittest.skipIf(sys.platform == "win32", "crbug.com/1148174") |
| def test_lower_priority(self): |
| out = self._test_lower_priority(True) |
| if sys.platform == "win32": |
| # See |
| # https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass |
| BELOW_NORMAL_PRIORITY_CLASS = 0x4000 |
| self.assertEqual(hex(BELOW_NORMAL_PRIORITY_CLASS), out) |
| else: |
| self.assertEqual(str(os.nice(0) + 1).encode(), out) |
| |
| def test_lower_priority_False(self): |
| out = self._test_lower_priority(False) |
| if sys.platform == "win32": |
| # Should be NORMAL_PRIORITY_CLASS. |
| p = ctypes.windll.kernel32.GetPriorityClass(-1) |
| self.assertEqual(hex(p).encode(), out) |
| else: |
| self.assertEqual(str(os.nice(0)).encode(), out) |
| |
| @staticmethod |
| def _cmd_print_good(): |
| # Used in test_containment_auto and test_containment_auto_limit_process. |
| return [ |
| sys.executable, |
| "-u", |
| "-c", |
| "import subprocess,sys; " |
| 'subprocess.call([sys.executable, "-c", "print(\\"good\\")"])', |
| ] |
| |
| def test_containment_none(self): |
| # Minimal test case. Starts two processes. |
| cmd = self._cmd_print_good() |
| containment = subprocess42.Containment( |
| containment_type=subprocess42.Containment.NONE |
| ) |
| self.assertEqual(0, subprocess42.check_call(cmd, containment=containment)) |
| |
| def test_containment_auto(self): |
| # Minimal test case. Starts two processes. |
| cmd = self._cmd_print_good() |
| containment = subprocess42.Containment( |
| containment_type=subprocess42.Containment.AUTO, |
| limit_processes=2, |
| limit_total_committed_memory=1024 * 1024 * 1024, |
| ) |
| self.assertEqual(0, subprocess42.check_call(cmd, containment=containment)) |
| |
| def test_containment_auto_limit_process(self): |
| # Process creates a children process. It should fail, throwing not enough |
| # quota. |
| cmd = self._cmd_print_good() |
| containment = subprocess42.Containment( |
| containment_type=subprocess42.Containment.JOB_OBJECT, limit_processes=1 |
| ) |
| start = lambda: subprocess42.Popen( |
| cmd, |
| stdout=subprocess42.PIPE, |
| stderr=subprocess42.PIPE, |
| containment=containment, |
| ) |
| |
| if sys.platform == "win32": |
| p = start() |
| out, err = p.communicate() |
| self.assertEqual(1, p.returncode) |
| self.assertEqual(b"", out) |
| self.assertIn(b"WinError", err) |
| # Value for ERROR_NOT_ENOUGH_QUOTA. See |
| # https://docs.microsoft.com/windows/desktop/debug/system-error-codes--1700-3999- |
| self.assertIn(b"1816", err) |
| else: |
| # JOB_OBJECT is not usable on non-Windows. |
| with self.assertRaises(NotImplementedError): |
| start() |
| |
| def test_containment_auto_kill(self): |
| # Test process killing. |
| cmd = [ |
| sys.executable, |
| "-u", |
| "-c", |
| 'import sys,time; print("hi");time.sleep(60)', |
| ] |
| containment = subprocess42.Containment( |
| containment_type=subprocess42.Containment.AUTO, |
| limit_processes=1, |
| limit_total_committed_memory=1024 * 1024 * 1024, |
| ) |
| p = subprocess42.Popen( |
| cmd, stdout=subprocess42.PIPE, containment=containment |
| ) |
| itr = p.yield_any_line() |
| self.assertEqual(("stdout", b"hi"), next(itr)) |
| p.kill() |
| p.wait() |
| if sys.platform != "win32": |
| # signal.SIGKILL is not defined on Windows. Validate our assumption here. |
| self.assertEqual(9, signal.SIGKILL) |
| if sys.platform == "win32": |
| # p.returncode is unsigned in python3 on windows |
| self.assertEqual(4294967287, p.returncode) |
| else: |
| self.assertEqual(-9, p.returncode) |
| |
| @unittest.skipIf(sys.platform == "win32", "pgid test") |
| def test_kill_background(self): |
| # Test process group killing. |
| |
| # Leaking a pipe through to the grandchild process is the only way to be |
| # sure that we have a way to detect that this grandchild process is still |
| # running or not. When all handles to the `w` end of the pipe have been |
| # dropped, the `r` end will unblock. |
| r, w = os.pipe() |
| |
| # Use fcntl instead of os.set_blocking because of python2 |
| import fcntl |
| |
| fcntl.fcntl(r, fcntl.F_SETFL, fcntl.fcntl(r, fcntl.F_GETFL) | os.O_NONBLOCK) |
| |
| p = subprocess42.Popen( |
| [sys.executable, self.output_script, "out_leak"], stdout=w, detached=True |
| ) |
| os.close(w) # close so we don't think that a child is hanging onto it. |
| |
| self.assertEqual(p.wait(), 0) # our immediate child has exited! |
| |
| with self.assertRaises(OSError): |
| # oops, something still has a handle to this pipe! it's the grandchild! |
| os.read(r, 1) |
| |
| # kill the group! That'll show 'em (unless the child actually daemonized, in |
| # which case we're hosed). |
| p.kill() |
| |
| # sleepy-loop until the pipe is closed, should take O(ms) but we generously |
| # wait up to 5s. The sub-child will wait 30s and should outlive this loop if |
| # somehow it survived the kill. |
| now = time.time() |
| while True: |
| try: |
| self.assertEqual(os.read(r, 1), b"") # i.e. EOF |
| return |
| except OSError as ex: |
| if ex.errno != errno.EWOULDBLOCK: |
| raise |
| time.sleep(0.1) |
| if time.time() - now > 5: |
| raise Exception("pipe not unblocked after 5s, bailing") |
| |
| @staticmethod |
| def _cmd_large_memory(): |
| # Used in test_large_memory and test_containment_auto_limit_memory. |
| return [ |
| sys.executable, |
| "-u", |
| "-c", |
| 'list(range(50*1024*1024)); print("hi")', |
| ] |
| |
| def test_large_memory(self): |
| # Just assert the process works normally. |
| cmd = self._cmd_large_memory() |
| self.assertEqual(b"hi", subprocess42.check_output(cmd).strip()) |
| |
| def test_containment_auto_limit_memory(self): |
| # Process allocates a lot of memory. It should fail due to quota. |
| cmd = self._cmd_large_memory() |
| containment = subprocess42.Containment( |
| containment_type=subprocess42.Containment.JOB_OBJECT, |
| # 20 MiB. |
| limit_total_committed_memory=20 * 1024 * 1024, |
| ) |
| start = lambda: subprocess42.Popen( |
| cmd, |
| stdout=subprocess42.PIPE, |
| stderr=subprocess42.PIPE, |
| containment=containment, |
| ) |
| |
| if sys.platform == "win32": |
| p = start() |
| out, err = p.communicate() |
| self.assertEqual(1, p.returncode) |
| self.assertEqual(b"", out) |
| self.assertIn(b"MemoryError", err) |
| else: |
| # JOB_OBJECT is not usable on non-Windows. |
| with self.assertRaises(NotImplementedError): |
| start() |
| |
| def test_call(self): |
| cmd = [sys.executable, "-u", "-c", "import sys; sys.exit(0)"] |
| self.assertEqual(0, subprocess42.call(cmd)) |
| |
| cmd = [sys.executable, "-u", "-c", "import sys; sys.exit(1)"] |
| self.assertEqual(1, subprocess42.call(cmd)) |
| |
| def test_check_call(self): |
| cmd = [sys.executable, "-u", "-c", "import sys; sys.exit(0)"] |
| self.assertEqual(0, subprocess42.check_call(cmd)) |
| |
| cmd = [sys.executable, "-u", "-c", "import sys; sys.exit(1)"] |
| try: |
| self.assertEqual(1, subprocess42.check_call(cmd)) |
| self.fail() |
| except subprocess42.CalledProcessError as e: |
| self.assertEqual(None, e.output) |
| |
| def test_check_output(self): |
| cmd = [sys.executable, "-u", "-c", "print('.')"] |
| self.assertEqual( |
| ".\n", subprocess42.check_output(cmd, universal_newlines=True) |
| ) |
| |
| cmd = [sys.executable, "-u", "-c", "import sys; print('.'); sys.exit(1)"] |
| try: |
| subprocess42.check_output(cmd, universal_newlines=True) |
| self.fail() |
| except subprocess42.CalledProcessError as e: |
| self.assertEqual(".\n", e.output) |
| |
| def test_recv_any(self): |
| # Test all pipe direction and output scenarios. |
| combinations = [ |
| { |
| "cmd": ["out_print", "err_print"], |
| "stdout": None, |
| "stderr": None, |
| "expected": {}, |
| "universal_newlines": False, |
| }, |
| { |
| "cmd": ["out_print", "err_print"], |
| "stdout": None, |
| "stderr": subprocess42.STDOUT, |
| "universal_newlines": False, |
| "expected": {}, |
| }, |
| { |
| "cmd": ["out_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.PIPE, |
| "universal_newlines": False, |
| "expected": {"stdout": b"printing"}, |
| }, |
| { |
| "cmd": ["out_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": None, |
| "universal_newlines": False, |
| "expected": {"stdout": b"printing"}, |
| }, |
| { |
| "cmd": ["out_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.STDOUT, |
| "universal_newlines": False, |
| "expected": {"stdout": b"printing"}, |
| }, |
| { |
| "cmd": ["err_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.PIPE, |
| "universal_newlines": False, |
| "expected": {"stderr": b"printing"}, |
| }, |
| { |
| "cmd": ["err_print"], |
| "stdout": None, |
| "stderr": subprocess42.PIPE, |
| "universal_newlines": False, |
| "expected": {"stderr": b"printing"}, |
| }, |
| { |
| "cmd": ["err_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.STDOUT, |
| "universal_newlines": False, |
| "expected": {"stdout": b"printing"}, |
| }, |
| { |
| "cmd": ["out_print", "err_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.PIPE, |
| "universal_newlines": False, |
| "expected": {"stderr": b"printing", "stdout": b"printing"}, |
| }, |
| { |
| "cmd": ["out_print", "err_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.STDOUT, |
| "universal_newlines": False, |
| "expected": {"stdout": b"printingprinting"}, |
| }, |
| { |
| "cmd": ["out_print", "err_print"], |
| "stdout": subprocess42.PIPE, |
| "stderr": subprocess42.PIPE, |
| "universal_newlines": True, |
| "expected": {"stderr": "printing", "stdout": "printing"}, |
| }, |
| ] |
| for i, testcase in enumerate(combinations): |
| default = "" if testcase["universal_newlines"] else b"" |
| cmd = [sys.executable, self.output_script] + testcase["cmd"] |
| p = subprocess42.Popen( |
| cmd, |
| env=ENV, |
| stdout=testcase["stdout"], |
| stderr=testcase["stderr"], |
| universal_newlines=testcase["universal_newlines"], |
| ) |
| actual = {} |
| while p.poll() is None: |
| pipe, data = p.recv_any() |
| if data: |
| actual.setdefault(pipe, default) |
| actual[pipe] += data |
| |
| # The process exited, read any remaining data in the pipes. |
| while True: |
| pipe, data = p.recv_any() |
| if pipe is None: |
| break |
| actual.setdefault(pipe, default) |
| actual[pipe] += data |
| self.assertEqual( |
| testcase["expected"], |
| actual, |
| (i, testcase["cmd"], testcase["expected"], actual), |
| ) |
| self.assertEqual((None, None), p.recv_any()) |
| self.assertEqual(0, p.returncode) |
| |
| def test_recv_any_different_buffering(self): |
| # Specifically test all buffering scenarios. |
| for flush, unbuffered in itertools.product([True, False], [True, False]): |
| actual = "" |
| proc = get_output_sleep_proc(flush, unbuffered, 0.5) |
| while True: |
| p, data = proc.recv_any() |
| if not p: |
| break |
| self.assertEqual("stdout", p) |
| self.assertTrue(data, (p, data)) |
| actual += data |
| |
| self.assertEqual("A\nB\n", actual) |
| # Contrary to yield_any() or recv_any(0), wait() needs to be used here. |
| proc.wait() |
| self.assertEqual(0, proc.returncode) |
| |
| def test_recv_any_timeout_0(self): |
| self._test_recv_any_timeout(False, False) |
| self._test_recv_any_timeout(False, True) |
| self._test_recv_any_timeout(True, False) |
| self._test_recv_any_timeout(True, True) |
| |
| def _test_recv_any_timeout(self, flush, unbuffered): |
| # rec_any() is expected to timeout and return None with no data pending at |
| # least once, due to the sleep of 'duration' and the use of timeout=0. |
| for duration in (0.05, 0.1, 0.5, 2): |
| got_none = False |
| actual = "" |
| try: |
| proc = get_output_sleep_proc(flush, unbuffered, duration) |
| try: |
| while True: |
| p, data = proc.recv_any(timeout=0) |
| if p: |
| self.assertEqual("stdout", p) |
| self.assertTrue(data, (p, data)) |
| actual += data |
| continue |
| if proc.poll() is None: |
| got_none = True |
| continue |
| break |
| |
| self.assertEqual("A\nB\n", actual) |
| self.assertEqual(0, proc.returncode) |
| self.assertEqual(True, got_none) |
| break |
| finally: |
| proc.kill() |
| proc.wait() |
| except AssertionError: |
| if duration != 2: |
| print("Sleeping rocks. Trying slower.") |
| continue |
| raise |
| |
| def test_yield_any_no_timeout(self): |
| for duration in (0.05, 0.1, 0.5, 2): |
| try: |
| proc = get_output_sleep_proc(True, True, duration) |
| try: |
| expected = [ |
| "A\n", |
| "B\n", |
| ] |
| for p, data in proc.yield_any(): |
| self.assertEqual("stdout", p) |
| self.assertEqual(expected.pop(0), data) |
| self.assertEqual(0, proc.returncode) |
| self.assertEqual([], expected) |
| break |
| finally: |
| proc.kill() |
| proc.wait() |
| except AssertionError: |
| if duration != 2: |
| print("Sleeping rocks. Trying slower.") |
| continue |
| raise |
| |
| def test_yield_any_timeout_0(self): |
| # rec_any() is expected to timeout and return None with no data pending at |
| # least once, due to the sleep of 'duration' and the use of timeout=0. |
| for duration in (0.05, 0.1, 0.5, 2): |
| try: |
| proc = get_output_sleep_proc(True, True, duration) |
| try: |
| expected = [ |
| "A\n", |
| "B\n", |
| ] |
| got_none = False |
| for p, data in proc.yield_any(timeout=0): |
| if not p: |
| got_none = True |
| continue |
| self.assertEqual("stdout", p) |
| self.assertEqual(expected.pop(0), data) |
| self.assertEqual(0, proc.returncode) |
| self.assertEqual([], expected) |
| self.assertEqual(True, got_none) |
| break |
| finally: |
| proc.kill() |
| proc.wait() |
| except AssertionError: |
| if duration != 2: |
| print("Sleeping rocks. Trying slower.") |
| continue |
| raise |
| |
| def test_yield_any_timeout_0_called(self): |
| # rec_any() is expected to timeout and return None with no data pending at |
| # least once, due to the sleep of 'duration' and the use of timeout=0. |
| for duration in (0.05, 0.1, 0.5, 2): |
| got_none = False |
| expected = ["A\n", "B\n"] |
| called = [] |
| |
| def timeout(): |
| # pylint: disable=cell-var-from-loop |
| called.append(0) |
| return 0 |
| |
| try: |
| proc = get_output_sleep_proc(True, True, duration) |
| try: |
| for p, data in proc.yield_any(timeout=timeout): |
| if not p: |
| got_none = True |
| continue |
| self.assertEqual("stdout", p) |
| self.assertEqual(expected.pop(0), data) |
| self.assertEqual(0, proc.returncode) |
| self.assertEqual([], expected) |
| self.assertEqual(True, got_none) |
| self.assertTrue(called) |
| break |
| finally: |
| proc.kill() |
| proc.wait() |
| except AssertionError: |
| if duration != 2: |
| print("Sleeping rocks. Trying slower.") |
| continue |
| raise |
| |
| def test_yield_any_returncode(self): |
| proc = subprocess42.Popen( |
| [sys.executable, "-c", 'import sys;sys.stdout.write("yo");sys.exit(1)'], |
| stdout=subprocess42.PIPE, |
| ) |
| for p, d in proc.yield_any(): |
| self.assertEqual("stdout", p) |
| self.assertEqual(b"yo", d) |
| # There was a bug where the second call to wait() would overwrite |
| # proc.returncode with 0 when timeout is not None. |
| self.assertEqual(1, proc.wait()) |
| self.assertEqual(1, proc.wait(timeout=0)) |
| self.assertEqual(1, proc.poll()) |
| self.assertEqual(1, proc.returncode) |
| # On Windows, the clock resolution is 15ms so Popen.duration() will likely |
| # be 0. |
| self.assertLessEqual(0, proc.duration()) |
| |
| def _wait_for_hi(self, proc, err): |
| actual = b"" |
| while True: |
| if err: |
| data = proc.recv_err(timeout=5) |
| else: |
| data = proc.recv_out(timeout=5) |
| if not data: |
| self.fail("%r" % actual) |
| self.assertTrue(data) |
| actual += data |
| if actual in (b"hi\n", b"hi\r\n"): |
| break |
| |
| def _proc(self, err, **kwargs): |
| # Do not use the -u flag here, we want to test when it is buffered by |
| # default. See reference above about PYTHONUNBUFFERED. |
| # That's why the two scripts uses .flush(). Sadly, the flush() call is |
| # needed on Windows even for sys.stderr (!) |
| cmd = [sys.executable, "-c", SCRIPT_ERR if err else SCRIPT_OUT] |
| # TODO(maruel): Make universal_newlines=True work and not hang. |
| if err: |
| kwargs["stderr"] = subprocess42.PIPE |
| else: |
| kwargs["stdout"] = subprocess42.PIPE |
| return subprocess42.Popen(cmd, **kwargs) |
| |
| def test_detached(self): |
| self._test_detached(False) |
| self._test_detached(True) |
| |
| def _test_detached(self, err): |
| is_win = sys.platform == "win32" |
| key = "stderr" if err else "stdout" |
| proc = self._proc(err, detached=True) |
| try: |
| self._wait_for_hi(proc, err) |
| proc.terminate() |
| if is_win: |
| # What happens on Windows is that the process is immediately killed |
| # after handling SIGBREAK. |
| self.assertEqual(0, proc.wait()) |
| # Windows... |
| self.assertIn( |
| proc.recv_any(), |
| ( |
| (key, b"got signal 21\r\nioerror\r\nbye\r\n"), |
| (key, b"got signal 21\nioerror\nbye\n"), |
| (key, b"got signal 21\r\nbye\r\n"), |
| (key, b"got signal 21\nbye\n"), |
| ), |
| ) |
| else: |
| self.assertEqual(0, proc.wait()) |
| self.assertEqual((key, b"got signal 15\nbye\n"), proc.recv_any()) |
| finally: |
| # In case the test fails. |
| proc.kill() |
| proc.wait() |
| |
| def test_attached(self): |
| self._test_attached(False) |
| self._test_attached(True) |
| |
| def _test_attached(self, err): |
| is_win = sys.platform == "win32" |
| key = "stderr" if err else "stdout" |
| proc = self._proc(err, detached=False) |
| try: |
| self._wait_for_hi(proc, err) |
| proc.terminate() |
| if is_win: |
| # If attached, it's hard killed. |
| self.assertEqual(1, proc.wait()) |
| self.assertEqual((None, None), proc.recv_any()) |
| else: |
| self.assertEqual(0, proc.wait()) |
| self.assertEqual((key, b"got signal 15\nbye\n"), proc.recv_any()) |
| finally: |
| # In case the test fails. |
| proc.kill() |
| proc.wait() |
| |
| def test_split(self): |
| data = [ |
| ("stdout", b"o1\no2\no3\n"), |
| ("stderr", b"e1\ne2\ne3\n"), |
| ("stdout", b"\n\n"), |
| ("stdout", b"\n"), |
| ("stdout", b"o4\no5"), |
| ("stdout", b"_sameline\npart1 of one line "), |
| ("stderr", b"err inserted between two parts of stdout\n"), |
| ("stdout", b"part2 of one line\n"), |
| ("stdout", b"incomplete last stdout"), |
| ("stderr", b"incomplete last stderr"), |
| ] |
| expected = [ |
| ("stdout", b"o1"), |
| ("stdout", b"o2"), |
| ("stdout", b"o3"), |
| ("stderr", b"e1"), |
| ("stderr", b"e2"), |
| ("stderr", b"e3"), |
| ("stdout", b""), |
| ("stdout", b""), |
| ("stdout", b""), |
| ("stdout", b"o4"), |
| ("stdout", b"o5_sameline"), |
| ("stderr", b"err inserted between two parts of stdout"), |
| ("stdout", b"part1 of one line part2 of one line"), |
| ("stderr", b"incomplete last stderr"), |
| ("stdout", b"incomplete last stdout"), |
| ] |
| if sys.platform == "win32": |
| data = [(d[0], d[1].replace(b"\n", b"\r\n")) for d in data] |
| |
| # With universal_newlines=False |
| self.assertEqual(list(subprocess42.split(data, False)), expected) |
| |
| # With universal_newlines=True |
| data = [(d[0], d[1].decode()) for d in data] |
| expected = [(e[0], e[1].decode()) for e in expected] |
| self.assertEqual(list(subprocess42.split(data, True)), expected) |
| |
| @params((None,), (10,)) |
| def test_wait_can_be_interrupted(self, timeout): |
| cmd = [ |
| sys.executable, |
| "-c", |
| textwrap.dedent( |
| r""" |
| import signal |
| import sys |
| import textwrap |
| import time |
| |
| from utils import subprocess42 |
| |
| class ExitError(Exception): |
| pass |
| def handler(signum, _frame): |
| raise ExitError |
| |
| sleep_script = textwrap.dedent(''' |
| import time |
| for _ in range(50): |
| time.sleep(0.2) |
| ''') |
| proc = subprocess42.Popen([sys.executable, '-c', sleep_script], |
| detached=True) |
| sig = signal.SIGBREAK if sys.platform =='win32' else signal.SIGTERM |
| with subprocess42.set_signal_handler([sig], handler): |
| try: |
| sys.stdout.write('hi\n') |
| sys.stdout.flush() |
| proc.wait(%s) |
| except ExitError: |
| sys.stdout.write('wait is interrupted') |
| sys.stdout.flush() |
| proc.kill() |
| """ |
| % timeout |
| ), |
| ] |
| # Set cwd to CLIENT_DIR so that the script can import subprocess42. |
| proc = subprocess42.Popen( |
| cmd, stdout=subprocess42.PIPE, cwd=test_env.CLIENT_DIR, detached=True |
| ) |
| self._wait_for_hi(proc, False) |
| time.sleep(0.5) |
| proc.terminate() |
| # proc is waiting for 'timeout' and SIGTERM/SIGBREAK is sent at 0.5s mark. |
| # Expect proc to write to stdout and exit almost immediately (decided |
| # by the poll interval of wait method). We wait for 8 second to give |
| # some buffer here. |
| start = time.time() |
| got = proc.recv_out(timeout=8) |
| want = b"wait is interrupted" |
| self.assertEqual( |
| got, want, "%s != %s after %s seconds" % (got, want, time.time() - start) |
| ) |
| |
| |
| if __name__ == "__main__": |
| test_env.main() |