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.server import utils as server_utils
20from autotest_lib.server.cros.dynamic_suite import constants
21
22
23class MalformedConfigEntry(Exception):
24    """Raised to indicate a failure to parse a Task out of a config."""
25    pass
26
27
28BARE_BRANCHES = ['factory', 'firmware']
29
30
31def PickBranchName(type, milestone):
32    """Pick branch name. If type is among BARE_BRANCHES, return type,
33    otherwise, return milestone.
34
35    @param type: type of the branch, e.g., 'release', 'factory', or 'firmware'
36    @param milestone: CrOS milestone number
37    """
38    if type in BARE_BRANCHES:
39        return type
40    return milestone
41
42
43class TotMilestoneManager(object):
44    """A class capable of converting tot string to milestone numbers.
45
46    This class is used as a cache for the tot milestone, so we don't
47    repeatedly hit google storage for all O(100) tasks in suite
48    scheduler's ini file.
49    """
50
51    __metaclass__ = server_utils.Singleton
52
53    # True if suite_scheduler is running for sanity check. When it's set to
54    # True, the code won't make gsutil call to get the actual tot milestone to
55    # avoid dependency on the installation of gsutil to run sanity check.
56    is_sanity = False
57
58
59    @staticmethod
60    def _tot_milestone():
61        """Get the tot milestone, eg: R40
62
63        @returns: A string representing the Tot milestone as declared by
64            the LATEST_BUILD_URL, or an empty string if LATEST_BUILD_URL
65            doesn't exist.
66        """
67        if TotMilestoneManager.is_sanity:
68            logging.info('suite_scheduler is running for sanity purpose, no '
69                         'need to get the actual tot milestone string.')
70            return 'R40'
71
72        cmd = ['gsutil', 'cat', constants.LATEST_BUILD_URL]
73        proc = subprocess.Popen(
74                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
75        stdout, stderr = proc.communicate()
76        if proc.poll():
77            logging.warning('Failed to get latest build: %s', stderr)
78            return ''
79        return stdout.split('-')[0]
80
81
82    def refresh(self):
83        """Refresh the tot milestone string managed by this class."""
84        self.tot = self._tot_milestone()
85
86
87    def __init__(self):
88        """Initialize a TotMilestoneManager."""
89        self.refresh()
90
91
92    def ConvertTotSpec(self, tot_spec):
93        """Converts a tot spec to the appropriate milestone.
94
95        Assume tot is R40:
96        tot   -> R40
97        tot-1 -> R39
98        tot-2 -> R38
99        tot-(any other numbers) -> R40
100
101        With the last option one assumes that a malformed configuration that has
102        'tot' in it, wants at least tot.
103
104        @param tot_spec: A string representing the tot spec.
105        @raises MalformedConfigEntry: If the tot_spec doesn't match the
106            expected format.
107        """
108        tot_spec = tot_spec.lower()
109        match = re.match('(tot)[-]?(1$|2$)?', tot_spec)
110        if not match:
111            raise MalformedConfigEntry(
112                    "%s isn't a valid branch spec." % tot_spec)
113        tot_mstone = self.tot
114        num_back = match.groups()[1]
115        if num_back:
116            tot_mstone_num = tot_mstone.lstrip('R')
117            tot_mstone = tot_mstone.replace(
118                    tot_mstone_num, str(int(tot_mstone_num)-int(num_back)))
119        return tot_mstone
120
121
122class Task(object):
123    """Represents an entry from the scheduler config.  Can schedule itself.
124
125    Each entry from the scheduler config file maps one-to-one to a
126    Task.  Each instance has enough info to schedule itself
127    on-demand with the AFE.
128
129    This class also overrides __hash__() and all comparator methods to enable
130    correct use in dicts, sets, etc.
131    """
132
133
134    @staticmethod
135    def CreateFromConfigSection(config, section):
136        """Create a Task from a section of a config file.
137
138        The section to parse should look like this:
139        [TaskName]
140        suite: suite_to_run  # Required
141        run_on: event_on which to run  # Required
142        hour: integer of the hour to run, only applies to nightly. # Optional
143        branch_specs: factory,firmware,>=R12 or ==R12 # Optional
144        pool: pool_of_devices  # Optional
145        num: sharding_factor  # int, Optional
146        boards: board1, board2  # comma seperated string, Optional
147
148        By default, Tasks run on all release branches, not factory or firmware.
149
150        @param config: a ForgivingConfigParser.
151        @param section: the section to parse into a Task.
152        @return keyword, Task object pair.  One or both will be None on error.
153        @raise MalformedConfigEntry if there's a problem parsing |section|.
154        """
155        if not config.has_section(section):
156            raise MalformedConfigEntry('unknown section %s' % section)
157
158        allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num',
159                       'boards', 'file_bugs', 'cros_build_spec',
160                       'firmware_rw_build_spec', 'test_source', 'job_retry',
161                       'hour', 'day'])
162        # The parameter of union() is the keys under the section in the config
163        # The union merges this with the allowed set, so if any optional keys
164        # are omitted, then they're filled in. If any extra keys are present,
165        # then they will expand unioned set, causing it to fail the following
166        # comparison against the allowed set.
167        section_headers = allowed.union(dict(config.items(section)).keys())
168        if allowed != section_headers:
169            raise MalformedConfigEntry('unknown entries: %s' %
170                      ", ".join(map(str, section_headers.difference(allowed))))
171
172        keyword = config.getstring(section, 'run_on')
173        hour = config.getstring(section, 'hour')
174        suite = config.getstring(section, 'suite')
175        branches = config.getstring(section, 'branch_specs')
176        pool = config.getstring(section, 'pool')
177        boards = config.getstring(section, 'boards')
178        file_bugs = config.getboolean(section, 'file_bugs')
179        cros_build_spec = config.getstring(section, 'cros_build_spec')
180        firmware_rw_build_spec = config.getstring(
181                section, 'firmware_rw_build_spec')
182        test_source = config.getstring(section, 'test_source')
183        job_retry = config.getboolean(section, 'job_retry')
184        for klass in driver.Driver.EVENT_CLASSES:
185            if klass.KEYWORD == keyword:
186                priority = klass.PRIORITY
187                timeout = klass.TIMEOUT
188                break
189        else:
190            priority = None
191            timeout = None
192        try:
193            num = config.getint(section, 'num')
194        except ValueError as e:
195            raise MalformedConfigEntry("Ill-specified 'num': %r" % e)
196        if not keyword:
197            raise MalformedConfigEntry('No event to |run_on|.')
198        if not suite:
199            raise MalformedConfigEntry('No |suite|')
200        try:
201            hour = config.getint(section, 'hour')
202        except ValueError as e:
203            raise MalformedConfigEntry("Ill-specified 'hour': %r" % e)
204        if hour is not None and (hour < 0 or hour > 23):
205            raise MalformedConfigEntry(
206                    '`hour` must be an integer between 0 and 23.')
207        if hour is not None and keyword != 'nightly':
208            raise MalformedConfigEntry(
209                    '`hour` is the trigger time that can only apply to nightly '
210                    'event.')
211
212        try:
213            day = config.getint(section, 'day')
214        except ValueError as e:
215            raise MalformedConfigEntry("Ill-specified 'day': %r" % e)
216        if day is not None and (day < 0 or day > 6):
217            raise MalformedConfigEntry(
218                    '`day` must be an integer between 0 and 6, where 0 is for '
219                    'Monday and 6 is for Sunday.')
220        if day is not None and keyword != 'weekly':
221            raise MalformedConfigEntry(
222                    '`day` is the trigger of the day of a week, that can only '
223                    'apply to weekly events.')
224
225        specs = []
226        if branches:
227            specs = re.split('\s*,\s*', branches)
228            Task.CheckBranchSpecs(specs)
229        return keyword, Task(section, suite, specs, pool, num, boards,
230                             priority, timeout,
231                             file_bugs=file_bugs if file_bugs else False,
232                             cros_build_spec=cros_build_spec,
233                             firmware_rw_build_spec=firmware_rw_build_spec,
234                             test_source=test_source, job_retry=job_retry,
235                             hour=hour, day=day)
236
237
238    @staticmethod
239    def CheckBranchSpecs(branch_specs):
240        """Make sure entries in the list branch_specs are correctly formed.
241
242        We accept any of BARE_BRANCHES in |branch_specs|, as
243        well as _one_ string of the form '>=RXX' or '==RXX', where 'RXX' is a
244        CrOS milestone number.
245
246        @param branch_specs: an iterable of branch specifiers.
247        @raise MalformedConfigEntry if there's a problem parsing |branch_specs|.
248        """
249        have_seen_numeric_constraint = False
250        for branch in branch_specs:
251            if branch in BARE_BRANCHES:
252                continue
253            if not have_seen_numeric_constraint:
254                #TODO(beeps): Why was <= dropped on the floor?
255                if branch.startswith('>=R') or branch.startswith('==R'):
256                    have_seen_numeric_constraint = True
257                elif 'tot' in branch:
258                    TotMilestoneManager().ConvertTotSpec(
259                            branch[branch.index('tot'):])
260                    have_seen_numeric_constraint = True
261                continue
262            raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
263
264
265    def __init__(self, name, suite, branch_specs, pool=None, num=None,
266                 boards=None, priority=None, timeout=None, file_bugs=False,
267                 cros_build_spec=None, firmware_rw_build_spec=None,
268                 test_source=None, job_retry=False, hour=None, day=None):
269        """Constructor
270
271        Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs,
272        we'll store them such that _FitsSpec() can be used to check whether a
273        given branch 'fits' with the specifications passed in here.
274        For example, given branch_specs = ['factory', '>=R18'], we'd set things
275        up so that _FitsSpec() would return True for 'factory', or 'RXX'
276        where XX is a number >= 18. Same check is done for branch_specs = [
277        'factory', '==R18'], which limit the test to only one specific branch.
278
279        Given branch_specs = ['factory', 'firmware'], _FitsSpec()
280        would pass only those two specific strings.
281
282        Example usage:
283          t = Task('Name', 'suite', ['factory', '>=R18'])
284          t._FitsSpec('factory')  # True
285          t._FitsSpec('R19')  # True
286          t._FitsSpec('R17')  # False
287          t._FitsSpec('firmware')  # False
288          t._FitsSpec('goober')  # False
289
290          t = Task('Name', 'suite', ['factory', '==R18'])
291          t._FitsSpec('R19')  # False, branch does not equal to 18
292          t._FitsSpec('R18')  # True
293          t._FitsSpec('R17')  # False
294
295        cros_build_spec and firmware_rw_build_spec are set for tests require
296        firmware update on the dut. Only one of them can be set.
297        For example:
298        branch_specs: ==tot
299        firmware_rw_build_spec: firmware
300        test_source: cros
301        This will run test using latest build on firmware branch, and the latest
302        ChromeOS build on ToT. The test source build is ChromeOS build.
303
304        branch_specs: firmware
305        cros_build_spec: ==tot-1
306        test_source: firmware_rw
307        This will run test using latest build on firmware branch, and the latest
308        ChromeOS build on dev channel (ToT-1). The test source build is the
309        firmware RW build.
310
311        branch_specs: ==tot
312        firmware_rw_build_spec: cros
313        test_source: cros
314        This will run test using latest ChromeOS and firmware RW build on ToT.
315        ChromeOS build on ToT. The test source build is ChromeOS build.
316
317        @param name: name of this task, e.g. 'NightlyPower'
318        @param suite: the name of the suite to run, e.g. 'bvt'
319        @param branch_specs: a pre-vetted iterable of branch specifiers,
320                             e.g. ['>=R18', 'factory']
321        @param pool: the pool of machines to use for scheduling purposes.
322                     Default: None
323        @param num: the number of devices across which to shard the test suite.
324                    Type: integer or None
325                    Default: None
326        @param boards: A comma separated list of boards to run this task on.
327                       Default: Run on all boards.
328        @param priority: The string name of a priority from
329                         client.common_lib.priorities.Priority.
330        @param timeout: The max lifetime of the suite in hours.
331        @param file_bugs: True if bug filing is desired for the suite created
332                          for this task.
333        @param cros_build_spec: Spec used to determine the ChromeOS build to
334                                test with a firmware build, e.g., tot, R41 etc.
335        @param firmware_rw_build_spec: Spec used to determine the firmware build
336                                       test with a ChromeOS build.
337        @param test_source: The source of test code when firmware will be
338                            updated in the test. The value can be `firmware_rw`
339                            or `cros`.
340        @param job_retry: Set to True to enable job-level retry. Default is
341                          False.
342        @param hour: An integer specifying the hour that a nightly run should
343                     be triggered, default is set to 21.
344        @param day: An integer specifying the day of a week that a weekly run
345                    should be triggered, default is set to 5, which is Saturday.
346        """
347        self._name = name
348        self._suite = suite
349        self._branch_specs = branch_specs
350        self._pool = pool
351        self._num = num
352        self._priority = priority
353        self._timeout = timeout
354        self._file_bugs = file_bugs
355        self._cros_build_spec = cros_build_spec
356        self._firmware_rw_build_spec = firmware_rw_build_spec
357        self._test_source = test_source
358        self._job_retry = job_retry
359        self.hour = hour
360        self.day = day
361
362        if ((self._firmware_rw_build_spec or cros_build_spec) and
363            not self.test_source in [Builds.FIRMWARE_RW, Builds.CROS]):
364            raise MalformedConfigEntry(
365                    'You must specify the build for test source. It can only '
366                    'be `firmware_rw` or `cros`.')
367        if self._firmware_rw_build_spec and cros_build_spec:
368            raise MalformedConfigEntry(
369                    'You cannot specify both firmware_rw_build_spec and '
370                    'cros_build_spec. firmware_rw_build_spec is used to specify'
371                    ' a firmware build when the suite requires firmware to be '
372                    'updated in the dut, its value can only be `firmware` or '
373                    '`cros`. cros_build_spec is used to specify a ChromeOS '
374                    'build when build_specs is set to firmware.')
375        if (self._firmware_rw_build_spec and
376            self._firmware_rw_build_spec not in ['firmware', 'cros']):
377            raise MalformedConfigEntry(
378                    'firmware_rw_build_spec can only be empty, firmware or '
379                    'cros. It does not support other build type yet.')
380
381        self._bare_branches = []
382        self._version_equal_constraint = False
383        self._version_gte_constraint = False
384        self._version_lte_constraint = False
385        if not branch_specs:
386            # Any milestone is OK.
387            self._numeric_constraint = version.LooseVersion('0')
388        else:
389            self._numeric_constraint = None
390            for spec in branch_specs:
391                if 'tot' in spec.lower():
392                    tot_str = spec[spec.index('tot'):]
393                    spec = spec.replace(
394                            tot_str, TotMilestoneManager().ConvertTotSpec(
395                                    tot_str))
396                if spec.startswith('>='):
397                    self._numeric_constraint = version.LooseVersion(
398                            spec.lstrip('>=R'))
399                    self._version_gte_constraint = True
400                elif spec.startswith('<='):
401                    self._numeric_constraint = version.LooseVersion(
402                            spec.lstrip('<=R'))
403                    self._version_lte_constraint = True
404                elif spec.startswith('=='):
405                    self._version_equal_constraint = True
406                    self._numeric_constraint = version.LooseVersion(
407                            spec.lstrip('==R'))
408                else:
409                    self._bare_branches.append(spec)
410
411        # Since we expect __hash__() and other comparator methods to be used
412        # frequently by set operations, and they use str() a lot, pre-compute
413        # the string representation of this object.
414        if num is None:
415            numStr = '[Default num]'
416        else:
417            numStr = '%d' % num
418
419        if boards is None:
420            self._boards = set()
421            boardsStr = '[All boards]'
422        else:
423            self._boards = set([x.strip() for x in boards.split(',')])
424            boardsStr = boards
425
426        self._str = ('%s: %s on %s with pool %s, boards [%s], file_bugs = %s '
427                     'across %s machines.' % (self.__class__.__name__,
428                     suite, branch_specs, pool, boardsStr, self._file_bugs,
429                     numStr))
430
431
432    def _FitsSpec(self, branch):
433        """Checks if a branch is deemed OK by this instance's branch specs.
434
435        When called on a branch name, will return whether that branch
436        'fits' the specifications stored in self._bare_branches,
437        self._numeric_constraint, self._version_equal_constraint,
438        self._version_gte_constraint and self._version_lte_constraint.
439
440        @param branch: the branch to check.
441        @return True if b 'fits' with stored specs, False otherwise.
442        """
443        if branch in BARE_BRANCHES:
444            return branch in self._bare_branches
445        if self._numeric_constraint:
446            if self._version_equal_constraint:
447                return version.LooseVersion(branch) == self._numeric_constraint
448            elif self._version_gte_constraint:
449                return version.LooseVersion(branch) >= self._numeric_constraint
450            elif self._version_lte_constraint:
451                return version.LooseVersion(branch) <= self._numeric_constraint
452            else:
453                # Default to great or equal constraint.
454                return version.LooseVersion(branch) >= self._numeric_constraint
455        else:
456            return False
457
458
459    @property
460    def name(self):
461        """Name of this task, e.g. 'NightlyPower'."""
462        return self._name
463
464
465    @property
466    def suite(self):
467        """Name of the suite to run, e.g. 'bvt'."""
468        return self._suite
469
470
471    @property
472    def branch_specs(self):
473        """a pre-vetted iterable of branch specifiers,
474        e.g. ['>=R18', 'factory']."""
475        return self._branch_specs
476
477
478    @property
479    def pool(self):
480        """The pool of machines to use for scheduling purposes."""
481        return self._pool
482
483
484    @property
485    def num(self):
486        """The number of devices across which to shard the test suite.
487        Type: integer or None"""
488        return self._num
489
490
491    @property
492    def boards(self):
493        """The boards on which to run this suite.
494        Type: Iterable of strings"""
495        return self._boards
496
497
498    @property
499    def priority(self):
500        """The priority of the suite"""
501        return self._priority
502
503
504    @property
505    def timeout(self):
506        """The maximum lifetime of the suite in hours."""
507        return self._timeout
508
509
510    @property
511    def cros_build_spec(self):
512        """The build spec of ChromeOS to test with a firmware build."""
513        return self._cros_build_spec
514
515
516    @property
517    def firmware_rw_build_spec(self):
518        """The build spec of firmware to test with a ChromeOS build."""
519        return self._firmware_rw_build_spec
520
521
522    @property
523    def test_source(self):
524        """Source of the test code, value can be `firmware_rw` or `cros`."""
525        return self._test_source
526
527
528    def __str__(self):
529        return self._str
530
531
532    def __repr__(self):
533        return self._str
534
535
536    def __lt__(self, other):
537        return str(self) < str(other)
538
539
540    def __le__(self, other):
541        return str(self) <= str(other)
542
543
544    def __eq__(self, other):
545        return str(self) == str(other)
546
547
548    def __ne__(self, other):
549        return str(self) != str(other)
550
551
552    def __gt__(self, other):
553        return str(self) > str(other)
554
555
556    def __ge__(self, other):
557        return str(self) >= str(other)
558
559
560    def __hash__(self):
561        """Allows instances to be correctly deduped when used in a set."""
562        return hash(str(self))
563
564
565    def _GetCrOSBuild(self, mv, board):
566        """Get the ChromeOS build name to test with firmware build.
567
568        The ChromeOS build to be used is determined by `self.cros_build_spec`.
569        Its value can be:
570        tot: use the latest ToT build.
571        tot-x: use the latest build in x milestone before ToT.
572        Rxx: use the latest build on xx milestone.
573
574        @param board: the board against which to run self._suite.
575        @param mv: an instance of manifest_versions.ManifestVersions.
576
577        @return: The ChromeOS build name to test with firmware build.
578
579        """
580        if not self.cros_build_spec:
581            return None
582        if self.cros_build_spec.startswith('tot'):
583            milestone = TotMilestoneManager().ConvertTotSpec(
584                    self.cros_build_spec)[1:]
585        elif self.cros_build_spec.startswith('R'):
586            milestone = self.cros_build_spec[1:]
587        milestone, latest_manifest = mv.GetLatestManifest(
588                board, 'release', milestone=milestone)
589        latest_build = base_event.BuildName(board, 'release', milestone,
590                                            latest_manifest)
591        logging.debug('Found latest build of %s for spec %s: %s',
592                      board, self.cros_build_spec, latest_build)
593        return latest_build
594
595
596    def _GetFirmwareRWBuild(self, mv, board, build_type):
597        """Get the firmware rw build name to test with ChromeOS build.
598
599        The firmware rw build to be used is determined by
600        `self.firmware_rw_build_spec`. Its value can be `firmware`, `cros` or
601        empty:
602        firmware: use the ToT build in firmware branch.
603        cros: use the ToT build in release (ChromeOS) branch.
604
605        @param mv: an instance of manifest_versions.ManifestVersions.
606        @param board: the board against which to run self._suite.
607        @param build_type: Build type of the firmware build, e.g., factory,
608                           firmware or release.
609
610        @return: The firmware rw build name to test with ChromeOS build.
611
612        """
613        if not self.firmware_rw_build_spec:
614            return None
615        latest_milestone, latest_manifest = mv.GetLatestManifest(
616                board, build_type)
617        latest_build = base_event.BuildName(board, build_type, latest_milestone,
618                                            latest_manifest)
619        logging.debug('Found latest firmware build of %s for spec %s: %s',
620                      board, self.firmware_rw_build_spec, latest_build)
621        return latest_build
622
623
624    def AvailableHosts(self, scheduler, board):
625        """Query what hosts are able to run a test on a board and pool
626        combination.
627
628        @param scheduler: an instance of DedupingScheduler, as defined in
629                          deduping_scheduler.py
630        @param board: the board against which one wants to run the test.
631        @return The list of hosts meeting the board and pool requirements,
632                or None if no hosts were found."""
633        if self._boards and board not in self._boards:
634            return []
635
636        labels = [Labels.BOARD_PREFIX + board]
637        if self._pool:
638            labels.append(Labels.POOL_PREFIX + self._pool)
639
640        return scheduler.CheckHostsExist(multiple_labels=labels)
641
642
643    def ShouldHaveAvailableHosts(self):
644        """As a sanity check, return true if we know for certain that
645        we should be able to schedule this test. If we claim this test
646        should be able to run, and it ends up not being scheduled, then
647        a warning will be reported.
648
649        @return True if this test should be able to run, False otherwise.
650        """
651        return self._pool == 'bvt'
652
653
654    def Run(self, scheduler, branch_builds, board, force=False, mv=None):
655        """Run this task.  Returns False if it should be destroyed.
656
657        Execute this task.  Attempt to schedule the associated suite.
658        Return True if this task should be kept around, False if it
659        should be destroyed.  This allows for one-shot Tasks.
660
661        @param scheduler: an instance of DedupingScheduler, as defined in
662                          deduping_scheduler.py
663        @param branch_builds: a dict mapping branch name to the build(s) to
664                              install for that branch, e.g.
665                              {'R18': ['x86-alex-release/R18-1655.0.0'],
666                               'R19': ['x86-alex-release/R19-2077.0.0']}
667        @param board: the board against which to run self._suite.
668        @param force: Always schedule the suite.
669        @param mv: an instance of manifest_versions.ManifestVersions.
670
671        @return True if the task should be kept, False if not
672
673        """
674        logging.info('Running %s on %s', self._name, board)
675        is_firmware_build = 'firmware' in self.branch_specs
676        # firmware_rw_build is only needed if firmware_rw_build_spec is given.
677        firmware_rw_build = None
678        try:
679            if is_firmware_build:
680                # When build specified in branch_specs is a firmware build,
681                # we need a ChromeOS build to test with the firmware build.
682                cros_build = self._GetCrOSBuild(mv, board)
683            elif self.firmware_rw_build_spec:
684                # When firmware_rw_build_spec is specified, the test involves
685                # updating the firmware by firmware build specified in
686                # firmware_rw_build_spec.
687                if self.firmware_rw_build_spec == 'cros':
688                    build_type = 'release'
689                else:
690                    build_type = self.firmware_rw_build_spec
691                firmware_rw_build = self._GetFirmwareRWBuild(
692                        mv, board, build_type)
693        except manifest_versions.QueryException as e:
694            logging.error(e)
695            logging.error('Running %s on %s is failed. Failed to find build '
696                          'required to run the suite.', self._name, board)
697            return False
698
699        builds = []
700        for branch, build in branch_builds.iteritems():
701            logging.info('Checking if %s fits spec %r',
702                         branch, self.branch_specs)
703            if self._FitsSpec(branch):
704                logging.debug('Build %s fits the spec.', build)
705                builds.extend(build)
706        for build in builds:
707            try:
708                if is_firmware_build:
709                    firmware_rw_build = build
710                else:
711                    cros_build = build
712                if self.test_source == Builds.FIRMWARE_RW:
713                    test_source_build = firmware_rw_build
714                elif self.test_source == Builds.CROS:
715                    test_source_build = cros_build
716                else:
717                    test_source_build = None
718                logging.debug('Schedule %s for builds %s.%s',
719                              self._suite, builds,
720                              (' Test source build is %s.' % test_source_build)
721                              if test_source_build else None)
722
723                if not scheduler.ScheduleSuite(
724                        self._suite, board, cros_build, self._pool, self._num,
725                        self._priority, self._timeout, force,
726                        file_bugs=self._file_bugs,
727                        firmware_rw_build=firmware_rw_build,
728                        test_source_build=test_source_build,
729                        job_retry=self._job_retry):
730                    logging.info('Skipping scheduling %s on %s for %s',
731                                 self._suite, builds, board)
732            except deduping_scheduler.DedupingSchedulerException as e:
733                logging.error(e)
734        return True
735
736
737class OneShotTask(Task):
738    """A Task that can be run only once.  Can schedule itself."""
739
740
741    def Run(self, scheduler, branch_builds, board, force=False, mv=None):
742        """Run this task.  Returns False, indicating it should be destroyed.
743
744        Run this task.  Attempt to schedule the associated suite.
745        Return False, indicating to the caller that it should discard this task.
746
747        @param scheduler: an instance of DedupingScheduler, as defined in
748                          deduping_scheduler.py
749        @param branch_builds: a dict mapping branch name to the build(s) to
750                              install for that branch, e.g.
751                              {'R18': ['x86-alex-release/R18-1655.0.0'],
752                               'R19': ['x86-alex-release/R19-2077.0.0']}
753        @param board: the board against which to run self._suite.
754        @param force: Always schedule the suite.
755        @param mv: an instance of manifest_versions.ManifestVersions.
756
757        @return False
758
759        """
760        super(OneShotTask, self).Run(scheduler, branch_builds, board, force,
761                                     mv)
762        return False
763