# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import re import subprocess import base_event import deduping_scheduler import driver import error import manifest_versions from distutils import version from constants import Labels from constants import Builds import common from autotest_lib.client.common_lib import global_config from autotest_lib.client.common_lib import priorities from autotest_lib.server import utils as server_utils from autotest_lib.server.cros.dynamic_suite import constants CONFIG = global_config.global_config OS_TYPE_CROS = 'cros' OS_TYPE_BRILLO = 'brillo' OS_TYPE_ANDROID = 'android' OS_TYPES = {OS_TYPE_CROS, OS_TYPE_BRILLO, OS_TYPE_ANDROID} OS_TYPES_LAUNCH_CONTROL = {OS_TYPE_BRILLO, OS_TYPE_ANDROID} _WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] # regex to parse the dut count from board label. Note that the regex makes sure # there is only one board specified in `boards` TESTBED_DUT_COUNT_REGEX = '[^,]*-(\d+)' BARE_BRANCHES = ['factory', 'firmware'] def PickBranchName(type, milestone): """Pick branch name. If type is among BARE_BRANCHES, return type, otherwise, return milestone. @param type: type of the branch, e.g., 'release', 'factory', or 'firmware' @param milestone: CrOS milestone number """ if type in BARE_BRANCHES: return type return milestone class TotMilestoneManager(object): """A class capable of converting tot string to milestone numbers. This class is used as a cache for the tot milestone, so we don't repeatedly hit google storage for all O(100) tasks in suite scheduler's ini file. """ __metaclass__ = server_utils.Singleton # True if suite_scheduler is running for sanity check. When it's set to # True, the code won't make gsutil call to get the actual tot milestone to # avoid dependency on the installation of gsutil to run sanity check. is_sanity = False @staticmethod def _tot_milestone(): """Get the tot milestone, eg: R40 @returns: A string representing the Tot milestone as declared by the LATEST_BUILD_URL, or an empty string if LATEST_BUILD_URL doesn't exist. """ if TotMilestoneManager.is_sanity: logging.info('suite_scheduler is running for sanity purpose, no ' 'need to get the actual tot milestone string.') return 'R40' cmd = ['gsutil', 'cat', constants.LATEST_BUILD_URL] proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() if proc.poll(): logging.warning('Failed to get latest build: %s', stderr) return '' return stdout.split('-')[0] def refresh(self): """Refresh the tot milestone string managed by this class.""" self.tot = self._tot_milestone() def __init__(self): """Initialize a TotMilestoneManager.""" self.refresh() def ConvertTotSpec(self, tot_spec): """Converts a tot spec to the appropriate milestone. Assume tot is R40: tot -> R40 tot-1 -> R39 tot-2 -> R38 tot-(any other numbers) -> R40 With the last option one assumes that a malformed configuration that has 'tot' in it, wants at least tot. @param tot_spec: A string representing the tot spec. @raises MalformedConfigEntry: If the tot_spec doesn't match the expected format. """ tot_spec = tot_spec.lower() match = re.match('(tot)[-]?(1$|2$)?', tot_spec) if not match: raise error.MalformedConfigEntry( "%s isn't a valid branch spec." % tot_spec) tot_mstone = self.tot num_back = match.groups()[1] if num_back: tot_mstone_num = tot_mstone.lstrip('R') tot_mstone = tot_mstone.replace( tot_mstone_num, str(int(tot_mstone_num)-int(num_back))) return tot_mstone class Task(object): """Represents an entry from the scheduler config. Can schedule itself. Each entry from the scheduler config file maps one-to-one to a Task. Each instance has enough info to schedule itself on-demand with the AFE. This class also overrides __hash__() and all comparator methods to enable correct use in dicts, sets, etc. """ @staticmethod def CreateFromConfigSection(config, section, board_lists={}): """Create a Task from a section of a config file. The section to parse should look like this: [TaskName] suite: suite_to_run # Required run_on: event_on which to run # Required hour: integer of the hour to run, only applies to nightly. # Optional branch_specs: factory,firmware,>=R12 or ==R12 # Optional pool: pool_of_devices # Optional num: sharding_factor # int, Optional boards: board1, board2 # comma seperated string, Optional # Settings for Launch Control builds only: os_type: brillo # Type of OS, e.g., cros, brillo, android. Default is cros. Required for android/brillo builds. branches: git_mnc_release # comma separated string of Launch Control branches. Required and only applicable for android/brillo builds. targets: dragonboard-eng # comma separated string of build targets. Required and only applicable for android/brillo builds. testbed_dut_count: Number of duts to test when using a testbed. By default, Tasks run on all release branches, not factory or firmware. @param config: a ForgivingConfigParser. @param section: the section to parse into a Task. @param board_lists: a dict including all board whitelist for tasks. @return keyword, Task object pair. One or both will be None on error. @raise MalformedConfigEntry if there's a problem parsing |section|. """ if not config.has_section(section): raise error.MalformedConfigEntry('unknown section %s' % section) allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num', 'boards', 'file_bugs', 'cros_build_spec', 'firmware_rw_build_spec', 'firmware_ro_build_spec', 'test_source', 'job_retry', 'hour', 'day', 'branches', 'targets', 'os_type', 'no_delay', 'owner', 'priority', 'timeout']) # The parameter of union() is the keys under the section in the config # The union merges this with the allowed set, so if any optional keys # are omitted, then they're filled in. If any extra keys are present, # then they will expand unioned set, causing it to fail the following # comparison against the allowed set. section_headers = allowed.union(dict(config.items(section)).keys()) if allowed != section_headers: raise error.MalformedConfigEntry('unknown entries: %s' % ", ".join(map(str, section_headers.difference(allowed)))) keyword = config.getstring(section, 'run_on') hour = config.getstring(section, 'hour') suite = config.getstring(section, 'suite') branch_specs = config.getstring(section, 'branch_specs') pool = config.getstring(section, 'pool') boards = config.getstring(section, 'boards') file_bugs = config.getboolean(section, 'file_bugs') cros_build_spec = config.getstring(section, 'cros_build_spec') firmware_rw_build_spec = config.getstring( section, 'firmware_rw_build_spec') firmware_ro_build_spec = config.getstring( section, 'firmware_ro_build_spec') test_source = config.getstring(section, 'test_source') job_retry = config.getboolean(section, 'job_retry') no_delay = config.getboolean(section, 'no_delay') # In case strings empty use sane low priority defaults. priority = 0 timeout = 24 # Set priority/timeout based on the event type. for klass in driver.Driver.EVENT_CLASSES: if klass.KEYWORD == keyword: priority = klass.PRIORITY timeout = klass.TIMEOUT break # Set priority/timeout from config file explicitly if set. priority_string = config.getstring(section, 'priority') if priority_string: # Try to parse priority as int first. If failed, then use the # global string->priority mapping to lookup its value. try: try: priority = int(priority_string) except ValueError: priority = priorities.Priority.get_value(priority_string) except ValueError: raise error.MalformedConfigEntry("Priority string not " "recognized as value (%s).", priority_string) timeout_value = config.getint(section, 'timeout') if timeout_value: timeout = timeout_value # Sanity Check for priority and timeout. if priority < 0 or priority > 100: raise error.MalformedConfigEntry('Priority(%d) should be inside ' 'the range 0-100.' % priority) if timeout <= 0: raise error.MalformedConfigEntry('Timeout(%d) needs to be positive ' 'integer (hours).' % timeout) try: num = config.getint(section, 'num') except ValueError as e: raise error.MalformedConfigEntry("Ill-specified 'num': %r" % e) if not keyword: raise error.MalformedConfigEntry('No event to |run_on|.') if not suite: raise error.MalformedConfigEntry('No |suite|') try: hour = config.getint(section, 'hour') except ValueError as e: raise error.MalformedConfigEntry("Ill-specified 'hour': %r" % e) if hour is not None and (hour < 0 or hour > 23): raise error.MalformedConfigEntry( '`hour` must be an integer between 0 and 23.') if hour is not None and keyword != 'nightly': raise error.MalformedConfigEntry( '`hour` is the trigger time that can only apply to nightly ' 'event.') testbed_dut_count = None if boards: match = re.match(TESTBED_DUT_COUNT_REGEX, boards) if match: testbed_dut_count = int(match.group(1)) try: day = config.getint(section, 'day') except ValueError as e: raise error.MalformedConfigEntry("Ill-specified 'day': %r" % e) if day is not None and (day < 0 or day > 6): raise error.MalformedConfigEntry( '`day` must be an integer between 0 and 6, where 0 is for ' 'Monday and 6 is for Sunday.') if day is not None and keyword != 'weekly': raise error.MalformedConfigEntry( '`day` is the trigger of the day of a week, that can only ' 'apply to weekly events.') specs = [] if branch_specs: specs = re.split('\s*,\s*', branch_specs) Task.CheckBranchSpecs(specs) os_type = config.getstring(section, 'os_type') or OS_TYPE_CROS if os_type not in OS_TYPES: raise error.MalformedConfigEntry( '`os_type` must be one of %s' % OS_TYPES) lc_branches = config.getstring(section, 'branches') lc_targets = config.getstring(section, 'targets') if os_type == OS_TYPE_CROS and (lc_branches or lc_targets): raise error.MalformedConfigEntry( '`branches` and `targets` are only supported for Launch ' 'Control builds, not ChromeOS builds.') if (os_type in OS_TYPES_LAUNCH_CONTROL and (not lc_branches or not lc_targets)): raise error.MalformedConfigEntry( '`branches` and `targets` must be specified for Launch ' 'Control builds.') if (os_type in OS_TYPES_LAUNCH_CONTROL and boards and not testbed_dut_count): raise error.MalformedConfigEntry( '`boards` for Launch Control builds are retrieved from ' '`targets` setting, it should not be set for Launch ' 'Control builds.') if os_type == OS_TYPE_CROS and testbed_dut_count: raise error.MalformedConfigEntry( 'testbed_dut_count is only supported for Launch Control ' 'builds testing with testbed.') # Extract boards from targets list. if os_type in OS_TYPES_LAUNCH_CONTROL: boards = '' for target in lc_targets.split(','): board_name, _ = server_utils.parse_launch_control_target( target.strip()) # Translate board name in build target to the actual board name. board_name = server_utils.ANDROID_TARGET_TO_BOARD_MAP.get( board_name, board_name) boards += '%s,' % board_name boards = boards.strip(',') elif os_type == OS_TYPE_CROS: if board_lists: if boards not in board_lists: logging.debug( 'The board_list name %s does not exist in ' 'section board_lists in config.', boards) # TODO(xixuan): Raise MalformedConfigEntry when a CrOS task # specify a 'boards' which is not defined in board_lists. # Currently exception won't be raised to make sure suite # scheduler keeps running when developers are in the middle # of migrating boards. else: boards = board_lists[boards] return keyword, Task(section, suite, specs, pool, num, boards, priority, timeout, file_bugs=file_bugs if file_bugs else False, cros_build_spec=cros_build_spec, firmware_rw_build_spec=firmware_rw_build_spec, firmware_ro_build_spec=firmware_ro_build_spec, test_source=test_source, job_retry=job_retry, hour=hour, day=day, os_type=os_type, launch_control_branches=lc_branches, launch_control_targets=lc_targets, testbed_dut_count=testbed_dut_count, no_delay=no_delay) @staticmethod def CheckBranchSpecs(branch_specs): """Make sure entries in the list branch_specs are correctly formed. We accept any of BARE_BRANCHES in |branch_specs|, as well as _one_ string of the form '>=RXX' or '==RXX', where 'RXX' is a CrOS milestone number. @param branch_specs: an iterable of branch specifiers. @raise MalformedConfigEntry if there's a problem parsing |branch_specs|. """ have_seen_numeric_constraint = False for branch in branch_specs: if branch in BARE_BRANCHES: continue if not have_seen_numeric_constraint: #TODO(beeps): Why was <= dropped on the floor? if branch.startswith('>=R') or branch.startswith('==R'): have_seen_numeric_constraint = True elif 'tot' in branch: TotMilestoneManager().ConvertTotSpec( branch[branch.index('tot'):]) have_seen_numeric_constraint = True continue raise error.MalformedConfigEntry( "%s isn't a valid branch spec.'" % branch) def __init__(self, name, suite, branch_specs, pool=None, num=None, boards=None, priority=None, timeout=None, file_bugs=False, cros_build_spec=None, firmware_rw_build_spec=None, firmware_ro_build_spec=None, test_source=None, job_retry=False, hour=None, day=None, os_type=OS_TYPE_CROS, launch_control_branches=None, launch_control_targets=None, testbed_dut_count=None, no_delay=False): """Constructor Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs, we'll store them such that _FitsSpec() can be used to check whether a given branch 'fits' with the specifications passed in here. For example, given branch_specs = ['factory', '>=R18'], we'd set things up so that _FitsSpec() would return True for 'factory', or 'RXX' where XX is a number >= 18. Same check is done for branch_specs = [ 'factory', '==R18'], which limit the test to only one specific branch. Given branch_specs = ['factory', 'firmware'], _FitsSpec() would pass only those two specific strings. Example usage: t = Task('Name', 'suite', ['factory', '>=R18']) t._FitsSpec('factory') # True t._FitsSpec('R19') # True t._FitsSpec('R17') # False t._FitsSpec('firmware') # False t._FitsSpec('goober') # False t = Task('Name', 'suite', ['factory', '==R18']) t._FitsSpec('R19') # False, branch does not equal to 18 t._FitsSpec('R18') # True t._FitsSpec('R17') # False cros_build_spec and firmware_rw_build_spec are set for tests require firmware update on the dut. Only one of them can be set. For example: branch_specs: ==tot firmware_rw_build_spec: firmware test_source: cros This will run test using latest build on firmware branch, and the latest ChromeOS build on ToT. The test source build is ChromeOS build. branch_specs: firmware cros_build_spec: ==tot-1 test_source: firmware_rw This will run test using latest build on firmware branch, and the latest ChromeOS build on dev channel (ToT-1). The test source build is the firmware RW build. branch_specs: ==tot firmware_rw_build_spec: cros test_source: cros This will run test using latest ChromeOS and firmware RW build on ToT. ChromeOS build on ToT. The test source build is ChromeOS build. @param name: name of this task, e.g. 'NightlyPower' @param suite: the name of the suite to run, e.g. 'bvt' @param branch_specs: a pre-vetted iterable of branch specifiers, e.g. ['>=R18', 'factory'] @param pool: the pool of machines to use for scheduling purposes. Default: None @param num: the number of devices across which to shard the test suite. Type: integer or None Default: None @param boards: A comma separated list of boards to run this task on. Default: Run on all boards. @param priority: The string name of a priority from client.common_lib.priorities.Priority. @param timeout: The max lifetime of the suite in hours. @param file_bugs: True if bug filing is desired for the suite created for this task. @param cros_build_spec: Spec used to determine the ChromeOS build to test with a firmware build, e.g., tot, R41 etc. @param firmware_rw_build_spec: Spec used to determine the firmware RW build test with a ChromeOS build. @param firmware_ro_build_spec: Spec used to determine the firmware RO build test with a ChromeOS build. @param test_source: The source of test code when firmware will be updated in the test. The value can be `firmware_rw`, `firmware_ro` or `cros`. @param job_retry: Set to True to enable job-level retry. Default is False. @param hour: An integer specifying the hour that a nightly run should be triggered, default is set to 21. @param day: An integer specifying the day of a week that a weekly run should be triggered, default is set to 5, which is Saturday. @param os_type: Type of OS, e.g., cros, brillo, android. Default is cros. The argument is required for android/brillo builds. @param launch_control_branches: Comma separated string of Launch Control branches. The argument is required and only applicable for android/brillo builds. @param launch_control_targets: Comma separated string of build targets for Launch Control builds. The argument is required and only applicable for android/brillo builds. @param testbed_dut_count: Number of duts to test when using a testbed. @param no_delay: Set to True to allow suite to be created without configuring delay_minutes. Default is False. """ self._name = name self._suite = suite self._branch_specs = branch_specs self._pool = pool self._num = num self._priority = priority self._timeout = timeout self._file_bugs = file_bugs self._cros_build_spec = cros_build_spec self._firmware_rw_build_spec = firmware_rw_build_spec self._firmware_ro_build_spec = firmware_ro_build_spec self._test_source = test_source self._job_retry = job_retry self._hour = hour self._day = day self._os_type = os_type self._launch_control_branches = ( [b.strip() for b in launch_control_branches.split(',')] if launch_control_branches else []) self._launch_control_targets = ( [t.strip() for t in launch_control_targets.split(',')] if launch_control_targets else []) self._testbed_dut_count = testbed_dut_count self._no_delay = no_delay if ((self._firmware_rw_build_spec or self._firmware_ro_build_spec or cros_build_spec) and not self.test_source in [Builds.FIRMWARE_RW, Builds.FIRMWARE_RO, Builds.CROS]): raise error.MalformedConfigEntry( 'You must specify the build for test source. It can only ' 'be `firmware_rw`, `firmware_ro` or `cros`.') if self._firmware_rw_build_spec and cros_build_spec: raise error.MalformedConfigEntry( 'You cannot specify both firmware_rw_build_spec and ' 'cros_build_spec. firmware_rw_build_spec is used to specify' ' a firmware build when the suite requires firmware to be ' 'updated in the dut, its value can only be `firmware` or ' '`cros`. cros_build_spec is used to specify a ChromeOS ' 'build when build_specs is set to firmware.') if (self._firmware_rw_build_spec and self._firmware_rw_build_spec not in ['firmware', 'cros']): raise error.MalformedConfigEntry( 'firmware_rw_build_spec can only be empty, firmware or ' 'cros. It does not support other build type yet.') if os_type not in OS_TYPES_LAUNCH_CONTROL and self._testbed_dut_count: raise error.MalformedConfigEntry( 'testbed_dut_count is only applicable to testbed to run ' 'test with builds from Launch Control.') self._bare_branches = [] self._version_equal_constraint = False self._version_gte_constraint = False self._version_lte_constraint = False if not branch_specs: # Any milestone is OK. self._numeric_constraint = version.LooseVersion('0') else: self._numeric_constraint = None for spec in branch_specs: if 'tot' in spec.lower(): tot_str = spec[spec.index('tot'):] spec = spec.replace( tot_str, TotMilestoneManager().ConvertTotSpec( tot_str)) if spec.startswith('>='): self._numeric_constraint = version.LooseVersion( spec.lstrip('>=R')) self._version_gte_constraint = True elif spec.startswith('<='): self._numeric_constraint = version.LooseVersion( spec.lstrip('<=R')) self._version_lte_constraint = True elif spec.startswith('=='): self._version_equal_constraint = True self._numeric_constraint = version.LooseVersion( spec.lstrip('==R')) else: self._bare_branches.append(spec) # Since we expect __hash__() and other comparator methods to be used # frequently by set operations, and they use str() a lot, pre-compute # the string representation of this object. if num is None: numStr = '[Default num]' else: numStr = '%d' % num if boards is None: self._boards = set() boardsStr = '[All boards]' else: self._boards = set([x.strip() for x in boards.split(',')]) boardsStr = boards time_str = '' if self._hour: time_str = ' Run at %d:00.' % self._hour elif self._day: time_str = ' Run on %s.' % _WEEKDAYS[self._day] if os_type == OS_TYPE_CROS: self._str = ('%s: %s on %s with pool %s, boards [%s], file_bugs = ' '%s across %s machines.%s' % (self.__class__.__name__, suite, branch_specs, pool, boardsStr, self._file_bugs, numStr, time_str)) else: testbed_dut_count_str = '.' if self._testbed_dut_count: testbed_dut_count_str = (', each with %d duts.' % self._testbed_dut_count) self._str = ('%s: %s on branches %s and targets %s with pool %s, ' 'boards [%s], file_bugs = %s across %s machines%s%s' % (self.__class__.__name__, suite, launch_control_branches, launch_control_targets, pool, boardsStr, self._file_bugs, numStr, testbed_dut_count_str, time_str)) def _FitsSpec(self, branch): """Checks if a branch is deemed OK by this instance's branch specs. When called on a branch name, will return whether that branch 'fits' the specifications stored in self._bare_branches, self._numeric_constraint, self._version_equal_constraint, self._version_gte_constraint and self._version_lte_constraint. @param branch: the branch to check. @return True if b 'fits' with stored specs, False otherwise. """ if branch in BARE_BRANCHES: return branch in self._bare_branches if self._numeric_constraint: if self._version_equal_constraint: return version.LooseVersion(branch) == self._numeric_constraint elif self._version_gte_constraint: return version.LooseVersion(branch) >= self._numeric_constraint elif self._version_lte_constraint: return version.LooseVersion(branch) <= self._numeric_constraint else: # Default to great or equal constraint. return version.LooseVersion(branch) >= self._numeric_constraint else: return False @property def name(self): """Name of this task, e.g. 'NightlyPower'.""" return self._name @property def suite(self): """Name of the suite to run, e.g. 'bvt'.""" return self._suite @property def branch_specs(self): """a pre-vetted iterable of branch specifiers, e.g. ['>=R18', 'factory'].""" return self._branch_specs @property def pool(self): """The pool of machines to use for scheduling purposes.""" return self._pool @property def num(self): """The number of devices across which to shard the test suite. Type: integer or None""" return self._num @property def boards(self): """The boards on which to run this suite. Type: Iterable of strings""" return self._boards @property def priority(self): """The priority of the suite""" return self._priority @property def timeout(self): """The maximum lifetime of the suite in hours.""" return self._timeout @property def cros_build_spec(self): """The build spec of ChromeOS to test with a firmware build.""" return self._cros_build_spec @property def firmware_rw_build_spec(self): """The build spec of RW firmware to test with a ChromeOS build. The value can be firmware or cros. """ return self._firmware_rw_build_spec @property def firmware_ro_build_spec(self): """The build spec of RO firmware to test with a ChromeOS build. The value can be stable, firmware or cros, where stable is the stable firmware build retrieved from stable_version table. """ return self._firmware_ro_build_spec @property def test_source(self): """Source of the test code, value can be `firmware_rw`, `firmware_ro` or `cros`.""" return self._test_source @property def hour(self): """An integer specifying the hour that a nightly run should be triggered """ return self._hour @property def day(self): """An integer specifying the day of a week that a weekly run should be triggered""" return self._day @property def os_type(self): """Type of OS, e.g., cros, brillo, android.""" return self._os_type @property def launch_control_branches(self): """A list of Launch Control builds.""" return self._launch_control_branches @property def launch_control_targets(self): """A list of Launch Control targets.""" return self._launch_control_targets def __str__(self): return self._str def __repr__(self): return self._str def __lt__(self, other): return str(self) < str(other) def __le__(self, other): return str(self) <= str(other) def __eq__(self, other): return str(self) == str(other) def __ne__(self, other): return str(self) != str(other) def __gt__(self, other): return str(self) > str(other) def __ge__(self, other): return str(self) >= str(other) def __hash__(self): """Allows instances to be correctly deduped when used in a set.""" return hash(str(self)) def _GetCrOSBuild(self, mv, board): """Get the ChromeOS build name to test with firmware build. The ChromeOS build to be used is determined by `self.cros_build_spec`. Its value can be: tot: use the latest ToT build. tot-x: use the latest build in x milestone before ToT. Rxx: use the latest build on xx milestone. @param board: the board against which to run self._suite. @param mv: an instance of manifest_versions.ManifestVersions. @return: The ChromeOS build name to test with firmware build. """ if not self.cros_build_spec: return None if self.cros_build_spec.startswith('tot'): milestone = TotMilestoneManager().ConvertTotSpec( self.cros_build_spec)[1:] elif self.cros_build_spec.startswith('R'): milestone = self.cros_build_spec[1:] milestone, latest_manifest = mv.GetLatestManifest( board, 'release', milestone=milestone) latest_build = base_event.BuildName(board, 'release', milestone, latest_manifest) logging.debug('Found latest build of %s for spec %s: %s', board, self.cros_build_spec, latest_build) return latest_build def _GetFirmwareBuild(self, spec, mv, board): """Get the firmware build name to test with ChromeOS build. @param spec: build spec for RO or RW firmware, e.g., firmware, cros. For RO firmware, the value can also be in the format of released_ro_X, where X is the index of the list or RO builds defined in global config RELEASED_RO_BUILDS_[board]. For example, for spec `released_ro_2`, and global config CROS/RELEASED_RO_BUILDS_veyron_jerry: build1,build2 the return firmare RO build should be build2. @param mv: an instance of manifest_versions.ManifestVersions. @param board: the board against which to run self._suite. @return: The firmware build name to test with ChromeOS build. """ if spec == 'stable': # TODO(crbug.com/577316): Query stable RO firmware. raise NotImplementedError('`stable` RO firmware build is not ' 'supported yet.') if not spec: return None if spec.startswith('released_ro_'): index = int(spec[12:]) released_ro_builds = CONFIG.get_config_value( 'CROS', 'RELEASED_RO_BUILDS_%s' % board, type=str, default='').split(',') if not released_ro_builds or len(released_ro_builds) < index: return None else: return released_ro_builds[index-1] # build_type is the build type of the firmware build, e.g., factory, # firmware or release. If spec is set to cros, build type should be # mapped to release. build_type = 'release' if spec == 'cros' else spec latest_milestone, latest_manifest = mv.GetLatestManifest( board, build_type) latest_build = base_event.BuildName(board, build_type, latest_milestone, latest_manifest) logging.debug('Found latest firmware build of %s for spec %s: %s', board, spec, latest_build) return latest_build def AvailableHosts(self, scheduler, board): """Query what hosts are able to run a test on a board and pool combination. @param scheduler: an instance of DedupingScheduler, as defined in deduping_scheduler.py @param board: the board against which one wants to run the test. @return The list of hosts meeting the board and pool requirements, or None if no hosts were found.""" if self._boards and board not in self._boards: return [] board_label = Labels.BOARD_PREFIX + board if self._testbed_dut_count: board_label += '-%d' % self._testbed_dut_count labels = [board_label] if self._pool: labels.append(Labels.POOL_PREFIX + self._pool) return scheduler.CheckHostsExist(multiple_labels=labels) def ShouldHaveAvailableHosts(self): """As a sanity check, return true if we know for certain that we should be able to schedule this test. If we claim this test should be able to run, and it ends up not being scheduled, then a warning will be reported. @return True if this test should be able to run, False otherwise. """ return self._pool == 'bvt' def _ScheduleSuite(self, scheduler, cros_build, firmware_rw_build, firmware_ro_build, test_source_build, launch_control_build, board, force, run_prod_code=False): """Try to schedule a suite with given build and board information. @param scheduler: an instance of DedupingScheduler, as defined in deduping_scheduler.py @oaran build: Build to run suite for, e.g., 'daisy-release/R18-1655.0.0' and 'git_mnc_release/shamu-eng/123'. @param firmware_rw_build: Firmware RW build to run test with. @param firmware_ro_build: Firmware RO build to run test with. @param test_source_build: Test source build, used for server-side packaging. @param launch_control_build: Name of a Launch Control build, e.g., 'git_mnc_release/shamu-eng/123' @param board: the board against which to run self._suite. @param force: Always schedule the suite. @param run_prod_code: If True, the suite will run the test code that lives in prod aka the test code currently on the lab servers. If False, the control files and test code for this suite run will be retrieved from the build artifacts. Default is False. """ test_source_build_msg = ( ' Test source build is %s.' % test_source_build if test_source_build else '') firmware_rw_build_msg = ( ' Firmware RW build is %s.' % firmware_rw_build if firmware_rw_build else '') firmware_ro_build_msg = ( ' Firmware RO build is %s.' % firmware_ro_build if firmware_ro_build else '') # If testbed_dut_count is set, the suite is for testbed. Update build # and board with the dut count. if self._testbed_dut_count: launch_control_build = '%s#%d' % (launch_control_build, self._testbed_dut_count) test_source_build = launch_control_build board = '%s-%d' % (board, self._testbed_dut_count) build_string = cros_build or launch_control_build logging.debug('Schedule %s for build %s.%s%s%s', self._suite, build_string, test_source_build_msg, firmware_rw_build_msg, firmware_ro_build_msg) if not scheduler.ScheduleSuite( self._suite, board, cros_build, self._pool, self._num, self._priority, self._timeout, force, file_bugs=self._file_bugs, firmware_rw_build=firmware_rw_build, firmware_ro_build=firmware_ro_build, test_source_build=test_source_build, job_retry=self._job_retry, launch_control_build=launch_control_build, run_prod_code=run_prod_code, testbed_dut_count=self._testbed_dut_count, no_delay=self._no_delay): logging.info('Skipping scheduling %s on %s for %s', self._suite, build_string, board) def _Run_CrOS_Builds(self, scheduler, branch_builds, board, force=False, mv=None): """Run this task for CrOS builds. Returns False if it should be destroyed. Execute this task. Attempt to schedule the associated suite. Return True if this task should be kept around, False if it should be destroyed. This allows for one-shot Tasks. @param scheduler: an instance of DedupingScheduler, as defined in deduping_scheduler.py @param branch_builds: a dict mapping branch name to the build(s) to install for that branch, e.g. {'R18': ['x86-alex-release/R18-1655.0.0'], 'R19': ['x86-alex-release/R19-2077.0.0']} @param board: the board against which to run self._suite. @param force: Always schedule the suite. @param mv: an instance of manifest_versions.ManifestVersions. @return True if the task should be kept, False if not """ logging.info('Running %s on %s', self._name, board) is_firmware_build = 'firmware' in self.branch_specs # firmware_xx_build is only needed if firmware_xx_build_spec is given. firmware_rw_build = None firmware_ro_build = None try: if is_firmware_build: # When build specified in branch_specs is a firmware build, # we need a ChromeOS build to test with the firmware build. cros_build = self._GetCrOSBuild(mv, board) elif self.firmware_rw_build_spec or self.firmware_ro_build_spec: # When firmware_xx_build_spec is specified, the test involves # updating the RW firmware by firmware build specified in # firmware_xx_build_spec. firmware_rw_build = self._GetFirmwareBuild( self.firmware_rw_build_spec, mv, board) firmware_ro_build = self._GetFirmwareBuild( self.firmware_ro_build_spec, mv, board) # If RO firmware is specified, force to create suite, because # dedupe based on test source build does not reflect the change # of RO firmware. if firmware_ro_build: force = True except manifest_versions.QueryException as e: logging.error(e) logging.error('Running %s on %s is failed. Failed to find build ' 'required to run the suite.', self._name, board) return False # Return if there is no firmware RO build found for given spec. if not firmware_ro_build and self.firmware_ro_build_spec: return True builds = [] for branch, build in branch_builds.iteritems(): logging.info('Checking if %s fits spec %r', branch, self.branch_specs) if self._FitsSpec(branch): logging.debug('Build %s fits the spec.', build) builds.extend(build) for build in builds: try: if is_firmware_build: firmware_rw_build = build else: cros_build = build if self.test_source == Builds.FIRMWARE_RW: test_source_build = firmware_rw_build elif self.test_source == Builds.CROS: test_source_build = cros_build else: test_source_build = None self._ScheduleSuite(scheduler, cros_build, firmware_rw_build, firmware_ro_build, test_source_build, None, board, force) except deduping_scheduler.DedupingSchedulerException as e: logging.error(e) return True def _Run_LaunchControl_Builds(self, scheduler, launch_control_builds, board, force=False): """Run this task. Returns False if it should be destroyed. Execute this task. Attempt to schedule the associated suite. Return True if this task should be kept around, False if it should be destroyed. This allows for one-shot Tasks. @param scheduler: an instance of DedupingScheduler, as defined in deduping_scheduler.py @param launch_control_builds: A list of Launch Control builds. @param board: the board against which to run self._suite. @param force: Always schedule the suite. @return True if the task should be kept, False if not """ logging.info('Running %s on %s', self._name, board) for build in launch_control_builds: # Filter out builds don't match the branches setting. # Launch Control branches are merged in # BaseEvents.launch_control_branches_targets property. That allows # each event only query Launch Control once to get all latest # builds. However, when a task tries to run, it should only process # the builds matches the branches specified in task config. if not any([branch in build for branch in self._launch_control_branches]): continue try: self._ScheduleSuite(scheduler, None, None, None, test_source_build=build, launch_control_build=build, board=board, force=force, run_prod_code=True) except deduping_scheduler.DedupingSchedulerException as e: logging.error(e) return True def Run(self, scheduler, branch_builds, board, force=False, mv=None, launch_control_builds=None): """Run this task. Returns False if it should be destroyed. Execute this task. Attempt to schedule the associated suite. Return True if this task should be kept around, False if it should be destroyed. This allows for one-shot Tasks. @param scheduler: an instance of DedupingScheduler, as defined in deduping_scheduler.py @param branch_builds: a dict mapping branch name to the build(s) to install for that branch, e.g. {'R18': ['x86-alex-release/R18-1655.0.0'], 'R19': ['x86-alex-release/R19-2077.0.0']} @param board: the board against which to run self._suite. @param force: Always schedule the suite. @param mv: an instance of manifest_versions.ManifestVersions. @param launch_control_builds: A list of Launch Control builds. @return True if the task should be kept, False if not """ if ((self._os_type == OS_TYPE_CROS and not branch_builds) or (self._os_type != OS_TYPE_CROS and not launch_control_builds)): logging.debug('No build to run, skip running %s on %s.', self._name, board) # Return True so the task will be kept, as the given build and board # do not match. return True if self._os_type == OS_TYPE_CROS: return self._Run_CrOS_Builds( scheduler, branch_builds, board, force, mv) else: return self._Run_LaunchControl_Builds( scheduler, launch_control_builds, board, force) class OneShotTask(Task): """A Task that can be run only once. Can schedule itself.""" def Run(self, scheduler, branch_builds, board, force=False, mv=None, launch_control_builds=None): """Run this task. Returns False, indicating it should be destroyed. Run this task. Attempt to schedule the associated suite. Return False, indicating to the caller that it should discard this task. @param scheduler: an instance of DedupingScheduler, as defined in deduping_scheduler.py @param branch_builds: a dict mapping branch name to the build(s) to install for that branch, e.g. {'R18': ['x86-alex-release/R18-1655.0.0'], 'R19': ['x86-alex-release/R19-2077.0.0']} @param board: the board against which to run self._suite. @param force: Always schedule the suite. @param mv: an instance of manifest_versions.ManifestVersions. @param launch_control_builds: A list of Launch Control builds. @return False """ super(OneShotTask, self).Run(scheduler, branch_builds, board, force, mv, launch_control_builds) return False