• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
6import logging
7import re
8import subprocess
9
10import base_event
11import deduping_scheduler
12import driver
13import manifest_versions
14from distutils import version
15from constants import Labels
16from constants import Builds
17
18import common
19from autotest_lib.client.common_lib import global_config
20from autotest_lib.server import utils as server_utils
21from autotest_lib.server.cros.dynamic_suite import constants
22
23
24CONFIG = global_config.global_config
25
26OS_TYPE_CROS = 'cros'
27OS_TYPE_BRILLO = 'brillo'
28OS_TYPE_ANDROID = 'android'
29OS_TYPES = {OS_TYPE_CROS, OS_TYPE_BRILLO, OS_TYPE_ANDROID}
30OS_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`
37TESTBED_DUT_COUNT_REGEX = '[^,]*-(\d+)'
38
39class MalformedConfigEntry(Exception):
40    """Raised to indicate a failure to parse a Task out of a config."""
41    pass
42
43
44BARE_BRANCHES = ['factory', 'firmware']
45
46
47def 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
59class 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
138class 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
1068class 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