Home | History | Annotate | Download | only in suite_scheduler
      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