blob: 611ca14cdfc98ade36db7f7c2cee92cea4cef10b [file]
#!/usr/bin/env python3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for test_env.py and its generated script_test wrapper.
The signal tests launch Python through test_env.py to simulate Swarming.
The wrapper test checks Windows argument and exit-code forwarding.
"""
import json
import os
import signal
import subprocess
import sys
import tempfile
import time
import unittest
if sys.platform == 'win32':
try:
import win32api
import win32con
import win32process
except ImportError:
win32api = None
HERE = os.path.dirname(os.path.abspath(__file__))
TEST_SCRIPT = os.path.join(HERE, 'test_env_user_script.py')
GENERATOR_SCRIPT = os.path.normpath(
os.path.join(HERE, '..', 'build', 'util', 'generate_wrapper.py')
)
def launch_process_windows(args):
# The `universal_newlines` option is equivalent to `text` in Python 3.
return subprocess.Popen(
[sys.executable, TEST_SCRIPT] + args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=os.environ.copy(),
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
universal_newlines=True,
)
def launch_process_nonwindows(args):
# The `universal_newlines` option is equivalent to `text` in Python 3.
return subprocess.Popen(
[sys.executable, TEST_SCRIPT] + args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=os.environ.copy(),
universal_newlines=True,
)
def read_subprocess_message(proc, starts_with):
"""Finds the value after first line prefix condition."""
for line in proc.stdout:
if line.startswith(starts_with):
return line.rstrip().replace(starts_with, '')
def send_and_wait(proc, sig, sleep_time=0.6):
"""Sends a signal to subprocess."""
time.sleep(sleep_time) # gives process time to launch.
os.kill(proc.pid, sig)
proc.wait()
class SignalingWindowsTest(unittest.TestCase):
def setUp(self):
super().setUp()
if sys.platform != 'win32':
self.skipTest('test only runs on Windows')
def test_send_ctrl_break_event(self):
proc = launch_process_windows([])
send_and_wait(proc, signal.CTRL_BREAK_EVENT)
sig = read_subprocess_message(proc, 'Signal :')
# This test is flaky because it relies on the child process starting quickly
# "enough", which it fails to do sometimes. This is tracked by
# https://crbug.com/1335123 and it is hoped that increasing the timeout will
# reduce the flakiness.
self.assertEqual(
sig,
str(int(signal.SIGBREAK)),
)
def test_job_object_kills_leaked_child_process(self):
proc = launch_process_windows(['--spawn-leaked-child'])
leaked_pid_str = read_subprocess_message(proc, 'Leaked PID:')
proc.wait()
self.assertIsNotNone(leaked_pid_str)
leaked_pid = int(leaked_pid_str)
time.sleep(0.2)
if not win32api:
return
try:
hproc = win32api.OpenProcess(
win32con.PROCESS_QUERY_LIMITED_INFORMATION, False, leaked_pid
)
if hproc:
exit_code = win32process.GetExitCodeProcess(hproc)
win32api.CloseHandle(hproc)
# 259 is STILL_ACTIVE; process should be terminated.
self.assertNotEqual(exit_code, 259)
except Exception:
# OpenProcess failing indicates the PID is no longer valid (process dead).
pass
class GeneratedWrapperTest(unittest.TestCase):
def setUp(self):
super().setUp()
temp_dir = tempfile.TemporaryDirectory(prefix='generated wrapper ')
self.addCleanup(temp_dir.cleanup)
self.temp_dir = temp_dir.name
probe = os.path.join(self.temp_dir, 'probe.py')
with open(probe, 'w', encoding='utf-8') as probe_file:
probe_file.write(
'import json\n'
'import sys\n'
'print("WRAPPER_ARGV=" + json.dumps(sys.argv[1:]))\n'
'sys.exit(int(sys.argv[1]))\n'
)
self.wrapper = self._generate_wrapper('run wrapper.bat', 'probe.py')
def _generate_wrapper(self, name, executable):
wrapper = os.path.join(self.temp_dir, name)
subprocess.run(
[
sys.executable,
GENERATOR_SCRIPT,
'--executable',
executable,
'--wrapper-script',
wrapper,
'--output-directory',
self.temp_dir,
'--script-language',
'batch',
],
cwd=self.temp_dir,
check=True,
timeout=60,
)
return wrapper
def _check_arguments_and_exit_code(self, command):
forwarded = [
'--isolated-script-test-filter='
':chromium-bidi!pytest:tests/:'
'test_resultsink_repeat.py#test_case[paired!marker!value]',
':chromium-bidi!pytest:tests/script/:test_serialization.py#'
"test_serialization_function[new Error('Woops!')-expected_serialized18]",
'unpaired!bang',
'argument with spaces & ampersand',
'*.test.js',
]
for exit_code in (0, 1, 5, 7):
with self.subTest(exit_code=exit_code):
expected_argv = [str(exit_code)] + forwarded
result = subprocess.run(
command + expected_argv,
cwd=self.temp_dir,
capture_output=True,
text=True,
check=False,
timeout=60,
)
self.assertEqual(
result.returncode, exit_code, result.stdout + result.stderr
)
marker = next(
(
line.removeprefix('WRAPPER_ARGV=')
for line in result.stdout.splitlines()
if line.startswith('WRAPPER_ARGV=')
),
None,
)
self.assertIsNotNone(marker, result.stdout + result.stderr)
self.assertEqual(json.loads(marker), expected_argv)
def test_python_entry_point(self):
# Check the batch/Python header under Python even on non-Windows hosts.
# This does not exercise cmd.exe's argument or exit-status handling.
self._check_arguments_and_exit_code([sys.executable, '-x', self.wrapper])
@unittest.skipUnless(sys.platform == 'win32', 'test only runs on Windows')
def test_preserves_arguments_and_exit_code(self):
self._check_arguments_and_exit_code([self.wrapper])
@unittest.skipUnless(sys.platform == 'win32', 'test only runs on Windows')
def test_nested_wrappers_preserve_arguments_and_exit_code(self):
# Also check the outer wrapper's process status: a generated suite wrapper
# must not hide failures reported by its child.
outer_wrapper = self._generate_wrapper(
'run outer wrapper.bat', '@WrappedPath(run wrapper.bat)'
)
self._check_arguments_and_exit_code([outer_wrapper])
class SignalingNonWindowsTest(unittest.TestCase):
def setUp(self):
super().setUp()
if sys.platform == 'win32':
self.skipTest('test does not run on Windows')
def test_send_sigterm(self):
proc = launch_process_nonwindows([])
send_and_wait(proc, signal.SIGTERM)
sig = read_subprocess_message(proc, 'Signal :')
self.assertEqual(sig, str(int(signal.SIGTERM)))
def test_send_sigint(self):
proc = launch_process_nonwindows([])
send_and_wait(proc, signal.SIGINT)
sig = read_subprocess_message(proc, 'Signal :')
self.assertEqual(sig, str(int(signal.SIGINT)))
if __name__ == '__main__':
unittest.main()