blob: 9de59b740f38f4f153fbd04b4624cf9a462e6ee4 [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
Alex Millerf2b57442013-09-07 18:40:02 -070055 boards: board1, board2 # comma seperated string, Optional
Chris Masone96f16632012-04-04 18:36:03 -070056
Chris Masone67f06d62012-04-12 15:16:56 -070057 By default, Tasks run on all release branches, not factory or firmware.
58
Chris Masone96f16632012-04-04 18:36:03 -070059 @param config: a ForgivingConfigParser.
60 @param section: the section to parse into a Task.
61 @return keyword, Task object pair. One or both will be None on error.
62 @raise MalformedConfigEntry if there's a problem parsing |section|.
63 """
Alex Miller06695022012-07-18 09:31:36 -070064 if not config.has_section(section):
65 raise MalformedConfigEntry('unknown section %s' % section)
66
Alex Millerf2b57442013-09-07 18:40:02 -070067 allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num',
68 'boards'])
Alex Millerbb535e22012-07-11 20:11:33 -070069 # The parameter of union() is the keys under the section in the config
70 # The union merges this with the allowed set, so if any optional keys
71 # are omitted, then they're filled in. If any extra keys are present,
72 # then they will expand unioned set, causing it to fail the following
73 # comparison against the allowed set.
74 section_headers = allowed.union(dict(config.items(section)).keys())
75 if allowed != section_headers:
76 raise MalformedConfigEntry('unknown entries: %s' %
77 ", ".join(map(str, section_headers.difference(allowed))))
78
Chris Masone96f16632012-04-04 18:36:03 -070079 keyword = config.getstring(section, 'run_on')
80 suite = config.getstring(section, 'suite')
81 branches = config.getstring(section, 'branch_specs')
82 pool = config.getstring(section, 'pool')
Alex Millerf2b57442013-09-07 18:40:02 -070083 boards = config.getstring(section, 'boards')
Chris Masone3eeaf0a2012-08-09 14:07:27 -070084 try:
85 num = config.getint(section, 'num')
86 except ValueError as e:
87 raise MalformedConfigEntry("Ill-specified 'num': %r" %e)
Chris Masone96f16632012-04-04 18:36:03 -070088 if not keyword:
89 raise MalformedConfigEntry('No event to |run_on|.')
90 if not suite:
91 raise MalformedConfigEntry('No |suite|')
92 specs = []
93 if branches:
94 specs = re.split('\s*,\s*', branches)
95 Task.CheckBranchSpecs(specs)
Alex Millerf2b57442013-09-07 18:40:02 -070096 return keyword, Task(section, suite, specs, pool, num, boards)
Chris Masone96f16632012-04-04 18:36:03 -070097
98
99 @staticmethod
100 def CheckBranchSpecs(branch_specs):
101 """Make sure entries in the list branch_specs are correctly formed.
102
Chris Masone67f06d62012-04-12 15:16:56 -0700103 We accept any of BARE_BRANCHES in |branch_specs|, as
Dan Shiaceb91d2013-02-20 12:41:28 -0800104 well as _one_ string of the form '>=RXX' or '==RXX', where 'RXX' is a
Chris Masone96f16632012-04-04 18:36:03 -0700105 CrOS milestone number.
106
107 @param branch_specs: an iterable of branch specifiers.
108 @raise MalformedConfigEntry if there's a problem parsing |branch_specs|.
109 """
110 have_seen_numeric_constraint = False
111 for branch in branch_specs:
Chris Masone67f06d62012-04-12 15:16:56 -0700112 if branch in BARE_BRANCHES:
Chris Masone96f16632012-04-04 18:36:03 -0700113 continue
Dan Shiaceb91d2013-02-20 12:41:28 -0800114 if ((branch.startswith('>=R') or branch.startswith('==R')) and
115 not have_seen_numeric_constraint):
Chris Masone96f16632012-04-04 18:36:03 -0700116 have_seen_numeric_constraint = True
117 continue
Chris Masone97cf0a72012-05-16 09:55:52 -0700118 raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
Chris Masone96f16632012-04-04 18:36:03 -0700119
120
Alex Millerf2b57442013-09-07 18:40:02 -0700121 def __init__(self, name, suite, branch_specs, pool=None, num=None,
122 boards=None):
Chris Masonefad911a2012-03-29 12:30:26 -0700123 """Constructor
124
Chris Masone96f16632012-04-04 18:36:03 -0700125 Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs,
126 we'll store them such that _FitsSpec() can be used to check whether a
127 given branch 'fits' with the specifications passed in here.
128 For example, given branch_specs = ['factory', '>=R18'], we'd set things
129 up so that _FitsSpec() would return True for 'factory', or 'RXX'
Dan Shiaceb91d2013-02-20 12:41:28 -0800130 where XX is a number >= 18. Same check is done for branch_specs = [
131 'factory', '==R18'], which limit the test to only one specific branch.
Chris Masone96f16632012-04-04 18:36:03 -0700132
133 Given branch_specs = ['factory', 'firmware'], _FitsSpec()
134 would pass only those two specific strings.
135
136 Example usage:
Chris Masonecc4631d2012-04-20 12:06:39 -0700137 t = Task('Name', 'suite', ['factory', '>=R18'])
Chris Masone96f16632012-04-04 18:36:03 -0700138 t._FitsSpec('factory') # True
139 t._FitsSpec('R19') # True
140 t._FitsSpec('R17') # False
141 t._FitsSpec('firmware') # False
142 t._FitsSpec('goober') # False
143
Dan Shiaceb91d2013-02-20 12:41:28 -0800144 t = Task('Name', 'suite', ['factory', '==R18'])
145 t._FitsSpec('R19') # False, branch does not equal to 18
146 t._FitsSpec('R18') # True
147 t._FitsSpec('R17') # False
148
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700149 @param name: name of this task, e.g. 'NightlyPower'
Chris Masonefad911a2012-03-29 12:30:26 -0700150 @param suite: the name of the suite to run, e.g. 'bvt'
Chris Masone96f16632012-04-04 18:36:03 -0700151 @param branch_specs: a pre-vetted iterable of branch specifiers,
152 e.g. ['>=R18', 'factory']
Chris Masonefad911a2012-03-29 12:30:26 -0700153 @param pool: the pool of machines to use for scheduling purposes.
154 Default: None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700155 @param num: the number of devices across which to shard the test suite.
Aviv Keshetd83ef442013-01-16 16:19:35 -0800156 Type: integer or None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700157 Default: None
Alex Millerf2b57442013-09-07 18:40:02 -0700158 @param boards: A comma seperated list of boards to run this task on.
159 Default: Run on all boards.
Chris Masonefad911a2012-03-29 12:30:26 -0700160 """
Chris Masonecc4631d2012-04-20 12:06:39 -0700161 self._name = name
Chris Masonefad911a2012-03-29 12:30:26 -0700162 self._suite = suite
Chris Masone96f16632012-04-04 18:36:03 -0700163 self._branch_specs = branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700164 self._pool = pool
Aviv Keshetd83ef442013-01-16 16:19:35 -0800165 self._num = num
Chris Masone96f16632012-04-04 18:36:03 -0700166
167 self._bare_branches = []
Dan Shiaceb91d2013-02-20 12:41:28 -0800168 self._version_equal_constraint = False
Chris Masone67f06d62012-04-12 15:16:56 -0700169 if not branch_specs:
170 # Any milestone is OK.
171 self._numeric_constraint = version.LooseVersion('0')
172 else:
173 self._numeric_constraint = None
174 for spec in branch_specs:
175 if spec.startswith('>='):
176 self._numeric_constraint = version.LooseVersion(
177 spec.lstrip('>=R'))
Dan Shiaceb91d2013-02-20 12:41:28 -0800178 elif spec.startswith('=='):
179 self._version_equal_constraint = True
180 self._numeric_constraint = version.LooseVersion(
181 spec.lstrip('==R'))
Chris Masone67f06d62012-04-12 15:16:56 -0700182 else:
183 self._bare_branches.append(spec)
Alex Millerf2b57442013-09-07 18:40:02 -0700184
Chris Masonefad911a2012-03-29 12:30:26 -0700185 # Since we expect __hash__() and other comparitor methods to be used
186 # frequently by set operations, and they use str() a lot, pre-compute
187 # the string representation of this object.
Aviv Keshet8ce3c982013-01-24 09:23:13 -0800188 if num is None:
189 numStr = '[Default num]'
190 else:
191 numStr = '%d' % num
Alex Millerf2b57442013-09-07 18:40:02 -0700192
193 if boards is None:
194 self._boards = set()
195 boardsStr = '[All boards]'
196 else:
197 self._boards = set([x.strip() for x in boards.split(',')])
198 boardsStr = boards
199
200 self._str = ('%s: %s on %s with pool %s, boards [%s], '
201 'across %s machines' % (self.__class__.__name__,
202 suite, branch_specs, pool, boardsStr, numStr))
Chris Masone96f16632012-04-04 18:36:03 -0700203
204
205 def _FitsSpec(self, branch):
206 """Checks if a branch is deemed OK by this instance's branch specs.
207
208 When called on a branch name, will return whether that branch
Dan Shiaceb91d2013-02-20 12:41:28 -0800209 'fits' the specifications stored in self._bare_branches,
210 self._numeric_constraint and self._version_equal_constraint.
Chris Masone96f16632012-04-04 18:36:03 -0700211
212 @param branch: the branch to check.
213 @return True if b 'fits' with stored specs, False otherwise.
214 """
Chris Masone657e1552012-05-30 17:06:20 -0700215 if branch in BARE_BRANCHES:
216 return branch in self._bare_branches
Dan Shiaceb91d2013-02-20 12:41:28 -0800217 if self._numeric_constraint:
218 if self._version_equal_constraint:
219 return version.LooseVersion(branch) == self._numeric_constraint
220 else:
221 return version.LooseVersion(branch) >= self._numeric_constraint
222 else:
223 return False
Chris Masonefad911a2012-03-29 12:30:26 -0700224
225
226 @property
Alex Miller9979b5a2012-11-01 17:36:12 -0700227 def name(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800228 """Name of this task, e.g. 'NightlyPower'."""
Alex Miller9979b5a2012-11-01 17:36:12 -0700229 return self._name
230
231
232 @property
Chris Masonefad911a2012-03-29 12:30:26 -0700233 def suite(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800234 """Name of the suite to run, e.g. 'bvt'."""
Chris Masonefad911a2012-03-29 12:30:26 -0700235 return self._suite
236
237
238 @property
Chris Masone96f16632012-04-04 18:36:03 -0700239 def branch_specs(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800240 """a pre-vetted iterable of branch specifiers,
241 e.g. ['>=R18', 'factory']."""
Chris Masone96f16632012-04-04 18:36:03 -0700242 return self._branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700243
244
245 @property
Chris Masone3fba86f2012-04-03 10:06:56 -0700246 def pool(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800247 """The pool of machines to use for scheduling purposes."""
Chris Masone3fba86f2012-04-03 10:06:56 -0700248 return self._pool
Chris Masonefad911a2012-03-29 12:30:26 -0700249
250
Alex Miller9979b5a2012-11-01 17:36:12 -0700251 @property
252 def num(self):
Dan Shiaceb91d2013-02-20 12:41:28 -0800253 """The number of devices across which to shard the test suite.
254 Type: integer or None"""
Alex Miller9979b5a2012-11-01 17:36:12 -0700255 return self._num
256
257
Alex Millerf2b57442013-09-07 18:40:02 -0700258 @property
259 def boards(self):
260 """The boards on which to run this suite.
261 Type: Iterable of strings"""
262 return self._boards
263
264
Chris Masonefad911a2012-03-29 12:30:26 -0700265 def __str__(self):
266 return self._str
267
268
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700269 def __repr__(self):
270 return self._str
271
272
Chris Masonefad911a2012-03-29 12:30:26 -0700273 def __lt__(self, other):
274 return str(self) < str(other)
275
276
277 def __le__(self, other):
278 return str(self) <= str(other)
279
280
281 def __eq__(self, other):
282 return str(self) == str(other)
283
284
285 def __ne__(self, other):
286 return str(self) != str(other)
287
288
289 def __gt__(self, other):
290 return str(self) > str(other)
291
292
293 def __ge__(self, other):
294 return str(self) >= str(other)
295
296
297 def __hash__(self):
298 """Allows instances to be correctly deduped when used in a set."""
299 return hash(str(self))
300
301
Alex Miller7ce1b362012-07-10 09:24:10 -0700302 def AvailableHosts(self, scheduler, board):
303 """Query what hosts are able to run a test on a board and pool
304 combination.
Alex Miller511a9e32012-07-03 09:16:47 -0700305
306 @param scheduler: an instance of DedupingScheduler, as defined in
307 deduping_scheduler.py
308 @param board: the board against which one wants to run the test.
Alex Miller7ce1b362012-07-10 09:24:10 -0700309 @return The list of hosts meeting the board and pool requirements,
310 or None if no hosts were found."""
Alex Millerf2b57442013-09-07 18:40:02 -0700311 if self._boards and board not in self._boards:
312 return []
313
Alex Miller511a9e32012-07-03 09:16:47 -0700314 labels = [Labels.BOARD_PREFIX + board]
315 if self._pool:
Chris Masonecd214e02012-07-10 16:22:10 -0700316 labels.append(Labels.POOL_PREFIX + self._pool)
Alex Miller511a9e32012-07-03 09:16:47 -0700317
Alex Miller7ce1b362012-07-10 09:24:10 -0700318 return scheduler.GetHosts(multiple_labels=labels)
Alex Miller511a9e32012-07-03 09:16:47 -0700319
320
Alex Millerd621cf22012-07-11 13:57:10 -0700321 def ShouldHaveAvailableHosts(self):
322 """As a sanity check, return true if we know for certain that
323 we should be able to schedule this test. If we claim this test
324 should be able to run, and it ends up not being scheduled, then
325 a warning will be reported.
326
327 @return True if this test should be able to run, False otherwise.
328 """
329 return self._pool == 'bvt'
330
331
Chris Masone96f16632012-04-04 18:36:03 -0700332 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700333 """Run this task. Returns False if it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700334
Chris Masone013859b2012-04-01 13:45:26 -0700335 Execute this task. Attempt to schedule the associated suite.
336 Return True if this task should be kept around, False if it
337 should be destroyed. This allows for one-shot Tasks.
Chris Masonefad911a2012-03-29 12:30:26 -0700338
339 @param scheduler: an instance of DedupingScheduler, as defined in
340 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700341 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700342 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700343 {'R18': ['x86-alex-release/R18-1655.0.0'],
344 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700345 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700346 @param force: Always schedule the suite.
Chris Masone013859b2012-04-01 13:45:26 -0700347 @return True if the task should be kept, False if not
Chris Masonefad911a2012-03-29 12:30:26 -0700348 """
Chris Masonea3a38172012-05-14 15:19:56 -0700349 logging.info('Running %s on %s', self._name, board)
Chris Masone96f16632012-04-04 18:36:03 -0700350 builds = []
Chris Masonefe5a5092012-04-11 18:29:07 -0700351 for branch, build in branch_builds.iteritems():
Chris Masonea3a38172012-05-14 15:19:56 -0700352 logging.info('Checking if %s fits spec %r',
353 branch, self.branch_specs)
Chris Masone96f16632012-04-04 18:36:03 -0700354 if self._FitsSpec(branch):
Chris Masone05b19442012-04-17 13:37:55 -0700355 builds.extend(build)
Chris Masone96f16632012-04-04 18:36:03 -0700356 for build in builds:
Chris Masone3fba86f2012-04-03 10:06:56 -0700357 try:
Chris Masone96f16632012-04-04 18:36:03 -0700358 if not scheduler.ScheduleSuite(self._suite, board, build,
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700359 self._pool, self._num, force):
Chris Masone96f16632012-04-04 18:36:03 -0700360 logging.info('Skipping scheduling %s on %s for %s',
361 self._suite, build, board)
Chris Masone3fba86f2012-04-03 10:06:56 -0700362 except deduping_scheduler.DedupingSchedulerException as e:
363 logging.error(e)
Chris Masonefad911a2012-03-29 12:30:26 -0700364 return True
365
366
Chris Masone013859b2012-04-01 13:45:26 -0700367class OneShotTask(Task):
368 """A Task that can be run only once. Can schedule itself."""
Chris Masonefad911a2012-03-29 12:30:26 -0700369
370
Chris Masone96f16632012-04-04 18:36:03 -0700371 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700372 """Run this task. Returns False, indicating it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700373
Chris Masone013859b2012-04-01 13:45:26 -0700374 Run this task. Attempt to schedule the associated suite.
375 Return False, indicating to the caller that it should discard this task.
Chris Masonefad911a2012-03-29 12:30:26 -0700376
377 @param scheduler: an instance of DedupingScheduler, as defined in
378 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700379 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700380 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700381 {'R18': ['x86-alex-release/R18-1655.0.0'],
382 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700383 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700384 @param force: Always schedule the suite.
385 @return False
386 """
Chris Masone96f16632012-04-04 18:36:03 -0700387 super(OneShotTask, self).Run(scheduler, branch_builds, board, force)
Chris Masonefad911a2012-03-29 12:30:26 -0700388 return False