blob: ae2de387c425b7f24230aaf9cf0b17c6139fdc39 [file] [log] [blame]
Chris Masonefad911a2012-03-29 12:30:26 -07001# 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
Chris Masone67f06d62012-04-12 15:16:56 -07005
Chris Masone96f16632012-04-04 18:36:03 -07006import logging, re
Aviv Keshetd83ef442013-01-16 16:19:35 -08007import deduping_scheduler
Chris Masone67f06d62012-04-12 15:16:56 -07008from distutils import version
Alex Miller511a9e32012-07-03 09:16:47 -07009from constants import Labels
Chris Masone96f16632012-04-04 18:36:03 -070010
11
12class MalformedConfigEntry(Exception):
13 """Raised to indicate a failure to parse a Task out of a config."""
14 pass
Chris Masonefad911a2012-03-29 12:30:26 -070015
16
Alex Miller0d003572013-03-18 11:51:30 -070017BARE_BRANCHES = ['factory', 'firmware']
Chris Masone67f06d62012-04-12 15:16:56 -070018
19
20def PickBranchName(type, milestone):
Dan Shiaceb91d2013-02-20 12:41:28 -080021 """Pick branch name. If type is among BARE_BRANCHES, return type,
22 otherwise, return milestone.
23
24 @param type: type of the branch, e.g., 'release', 'factory', or 'firmware'
25 @param milestone: CrOS milestone number
26 """
Chris Masone67f06d62012-04-12 15:16:56 -070027 if type in BARE_BRANCHES:
28 return type
29 return milestone
30
31
Chris Masone013859b2012-04-01 13:45:26 -070032class Task(object):
Chris Masonefad911a2012-03-29 12:30:26 -070033 """Represents an entry from the scheduler config. Can schedule itself.
34
35 Each entry from the scheduler config file maps one-to-one to a
Chris Masone013859b2012-04-01 13:45:26 -070036 Task. Each instance has enough info to schedule itself
Chris Masonefad911a2012-03-29 12:30:26 -070037 on-demand with the AFE.
38
39 This class also overrides __hash__() and all comparitor methods to enable
40 correct use in dicts, sets, etc.
41 """
42
Chris Masone96f16632012-04-04 18:36:03 -070043
44 @staticmethod
45 def CreateFromConfigSection(config, section):
46 """Create a Task from a section of a config file.
47
48 The section to parse should look like this:
49 [TaskName]
50 suite: suite_to_run # Required
51 run_on: event_on which to run # Required
Dan Shiaceb91d2013-02-20 12:41:28 -080052 branch_specs: factory,firmware,>=R12 or ==R12 # Optional
Chris Masone96f16632012-04-04 18:36:03 -070053 pool: pool_of_devices # Optional
Chris Masone3eeaf0a2012-08-09 14:07:27 -070054 num: sharding_factor # int, Optional
Chris Masone96f16632012-04-04 18:36:03 -070055
Chris Masone67f06d62012-04-12 15:16:56 -070056 By default, Tasks run on all release branches, not factory or firmware.
57
Chris Masone96f16632012-04-04 18:36:03 -070058 @param config: a ForgivingConfigParser.
59 @param section: the section to parse into a Task.
60 @return keyword, Task object pair. One or both will be None on error.
61 @raise MalformedConfigEntry if there's a problem parsing |section|.
62 """
Alex Miller06695022012-07-18 09:31:36 -070063 if not config.has_section(section):
64 raise MalformedConfigEntry('unknown section %s' % section)
65
Chris Masone3eeaf0a2012-08-09 14:07:27 -070066 allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num'])
Alex Millerbb535e22012-07-11 20:11:33 -070067 # The parameter of union() is the keys under the section in the config
68 # The union merges this with the allowed set, so if any optional keys
69 # are omitted, then they're filled in. If any extra keys are present,
70 # then they will expand unioned set, causing it to fail the following
71 # comparison against the allowed set.
72 section_headers = allowed.union(dict(config.items(section)).keys())
73 if allowed != section_headers:
74 raise MalformedConfigEntry('unknown entries: %s' %
75 ", ".join(map(str, section_headers.difference(allowed))))
76
Chris Masone96f16632012-04-04 18:36:03 -070077 keyword = config.getstring(section, 'run_on')
78 suite = config.getstring(section, 'suite')
79 branches = config.getstring(section, 'branch_specs')
80 pool = config.getstring(section, 'pool')
Chris Masone3eeaf0a2012-08-09 14:07:27 -070081 try:
82 num = config.getint(section, 'num')
83 except ValueError as e:
84 raise MalformedConfigEntry("Ill-specified 'num': %r" %e)
Chris Masone96f16632012-04-04 18:36:03 -070085 if not keyword:
86 raise MalformedConfigEntry('No event to |run_on|.')
87 if not suite:
88 raise MalformedConfigEntry('No |suite|')
89 specs = []
90 if branches:
91 specs = re.split('\s*,\s*', branches)
92 Task.CheckBranchSpecs(specs)
Chris Masone3eeaf0a2012-08-09 14:07:27 -070093 return keyword, Task(section, suite, specs, pool, num)
Chris Masone96f16632012-04-04 18:36:03 -070094
95
96 @staticmethod
97 def CheckBranchSpecs(branch_specs):
98 """Make sure entries in the list branch_specs are correctly formed.
99
Chris Masone67f06d62012-04-12 15:16:56 -0700100 We accept any of BARE_BRANCHES in |branch_specs|, as
Dan Shiaceb91d2013-02-20 12:41:28 -0800101 well as _one_ string of the form '>=RXX' or '==RXX', where 'RXX' is a
Chris Masone96f16632012-04-04 18:36:03 -0700102 CrOS milestone number.
103
104 @param branch_specs: an iterable of branch specifiers.
105 @raise MalformedConfigEntry if there's a problem parsing |branch_specs|.
106 """
107 have_seen_numeric_constraint = False
108 for branch in branch_specs:
Chris Masone67f06d62012-04-12 15:16:56 -0700109 if branch in BARE_BRANCHES:
Chris Masone96f16632012-04-04 18:36:03 -0700110 continue
Dan Shiaceb91d2013-02-20 12:41:28 -0800111 if ((branch.startswith('>=R') or branch.startswith('==R')) and
112 not have_seen_numeric_constraint):
Chris Masone96f16632012-04-04 18:36:03 -0700113 have_seen_numeric_constraint = True
114 continue
Chris Masone97cf0a72012-05-16 09:55:52 -0700115 raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
Chris Masone96f16632012-04-04 18:36:03 -0700116
117
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700118 def __init__(self, name, suite, branch_specs, pool=None, num=None):
Chris Masonefad911a2012-03-29 12:30:26 -0700119 """Constructor
120
Chris Masone96f16632012-04-04 18:36:03 -0700121 Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs,
122 we'll store them such that _FitsSpec() can be used to check whether a
123 given branch 'fits' with the specifications passed in here.
124 For example, given branch_specs = ['factory', '>=R18'], we'd set things
125 up so that _FitsSpec() would return True for 'factory', or 'RXX'
Dan Shiaceb91d2013-02-20 12:41:28 -0800126 where XX is a number >= 18. Same check is done for branch_specs = [
127 'factory', '==R18'], which limit the test to only one specific branch.
Chris Masone96f16632012-04-04 18:36:03 -0700128
129 Given branch_specs = ['factory', 'firmware'], _FitsSpec()
130 would pass only those two specific strings.
131
132 Example usage:
Chris Masonecc4631d2012-04-20 12:06:39 -0700133 t = Task('Name', 'suite', ['factory', '>=R18'])
Chris Masone96f16632012-04-04 18:36:03 -0700134 t._FitsSpec('factory') # True
135 t._FitsSpec('R19') # True
136 t._FitsSpec('R17') # False
137 t._FitsSpec('firmware') # False
138 t._FitsSpec('goober') # False
139
Dan Shiaceb91d2013-02-20 12:41:28 -0800140 t = Task('Name', 'suite', ['factory', '==R18'])
141 t._FitsSpec('R19') # False, branch does not equal to 18
142 t._FitsSpec('R18') # True
143 t._FitsSpec('R17') # False
144
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700145 @param name: name of this task, e.g. 'NightlyPower'
Chris Masonefad911a2012-03-29 12:30:26 -0700146 @param suite: the name of the suite to run, e.g. 'bvt'
Chris Masone96f16632012-04-04 18:36:03 -0700147 @param branch_specs: a pre-vetted iterable of branch specifiers,
148 e.g. ['>=R18', 'factory']
Chris Masonefad911a2012-03-29 12:30:26 -0700149 @param pool: the pool of machines to use for scheduling purposes.
150 Default: None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700151 @param num: the number of devices across which to shard the test suite.
Aviv Keshetd83ef442013-01-16 16:19:35 -0800152 Type: integer or None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700153 Default: None
Chris Masonefad911a2012-03-29 12:30:26 -0700154 """
Chris Masonecc4631d2012-04-20 12:06:39 -0700155 self._name = name
Chris Masonefad911a2012-03-29 12:30:26 -0700156 self._suite = suite
Chris Masone96f16632012-04-04 18:36:03 -0700157 self._branch_specs = branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700158 self._pool = pool
Aviv Keshetd83ef442013-01-16 16:19:35 -0800159 self._num = num
Chris Masone96f16632012-04-04 18:36:03 -0700160
161 self._bare_branches = []
Dan Shiaceb91d2013-02-20 12:41:28 -0800162 self._version_equal_constraint = False
Chris Masone67f06d62012-04-12 15:16:56 -0700163 if not branch_specs:
164 # Any milestone is OK.
165 self._numeric_constraint = version.LooseVersion('0')
166 else:
167 self._numeric_constraint = None
168 for spec in branch_specs:
169 if spec.startswith('>='):
170 self._numeric_constraint = version.LooseVersion(
171 spec.lstrip('>=R'))
Dan Shiaceb91d2013-02-20 12:41:28 -0800172 elif spec.startswith('=='):
173 self._version_equal_constraint = True
174 self._numeric_constraint = version.LooseVersion(
175 spec.lstrip('==R'))
Chris Masone67f06d62012-04-12 15:16:56 -0700176 else:
177 self._bare_branches.append(spec)
Chris Masonefad911a2012-03-29 12:30:26 -0700178 # Since we expect __hash__() and other comparitor methods to be used
179 # frequently by set operations, and they use str() a lot, pre-compute
180 # the string representation of this object.
Aviv Keshet8ce3c982013-01-24 09:23:13 -0800181 if num is None:
182 numStr = '[Default num]'
183 else:
184 numStr = '%d' % num
185 self._str = '%s: %s on %s with pool %s, across %s machines' % (
186 self.__class__.__name__, suite, branch_specs, pool, numStr)
Chris Masone96f16632012-04-04 18:36:03 -0700187
188
189 def _FitsSpec(self, branch):
190 """Checks if a branch is deemed OK by this instance's branch specs.
191
192 When called on a branch name, will return whether that branch
Dan Shiaceb91d2013-02-20 12:41:28 -0800193 'fits' the specifications stored in self._bare_branches,
194 self._numeric_constraint and self._version_equal_constraint.
Chris Masone96f16632012-04-04 18:36:03 -0700195
196 @param branch: the branch to check.
197 @return True if b 'fits' with stored specs, False otherwise.
198 """
Chris Masone657e1552012-05-30 17:06:20 -0700199 if branch in BARE_BRANCHES:
200 return branch in self._bare_branches
Dan Shiaceb91d2013-02-20 12:41:28 -0800201 if self._numeric_constraint:
202 if self._version_equal_constraint:
203 return version.LooseVersion(branch) == self._numeric_constraint
204 else:
205 return version.LooseVersion(branch) >= self._numeric_constraint
206 else:
207 return False
Chris Masonefad911a2012-03-29 12:30:26 -0700208
209
210 @property
Alex Miller9979b5a2012-11-01 17:36:12 -0700211 def name(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800212 """Name of this task, e.g. 'NightlyPower'."""
Alex Miller9979b5a2012-11-01 17:36:12 -0700213 return self._name
214
215
216 @property
Chris Masonefad911a2012-03-29 12:30:26 -0700217 def suite(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800218 """Name of the suite to run, e.g. 'bvt'."""
Chris Masonefad911a2012-03-29 12:30:26 -0700219 return self._suite
220
221
222 @property
Chris Masone96f16632012-04-04 18:36:03 -0700223 def branch_specs(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800224 """a pre-vetted iterable of branch specifiers,
225 e.g. ['>=R18', 'factory']."""
Chris Masone96f16632012-04-04 18:36:03 -0700226 return self._branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700227
228
229 @property
Chris Masone3fba86f2012-04-03 10:06:56 -0700230 def pool(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800231 """The pool of machines to use for scheduling purposes."""
Chris Masone3fba86f2012-04-03 10:06:56 -0700232 return self._pool
Chris Masonefad911a2012-03-29 12:30:26 -0700233
234
Alex Miller9979b5a2012-11-01 17:36:12 -0700235 @property
236 def num(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800237 """The number of devices across which to shard the test suite.
238 Type: integer or None"""
Alex Miller9979b5a2012-11-01 17:36:12 -0700239 return self._num
240
241
Chris Masonefad911a2012-03-29 12:30:26 -0700242 def __str__(self):
243 return self._str
244
245
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700246 def __repr__(self):
247 return self._str
248
249
Chris Masonefad911a2012-03-29 12:30:26 -0700250 def __lt__(self, other):
251 return str(self) < str(other)
252
253
254 def __le__(self, other):
255 return str(self) <= str(other)
256
257
258 def __eq__(self, other):
259 return str(self) == str(other)
260
261
262 def __ne__(self, other):
263 return str(self) != str(other)
264
265
266 def __gt__(self, other):
267 return str(self) > str(other)
268
269
270 def __ge__(self, other):
271 return str(self) >= str(other)
272
273
274 def __hash__(self):
275 """Allows instances to be correctly deduped when used in a set."""
276 return hash(str(self))
277
278
Alex Miller7ce1b362012-07-10 09:24:10 -0700279 def AvailableHosts(self, scheduler, board):
280 """Query what hosts are able to run a test on a board and pool
281 combination.
Alex Miller511a9e32012-07-03 09:16:47 -0700282
283 @param scheduler: an instance of DedupingScheduler, as defined in
284 deduping_scheduler.py
285 @param board: the board against which one wants to run the test.
Alex Miller7ce1b362012-07-10 09:24:10 -0700286 @return The list of hosts meeting the board and pool requirements,
287 or None if no hosts were found."""
Alex Miller511a9e32012-07-03 09:16:47 -0700288 labels = [Labels.BOARD_PREFIX + board]
289 if self._pool:
Chris Masonecd214e02012-07-10 16:22:10 -0700290 labels.append(Labels.POOL_PREFIX + self._pool)
Alex Miller511a9e32012-07-03 09:16:47 -0700291
Alex Miller7ce1b362012-07-10 09:24:10 -0700292 return scheduler.GetHosts(multiple_labels=labels)
Alex Miller511a9e32012-07-03 09:16:47 -0700293
294
Alex Millerd621cf22012-07-11 13:57:10 -0700295 def ShouldHaveAvailableHosts(self):
296 """As a sanity check, return true if we know for certain that
297 we should be able to schedule this test. If we claim this test
298 should be able to run, and it ends up not being scheduled, then
299 a warning will be reported.
300
301 @return True if this test should be able to run, False otherwise.
302 """
303 return self._pool == 'bvt'
304
305
Chris Masone96f16632012-04-04 18:36:03 -0700306 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700307 """Run this task. Returns False if it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700308
Chris Masone013859b2012-04-01 13:45:26 -0700309 Execute this task. Attempt to schedule the associated suite.
310 Return True if this task should be kept around, False if it
311 should be destroyed. This allows for one-shot Tasks.
Chris Masonefad911a2012-03-29 12:30:26 -0700312
313 @param scheduler: an instance of DedupingScheduler, as defined in
314 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700315 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700316 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700317 {'R18': ['x86-alex-release/R18-1655.0.0'],
318 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700319 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700320 @param force: Always schedule the suite.
Chris Masone013859b2012-04-01 13:45:26 -0700321 @return True if the task should be kept, False if not
Chris Masonefad911a2012-03-29 12:30:26 -0700322 """
Chris Masonea3a38172012-05-14 15:19:56 -0700323 logging.info('Running %s on %s', self._name, board)
Chris Masone96f16632012-04-04 18:36:03 -0700324 builds = []
Chris Masonefe5a5092012-04-11 18:29:07 -0700325 for branch, build in branch_builds.iteritems():
Chris Masonea3a38172012-05-14 15:19:56 -0700326 logging.info('Checking if %s fits spec %r',
327 branch, self.branch_specs)
Chris Masone96f16632012-04-04 18:36:03 -0700328 if self._FitsSpec(branch):
Chris Masone05b19442012-04-17 13:37:55 -0700329 builds.extend(build)
Chris Masone96f16632012-04-04 18:36:03 -0700330 for build in builds:
Chris Masone3fba86f2012-04-03 10:06:56 -0700331 try:
Chris Masone96f16632012-04-04 18:36:03 -0700332 if not scheduler.ScheduleSuite(self._suite, board, build,
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700333 self._pool, self._num, force):
Chris Masone96f16632012-04-04 18:36:03 -0700334 logging.info('Skipping scheduling %s on %s for %s',
335 self._suite, build, board)
Chris Masone3fba86f2012-04-03 10:06:56 -0700336 except deduping_scheduler.DedupingSchedulerException as e:
337 logging.error(e)
Chris Masonefad911a2012-03-29 12:30:26 -0700338 return True
339
340
Chris Masone013859b2012-04-01 13:45:26 -0700341class OneShotTask(Task):
342 """A Task that can be run only once. Can schedule itself."""
Chris Masonefad911a2012-03-29 12:30:26 -0700343
344
Chris Masone96f16632012-04-04 18:36:03 -0700345 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700346 """Run this task. Returns False, indicating it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700347
Chris Masone013859b2012-04-01 13:45:26 -0700348 Run this task. Attempt to schedule the associated suite.
349 Return False, indicating to the caller that it should discard this task.
Chris Masonefad911a2012-03-29 12:30:26 -0700350
351 @param scheduler: an instance of DedupingScheduler, as defined in
352 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700353 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700354 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700355 {'R18': ['x86-alex-release/R18-1655.0.0'],
356 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700357 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700358 @param force: Always schedule the suite.
359 @return False
360 """
Chris Masone96f16632012-04-04 18:36:03 -0700361 super(OneShotTask, self).Run(scheduler, branch_builds, board, force)
Chris Masonefad911a2012-03-29 12:30:26 -0700362 return False