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 5 6 import logging 7 import re 8 import subprocess 9 10 import base_event 11 import deduping_scheduler 12 import driver 13 import manifest_versions 14 from distutils import version 15 from constants import Labels 16 from constants import Builds 17 18 import common 19 from autotest_lib.client.common_lib import global_config 20 from autotest_lib.server import utils as server_utils 21 from autotest_lib.server.cros.dynamic_suite import constants 22 23 24 CONFIG = global_config.global_config 25 26 OS_TYPE_CROS = 'cros' 27 OS_TYPE_BRILLO = 'brillo' 28 OS_TYPE_ANDROID = 'android' 29 OS_TYPES = {OS_TYPE_CROS, OS_TYPE_BRILLO, OS_TYPE_ANDROID} 30 OS_TYPES_LAUNCH_CONTROL = {OS_TYPE_BRILLO, OS_TYPE_ANDROID} 31 32 _WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 33 'Sunday'] 34 35 # regex to parse the dut count from board label. Note that the regex makes sure 36 # there is only one board specified in `boards` 37 TESTBED_DUT_COUNT_REGEX = '[^,]*-(\d+)' 38 39 class MalformedConfigEntry(Exception): 40 """Raised to indicate a failure to parse a Task out of a config.""" 41 pass 42 43 44 BARE_BRANCHES = ['factory', 'firmware'] 45 46 47 def PickBranchName(type, milestone): 48 """Pick branch name. If type is among BARE_BRANCHES, return type, 49 otherwise, return milestone. 50 51 @param type: type of the branch, e.g., 'release', 'factory', or 'firmware' 52 @param milestone: CrOS milestone number 53 """ 54 if type in BARE_BRANCHES: 55 return type 56 return milestone 57 58 59 class TotMilestoneManager(object): 60 """A class capable of converting tot string to milestone numbers. 61 62 This class is used as a cache for the tot milestone, so we don't 63 repeatedly hit google storage for all O(100) tasks in suite 64 scheduler's ini file. 65 """ 66 67 __metaclass__ = server_utils.Singleton 68 69 # True if suite_scheduler is running for sanity check. When it's set to 70 # True, the code won't make gsutil call to get the actual tot milestone to 71 # avoid dependency on the installation of gsutil to run sanity check. 72 is_sanity = False 73 74 75 @staticmethod 76 def _tot_milestone(): 77 """Get the tot milestone, eg: R40 78 79 @returns: A string representing the Tot milestone as declared by 80 the LATEST_BUILD_URL, or an empty string if LATEST_BUILD_URL 81 doesn't exist. 82 """ 83 if TotMilestoneManager.is_sanity: 84 logging.info('suite_scheduler is running for sanity purpose, no ' 85 'need to get the actual tot milestone string.') 86 return 'R40' 87 88 cmd = ['gsutil', 'cat', constants.LATEST_BUILD_URL] 89 proc = subprocess.Popen( 90 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 91 stdout, stderr = proc.communicate() 92 if proc.poll(): 93 logging.warning('Failed to get latest build: %s', stderr) 94 return '' 95 return stdout.split('-')[0] 96 97 98 def refresh(self): 99 """Refresh the tot milestone string managed by this class.""" 100 self.tot = self._tot_milestone() 101 102 103 def __init__(self): 104 """Initialize a TotMilestoneManager.""" 105 self.refresh() 106 107 108 def ConvertTotSpec(self, tot_spec): 109 """Converts a tot spec to the appropriate milestone. 110 111 Assume tot is R40: 112 tot -> R40 113 tot-1 -> R39 114 tot-2 -> R38 115 tot-(any other numbers) -> R40 116 117 With the last option one assumes that a malformed configuration that has 118 'tot' in it, wants at least tot. 119 120 @param tot_spec: A string representing the tot spec. 121 @raises MalformedConfigEntry: If the tot_spec doesn't match the 122 expected format. 123 """ 124 tot_spec = tot_spec.lower() 125 match = re.match('(tot)[-]?(1$|2$)?', tot_spec) 126 if not match: 127 raise MalformedConfigEntry( 128 "%s isn't a valid branch spec." % tot_spec) 129 tot_mstone = self.tot 130 num_back = match.groups()[1] 131 if num_back: 132 tot_mstone_num = tot_mstone.lstrip('R') 133 tot_mstone = tot_mstone.replace( 134 tot_mstone_num, str(int(tot_mstone_num)-int(num_back))) 135 return tot_mstone 136 137 138 class Task(object): 139 """Represents an entry from the scheduler config. Can schedule itself. 140 141 Each entry from the scheduler config file maps one-to-one to a 142 Task. Each instance has enough info to schedule itself 143 on-demand with the AFE. 144 145 This class also overrides __hash__() and all comparator methods to enable 146 correct use in dicts, sets, etc. 147 """ 148 149 150 @staticmethod 151 def CreateFromConfigSection(config, section): 152 """Create a Task from a section of a config file. 153 154 The section to parse should look like this: 155 [TaskName] 156 suite: suite_to_run # Required 157 run_on: event_on which to run # Required 158 hour: integer of the hour to run, only applies to nightly. # Optional 159 branch_specs: factory,firmware,>=R12 or ==R12 # Optional 160 pool: pool_of_devices # Optional 161 num: sharding_factor # int, Optional 162 boards: board1, board2 # comma seperated string, Optional 163 # Settings for Launch Control builds only: 164 os_type: brillo # Type of OS, e.g., cros, brillo, android. Default is 165 cros. Required for android/brillo builds. 166 branches: git_mnc_release # comma separated string of Launch Control 167 branches. Required and only applicable for android/brillo 168 builds. 169 targets: dragonboard-eng # comma separated string of build targets. 170 Required and only applicable for android/brillo builds. 171 testbed_dut_count: Number of duts to test when using a testbed. 172 173 By default, Tasks run on all release branches, not factory or firmware. 174 175 @param config: a ForgivingConfigParser. 176 @param section: the section to parse into a Task. 177 @return keyword, Task object pair. One or both will be None on error. 178 @raise MalformedConfigEntry if there's a problem parsing |section|. 179 """ 180 if not config.has_section(section): 181 raise MalformedConfigEntry('unknown section %s' % section) 182 183 allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num', 184 'boards', 'file_bugs', 'cros_build_spec', 185 'firmware_rw_build_spec', 'firmware_ro_build_spec', 186 'test_source', 'job_retry', 'hour', 'day', 'branches', 187 'targets', 'os_type', 'no_delay']) 188 # The parameter of union() is the keys under the section in the config 189 # The union merges this with the allowed set, so if any optional keys 190 # are omitted, then they're filled in. If any extra keys are present, 191 # then they will expand unioned set, causing it to fail the following 192 # comparison against the allowed set. 193 section_headers = allowed.union(dict(config.items(section)).keys()) 194 if allowed != section_headers: 195 raise MalformedConfigEntry('unknown entries: %s' % 196 ", ".join(map(str, section_headers.difference(allowed)))) 197 198 keyword = config.getstring(section, 'run_on') 199 hour = config.getstring(section, 'hour') 200 suite = config.getstring(section, 'suite') 201 branch_specs = config.getstring(section, 'branch_specs') 202 pool = config.getstring(section, 'pool') 203 boards = config.getstring(section, 'boards') 204 file_bugs = config.getboolean(section, 'file_bugs') 205 cros_build_spec = config.getstring(section, 'cros_build_spec') 206 firmware_rw_build_spec = config.getstring( 207 section, 'firmware_rw_build_spec') 208 firmware_ro_build_spec = config.getstring( 209 section, 'firmware_ro_build_spec') 210 test_source = config.getstring(section, 'test_source') 211 job_retry = config.getboolean(section, 'job_retry') 212 no_delay = config.getboolean(section, 'no_delay') 213 for klass in driver.Driver.EVENT_CLASSES: 214 if klass.KEYWORD == keyword: 215 priority = klass.PRIORITY 216 timeout = klass.TIMEOUT 217 break 218 else: 219 priority = None 220 timeout = None 221 try: 222 num = config.getint(section, 'num') 223 except ValueError as e: 224 raise MalformedConfigEntry("Ill-specified 'num': %r" % e) 225 if not keyword: 226 raise MalformedConfigEntry('No event to |run_on|.') 227 if not suite: 228 raise MalformedConfigEntry('No |suite|') 229 try: 230 hour = config.getint(section, 'hour') 231 except ValueError as e: 232 raise MalformedConfigEntry("Ill-specified 'hour': %r" % e) 233 if hour is not None and (hour < 0 or hour > 23): 234 raise MalformedConfigEntry( 235 '`hour` must be an integer between 0 and 23.') 236 if hour is not None and keyword != 'nightly': 237 raise MalformedConfigEntry( 238 '`hour` is the trigger time that can only apply to nightly ' 239 'event.') 240 241 testbed_dut_count = None 242 if boards: 243 match = re.match(TESTBED_DUT_COUNT_REGEX, boards) 244 if match: 245 testbed_dut_count = int(match.group(1)) 246 247 try: 248 day = config.getint(section, 'day') 249 except ValueError as e: 250 raise MalformedConfigEntry("Ill-specified 'day': %r" % e) 251 if day is not None and (day < 0 or day > 6): 252 raise MalformedConfigEntry( 253 '`day` must be an integer between 0 and 6, where 0 is for ' 254 'Monday and 6 is for Sunday.') 255 if day is not None and keyword != 'weekly': 256 raise MalformedConfigEntry( 257 '`day` is the trigger of the day of a week, that can only ' 258 'apply to weekly events.') 259 260 specs = [] 261 if branch_specs: 262 specs = re.split('\s*,\s*', branch_specs) 263 Task.CheckBranchSpecs(specs) 264 265 os_type = config.getstring(section, 'os_type') or OS_TYPE_CROS 266 if os_type not in OS_TYPES: 267 raise MalformedConfigEntry('`os_type` must be one of %s' % OS_TYPES) 268 269 lc_branches = config.getstring(section, 'branches') 270 lc_targets = config.getstring(section, 'targets') 271 if os_type == OS_TYPE_CROS and (lc_branches or lc_targets): 272 raise MalformedConfigEntry( 273 '`branches` and `targets` are only supported for Launch ' 274 'Control builds, not ChromeOS builds.') 275 if (os_type in OS_TYPES_LAUNCH_CONTROL and 276 (not lc_branches or not lc_targets)): 277 raise MalformedConfigEntry( 278 '`branches` and `targets` must be specified for Launch ' 279 'Control builds.') 280 if (os_type in OS_TYPES_LAUNCH_CONTROL and boards and 281 not testbed_dut_count): 282 raise MalformedConfigEntry( 283 '`boards` for Launch Control builds are retrieved from ' 284 '`targets` setting, it should not be set for Launch ' 285 'Control builds.') 286 if os_type == OS_TYPE_CROS and testbed_dut_count: 287 raise MalformedConfigEntry( 288 'testbed_dut_count is only supported for Launch Control ' 289 'builds testing with testbed.') 290 291 # Extract boards from targets list. 292 if os_type in OS_TYPES_LAUNCH_CONTROL: 293 boards = '' 294 for target in lc_targets.split(','): 295 board_name, _ = server_utils.parse_launch_control_target( 296 target.strip()) 297 # Translate board name in build target to the actual board name. 298 board_name = server_utils.ANDROID_TARGET_TO_BOARD_MAP.get( 299 board_name, board_name) 300 boards += '%s,' % board_name 301 boards = boards.strip(',') 302 303 return keyword, Task(section, suite, specs, pool, num, boards, 304 priority, timeout, 305 file_bugs=file_bugs if file_bugs else False, 306 cros_build_spec=cros_build_spec, 307 firmware_rw_build_spec=firmware_rw_build_spec, 308 firmware_ro_build_spec=firmware_ro_build_spec, 309 test_source=test_source, job_retry=job_retry, 310 hour=hour, day=day, os_type=os_type, 311 launch_control_branches=lc_branches, 312 launch_control_targets=lc_targets, 313 testbed_dut_count=testbed_dut_count, 314 no_delay=no_delay) 315 316 317 @staticmethod 318 def CheckBranchSpecs(branch_specs): 319 """Make sure entries in the list branch_specs are correctly formed. 320 321 We accept any of BARE_BRANCHES in |branch_specs|, as 322 well as _one_ string of the form '>=RXX' or '==RXX', where 'RXX' is a 323 CrOS milestone number. 324 325 @param branch_specs: an iterable of branch specifiers. 326 @raise MalformedConfigEntry if there's a problem parsing |branch_specs|. 327 """ 328 have_seen_numeric_constraint = False 329 for branch in branch_specs: 330 if branch in BARE_BRANCHES: 331 continue 332 if not have_seen_numeric_constraint: 333 #TODO(beeps): Why was <= dropped on the floor? 334 if branch.startswith('>=R') or branch.startswith('==R'): 335 have_seen_numeric_constraint = True 336 elif 'tot' in branch: 337 TotMilestoneManager().ConvertTotSpec( 338 branch[branch.index('tot'):]) 339 have_seen_numeric_constraint = True 340 continue 341 raise MalformedConfigEntry("%s isn't a valid branch spec." % branch) 342 343 344 def __init__(self, name, suite, branch_specs, pool=None, num=None, 345 boards=None, priority=None, timeout=None, file_bugs=False, 346 cros_build_spec=None, firmware_rw_build_spec=None, 347 firmware_ro_build_spec=None, test_source=None, job_retry=False, 348 hour=None, day=None, os_type=OS_TYPE_CROS, 349 launch_control_branches=None, launch_control_targets=None, 350 testbed_dut_count=None, no_delay=False): 351 """Constructor 352 353 Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs, 354 we'll store them such that _FitsSpec() can be used to check whether a 355 given branch 'fits' with the specifications passed in here. 356 For example, given branch_specs = ['factory', '>=R18'], we'd set things 357 up so that _FitsSpec() would return True for 'factory', or 'RXX' 358 where XX is a number >= 18. Same check is done for branch_specs = [ 359 'factory', '==R18'], which limit the test to only one specific branch. 360 361 Given branch_specs = ['factory', 'firmware'], _FitsSpec() 362 would pass only those two specific strings. 363 364 Example usage: 365 t = Task('Name', 'suite', ['factory', '>=R18']) 366 t._FitsSpec('factory') # True 367 t._FitsSpec('R19') # True 368 t._FitsSpec('R17') # False 369 t._FitsSpec('firmware') # False 370 t._FitsSpec('goober') # False 371 372 t = Task('Name', 'suite', ['factory', '==R18']) 373 t._FitsSpec('R19') # False, branch does not equal to 18 374 t._FitsSpec('R18') # True 375 t._FitsSpec('R17') # False 376 377 cros_build_spec and firmware_rw_build_spec are set for tests require 378 firmware update on the dut. Only one of them can be set. 379 For example: 380 branch_specs: ==tot 381 firmware_rw_build_spec: firmware 382 test_source: cros 383 This will run test using latest build on firmware branch, and the latest 384 ChromeOS build on ToT. The test source build is ChromeOS build. 385 386 branch_specs: firmware 387 cros_build_spec: ==tot-1 388 test_source: firmware_rw 389 This will run test using latest build on firmware branch, and the latest 390 ChromeOS build on dev channel (ToT-1). The test source build is the 391 firmware RW build. 392 393 branch_specs: ==tot 394 firmware_rw_build_spec: cros 395 test_source: cros 396 This will run test using latest ChromeOS and firmware RW build on ToT. 397 ChromeOS build on ToT. The test source build is ChromeOS build. 398 399 @param name: name of this task, e.g. 'NightlyPower' 400 @param suite: the name of the suite to run, e.g. 'bvt' 401 @param branch_specs: a pre-vetted iterable of branch specifiers, 402 e.g. ['>=R18', 'factory'] 403 @param pool: the pool of machines to use for scheduling purposes. 404 Default: None 405 @param num: the number of devices across which to shard the test suite. 406 Type: integer or None 407 Default: None 408 @param boards: A comma separated list of boards to run this task on. 409 Default: Run on all boards. 410 @param priority: The string name of a priority from 411 client.common_lib.priorities.Priority. 412 @param timeout: The max lifetime of the suite in hours. 413 @param file_bugs: True if bug filing is desired for the suite created 414 for this task. 415 @param cros_build_spec: Spec used to determine the ChromeOS build to 416 test with a firmware build, e.g., tot, R41 etc. 417 @param firmware_rw_build_spec: Spec used to determine the firmware RW 418 build test with a ChromeOS build. 419 @param firmware_ro_build_spec: Spec used to determine the firmware RO 420 build test with a ChromeOS build. 421 @param test_source: The source of test code when firmware will be 422 updated in the test. The value can be `firmware_rw`, 423 `firmware_ro` or `cros`. 424 @param job_retry: Set to True to enable job-level retry. Default is 425 False. 426 @param hour: An integer specifying the hour that a nightly run should 427 be triggered, default is set to 21. 428 @param day: An integer specifying the day of a week that a weekly run 429 should be triggered, default is set to 5, which is Saturday. 430 @param os_type: Type of OS, e.g., cros, brillo, android. Default is 431 cros. The argument is required for android/brillo builds. 432 @param launch_control_branches: Comma separated string of Launch Control 433 branches. The argument is required and only applicable for 434 android/brillo builds. 435 @param launch_control_targets: Comma separated string of build targets 436 for Launch Control builds. The argument is required and only 437 applicable for android/brillo builds. 438 @param testbed_dut_count: Number of duts to test when using a testbed. 439 @param no_delay: Set to True to allow suite to be created without 440 configuring delay_minutes. Default is False. 441 """ 442 self._name = name 443 self._suite = suite 444 self._branch_specs = branch_specs 445 self._pool = pool 446 self._num = num 447 self._priority = priority 448 self._timeout = timeout 449 self._file_bugs = file_bugs 450 self._cros_build_spec = cros_build_spec 451 self._firmware_rw_build_spec = firmware_rw_build_spec 452 self._firmware_ro_build_spec = firmware_ro_build_spec 453 self._test_source = test_source 454 self._job_retry = job_retry 455 self._hour = hour 456 self._day = day 457 self._os_type = os_type 458 self._launch_control_branches = ( 459 [b.strip() for b in launch_control_branches.split(',')] 460 if launch_control_branches else []) 461 self._launch_control_targets = ( 462 [t.strip() for t in launch_control_targets.split(',')] 463 if launch_control_targets else []) 464 self._testbed_dut_count = testbed_dut_count 465 self._no_delay = no_delay 466 467 if ((self._firmware_rw_build_spec or self._firmware_ro_build_spec or 468 cros_build_spec) and 469 not self.test_source in [Builds.FIRMWARE_RW, Builds.FIRMWARE_RO, 470 Builds.CROS]): 471 raise MalformedConfigEntry( 472 'You must specify the build for test source. It can only ' 473 'be `firmware_rw`, `firmware_ro` or `cros`.') 474 if self._firmware_rw_build_spec and cros_build_spec: 475 raise MalformedConfigEntry( 476 'You cannot specify both firmware_rw_build_spec and ' 477 'cros_build_spec. firmware_rw_build_spec is used to specify' 478 ' a firmware build when the suite requires firmware to be ' 479 'updated in the dut, its value can only be `firmware` or ' 480 '`cros`. cros_build_spec is used to specify a ChromeOS ' 481 'build when build_specs is set to firmware.') 482 if (self._firmware_rw_build_spec and 483 self._firmware_rw_build_spec not in ['firmware', 'cros']): 484 raise MalformedConfigEntry( 485 'firmware_rw_build_spec can only be empty, firmware or ' 486 'cros. It does not support other build type yet.') 487 488 if os_type not in OS_TYPES_LAUNCH_CONTROL and self._testbed_dut_count: 489 raise MalformedConfigEntry( 490 'testbed_dut_count is only applicable to testbed to run ' 491 'test with builds from Launch Control.') 492 493 self._bare_branches = [] 494 self._version_equal_constraint = False 495 self._version_gte_constraint = False 496 self._version_lte_constraint = False 497 if not branch_specs: 498 # Any milestone is OK. 499 self._numeric_constraint = version.LooseVersion('0') 500 else: 501 self._numeric_constraint = None 502 for spec in branch_specs: 503 if 'tot' in spec.lower(): 504 tot_str = spec[spec.index('tot'):] 505 spec = spec.replace( 506 tot_str, TotMilestoneManager().ConvertTotSpec( 507 tot_str)) 508 if spec.startswith('>='): 509 self._numeric_constraint = version.LooseVersion( 510 spec.lstrip('>=R')) 511 self._version_gte_constraint = True 512 elif spec.startswith('<='): 513 self._numeric_constraint = version.LooseVersion( 514 spec.lstrip('<=R')) 515 self._version_lte_constraint = True 516 elif spec.startswith('=='): 517 self._version_equal_constraint = True 518 self._numeric_constraint = version.LooseVersion( 519 spec.lstrip('==R')) 520 else: 521 self._bare_branches.append(spec) 522 523 # Since we expect __hash__() and other comparator methods to be used 524 # frequently by set operations, and they use str() a lot, pre-compute 525 # the string representation of this object. 526 if num is None: 527 numStr = '[Default num]' 528 else: 529 numStr = '%d' % num 530 531 if boards is None: 532 self._boards = set() 533 boardsStr = '[All boards]' 534 else: 535 self._boards = set([x.strip() for x in boards.split(',')]) 536 boardsStr = boards 537 538 time_str = '' 539 if self._hour: 540 time_str = ' Run at %d:00.' % self._hour 541 elif self._day: 542 time_str = ' Run on %s.' % _WEEKDAYS[self._day] 543 if os_type == OS_TYPE_CROS: 544 self._str = ('%s: %s on %s with pool %s, boards [%s], file_bugs = ' 545 '%s across %s machines.%s' % 546 (self.__class__.__name__, suite, branch_specs, pool, 547 boardsStr, self._file_bugs, numStr, time_str)) 548 else: 549 testbed_dut_count_str = '.' 550 if self._testbed_dut_count: 551 testbed_dut_count_str = (', each with %d duts.' % 552 self._testbed_dut_count) 553 self._str = ('%s: %s on branches %s and targets %s with pool %s, ' 554 'boards [%s], file_bugs = %s across %s machines%s%s' % 555 (self.__class__.__name__, suite, 556 launch_control_branches, launch_control_targets, 557 pool, boardsStr, self._file_bugs, numStr, 558 testbed_dut_count_str, time_str)) 559 560 561 def _FitsSpec(self, branch): 562 """Checks if a branch is deemed OK by this instance's branch specs. 563 564 When called on a branch name, will return whether that branch 565 'fits' the specifications stored in self._bare_branches, 566 self._numeric_constraint, self._version_equal_constraint, 567 self._version_gte_constraint and self._version_lte_constraint. 568 569 @param branch: the branch to check. 570 @return True if b 'fits' with stored specs, False otherwise. 571 """ 572 if branch in BARE_BRANCHES: 573 return branch in self._bare_branches 574 if self._numeric_constraint: 575 if self._version_equal_constraint: 576 return version.LooseVersion(branch) == self._numeric_constraint 577 elif self._version_gte_constraint: 578 return version.LooseVersion(branch) >= self._numeric_constraint 579 elif self._version_lte_constraint: 580 return version.LooseVersion(branch) <= self._numeric_constraint 581 else: 582 # Default to great or equal constraint. 583 return version.LooseVersion(branch) >= self._numeric_constraint 584 else: 585 return False 586 587 588 @property 589 def name(self): 590 """Name of this task, e.g. 'NightlyPower'.""" 591 return self._name 592 593 594 @property 595 def suite(self): 596 """Name of the suite to run, e.g. 'bvt'.""" 597 return self._suite 598 599 600 @property 601 def branch_specs(self): 602 """a pre-vetted iterable of branch specifiers, 603 e.g. ['>=R18', 'factory'].""" 604 return self._branch_specs 605 606 607 @property 608 def pool(self): 609 """The pool of machines to use for scheduling purposes.""" 610 return self._pool 611 612 613 @property 614 def num(self): 615 """The number of devices across which to shard the test suite. 616 Type: integer or None""" 617 return self._num 618 619 620 @property 621 def boards(self): 622 """The boards on which to run this suite. 623 Type: Iterable of strings""" 624 return self._boards 625 626 627 @property 628 def priority(self): 629 """The priority of the suite""" 630 return self._priority 631 632 633 @property 634 def timeout(self): 635 """The maximum lifetime of the suite in hours.""" 636 return self._timeout 637 638 639 @property 640 def cros_build_spec(self): 641 """The build spec of ChromeOS to test with a firmware build.""" 642 return self._cros_build_spec 643 644 645 @property 646 def firmware_rw_build_spec(self): 647 """The build spec of RW firmware to test with a ChromeOS build. 648 649 The value can be firmware or cros. 650 """ 651 return self._firmware_rw_build_spec 652 653 654 @property 655 def firmware_ro_build_spec(self): 656 """The build spec of RO firmware to test with a ChromeOS build. 657 658 The value can be stable, firmware or cros, where stable is the stable 659 firmware build retrieved from stable_version table. 660 """ 661 return self._firmware_ro_build_spec 662 663 664 @property 665 def test_source(self): 666 """Source of the test code, value can be `firmware_rw`, `firmware_ro` or 667 `cros`.""" 668 return self._test_source 669 670 671 @property 672 def hour(self): 673 """An integer specifying the hour that a nightly run should be triggered 674 """ 675 return self._hour 676 677 678 @property 679 def day(self): 680 """An integer specifying the day of a week that a weekly run should be 681 triggered""" 682 return self._day 683 684 685 @property 686 def os_type(self): 687 """Type of OS, e.g., cros, brillo, android.""" 688 return self._os_type 689 690 691 @property 692 def launch_control_branches(self): 693 """A list of Launch Control builds.""" 694 return self._launch_control_branches 695 696 697 @property 698 def launch_control_targets(self): 699 """A list of Launch Control targets.""" 700 return self._launch_control_targets 701 702 703 def __str__(self): 704 return self._str 705 706 707 def __repr__(self): 708 return self._str 709 710 711 def __lt__(self, other): 712 return str(self) < str(other) 713 714 715 def __le__(self, other): 716 return str(self) <= str(other) 717 718 719 def __eq__(self, other): 720 return str(self) == str(other) 721 722 723 def __ne__(self, other): 724 return str(self) != str(other) 725 726 727 def __gt__(self, other): 728 return str(self) > str(other) 729 730 731 def __ge__(self, other): 732 return str(self) >= str(other) 733 734 735 def __hash__(self): 736 """Allows instances to be correctly deduped when used in a set.""" 737 return hash(str(self)) 738 739 740 def _GetCrOSBuild(self, mv, board): 741 """Get the ChromeOS build name to test with firmware build. 742 743 The ChromeOS build to be used is determined by `self.cros_build_spec`. 744 Its value can be: 745 tot: use the latest ToT build. 746 tot-x: use the latest build in x milestone before ToT. 747 Rxx: use the latest build on xx milestone. 748 749 @param board: the board against which to run self._suite. 750 @param mv: an instance of manifest_versions.ManifestVersions. 751 752 @return: The ChromeOS build name to test with firmware build. 753 754 """ 755 if not self.cros_build_spec: 756 return None 757 if self.cros_build_spec.startswith('tot'): 758 milestone = TotMilestoneManager().ConvertTotSpec( 759 self.cros_build_spec)[1:] 760 elif self.cros_build_spec.startswith('R'): 761 milestone = self.cros_build_spec[1:] 762 milestone, latest_manifest = mv.GetLatestManifest( 763 board, 'release', milestone=milestone) 764 latest_build = base_event.BuildName(board, 'release', milestone, 765 latest_manifest) 766 logging.debug('Found latest build of %s for spec %s: %s', 767 board, self.cros_build_spec, latest_build) 768 return latest_build 769 770 771 def _GetFirmwareBuild(self, spec, mv, board): 772 """Get the firmware build name to test with ChromeOS build. 773 774 @param spec: build spec for RO or RW firmware, e.g., firmware, cros. 775 For RO firmware, the value can also be in the format of 776 released_ro_X, where X is the index of the list or RO builds 777 defined in global config RELEASED_RO_BUILDS_[board]. 778 For example, for spec `released_ro_2`, and global config 779 CROS/RELEASED_RO_BUILDS_veyron_jerry: build1,build2 780 the return firmare RO build should be build2. 781 @param mv: an instance of manifest_versions.ManifestVersions. 782 @param board: the board against which to run self._suite. 783 784 @return: The firmware build name to test with ChromeOS build. 785 """ 786 if spec == 'stable': 787 # TODO(crbug.com/577316): Query stable RO firmware. 788 raise NotImplementedError('`stable` RO firmware build is not ' 789 'supported yet.') 790 if not spec: 791 return None 792 793 if spec.startswith('released_ro_'): 794 index = int(spec[12:]) 795 released_ro_builds = CONFIG.get_config_value( 796 'CROS', 'RELEASED_RO_BUILDS_%s' % board, type=str, 797 default='').split(',') 798 if not released_ro_builds or len(released_ro_builds) < index: 799 return None 800 else: 801 return released_ro_builds[index-1] 802 803 # build_type is the build type of the firmware build, e.g., factory, 804 # firmware or release. If spec is set to cros, build type should be 805 # mapped to release. 806 build_type = 'release' if spec == 'cros' else spec 807 latest_milestone, latest_manifest = mv.GetLatestManifest( 808 board, build_type) 809 latest_build = base_event.BuildName(board, build_type, latest_milestone, 810 latest_manifest) 811 logging.debug('Found latest firmware build of %s for spec %s: %s', 812 board, spec, latest_build) 813 return latest_build 814 815 816 def AvailableHosts(self, scheduler, board): 817 """Query what hosts are able to run a test on a board and pool 818 combination. 819 820 @param scheduler: an instance of DedupingScheduler, as defined in 821 deduping_scheduler.py 822 @param board: the board against which one wants to run the test. 823 @return The list of hosts meeting the board and pool requirements, 824 or None if no hosts were found.""" 825 if self._boards and board not in self._boards: 826 return [] 827 828 board_label = Labels.BOARD_PREFIX + board 829 if self._testbed_dut_count: 830 board_label += '-%d' % self._testbed_dut_count 831 labels = [board_label] 832 if self._pool: 833 labels.append(Labels.POOL_PREFIX + self._pool) 834 835 return scheduler.CheckHostsExist(multiple_labels=labels) 836 837 838 def ShouldHaveAvailableHosts(self): 839 """As a sanity check, return true if we know for certain that 840 we should be able to schedule this test. If we claim this test 841 should be able to run, and it ends up not being scheduled, then 842 a warning will be reported. 843 844 @return True if this test should be able to run, False otherwise. 845 """ 846 return self._pool == 'bvt' 847 848 849 def _ScheduleSuite(self, scheduler, cros_build, firmware_rw_build, 850 firmware_ro_build, test_source_build, 851 launch_control_build, board, force, run_prod_code=False): 852 """Try to schedule a suite with given build and board information. 853 854 @param scheduler: an instance of DedupingScheduler, as defined in 855 deduping_scheduler.py 856 @oaran build: Build to run suite for, e.g., 'daisy-release/R18-1655.0.0' 857 and 'git_mnc_release/shamu-eng/123'. 858 @param firmware_rw_build: Firmware RW build to run test with. 859 @param firmware_ro_build: Firmware RO build to run test with. 860 @param test_source_build: Test source build, used for server-side 861 packaging. 862 @param launch_control_build: Name of a Launch Control build, e.g., 863 'git_mnc_release/shamu-eng/123' 864 @param board: the board against which to run self._suite. 865 @param force: Always schedule the suite. 866 @param run_prod_code: If True, the suite will run the test code that 867 lives in prod aka the test code currently on the 868 lab servers. If False, the control files and test 869 code for this suite run will be retrieved from the 870 build artifacts. Default is False. 871 """ 872 test_source_build_msg = ( 873 ' Test source build is %s.' % test_source_build 874 if test_source_build else '') 875 firmware_rw_build_msg = ( 876 ' Firmware RW build is %s.' % firmware_rw_build 877 if firmware_rw_build else '') 878 firmware_ro_build_msg = ( 879 ' Firmware RO build is %s.' % firmware_ro_build 880 if firmware_ro_build else '') 881 # If testbed_dut_count is set, the suite is for testbed. Update build 882 # and board with the dut count. 883 if self._testbed_dut_count: 884 launch_control_build = '%s#%d' % (launch_control_build, 885 self._testbed_dut_count) 886 test_source_build = launch_control_build 887 board = '%s-%d' % (board, self._testbed_dut_count) 888 build_string = cros_build or launch_control_build 889 logging.debug('Schedule %s for build %s.%s%s%s', 890 self._suite, build_string, test_source_build_msg, 891 firmware_rw_build_msg, firmware_ro_build_msg) 892 893 if not scheduler.ScheduleSuite( 894 self._suite, board, cros_build, self._pool, self._num, 895 self._priority, self._timeout, force, 896 file_bugs=self._file_bugs, 897 firmware_rw_build=firmware_rw_build, 898 firmware_ro_build=firmware_ro_build, 899 test_source_build=test_source_build, 900 job_retry=self._job_retry, 901 launch_control_build=launch_control_build, 902 run_prod_code=run_prod_code, 903 testbed_dut_count=self._testbed_dut_count, 904 no_delay=self._no_delay): 905 logging.info('Skipping scheduling %s on %s for %s', 906 self._suite, build_string, board) 907 908 909 def _Run_CrOS_Builds(self, scheduler, branch_builds, board, force=False, 910 mv=None): 911 """Run this task for CrOS builds. Returns False if it should be 912 destroyed. 913 914 Execute this task. Attempt to schedule the associated suite. 915 Return True if this task should be kept around, False if it 916 should be destroyed. This allows for one-shot Tasks. 917 918 @param scheduler: an instance of DedupingScheduler, as defined in 919 deduping_scheduler.py 920 @param branch_builds: a dict mapping branch name to the build(s) to 921 install for that branch, e.g. 922 {'R18': ['x86-alex-release/R18-1655.0.0'], 923 'R19': ['x86-alex-release/R19-2077.0.0']} 924 @param board: the board against which to run self._suite. 925 @param force: Always schedule the suite. 926 @param mv: an instance of manifest_versions.ManifestVersions. 927 928 @return True if the task should be kept, False if not 929 930 """ 931 logging.info('Running %s on %s', self._name, board) 932 is_firmware_build = 'firmware' in self.branch_specs 933 934 # firmware_xx_build is only needed if firmware_xx_build_spec is given. 935 firmware_rw_build = None 936 firmware_ro_build = None 937 try: 938 if is_firmware_build: 939 # When build specified in branch_specs is a firmware build, 940 # we need a ChromeOS build to test with the firmware build. 941 cros_build = self._GetCrOSBuild(mv, board) 942 elif self.firmware_rw_build_spec or self.firmware_ro_build_spec: 943 # When firmware_xx_build_spec is specified, the test involves 944 # updating the RW firmware by firmware build specified in 945 # firmware_xx_build_spec. 946 firmware_rw_build = self._GetFirmwareBuild( 947 self.firmware_rw_build_spec, mv, board) 948 firmware_ro_build = self._GetFirmwareBuild( 949 self.firmware_ro_build_spec, mv, board) 950 # If RO firmware is specified, force to create suite, because 951 # dedupe based on test source build does not reflect the change 952 # of RO firmware. 953 if firmware_ro_build: 954 force = True 955 except manifest_versions.QueryException as e: 956 logging.error(e) 957 logging.error('Running %s on %s is failed. Failed to find build ' 958 'required to run the suite.', self._name, board) 959 return False 960 961 # Return if there is no firmware RO build found for given spec. 962 if not firmware_ro_build and self.firmware_ro_build_spec: 963 return True 964 965 builds = [] 966 for branch, build in branch_builds.iteritems(): 967 logging.info('Checking if %s fits spec %r', 968 branch, self.branch_specs) 969 if self._FitsSpec(branch): 970 logging.debug('Build %s fits the spec.', build) 971 builds.extend(build) 972 for build in builds: 973 try: 974 if is_firmware_build: 975 firmware_rw_build = build 976 else: 977 cros_build = build 978 if self.test_source == Builds.FIRMWARE_RW: 979 test_source_build = firmware_rw_build 980 elif self.test_source == Builds.CROS: 981 test_source_build = cros_build 982 else: 983 test_source_build = None 984 self._ScheduleSuite(scheduler, cros_build, firmware_rw_build, 985 firmware_ro_build, test_source_build, 986 None, board, force) 987 except deduping_scheduler.DedupingSchedulerException as e: 988 logging.error(e) 989 return True 990 991 992 def _Run_LaunchControl_Builds(self, scheduler, launch_control_builds, board, 993 force=False): 994 """Run this task. Returns False if it should be destroyed. 995 996 Execute this task. Attempt to schedule the associated suite. 997 Return True if this task should be kept around, False if it 998 should be destroyed. This allows for one-shot Tasks. 999 1000 @param scheduler: an instance of DedupingScheduler, as defined in 1001 deduping_scheduler.py 1002 @param launch_control_builds: A list of Launch Control builds. 1003 @param board: the board against which to run self._suite. 1004 @param force: Always schedule the suite. 1005 1006 @return True if the task should be kept, False if not 1007 1008 """ 1009 logging.info('Running %s on %s', self._name, board) 1010 for build in launch_control_builds: 1011 # Filter out builds don't match the branches setting. 1012 # Launch Control branches are merged in 1013 # BaseEvents.launch_control_branches_targets property. That allows 1014 # each event only query Launch Control once to get all latest 1015 # builds. However, when a task tries to run, it should only process 1016 # the builds matches the branches specified in task config. 1017 if not any([branch in build 1018 for branch in self._launch_control_branches]): 1019 continue 1020 try: 1021 self._ScheduleSuite(scheduler, None, None, None, 1022 test_source_build=build, 1023 launch_control_build=build, board=board, 1024 force=force, run_prod_code=True) 1025 except deduping_scheduler.DedupingSchedulerException as e: 1026 logging.error(e) 1027 return True 1028 1029 1030 def Run(self, scheduler, branch_builds, board, force=False, mv=None, 1031 launch_control_builds=None): 1032 """Run this task. Returns False if it should be destroyed. 1033 1034 Execute this task. Attempt to schedule the associated suite. 1035 Return True if this task should be kept around, False if it 1036 should be destroyed. This allows for one-shot Tasks. 1037 1038 @param scheduler: an instance of DedupingScheduler, as defined in 1039 deduping_scheduler.py 1040 @param branch_builds: a dict mapping branch name to the build(s) to 1041 install for that branch, e.g. 1042 {'R18': ['x86-alex-release/R18-1655.0.0'], 1043 'R19': ['x86-alex-release/R19-2077.0.0']} 1044 @param board: the board against which to run self._suite. 1045 @param force: Always schedule the suite. 1046 @param mv: an instance of manifest_versions.ManifestVersions. 1047 @param launch_control_builds: A list of Launch Control builds. 1048 1049 @return True if the task should be kept, False if not 1050 1051 """ 1052 if ((self._os_type == OS_TYPE_CROS and not branch_builds) or 1053 (self._os_type != OS_TYPE_CROS and not launch_control_builds)): 1054 logging.debug('No build to run, skip running %s on %s.', self._name, 1055 board) 1056 # Return True so the task will be kept, as the given build and board 1057 # do not match. 1058 return True 1059 1060 if self._os_type == OS_TYPE_CROS: 1061 return self._Run_CrOS_Builds( 1062 scheduler, branch_builds, board, force, mv) 1063 else: 1064 return self._Run_LaunchControl_Builds( 1065 scheduler, launch_control_builds, board, force) 1066 1067 1068 class OneShotTask(Task): 1069 """A Task that can be run only once. Can schedule itself.""" 1070 1071 1072 def Run(self, scheduler, branch_builds, board, force=False, mv=None, 1073 launch_control_builds=None): 1074 """Run this task. Returns False, indicating it should be destroyed. 1075 1076 Run this task. Attempt to schedule the associated suite. 1077 Return False, indicating to the caller that it should discard this task. 1078 1079 @param scheduler: an instance of DedupingScheduler, as defined in 1080 deduping_scheduler.py 1081 @param branch_builds: a dict mapping branch name to the build(s) to 1082 install for that branch, e.g. 1083 {'R18': ['x86-alex-release/R18-1655.0.0'], 1084 'R19': ['x86-alex-release/R19-2077.0.0']} 1085 @param board: the board against which to run self._suite. 1086 @param force: Always schedule the suite. 1087 @param mv: an instance of manifest_versions.ManifestVersions. 1088 @param launch_control_builds: A list of Launch Control builds. 1089 1090 @return False 1091 1092 """ 1093 super(OneShotTask, self).Run(scheduler, branch_builds, board, force, 1094 mv, launch_control_builds) 1095 return False 1096