1#!/usr/bin/python2 2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6from __future__ import absolute_import 7from __future__ import division 8from __future__ import print_function 9 10import argparse 11import os 12import signal 13import subprocess 14import sys 15 16import logging 17# Turn the logging level to INFO before importing other autotest 18# code, to avoid having failed import logging messages confuse the 19# test_that user. 20logging.basicConfig(level=logging.INFO) 21 22import common 23from autotest_lib.client.common_lib import error, logging_manager 24from autotest_lib.server import server_logging_config 25from autotest_lib.server.cros.dynamic_suite import constants 26from autotest_lib.server.hosts import factory 27from autotest_lib.site_utils import test_runner_utils 28 29 30_QUICKMERGE_SCRIPTNAME = '/mnt/host/source/chromite/bin/autotest_quickmerge' 31 32 33def _get_board_from_host(remote): 34 """Get the board of the remote host. 35 36 @param remote: string representing the IP of the remote host. 37 38 @return: A string representing the board of the remote host. 39 """ 40 logging.info('Board unspecified, attempting to determine board from host.') 41 host = factory.create_host(remote) 42 try: 43 board = host.get_board().replace(constants.BOARD_PREFIX, '') 44 except error.AutoservRunError: 45 raise test_runner_utils.TestThatRunError( 46 'Cannot determine board, please specify a --board option.') 47 logging.info('Detected host board: %s', board) 48 return board 49 50 51def _get_model_from_host(remote): 52 """Get the model of the remote host. 53 54 @param remote: string representing the IP of the remote host. 55 56 @return: A string representing the board of the remote host. 57 """ 58 logging.info('Model unspecified, attempting to determine model from host.') 59 host = factory.create_host(remote) 60 try: 61 model = host.get_platform() 62 except error.AutoservRunError: 63 raise test_runner_utils.TestThatRunError( 64 'Cannot determine model, please specify a --model option.') 65 logging.info('Detected host model: %s', model) 66 return model 67 68 69def validate_arguments(arguments): 70 """ 71 Validates parsed arguments. 72 73 @param arguments: arguments object, as parsed by ParseArguments 74 @raises: ValueError if arguments were invalid. 75 """ 76 if arguments.remote == ':lab:': 77 if arguments.args: 78 raise ValueError('--args flag not supported when running against ' 79 ':lab:') 80 if arguments.pretend: 81 raise ValueError('--pretend flag not supported when running ' 82 'against :lab:') 83 if arguments.ssh_verbosity: 84 raise ValueError('--ssh_verbosity flag not supported when running ' 85 'against :lab:') 86 if not arguments.board or arguments.build == test_runner_utils.NO_BUILD: 87 raise ValueError('--board and --build are both required when ' 88 'running against :lab:') 89 else: 90 if arguments.web: 91 raise ValueError('--web flag not supported when running locally') 92 93 94def parse_arguments(argv): 95 """ 96 Parse command line arguments 97 98 @param argv: argument list to parse 99 @returns: parsed arguments 100 @raises SystemExit if arguments are malformed, or required arguments 101 are not present. 102 """ 103 return _parse_arguments_internal(argv)[0] 104 105 106def _parse_arguments_internal(argv): 107 """ 108 Parse command line arguments 109 110 @param argv: argument list to parse 111 @returns: tuple of parsed arguments and argv suitable for remote runs 112 @raises SystemExit if arguments are malformed, or required arguments 113 are not present. 114 """ 115 local_parser, remote_argv = parse_local_arguments(argv) 116 117 parser = argparse.ArgumentParser(description='Run remote tests.', 118 parents=[local_parser]) 119 120 parser.add_argument('remote', metavar='REMOTE', 121 help='hostname[:port] for remote device. Specify ' 122 ':lab: to run in test lab. When tests are run in ' 123 'the lab, test_that will use the client autotest ' 124 'package for the build specified with --build, ' 125 'and the lab server code rather than local ' 126 'changes.') 127 test_runner_utils.add_common_args(parser) 128 parser.add_argument('-b', '--board', metavar='BOARD', 129 action='store', 130 help='Board for which the test will run. ' 131 'Default: %(default)s') 132 parser.add_argument('-m', 133 '--model', 134 metavar='MODEL', 135 help='Specific model the test will run against. ' 136 'Matches the model:FAKE_MODEL label for the host.') 137 parser.add_argument('-i', '--build', metavar='BUILD', 138 default=test_runner_utils.NO_BUILD, 139 help='Build to test. Device will be reimaged if ' 140 'necessary. Omit flag to skip reimage and test ' 141 'against already installed DUT image. Examples: ' 142 'link-paladin/R34-5222.0.0-rc2, ' 143 'lumpy-release/R34-5205.0.0') 144 parser.add_argument('-p', '--pool', metavar='POOL', default='suites', 145 help='Pool to use when running tests in the lab. ' 146 'Default is "suites"') 147 parser.add_argument('--autotest_dir', metavar='AUTOTEST_DIR', 148 help='Use AUTOTEST_DIR instead of normal board sysroot ' 149 'copy of autotest, and skip the quickmerge step.') 150 parser.add_argument('--no-quickmerge', action='store_true', default=False, 151 dest='no_quickmerge', 152 help='Skip the quickmerge step and use the sysroot ' 153 'as it currently is. May result in un-merged ' 154 'source tree changes not being reflected in the ' 155 'run. If using --autotest_dir, this flag is ' 156 'automatically applied.') 157 parser.add_argument('--allow-chrome-crashes', 158 action='store_true', 159 default=False, 160 dest='allow_chrome_crashes', 161 help='Ignore chrome crashes when producing test ' 162 'report. This flag gets passed along to the ' 163 'report generation tool.') 164 parser.add_argument('--ssh_private_key', action='store', 165 default=test_runner_utils.TEST_KEY_PATH, 166 help='Path to the private ssh key.') 167 return parser.parse_args(argv), remote_argv 168 169 170def parse_local_arguments(argv): 171 """ 172 Strips out arguments that are not to be passed through to runs. 173 174 Add any arguments that should not be passed to remote test_that runs here. 175 176 @param argv: argument list to parse. 177 @returns: tuple of local argument parser and remaining argv. 178 """ 179 parser = argparse.ArgumentParser(add_help=False) 180 parser.add_argument('-w', '--web', dest='web', default=None, 181 help='Address of a webserver to receive test requests.') 182 parser.add_argument('-x', '--max_runtime_mins', type=int, 183 dest='max_runtime_mins', default=20, 184 help='Default time allowed for the tests to complete.') 185 parser.add_argument('--no-retries', '--no-retry', 186 dest='retry', action='store_false', default=True, 187 help='For local runs only, ignore any retries ' 188 'specified in the control files.') 189 _, remaining_argv = parser.parse_known_args(argv) 190 return parser, remaining_argv 191 192 193def perform_bootstrap_into_autotest_root(arguments, autotest_path, argv): 194 """ 195 Perfoms a bootstrap to run test_that from the |autotest_path|. 196 197 This function is to be called from test_that's main() script, when 198 test_that is executed from the source tree location. It runs 199 autotest_quickmerge to update the sysroot unless arguments.no_quickmerge 200 is set. It then executes and waits on the version of test_that.py 201 in |autotest_path|. 202 203 @param arguments: A parsed arguments object, as returned from 204 test_that.parse_arguments(...). 205 @param autotest_path: Full absolute path to the autotest root directory. 206 @param argv: The arguments list, as passed to main(...) 207 208 @returns: The return code of the test_that script that was executed in 209 |autotest_path|. 210 """ 211 logging_manager.configure_logging( 212 server_logging_config.ServerLoggingConfig(), 213 use_console=True, 214 verbose=arguments.debug) 215 if arguments.no_quickmerge: 216 logging.info('Skipping quickmerge step.') 217 else: 218 logging.info('Running autotest_quickmerge step.') 219 command = [_QUICKMERGE_SCRIPTNAME, '--board='+arguments.board] 220 s = subprocess.Popen(command, 221 stdout=subprocess.PIPE, 222 stderr=subprocess.STDOUT) 223 for message in iter(s.stdout.readline, b''): 224 logging.info('quickmerge| %s', message.strip()) 225 return_code = s.wait() 226 if return_code: 227 raise test_runner_utils.TestThatRunError( 228 'autotest_quickmerge failed with error code %s.' % 229 return_code) 230 231 logging.info('Re-running test_that script in %s copy of autotest.', 232 autotest_path) 233 script_command = os.path.join(autotest_path, 'site_utils', 234 'test_that.py') 235 if not os.path.exists(script_command): 236 raise test_runner_utils.TestThatRunError( 237 'Unable to bootstrap to autotest root, %s not found.' % 238 script_command) 239 proc = None 240 def resend_sig(signum, stack_frame): 241 #pylint: disable-msg=C0111 242 if proc: 243 proc.send_signal(signum) 244 signal.signal(signal.SIGINT, resend_sig) 245 signal.signal(signal.SIGTERM, resend_sig) 246 247 proc = subprocess.Popen([script_command] + argv) 248 249 return proc.wait() 250 251 252def _main_for_local_run(argv, arguments): 253 """ 254 Effective entry point for local test_that runs. 255 256 @param argv: Script command line arguments. 257 @param arguments: Parsed command line arguments. 258 """ 259 if not os.path.exists('/etc/cros_chroot_version'): 260 print('For local runs, script must be run inside chroot.', file=sys.stderr) 261 return 1 262 263 results_directory = test_runner_utils.create_results_directory( 264 arguments.results_dir, arguments.board) 265 test_runner_utils.add_ssh_identity(results_directory, 266 arguments.ssh_private_key) 267 arguments.results_dir = results_directory 268 269 # If the board and/or model is not specified through --board and/or 270 # --model, and is not set in the default_board file, determine the board by 271 # ssh-ing into the host. Also prepend it to argv so we can re-use it when we 272 # run test_that from the sysroot. 273 if arguments.board is None: 274 arguments.board = _get_board_from_host(arguments.remote) 275 argv = ['--board=%s' % (arguments.board,)] + argv 276 277 if arguments.model is None: 278 arguments.model = _get_model_from_host(arguments.remote) 279 argv = ['--model=%s' % (arguments.model, )] + argv 280 281 if arguments.autotest_dir: 282 autotest_path = arguments.autotest_dir 283 arguments.no_quickmerge = True 284 else: 285 sysroot_path = os.path.join('/build', arguments.board, '') 286 287 if not os.path.exists(sysroot_path): 288 print(('%s does not exist. Have you run ' 289 'setup_board?' % sysroot_path), file=sys.stderr) 290 return 1 291 292 path_ending = 'usr/local/build/autotest' 293 autotest_path = os.path.join(sysroot_path, path_ending) 294 295 site_utils_path = os.path.join(autotest_path, 'site_utils') 296 297 if not os.path.exists(autotest_path): 298 print(('%s does not exist. Have you run ' 299 'build_packages? Or if you are using ' 300 '--autotest_dir, make sure it points to ' 301 'a valid autotest directory.' % autotest_path), file=sys.stderr) 302 return 1 303 304 realpath = os.path.realpath(__file__) 305 site_utils_path = os.path.realpath(site_utils_path) 306 307 # If we are not running the sysroot version of script, perform 308 # a quickmerge if necessary and then re-execute 309 # the sysroot version of script with the same arguments. 310 if os.path.dirname(realpath) != site_utils_path: 311 return perform_bootstrap_into_autotest_root( 312 arguments, autotest_path, argv) 313 else: 314 return test_runner_utils.perform_run_from_autotest_root( 315 autotest_path, 316 argv, 317 arguments.tests, 318 arguments.remote, 319 build=arguments.build, 320 board=arguments.board, 321 model=arguments.model, 322 args=arguments.args, 323 ignore_deps=not arguments.enforce_deps, 324 results_directory=results_directory, 325 ssh_verbosity=arguments.ssh_verbosity, 326 ssh_options=arguments.ssh_options, 327 iterations=arguments.iterations, 328 fast_mode=arguments.fast_mode, 329 debug=arguments.debug, 330 allow_chrome_crashes=arguments.allow_chrome_crashes, 331 pretend=arguments.pretend, 332 job_retry=arguments.retry) 333 334 335def _main_for_lab_run(argv, arguments): 336 """ 337 Effective entry point for lab test_that runs. 338 339 @param argv: Script command line arguments. 340 @param arguments: Parsed command line arguments. 341 """ 342 autotest_path = os.path.realpath(os.path.join( 343 os.path.dirname(os.path.realpath(__file__)), 344 '..', 345 )) 346 command = [os.path.join(autotest_path, 'site_utils', 347 'run_suite.py'), 348 '--board=%s' % (arguments.board,), 349 '--build=%s' % (arguments.build,), 350 '--model=%s' % (arguments.model,), 351 '--suite_name=%s' % 'test_that_wrapper', 352 '--pool=%s' % (arguments.pool,), 353 '--max_runtime_mins=%s' % str(arguments.max_runtime_mins), 354 '--suite_args=%s' 355 % repr({'tests': _suite_arg_tests(argv)})] 356 if arguments.web: 357 command.extend(['--web=%s' % (arguments.web,)]) 358 logging.info('About to start lab suite with command %s.', command) 359 return subprocess.call(command) 360 361 362def _suite_arg_tests(argv): 363 """ 364 Construct a list of tests to pass into suite_args. 365 366 This is passed in suite_args to run_suite for running a test in the 367 lab. 368 369 @param argv: Remote Script command line arguments. 370 """ 371 arguments = parse_arguments(argv) 372 return arguments.tests 373 374 375def main(argv): 376 """ 377 Entry point for test_that script. 378 379 @param argv: arguments list 380 """ 381 arguments, remote_argv = _parse_arguments_internal(argv) 382 try: 383 validate_arguments(arguments) 384 except ValueError as err: 385 print(('Invalid arguments. %s' % str(err)), file=sys.stderr) 386 return 1 387 388 if arguments.remote == ':lab:': 389 return _main_for_lab_run(remote_argv, arguments) 390 else: 391 return _main_for_local_run(argv, arguments) 392 393 394if __name__ == '__main__': 395 sys.exit(main(sys.argv[1:])) 396