blob: c4cb3c48fcc35546a0e4d4d1961989f67a8aab34 [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
Alex Millerc7bcf8b2013-09-07 20:13:20 -07008import driver
Chris Masone67f06d62012-04-12 15:16:56 -07009from distutils import version
Alex Miller511a9e32012-07-03 09:16:47 -070010from constants import Labels
Chris Masone96f16632012-04-04 18:36:03 -070011
12
13class MalformedConfigEntry(Exception):
14 """Raised to indicate a failure to parse a Task out of a config."""
15 pass
Chris Masonefad911a2012-03-29 12:30:26 -070016
17
Alex Miller0d003572013-03-18 11:51:30 -070018BARE_BRANCHES = ['factory', 'firmware']
Chris Masone67f06d62012-04-12 15:16:56 -070019
20
21def PickBranchName(type, milestone):
Dan Shiaceb91d2013-02-20 12:41:28 -080022 """Pick branch name. If type is among BARE_BRANCHES, return type,
23 otherwise, return milestone.
24
25 @param type: type of the branch, e.g., 'release', 'factory', or 'firmware'
26 @param milestone: CrOS milestone number
27 """
Chris Masone67f06d62012-04-12 15:16:56 -070028 if type in BARE_BRANCHES:
29 return type
30 return milestone
31
32
Chris Masone013859b2012-04-01 13:45:26 -070033class Task(object):
Chris Masonefad911a2012-03-29 12:30:26 -070034 """Represents an entry from the scheduler config. Can schedule itself.
35
36 Each entry from the scheduler config file maps one-to-one to a
Chris Masone013859b2012-04-01 13:45:26 -070037 Task. Each instance has enough info to schedule itself
Chris Masonefad911a2012-03-29 12:30:26 -070038 on-demand with the AFE.
39
40 This class also overrides __hash__() and all comparitor methods to enable
41 correct use in dicts, sets, etc.
42 """
43
Chris Masone96f16632012-04-04 18:36:03 -070044
45 @staticmethod
46 def CreateFromConfigSection(config, section):
47 """Create a Task from a section of a config file.
48
49 The section to parse should look like this:
50 [TaskName]
51 suite: suite_to_run # Required
52 run_on: event_on which to run # Required
Dan Shiaceb91d2013-02-20 12:41:28 -080053 branch_specs: factory,firmware,>=R12 or ==R12 # Optional
Chris Masone96f16632012-04-04 18:36:03 -070054 pool: pool_of_devices # Optional
Chris Masone3eeaf0a2012-08-09 14:07:27 -070055 num: sharding_factor # int, Optional
Alex Millerf2b57442013-09-07 18:40:02 -070056 boards: board1, board2 # comma seperated string, Optional
Chris Masone96f16632012-04-04 18:36:03 -070057
Chris Masone67f06d62012-04-12 15:16:56 -070058 By default, Tasks run on all release branches, not factory or firmware.
59
Chris Masone96f16632012-04-04 18:36:03 -070060 @param config: a ForgivingConfigParser.
61 @param section: the section to parse into a Task.
62 @return keyword, Task object pair. One or both will be None on error.
63 @raise MalformedConfigEntry if there's a problem parsing |section|.
64 """
Alex Miller06695022012-07-18 09:31:36 -070065 if not config.has_section(section):
66 raise MalformedConfigEntry('unknown section %s' % section)
67
Alex Millerf2b57442013-09-07 18:40:02 -070068 allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num',
69 'boards'])
Alex Millerbb535e22012-07-11 20:11:33 -070070 # The parameter of union() is the keys under the section in the config
71 # The union merges this with the allowed set, so if any optional keys
72 # are omitted, then they're filled in. If any extra keys are present,
73 # then they will expand unioned set, causing it to fail the following
74 # comparison against the allowed set.
75 section_headers = allowed.union(dict(config.items(section)).keys())
76 if allowed != section_headers:
77 raise MalformedConfigEntry('unknown entries: %s' %
78 ", ".join(map(str, section_headers.difference(allowed))))
79
Chris Masone96f16632012-04-04 18:36:03 -070080 keyword = config.getstring(section, 'run_on')
81 suite = config.getstring(section, 'suite')
82 branches = config.getstring(section, 'branch_specs')
83 pool = config.getstring(section, 'pool')
Alex Millerf2b57442013-09-07 18:40:02 -070084 boards = config.getstring(section, 'boards')
Alex Millerc7bcf8b2013-09-07 20:13:20 -070085 for klass in driver.Driver.EVENT_CLASSES:
86 if klass.KEYWORD == keyword:
87 priority = klass.PRIORITY
88 timeout = klass.TIMEOUT
89 break
90 else:
91 priority = None
92 timeout = None
Chris Masone3eeaf0a2012-08-09 14:07:27 -070093 try:
94 num = config.getint(section, 'num')
95 except ValueError as e:
96 raise MalformedConfigEntry("Ill-specified 'num': %r" %e)
Chris Masone96f16632012-04-04 18:36:03 -070097 if not keyword:
98 raise MalformedConfigEntry('No event to |run_on|.')
99 if not suite:
100 raise MalformedConfigEntry('No |suite|')
101 specs = []
102 if branches:
103 specs = re.split('\s*,\s*', branches)
104 Task.CheckBranchSpecs(specs)
Alex Millerc7bcf8b2013-09-07 20:13:20 -0700105 return keyword, Task(section, suite, specs, pool, num, boards,
106 priority, timeout)
Chris Masone96f16632012-04-04 18:36:03 -0700107
108
109 @staticmethod
110 def CheckBranchSpecs(branch_specs):
111 """Make sure entries in the list branch_specs are correctly formed.
112
Chris Masone67f06d62012-04-12 15:16:56 -0700113 We accept any of BARE_BRANCHES in |branch_specs|, as
Dan Shiaceb91d2013-02-20 12:41:28 -0800114 well as _one_ string of the form '>=RXX' or '==RXX', where 'RXX' is a
Chris Masone96f16632012-04-04 18:36:03 -0700115 CrOS milestone number.
116
117 @param branch_specs: an iterable of branch specifiers.
118 @raise MalformedConfigEntry if there's a problem parsing |branch_specs|.
119 """
120 have_seen_numeric_constraint = False
121 for branch in branch_specs:
Chris Masone67f06d62012-04-12 15:16:56 -0700122 if branch in BARE_BRANCHES:
Chris Masone96f16632012-04-04 18:36:03 -0700123 continue
Dan Shiaceb91d2013-02-20 12:41:28 -0800124 if ((branch.startswith('>=R') or branch.startswith('==R')) and
125 not have_seen_numeric_constraint):
Chris Masone96f16632012-04-04 18:36:03 -0700126 have_seen_numeric_constraint = True
127 continue
Chris Masone97cf0a72012-05-16 09:55:52 -0700128 raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
Chris Masone96f16632012-04-04 18:36:03 -0700129
130
Alex Millerf2b57442013-09-07 18:40:02 -0700131 def __init__(self, name, suite, branch_specs, pool=None, num=None,
Alex Millerc7bcf8b2013-09-07 20:13:20 -0700132 boards=None, priority=None, timeout=None):
Chris Masonefad911a2012-03-29 12:30:26 -0700133 """Constructor
134
Chris Masone96f16632012-04-04 18:36:03 -0700135 Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs,
136 we'll store them such that _FitsSpec() can be used to check whether a
137 given branch 'fits' with the specifications passed in here.
138 For example, given branch_specs = ['factory', '>=R18'], we'd set things
139 up so that _FitsSpec() would return True for 'factory', or 'RXX'
Dan Shiaceb91d2013-02-20 12:41:28 -0800140 where XX is a number >= 18. Same check is done for branch_specs = [
141 'factory', '==R18'], which limit the test to only one specific branch.
Chris Masone96f16632012-04-04 18:36:03 -0700142
143 Given branch_specs = ['factory', 'firmware'], _FitsSpec()
144 would pass only those two specific strings.
145
146 Example usage:
Chris Masonecc4631d2012-04-20 12:06:39 -0700147 t = Task('Name', 'suite', ['factory', '>=R18'])
Chris Masone96f16632012-04-04 18:36:03 -0700148 t._FitsSpec('factory') # True
149 t._FitsSpec('R19') # True
150 t._FitsSpec('R17') # False
151 t._FitsSpec('firmware') # False
152 t._FitsSpec('goober') # False
153
Dan Shiaceb91d2013-02-20 12:41:28 -0800154 t = Task('Name', 'suite', ['factory', '==R18'])
155 t._FitsSpec('R19') # False, branch does not equal to 18
156 t._FitsSpec('R18') # True
157 t._FitsSpec('R17') # False
158
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700159 @param name: name of this task, e.g. 'NightlyPower'
Chris Masonefad911a2012-03-29 12:30:26 -0700160 @param suite: the name of the suite to run, e.g. 'bvt'
Chris Masone96f16632012-04-04 18:36:03 -0700161 @param branch_specs: a pre-vetted iterable of branch specifiers,
162 e.g. ['>=R18', 'factory']
Chris Masonefad911a2012-03-29 12:30:26 -0700163 @param pool: the pool of machines to use for scheduling purposes.
164 Default: None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700165 @param num: the number of devices across which to shard the test suite.
Aviv Keshetd83ef442013-01-16 16:19:35 -0800166 Type: integer or None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700167 Default: None
Alex Millerf2b57442013-09-07 18:40:02 -0700168 @param boards: A comma seperated list of boards to run this task on.
169 Default: Run on all boards.
Alex Millerc7bcf8b2013-09-07 20:13:20 -0700170 @param priority: The string name of a priority from
171 client.common_lib.priorities.Priority.
172 @param timeout: The max lifetime of the suite in hours.
Chris Masonefad911a2012-03-29 12:30:26 -0700173 """
Chris Masonecc4631d2012-04-20 12:06:39 -0700174 self._name = name
Chris Masonefad911a2012-03-29 12:30:26 -0700175 self._suite = suite
Chris Masone96f16632012-04-04 18:36:03 -0700176 self._branch_specs = branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700177 self._pool = pool
Aviv Keshetd83ef442013-01-16 16:19:35 -0800178 self._num = num
Alex Millerc7bcf8b2013-09-07 20:13:20 -0700179 self._priority = priority
180 self._timeout = timeout
Chris Masone96f16632012-04-04 18:36:03 -0700181
182 self._bare_branches = []
Dan Shiaceb91d2013-02-20 12:41:28 -0800183 self._version_equal_constraint = False
Chris Masone67f06d62012-04-12 15:16:56 -0700184 if not branch_specs:
185 # Any milestone is OK.
186 self._numeric_constraint = version.LooseVersion('0')
187 else:
188 self._numeric_constraint = None
189 for spec in branch_specs:
190 if spec.startswith('>='):
191 self._numeric_constraint = version.LooseVersion(
192 spec.lstrip('>=R'))
Dan Shiaceb91d2013-02-20 12:41:28 -0800193 elif spec.startswith('=='):
194 self._version_equal_constraint = True
195 self._numeric_constraint = version.LooseVersion(
196 spec.lstrip('==R'))
Chris Masone67f06d62012-04-12 15:16:56 -0700197 else:
198 self._bare_branches.append(spec)
Alex Millerf2b57442013-09-07 18:40:02 -0700199
Chris Masonefad911a2012-03-29 12:30:26 -0700200 # Since we expect __hash__() and other comparitor methods to be used
201 # frequently by set operations, and they use str() a lot, pre-compute
202 # the string representation of this object.
Aviv Keshet8ce3c982013-01-24 09:23:13 -0800203 if num is None:
204 numStr = '[Default num]'
205 else:
206 numStr = '%d' % num
Alex Millerf2b57442013-09-07 18:40:02 -0700207
208 if boards is None:
209 self._boards = set()
210 boardsStr = '[All boards]'
211 else:
212 self._boards = set([x.strip() for x in boards.split(',')])
213 boardsStr = boards
214
215 self._str = ('%s: %s on %s with pool %s, boards [%s], '
216 'across %s machines' % (self.__class__.__name__,
217 suite, branch_specs, pool, boardsStr, numStr))
Chris Masone96f16632012-04-04 18:36:03 -0700218
219
220 def _FitsSpec(self, branch):
221 """Checks if a branch is deemed OK by this instance's branch specs.
222
223 When called on a branch name, will return whether that branch
Dan Shiaceb91d2013-02-20 12:41:28 -0800224 'fits' the specifications stored in self._bare_branches,
225 self._numeric_constraint and self._version_equal_constraint.
Chris Masone96f16632012-04-04 18:36:03 -0700226
227 @param branch: the branch to check.
228 @return True if b 'fits' with stored specs, False otherwise.
229 """
Chris Masone657e1552012-05-30 17:06:20 -0700230 if branch in BARE_BRANCHES:
231 return branch in self._bare_branches
Dan Shiaceb91d2013-02-20 12:41:28 -0800232 if self._numeric_constraint:
233 if self._version_equal_constraint:
234 return version.LooseVersion(branch) == self._numeric_constraint
235 else:
236 return version.LooseVersion(branch) >= self._numeric_constraint
237 else:
238 return False
Chris Masonefad911a2012-03-29 12:30:26 -0700239
240
241 @property
Alex Miller9979b5a2012-11-01 17:36:12 -0700242 def name(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800243 """Name of this task, e.g. 'NightlyPower'."""
Alex Miller9979b5a2012-11-01 17:36:12 -0700244 return self._name
245
246
247 @property
Chris Masonefad911a2012-03-29 12:30:26 -0700248 def suite(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800249 """Name of the suite to run, e.g. 'bvt'."""
Chris Masonefad911a2012-03-29 12:30:26 -0700250 return self._suite
251
252
253 @property
Chris Masone96f16632012-04-04 18:36:03 -0700254 def branch_specs(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800255 """a pre-vetted iterable of branch specifiers,
256 e.g. ['>=R18', 'factory']."""
Chris Masone96f16632012-04-04 18:36:03 -0700257 return self._branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700258
259
260 @property
Chris Masone3fba86f2012-04-03 10:06:56 -0700261 def pool(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800262 """The pool of machines to use for scheduling purposes."""
Chris Masone3fba86f2012-04-03 10:06:56 -0700263 return self._pool
Chris Masonefad911a2012-03-29 12:30:26 -0700264
265
Alex Miller9979b5a2012-11-01 17:36:12 -0700266 @property
267 def num(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800268 """The number of devices across which to shard the test suite.
269 Type: integer or None"""
Alex Miller9979b5a2012-11-01 17:36:12 -0700270 return self._num
271
272
Alex Millerf2b57442013-09-07 18:40:02 -0700273 @property
274 def boards(self):
275 """The boards on which to run this suite.
276 Type: Iterable of strings"""
277 return self._boards
278
279
Alex Millerc7bcf8b2013-09-07 20:13:20 -0700280 @property
281 def priority(self):
282 """The priority of the suite"""
283 return self._priority
284
285
286 @property
287 def timeout(self):
288 """The maximum lifetime of the suite in hours."""
289 return self._timeout
290
291
Chris Masonefad911a2012-03-29 12:30:26 -0700292 def __str__(self):
293 return self._str
294
295
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700296 def __repr__(self):
297 return self._str
298
299
Chris Masonefad911a2012-03-29 12:30:26 -0700300 def __lt__(self, other):
301 return str(self) < str(other)
302
303
304 def __le__(self, other):
305 return str(self) <= str(other)
306
307
308 def __eq__(self, other):
309 return str(self) == str(other)
310
311
312 def __ne__(self, other):
313 return str(self) != str(other)
314
315
316 def __gt__(self, other):
317 return str(self) > str(other)
318
319
320 def __ge__(self, other):
321 return str(self) >= str(other)
322
323
324 def __hash__(self):
325 """Allows instances to be correctly deduped when used in a set."""
326 return hash(str(self))
327
328
Alex Miller7ce1b362012-07-10 09:24:10 -0700329 def AvailableHosts(self, scheduler, board):
330 """Query what hosts are able to run a test on a board and pool
331 combination.
Alex Miller511a9e32012-07-03 09:16:47 -0700332
333 @param scheduler: an instance of DedupingScheduler, as defined in
334 deduping_scheduler.py
335 @param board: the board against which one wants to run the test.
Alex Miller7ce1b362012-07-10 09:24:10 -0700336 @return The list of hosts meeting the board and pool requirements,
337 or None if no hosts were found."""
Alex Millerf2b57442013-09-07 18:40:02 -0700338 if self._boards and board not in self._boards:
339 return []
340
Alex Miller511a9e32012-07-03 09:16:47 -0700341 labels = [Labels.BOARD_PREFIX + board]
342 if self._pool:
Chris Masonecd214e02012-07-10 16:22:10 -0700343 labels.append(Labels.POOL_PREFIX + self._pool)
Alex Miller511a9e32012-07-03 09:16:47 -0700344
Alex Miller7ce1b362012-07-10 09:24:10 -0700345 return scheduler.GetHosts(multiple_labels=labels)
Alex Miller511a9e32012-07-03 09:16:47 -0700346
347
Alex Millerd621cf22012-07-11 13:57:10 -0700348 def ShouldHaveAvailableHosts(self):
349 """As a sanity check, return true if we know for certain that
350 we should be able to schedule this test. If we claim this test
351 should be able to run, and it ends up not being scheduled, then
352 a warning will be reported.
353
354 @return True if this test should be able to run, False otherwise.
355 """
356 return self._pool == 'bvt'
357
358
Chris Masone96f16632012-04-04 18:36:03 -0700359 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700360 """Run this task. Returns False if it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700361
Chris Masone013859b2012-04-01 13:45:26 -0700362 Execute this task. Attempt to schedule the associated suite.
363 Return True if this task should be kept around, False if it
364 should be destroyed. This allows for one-shot Tasks.
Chris Masonefad911a2012-03-29 12:30:26 -0700365
366 @param scheduler: an instance of DedupingScheduler, as defined in
367 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700368 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700369 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700370 {'R18': ['x86-alex-release/R18-1655.0.0'],
371 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700372 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700373 @param force: Always schedule the suite.
Chris Masone013859b2012-04-01 13:45:26 -0700374 @return True if the task should be kept, False if not
Chris Masonefad911a2012-03-29 12:30:26 -0700375 """
Chris Masonea3a38172012-05-14 15:19:56 -0700376 logging.info('Running %s on %s', self._name, board)
Chris Masone96f16632012-04-04 18:36:03 -0700377 builds = []
Chris Masonefe5a5092012-04-11 18:29:07 -0700378 for branch, build in branch_builds.iteritems():
Chris Masonea3a38172012-05-14 15:19:56 -0700379 logging.info('Checking if %s fits spec %r',
380 branch, self.branch_specs)
Chris Masone96f16632012-04-04 18:36:03 -0700381 if self._FitsSpec(branch):
Chris Masone05b19442012-04-17 13:37:55 -0700382 builds.extend(build)
Chris Masone96f16632012-04-04 18:36:03 -0700383 for build in builds:
Chris Masone3fba86f2012-04-03 10:06:56 -0700384 try:
Chris Masone96f16632012-04-04 18:36:03 -0700385 if not scheduler.ScheduleSuite(self._suite, board, build,
Alex Millerc7bcf8b2013-09-07 20:13:20 -0700386 self._pool, self._num,
387 self._priority, self._timeout,
388 force):
Chris Masone96f16632012-04-04 18:36:03 -0700389 logging.info('Skipping scheduling %s on %s for %s',
390 self._suite, build, board)
Chris Masone3fba86f2012-04-03 10:06:56 -0700391 except deduping_scheduler.DedupingSchedulerException as e:
392 logging.error(e)
Chris Masonefad911a2012-03-29 12:30:26 -0700393 return True
394
395
Chris Masone013859b2012-04-01 13:45:26 -0700396class OneShotTask(Task):
397 """A Task that can be run only once. Can schedule itself."""
Chris Masonefad911a2012-03-29 12:30:26 -0700398
399
Chris Masone96f16632012-04-04 18:36:03 -0700400 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700401 """Run this task. Returns False, indicating it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700402
Chris Masone013859b2012-04-01 13:45:26 -0700403 Run this task. Attempt to schedule the associated suite.
404 Return False, indicating to the caller that it should discard this task.
Chris Masonefad911a2012-03-29 12:30:26 -0700405
406 @param scheduler: an instance of DedupingScheduler, as defined in
407 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700408 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700409 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700410 {'R18': ['x86-alex-release/R18-1655.0.0'],
411 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700412 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700413 @param force: Always schedule the suite.
414 @return False
415 """
Chris Masone96f16632012-04-04 18:36:03 -0700416 super(OneShotTask, self).Run(scheduler, branch_builds, board, force)
Chris Masonefad911a2012-03-29 12:30:26 -0700417 return False