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