| # -*- coding: utf-8 -*- |
| # Copyright 2018 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Library for running Chrome OS tests.""" |
| |
| from __future__ import print_function |
| |
| import datetime |
| import os |
| import sys |
| |
| from chromite.cli.cros import cros_chrome_sdk |
| from chromite.lib import chrome_util |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import device |
| from chromite.lib import osutils |
| from chromite.lib import path_util |
| from chromite.lib import vm |
| from chromite.lib.xbuddy import xbuddy |
| |
| |
| assert sys.version_info >= (3, 6), 'This module requires Python 3.6+' |
| |
| |
| class CrOSTest(object): |
| """Class for running Chrome OS tests.""" |
| |
| def __init__(self, opts): |
| """Initialize CrOSTest. |
| |
| Args: |
| opts: command line options. |
| """ |
| self.start_time = datetime.datetime.utcnow() |
| |
| self.start_vm = opts.start_vm |
| self.cache_dir = opts.cache_dir |
| self.dryrun = opts.dryrun |
| |
| self.build = opts.build |
| self.flash = opts.flash |
| self.public_image = opts.public_image |
| self.xbuddy = opts.xbuddy |
| self.deploy = opts.deploy |
| self.deploy_lacros = opts.deploy_lacros |
| self.nostrip = opts.nostrip |
| self.build_dir = opts.build_dir |
| self.mount = opts.mount |
| |
| self.catapult_tests = opts.catapult_tests |
| self.guest = opts.guest |
| |
| self.autotest = opts.autotest |
| self.tast = opts.tast |
| self.tast_vars = opts.tast_vars |
| self.tast_total_shards = opts.tast_total_shards |
| self.tast_shard_index = opts.tast_shard_index |
| self.results_dir = opts.results_dir |
| self.test_that_args = opts.test_that_args |
| self.test_timeout = opts.test_timeout |
| |
| self.remote_cmd = opts.remote_cmd |
| self.host_cmd = opts.host_cmd |
| self.cwd = opts.cwd |
| self.files = opts.files |
| self.files_from = opts.files_from |
| self.as_chronos = opts.as_chronos |
| self.args = opts.args[1:] if opts.args else None |
| |
| self.results_src = opts.results_src |
| self.results_dest_dir = opts.results_dest_dir |
| self.save_snapshot_on_failure = opts.save_snapshot_on_failure |
| |
| self.chrome_test = opts.chrome_test |
| if self.chrome_test: |
| self.chrome_test_target = os.path.basename(opts.args[1]) |
| self.chrome_test_deploy_target_dir = '/usr/local/chrome_test' |
| else: |
| self.chrome_test_target = None |
| self.chrome_test_deploy_target_dir = None |
| self.staging_dir = None |
| |
| self._device = device.Device.Create(opts) |
| |
| def __del__(self): |
| self._StopVM() |
| |
| logging.info('Time elapsed: %s', |
| datetime.datetime.utcnow() - self.start_time) |
| |
| def Run(self): |
| """Start a VM, build/deploy, run tests, stop the VM.""" |
| if self._device.should_start_vm: |
| self._StartVM() |
| else: |
| self._device.WaitForBoot() |
| |
| self._Build() |
| self._Flash() |
| self._Deploy() |
| |
| returncode = self._RunTests() |
| |
| self._StopVM() |
| return returncode |
| |
| def _StartVM(self): |
| """Start a VM if necessary. |
| |
| If --start-vm is specified, we launch a new VM, otherwise we use an |
| existing VM. |
| """ |
| if not self._device.should_start_vm: |
| return |
| |
| if not self._device.IsRunning(): |
| self.start_vm = True |
| |
| if self.start_vm: |
| self._device.Start() |
| |
| def _StopVM(self): |
| """Stop the VM if necessary. |
| |
| If --start-vm was specified, we launched this VM, so we now stop it. |
| """ |
| if self._device and self.start_vm: |
| self._device.Stop() |
| |
| def _Build(self): |
| """Build chrome.""" |
| if not self.build: |
| return |
| |
| build_target = self.chrome_test_target or 'chromiumos_preflight' |
| cros_build_lib.run(['autoninja', '-C', self.build_dir, build_target], |
| dryrun=self.dryrun) |
| |
| def _Flash(self): |
| """Flash device.""" |
| if not self.flash: |
| return |
| |
| if self.xbuddy: |
| xbuddy_path = self.xbuddy.format(board=self._device.board) |
| else: |
| version = xbuddy.LATEST |
| if path_util.DetermineCheckout().type != path_util.CHECKOUT_TYPE_REPO: |
| # Try flashing to the full version of the board used in the Simple |
| # Chrome SDK if it's present in the cache. Otherwise default to using |
| # latest. |
| cache = self.cache_dir or path_util.GetCacheDir() |
| version = cros_chrome_sdk.SDKFetcher.GetCachedFullVersion( |
| cache, self._device.board) or version |
| suffix = '' |
| if self.public_image: |
| suffix = '-full' |
| xbuddy_path = 'xbuddy://remote/%s%s/%s' % ( |
| self._device.board, suffix, version) |
| |
| # Only considers skipping flashing if it's NOT for lacros-chrome tests |
| # because at this time, automated/CI tests can't assume that ash-chrome is |
| # left in a clean state and lacros-chrome depends on ash-chrome. |
| if not self.deploy_lacros: |
| # Skip the flash if the device is already running the requested version. |
| device_version = self._device.remote.version |
| _, _, requested_version, _ = xbuddy.XBuddy.InterpretPath(xbuddy_path) |
| # Split on the first "-" when comparing versions since xbuddy requires |
| # the RX- prefix, but the device may not advertise it. |
| if xbuddy.LATEST not in requested_version: |
| if (requested_version == device_version or |
| ('-' in requested_version and |
| requested_version.split('-', 1)[1] == device_version)): |
| logging.info( |
| 'Skipping the flash. Device running %s when %s was requested', |
| device_version, xbuddy_path) |
| return |
| |
| device_name = 'ssh://' + self._device.device |
| if self._device.ssh_port: |
| device_name += ':' + str(self._device.ssh_port) |
| flash_cmd = [ |
| os.path.join(constants.CHROMITE_BIN_DIR, 'cros'), |
| 'flash', |
| device_name, |
| xbuddy_path, |
| '--board', self._device.board, |
| '--disable-rootfs-verification', |
| '--clobber-stateful', |
| '--clear-tpm-owner', |
| ] |
| cros_build_lib.run(flash_cmd, dryrun=self.dryrun) |
| |
| def _Deploy(self): |
| """Deploy binary files to device.""" |
| if not self.build and not self.deploy and not self.deploy_lacros: |
| return |
| |
| if self.chrome_test: |
| self._DeployChromeTest() |
| else: |
| self._DeployChrome() |
| |
| def _DeployChrome(self): |
| """Deploy lacros-chrome or ash-chrome.""" |
| deploy_cmd = [ |
| 'deploy_chrome', |
| '--force', |
| '--build-dir', |
| self.build_dir, |
| '--process-timeout', |
| '180', |
| ] |
| if self._device.ssh_port: |
| deploy_cmd += [ |
| '--device', |
| '%s:%d' % (self._device.device, self._device.ssh_port) |
| ] |
| else: |
| deploy_cmd += ['--device', self._device.device] |
| |
| if self.cache_dir: |
| deploy_cmd += ['--cache-dir', self.cache_dir] |
| |
| if self.deploy_lacros: |
| # By default, deploying lacros-chrome modifies the /etc/chrome_dev.conf |
| # file, which is desired behavior for local development, however, a |
| # modified config file interferes with automated testing. |
| deploy_cmd += ['--lacros', '--nostrip', '--skip-updating-config-file'] |
| else: |
| deploy_cmd.append('--deploy-test-binaries') |
| if self._device.board: |
| deploy_cmd += ['--board', self._device.board] |
| if self.nostrip: |
| deploy_cmd += ['--nostrip'] |
| if self.mount: |
| deploy_cmd += ['--mount'] |
| |
| cros_build_lib.run(deploy_cmd, dryrun=self.dryrun) |
| self._device.WaitForBoot() |
| |
| def _DeployChromeTest(self): |
| """Deploy chrome test binary and its runtime files to device.""" |
| src_dir = os.path.dirname(os.path.dirname(self.build_dir)) |
| self._DeployCopyPaths(src_dir, self.chrome_test_deploy_target_dir, |
| chrome_util.GetChromeTestCopyPaths( |
| self.build_dir, self.chrome_test_target)) |
| |
| def _DeployCopyPaths(self, host_src_dir, remote_target_dir, copy_paths): |
| """Deploy files in copy_paths to device. |
| |
| Args: |
| host_src_dir: Source dir on the host that files in |copy_paths| are |
| relative to. |
| remote_target_dir: Target dir on the remote device that the files in |
| |copy_paths| are copied to. |
| copy_paths: A list of chrome_utils.Path of files to be copied. |
| """ |
| with osutils.TempDir(set_global=True) as tempdir: |
| self.staging_dir = tempdir |
| strip_bin = None |
| chrome_util.StageChromeFromBuildDir( |
| self.staging_dir, host_src_dir, strip_bin, copy_paths=copy_paths) |
| |
| if self._device.remote.HasRsync(): |
| self._device.remote.CopyToDevice( |
| '%s/' % os.path.abspath(self.staging_dir), remote_target_dir, |
| mode='rsync', inplace=True, compress=True, debug_level=logging.INFO) |
| else: |
| self._device.remote.CopyToDevice( |
| '%s/' % os.path.abspath(self.staging_dir), remote_target_dir, |
| mode='scp', debug_level=logging.INFO) |
| |
| def _RunCatapultTests(self): |
| """Run catapult tests matching a pattern using run_tests. |
| |
| Returns: |
| cros_build_lib.CommandResult object. |
| """ |
| |
| browser = 'system-guest' if self.guest else 'system' |
| return self._device.remote_run([ |
| 'python', |
| '/usr/local/telemetry/src/third_party/catapult/telemetry/bin/run_tests', |
| '--browser=%s' % browser, |
| ] + self.catapult_tests, stream_output=True) |
| |
| def _RunAutotest(self): |
| """Run an autotest using test_that. |
| |
| Returns: |
| cros_build_lib.CommandResult object. |
| """ |
| cmd = ['test_that'] |
| if self._device.board: |
| cmd += ['--board', self._device.board] |
| if self.results_dir: |
| cmd += ['--results_dir', path_util.ToChrootPath(self.results_dir)] |
| if self._device.private_key: |
| cmd += ['--ssh_private_key', |
| path_util.ToChrootPath(self._device.private_key)] |
| if self._device.log_level == 'debug': |
| cmd += ['--debug'] |
| if self.test_that_args: |
| cmd += self.test_that_args[1:] |
| cmd += [ |
| '--no-quickmerge', |
| '--ssh_options', '-F /dev/null -i /dev/null', |
| ] |
| if self._device.ssh_port: |
| cmd += ['%s:%d' % (self._device.device, self._device.ssh_port)] |
| else: |
| cmd += [self._device.device] |
| cmd += self.autotest |
| return cros_build_lib.run(cmd, dryrun=self.dryrun, enter_chroot=True) |
| |
| def _RunTastTests(self): |
| """Run Tast tests. |
| |
| Returns: |
| cros_build_lib.CommandResult object. |
| """ |
| # Try using the Tast binaries that the SimpleChrome SDK downloads |
| # automatically. |
| tast_cache_dir = cros_chrome_sdk.SDKFetcher.GetCachePath( |
| 'chromeos-base', self.cache_dir, self._device.board) |
| if tast_cache_dir: |
| tast_bin_dir = os.path.join(tast_cache_dir, 'tast-cmd', 'usr', 'bin') |
| cmd = [os.path.join(tast_bin_dir, 'tast')] |
| need_chroot = False |
| else: |
| # Silently fall back to using the chroot if there's no SimpleChrome SDK |
| # present. |
| cmd = ['tast'] |
| need_chroot = True |
| |
| if self._device.log_level == 'debug': |
| cmd += ['-verbose'] |
| cmd += ['run', '-build=false', '-waituntilready',] |
| # If the tests are not informational, then fail on test failure. |
| # TODO(dhanyaganesh@): Make this less hack-y crbug.com/1034403. |
| if '!informational' in self.tast[0]: |
| cmd += ['-failfortests'] |
| if not need_chroot: |
| # The test runner needs to be pointed to the location of the test files |
| # when we're using those in the SimpleChrome cache. |
| remote_runner_path = os.path.join(tast_bin_dir, 'remote_test_runner') |
| remote_bundle_dir = os.path.join( |
| tast_cache_dir, 'tast-remote-tests-cros', 'usr', 'libexec', 'tast', |
| 'bundles', 'remote') |
| remote_data_dir = os.path.join( |
| tast_cache_dir, 'tast-remote-tests-cros', 'usr', 'share', 'tast', |
| 'data') |
| private_key = (self._device.private_key or |
| self._device.remote.GetAgent().private_key) |
| assert private_key, 'ssh private key not found.' |
| cmd += [ |
| '-remoterunner=%s' % remote_runner_path, |
| '-remotebundledir=%s' % remote_bundle_dir, |
| '-remotedatadir=%s' % remote_data_dir, |
| '-ephemeraldevserver=true', |
| '-keyfile', |
| private_key, |
| ] |
| # Tast may make calls to gsutil during the tests. If we're outside the |
| # chroot, we may not have gsutil on PATH. So push chromite's copy of |
| # gsutil onto path during the test. |
| gsutil_dir = constants.CHROMITE_SCRIPTS_DIR |
| extra_env = {'PATH': os.environ.get('PATH', '') + ':' + gsutil_dir} |
| else: |
| extra_env = None |
| |
| if self.test_timeout > 0: |
| cmd += ['-timeout=%d' % self.test_timeout] |
| # This flag is needed when running Tast tests on VMs. Note that this check |
| # is only true if we're handling VM start-up/tear-down ourselves for the |
| # duration of the test. If the caller has already launched a VM themselves |
| # and has pointed the '--device' arg at it, this check will be false. |
| if self._device.should_start_vm: |
| cmd += ['-extrauseflags=tast_vm'] |
| if self.results_dir: |
| results_dir = self.results_dir |
| if need_chroot: |
| results_dir = path_util.ToChrootPath(self.results_dir) |
| cmd += ['-resultsdir', results_dir] |
| if self.tast_vars: |
| cmd += ['-var=%s' % v for v in self.tast_vars] |
| if self.tast_total_shards: |
| cmd += [ |
| '-totalshards=%d' % self.tast_total_shards, |
| '-shardindex=%d' % self.tast_shard_index |
| ] |
| if self._device.ssh_port: |
| cmd += ['%s:%d' % (self._device.device, self._device.ssh_port)] |
| else: |
| cmd += [self._device.device] |
| cmd += self.tast |
| return cros_build_lib.run( |
| cmd, |
| dryrun=self.dryrun, |
| extra_env=extra_env, |
| # Don't raise an exception if the command fails. |
| check=False, |
| enter_chroot=need_chroot and not cros_build_lib.IsInsideChroot()) |
| |
| def _RunTests(self): |
| """Run tests. |
| |
| Run user-specified tests, catapult tests, tast tests, autotest, or the |
| default, vm_sanity. |
| |
| Returns: |
| Command execution return code. |
| """ |
| if self.remote_cmd: |
| result = self._RunDeviceCmd() |
| elif self.host_cmd: |
| extra_env = {} |
| if self.build_dir: |
| extra_env['CHROMIUM_OUTPUT_DIR'] = self.build_dir |
| # Don't raise an exception if the command fails. |
| result = cros_build_lib.run( |
| self.args, check=False, dryrun=self.dryrun, extra_env=extra_env) |
| elif self.catapult_tests: |
| result = self._RunCatapultTests() |
| elif self.autotest: |
| result = self._RunAutotest() |
| elif self.tast: |
| result = self._RunTastTests() |
| elif self.chrome_test: |
| result = self._RunChromeTest() |
| else: |
| result = self._device.remote_run( |
| ['/usr/local/autotest/bin/vm_sanity.py'], stream_output=True) |
| |
| self._MaybeSaveVMImage(result) |
| self._FetchResults() |
| |
| name = self.args[0] if self.args else 'Test process' |
| logging.info('%s exited with status code %d.', name, result.returncode) |
| |
| return result.returncode |
| |
| def _MaybeSaveVMImage(self, result): |
| """Tells the VM to save its image on shutdown if the test failed. |
| |
| Args: |
| result: A cros_build_lib.CommandResult object from a test run. |
| """ |
| if not self._device.should_start_vm or not self.save_snapshot_on_failure: |
| return |
| if not result.returncode: |
| return |
| osutils.SafeMakedirs(self.results_dest_dir) |
| self._device.SaveVMImageOnShutdown(self.results_dest_dir) |
| |
| def _FetchResults(self): |
| """Fetch results files/directories.""" |
| if not self.results_src: |
| return |
| osutils.SafeMakedirs(self.results_dest_dir) |
| for src in self.results_src: |
| logging.info('Fetching %s to %s', src, self.results_dest_dir) |
| self._device.remote.CopyFromDevice(src=src, dest=self.results_dest_dir, |
| mode='scp', debug_level=logging.INFO) |
| |
| def _RunDeviceCmd(self): |
| """Run a command on the device. |
| |
| Copy src files to /usr/local/cros_test/, change working directory to |
| self.cwd, run the command in self.args, and cleanup. |
| |
| Returns: |
| cros_build_lib.CommandResult object. |
| """ |
| DEST_BASE = '/usr/local/cros_test' |
| files = FileList(self.files, self.files_from) |
| # Copy files, preserving the directory structure. |
| copy_paths = [] |
| for f in files: |
| is_exe = os.path.isfile(f) and os.access(f, os.X_OK) |
| has_exe = False |
| if os.path.isdir(f): |
| if not f.endswith('/'): |
| f += '/' |
| for sub_dir, _, sub_files in os.walk(f): |
| for sub_file in sub_files: |
| if os.access(os.path.join(sub_dir, sub_file), os.X_OK): |
| has_exe = True |
| break |
| if has_exe: |
| break |
| copy_paths.append(chrome_util.Path(f, exe=is_exe or has_exe)) |
| if copy_paths: |
| self._DeployCopyPaths(os.getcwd(), DEST_BASE, copy_paths) |
| |
| # Make cwd an absolute path (if it isn't one) rooted in DEST_BASE. |
| cwd = self.cwd |
| if files and not (cwd and os.path.isabs(cwd)): |
| cwd = os.path.join(DEST_BASE, cwd) if cwd else DEST_BASE |
| self._device.remote_run(['mkdir', '-p', cwd]) |
| |
| if self.as_chronos: |
| # This authorizes the test ssh keys with chronos. |
| self._device.remote_run(['cp', '-r', '/root/.ssh/', |
| '/home/chronos/user/']) |
| if files: |
| # The trailing ':' after the user also changes the group to the user's |
| # primary group. |
| self._device.remote_run(['chown', '-R', 'chronos:', DEST_BASE]) |
| |
| user = 'chronos' if self.as_chronos else None |
| if cwd: |
| # Run the remote command with cwd. |
| cmd = 'cd %s && %s' % (cwd, ' '.join(self.args)) |
| # Pass shell=True because of && in the cmd. |
| result = self._device.remote_run(cmd, stream_output=True, shell=True, |
| remote_user=user) |
| else: |
| result = self._device.remote_run(self.args, stream_output=True, |
| remote_user=user) |
| |
| # Cleanup. |
| if files: |
| self._device.remote_run(['rm', '-rf', DEST_BASE]) |
| |
| return result |
| |
| def _RunChromeTest(self): |
| # Stop UI in case the test needs to grab GPU. |
| self._device.remote_run('stop ui') |
| |
| # Send a user activity ping to powerd to light up the display. |
| self._device.remote_run(['dbus-send', '--system', '--type=method_call', |
| '--dest=org.chromium.PowerManager', |
| '/org/chromium/PowerManager', |
| 'org.chromium.PowerManager.HandleUserActivity', |
| 'int32:0']) |
| |
| # Run test. |
| chrome_src_dir = os.path.dirname(os.path.dirname(self.build_dir)) |
| test_binary = os.path.relpath( |
| os.path.join(self.build_dir, self.chrome_test_target), chrome_src_dir) |
| test_args = self.args[1:] |
| command = 'cd %s && su chronos -c -- "%s %s"' % \ |
| (self.chrome_test_deploy_target_dir, test_binary, ' '.join(test_args)) |
| result = self._device.remote_run(command, stream_output=True) |
| return result |
| |
| |
| def ParseCommandLine(argv): |
| """Parse the command line. |
| |
| Args: |
| argv: Command arguments. |
| |
| Returns: |
| List of parsed options for CrOSTest. |
| """ |
| |
| parser = vm.VM.GetParser() |
| parser.add_argument('--start-vm', action='store_true', default=False, |
| help='Start a new VM before running tests.') |
| parser.add_argument('--catapult-tests', nargs='+', |
| help='Catapult test pattern to run, passed to run_tests.') |
| parser.add_argument('--autotest', nargs='+', |
| help='Autotest test pattern to run, passed to test_that.') |
| parser.add_argument('--tast', nargs='+', |
| help='Tast test pattern to run, passed to tast. ' |
| 'See go/tast-running for patterns.') |
| parser.add_argument('--tast-var', dest='tast_vars', action='append', |
| help='Runtime variables for Tast tests, and the format ' |
| 'are expected to be "key=value" pairs.') |
| parser.add_argument('--tast-shard-index', type=int, default=0, |
| help='Shard index to use when running Tast tests.') |
| parser.add_argument('--tast-total-shards', type=int, default=0, |
| help='Total number of shards when running Tast tests.') |
| parser.add_argument('--chrome-test', action='store_true', default=False, |
| help='Run chrome test on device. The first arg in the ' |
| 'remote command should be the test binary name, such as ' |
| 'interactive_ui_tests. It is used for building and ' |
| 'collecting runtime deps files.') |
| parser.add_argument('--guest', action='store_true', default=False, |
| help='Run tests in incognito mode.') |
| parser.add_argument('--build', action='store_true', default=False, |
| help='Before running tests, build chrome using ninja, ' |
| '--build-dir must be specified.') |
| parser.add_argument('--build-dir', type='path', |
| help='Directory for building and deploying chrome.') |
| parser.add_argument('--flash', action='store_true', default=False, |
| help='Before running tests, flash the device.') |
| parser.add_argument('--public-image', action='store_true', default=False, |
| help='Flash with a public image.') |
| parser.add_argument('--xbuddy', |
| help='xbuddy link to use for flashing the device. Will ' |
| "default to the board's version used in the cros " |
| 'chrome-sdk if available, or "latest" otherwise.') |
| parser.add_argument('--deploy-lacros', action='store_true', default=False, |
| help='Before running tests, deploy lacros-chrome, ' |
| '--build-dir must be specified.') |
| parser.add_argument('--deploy', action='store_true', default=False, |
| help='Before running tests, deploy ash-chrome, ' |
| '--build-dir must be specified.') |
| parser.add_argument('--nostrip', action='store_true', default=False, |
| help="Don't strip symbols from binaries if deploying.") |
| parser.add_argument('--mount', action='store_true', default=False, |
| help='Deploy ash-chrome to the default target directory ' |
| 'and bind it to the default mount directory. Useful for ' |
| 'large ash-chrome binaries.') |
| # type='path' converts a relative path for cwd into an absolute one on the |
| # host, which we don't want. |
| parser.add_argument('--cwd', help='Change working directory. ' |
| 'An absolute path or a path relative to CWD on the host.') |
| parser.add_argument('--files', default=[], action='append', |
| help='Files to scp to the device.') |
| parser.add_argument('--files-from', type='path', |
| help='File with list of files to copy.') |
| parser.add_argument('--results-src', default=[], action='append', |
| help='Files/Directories to copy from ' |
| 'the device into CWD after running the test.') |
| parser.add_argument('--results-dest-dir', type='path', |
| help='Destination directory to copy results to.') |
| parser.add_argument('--remote-cmd', action='store_true', default=False, |
| help='Run a command on the device.') |
| parser.add_argument('--as-chronos', action='store_true', |
| help='Runs the remote test as the chronos user on ' |
| 'the device. Only supported for --remote-cmd tests. ' |
| 'Runs as root if not set.') |
| parser.add_argument('--host-cmd', action='store_true', default=False, |
| help='Run a command on the host.') |
| parser.add_argument('--results-dir', type='path', |
| help='Autotest results directory.') |
| parser.add_argument('--test_that-args', action='append_option_value', |
| help='Args to pass directly to test_that for autotest.') |
| parser.add_argument('--test-timeout', type=int, default=0, |
| help='Timeout for running all tests (for --tast).') |
| parser.add_argument('--save-snapshot-on-failure', action='store_true', |
| default=False, |
| help='Save a snapshot of the VM on test failure to ' |
| 'results-dest-dir.') |
| |
| opts = parser.parse_args(argv) |
| |
| if opts.device and opts.device.port and opts.ssh_port: |
| parser.error('Must not specify SSH port via both --ssh-port and --device.') |
| |
| if opts.chrome_test: |
| if not opts.args: |
| parser.error('Must specify a test command with --chrome-test') |
| |
| if not opts.build_dir: |
| opts.build_dir = os.path.dirname(opts.args[1]) |
| |
| if opts.build or opts.deploy or opts.deploy_lacros: |
| if not opts.build_dir: |
| parser.error('Must specify --build-dir with --build or --deploy.') |
| if not os.path.isdir(opts.build_dir): |
| parser.error('%s is not a directory.' % opts.build_dir) |
| |
| if opts.tast_vars and not opts.tast: |
| parser.error('--tast-var is only applicable to Tast tests.') |
| |
| if opts.deploy and opts.deploy_lacros: |
| parser.error('Cannot deploy lacros-chrome and ash-chrome at the same time.') |
| |
| if opts.results_src: |
| for src in opts.results_src: |
| if not os.path.isabs(src): |
| parser.error('results-src must be absolute.') |
| if not opts.results_dest_dir: |
| parser.error('results-dest-dir must be specified with results-src.') |
| if opts.results_dest_dir: |
| if not opts.results_src: |
| parser.error('results-src must be specified with results-dest-dir.') |
| if os.path.isfile(opts.results_dest_dir): |
| parser.error('results-dest-dir %s is an existing file.' |
| % opts.results_dest_dir) |
| |
| if opts.save_snapshot_on_failure and not opts.results_dest_dir: |
| parser.error('Must specify results-dest-dir with save-snapshot-on-failure') |
| |
| # Ensure command is provided. For e.g. to copy out to the device and run |
| # out/unittest: |
| # cros_run_test --files out --cwd out --cmd -- ./unittest |
| # Treat --cmd as --remote-cmd. |
| opts.remote_cmd = opts.remote_cmd or opts.cmd |
| if (opts.remote_cmd or opts.host_cmd) and len(opts.args) < 2: |
| parser.error('Must specify test command to run.') |
| if opts.as_chronos and not opts.remote_cmd: |
| parser.error('as-chronos only supported when running test commands.') |
| # Verify additional args. |
| if opts.args: |
| if not opts.remote_cmd and not opts.host_cmd and not opts.chrome_test: |
| parser.error('Additional args may be specified with either ' |
| '--remote-cmd or --host-cmd or --chrome-test: %s' % |
| opts.args) |
| if opts.args[0] != '--': |
| parser.error("Additional args must start with '--': %s" % opts.args) |
| |
| # Verify CWD. |
| if opts.cwd: |
| if opts.cwd.startswith('..'): |
| parser.error('cwd cannot start with ..') |
| if not os.path.isabs(opts.cwd) and not opts.files and not opts.files_from: |
| parser.error('cwd must be an absolute path if ' |
| '--files or --files-from is not specified') |
| |
| # Verify files. |
| if opts.files_from: |
| if opts.files: |
| parser.error('--files and --files-from cannot both be specified') |
| if not os.path.isfile(opts.files_from): |
| parser.error('%s is not a file' % opts.files_from) |
| files = FileList(opts.files, opts.files_from) |
| for f in files: |
| if os.path.isabs(f): |
| parser.error('%s should be a relative path' % f) |
| # Restrict paths to under CWD on the host. See crbug.com/829612. |
| if f.startswith('..'): |
| parser.error('%s cannot start with ..' % f) |
| if not os.path.exists(f): |
| parser.error('%s does not exist' % f) |
| |
| # Verify Tast. |
| if opts.tast_shard_index or opts.tast_total_shards: |
| if not opts.tast: |
| parser.error('Can only specify --tast-total-shards and ' |
| '--tast-shard-index with --tast.') |
| if opts.tast_shard_index >= opts.tast_total_shards: |
| parser.error('Shard index must be < total shards.') |
| |
| return opts |
| |
| |
| def FileList(files, files_from): |
| """Get list of files from command line args --files and --files-from. |
| |
| Args: |
| files: files specified directly on the command line. |
| files_from: files specified in a file. |
| |
| Returns: |
| Contents of files_from if it exists, otherwise files. |
| """ |
| if files_from and os.path.isfile(files_from): |
| with open(files_from) as f: |
| files = [line.rstrip() for line in f] |
| return files |