blob: 7a5d5e2fc8ff941b1d827e3a8f166b9680face4d [file] [edit]
#!/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()