test_that.py revision 30322f95d61f6a2cffada92472729d3f2a926f6b
1#!/usr/bin/python 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 6import argparse 7import logging 8import os 9import re 10import signal 11import stat 12import subprocess 13import sys 14import tempfile 15import threading 16 17import common 18from autotest_lib.client.common_lib.cros import dev_server, retry 19from autotest_lib.server.cros.dynamic_suite import suite 20from autotest_lib.server.cros.dynamic_suite import constants 21from autotest_lib.server import autoserv_utils 22 23try: 24 from chromite.lib import cros_build_lib 25except ImportError: 26 print 'Unable to import chromite.' 27 print 'This script must be either:' 28 print ' - Be run in the chroot.' 29 print ' - (not yet supported) be run after running ' 30 print ' ../utils/build_externals.py' 31 32_autoserv_proc = None 33_sigint_handler_lock = threading.Lock() 34 35_AUTOSERV_SIGINT_TIMEOUT_SECONDS = 5 36_NO_BOARD = 'ad_hoc_board' 37_NO_BUILD = 'ad_hoc_build' 38 39_QUICKMERGE_SCRIPTNAME = '/mnt/host/source/chromite/bin/autotest_quickmerge' 40 41_TEST_REPORT_SCRIPTNAME = '/usr/bin/generate_test_report' 42 43 44def schedule_local_suite(autotest_path, suite_name, afe, build=_NO_BUILD, 45 board=_NO_BOARD, results_directory=None): 46 """ 47 Schedule a suite against a mock afe object, for a local suite run. 48 @param autotest_path: Absolute path to autotest (in sysroot). 49 @param suite_name: Name of suite to schedule. 50 @param afe: afe object to schedule against (typically a directAFE) 51 @param build: Build to schedule suite for. 52 @param board: Board to schedule suite for. 53 @param results_directory: Absolute path of directory to store results in. 54 (results will be stored in subdirectory of this). 55 @returns: The number of tests scheduled. 56 """ 57 fs_getter = suite.Suite.create_fs_getter(autotest_path) 58 devserver = dev_server.ImageServer('') 59 my_suite = suite.Suite.create_from_name(suite_name, build, board, 60 devserver, fs_getter, afe=afe, ignore_deps=True, 61 results_dir=results_directory) 62 if len(my_suite.tests) == 0: 63 raise ValueError('Suite named %s does not exist, or contains no ' 64 'tests.' % suite_name) 65 my_suite.schedule(lambda x: None) # Schedule tests, discard record calls. 66 return len(my_suite.tests) 67 68 69def schedule_local_test(autotest_path, test_name, afe, build=_NO_BUILD, 70 board=_NO_BOARD, results_directory=None): 71 #temporarily disabling pylint 72 #pylint: disable-msg=C0111 73 """ 74 Schedule an individual test against a mock afe object, for a local run. 75 @param autotest_path: Absolute path to autotest (in sysroot). 76 @param test_name: Name of test to schedule. 77 @param afe: afe object to schedule against (typically a directAFE) 78 @param build: Build to schedule suite for. 79 @param board: Board to schedule suite for. 80 @param results_directory: Absolute path of directory to store results in. 81 (results will be stored in subdirectory of this). 82 @returns: The number of tests scheduled (may be >1 if there are 83 multiple tests with the same name). 84 """ 85 fs_getter = suite.Suite.create_fs_getter(autotest_path) 86 devserver = dev_server.ImageServer('') 87 predicates = [suite.Suite.test_name_equals_predicate(test_name)] 88 suite_name = 'suite_' + test_name 89 my_suite = suite.Suite.create_from_predicates(predicates, build, board, 90 devserver, fs_getter, afe=afe, name=suite_name, ignore_deps=True, 91 results_dir=results_directory) 92 if len(my_suite.tests) == 0: 93 raise ValueError('No tests named %s.' % test_name) 94 my_suite.schedule(lambda x: None) # Schedule tests, discard record calls. 95 return len(my_suite.tests) 96 97 98def run_job(job, host, sysroot_autotest_path, results_directory, fast_mode, 99 id_digits=1, args=None): 100 """ 101 Shell out to autoserv to run an individual test job. 102 103 @param job: A Job object containing the control file contents and other 104 relevent metadata for this test. 105 @param host: Hostname of DUT to run test against. 106 @param sysroot_autotest_path: Absolute path of autotest directory. 107 @param results_directory: Absolute path of directory to store results in. 108 (results will be stored in subdirectory of this). 109 @param fast_mode: bool to use fast mode (disables slow autotest features). 110 @param id_digits: The minimum number of digits that job ids should be 111 0-padded to when formatting as a string for results 112 directory. 113 @param args: String that should be passed as args parameter to autoserv, 114 and then ultimitely to test itself. 115 @returns: Absolute path of directory where results were stored. 116 """ 117 with tempfile.NamedTemporaryFile() as temp_file: 118 temp_file.write(job.control_file) 119 temp_file.flush() 120 results_directory = os.path.join(results_directory, 121 'results-%0*d' % (id_digits, job.id)) 122 extra_args = [temp_file.name] 123 if args: 124 extra_args.extend(['--args', args]) 125 126 command = autoserv_utils.autoserv_run_job_command( 127 os.path.join(sysroot_autotest_path, 'server'), 128 machines=host, job=job, verbose=False, 129 results_directory=results_directory, 130 fast_mode=fast_mode, 131 extra_args=extra_args) 132 global _autoserv_proc 133 _autoserv_proc = subprocess.Popen(command) 134 _autoserv_proc.wait() 135 _autoserv_proc = None 136 return results_directory 137 138 139def setup_local_afe(): 140 """ 141 Setup a local afe database and return a direct_afe object to access it. 142 143 @returns: A autotest_lib.frontend.afe.direct_afe instance. 144 """ 145 # This import statement is delayed until now rather than running at 146 # module load time, because it kicks off a local sqlite :memory: backed 147 # database, and we don't need that unless we are doing a local run. 148 from autotest_lib.frontend import setup_django_lite_environment 149 from autotest_lib.frontend.afe import direct_afe 150 return direct_afe.directAFE() 151 152 153def perform_local_run(afe, autotest_path, tests, remote, fast_mode, 154 build=_NO_BUILD, board=_NO_BOARD, args=None): 155 """ 156 @param afe: A direct_afe object used to interact with local afe database. 157 @param autotest_path: Absolute path of sysroot installed autotest. 158 @param tests: List of strings naming tests and suites to run. Suite strings 159 should be formed like "suite:smoke". 160 @param remote: Remote hostname. 161 @param fast_mode: bool to use fast mode (disables slow autotest features). 162 @param build: String specifying build for local run. 163 @param board: String specifyinb board for local run. 164 @param args: String that should be passed as args parameter to autoserv, 165 and then ultimitely to test itself. 166 167 @returns: directory in which results are stored. 168 """ 169 afe.create_label(constants.VERSION_PREFIX + build) 170 afe.create_label(board) 171 afe.create_host(remote) 172 173 results_directory = tempfile.mkdtemp(prefix='test_that_results_') 174 os.chmod(results_directory, stat.S_IWOTH | stat.S_IROTH | stat.S_IXOTH) 175 logging.info('Running jobs. Results will be placed in %s', 176 results_directory) 177 # Schedule tests / suites in local afe 178 for test in tests: 179 suitematch = re.match(r'suite:(.*)', test) 180 if suitematch: 181 suitename = suitematch.group(1) 182 logging.info('Scheduling suite %s...', suitename) 183 ntests = schedule_local_suite(autotest_path, suitename, afe, 184 build=build, board=board, 185 results_directory=results_directory) 186 else: 187 logging.info('Scheduling test %s...', test) 188 ntests = schedule_local_test(autotest_path, test, afe, 189 build=build, board=board, 190 results_directory=results_directory) 191 logging.info('... scheduled %s tests.', ntests) 192 193 if not afe.get_jobs(): 194 logging.info('No jobs scheduled. End of local run.') 195 return results_directory 196 197 last_job_id = afe.get_jobs()[-1].id 198 job_id_digits=len(str(last_job_id)) 199 for job in afe.get_jobs(): 200 run_job(job, remote, autotest_path, results_directory, fast_mode, 201 job_id_digits, args) 202 203 return results_directory 204 205 206def validate_arguments(arguments): 207 """ 208 Validates parsed arguments. 209 210 @param arguments: arguments object, as parsed by ParseArguments 211 @raises: ValueError if arguments were invalid. 212 """ 213 if arguments.build: 214 raise ValueError('-i/--build flag not yet supported.') 215 216 if not arguments.board: 217 raise ValueError('Board autodetection not yet supported. ' 218 '--board required.') 219 220 if arguments.remote == ':lab:': 221 raise ValueError('Running tests in test lab not yet supported.') 222 if arguments.args: 223 raise ValueError('--args flag not supported when running against ' 224 ':lab:') 225 226 227def parse_arguments(argv): 228 """ 229 Parse command line arguments 230 231 @param argv: argument list to parse 232 @returns: parsed arguments. 233 """ 234 parser = argparse.ArgumentParser(description='Run remote tests.') 235 236 parser.add_argument('remote', metavar='REMOTE', 237 help='hostname[:port] for remote device. Specify ' 238 ':lab: to run in test lab, or :vm:PORT_NUMBER to ' 239 'run in vm.') 240 parser.add_argument('tests', nargs='+', metavar='TEST', 241 help='Run given test(s). Use suite:SUITE to specify ' 242 'test suite.') 243 parser.add_argument('-b', '--board', metavar='BOARD', 244 action='store', 245 help='Board for which the test will run.') 246 parser.add_argument('-i', '--build', metavar='BUILD', 247 help='Build to test. Device will be reimaged if ' 248 'necessary. Omit flag to skip reimage and test ' 249 'against already installed DUT image.') 250 parser.add_argument('--fast', action='store_true', dest='fast_mode', 251 default=False, 252 help='Enable fast mode. This will cause test_that to ' 253 'skip time consuming steps like sysinfo and ' 254 'collecting crash information.') 255 parser.add_argument('--args', metavar='ARGS', 256 help='Argument string to pass through to test. Only ' 257 'supported for runs against a local DUT.') 258 259 return parser.parse_args(argv) 260 261 262def sigint_handler(signum, stack_frame): 263 #pylint: disable-msg=C0111 264 """Handle SIGINT or SIGTERM to a local test_that run. 265 266 This handler sends a SIGINT to the running autoserv process, 267 if one is running, giving it up to 5 seconds to clean up and exit. After 268 the timeout elapses, autoserv is killed. In either case, after autoserv 269 exits then this process exits with status 1. 270 """ 271 # If multiple signals arrive before handler is unset, ignore duplicates 272 if not _sigint_handler_lock.acquire(False): 273 return 274 try: 275 # Ignore future signals by unsetting handler. 276 signal.signal(signal.SIGINT, signal.SIG_IGN) 277 signal.signal(signal.SIGTERM, signal.SIG_IGN) 278 279 logging.warning('Received SIGINT or SIGTERM. Cleaning up and exiting.') 280 if _autoserv_proc: 281 logging.warning('Sending SIGINT to autoserv process. Waiting up ' 282 'to %s seconds for cleanup.', 283 _AUTOSERV_SIGINT_TIMEOUT_SECONDS) 284 _autoserv_proc.send_signal(signal.SIGINT) 285 timed_out, _ = retry.timeout(_autoserv_proc.wait, 286 timeout_sec=_AUTOSERV_SIGINT_TIMEOUT_SECONDS) 287 if timed_out: 288 _autoserv_proc.kill() 289 logging.warning('Timed out waiting for autoserv to handle ' 290 'SIGINT. Killed autoserv.') 291 finally: 292 _sigint_handler_lock.release() # this is not really necessary? 293 sys.exit(1) 294 295 296def main(argv): 297 """ 298 Entry point for test_that script. 299 @param argv: arguments list 300 """ 301 if not cros_build_lib.IsInsideChroot(): 302 logging.error('Script must be invoked inside the chroot.') 303 return 1 304 305 logging.getLogger('').setLevel(logging.INFO) 306 307 arguments = parse_arguments(argv) 308 try: 309 validate_arguments(arguments) 310 except ValueError as err: 311 logging.error('Invalid arguments. %s', err.message) 312 return 1 313 314 # TODO: Determine the following string programatically. 315 # (same TODO applied to autotest_quickmerge) 316 sysroot_path = os.path.join('/build', arguments.board, '') 317 sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local', 318 'autotest', '') 319 sysroot_site_utils_path = os.path.join(sysroot_autotest_path, 320 'site_utils') 321 322 if not os.path.exists(sysroot_path): 323 logging.error('%s does not exist. Have you run setup_board?', 324 sysroot_path) 325 return 1 326 if not os.path.exists(sysroot_autotest_path): 327 logging.error('%s does not exist. Have you run build_packages?', 328 sysroot_autotest_path) 329 return 1 330 331 # If we are not running the sysroot version of script, perform 332 # a quickmerge if necessary and then re-execute 333 # the sysroot version of script with the same arguments. 334 realpath = os.path.realpath(__file__) 335 if os.path.dirname(realpath) != sysroot_site_utils_path: 336 subprocess.call([_QUICKMERGE_SCRIPTNAME, '--board='+arguments.board]) 337 338 script_command = os.path.join(sysroot_site_utils_path, 339 os.path.basename(realpath)) 340 proc = None 341 def resend_sig(signum, stack_frame): 342 #pylint: disable-msg=C0111 343 if proc: 344 proc.send_signal(signum) 345 signal.signal(signal.SIGINT, resend_sig) 346 signal.signal(signal.SIGTERM, resend_sig) 347 348 proc = subprocess.Popen([script_command] + argv) 349 350 return proc.wait() 351 352 # Hard coded to True temporarily. This will eventually be parsed to false 353 # if we are doing a run in the test lab. 354 local_run = True 355 356 signal.signal(signal.SIGINT, sigint_handler) 357 signal.signal(signal.SIGTERM, sigint_handler) 358 359 if local_run: 360 afe = setup_local_afe() 361 res_dir= perform_local_run(afe, sysroot_autotest_path, arguments.tests, 362 arguments.remote, arguments.fast_mode, 363 args=arguments.args) 364 final_result = subprocess.call([_TEST_REPORT_SCRIPTNAME, res_dir]) 365 logging.info('Finished running tests. Results can be found in %s', 366 res_dir) 367 return final_result 368 369 370if __name__ == '__main__': 371 sys.exit(main(sys.argv[1:])) 372