blob: 17e727ad300f95b4a4ffdc19c4e99f54ed9115fe [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
Chris Masone67f06d62012-04-12 15:16:56 -070017BARE_BRANCHES = ['factory', 'firmware']
18
19
20def PickBranchName(type, milestone):
21 if type in BARE_BRANCHES:
22 return type
23 return milestone
24
25
Chris Masone013859b2012-04-01 13:45:26 -070026class Task(object):
Chris Masonefad911a2012-03-29 12:30:26 -070027 """Represents an entry from the scheduler config. Can schedule itself.
28
29 Each entry from the scheduler config file maps one-to-one to a
Chris Masone013859b2012-04-01 13:45:26 -070030 Task. Each instance has enough info to schedule itself
Chris Masonefad911a2012-03-29 12:30:26 -070031 on-demand with the AFE.
32
33 This class also overrides __hash__() and all comparitor methods to enable
34 correct use in dicts, sets, etc.
35 """
36
Chris Masone96f16632012-04-04 18:36:03 -070037
38 @staticmethod
39 def CreateFromConfigSection(config, section):
40 """Create a Task from a section of a config file.
41
42 The section to parse should look like this:
43 [TaskName]
44 suite: suite_to_run # Required
45 run_on: event_on which to run # Required
46 branch_specs: factory,firmware,>=R12 # Optional
47 pool: pool_of_devices # Optional
Chris Masone3eeaf0a2012-08-09 14:07:27 -070048 num: sharding_factor # int, Optional
Chris Masone96f16632012-04-04 18:36:03 -070049
Chris Masone67f06d62012-04-12 15:16:56 -070050 By default, Tasks run on all release branches, not factory or firmware.
51
Chris Masone96f16632012-04-04 18:36:03 -070052 @param config: a ForgivingConfigParser.
53 @param section: the section to parse into a Task.
54 @return keyword, Task object pair. One or both will be None on error.
55 @raise MalformedConfigEntry if there's a problem parsing |section|.
56 """
Alex Miller06695022012-07-18 09:31:36 -070057 if not config.has_section(section):
58 raise MalformedConfigEntry('unknown section %s' % section)
59
Chris Masone3eeaf0a2012-08-09 14:07:27 -070060 allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num'])
Alex Millerbb535e22012-07-11 20:11:33 -070061 # The parameter of union() is the keys under the section in the config
62 # The union merges this with the allowed set, so if any optional keys
63 # are omitted, then they're filled in. If any extra keys are present,
64 # then they will expand unioned set, causing it to fail the following
65 # comparison against the allowed set.
66 section_headers = allowed.union(dict(config.items(section)).keys())
67 if allowed != section_headers:
68 raise MalformedConfigEntry('unknown entries: %s' %
69 ", ".join(map(str, section_headers.difference(allowed))))
70
Chris Masone96f16632012-04-04 18:36:03 -070071 keyword = config.getstring(section, 'run_on')
72 suite = config.getstring(section, 'suite')
73 branches = config.getstring(section, 'branch_specs')
74 pool = config.getstring(section, 'pool')
Chris Masone3eeaf0a2012-08-09 14:07:27 -070075 try:
76 num = config.getint(section, 'num')
77 except ValueError as e:
78 raise MalformedConfigEntry("Ill-specified 'num': %r" %e)
Chris Masone96f16632012-04-04 18:36:03 -070079 if not keyword:
80 raise MalformedConfigEntry('No event to |run_on|.')
81 if not suite:
82 raise MalformedConfigEntry('No |suite|')
83 specs = []
84 if branches:
85 specs = re.split('\s*,\s*', branches)
86 Task.CheckBranchSpecs(specs)
Chris Masone3eeaf0a2012-08-09 14:07:27 -070087 return keyword, Task(section, suite, specs, pool, num)
Chris Masone96f16632012-04-04 18:36:03 -070088
89
90 @staticmethod
91 def CheckBranchSpecs(branch_specs):
92 """Make sure entries in the list branch_specs are correctly formed.
93
Chris Masone67f06d62012-04-12 15:16:56 -070094 We accept any of BARE_BRANCHES in |branch_specs|, as
Chris Masone96f16632012-04-04 18:36:03 -070095 well as _one_ string of the form '>=RXX', where 'RXX' is a
96 CrOS milestone number.
97
98 @param branch_specs: an iterable of branch specifiers.
99 @raise MalformedConfigEntry if there's a problem parsing |branch_specs|.
100 """
101 have_seen_numeric_constraint = False
102 for branch in branch_specs:
Chris Masone67f06d62012-04-12 15:16:56 -0700103 if branch in BARE_BRANCHES:
Chris Masone96f16632012-04-04 18:36:03 -0700104 continue
105 if branch.startswith('>=R') and not have_seen_numeric_constraint:
106 have_seen_numeric_constraint = True
107 continue
Chris Masone97cf0a72012-05-16 09:55:52 -0700108 raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
Chris Masone96f16632012-04-04 18:36:03 -0700109
110
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700111 def __init__(self, name, suite, branch_specs, pool=None, num=None):
Chris Masonefad911a2012-03-29 12:30:26 -0700112 """Constructor
113
Chris Masone96f16632012-04-04 18:36:03 -0700114 Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs,
115 we'll store them such that _FitsSpec() can be used to check whether a
116 given branch 'fits' with the specifications passed in here.
117 For example, given branch_specs = ['factory', '>=R18'], we'd set things
118 up so that _FitsSpec() would return True for 'factory', or 'RXX'
119 where XX is a number >= 18.
120
121 Given branch_specs = ['factory', 'firmware'], _FitsSpec()
122 would pass only those two specific strings.
123
124 Example usage:
Chris Masonecc4631d2012-04-20 12:06:39 -0700125 t = Task('Name', 'suite', ['factory', '>=R18'])
Chris Masone96f16632012-04-04 18:36:03 -0700126 t._FitsSpec('factory') # True
127 t._FitsSpec('R19') # True
128 t._FitsSpec('R17') # False
129 t._FitsSpec('firmware') # False
130 t._FitsSpec('goober') # False
131
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700132 @param name: name of this task, e.g. 'NightlyPower'
Chris Masonefad911a2012-03-29 12:30:26 -0700133 @param suite: the name of the suite to run, e.g. 'bvt'
Chris Masone96f16632012-04-04 18:36:03 -0700134 @param branch_specs: a pre-vetted iterable of branch specifiers,
135 e.g. ['>=R18', 'factory']
Chris Masonefad911a2012-03-29 12:30:26 -0700136 @param pool: the pool of machines to use for scheduling purposes.
137 Default: None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700138 @param num: the number of devices across which to shard the test suite.
Aviv Keshetd83ef442013-01-16 16:19:35 -0800139 Type: integer or None
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700140 Default: None
Chris Masonefad911a2012-03-29 12:30:26 -0700141 """
Chris Masonecc4631d2012-04-20 12:06:39 -0700142 self._name = name
Chris Masonefad911a2012-03-29 12:30:26 -0700143 self._suite = suite
Chris Masone96f16632012-04-04 18:36:03 -0700144 self._branch_specs = branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700145 self._pool = pool
Aviv Keshetd83ef442013-01-16 16:19:35 -0800146 self._num = num
Chris Masone96f16632012-04-04 18:36:03 -0700147
148 self._bare_branches = []
Chris Masone67f06d62012-04-12 15:16:56 -0700149 if not branch_specs:
150 # Any milestone is OK.
151 self._numeric_constraint = version.LooseVersion('0')
152 else:
153 self._numeric_constraint = None
154 for spec in branch_specs:
155 if spec.startswith('>='):
156 self._numeric_constraint = version.LooseVersion(
157 spec.lstrip('>=R'))
158 else:
159 self._bare_branches.append(spec)
Chris Masonefad911a2012-03-29 12:30:26 -0700160 # Since we expect __hash__() and other comparitor methods to be used
161 # frequently by set operations, and they use str() a lot, pre-compute
162 # the string representation of this object.
Aviv Keshet8ce3c982013-01-24 09:23:13 -0800163 if num is None:
164 numStr = '[Default num]'
165 else:
166 numStr = '%d' % num
167 self._str = '%s: %s on %s with pool %s, across %s machines' % (
168 self.__class__.__name__, suite, branch_specs, pool, numStr)
Chris Masone96f16632012-04-04 18:36:03 -0700169
170
171 def _FitsSpec(self, branch):
172 """Checks if a branch is deemed OK by this instance's branch specs.
173
174 When called on a branch name, will return whether that branch
175 'fits' the specifications stored in self._bare_branches and
176 self._numeric_constraint.
177
178 @param branch: the branch to check.
179 @return True if b 'fits' with stored specs, False otherwise.
180 """
Chris Masone657e1552012-05-30 17:06:20 -0700181 if branch in BARE_BRANCHES:
182 return branch in self._bare_branches
183 return (self._numeric_constraint and
184 version.LooseVersion(branch) >= self._numeric_constraint)
Chris Masonefad911a2012-03-29 12:30:26 -0700185
186
187 @property
Alex Miller9979b5a2012-11-01 17:36:12 -0700188 def name(self):
189 return self._name
190
191
192 @property
Chris Masonefad911a2012-03-29 12:30:26 -0700193 def suite(self):
194 return self._suite
195
196
197 @property
Chris Masone96f16632012-04-04 18:36:03 -0700198 def branch_specs(self):
199 return self._branch_specs
Chris Masonefad911a2012-03-29 12:30:26 -0700200
201
202 @property
Chris Masone3fba86f2012-04-03 10:06:56 -0700203 def pool(self):
204 return self._pool
Chris Masonefad911a2012-03-29 12:30:26 -0700205
206
Alex Miller9979b5a2012-11-01 17:36:12 -0700207 @property
208 def num(self):
209 return self._num
210
211
Chris Masonefad911a2012-03-29 12:30:26 -0700212 def __str__(self):
213 return self._str
214
215
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700216 def __repr__(self):
217 return self._str
218
219
Chris Masonefad911a2012-03-29 12:30:26 -0700220 def __lt__(self, other):
221 return str(self) < str(other)
222
223
224 def __le__(self, other):
225 return str(self) <= str(other)
226
227
228 def __eq__(self, other):
229 return str(self) == str(other)
230
231
232 def __ne__(self, other):
233 return str(self) != str(other)
234
235
236 def __gt__(self, other):
237 return str(self) > str(other)
238
239
240 def __ge__(self, other):
241 return str(self) >= str(other)
242
243
244 def __hash__(self):
245 """Allows instances to be correctly deduped when used in a set."""
246 return hash(str(self))
247
248
Alex Miller7ce1b362012-07-10 09:24:10 -0700249 def AvailableHosts(self, scheduler, board):
250 """Query what hosts are able to run a test on a board and pool
251 combination.
Alex Miller511a9e32012-07-03 09:16:47 -0700252
253 @param scheduler: an instance of DedupingScheduler, as defined in
254 deduping_scheduler.py
255 @param board: the board against which one wants to run the test.
Alex Miller7ce1b362012-07-10 09:24:10 -0700256 @return The list of hosts meeting the board and pool requirements,
257 or None if no hosts were found."""
Alex Miller511a9e32012-07-03 09:16:47 -0700258 labels = [Labels.BOARD_PREFIX + board]
259 if self._pool:
Chris Masonecd214e02012-07-10 16:22:10 -0700260 labels.append(Labels.POOL_PREFIX + self._pool)
Alex Miller511a9e32012-07-03 09:16:47 -0700261
Alex Miller7ce1b362012-07-10 09:24:10 -0700262 return scheduler.GetHosts(multiple_labels=labels)
Alex Miller511a9e32012-07-03 09:16:47 -0700263
264
Alex Millerd621cf22012-07-11 13:57:10 -0700265 def ShouldHaveAvailableHosts(self):
266 """As a sanity check, return true if we know for certain that
267 we should be able to schedule this test. If we claim this test
268 should be able to run, and it ends up not being scheduled, then
269 a warning will be reported.
270
271 @return True if this test should be able to run, False otherwise.
272 """
273 return self._pool == 'bvt'
274
275
Chris Masone96f16632012-04-04 18:36:03 -0700276 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700277 """Run this task. Returns False if it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700278
Chris Masone013859b2012-04-01 13:45:26 -0700279 Execute this task. Attempt to schedule the associated suite.
280 Return True if this task should be kept around, False if it
281 should be destroyed. This allows for one-shot Tasks.
Chris Masonefad911a2012-03-29 12:30:26 -0700282
283 @param scheduler: an instance of DedupingScheduler, as defined in
284 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700285 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700286 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700287 {'R18': ['x86-alex-release/R18-1655.0.0'],
288 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700289 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700290 @param force: Always schedule the suite.
Chris Masone013859b2012-04-01 13:45:26 -0700291 @return True if the task should be kept, False if not
Chris Masonefad911a2012-03-29 12:30:26 -0700292 """
Chris Masonea3a38172012-05-14 15:19:56 -0700293 logging.info('Running %s on %s', self._name, board)
Chris Masone96f16632012-04-04 18:36:03 -0700294 builds = []
Chris Masonefe5a5092012-04-11 18:29:07 -0700295 for branch, build in branch_builds.iteritems():
Chris Masonea3a38172012-05-14 15:19:56 -0700296 logging.info('Checking if %s fits spec %r',
297 branch, self.branch_specs)
Chris Masone96f16632012-04-04 18:36:03 -0700298 if self._FitsSpec(branch):
Chris Masone05b19442012-04-17 13:37:55 -0700299 builds.extend(build)
Chris Masone96f16632012-04-04 18:36:03 -0700300 for build in builds:
Chris Masone3fba86f2012-04-03 10:06:56 -0700301 try:
Chris Masone96f16632012-04-04 18:36:03 -0700302 if not scheduler.ScheduleSuite(self._suite, board, build,
Chris Masone3eeaf0a2012-08-09 14:07:27 -0700303 self._pool, self._num, force):
Chris Masone96f16632012-04-04 18:36:03 -0700304 logging.info('Skipping scheduling %s on %s for %s',
305 self._suite, build, board)
Chris Masone3fba86f2012-04-03 10:06:56 -0700306 except deduping_scheduler.DedupingSchedulerException as e:
307 logging.error(e)
Chris Masonefad911a2012-03-29 12:30:26 -0700308 return True
309
310
Chris Masone013859b2012-04-01 13:45:26 -0700311class OneShotTask(Task):
312 """A Task that can be run only once. Can schedule itself."""
Chris Masonefad911a2012-03-29 12:30:26 -0700313
314
Chris Masone96f16632012-04-04 18:36:03 -0700315 def Run(self, scheduler, branch_builds, board, force=False):
Chris Masone013859b2012-04-01 13:45:26 -0700316 """Run this task. Returns False, indicating it should be destroyed.
Chris Masonefad911a2012-03-29 12:30:26 -0700317
Chris Masone013859b2012-04-01 13:45:26 -0700318 Run this task. Attempt to schedule the associated suite.
319 Return False, indicating to the caller that it should discard this task.
Chris Masonefad911a2012-03-29 12:30:26 -0700320
321 @param scheduler: an instance of DedupingScheduler, as defined in
322 deduping_scheduler.py
Chris Masone05b19442012-04-17 13:37:55 -0700323 @param branch_builds: a dict mapping branch name to the build(s) to
Chris Masone96f16632012-04-04 18:36:03 -0700324 install for that branch, e.g.
Chris Masone05b19442012-04-17 13:37:55 -0700325 {'R18': ['x86-alex-release/R18-1655.0.0'],
326 'R19': ['x86-alex-release/R19-2077.0.0']}
Chris Masone96f16632012-04-04 18:36:03 -0700327 @param board: the board against which to run self._suite.
Chris Masonefad911a2012-03-29 12:30:26 -0700328 @param force: Always schedule the suite.
329 @return False
330 """
Chris Masone96f16632012-04-04 18:36:03 -0700331 super(OneShotTask, self).Run(scheduler, branch_builds, board, force)
Chris Masonefad911a2012-03-29 12:30:26 -0700332 return False