blob: 6c20fb7bb55f41a71bc828461e0ec76adf901dbd [file] [log] [blame]
Chris Masone8ac66712012-02-15 14:21:02 -08001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Chris Masone6fed6462011-10-20 16:36:43 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import common
6import compiler, logging, os, random, re, time
Chris Masoneab3e7332012-02-29 18:54:58 -08007from autotest_lib.client.common_lib import base_job, control_data, global_config
8from autotest_lib.client.common_lib import error, utils
Chris Masone8b7cd422012-02-22 13:16:11 -08009from autotest_lib.client.common_lib.cros import dev_server
Chris Masone8ac66712012-02-15 14:21:02 -080010from autotest_lib.server.cros import control_file_getter, frontend_wrappers
Chris Masone6fed6462011-10-20 16:36:43 -070011from autotest_lib.server import frontend
12
13
Scott Zawalski65650172012-02-16 11:48:26 -050014VERSION_PREFIX = 'cros-version:'
Chris Masone2ef1d4e2011-12-20 11:06:53 -080015CONFIG = global_config.global_config
16
17
Chris Masoneab3e7332012-02-29 18:54:58 -080018class SuiteArgumentException(Exception):
19 """Raised when improper arguments are used to run a suite."""
20 pass
21
22
23def reimage_and_run(**dargs):
24 """
25 Backward-compatible API for dynamic_suite.
26
27 Will re-image a number of devices (of the specified board) with the
28 provided build, and then run the indicated test suite on them.
29 Guaranteed to be compatible with any build from stable to dev.
30
31 Currently required args:
32 @param build: the build to install e.g.
33 x86-alex-release/R18-1655.0.0-a1-b1584.
34 @param board: which kind of devices to reimage.
35 @param name: a value of the SUITE control file variable to search for.
36 @param job: an instance of client.common_lib.base_job representing the
37 currently running suite job.
38
39 Currently supported optional args:
40 @param pool: specify the pool of machines to use for scheduling purposes.
41 Default: None
42 @param num: how many devices to reimage.
43 Default in global_config
44 @param skip_reimage: skip reimaging, used for testing purposes.
45 Default: False
46 @param add_experimental: schedule experimental tests as well, or not.
47 Default: True
48 """
49 build, board, name, job, pool, num, skip_reimage, add_experimental = \
50 _vet_remage_and_run_args(**dargs)
51 reimager = Reimager(job.autodir, pool=pool)
52 if skip_reimage or reimager.attempt(build, board, job.record, num=num):
53 suite = Suite.create_from_name(name, build, pool=pool,
54 results_dir=job.resultdir)
55 suite.run_and_wait(job.record, add_experimental=add_experimental)
56
57
58def _vet_reimage_and_run_args(build=None, board=None, name=None, job=None,
59 pool=None, num=None, skip_reimage=False,
60 add_experimental=True, **dargs):
61 """
62 Vets arguments for reimage_and_run().
63
64 Currently required args:
65 @param build: the build to install e.g.
66 x86-alex-release/R18-1655.0.0-a1-b1584.
67 @param board: which kind of devices to reimage.
68 @param name: a value of the SUITE control file variable to search for.
69 @param job: an instance of client.common_lib.base_job representing the
70 currently running suite job.
71
72 Currently supported optional args:
73 @param pool: specify the pool of machines to use for scheduling purposes.
74 Default: None
75 @param num: how many devices to reimage.
76 Default in global_config
77 @param skip_reimage: skip reimaging, used for testing purposes.
78 Default: False
79 @param add_experimental: schedule experimental tests as well, or not.
80 Default: True
81 @return a tuple of args set to provided (or default) values.
82 """
83 required_keywords = {'build': str,
84 'board': str,
85 'name': str,
86 'job': base_job.base_job}
87 for key, expected in required_keywords.iteritems():
88 value = locals().get(key)
89 if not value or not isinstance(value, expected):
90 raise SuiteArgumentException("reimage_and_run() needs %s=<%r>" % (
91 key, expected))
92 return build, board, name, job, pool, num, skip_reimage, add_experimental
93
94
Chris Masone8b764252012-01-17 11:12:51 -080095def inject_vars(vars, control_file_in):
96 """
Chris Masoneab3e7332012-02-29 18:54:58 -080097 Inject the contents of |vars| into |control_file_in|.
Chris Masone8b764252012-01-17 11:12:51 -080098
99 @param vars: a dict to shoehorn into the provided control file string.
100 @param control_file_in: the contents of a control file to munge.
101 @return the modified control file string.
102 """
103 control_file = ''
104 for key, value in vars.iteritems():
105 control_file += "%s='%s'\n" % (key, value)
106 return control_file + control_file_in
107
108
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800109def _image_url_pattern():
110 return CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
111
112
113def _package_url_pattern():
114 return CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
115
Chris Masone6fed6462011-10-20 16:36:43 -0700116
Chris Masoneab3e7332012-02-29 18:54:58 -0800117def skip_reimage(g):
118 return g.get('SKIP_IMAGE')
119
120
Chris Masone6fed6462011-10-20 16:36:43 -0700121class Reimager(object):
122 """
123 A class that can run jobs to reimage devices.
124
125 @var _afe: a frontend.AFE instance used to talk to autotest.
126 @var _tko: a frontend.TKO instance used to query the autotest results db.
127 @var _cf_getter: a ControlFileGetter used to get the AU control file.
128 """
129
130
Scott Zawalski65650172012-02-16 11:48:26 -0500131 def __init__(self, autotest_dir, afe=None, tko=None, pool=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700132 """
133 Constructor
134
135 @param autotest_dir: the place to find autotests.
136 @param afe: an instance of AFE as defined in server/frontend.py.
137 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500138 @param pool: Specify the pool of machines to use for scheduling
139 purposes.
Chris Masone6fed6462011-10-20 16:36:43 -0700140 """
Chris Masone8ac66712012-02-15 14:21:02 -0800141 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
142 delay_sec=10,
143 debug=False)
144 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
145 delay_sec=10,
146 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500147 self._pool = pool
Chris Masone6fed6462011-10-20 16:36:43 -0700148 self._cf_getter = control_file_getter.FileSystemGetter(
149 [os.path.join(autotest_dir, 'server/site_tests')])
150
151
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800152 def skip(self, g):
Chris Masoneab3e7332012-02-29 18:54:58 -0800153 """Deprecated in favor of dynamic_suite.skip_reimage()."""
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800154 return 'SKIP_IMAGE' in g and g['SKIP_IMAGE']
155
156
Scott Zawalski65650172012-02-16 11:48:26 -0500157 def attempt(self, build, board, record, num=None, pool=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700158 """
159 Synchronously attempt to reimage some machines.
160
161 Fire off attempts to reimage |num| machines of type |board|, using an
Chris Masone8abb6fc2012-01-31 09:27:36 -0800162 image at |url| called |build|. Wait for completion, polling every
Chris Masone6fed6462011-10-20 16:36:43 -0700163 10s, and log results with |record| upon completion.
164
Chris Masone8abb6fc2012-01-31 09:27:36 -0800165 @param build: the build to install e.g.
166 x86-alex-release/R18-1655.0.0-a1-b1584.
Chris Masone6fed6462011-10-20 16:36:43 -0700167 @param board: which kind of devices to reimage.
168 @param record: callable that records job status.
Chris Masone796fcf12012-02-22 16:53:31 -0800169 prototype:
170 record(status, subdir, name, reason)
Chris Masone5552dd72012-02-15 15:01:04 -0800171 @param num: how many devices to reimage.
Scott Zawalski65650172012-02-16 11:48:26 -0500172 @param pool: Specify the pool of machines to use for scheduling
Chris Masone796fcf12012-02-22 16:53:31 -0800173 purposes.
Chris Masone6fed6462011-10-20 16:36:43 -0700174 @return True if all reimaging jobs succeed, false otherwise.
175 """
Chris Masone5552dd72012-02-15 15:01:04 -0800176 if not num:
177 num = CONFIG.get_config_value('CROS', 'sharding_factor', type=int)
Scott Zawalski65650172012-02-16 11:48:26 -0500178 if pool:
179 self._pool = pool
180 logging.debug("scheduling reimaging across %d machines", num)
Chris Masone73f65022012-01-31 14:00:43 -0800181 wrapper_job_name = 'try new image'
182 record('START', None, wrapper_job_name)
Chris Masone796fcf12012-02-22 16:53:31 -0800183 try:
184 self._ensure_version_label(VERSION_PREFIX + build)
185 canary = self._schedule_reimage_job(build, num, board)
186 logging.debug('Created re-imaging job: %d', canary.id)
187 while len(self._afe.get_jobs(id=canary.id, not_yet_run=True)) > 0:
188 time.sleep(10)
189 logging.debug('Re-imaging job running.')
190 while len(self._afe.get_jobs(id=canary.id, finished=True)) == 0:
191 time.sleep(10)
192 logging.debug('Re-imaging job finished.')
193 canary.result = self._afe.poll_job_results(self._tko, canary, 0)
194 except Exception as e:
195 # catch Exception so we record the job as terminated no matter what.
196 logging.error(e)
197 record('END ERROR', None, wrapper_job_name, str(e))
198 return False
Chris Masone6fed6462011-10-20 16:36:43 -0700199
200 if canary.result is True:
201 self._report_results(canary, record)
Chris Masone73f65022012-01-31 14:00:43 -0800202 record('END GOOD', None, wrapper_job_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700203 return True
204
205 if canary.result is None:
206 record('FAIL', None, canary.name, 're-imaging tasks did not run')
207 else: # canary.result is False
208 self._report_results(canary, record)
209
Chris Masone73f65022012-01-31 14:00:43 -0800210 record('END FAIL', None, wrapper_job_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700211 return False
212
213
214 def _ensure_version_label(self, name):
215 """
216 Ensure that a label called |name| exists in the autotest DB.
217
218 @param name: the label to check for/create.
219 """
220 labels = self._afe.get_labels(name=name)
221 if len(labels) == 0:
222 self._afe.create_label(name=name)
223
224
Chris Masone8abb6fc2012-01-31 09:27:36 -0800225 def _schedule_reimage_job(self, build, num_machines, board):
Chris Masone6fed6462011-10-20 16:36:43 -0700226 """
227 Schedules the reimaging of |num_machines| |board| devices with |image|.
228
229 Sends an RPC to the autotest frontend to enqueue reimaging jobs on
230 |num_machines| devices of type |board|
231
Chris Masone8abb6fc2012-01-31 09:27:36 -0800232 @param build: the build to install (must be unique).
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800233 @param num_machines: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700234 @param board: which kind of devices to reimage.
235 @return a frontend.Job object for the reimaging job we scheduled.
236 """
Chris Masone8b764252012-01-17 11:12:51 -0800237 control_file = inject_vars(
Chris Masone8abb6fc2012-01-31 09:27:36 -0800238 {'image_url': _image_url_pattern() % build, 'image_name': build},
Chris Masone6fed6462011-10-20 16:36:43 -0700239 self._cf_getter.get_control_file_contents_by_name('autoupdate'))
Scott Zawalski65650172012-02-16 11:48:26 -0500240 job_deps = []
241 if self._pool:
242 meta_host = 'pool:%s' % self._pool
243 board_label = 'board:%s' % board
244 job_deps.append(board_label)
245 else:
246 # No pool specified use board.
247 meta_host = 'board:%s' % board
Chris Masone6fed6462011-10-20 16:36:43 -0700248
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800249 return self._afe.create_job(control_file=control_file,
Chris Masone8abb6fc2012-01-31 09:27:36 -0800250 name=build + '-try',
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800251 control_type='Server',
Scott Zawalski65650172012-02-16 11:48:26 -0500252 meta_hosts=[meta_host] * num_machines,
253 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700254
255
256 def _report_results(self, job, record):
257 """
258 Record results from a completed frontend.Job object.
259
260 @param job: a completed frontend.Job object populated by
261 frontend.AFE.poll_job_results.
262 @param record: callable that records job status.
263 prototype:
264 record(status, subdir, name, reason)
265 """
266 if job.result == True:
267 record('GOOD', None, job.name)
268 return
269
270 for platform in job.results_platform_map:
271 for status in job.results_platform_map[platform]:
272 if status == 'Total':
273 continue
274 for host in job.results_platform_map[platform][status]:
275 if host not in job.test_status:
276 record('ERROR', None, host, 'Job failed to run.')
277 elif status == 'Failed':
278 for test_status in job.test_status[host].fail:
279 record('FAIL', None, host, test_status.reason)
280 elif status == 'Aborted':
281 for test_status in job.test_status[host].fail:
282 record('ABORT', None, host, test_status.reason)
283 elif status == 'Completed':
284 record('GOOD', None, host)
285
286
287class Suite(object):
288 """
289 A suite of tests, defined by some predicate over control file variables.
290
291 Given a place to search for control files a predicate to match the desired
292 tests, can gather tests and fire off jobs to run them, and then wait for
293 results.
294
295 @var _predicate: a function that should return True when run over a
296 ControlData representation of a control file that should be in
297 this Suite.
298 @var _tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800299 @var _build: the build on which we're running this suite.
Chris Masone6fed6462011-10-20 16:36:43 -0700300 @var _afe: an instance of AFE as defined in server/frontend.py.
301 @var _tko: an instance of TKO as defined in server/frontend.py.
302 @var _jobs: currently scheduled jobs, if any.
303 @var _cf_getter: a control_file_getter.ControlFileGetter
304 """
305
306
Chris Masonefef21382012-01-17 11:16:32 -0800307 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800308 def create_ds_getter(build):
Chris Masonefef21382012-01-17 11:16:32 -0800309 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800310 @param build: the build on which we're running this suite.
Chris Masonefef21382012-01-17 11:16:32 -0800311 @return a FileSystemGetter instance that looks under |autotest_dir|.
312 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800313 return control_file_getter.DevServerGetter(
314 build, dev_server.DevServer.create())
Chris Masonefef21382012-01-17 11:16:32 -0800315
316
317 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800318 def create_fs_getter(autotest_dir):
319 """
320 @param autotest_dir: the place to find autotests.
321 @return a FileSystemGetter instance that looks under |autotest_dir|.
322 """
323 # currently hard-coded places to look for tests.
324 subpaths = ['server/site_tests', 'client/site_tests',
325 'server/tests', 'client/tests']
326 directories = [os.path.join(autotest_dir, p) for p in subpaths]
327 return control_file_getter.FileSystemGetter(directories)
328
329
330 @staticmethod
Chris Masone84564792012-02-23 10:52:42 -0800331 def name_in_tag_predicate(name):
332 """Returns predicate that takes a control file and looks for |name|.
333
334 Builds a predicate that takes in a parsed control file (a ControlData)
335 and returns True if the SUITE tag is present and contains |name|.
336
337 @param name: the suite name to base the predicate on.
338 @return a callable that takes a ControlData and looks for |name| in that
339 ControlData object's suite member.
340 """
341 def parse(suite):
342 """Splits a string on ',' optionally surrounded by whitespace."""
343 return map(lambda x: x.strip(), suite.split(','))
344
345 return lambda t: hasattr(t, 'suite') and name in parse(t.suite)
346
347
348 @staticmethod
Scott Zawalski9ece6532012-02-28 14:10:47 -0500349 def create_from_name(name, build, cf_getter=None, afe=None, tko=None,
350 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700351 """
352 Create a Suite using a predicate based on the SUITE control file var.
353
354 Makes a predicate based on |name| and uses it to instantiate a Suite
355 that looks for tests in |autotest_dir| and will schedule them using
Chris Masoned6f38c82012-02-22 14:53:42 -0800356 |afe|. Pulls control files from the default dev server.
357 Results will be pulled from |tko| upon completion.
Chris Masone6fed6462011-10-20 16:36:43 -0700358
359 @param name: a value of the SUITE control file variable to search for.
Chris Masone8b7cd422012-02-22 13:16:11 -0800360 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800361 @param cf_getter: a control_file_getter.ControlFileGetter.
362 If None, default to using a DevServerGetter.
Chris Masone6fed6462011-10-20 16:36:43 -0700363 @param afe: an instance of AFE as defined in server/frontend.py.
364 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500365 @param pool: Specify the pool of machines to use for scheduling
Chris Masoned6f38c82012-02-22 14:53:42 -0800366 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500367 @param results_dir: The directory where the job can write results to.
368 This must be set if you want job_id of sub-jobs
369 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700370 @return a Suite instance.
371 """
Chris Masoned6f38c82012-02-22 14:53:42 -0800372 if cf_getter is None:
373 cf_getter = Suite.create_ds_getter(build)
Chris Masone84564792012-02-23 10:52:42 -0800374 return Suite(Suite.name_in_tag_predicate(name),
Scott Zawalski9ece6532012-02-28 14:10:47 -0500375 name, build, cf_getter, afe, tko, pool, results_dir)
Chris Masone6fed6462011-10-20 16:36:43 -0700376
377
Chris Masoned6f38c82012-02-22 14:53:42 -0800378 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Scott Zawalski9ece6532012-02-28 14:10:47 -0500379 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700380 """
381 Constructor
382
383 @param predicate: a function that should return True when run over a
384 ControlData representation of a control file that should be in
385 this Suite.
386 @param tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800387 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800388 @param cf_getter: a control_file_getter.ControlFileGetter
Chris Masone6fed6462011-10-20 16:36:43 -0700389 @param afe: an instance of AFE as defined in server/frontend.py.
390 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500391 @param pool: Specify the pool of machines to use for scheduling
392 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500393 @param results_dir: The directory where the job can write results to.
394 This must be set if you want job_id of sub-jobs
395 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700396 """
397 self._predicate = predicate
398 self._tag = tag
Chris Masone8b7cd422012-02-22 13:16:11 -0800399 self._build = build
Chris Masoned6f38c82012-02-22 14:53:42 -0800400 self._cf_getter = cf_getter
Scott Zawalski9ece6532012-02-28 14:10:47 -0500401 self._results_dir = results_dir
Chris Masone8ac66712012-02-15 14:21:02 -0800402 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
403 delay_sec=10,
404 debug=False)
405 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
406 delay_sec=10,
407 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500408 self._pool = pool
Chris Masone6fed6462011-10-20 16:36:43 -0700409 self._jobs = []
Chris Masone6fed6462011-10-20 16:36:43 -0700410 self._tests = Suite.find_and_parse_tests(self._cf_getter,
411 self._predicate,
412 add_experimental=True)
413
414
415 @property
416 def tests(self):
417 """
418 A list of ControlData objects in the suite, with added |text| attr.
419 """
420 return self._tests
421
422
423 def stable_tests(self):
424 """
425 |self.tests|, filtered for non-experimental tests.
426 """
427 return filter(lambda t: not t.experimental, self.tests)
428
429
430 def unstable_tests(self):
431 """
432 |self.tests|, filtered for experimental tests.
433 """
434 return filter(lambda t: t.experimental, self.tests)
435
436
Chris Masone8b7cd422012-02-22 13:16:11 -0800437 def _create_job(self, test):
Chris Masone6fed6462011-10-20 16:36:43 -0700438 """
439 Thin wrapper around frontend.AFE.create_job().
440
441 @param test: ControlData object for a test to run.
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500442 @return a frontend.Job object with an added test_name member.
443 test_name is used to preserve the higher level TEST_NAME
444 name of the job.
Chris Masone6fed6462011-10-20 16:36:43 -0700445 """
Scott Zawalski65650172012-02-16 11:48:26 -0500446 job_deps = []
447 if self._pool:
448 meta_hosts = 'pool:%s' % self._pool
Chris Masone8b7cd422012-02-22 13:16:11 -0800449 cros_label = VERSION_PREFIX + self._build
Scott Zawalski65650172012-02-16 11:48:26 -0500450 job_deps.append(cros_label)
451 else:
452 # No pool specified use any machines with the following label.
Chris Masone8b7cd422012-02-22 13:16:11 -0800453 meta_hosts = VERSION_PREFIX + self._build
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500454 test_obj = self._afe.create_job(
Chris Masone6fed6462011-10-20 16:36:43 -0700455 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800456 name='/'.join([self._build, self._tag, test.name]),
Chris Masone6fed6462011-10-20 16:36:43 -0700457 control_type=test.test_type.capitalize(),
Scott Zawalski65650172012-02-16 11:48:26 -0500458 meta_hosts=[meta_hosts],
459 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700460
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500461 setattr(test_obj, 'test_name', test.name)
462
463 return test_obj
464
Chris Masone6fed6462011-10-20 16:36:43 -0700465
Chris Masone8b7cd422012-02-22 13:16:11 -0800466 def run_and_wait(self, record, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700467 """
468 Synchronously run tests in |self.tests|.
469
Chris Masone8b7cd422012-02-22 13:16:11 -0800470 Schedules tests against a device running image |self._build|, and
Chris Masone6fed6462011-10-20 16:36:43 -0700471 then polls for status, using |record| to print status when each
472 completes.
473
474 Tests returned by self.stable_tests() will always be run, while tests
475 in self.unstable_tests() will only be run if |add_experimental| is true.
476
Chris Masone6fed6462011-10-20 16:36:43 -0700477 @param record: callable that records job status.
478 prototype:
479 record(status, subdir, name, reason)
480 @param add_experimental: schedule experimental tests as well, or not.
481 """
482 try:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500483 record('INFO', None, 'Start %s' % self._tag)
Chris Masone8b7cd422012-02-22 13:16:11 -0800484 self.schedule(add_experimental)
Chris Masone6fed6462011-10-20 16:36:43 -0700485 try:
486 for result in self.wait_for_results():
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500487 # |result| will be a tuple of a maximum of 4 entries and a
488 # minimum of 3. We use the first 3 for START and END
489 # entries so we separate those variables out for legible
490 # variable names, nothing more.
491 status = result[0]
492 test_name = result[2]
493 record('START', None, test_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700494 record(*result)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500495 record('END %s' % status, None, test_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700496 except Exception as e:
497 logging.error(e)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500498 record('FAIL', None, self._tag,
499 'Exception waiting for results')
Chris Masone6fed6462011-10-20 16:36:43 -0700500 except Exception as e:
501 logging.error(e)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500502 record('FAIL', None, self._tag,
503 'Exception while scheduling suite')
Chris Masone6fed6462011-10-20 16:36:43 -0700504
505
Chris Masone8b7cd422012-02-22 13:16:11 -0800506 def schedule(self, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700507 """
508 Schedule jobs using |self._afe|.
509
510 frontend.Job objects representing each scheduled job will be put in
511 |self._jobs|.
512
Chris Masone6fed6462011-10-20 16:36:43 -0700513 @param add_experimental: schedule experimental tests as well, or not.
514 """
515 for test in self.stable_tests():
516 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800517 self._jobs.append(self._create_job(test))
Chris Masone6fed6462011-10-20 16:36:43 -0700518
519 if add_experimental:
520 # TODO(cmasone): ensure I can log results from these differently.
521 for test in self.unstable_tests():
522 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800523 self._jobs.append(self._create_job(test))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500524 if self._results_dir:
525 self._record_scheduled_jobs()
526
527
528 def _record_scheduled_jobs(self):
529 """
530 Record scheduled job ids as keyvals, so they can be referenced later.
531
532 """
533 for job in self._jobs:
534 job_id_owner = '%s-%s' % (job.id, job.owner)
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500535 utils.write_keyval(self._results_dir, {job.test_name: job_id_owner})
Chris Masone6fed6462011-10-20 16:36:43 -0700536
537
538 def _status_is_relevant(self, status):
539 """
540 Indicates whether the status of a given test is meaningful or not.
541
542 @param status: frontend.TestStatus object to look at.
543 @return True if this is a test result worth looking at further.
544 """
545 return not (status.test_name.startswith('SERVER_JOB') or
546 status.test_name.startswith('CLIENT_JOB'))
547
548
549 def _collate_aborted(self, current_value, entry):
550 """
551 reduce() over a list of HostQueueEntries for a job; True if any aborted.
552
553 Functor that can be reduced()ed over a list of
554 HostQueueEntries for a job. If any were aborted
555 (|entry.aborted| exists and is True), then the reduce() will
556 return True.
557
558 Ex:
559 entries = self._afe.run('get_host_queue_entries', job=job.id)
560 reduce(self._collate_aborted, entries, False)
561
562 @param current_value: the current accumulator (a boolean).
563 @param entry: the current entry under consideration.
564 @return the value of |entry.aborted| if it exists, False if not.
565 """
566 return current_value or ('aborted' in entry and entry['aborted'])
567
568
569 def wait_for_results(self):
570 """
571 Wait for results of all tests in all jobs in |self._jobs|.
572
573 Currently polls for results every 5s. When all results are available,
574 @return a list of tuples, one per test: (status, subdir, name, reason)
575 """
Chris Masone6fed6462011-10-20 16:36:43 -0700576 while self._jobs:
577 for job in list(self._jobs):
578 if not self._afe.get_jobs(id=job.id, finished=True):
579 continue
580
581 self._jobs.remove(job)
582
583 entries = self._afe.run('get_host_queue_entries', job=job.id)
584 if reduce(self._collate_aborted, entries, False):
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500585 yield('ABORT', None, job.name)
Chris Masone6fed6462011-10-20 16:36:43 -0700586 else:
587 statuses = self._tko.get_status_counts(job=job.id)
588 for s in filter(self._status_is_relevant, statuses):
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500589 yield(s.status, None, s.test_name, s.reason)
Chris Masone6fed6462011-10-20 16:36:43 -0700590 time.sleep(5)
591
Chris Masone6fed6462011-10-20 16:36:43 -0700592
Chris Masonefef21382012-01-17 11:16:32 -0800593 @staticmethod
594 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
Chris Masone6fed6462011-10-20 16:36:43 -0700595 """
596 Function to scan through all tests and find eligible tests.
597
598 Looks at control files returned by _cf_getter.get_control_file_list()
599 for tests that pass self._predicate().
600
601 @param cf_getter: a control_file_getter.ControlFileGetter used to list
602 and fetch the content of control files
603 @param predicate: a function that should return True when run over a
604 ControlData representation of a control file that should be in
605 this Suite.
606 @param add_experimental: add tests with experimental attribute set.
607
608 @return list of ControlData objects that should be run, with control
609 file text added in |text| attribute.
610 """
611 tests = {}
612 files = cf_getter.get_control_file_list()
613 for file in files:
614 text = cf_getter.get_control_file_contents(file)
615 try:
616 found_test = control_data.parse_control_string(text,
617 raise_warnings=True)
618 if not add_experimental and found_test.experimental:
619 continue
620
621 found_test.text = text
Chris Masonee8a4eff2012-02-28 16:33:43 -0800622 found_test.path = file
Chris Masone6fed6462011-10-20 16:36:43 -0700623 tests[file] = found_test
624 except control_data.ControlVariableException, e:
625 logging.warn("Skipping %s\n%s", file, e)
626 except Exception, e:
627 logging.error("Bad %s\n%s", file, e)
628
629 return [test for test in tests.itervalues() if predicate(test)]