mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2024-12-01 09:46:09 +03:00
477 lines
18 KiB
Python
477 lines
18 KiB
Python
|
#!/usr/bin/env vpython
|
||
|
#
|
||
|
# Copyright 2018 The Chromium Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
# found in the LICENSE file.
|
||
|
|
||
|
import argparse
|
||
|
import json
|
||
|
import logging
|
||
|
import os
|
||
|
import re
|
||
|
import signal
|
||
|
import sys
|
||
|
import tempfile
|
||
|
|
||
|
import psutil # pylint: disable=import-error
|
||
|
|
||
|
CHROMIUM_SRC_PATH = os.path.abspath(os.path.join(
|
||
|
os.path.dirname(__file__), '..', '..'))
|
||
|
|
||
|
# Use the android test-runner's gtest results support library for generating
|
||
|
# output json ourselves.
|
||
|
sys.path.insert(0, os.path.join(CHROMIUM_SRC_PATH, 'build', 'android'))
|
||
|
from pylib.base import base_test_result # pylint: disable=import-error
|
||
|
from pylib.results import json_results # pylint: disable=import-error
|
||
|
|
||
|
# Use luci-py's subprocess42.py
|
||
|
sys.path.insert(
|
||
|
0, os.path.join(CHROMIUM_SRC_PATH, 'tools', 'swarming_client', 'utils'))
|
||
|
import subprocess42 # pylint: disable=import-error
|
||
|
|
||
|
CHROMITE_PATH = os.path.abspath(os.path.join(
|
||
|
CHROMIUM_SRC_PATH, 'third_party', 'chromite'))
|
||
|
CROS_RUN_VM_TEST_PATH = os.path.abspath(os.path.join(
|
||
|
CHROMITE_PATH, 'bin', 'cros_run_vm_test'))
|
||
|
|
||
|
# GN target that corresponds to the cros browser sanity test.
|
||
|
SANITY_TEST_TARGET = 'cros_vm_sanity_test'
|
||
|
|
||
|
|
||
|
class TestFormatError(Exception):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class RemoteTest(object):
|
||
|
|
||
|
def __init__(self, args, unknown_args):
|
||
|
self._additional_args = unknown_args
|
||
|
self._path_to_outdir = args.path_to_outdir
|
||
|
self._test_exe = args.test_exe
|
||
|
self._test_launcher_summary_output = args.test_launcher_summary_output
|
||
|
self._vm_logs_dir = args.vm_logs_dir
|
||
|
|
||
|
self._test_env = os.environ.copy()
|
||
|
self._retries = 0
|
||
|
self._timeout = None
|
||
|
|
||
|
self._vm_test_cmd = [
|
||
|
CROS_RUN_VM_TEST_PATH,
|
||
|
'--start',
|
||
|
'--board', args.board,
|
||
|
'--cache-dir', args.cros_cache,
|
||
|
]
|
||
|
if args.vm_logs_dir:
|
||
|
self._vm_test_cmd += [
|
||
|
'--results-src', '/var/log/',
|
||
|
'--results-dest-dir', args.vm_logs_dir,
|
||
|
]
|
||
|
|
||
|
@property
|
||
|
def vm_test_cmd(self):
|
||
|
return self._vm_test_cmd
|
||
|
|
||
|
def run_test(self):
|
||
|
# Traps SIGTERM and kills all child processes of cros_run_vm_test when it's
|
||
|
# caught. This will allow us to capture logs from the VM if a test hangs
|
||
|
# and gets timeout-killed by swarming. See also:
|
||
|
# https://chromium.googlesource.com/infra/luci/luci-py/+/master/appengine/swarming/doc/Bot.md#graceful-termination_aka-the-sigterm-and-sigkill-dance
|
||
|
test_proc = None
|
||
|
def _kill_child_procs(trapped_signal, _):
|
||
|
logging.warning(
|
||
|
'Received signal %d. Killing child processes of test.',
|
||
|
trapped_signal)
|
||
|
if not test_proc or not test_proc.pid:
|
||
|
# This shouldn't happen?
|
||
|
logging.error('Test process not running.')
|
||
|
return
|
||
|
for child in psutil.Process(test_proc.pid).children():
|
||
|
logging.warning('Killing process %s', child)
|
||
|
child.kill()
|
||
|
|
||
|
signal.signal(signal.SIGTERM, _kill_child_procs)
|
||
|
|
||
|
for i in xrange(self._retries+1):
|
||
|
logging.info('########################################')
|
||
|
logging.info('Test attempt #%d', i)
|
||
|
logging.info('########################################')
|
||
|
test_proc = subprocess42.Popen(
|
||
|
self._vm_test_cmd, stdout=sys.stdout, stderr=sys.stderr,
|
||
|
env=self._test_env)
|
||
|
try:
|
||
|
test_proc.wait(timeout=self._timeout)
|
||
|
except subprocess42.TimeoutExpired:
|
||
|
logging.error('Test timed out. Sending SIGTERM.')
|
||
|
# SIGTERM the proc and wait 10s for it to close.
|
||
|
test_proc.terminate()
|
||
|
try:
|
||
|
test_proc.wait(timeout=10)
|
||
|
except subprocess42.TimeoutExpired:
|
||
|
# If it hasn't closed in 10s, SIGKILL it.
|
||
|
logging.error('Test did not exit in time. Sending SIGKILL.')
|
||
|
test_proc.kill()
|
||
|
test_proc.wait()
|
||
|
logging.info('Test exitted with %d.', test_proc.returncode)
|
||
|
if test_proc.returncode == 0:
|
||
|
break
|
||
|
|
||
|
self.post_run(test_proc.returncode)
|
||
|
return test_proc.returncode
|
||
|
|
||
|
def post_run(self, return_code):
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
|
||
|
class GTestTest(RemoteTest):
|
||
|
|
||
|
_FILE_BLACKLIST = [
|
||
|
re.compile(r'.*build/chromeos.*'),
|
||
|
re.compile(r'.*build/cros_cache.*'),
|
||
|
re.compile(r'.*third_party/chromite.*'),
|
||
|
]
|
||
|
|
||
|
def __init__(self, args, unknown_args):
|
||
|
super(GTestTest, self).__init__(args, unknown_args)
|
||
|
|
||
|
self._runtime_deps_path = args.runtime_deps_path
|
||
|
self._vpython_dir = args.vpython_dir
|
||
|
|
||
|
self._test_launcher_shard_index = args.test_launcher_shard_index
|
||
|
self._test_launcher_total_shards = args.test_launcher_total_shards
|
||
|
|
||
|
self._on_vm_script = None
|
||
|
|
||
|
def build_test_command(self):
|
||
|
# To keep things easy for us, ensure both types of output locations are
|
||
|
# the same.
|
||
|
if self._test_launcher_summary_output and self._vm_logs_dir:
|
||
|
json_out_dir = os.path.dirname(self._test_launcher_summary_output) or '.'
|
||
|
if os.path.abspath(json_out_dir) != os.path.abspath(self._vm_logs_dir):
|
||
|
raise TestFormatError(
|
||
|
'--test-launcher-summary-output and --vm-logs-dir must point to '
|
||
|
'the same directory.')
|
||
|
|
||
|
if self._test_launcher_summary_output:
|
||
|
result_dir, result_file = os.path.split(
|
||
|
self._test_launcher_summary_output)
|
||
|
# If args.test_launcher_summary_output is a file in cwd, result_dir will
|
||
|
# be an empty string, so replace it with '.' when this is the case so
|
||
|
# cros_run_vm_test can correctly handle it.
|
||
|
if not result_dir:
|
||
|
result_dir = '.'
|
||
|
vm_result_file = '/tmp/%s' % result_file
|
||
|
self._vm_test_cmd += [
|
||
|
'--results-src', vm_result_file,
|
||
|
'--results-dest-dir', result_dir,
|
||
|
]
|
||
|
|
||
|
# Build the shell script that will be used on the VM to invoke the test.
|
||
|
vm_test_script_contents = ['#!/bin/sh']
|
||
|
|
||
|
# /home is mounted with "noexec" in the VM, but some of our tools
|
||
|
# and tests use the home dir as a workspace (eg: vpython downloads
|
||
|
# python binaries to ~/.vpython-root). /tmp doesn't have this
|
||
|
# restriction, so change the location of the home dir for the
|
||
|
# duration of the test.
|
||
|
vm_test_script_contents.append('export HOME=/tmp')
|
||
|
|
||
|
if self._vpython_dir:
|
||
|
vpython_spec_path = os.path.relpath(
|
||
|
os.path.join(CHROMIUM_SRC_PATH, '.vpython'),
|
||
|
self._path_to_outdir)
|
||
|
# Initialize the vpython cache. This can take 10-20s, and some tests
|
||
|
# can't afford to wait that long on the first invocation.
|
||
|
vm_test_script_contents.extend([
|
||
|
'export PATH=$PATH:$PWD/%s' % (self._vpython_dir),
|
||
|
'vpython -vpython-spec %s -vpython-tool install' % (
|
||
|
vpython_spec_path),
|
||
|
])
|
||
|
|
||
|
test_invocation = (
|
||
|
'./%s --test-launcher-shard-index=%d '
|
||
|
'--test-launcher-total-shards=%d' % (
|
||
|
self._test_exe, self._test_launcher_shard_index,
|
||
|
self._test_launcher_total_shards)
|
||
|
)
|
||
|
if self._test_launcher_summary_output:
|
||
|
test_invocation += ' --test-launcher-summary-output=%s' % vm_result_file
|
||
|
if self._additional_args:
|
||
|
test_invocation += ' %s' % ' '.join(self._additional_args)
|
||
|
vm_test_script_contents.append(test_invocation)
|
||
|
|
||
|
logging.info('Running the following command in the VM:')
|
||
|
logging.info('\n'.join(vm_test_script_contents))
|
||
|
fd, tmp_path = tempfile.mkstemp(suffix='.sh', dir=self._path_to_outdir)
|
||
|
os.fchmod(fd, 0755)
|
||
|
with os.fdopen(fd, 'wb') as f:
|
||
|
f.write('\n'.join(vm_test_script_contents))
|
||
|
self._on_vm_script = tmp_path
|
||
|
|
||
|
runtime_files = [os.path.relpath(self._on_vm_script)]
|
||
|
runtime_files += self._read_runtime_files()
|
||
|
if self._vpython_dir:
|
||
|
# --vpython-dir is relative to the out dir, but --files expects paths
|
||
|
# relative to src dir, so fix the path up a bit.
|
||
|
runtime_files.append(
|
||
|
os.path.relpath(
|
||
|
os.path.abspath(os.path.join(self._path_to_outdir,
|
||
|
self._vpython_dir)),
|
||
|
CHROMIUM_SRC_PATH))
|
||
|
# TODO(bpastene): Add the vpython spec to the test's runtime deps instead
|
||
|
# of handling it here.
|
||
|
runtime_files.append('.vpython')
|
||
|
|
||
|
# Since we're pushing files, we need to set the cwd.
|
||
|
self._vm_test_cmd.extend(
|
||
|
['--cwd', os.path.relpath(self._path_to_outdir, CHROMIUM_SRC_PATH)])
|
||
|
for f in runtime_files:
|
||
|
self._vm_test_cmd.extend(['--files', f])
|
||
|
|
||
|
self._vm_test_cmd += [
|
||
|
# Some tests fail as root, so run as the less privileged user 'chronos'.
|
||
|
'--as-chronos',
|
||
|
'--cmd',
|
||
|
'--',
|
||
|
'./' + os.path.relpath(self._on_vm_script, self._path_to_outdir)
|
||
|
]
|
||
|
|
||
|
def _read_runtime_files(self):
|
||
|
if not self._runtime_deps_path:
|
||
|
return []
|
||
|
|
||
|
abs_runtime_deps_path = os.path.abspath(
|
||
|
os.path.join(self._path_to_outdir, self._runtime_deps_path))
|
||
|
with open(abs_runtime_deps_path) as runtime_deps_file:
|
||
|
files = [l.strip() for l in runtime_deps_file if l]
|
||
|
rel_file_paths = []
|
||
|
for f in files:
|
||
|
rel_file_path = os.path.relpath(
|
||
|
os.path.abspath(os.path.join(self._path_to_outdir, f)))
|
||
|
if not any(regex.match(rel_file_path) for regex in self._FILE_BLACKLIST):
|
||
|
rel_file_paths.append(rel_file_path)
|
||
|
return rel_file_paths
|
||
|
|
||
|
def post_run(self, _):
|
||
|
if self._on_vm_script:
|
||
|
os.remove(self._on_vm_script)
|
||
|
|
||
|
|
||
|
class BrowserSanityTest(RemoteTest):
|
||
|
|
||
|
def __init__(self, args, unknown_args):
|
||
|
super(BrowserSanityTest, self).__init__(args, unknown_args)
|
||
|
|
||
|
# 5 min should be enough time for the sanity test to pass.
|
||
|
self._retries = 2
|
||
|
self._timeout = 300
|
||
|
|
||
|
def build_test_command(self):
|
||
|
if '--gtest_filter=%s' % SANITY_TEST_TARGET in self._additional_args:
|
||
|
logging.info(
|
||
|
'GTest filtering not supported for the sanity test. The '
|
||
|
'--gtest_filter arg will be ignored.')
|
||
|
self._additional_args.remove('--gtest_filter=%s' % SANITY_TEST_TARGET)
|
||
|
|
||
|
if self._additional_args:
|
||
|
raise TestFormatError(
|
||
|
'Sanity test should not have additional args: %s' % (
|
||
|
self._additional_args))
|
||
|
|
||
|
# run_cros_vm_test's default behavior when no cmd is specified is the sanity
|
||
|
# test that's baked into the VM image. This test smoke-checks the system
|
||
|
# browser, so deploy our locally-built chrome to the VM before testing.
|
||
|
self._vm_test_cmd += [
|
||
|
'--deploy',
|
||
|
'--build-dir', os.path.relpath(self._path_to_outdir, CHROMIUM_SRC_PATH),
|
||
|
]
|
||
|
|
||
|
# deploy_chrome needs a set of GN args used to build chrome to determine if
|
||
|
# certain libraries need to be pushed to the VM. It looks for the args via
|
||
|
# an env var. To trigger the default deploying behavior, give it a dummy set
|
||
|
# of args.
|
||
|
# TODO(crbug.com/823996): Make the GN-dependent deps controllable via cmd
|
||
|
# line args.
|
||
|
if not self._test_env.get('GN_ARGS'):
|
||
|
self._test_env['GN_ARGS'] = 'is_chromeos = true'
|
||
|
self._test_env['PATH'] = (
|
||
|
self._test_env['PATH'] + ':' + os.path.join(CHROMITE_PATH, 'bin'))
|
||
|
|
||
|
def post_run(self, return_code):
|
||
|
# Create a simple json results file for the sanity test if needed. The
|
||
|
# results will contain only one test (SANITY_TEST_TARGET), and will
|
||
|
# either be a PASS or FAIL depending on the return code of cros_run_vm_test.
|
||
|
if self._test_launcher_summary_output:
|
||
|
result = (base_test_result.ResultType.FAIL if return_code else
|
||
|
base_test_result.ResultType.PASS)
|
||
|
sanity_test_result = base_test_result.BaseTestResult(
|
||
|
SANITY_TEST_TARGET, result)
|
||
|
run_results = base_test_result.TestRunResults()
|
||
|
run_results.AddResult(sanity_test_result)
|
||
|
with open(self._test_launcher_summary_output, 'w') as f:
|
||
|
json.dump(json_results.GenerateResultsDict([run_results]), f)
|
||
|
|
||
|
|
||
|
def vm_test(args, unknown_args):
|
||
|
# cros_run_vm_test has trouble with relative paths that go up directories,
|
||
|
# so cd to src/, which should be the root of all data deps.
|
||
|
os.chdir(CHROMIUM_SRC_PATH)
|
||
|
|
||
|
# pylint: disable=redefined-variable-type
|
||
|
# TODO: Remove the above when depot_tool's pylint is updated to include the
|
||
|
# fix to https://github.com/PyCQA/pylint/issues/710.
|
||
|
if args.test_exe == SANITY_TEST_TARGET:
|
||
|
test = BrowserSanityTest(args, unknown_args)
|
||
|
else:
|
||
|
test = GTestTest(args, unknown_args)
|
||
|
|
||
|
test.build_test_command()
|
||
|
logging.info('Running the following command on the host:')
|
||
|
logging.info(' '.join(test.vm_test_cmd))
|
||
|
|
||
|
return test.run_test()
|
||
|
|
||
|
|
||
|
def host_cmd(args, unknown_args):
|
||
|
if not args.cmd:
|
||
|
raise TestFormatError('Must specify command to run on the host.')
|
||
|
elif unknown_args:
|
||
|
raise TestFormatError(
|
||
|
'Args "%s" unsupported. Is your host command correctly formatted?' % (
|
||
|
' '.join(unknown_args)))
|
||
|
elif args.deploy_chrome and not args.path_to_outdir:
|
||
|
raise TestFormatError(
|
||
|
'--path-to-outdir must be specified if --deploy-chrome is passed.')
|
||
|
|
||
|
cros_run_vm_test_cmd = [
|
||
|
CROS_RUN_VM_TEST_PATH,
|
||
|
'--start',
|
||
|
'--board', args.board,
|
||
|
'--cache-dir', args.cros_cache,
|
||
|
]
|
||
|
if args.verbose:
|
||
|
cros_run_vm_test_cmd.append('--debug')
|
||
|
|
||
|
test_env = os.environ.copy()
|
||
|
if args.deploy_chrome:
|
||
|
cros_run_vm_test_cmd += [
|
||
|
'--deploy',
|
||
|
'--build-dir', os.path.abspath(args.path_to_outdir),
|
||
|
]
|
||
|
# If we're deploying, push chromite/bin's deploy_chrome onto PATH.
|
||
|
test_env['PATH'] = (
|
||
|
test_env['PATH'] + ':' + os.path.join(CHROMITE_PATH, 'bin'))
|
||
|
# deploy_chrome needs a set of GN args used to build chrome to determine if
|
||
|
# certain libraries need to be pushed to the VM. It looks for the args via
|
||
|
# an env var. To trigger the default deploying behavior, give it a dummy set
|
||
|
# of args.
|
||
|
# TODO(crbug.com/823996): Make the GN-dependent deps controllable via cmd
|
||
|
# line args.
|
||
|
if not test_env.get('GN_ARGS'):
|
||
|
test_env['GN_ARGS'] = 'is_chromeos = true'
|
||
|
|
||
|
cros_run_vm_test_cmd += [
|
||
|
'--host-cmd',
|
||
|
'--',
|
||
|
] + args.cmd
|
||
|
|
||
|
logging.info('Running the following command:')
|
||
|
logging.info(' '.join(cros_run_vm_test_cmd))
|
||
|
|
||
|
return subprocess42.call(
|
||
|
cros_run_vm_test_cmd, stdout=sys.stdout, stderr=sys.stderr, env=test_env)
|
||
|
|
||
|
|
||
|
def main():
|
||
|
parser = argparse.ArgumentParser()
|
||
|
parser.add_argument('--verbose', '-v', action='store_true')
|
||
|
# Required args.
|
||
|
parser.add_argument(
|
||
|
'--board', type=str, required=True, help='Type of CrOS device.')
|
||
|
subparsers = parser.add_subparsers(dest='test_type')
|
||
|
# Host-side test args.
|
||
|
host_cmd_parser = subparsers.add_parser(
|
||
|
'host-cmd',
|
||
|
help='Runs a host-side test. Pass the host-side command to run after '
|
||
|
'"--". Hostname and port for the VM will be 127.0.0.1:9222.')
|
||
|
host_cmd_parser.set_defaults(func=host_cmd)
|
||
|
host_cmd_parser.add_argument(
|
||
|
'--cros-cache', type=str, required=True, help='Path to cros cache.')
|
||
|
host_cmd_parser.add_argument(
|
||
|
'--path-to-outdir', type=os.path.realpath,
|
||
|
help='Path to output directory, all of whose contents will be deployed '
|
||
|
'to the device.')
|
||
|
host_cmd_parser.add_argument(
|
||
|
'--deploy-chrome', action='store_true',
|
||
|
help='Will deploy a locally built Chrome binary to the VM before running '
|
||
|
'the host-cmd.')
|
||
|
host_cmd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||
|
# VM-side test args.
|
||
|
vm_test_parser = subparsers.add_parser(
|
||
|
'vm-test',
|
||
|
help='Runs a vm-side gtest.')
|
||
|
vm_test_parser.set_defaults(func=vm_test)
|
||
|
vm_test_parser.add_argument(
|
||
|
'--cros-cache', type=str, required=True, help='Path to cros cache.')
|
||
|
vm_test_parser.add_argument(
|
||
|
'--test-exe', type=str, required=True,
|
||
|
help='Path to test executable to run inside VM. If the value is '
|
||
|
'%s, the sanity test that ships with the VM '
|
||
|
'image runs instead. This test smokes-check the system browser '
|
||
|
'(eg: loads a simple webpage, executes some javascript), so a '
|
||
|
'fully-built Chrome binary that can get deployed to the VM is '
|
||
|
'expected to be available in the out-dir.' % SANITY_TEST_TARGET)
|
||
|
|
||
|
# GTest args. Some are passed down to the test binary in the VM. Others are
|
||
|
# parsed here since they might need tweaking or special handling.
|
||
|
vm_test_parser.add_argument(
|
||
|
'--test-launcher-summary-output', type=str,
|
||
|
help='When set, will pass the same option down to the test and retrieve '
|
||
|
'its result file at the specified location.')
|
||
|
# Shard args are parsed here since we might also specify them via env vars.
|
||
|
vm_test_parser.add_argument(
|
||
|
'--test-launcher-shard-index',
|
||
|
type=int, default=os.environ.get('GTEST_SHARD_INDEX', 0),
|
||
|
help='Index of the external shard to run.')
|
||
|
vm_test_parser.add_argument(
|
||
|
'--test-launcher-total-shards',
|
||
|
type=int, default=os.environ.get('GTEST_TOTAL_SHARDS', 1),
|
||
|
help='Total number of external shards.')
|
||
|
|
||
|
# Misc args.
|
||
|
vm_test_parser.add_argument(
|
||
|
'--path-to-outdir', type=str, required=True,
|
||
|
help='Path to output directory, all of whose contents will be deployed '
|
||
|
'to the device.')
|
||
|
vm_test_parser.add_argument(
|
||
|
'--runtime-deps-path', type=str,
|
||
|
help='Runtime data dependency file from GN.')
|
||
|
vm_test_parser.add_argument(
|
||
|
'--vpython-dir', type=str,
|
||
|
help='Location on host of a directory containing a vpython binary to '
|
||
|
'deploy to the VM before the test starts. The location of this dir '
|
||
|
'will be added onto PATH in the VM. WARNING: The arch of the VM '
|
||
|
'might not match the arch of the host, so avoid using "${platform}" '
|
||
|
'when downloading vpython via CIPD.')
|
||
|
vm_test_parser.add_argument(
|
||
|
'--vm-logs-dir', type=str,
|
||
|
help='Will copy everything under /var/log/ from the VM after the test '
|
||
|
'into the specified dir.')
|
||
|
args, unknown_args = parser.parse_known_args()
|
||
|
|
||
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARN)
|
||
|
|
||
|
if not os.path.exists('/dev/kvm'):
|
||
|
logging.error('/dev/kvm is missing. Is KVM installed on this machine?')
|
||
|
return 1
|
||
|
elif not os.access('/dev/kvm', os.W_OK):
|
||
|
logging.error(
|
||
|
'/dev/kvm is not writable as current user. Perhaps you should be root?')
|
||
|
return 1
|
||
|
|
||
|
args.cros_cache = os.path.abspath(args.cros_cache)
|
||
|
return args.func(args, unknown_args)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
sys.exit(main())
|