1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import abc 6import datetime 7import difflib 8import functools 9import hashlib 10import logging 11import operator 12import os 13import re 14import sys 15import warnings 16 17import common 18 19from autotest_lib.frontend.afe.json_rpc import proxy 20from autotest_lib.client.common_lib import control_data 21from autotest_lib.client.common_lib import enum 22from autotest_lib.client.common_lib import error 23from autotest_lib.client.common_lib import global_config 24from autotest_lib.client.common_lib import priorities 25from autotest_lib.client.common_lib import time_utils 26from autotest_lib.client.common_lib import utils 27from autotest_lib.frontend.afe.json_rpc import proxy 28from autotest_lib.server.cros import provision 29from autotest_lib.server.cros.dynamic_suite import constants 30from autotest_lib.server.cros.dynamic_suite import control_file_getter 31from autotest_lib.server.cros.dynamic_suite import frontend_wrappers 32from autotest_lib.server.cros.dynamic_suite import job_status 33from autotest_lib.server.cros.dynamic_suite import tools 34from autotest_lib.server.cros.dynamic_suite.job_status import Status 35 36try: 37 from chromite.lib import boolparse_lib 38 from chromite.lib import cros_logging as logging 39except ImportError: 40 print 'Unable to import chromite.' 41 print 'This script must be either:' 42 print ' - Be run in the chroot.' 43 print ' - (not yet supported) be run after running ' 44 print ' ../utils/build_externals.py' 45 46_FILE_BUG_SUITES = ['au', 'bvt', 'bvt-cq', 'bvt-inline', 'paygen_au_beta', 47 'paygen_au_canary', 'paygen_au_dev', 'paygen_au_stable', 48 'sanity', 'push_to_prod'] 49_AUTOTEST_DIR = global_config.global_config.get_config_value( 50 'SCHEDULER', 'drone_installation_directory') 51ENABLE_CONTROLS_IN_BATCH = global_config.global_config.get_config_value( 52 'CROS', 'enable_getting_controls_in_batch', type=bool, default=False) 53 54class RetryHandler(object): 55 """Maintain retry information. 56 57 @var _retry_map: A dictionary that stores retry history. 58 The key is afe job id. The value is a dictionary. 59 {job_id: {'state':RetryHandler.States, 'retry_max':int}} 60 - state: 61 The retry state of a job. 62 NOT_ATTEMPTED: 63 We haven't done anything about the job. 64 ATTEMPTED: 65 We've made an attempt to schedule a retry job. The 66 scheduling may or may not be successful, e.g. 67 it might encounter an rpc error. Note failure 68 in scheduling a retry is different from a retry job failure. 69 For each job, we only attempt to schedule a retry once. 70 For example, assume we have a test with JOB_RETRIES=5 and 71 its second retry job failed. When we attempt to create 72 a third retry job to retry the second, we hit an rpc 73 error. In such case, we will give up on all following 74 retries. 75 RETRIED: 76 A retry job has already been successfully 77 scheduled. 78 - retry_max: 79 The maximum of times the job can still 80 be retried, taking into account retries 81 that have occurred. 82 @var _retry_level: A retry might be triggered only if the result 83 is worse than the level. 84 @var _max_retries: Maximum retry limit at suite level. 85 Regardless how many times each individual test 86 has been retried, the total number of retries happening in 87 the suite can't exceed _max_retries. 88 """ 89 90 States = enum.Enum('NOT_ATTEMPTED', 'ATTEMPTED', 'RETRIED', 91 start_value=1, step=1) 92 93 def __init__(self, initial_jobs_to_tests, retry_level='WARN', 94 max_retries=None): 95 """Initialize RetryHandler. 96 97 @param initial_jobs_to_tests: A dictionary that maps a job id to 98 a ControlData object. This dictionary should contain 99 jobs that are originally scheduled by the suite. 100 @param retry_level: A retry might be triggered only if the result is 101 worse than the level. 102 @param max_retries: Integer, maxmium total retries allowed 103 for the suite. Default to None, no max. 104 """ 105 self._retry_map = {} 106 self._retry_level = retry_level 107 self._max_retries = (max_retries 108 if max_retries is not None else sys.maxint) 109 for job_id, test in initial_jobs_to_tests.items(): 110 if test.job_retries > 0: 111 self._add_job(new_job_id=job_id, 112 retry_max=test.job_retries) 113 114 115 def _add_job(self, new_job_id, retry_max): 116 """Add a newly-created job to the retry map. 117 118 @param new_job_id: The afe_job_id of a newly created job. 119 @param retry_max: The maximum of times that we could retry 120 the test if the job fails. 121 122 @raises ValueError if new_job_id is already in retry map. 123 124 """ 125 if new_job_id in self._retry_map: 126 raise ValueError('add_job called when job is already in retry map.') 127 128 self._retry_map[new_job_id] = { 129 'state': self.States.NOT_ATTEMPTED, 130 'retry_max': retry_max} 131 132 133 def _suite_max_reached(self): 134 """Return whether maximum retry limit for a suite has been reached.""" 135 return self._max_retries <= 0 136 137 138 def add_retry(self, old_job_id, new_job_id): 139 """Record a retry. 140 141 Update retry map with the retry information. 142 143 @param old_job_id: The afe_job_id of the job that is retried. 144 @param new_job_id: The afe_job_id of the retry job. 145 146 @raises KeyError if old_job_id isn't in the retry map. 147 @raises ValueError if we have already retried or made an attempt 148 to retry the old job. 149 150 """ 151 old_record = self._retry_map[old_job_id] 152 if old_record['state'] != self.States.NOT_ATTEMPTED: 153 raise ValueError( 154 'We have already retried or attempted to retry job %d' % 155 old_job_id) 156 old_record['state'] = self.States.RETRIED 157 self._add_job(new_job_id=new_job_id, 158 retry_max=old_record['retry_max'] - 1) 159 self._max_retries -= 1 160 161 162 def set_attempted(self, job_id): 163 """Set the state of the job to ATTEMPTED. 164 165 @param job_id: afe_job_id of a job. 166 167 @raises KeyError if job_id isn't in the retry map. 168 @raises ValueError if the current state is not NOT_ATTEMPTED. 169 170 """ 171 current_state = self._retry_map[job_id]['state'] 172 if current_state != self.States.NOT_ATTEMPTED: 173 # We are supposed to retry or attempt to retry each job 174 # only once. Raise an error if this is not the case. 175 raise ValueError('Unexpected state transition: %s -> %s' % 176 (self.States.get_string(current_state), 177 self.States.get_string(self.States.ATTEMPTED))) 178 else: 179 self._retry_map[job_id]['state'] = self.States.ATTEMPTED 180 181 182 def has_following_retry(self, result): 183 """Check whether there will be a following retry. 184 185 We have the following cases for a given job id (result.id), 186 - no retry map entry -> retry not required, no following retry 187 - has retry map entry: 188 - already retried -> has following retry 189 - has not retried 190 (this branch can be handled by checking should_retry(result)) 191 - retry_max == 0 --> the last retry job, no more retry 192 - retry_max > 0 193 - attempted, but has failed in scheduling a 194 following retry due to rpc error --> no more retry 195 - has not attempped --> has following retry if test failed. 196 197 @param result: A result, encapsulating the status of the job. 198 199 @returns: True, if there will be a following retry. 200 False otherwise. 201 202 """ 203 return (result.test_executed 204 and result.id in self._retry_map 205 and (self._retry_map[result.id]['state'] == self.States.RETRIED 206 or self._should_retry(result))) 207 208 209 def _should_retry(self, result): 210 """Check whether we should retry a job based on its result. 211 212 We will retry the job that corresponds to the result 213 when all of the following are true. 214 a) The test was actually executed, meaning that if 215 a job was aborted before it could ever reach the state 216 of 'Running', the job will not be retried. 217 b) The result is worse than |self._retry_level| which 218 defaults to 'WARN'. 219 c) The test requires retry, i.e. the job has an entry in the retry map. 220 d) We haven't made any retry attempt yet, i.e. state == NOT_ATTEMPTED 221 Note that if a test has JOB_RETRIES=5, and the second time 222 it was retried it hit an rpc error, we will give up on 223 all following retries. 224 e) The job has not reached its retry max, i.e. retry_max > 0 225 226 @param result: A result, encapsulating the status of the job. 227 228 @returns: True if we should retry the job. 229 230 """ 231 return ( 232 result.test_executed 233 and result.id in self._retry_map 234 and not self._suite_max_reached() 235 and result.is_worse_than( 236 job_status.Status(self._retry_level, '', 'reason')) 237 and self._retry_map[result.id]['state'] == self.States.NOT_ATTEMPTED 238 and self._retry_map[result.id]['retry_max'] > 0 239 ) 240 241 242 def get_retry_max(self, job_id): 243 """Get the maximum times the job can still be retried. 244 245 @param job_id: afe_job_id of a job. 246 247 @returns: An int, representing the maximum times the job can still be 248 retried. 249 @raises KeyError if job_id isn't in the retry map. 250 251 """ 252 return self._retry_map[job_id]['retry_max'] 253 254 255class _SuiteChildJobCreator(object): 256 """Create test jobs for a suite.""" 257 258 def __init__( 259 self, 260 tag, 261 builds, 262 board, 263 afe=None, 264 max_runtime_mins=24*60, 265 timeout_mins=24*60, 266 suite_job_id=None, 267 ignore_deps=False, 268 extra_deps=(), 269 priority=priorities.Priority.DEFAULT, 270 offload_failures_only=False, 271 test_source_build=None, 272 job_keyvals=None): 273 """ 274 Constructor 275 276 @param tag: a string with which to tag jobs run in this suite. 277 @param builds: the builds on which we're running this suite. 278 @param board: the board on which we're running this suite. 279 @param afe: an instance of AFE as defined in server/frontend.py. 280 @param max_runtime_mins: Maximum suite runtime, in minutes. 281 @param timeout_mins: Maximum job lifetime, in minutes. 282 @param suite_job_id: Job id that will act as parent id to all sub jobs. 283 Default: None 284 @param ignore_deps: True if jobs should ignore the DEPENDENCIES 285 attribute and skip applying of dependency labels. 286 (Default:False) 287 @param extra_deps: A list of strings which are the extra DEPENDENCIES 288 to add to each test being scheduled. 289 @param priority: Integer priority level. Higher is more important. 290 @param offload_failures_only: Only enable gs_offloading for failed 291 jobs. 292 @param test_source_build: Build that contains the server-side test code. 293 @param job_keyvals: General job keyvals to be inserted into keyval file, 294 which will be used by tko/parse later. 295 """ 296 self._tag = tag 297 self._builds = builds 298 self._board = board 299 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30, 300 delay_sec=10, 301 debug=False) 302 self._max_runtime_mins = max_runtime_mins 303 self._timeout_mins = timeout_mins 304 self._suite_job_id = suite_job_id 305 self._ignore_deps = ignore_deps 306 self._extra_deps = tuple(extra_deps) 307 self._priority = priority 308 self._offload_failures_only = offload_failures_only 309 self._test_source_build = test_source_build 310 self._job_keyvals = job_keyvals 311 312 313 @property 314 def cros_build(self): 315 """Return the CrOS build or the first build in the builds dict.""" 316 # TODO(ayatane): Note that the builds dict isn't ordered. I'm not 317 # sure what the implications of this are, but it's probably not a 318 # good thing. 319 return self._builds.get(provision.CROS_VERSION_PREFIX, 320 self._builds.values()[0]) 321 322 323 def create_job(self, test, retry_for=None): 324 """ 325 Thin wrapper around frontend.AFE.create_job(). 326 327 @param test: ControlData object for a test to run. 328 @param retry_for: If the to-be-created job is a retry for an 329 old job, the afe_job_id of the old job will 330 be passed in as |retry_for|, which will be 331 recorded in the new job's keyvals. 332 @returns: A frontend.Job object with an added test_name member. 333 test_name is used to preserve the higher level TEST_NAME 334 name of the job. 335 """ 336 test_obj = self._afe.create_job( 337 control_file=test.text, 338 name=tools.create_job_name( 339 self._test_source_build or self.cros_build, 340 self._tag, 341 test.name), 342 control_type=test.test_type.capitalize(), 343 meta_hosts=[self._board]*test.sync_count, 344 dependencies=self._create_job_deps(test), 345 keyvals=self._create_keyvals_for_test_job(test, retry_for), 346 max_runtime_mins=self._max_runtime_mins, 347 timeout_mins=self._timeout_mins, 348 parent_job_id=self._suite_job_id, 349 test_retry=test.retries, 350 priority=self._priority, 351 synch_count=test.sync_count, 352 require_ssp=test.require_ssp) 353 354 test_obj.test_name = test.name 355 return test_obj 356 357 358 def _create_job_deps(self, test): 359 """Create job deps list for a test job. 360 361 @returns: A list of dependency strings. 362 """ 363 if self._ignore_deps: 364 job_deps = [] 365 else: 366 job_deps = list(test.dependencies) 367 job_deps.extend(self._extra_deps) 368 return job_deps 369 370 371 def _create_keyvals_for_test_job(self, test, retry_for=None): 372 """Create keyvals dict for creating a test job. 373 374 @param test: ControlData object for a test to run. 375 @param retry_for: If the to-be-created job is a retry for an 376 old job, the afe_job_id of the old job will 377 be passed in as |retry_for|, which will be 378 recorded in the new job's keyvals. 379 @returns: A keyvals dict for creating the test job. 380 """ 381 keyvals = { 382 constants.JOB_BUILD_KEY: self.cros_build, 383 constants.JOB_SUITE_KEY: self._tag, 384 constants.JOB_EXPERIMENTAL_KEY: test.experimental, 385 constants.JOB_BUILDS_KEY: self._builds 386 } 387 # test_source_build is saved to job_keyvals so scheduler can retrieve 388 # the build name from database when compiling autoserv commandline. 389 # This avoid a database change to add a new field in afe_jobs. 390 # 391 # Only add `test_source_build` to job keyvals if the build is different 392 # from the CrOS build or the job uses more than one build, e.g., both 393 # firmware and CrOS will be updated in the dut. 394 # This is for backwards compatibility, so the update Autotest code can 395 # compile an autoserv command line to run in a SSP container using 396 # previous builds. 397 if (self._test_source_build and 398 (self.cros_build != self._test_source_build or 399 len(self._builds) > 1)): 400 keyvals[constants.JOB_TEST_SOURCE_BUILD_KEY] = \ 401 self._test_source_build 402 for prefix, build in self._builds.iteritems(): 403 if prefix == provision.FW_RW_VERSION_PREFIX: 404 keyvals[constants.FWRW_BUILD]= build 405 elif prefix == provision.FW_RO_VERSION_PREFIX: 406 keyvals[constants.FWRO_BUILD] = build 407 # Add suite job id to keyvals so tko parser can read it from keyval 408 # file. 409 if self._suite_job_id: 410 keyvals[constants.PARENT_JOB_ID] = self._suite_job_id 411 # We drop the old job's id in the new job's keyval file so that 412 # later our tko parser can figure out the retry relationship and 413 # invalidate the results of the old job in tko database. 414 if retry_for: 415 keyvals[constants.RETRY_ORIGINAL_JOB_ID] = retry_for 416 if self._offload_failures_only: 417 keyvals[constants.JOB_OFFLOAD_FAILURES_KEY] = True 418 if self._job_keyvals: 419 for key in constants.INHERITED_KEYVALS: 420 if key in self._job_keyvals: 421 keyvals[key] = self._job_keyvals[key] 422 return keyvals 423 424 425def _get_cf_retriever(cf_getter, forgiving_parser=True, run_prod_code=False, 426 test_args=None): 427 """Return the correct _ControlFileRetriever instance. 428 429 If cf_getter is a File system ControlFileGetter, return a 430 _ControlFileRetriever. This performs a full parse of the root 431 directory associated with the getter. This is the case when it's 432 invoked from suite_preprocessor. 433 434 If cf_getter is a devserver getter, return a 435 _BatchControlFileRetriever. This looks up the suite_name in a suite 436 to control file map generated at build time, and parses the relevant 437 control files alone. This lookup happens on the devserver, so as far 438 as this method is concerned, both cases are equivalent. If 439 enable_controls_in_batch is switched on, this function will call 440 cf_getter.get_suite_info() to get a dict of control files and 441 contents in batch. 442 """ 443 if _should_batch_with(cf_getter): 444 cls = _BatchControlFileRetriever 445 else: 446 cls = _ControlFileRetriever 447 return cls(cf_getter, forgiving_parser, run_prod_code, test_args) 448 449 450def _should_batch_with(cf_getter): 451 """Return whether control files should be fetched in batch. 452 453 This depends on the control file getter and configuration options. 454 455 @param cf_getter: a control_file_getter.ControlFileGetter used to list 456 and fetch the content of control files 457 """ 458 return (ENABLE_CONTROLS_IN_BATCH 459 and isinstance(cf_getter, control_file_getter.DevServerGetter)) 460 461 462class _ControlFileRetriever(object): 463 """Retrieves control files. 464 465 This returns control data instances, unlike control file getters 466 which simply return the control file text contents. 467 """ 468 469 def __init__(self, cf_getter, forgiving_parser=True, run_prod_code=False, 470 test_args=None): 471 """Initialize instance. 472 473 @param cf_getter: a control_file_getter.ControlFileGetter used to list 474 and fetch the content of control files 475 @param forgiving_parser: If False, will raise ControlVariableExceptions 476 if any are encountered when parsing control 477 files. Note that this can raise an exception 478 for syntax errors in unrelated files, because 479 we parse them before applying the predicate. 480 @param run_prod_code: If true, the retrieved tests will run the test 481 code that lives in prod aka the test code 482 currently on the lab servers by disabling 483 SSP for the discovered tests. 484 @param test_args: A dict of args to be seeded in test control file under 485 the name |args_dict|. 486 """ 487 self._cf_getter = cf_getter 488 self._forgiving_parser = forgiving_parser 489 self._run_prod_code = run_prod_code 490 self._test_args = test_args 491 492 493 def retrieve(self, test_name): 494 """Retrieve a test's control data. 495 496 This ignores forgiving_parser because we cannot return a 497 forgiving value. 498 499 @param test_name: Name of test to retrieve. 500 501 @raises ControlVariableException: There is a syntax error in a 502 control file. 503 504 @returns a ControlData object 505 """ 506 path = self._cf_getter.get_control_file_path(test_name) 507 text = self._cf_getter.get_control_file_contents(path) 508 return self._parse_cf_text(path, text) 509 510 511 def retrieve_for_suite(self, suite_name=''): 512 """Scan through all tests and find all tests. 513 514 @param suite_name: If specified, this method will attempt to restrain 515 the search space to just this suite's control files. 516 517 @raises ControlVariableException: If forgiving_parser is False and there 518 is a syntax error in a control file. 519 520 @returns a dictionary of ControlData objects that based on given 521 parameters. 522 """ 523 control_file_texts = self._get_cf_texts_for_suite(suite_name) 524 return self._parse_cf_text_many(control_file_texts) 525 526 527 def _filter_cf_paths(self, paths): 528 """Remove certain control file paths 529 530 @param paths: Iterable of paths 531 @returns: generator yielding paths 532 """ 533 matcher = re.compile(r'[^/]+/(deps|profilers)/.+') 534 return (path for path in paths if not matcher.match(path)) 535 536 537 def _get_cf_texts_for_suite(self, suite_name): 538 """Get control file content for given suite. 539 540 @param suite_name: If specified, this method will attempt to restrain 541 the search space to just this suite's control files. 542 @returns: generator yielding (path, text) tuples 543 """ 544 files = self._cf_getter.get_control_file_list(suite_name=suite_name) 545 filtered_files = self._filter_cf_paths(files) 546 for path in filtered_files: 547 yield path, self._cf_getter.get_control_file_contents(path) 548 549 550 def _parse_cf_text_many(self, control_file_texts): 551 """Parse control file texts. 552 553 @param control_file_texts: iterable of (path, text) pairs 554 @returns: a dictionary of ControlData objects 555 """ 556 tests = {} 557 for path, text in control_file_texts: 558 # Seed test_args into the control file. 559 if self._test_args: 560 text = tools.inject_vars(self._test_args, text) 561 try: 562 found_test = self._parse_cf_text(path, text) 563 except control_data.ControlVariableException, e: 564 if not self._forgiving_parser: 565 msg = "Failed parsing %s\n%s" % (path, e) 566 raise control_data.ControlVariableException(msg) 567 logging.warning("Skipping %s\n%s", path, e) 568 except Exception, e: 569 logging.error("Bad %s\n%s", path, e) 570 else: 571 tests[path] = found_test 572 return tests 573 574 575 def _parse_cf_text(self, path, text): 576 """Parse control file text. 577 578 This ignores forgiving_parser because we cannot return a 579 forgiving value. 580 581 @param path: path to control file 582 @param text: control file text contents 583 @returns: a ControlData object 584 585 @raises ControlVariableException: There is a syntax error in a 586 control file. 587 """ 588 test = control_data.parse_control_string( 589 text, raise_warnings=True, path=path) 590 test.text = text 591 if self._run_prod_code: 592 test.require_ssp = False 593 return test 594 595 596class _BatchControlFileRetriever(_ControlFileRetriever): 597 """Subclass that can retrieve suite control files in batch.""" 598 599 600 def _get_cf_texts_for_suite(self, suite_name): 601 """Get control file content for given suite. 602 603 @param suite_name: If specified, this method will attempt to restrain 604 the search space to just this suite's control files. 605 @returns: generator yielding (path, text) tuples 606 """ 607 suite_info = self._cf_getter.get_suite_info(suite_name=suite_name) 608 files = suite_info.keys() 609 filtered_files = self._filter_cf_paths(files) 610 for path in filtered_files: 611 yield path, suite_info[path] 612 613 614def get_test_source_build(builds, **dargs): 615 """Get the build of test code. 616 617 Get the test source build from arguments. If parameter 618 `test_source_build` is set and has a value, return its value. Otherwise 619 returns the ChromeOS build name if it exists. If ChromeOS build is not 620 specified either, raise SuiteArgumentException. 621 622 @param builds: the builds on which we're running this suite. It's a 623 dictionary of version_prefix:build. 624 @param **dargs: Any other Suite constructor parameters, as described 625 in Suite.__init__ docstring. 626 627 @return: The build contains the test code. 628 @raise: SuiteArgumentException if both test_source_build and ChromeOS 629 build are not specified. 630 631 """ 632 if dargs.get('test_source_build', None): 633 return dargs['test_source_build'] 634 test_source_build = builds.get(provision.CROS_VERSION_PREFIX, None) 635 if not test_source_build: 636 raise error.SuiteArgumentException( 637 'test_source_build must be specified if CrOS build is not ' 638 'specified.') 639 return test_source_build 640 641 642def list_all_suites(build, devserver, cf_getter=None): 643 """ 644 Parses all ControlData objects with a SUITE tag and extracts all 645 defined suite names. 646 647 @param build: the build on which we're running this suite. 648 @param devserver: the devserver which contains the build. 649 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to 650 using DevServerGetter. 651 652 @return list of suites 653 """ 654 if cf_getter is None: 655 cf_getter = _create_ds_getter(build, devserver) 656 657 suites = set() 658 predicate = lambda t: True 659 for test in find_and_parse_tests(cf_getter, predicate): 660 suites.update(test.suite_tag_parts) 661 return list(suites) 662 663 664def test_file_similarity_predicate(test_file_pattern): 665 """Returns predicate that gets the similarity based on a test's file 666 name pattern. 667 668 Builds a predicate that takes in a parsed control file (a ControlData) 669 and returns a tuple of (file path, ratio), where ratio is the 670 similarity between the test file name and the given test_file_pattern. 671 672 @param test_file_pattern: regular expression (string) to match against 673 control file names. 674 @return a callable that takes a ControlData and and returns a tuple of 675 (file path, ratio), where ratio is the similarity between the 676 test file name and the given test_file_pattern. 677 """ 678 return lambda t: ((None, 0) if not hasattr(t, 'path') else 679 (t.path, difflib.SequenceMatcher(a=t.path, 680 b=test_file_pattern).ratio())) 681 682 683def test_name_similarity_predicate(test_name): 684 """Returns predicate that matched based on a test's name. 685 686 Builds a predicate that takes in a parsed control file (a ControlData) 687 and returns a tuple of (test name, ratio), where ratio is the similarity 688 between the test name and the given test_name. 689 690 @param test_name: the test name to base the predicate on. 691 @return a callable that takes a ControlData and returns a tuple of 692 (test name, ratio), where ratio is the similarity between the 693 test name and the given test_name. 694 """ 695 return lambda t: ((None, 0) if not hasattr(t, 'name') else 696 (t.name, 697 difflib.SequenceMatcher(a=t.name, b=test_name).ratio())) 698 699 700def matches_attribute_expression_predicate(test_attr_boolstr): 701 """Returns predicate that matches based on boolean expression of 702 attributes. 703 704 Builds a predicate that takes in a parsed control file (a ControlData) 705 ans returns True if the test attributes satisfy the given attribute 706 boolean expression. 707 708 @param test_attr_boolstr: boolean expression of the attributes to be 709 test, like 'system:all and interval:daily'. 710 711 @return a callable that takes a ControlData and returns True if the test 712 attributes satisfy the given boolean expression. 713 """ 714 return lambda t: boolparse_lib.BoolstrResult( 715 test_attr_boolstr, t.attributes) 716 717 718def test_file_matches_pattern_predicate(test_file_pattern): 719 """Returns predicate that matches based on a test's file name pattern. 720 721 Builds a predicate that takes in a parsed control file (a ControlData) 722 and returns True if the test's control file name matches the given 723 regular expression. 724 725 @param test_file_pattern: regular expression (string) to match against 726 control file names. 727 @return a callable that takes a ControlData and and returns 728 True if control file name matches the pattern. 729 """ 730 return lambda t: hasattr(t, 'path') and re.match(test_file_pattern, 731 t.path) 732 733 734def test_name_matches_pattern_predicate(test_name_pattern): 735 """Returns predicate that matches based on a test's name pattern. 736 737 Builds a predicate that takes in a parsed control file (a ControlData) 738 and returns True if the test name matches the given regular expression. 739 740 @param test_name_pattern: regular expression (string) to match against 741 test names. 742 @return a callable that takes a ControlData and returns 743 True if the name fields matches the pattern. 744 """ 745 return lambda t: hasattr(t, 'name') and re.match(test_name_pattern, 746 t.name) 747 748 749def test_name_equals_predicate(test_name): 750 """Returns predicate that matched based on a test's name. 751 752 Builds a predicate that takes in a parsed control file (a ControlData) 753 and returns True if the test name is equal to |test_name|. 754 755 @param test_name: the test name to base the predicate on. 756 @return a callable that takes a ControlData and looks for |test_name| 757 in that ControlData's name. 758 """ 759 return lambda t: hasattr(t, 'name') and test_name == t.name 760 761 762def name_in_tag_similarity_predicate(name): 763 """Returns predicate that takes a control file and gets the similarity 764 of the suites in the control file and the given name. 765 766 Builds a predicate that takes in a parsed control file (a ControlData) 767 and returns a list of tuples of (suite name, ratio), where suite name 768 is each suite listed in the control file, and ratio is the similarity 769 between each suite and the given name. 770 771 @param name: the suite name to base the predicate on. 772 @return a callable that takes a ControlData and returns a list of tuples 773 of (suite name, ratio), where suite name is each suite listed in 774 the control file, and ratio is the similarity between each suite 775 and the given name. 776 """ 777 return lambda t: [(suite, 778 difflib.SequenceMatcher(a=suite, b=name).ratio()) 779 for suite in t.suite_tag_parts] or [(None, 0)] 780 781 782def name_in_tag_predicate(name): 783 """Returns predicate that takes a control file and looks for |name|. 784 785 Builds a predicate that takes in a parsed control file (a ControlData) 786 and returns True if the SUITE tag is present and contains |name|. 787 788 @param name: the suite name to base the predicate on. 789 @return a callable that takes a ControlData and looks for |name| in that 790 ControlData object's suite member. 791 """ 792 return lambda t: name in t.suite_tag_parts 793 794 795def create_fs_getter(autotest_dir): 796 """ 797 @param autotest_dir: the place to find autotests. 798 @return a FileSystemGetter instance that looks under |autotest_dir|. 799 """ 800 # currently hard-coded places to look for tests. 801 subpaths = ['server/site_tests', 'client/site_tests', 802 'server/tests', 'client/tests'] 803 directories = [os.path.join(autotest_dir, p) for p in subpaths] 804 return control_file_getter.FileSystemGetter(directories) 805 806 807def _create_ds_getter(build, devserver): 808 """ 809 @param build: the build on which we're running this suite. 810 @param devserver: the devserver which contains the build. 811 @return a FileSystemGetter instance that looks under |autotest_dir|. 812 """ 813 return control_file_getter.DevServerGetter(build, devserver) 814 815 816def _non_experimental_tests_predicate(test_data): 817 """Test predicate for non-experimental tests.""" 818 return not test_data.experimental 819 820 821def find_and_parse_tests(cf_getter, predicate, suite_name='', 822 add_experimental=False, forgiving_parser=True, 823 run_prod_code=False, test_args=None): 824 """ 825 Function to scan through all tests and find eligible tests. 826 827 Search through all tests based on given cf_getter, suite_name, 828 add_experimental and forgiving_parser, return the tests that match 829 given predicate. 830 831 @param cf_getter: a control_file_getter.ControlFileGetter used to list 832 and fetch the content of control files 833 @param predicate: a function that should return True when run over a 834 ControlData representation of a control file that should be in 835 this Suite. 836 @param suite_name: If specified, this method will attempt to restrain 837 the search space to just this suite's control files. 838 @param add_experimental: add tests with experimental attribute set. 839 @param forgiving_parser: If False, will raise ControlVariableExceptions 840 if any are encountered when parsing control 841 files. Note that this can raise an exception 842 for syntax errors in unrelated files, because 843 we parse them before applying the predicate. 844 @param run_prod_code: If true, the suite will run the test code that 845 lives in prod aka the test code currently on the 846 lab servers by disabling SSP for the discovered 847 tests. 848 @param test_args: A dict of args to be seeded in test control file. 849 850 @raises ControlVariableException: If forgiving_parser is False and there 851 is a syntax error in a control file. 852 853 @return list of ControlData objects that should be run, with control 854 file text added in |text| attribute. Results are sorted based 855 on the TIME setting in control file, slowest test comes first. 856 """ 857 logging.debug('Getting control file list for suite: %s', suite_name) 858 retriever = _get_cf_retriever(cf_getter, 859 forgiving_parser=forgiving_parser, 860 run_prod_code=run_prod_code, 861 test_args=test_args) 862 tests = retriever.retrieve_for_suite(suite_name) 863 logging.debug('Parsed %s control files.', len(tests)) 864 if not add_experimental: 865 predicate = _ComposedPredicate([predicate, 866 _non_experimental_tests_predicate]) 867 tests = [test for test in tests.itervalues() if predicate(test)] 868 tests.sort(key=lambda t: 869 control_data.ControlData.get_test_time_index(t.time), 870 reverse=True) 871 return tests 872 873 874def find_possible_tests(cf_getter, predicate, suite_name='', count=10): 875 """ 876 Function to scan through all tests and find possible tests. 877 878 Search through all tests based on given cf_getter, suite_name, 879 add_experimental and forgiving_parser. Use the given predicate to 880 calculate the similarity and return the top 10 matches. 881 882 @param cf_getter: a control_file_getter.ControlFileGetter used to list 883 and fetch the content of control files 884 @param predicate: a function that should return a tuple of (name, ratio) 885 when run over a ControlData representation of a control file that 886 should be in this Suite. `name` is the key to be compared, e.g., 887 a suite name or test name. `ratio` is a value between [0,1] 888 indicating the similarity of `name` and the value to be compared. 889 @param suite_name: If specified, this method will attempt to restrain 890 the search space to just this suite's control files. 891 @param count: Number of suggestions to return, default to 10. 892 893 @return list of top names that similar to the given test, sorted by 894 match ratio. 895 """ 896 logging.debug('Getting control file list for suite: %s', suite_name) 897 tests = _get_cf_retriever(cf_getter).retrieve_for_suite(suite_name) 898 logging.debug('Parsed %s control files.', len(tests)) 899 similarities = {} 900 for test in tests.itervalues(): 901 ratios = predicate(test) 902 # Some predicates may return a list of tuples, e.g., 903 # name_in_tag_similarity_predicate. Convert all returns to a list. 904 if not isinstance(ratios, list): 905 ratios = [ratios] 906 for name, ratio in ratios: 907 similarities[name] = ratio 908 return [s[0] for s in 909 sorted(similarities.items(), key=operator.itemgetter(1), 910 reverse=True)][:count] 911 912 913def _deprecated_suite_method(func): 914 """Decorator for deprecated Suite static methods. 915 916 TODO(ayatane): This is used to decorate functions that are called as 917 static methods on Suite. 918 """ 919 @functools.wraps(func) 920 def wrapper(*args, **kwargs): 921 """Wraps |func| for warning.""" 922 warnings.warn('Calling method "%s" from Suite is deprecated' % 923 func.__name__) 924 return func(*args, **kwargs) 925 return staticmethod(wrapper) 926 927 928class _BaseSuite(object): 929 """ 930 A suite of tests, defined by some predicate over control file variables. 931 932 Given a place to search for control files a predicate to match the desired 933 tests, can gather tests and fire off jobs to run them, and then wait for 934 results. 935 936 @var _predicate: a function that should return True when run over a 937 ControlData representation of a control file that should be in 938 this Suite. 939 @var _tag: a string with which to tag jobs run in this suite. 940 @var _builds: the builds on which we're running this suite. 941 @var _afe: an instance of AFE as defined in server/frontend.py. 942 @var _tko: an instance of TKO as defined in server/frontend.py. 943 @var _jobs: currently scheduled jobs, if any. 944 @var _jobs_to_tests: a dictionary that maps job ids to tests represented 945 ControlData objects. 946 @var _retry: a bool value indicating whether jobs should be retried on 947 failure. 948 @var _retry_handler: a RetryHandler object. 949 950 """ 951 952 953 def __init__( 954 self, 955 tests, 956 tag, 957 builds, 958 board, 959 afe=None, 960 tko=None, 961 pool=None, 962 results_dir=None, 963 max_runtime_mins=24*60, 964 timeout_mins=24*60, 965 file_bugs=False, 966 suite_job_id=None, 967 ignore_deps=False, 968 extra_deps=None, 969 priority=priorities.Priority.DEFAULT, 970 wait_for_results=True, 971 job_retry=False, 972 max_retries=sys.maxint, 973 offload_failures_only=False, 974 test_source_build=None, 975 job_keyvals=None 976 ): 977 """Initialize instance. 978 979 @param tests: Iterable of tests to run. 980 @param tag: a string with which to tag jobs run in this suite. 981 @param builds: the builds on which we're running this suite. 982 @param board: the board on which we're running this suite. 983 @param afe: an instance of AFE as defined in server/frontend.py. 984 @param tko: an instance of TKO as defined in server/frontend.py. 985 @param pool: Specify the pool of machines to use for scheduling 986 purposes. 987 @param results_dir: The directory where the job can write results to. 988 This must be set if you want job_id of sub-jobs 989 list in the job keyvals. 990 @param max_runtime_mins: Maximum suite runtime, in minutes. 991 @param timeout: Maximum job lifetime, in hours. 992 @param suite_job_id: Job id that will act as parent id to all sub jobs. 993 Default: None 994 @param ignore_deps: True if jobs should ignore the DEPENDENCIES 995 attribute and skip applying of dependency labels. 996 (Default:False) 997 @param extra_deps: A list of strings which are the extra DEPENDENCIES 998 to add to each test being scheduled. 999 @param priority: Integer priority level. Higher is more important. 1000 @param wait_for_results: Set to False to run the suite job without 1001 waiting for test jobs to finish. Default is 1002 True. 1003 @param job_retry: A bool value indicating whether jobs should be retired 1004 on failure. If True, the field 'JOB_RETRIES' in 1005 control files will be respected. If False, do not 1006 retry. 1007 @param max_retries: Maximum retry limit at suite level. 1008 Regardless how many times each individual test 1009 has been retried, the total number of retries 1010 happening in the suite can't exceed _max_retries. 1011 Default to sys.maxint. 1012 @param offload_failures_only: Only enable gs_offloading for failed 1013 jobs. 1014 @param test_source_build: Build that contains the server-side test code. 1015 @param job_keyvals: General job keyvals to be inserted into keyval file, 1016 which will be used by tko/parse later. 1017 """ 1018 1019 self.tests = list(tests) 1020 self._tag = tag 1021 self._builds = builds 1022 self._results_dir = results_dir 1023 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30, 1024 delay_sec=10, 1025 debug=False) 1026 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30, 1027 delay_sec=10, 1028 debug=False) 1029 self._jobs = [] 1030 self._jobs_to_tests = {} 1031 1032 self._file_bugs = file_bugs 1033 self._suite_job_id = suite_job_id 1034 self._job_retry=job_retry 1035 self._max_retries = max_retries 1036 # RetryHandler to be initialized in schedule() 1037 self._retry_handler = None 1038 self.wait_for_results = wait_for_results 1039 self._job_keyvals = job_keyvals 1040 1041 if extra_deps is None: 1042 extra_deps = [] 1043 extra_deps.append(board) 1044 if pool: 1045 extra_deps.append(pool) 1046 self._job_creator = _SuiteChildJobCreator( 1047 tag=tag, 1048 builds=builds, 1049 board=board, 1050 afe=afe, 1051 max_runtime_mins=max_runtime_mins, 1052 timeout_mins=timeout_mins, 1053 suite_job_id=suite_job_id, 1054 ignore_deps=ignore_deps, 1055 extra_deps=extra_deps, 1056 priority=priority, 1057 offload_failures_only=offload_failures_only, 1058 test_source_build=test_source_build, 1059 job_keyvals=job_keyvals, 1060 ) 1061 1062 1063 def _schedule_test(self, record, test, retry_for=None): 1064 """Schedule a single test and return the job. 1065 1066 Schedule a single test by creating a job, and then update relevant 1067 data structures that are used to keep track of all running jobs. 1068 1069 Emits a TEST_NA status log entry if it failed to schedule the test due 1070 to NoEligibleHostException or a non-existent board label. 1071 1072 Returns a frontend.Job object if the test is successfully scheduled. 1073 If scheduling failed due to NoEligibleHostException or a non-existent 1074 board label, returns None. 1075 1076 @param record: A callable to use for logging. 1077 prototype: record(base_job.status_log_entry) 1078 @param test: ControlData for a test to run. 1079 @param retry_for: If we are scheduling a test to retry an 1080 old job, the afe_job_id of the old job 1081 will be passed in as |retry_for|. 1082 1083 @returns: A frontend.Job object or None 1084 """ 1085 msg = 'Scheduling %s' % test.name 1086 if retry_for: 1087 msg = msg + ', to retry afe job %d' % retry_for 1088 logging.debug(msg) 1089 begin_time_str = datetime.datetime.now().strftime(time_utils.TIME_FMT) 1090 try: 1091 job = self._job_creator.create_job(test, retry_for=retry_for) 1092 except (error.NoEligibleHostException, proxy.ValidationError) as e: 1093 if (isinstance(e, error.NoEligibleHostException) 1094 or (isinstance(e, proxy.ValidationError) 1095 and _is_nonexistent_board_error(e))): 1096 # Treat a dependency on a non-existent board label the same as 1097 # a dependency on a board that exists, but for which there's no 1098 # hardware. 1099 logging.debug('%s not applicable for this board/pool. ' 1100 'Emitting TEST_NA.', test.name) 1101 Status('TEST_NA', test.name, 1102 'Skipping: test not supported on this board/pool.', 1103 begin_time_str=begin_time_str).record_all(record) 1104 return None 1105 else: 1106 raise e 1107 except (error.RPCException, proxy.JSONRPCException): 1108 if retry_for: 1109 # Mark that we've attempted to retry the old job. 1110 self._retry_handler.set_attempted(job_id=retry_for) 1111 raise 1112 else: 1113 self._jobs.append(job) 1114 self._jobs_to_tests[job.id] = test 1115 if retry_for: 1116 # A retry job was just created, record it. 1117 self._retry_handler.add_retry( 1118 old_job_id=retry_for, new_job_id=job.id) 1119 retry_count = (test.job_retries - 1120 self._retry_handler.get_retry_max(job.id)) 1121 logging.debug('Job %d created to retry job %d. ' 1122 'Have retried for %d time(s)', 1123 job.id, retry_for, retry_count) 1124 self._remember_job_keyval(job) 1125 return job 1126 1127 1128 def schedule(self, record): 1129 #pylint: disable-msg=C0111 1130 """ 1131 Schedule jobs using |self._afe|. 1132 1133 frontend.Job objects representing each scheduled job will be put in 1134 |self._jobs|. 1135 1136 @param record: A callable to use for logging. 1137 prototype: record(base_job.status_log_entry) 1138 @returns: The number of tests that were scheduled. 1139 """ 1140 scheduled_test_names = [] 1141 logging.debug('Discovered %d tests.', len(self.tests)) 1142 1143 Status('INFO', 'Start %s' % self._tag).record_result(record) 1144 try: 1145 # Write job_keyvals into keyval file. 1146 if self._job_keyvals: 1147 utils.write_keyval(self._results_dir, self._job_keyvals) 1148 1149 # TODO(crbug.com/730885): This is a hack to protect tests that are 1150 # not usually retried from getting hit by a provision error when run 1151 # as part of a suite. Remove this hack once provision is separated 1152 # out in its own suite. 1153 self._bump_up_test_retries(self.tests) 1154 for test in self.tests: 1155 scheduled_job = self._schedule_test(record, test) 1156 if scheduled_job is not None: 1157 scheduled_test_names.append(test.name) 1158 1159 # Write the num of scheduled tests and name of them to keyval file. 1160 logging.debug('Scheduled %d tests, writing the total to keyval.', 1161 len(scheduled_test_names)) 1162 utils.write_keyval( 1163 self._results_dir, 1164 self._make_scheduled_tests_keyvals(scheduled_test_names)) 1165 except Exception: # pylint: disable=W0703 1166 logging.exception('Exception while scheduling suite') 1167 Status('FAIL', self._tag, 1168 'Exception while scheduling suite').record_result(record) 1169 1170 if self._job_retry: 1171 self._retry_handler = RetryHandler( 1172 initial_jobs_to_tests=self._jobs_to_tests, 1173 max_retries=self._max_retries) 1174 return len(scheduled_test_names) 1175 1176 1177 def _bump_up_test_retries(self, tests): 1178 """Bump up individual test retries to match suite retry options.""" 1179 if not self._job_retry: 1180 return 1181 1182 for test in tests: 1183 if not test.job_retries: 1184 logging.debug( 1185 'Test %s requested no retries, but suite requires ' 1186 'retries. Bumping retries up to 1. ' 1187 '(See crbug.com/730885)', 1188 test.name) 1189 test.job_retries = 1 1190 1191 1192 def _make_scheduled_tests_keyvals(self, scheduled_test_names): 1193 """Make a keyvals dict to write for scheduled test names. 1194 1195 @param scheduled_test_names: A list of scheduled test name strings. 1196 1197 @returns: A keyvals dict. 1198 """ 1199 return { 1200 constants.SCHEDULED_TEST_COUNT_KEY: len(scheduled_test_names), 1201 constants.SCHEDULED_TEST_NAMES_KEY: repr(scheduled_test_names), 1202 } 1203 1204 1205 def _should_report(self, result): 1206 """ 1207 Returns True if this failure requires to be reported. 1208 1209 @param result: A result, encapsulating the status of the failed job. 1210 @return: True if we should report this failure. 1211 """ 1212 if self._has_retry(result): 1213 return False 1214 1215 return (self._file_bugs and result.test_executed and 1216 not result.is_testna() and 1217 result.is_worse_than(job_status.Status('GOOD', '', 'reason'))) 1218 1219 1220 def _has_retry(self, result): 1221 """ 1222 Return True if this result gets to retry. 1223 1224 @param result: A result, encapsulating the status of the failed job. 1225 @return: bool 1226 """ 1227 return (self._job_retry 1228 and self._retry_handler.has_following_retry(result)) 1229 1230 1231 def wait(self, record, reporter): 1232 """ 1233 Polls for the job statuses, using |record| to print status when each 1234 completes. 1235 1236 @param record: callable that records job status. 1237 prototype: 1238 record(base_job.status_log_entry) 1239 @param reporter: _ResultReporter instance. 1240 """ 1241 try: 1242 if self._suite_job_id: 1243 results_generator = job_status.wait_for_child_results( 1244 self._afe, self._tko, self._suite_job_id) 1245 else: 1246 logging.warning('Unknown suite_job_id, falling back to less ' 1247 'efficient results_generator.') 1248 results_generator = job_status.wait_for_results(self._afe, 1249 self._tko, 1250 self._jobs) 1251 for result in results_generator: 1252 self._record_result( 1253 result=result, 1254 record=record, 1255 results_generator=results_generator, 1256 reporter=reporter) 1257 1258 except Exception: # pylint: disable=W0703 1259 logging.exception('Exception waiting for results') 1260 Status('FAIL', self._tag, 1261 'Exception waiting for results').record_result(record) 1262 1263 1264 def get_result_reporter(self, bug_template): 1265 """Return the _ResultReporter instance to use for the suite. 1266 1267 @param bug_template: A template dictionary specifying the default bug 1268 filing options for failures in this suite. 1269 """ 1270 # reporting modules have dependency on external packages, e.g., httplib2 1271 # Such dependency can cause issue to any module tries to import suite.py 1272 # without building site-packages first. Since the reporting modules are 1273 # only used in this function, move the imports here avoid the 1274 # requirement of building site packages to use other functions in this 1275 # module. 1276 from autotest_lib.server.cros.dynamic_suite import reporting 1277 1278 if self._should_file_bugs: 1279 if self._file_bugs: 1280 bug_reporter = reporting.Reporter() 1281 else: 1282 bug_reporter = reporting.NullReporter() 1283 return _BugResultReporter(self, bug_reporter, bug_template) 1284 else: 1285 return _EmailResultReporter(self, bug_template) 1286 1287 1288 def _record_result(self, result, record, results_generator, reporter): 1289 """ 1290 Record a single test job result. 1291 1292 @param result: Status instance for job. 1293 @param record: callable that records job status. 1294 prototype: 1295 record(base_job.status_log_entry) 1296 @param results_generator: Results generator for sending job retries. 1297 @param reporter: _ResultReporter instance. 1298 """ 1299 result.record_all(record) 1300 self._remember_job_keyval(result) 1301 1302 if self._job_retry and self._retry_handler._should_retry(result): 1303 test = self._jobs_to_tests[result.id] 1304 try: 1305 new_job = self._schedule_test( 1306 record=record, test=test, retry_for=result.id) 1307 except (error.RPCException, proxy.JSONRPCException) as e: 1308 logging.error('Failed to schedule test: %s, Reason: %s', 1309 test.name, e) 1310 else: 1311 results_generator.send([new_job]) 1312 1313 # TODO (fdeng): If the suite times out before a retry could 1314 # finish, we would lose the chance to file a bug for the 1315 # original job. 1316 if self._should_report(result): 1317 reporter.report(result) 1318 1319 def _get_bug_template(self, result, bug_template): 1320 """Get BugTemplate for test job. 1321 1322 @param result: Status instance for job. 1323 @param bug_template: A template dictionary specifying the default bug 1324 filing options for failures in this suite. 1325 @returns: BugTemplate instance 1326 """ 1327 # reporting modules have dependency on external packages, e.g., httplib2 1328 # Such dependency can cause issue to any module tries to import suite.py 1329 # without building site-packages first. Since the reporting modules are 1330 # only used in this function, move the imports here avoid the 1331 # requirement of building site packages to use other functions in this 1332 # module. 1333 from autotest_lib.server.cros.dynamic_suite import reporting_utils 1334 1335 # Try to merge with bug template in test control file. 1336 template = reporting_utils.BugTemplate(bug_template) 1337 try: 1338 test_data = self._jobs_to_tests[result.id] 1339 return template.finalize_bug_template( 1340 test_data.bug_template) 1341 except AttributeError: 1342 # Test control file does not have bug template defined. 1343 return template.bug_template 1344 except reporting_utils.InvalidBugTemplateException as e: 1345 logging.error('Merging bug templates failed with ' 1346 'error: %s An empty bug template will ' 1347 'be used.', e) 1348 return {} 1349 1350 1351 def _get_test_bug(self, result): 1352 """Get TestBug for the given result. 1353 1354 @param result: Status instance for a test job. 1355 @returns: TestBug instance. 1356 """ 1357 # reporting modules have dependency on external packages, e.g., httplib2 1358 # Such dependency can cause issue to any module tries to import suite.py 1359 # without building site-packages first. Since the reporting modules are 1360 # only used in this function, move the imports here avoid the 1361 # requirement of building site packages to use other functions in this 1362 # module. 1363 from autotest_lib.server.cros.dynamic_suite import reporting 1364 1365 job_views = self._tko.run('get_detailed_test_views', 1366 afe_job_id=result.id) 1367 return reporting.TestBug(self._job_creator.cros_build, 1368 utils.get_chrome_version(job_views), 1369 self._tag, 1370 result) 1371 1372 1373 @property 1374 def _should_file_bugs(self): 1375 """Return whether bugs should be filed. 1376 1377 @returns: bool 1378 """ 1379 # File bug when failure is one of the _FILE_BUG_SUITES, 1380 # otherwise send an email to the owner anc cc. 1381 return self._tag in _FILE_BUG_SUITES 1382 1383 1384 def _file_bug(self, result, bug_reporter, bug_template): 1385 """File a bug for a test job result. 1386 1387 @param result: Status instance for job. 1388 @param bug_reporter: Reporter instance for reporting bugs. 1389 @param bug_template: A template dictionary specifying the default bug 1390 filing options for failures in this suite. 1391 """ 1392 bug_id, bug_count = bug_reporter.report( 1393 self._get_test_bug(result), 1394 self._get_bug_template(result, bug_template)) 1395 1396 # We use keyvals to communicate bugs filed with run_suite. 1397 if bug_id is not None: 1398 bug_keyvals = tools.create_bug_keyvals( 1399 result.id, result.test_name, 1400 (bug_id, bug_count)) 1401 try: 1402 utils.write_keyval(self._results_dir, 1403 bug_keyvals) 1404 except ValueError: 1405 logging.error('Unable to log bug keyval for:%s', 1406 result.test_name) 1407 1408 1409 def abort(self): 1410 """ 1411 Abort all scheduled test jobs. 1412 """ 1413 if self._jobs: 1414 job_ids = [job.id for job in self._jobs] 1415 self._afe.run('abort_host_queue_entries', job__id__in=job_ids) 1416 1417 1418 def _remember_job_keyval(self, job): 1419 """ 1420 Record provided job as a suite job keyval, for later referencing. 1421 1422 @param job: some representation of a job that has the attributes: 1423 id, test_name, and owner 1424 """ 1425 if self._results_dir and job.id and job.owner and job.test_name: 1426 job_id_owner = '%s-%s' % (job.id, job.owner) 1427 logging.debug('Adding job keyval for %s=%s', 1428 job.test_name, job_id_owner) 1429 utils.write_keyval( 1430 self._results_dir, 1431 {hashlib.md5(job.test_name).hexdigest(): job_id_owner}) 1432 1433 1434class Suite(_BaseSuite): 1435 """ 1436 A suite of tests, defined by some predicate over control file variables. 1437 1438 Given a place to search for control files a predicate to match the desired 1439 tests, can gather tests and fire off jobs to run them, and then wait for 1440 results. 1441 1442 @var _predicate: a function that should return True when run over a 1443 ControlData representation of a control file that should be in 1444 this Suite. 1445 @var _tag: a string with which to tag jobs run in this suite. 1446 @var _builds: the builds on which we're running this suite. 1447 @var _afe: an instance of AFE as defined in server/frontend.py. 1448 @var _tko: an instance of TKO as defined in server/frontend.py. 1449 @var _jobs: currently scheduled jobs, if any. 1450 @var _jobs_to_tests: a dictionary that maps job ids to tests represented 1451 ControlData objects. 1452 @var _cf_getter: a control_file_getter.ControlFileGetter 1453 @var _retry: a bool value indicating whether jobs should be retried on 1454 failure. 1455 @var _retry_handler: a RetryHandler object. 1456 1457 """ 1458 1459 # TODO(ayatane): These methods are kept on the Suite class for 1460 # backward compatibility. 1461 find_and_parse_tests = _deprecated_suite_method(find_and_parse_tests) 1462 find_possible_tests = _deprecated_suite_method(find_possible_tests) 1463 create_fs_getter = _deprecated_suite_method(create_fs_getter) 1464 name_in_tag_predicate = _deprecated_suite_method(name_in_tag_predicate) 1465 name_in_tag_similarity_predicate = _deprecated_suite_method( 1466 name_in_tag_similarity_predicate) 1467 test_name_equals_predicate = _deprecated_suite_method( 1468 test_name_equals_predicate) 1469 test_name_matches_pattern_predicate = _deprecated_suite_method( 1470 test_name_matches_pattern_predicate) 1471 test_file_matches_pattern_predicate = _deprecated_suite_method( 1472 test_file_matches_pattern_predicate) 1473 matches_attribute_expression_predicate = _deprecated_suite_method( 1474 matches_attribute_expression_predicate) 1475 test_name_similarity_predicate = _deprecated_suite_method( 1476 test_name_similarity_predicate) 1477 test_file_similarity_predicate = _deprecated_suite_method( 1478 test_file_similarity_predicate) 1479 list_all_suites = _deprecated_suite_method(list_all_suites) 1480 get_test_source_build = _deprecated_suite_method(get_test_source_build) 1481 1482 1483 @classmethod 1484 def create_from_predicates(cls, predicates, builds, board, devserver, 1485 cf_getter=None, name='ad_hoc_suite', 1486 run_prod_code=False, **dargs): 1487 """ 1488 Create a Suite using a given predicate test filters. 1489 1490 Uses supplied predicate(s) to instantiate a Suite. Looks for tests in 1491 |autotest_dir| and will schedule them using |afe|. Pulls control files 1492 from the default dev server. Results will be pulled from |tko| upon 1493 completion. 1494 1495 @param predicates: A list of callables that accept ControlData 1496 representations of control files. A test will be 1497 included in suite if all callables in this list 1498 return True on the given control file. 1499 @param builds: the builds on which we're running this suite. It's a 1500 dictionary of version_prefix:build. 1501 @param board: the board on which we're running this suite. 1502 @param devserver: the devserver which contains the build. 1503 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to 1504 using DevServerGetter. 1505 @param name: name of suite. Defaults to 'ad_hoc_suite' 1506 @param run_prod_code: If true, the suite will run the tests that 1507 lives in prod aka the test code currently on the 1508 lab servers. 1509 @param **dargs: Any other Suite constructor parameters, as described 1510 in Suite.__init__ docstring. 1511 @return a Suite instance. 1512 """ 1513 if cf_getter is None: 1514 if run_prod_code: 1515 cf_getter = create_fs_getter(_AUTOTEST_DIR) 1516 else: 1517 build = get_test_source_build(builds, **dargs) 1518 cf_getter = _create_ds_getter(build, devserver) 1519 1520 return cls(predicates, 1521 name, builds, board, cf_getter, run_prod_code, **dargs) 1522 1523 1524 @classmethod 1525 def create_from_name(cls, name, builds, board, devserver, cf_getter=None, 1526 **dargs): 1527 """ 1528 Create a Suite using a predicate based on the SUITE control file var. 1529 1530 Makes a predicate based on |name| and uses it to instantiate a Suite 1531 that looks for tests in |autotest_dir| and will schedule them using 1532 |afe|. Pulls control files from the default dev server. 1533 Results will be pulled from |tko| upon completion. 1534 1535 @param name: a value of the SUITE control file variable to search for. 1536 @param builds: the builds on which we're running this suite. It's a 1537 dictionary of version_prefix:build. 1538 @param board: the board on which we're running this suite. 1539 @param devserver: the devserver which contains the build. 1540 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to 1541 using DevServerGetter. 1542 @param **dargs: Any other Suite constructor parameters, as described 1543 in Suite.__init__ docstring. 1544 @return a Suite instance. 1545 """ 1546 if cf_getter is None: 1547 build = get_test_source_build(builds, **dargs) 1548 cf_getter = _create_ds_getter(build, devserver) 1549 1550 return cls([name_in_tag_predicate(name)], 1551 name, builds, board, cf_getter, **dargs) 1552 1553 1554 def __init__( 1555 self, 1556 predicates, 1557 tag, 1558 builds, 1559 board, 1560 cf_getter, 1561 run_prod_code=False, 1562 afe=None, 1563 tko=None, 1564 pool=None, 1565 results_dir=None, 1566 max_runtime_mins=24*60, 1567 timeout_mins=24*60, 1568 file_bugs=False, 1569 suite_job_id=None, 1570 ignore_deps=False, 1571 extra_deps=None, 1572 priority=priorities.Priority.DEFAULT, 1573 forgiving_parser=True, 1574 wait_for_results=True, 1575 job_retry=False, 1576 max_retries=sys.maxint, 1577 offload_failures_only=False, 1578 test_source_build=None, 1579 job_keyvals=None, 1580 test_args=None 1581 ): 1582 """ 1583 Constructor 1584 1585 @param predicates: A list of callables that accept ControlData 1586 representations of control files. A test will be 1587 included in suite if all callables in this list 1588 return True on the given control file. 1589 @param tag: a string with which to tag jobs run in this suite. 1590 @param builds: the builds on which we're running this suite. 1591 @param board: the board on which we're running this suite. 1592 @param cf_getter: a control_file_getter.ControlFileGetter 1593 @param afe: an instance of AFE as defined in server/frontend.py. 1594 @param tko: an instance of TKO as defined in server/frontend.py. 1595 @param pool: Specify the pool of machines to use for scheduling 1596 purposes. 1597 @param run_prod_code: If true, the suite will run the test code that 1598 lives in prod aka the test code currently on the 1599 lab servers. 1600 @param results_dir: The directory where the job can write results to. 1601 This must be set if you want job_id of sub-jobs 1602 list in the job keyvals. 1603 @param max_runtime_mins: Maximum suite runtime, in minutes. 1604 @param timeout: Maximum job lifetime, in hours. 1605 @param suite_job_id: Job id that will act as parent id to all sub jobs. 1606 Default: None 1607 @param ignore_deps: True if jobs should ignore the DEPENDENCIES 1608 attribute and skip applying of dependency labels. 1609 (Default:False) 1610 @param extra_deps: A list of strings which are the extra DEPENDENCIES 1611 to add to each test being scheduled. 1612 @param priority: Integer priority level. Higher is more important. 1613 @param wait_for_results: Set to False to run the suite job without 1614 waiting for test jobs to finish. Default is 1615 True. 1616 @param job_retry: A bool value indicating whether jobs should be retired 1617 on failure. If True, the field 'JOB_RETRIES' in 1618 control files will be respected. If False, do not 1619 retry. 1620 @param max_retries: Maximum retry limit at suite level. 1621 Regardless how many times each individual test 1622 has been retried, the total number of retries 1623 happening in the suite can't exceed _max_retries. 1624 Default to sys.maxint. 1625 @param offload_failures_only: Only enable gs_offloading for failed 1626 jobs. 1627 @param test_source_build: Build that contains the server-side test code. 1628 @param job_keyvals: General job keyvals to be inserted into keyval file, 1629 which will be used by tko/parse later. 1630 @param test_args: A dict of args passed all the way to each individual 1631 test that will be actually ran. 1632 1633 """ 1634 tests = find_and_parse_tests( 1635 cf_getter, 1636 _ComposedPredicate(predicates), 1637 tag, 1638 forgiving_parser=forgiving_parser, 1639 run_prod_code=run_prod_code, 1640 test_args=test_args, 1641 ) 1642 super(Suite, self).__init__( 1643 tests=tests, 1644 tag=tag, 1645 builds=builds, 1646 board=board, 1647 afe=afe, 1648 tko=tko, 1649 pool=pool, 1650 results_dir=results_dir, 1651 max_runtime_mins=max_runtime_mins, 1652 timeout_mins=timeout_mins, 1653 file_bugs=file_bugs, 1654 suite_job_id=suite_job_id, 1655 ignore_deps=ignore_deps, 1656 extra_deps=extra_deps, 1657 priority=priority, 1658 wait_for_results=wait_for_results, 1659 job_retry=job_retry, 1660 max_retries=max_retries, 1661 offload_failures_only=offload_failures_only, 1662 test_source_build=test_source_build, 1663 job_keyvals=job_keyvals) 1664 1665 1666class ProvisionSuite(_BaseSuite): 1667 """ 1668 A suite for provisioning DUTs. 1669 1670 This is done by creating dummy_Pass tests. 1671 """ 1672 1673 1674 def __init__( 1675 self, 1676 tag, 1677 builds, 1678 board, 1679 count, 1680 devserver, 1681 cf_getter=None, 1682 run_prod_code=False, 1683 test_args=None, 1684 test_source_build=None, 1685 **kwargs): 1686 """ 1687 Constructor 1688 1689 @param tag: a string with which to tag jobs run in this suite. 1690 @param builds: the builds on which we're running this suite. 1691 @param board: the board on which we're running this suite. 1692 @param count: number of dummy tests to make 1693 @param devserver: the devserver which contains the build. 1694 @param cf_getter: a control_file_getter.ControlFileGetter. 1695 @param test_args: A dict of args passed all the way to each individual 1696 test that will be actually ran. 1697 @param test_source_build: Build that contains the server-side test code. 1698 @param kwargs: Various keyword arguments passed to 1699 _BaseSuite constructor. 1700 """ 1701 dummy_test = _load_dummy_test( 1702 builds, devserver, cf_getter, 1703 run_prod_code, test_args, test_source_build) 1704 1705 super(ProvisionSuite, self).__init__( 1706 tests=[dummy_test] * count, 1707 tag=tag, 1708 builds=builds, 1709 board=board, 1710 **kwargs) 1711 1712 1713def _load_dummy_test( 1714 builds, 1715 devserver, 1716 cf_getter=None, 1717 run_prod_code=False, 1718 test_args=None, 1719 test_source_build=None): 1720 """ 1721 Load and return the dummy pass test. 1722 1723 @param builds: the builds on which we're running this suite. 1724 @param devserver: the devserver which contains the build. 1725 @param cf_getter: a control_file_getter.ControlFileGetter. 1726 @param test_args: A dict of args passed all the way to each individual 1727 test that will be actually ran. 1728 @param test_source_build: Build that contains the server-side test code. 1729 """ 1730 if cf_getter is None: 1731 if run_prod_code: 1732 cf_getter = create_fs_getter(_AUTOTEST_DIR) 1733 else: 1734 build = get_test_source_build( 1735 builds, test_source_build=test_source_build) 1736 cf_getter = _create_ds_getter(build, devserver) 1737 retriever = _get_cf_retriever(cf_getter, 1738 run_prod_code=run_prod_code, 1739 test_args=test_args) 1740 return retriever.retrieve('dummy_Pass') 1741 1742 1743class _ComposedPredicate(object): 1744 """Return the composition of the predicates. 1745 1746 Predicates are functions that take a test control data object and 1747 return True of that test is to be included. The returned 1748 predicate's set is the intersection of all of the input predicates' 1749 sets (it returns True if all predicates return True). 1750 """ 1751 1752 def __init__(self, predicates): 1753 """Initialize instance. 1754 1755 @param predicates: Iterable of predicates. 1756 """ 1757 self._predicates = list(predicates) 1758 1759 def __repr__(self): 1760 return '{cls}({this._predicates!r})'.format( 1761 cls=type(self).__name__, 1762 this=self, 1763 ) 1764 1765 def __call__(self, control_data_): 1766 return all(f(control_data_) for f in self._predicates) 1767 1768 1769def _is_nonexistent_board_error(e): 1770 """Return True if error is caused by nonexistent board label. 1771 1772 As of this writing, the particular case we want looks like this: 1773 1774 1) e.problem_keys is a dictionary 1775 2) e.problem_keys['meta_hosts'] exists as the only key 1776 in the dictionary. 1777 3) e.problem_keys['meta_hosts'] matches this pattern: 1778 "Label "board:.*" not found" 1779 1780 We check for conditions 1) and 2) on the 1781 theory that they're relatively immutable. 1782 We don't check condition 3) because it seems 1783 likely to be a maintenance burden, and for the 1784 times when we're wrong, being right shouldn't 1785 matter enough (we _hope_). 1786 1787 @param e: proxy.ValidationError instance 1788 @returns: boolean 1789 """ 1790 return (isinstance(e.problem_keys, dict) 1791 and len(e.problem_keys) == 1 1792 and 'meta_hosts' in e.problem_keys) 1793 1794 1795class _ResultReporter(object): 1796 """Abstract base class for reporting test results. 1797 1798 Usually, this is used to report test failures. 1799 """ 1800 1801 __metaclass__ = abc.ABCMeta 1802 1803 @abc.abstractmethod 1804 def report(self, result): 1805 """Report test result. 1806 1807 @param result: Status instance for job. 1808 """ 1809 1810 1811class MemoryResultReporter(_ResultReporter): 1812 """Reporter that stores results internally for testing.""" 1813 1814 def __init__(self): 1815 self.results = [] 1816 1817 def report(self, result): 1818 self.results.append(result) 1819 1820 1821class _BugResultReporter(_ResultReporter): 1822 """ 1823 Report test results as bugs. 1824 """ 1825 1826 def __init__(self, suite, bug_reporter, bug_template): 1827 """ 1828 Instantiate instance. 1829 1830 @param suite: _BaseSuite instance 1831 @param bug_reporter: Reporter instance for reporting bugs. 1832 @param bug_template: A template dictionary specifying the default bug 1833 filing options for failures in this suite. 1834 """ 1835 self._suite = suite 1836 self._bug_reporter = bug_reporter 1837 self._bug_template = bug_template 1838 1839 def report(self, result): 1840 self._suite._file_bug(result, self._bug_reporter, self._bug_template) 1841 1842 1843class _EmailResultReporter(_ResultReporter): 1844 """ 1845 Report test results as email. 1846 1847 @param suite: _BaseSuite instance 1848 @param bug_template: A template dictionary specifying the default bug 1849 filing options for failures in this suite. 1850 """ 1851 1852 def __init__(self, suite, bug_template): 1853 self._suite = suite 1854 self._bug_template = bug_template 1855 1856 def report(self, result): 1857 # reporting modules have dependency on external 1858 # packages, e.g., httplib2 Such dependency can cause 1859 # issue to any module tries to import suite.py without 1860 # building site-packages first. Since the reporting 1861 # modules are only used in this function, move the 1862 # imports here avoid the requirement of building site 1863 # packages to use other functions in this module. 1864 from autotest_lib.server.cros.dynamic_suite import reporting 1865 1866 reporting.send_email( 1867 self._suite._get_test_bug(result), 1868 self._suite._get_bug_template(result, self._bug_template)) 1869