blob: 3fd7458135ab871d0936213e6463658726c804be [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 = \
Chris Masone9f13ff22012-03-05 13:45:25 -080050 _vet_reimage_and_run_args(**dargs)
51 reimager = Reimager(job.autodir, pool=pool, results_dir=job.resultdir)
Chris Masoneab3e7332012-02-29 18:54:58 -080052 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():
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800105 # None gets injected as 'None' without this check; same for digits.
106 if isinstance(value, str):
107 control_file += "%s='%s'\n" % (key, value)
108 else:
109 control_file += "%s=%r\n" % (key, value)
Chris Masone8b764252012-01-17 11:12:51 -0800110 return control_file + control_file_in
111
112
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800113def _image_url_pattern():
114 return CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
115
116
117def _package_url_pattern():
118 return CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
119
Chris Masone6fed6462011-10-20 16:36:43 -0700120
Chris Masoneab3e7332012-02-29 18:54:58 -0800121def skip_reimage(g):
122 return g.get('SKIP_IMAGE')
123
124
Chris Masone6fed6462011-10-20 16:36:43 -0700125class Reimager(object):
126 """
127 A class that can run jobs to reimage devices.
128
129 @var _afe: a frontend.AFE instance used to talk to autotest.
130 @var _tko: a frontend.TKO instance used to query the autotest results db.
131 @var _cf_getter: a ControlFileGetter used to get the AU control file.
132 """
133
134
Chris Masone9f13ff22012-03-05 13:45:25 -0800135 def __init__(self, autotest_dir, afe=None, tko=None, pool=None,
136 results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700137 """
138 Constructor
139
140 @param autotest_dir: the place to find autotests.
141 @param afe: an instance of AFE as defined in server/frontend.py.
142 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500143 @param pool: Specify the pool of machines to use for scheduling
144 purposes.
Chris Masone9f13ff22012-03-05 13:45:25 -0800145 @param results_dir: The directory where the job can write results to.
146 This must be set if you want job_id of sub-jobs
147 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700148 """
Chris Masone8ac66712012-02-15 14:21:02 -0800149 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
150 delay_sec=10,
151 debug=False)
152 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
153 delay_sec=10,
154 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500155 self._pool = pool
Chris Masone9f13ff22012-03-05 13:45:25 -0800156 self._results_dir = results_dir
Chris Masone6fed6462011-10-20 16:36:43 -0700157 self._cf_getter = control_file_getter.FileSystemGetter(
158 [os.path.join(autotest_dir, 'server/site_tests')])
159
160
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800161 def skip(self, g):
Chris Masoneab3e7332012-02-29 18:54:58 -0800162 """Deprecated in favor of dynamic_suite.skip_reimage()."""
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800163 return 'SKIP_IMAGE' in g and g['SKIP_IMAGE']
164
165
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800166 def attempt(self, build, board, record, num=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700167 """
168 Synchronously attempt to reimage some machines.
169
170 Fire off attempts to reimage |num| machines of type |board|, using an
Chris Masone8abb6fc2012-01-31 09:27:36 -0800171 image at |url| called |build|. Wait for completion, polling every
Chris Masone6fed6462011-10-20 16:36:43 -0700172 10s, and log results with |record| upon completion.
173
Chris Masone8abb6fc2012-01-31 09:27:36 -0800174 @param build: the build to install e.g.
175 x86-alex-release/R18-1655.0.0-a1-b1584.
Chris Masone6fed6462011-10-20 16:36:43 -0700176 @param board: which kind of devices to reimage.
177 @param record: callable that records job status.
Chris Masone796fcf12012-02-22 16:53:31 -0800178 prototype:
179 record(status, subdir, name, reason)
Chris Masone5552dd72012-02-15 15:01:04 -0800180 @param num: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700181 @return True if all reimaging jobs succeed, false otherwise.
182 """
Chris Masone5552dd72012-02-15 15:01:04 -0800183 if not num:
184 num = CONFIG.get_config_value('CROS', 'sharding_factor', type=int)
Scott Zawalski65650172012-02-16 11:48:26 -0500185 logging.debug("scheduling reimaging across %d machines", num)
Chris Masone9f13ff22012-03-05 13:45:25 -0800186 wrapper_job_name = 'try_new_image'
Chris Masone73f65022012-01-31 14:00:43 -0800187 record('START', None, wrapper_job_name)
Chris Masone796fcf12012-02-22 16:53:31 -0800188 try:
189 self._ensure_version_label(VERSION_PREFIX + build)
190 canary = self._schedule_reimage_job(build, num, board)
Chris Masone9f13ff22012-03-05 13:45:25 -0800191 self._record_job_if_possible(wrapper_job_name, canary)
Chris Masone796fcf12012-02-22 16:53:31 -0800192 logging.debug('Created re-imaging job: %d', canary.id)
193 while len(self._afe.get_jobs(id=canary.id, not_yet_run=True)) > 0:
194 time.sleep(10)
195 logging.debug('Re-imaging job running.')
196 while len(self._afe.get_jobs(id=canary.id, finished=True)) == 0:
197 time.sleep(10)
198 logging.debug('Re-imaging job finished.')
199 canary.result = self._afe.poll_job_results(self._tko, canary, 0)
200 except Exception as e:
201 # catch Exception so we record the job as terminated no matter what.
202 logging.error(e)
203 record('END ERROR', None, wrapper_job_name, str(e))
204 return False
Chris Masone6fed6462011-10-20 16:36:43 -0700205
206 if canary.result is True:
207 self._report_results(canary, record)
Chris Masone73f65022012-01-31 14:00:43 -0800208 record('END GOOD', None, wrapper_job_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700209 return True
210
211 if canary.result is None:
212 record('FAIL', None, canary.name, 're-imaging tasks did not run')
213 else: # canary.result is False
214 self._report_results(canary, record)
215
Chris Masone73f65022012-01-31 14:00:43 -0800216 record('END FAIL', None, wrapper_job_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700217 return False
218
219
Chris Masone9f13ff22012-03-05 13:45:25 -0800220 def _record_job_if_possible(self, test_name, job):
221 """
222 Record job id as keyval, if possible, so it can be referenced later.
223
224 If |self._results_dir| is None, then this is a NOOP.
225 """
226 if self._results_dir:
227 job_id_owner = '%s-%s' % (job.id, job.owner)
228 utils.write_keyval(self._results_dir, {test_name: job_id_owner})
229
230
Chris Masone6fed6462011-10-20 16:36:43 -0700231 def _ensure_version_label(self, name):
232 """
233 Ensure that a label called |name| exists in the autotest DB.
234
235 @param name: the label to check for/create.
236 """
237 labels = self._afe.get_labels(name=name)
238 if len(labels) == 0:
239 self._afe.create_label(name=name)
240
241
Chris Masone8abb6fc2012-01-31 09:27:36 -0800242 def _schedule_reimage_job(self, build, num_machines, board):
Chris Masone6fed6462011-10-20 16:36:43 -0700243 """
244 Schedules the reimaging of |num_machines| |board| devices with |image|.
245
246 Sends an RPC to the autotest frontend to enqueue reimaging jobs on
247 |num_machines| devices of type |board|
248
Chris Masone8abb6fc2012-01-31 09:27:36 -0800249 @param build: the build to install (must be unique).
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800250 @param num_machines: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700251 @param board: which kind of devices to reimage.
252 @return a frontend.Job object for the reimaging job we scheduled.
253 """
Chris Masone8b764252012-01-17 11:12:51 -0800254 control_file = inject_vars(
Chris Masone8abb6fc2012-01-31 09:27:36 -0800255 {'image_url': _image_url_pattern() % build, 'image_name': build},
Chris Masone6fed6462011-10-20 16:36:43 -0700256 self._cf_getter.get_control_file_contents_by_name('autoupdate'))
Scott Zawalski65650172012-02-16 11:48:26 -0500257 job_deps = []
258 if self._pool:
259 meta_host = 'pool:%s' % self._pool
260 board_label = 'board:%s' % board
261 job_deps.append(board_label)
262 else:
263 # No pool specified use board.
264 meta_host = 'board:%s' % board
Chris Masone6fed6462011-10-20 16:36:43 -0700265
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800266 return self._afe.create_job(control_file=control_file,
Chris Masone8abb6fc2012-01-31 09:27:36 -0800267 name=build + '-try',
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800268 control_type='Server',
Scott Zawalski65650172012-02-16 11:48:26 -0500269 meta_hosts=[meta_host] * num_machines,
270 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700271
272
273 def _report_results(self, job, record):
274 """
275 Record results from a completed frontend.Job object.
276
277 @param job: a completed frontend.Job object populated by
278 frontend.AFE.poll_job_results.
279 @param record: callable that records job status.
280 prototype:
281 record(status, subdir, name, reason)
282 """
283 if job.result == True:
284 record('GOOD', None, job.name)
285 return
286
287 for platform in job.results_platform_map:
288 for status in job.results_platform_map[platform]:
289 if status == 'Total':
290 continue
291 for host in job.results_platform_map[platform][status]:
292 if host not in job.test_status:
293 record('ERROR', None, host, 'Job failed to run.')
294 elif status == 'Failed':
295 for test_status in job.test_status[host].fail:
296 record('FAIL', None, host, test_status.reason)
297 elif status == 'Aborted':
298 for test_status in job.test_status[host].fail:
299 record('ABORT', None, host, test_status.reason)
300 elif status == 'Completed':
301 record('GOOD', None, host)
302
303
304class Suite(object):
305 """
306 A suite of tests, defined by some predicate over control file variables.
307
308 Given a place to search for control files a predicate to match the desired
309 tests, can gather tests and fire off jobs to run them, and then wait for
310 results.
311
312 @var _predicate: a function that should return True when run over a
313 ControlData representation of a control file that should be in
314 this Suite.
315 @var _tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800316 @var _build: the build on which we're running this suite.
Chris Masone6fed6462011-10-20 16:36:43 -0700317 @var _afe: an instance of AFE as defined in server/frontend.py.
318 @var _tko: an instance of TKO as defined in server/frontend.py.
319 @var _jobs: currently scheduled jobs, if any.
320 @var _cf_getter: a control_file_getter.ControlFileGetter
321 """
322
323
Chris Masonefef21382012-01-17 11:16:32 -0800324 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800325 def create_ds_getter(build):
Chris Masonefef21382012-01-17 11:16:32 -0800326 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800327 @param build: the build on which we're running this suite.
Chris Masonefef21382012-01-17 11:16:32 -0800328 @return a FileSystemGetter instance that looks under |autotest_dir|.
329 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800330 return control_file_getter.DevServerGetter(
331 build, dev_server.DevServer.create())
Chris Masonefef21382012-01-17 11:16:32 -0800332
333
334 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800335 def create_fs_getter(autotest_dir):
336 """
337 @param autotest_dir: the place to find autotests.
338 @return a FileSystemGetter instance that looks under |autotest_dir|.
339 """
340 # currently hard-coded places to look for tests.
341 subpaths = ['server/site_tests', 'client/site_tests',
342 'server/tests', 'client/tests']
343 directories = [os.path.join(autotest_dir, p) for p in subpaths]
344 return control_file_getter.FileSystemGetter(directories)
345
346
347 @staticmethod
Chris Masone84564792012-02-23 10:52:42 -0800348 def name_in_tag_predicate(name):
349 """Returns predicate that takes a control file and looks for |name|.
350
351 Builds a predicate that takes in a parsed control file (a ControlData)
352 and returns True if the SUITE tag is present and contains |name|.
353
354 @param name: the suite name to base the predicate on.
355 @return a callable that takes a ControlData and looks for |name| in that
356 ControlData object's suite member.
357 """
358 def parse(suite):
359 """Splits a string on ',' optionally surrounded by whitespace."""
360 return map(lambda x: x.strip(), suite.split(','))
361
362 return lambda t: hasattr(t, 'suite') and name in parse(t.suite)
363
364
365 @staticmethod
Scott Zawalski9ece6532012-02-28 14:10:47 -0500366 def create_from_name(name, build, cf_getter=None, afe=None, tko=None,
367 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700368 """
369 Create a Suite using a predicate based on the SUITE control file var.
370
371 Makes a predicate based on |name| and uses it to instantiate a Suite
372 that looks for tests in |autotest_dir| and will schedule them using
Chris Masoned6f38c82012-02-22 14:53:42 -0800373 |afe|. Pulls control files from the default dev server.
374 Results will be pulled from |tko| upon completion.
Chris Masone6fed6462011-10-20 16:36:43 -0700375
376 @param name: a value of the SUITE control file variable to search for.
Chris Masone8b7cd422012-02-22 13:16:11 -0800377 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800378 @param cf_getter: a control_file_getter.ControlFileGetter.
379 If None, default to using a DevServerGetter.
Chris Masone6fed6462011-10-20 16:36:43 -0700380 @param afe: an instance of AFE as defined in server/frontend.py.
381 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500382 @param pool: Specify the pool of machines to use for scheduling
Chris Masoned6f38c82012-02-22 14:53:42 -0800383 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500384 @param results_dir: The directory where the job can write results to.
385 This must be set if you want job_id of sub-jobs
386 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700387 @return a Suite instance.
388 """
Chris Masoned6f38c82012-02-22 14:53:42 -0800389 if cf_getter is None:
390 cf_getter = Suite.create_ds_getter(build)
Chris Masone84564792012-02-23 10:52:42 -0800391 return Suite(Suite.name_in_tag_predicate(name),
Scott Zawalski9ece6532012-02-28 14:10:47 -0500392 name, build, cf_getter, afe, tko, pool, results_dir)
Chris Masone6fed6462011-10-20 16:36:43 -0700393
394
Chris Masoned6f38c82012-02-22 14:53:42 -0800395 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Scott Zawalski9ece6532012-02-28 14:10:47 -0500396 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700397 """
398 Constructor
399
400 @param predicate: a function that should return True when run over a
401 ControlData representation of a control file that should be in
402 this Suite.
403 @param tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800404 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800405 @param cf_getter: a control_file_getter.ControlFileGetter
Chris Masone6fed6462011-10-20 16:36:43 -0700406 @param afe: an instance of AFE as defined in server/frontend.py.
407 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500408 @param pool: Specify the pool of machines to use for scheduling
409 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500410 @param results_dir: The directory where the job can write results to.
411 This must be set if you want job_id of sub-jobs
412 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700413 """
414 self._predicate = predicate
415 self._tag = tag
Chris Masone8b7cd422012-02-22 13:16:11 -0800416 self._build = build
Chris Masoned6f38c82012-02-22 14:53:42 -0800417 self._cf_getter = cf_getter
Scott Zawalski9ece6532012-02-28 14:10:47 -0500418 self._results_dir = results_dir
Chris Masone8ac66712012-02-15 14:21:02 -0800419 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
420 delay_sec=10,
421 debug=False)
422 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
423 delay_sec=10,
424 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500425 self._pool = pool
Chris Masone6fed6462011-10-20 16:36:43 -0700426 self._jobs = []
Chris Masone6fed6462011-10-20 16:36:43 -0700427 self._tests = Suite.find_and_parse_tests(self._cf_getter,
428 self._predicate,
429 add_experimental=True)
430
431
432 @property
433 def tests(self):
434 """
435 A list of ControlData objects in the suite, with added |text| attr.
436 """
437 return self._tests
438
439
440 def stable_tests(self):
441 """
442 |self.tests|, filtered for non-experimental tests.
443 """
444 return filter(lambda t: not t.experimental, self.tests)
445
446
447 def unstable_tests(self):
448 """
449 |self.tests|, filtered for experimental tests.
450 """
451 return filter(lambda t: t.experimental, self.tests)
452
453
Chris Masone8b7cd422012-02-22 13:16:11 -0800454 def _create_job(self, test):
Chris Masone6fed6462011-10-20 16:36:43 -0700455 """
456 Thin wrapper around frontend.AFE.create_job().
457
458 @param test: ControlData object for a test to run.
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500459 @return a frontend.Job object with an added test_name member.
460 test_name is used to preserve the higher level TEST_NAME
461 name of the job.
Chris Masone6fed6462011-10-20 16:36:43 -0700462 """
Scott Zawalski65650172012-02-16 11:48:26 -0500463 job_deps = []
464 if self._pool:
465 meta_hosts = 'pool:%s' % self._pool
Chris Masone8b7cd422012-02-22 13:16:11 -0800466 cros_label = VERSION_PREFIX + self._build
Scott Zawalski65650172012-02-16 11:48:26 -0500467 job_deps.append(cros_label)
468 else:
469 # No pool specified use any machines with the following label.
Chris Masone8b7cd422012-02-22 13:16:11 -0800470 meta_hosts = VERSION_PREFIX + self._build
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500471 test_obj = self._afe.create_job(
Chris Masone6fed6462011-10-20 16:36:43 -0700472 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800473 name='/'.join([self._build, self._tag, test.name]),
Chris Masone6fed6462011-10-20 16:36:43 -0700474 control_type=test.test_type.capitalize(),
Scott Zawalski65650172012-02-16 11:48:26 -0500475 meta_hosts=[meta_hosts],
476 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700477
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500478 setattr(test_obj, 'test_name', test.name)
479
480 return test_obj
481
Chris Masone6fed6462011-10-20 16:36:43 -0700482
Chris Masone8b7cd422012-02-22 13:16:11 -0800483 def run_and_wait(self, record, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700484 """
485 Synchronously run tests in |self.tests|.
486
Chris Masone8b7cd422012-02-22 13:16:11 -0800487 Schedules tests against a device running image |self._build|, and
Chris Masone6fed6462011-10-20 16:36:43 -0700488 then polls for status, using |record| to print status when each
489 completes.
490
491 Tests returned by self.stable_tests() will always be run, while tests
492 in self.unstable_tests() will only be run if |add_experimental| is true.
493
Chris Masone6fed6462011-10-20 16:36:43 -0700494 @param record: callable that records job status.
495 prototype:
496 record(status, subdir, name, reason)
497 @param add_experimental: schedule experimental tests as well, or not.
498 """
499 try:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500500 record('INFO', None, 'Start %s' % self._tag)
Chris Masone8b7cd422012-02-22 13:16:11 -0800501 self.schedule(add_experimental)
Chris Masone6fed6462011-10-20 16:36:43 -0700502 try:
503 for result in self.wait_for_results():
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500504 # |result| will be a tuple of a maximum of 4 entries and a
505 # minimum of 3. We use the first 3 for START and END
506 # entries so we separate those variables out for legible
507 # variable names, nothing more.
508 status = result[0]
509 test_name = result[2]
510 record('START', None, test_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700511 record(*result)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500512 record('END %s' % status, None, test_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700513 except Exception as e:
514 logging.error(e)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500515 record('FAIL', None, self._tag,
516 'Exception waiting for results')
Chris Masone6fed6462011-10-20 16:36:43 -0700517 except Exception as e:
518 logging.error(e)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500519 record('FAIL', None, self._tag,
520 'Exception while scheduling suite')
Chris Masone6fed6462011-10-20 16:36:43 -0700521
522
Chris Masone8b7cd422012-02-22 13:16:11 -0800523 def schedule(self, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700524 """
525 Schedule jobs using |self._afe|.
526
527 frontend.Job objects representing each scheduled job will be put in
528 |self._jobs|.
529
Chris Masone6fed6462011-10-20 16:36:43 -0700530 @param add_experimental: schedule experimental tests as well, or not.
531 """
532 for test in self.stable_tests():
533 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800534 self._jobs.append(self._create_job(test))
Chris Masone6fed6462011-10-20 16:36:43 -0700535
536 if add_experimental:
537 # TODO(cmasone): ensure I can log results from these differently.
538 for test in self.unstable_tests():
539 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800540 self._jobs.append(self._create_job(test))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500541 if self._results_dir:
542 self._record_scheduled_jobs()
543
544
545 def _record_scheduled_jobs(self):
546 """
547 Record scheduled job ids as keyvals, so they can be referenced later.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500548 """
549 for job in self._jobs:
550 job_id_owner = '%s-%s' % (job.id, job.owner)
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500551 utils.write_keyval(self._results_dir, {job.test_name: job_id_owner})
Chris Masone6fed6462011-10-20 16:36:43 -0700552
553
554 def _status_is_relevant(self, status):
555 """
556 Indicates whether the status of a given test is meaningful or not.
557
558 @param status: frontend.TestStatus object to look at.
559 @return True if this is a test result worth looking at further.
560 """
561 return not (status.test_name.startswith('SERVER_JOB') or
562 status.test_name.startswith('CLIENT_JOB'))
563
564
565 def _collate_aborted(self, current_value, entry):
566 """
567 reduce() over a list of HostQueueEntries for a job; True if any aborted.
568
569 Functor that can be reduced()ed over a list of
570 HostQueueEntries for a job. If any were aborted
571 (|entry.aborted| exists and is True), then the reduce() will
572 return True.
573
574 Ex:
575 entries = self._afe.run('get_host_queue_entries', job=job.id)
576 reduce(self._collate_aborted, entries, False)
577
578 @param current_value: the current accumulator (a boolean).
579 @param entry: the current entry under consideration.
580 @return the value of |entry.aborted| if it exists, False if not.
581 """
582 return current_value or ('aborted' in entry and entry['aborted'])
583
584
585 def wait_for_results(self):
586 """
587 Wait for results of all tests in all jobs in |self._jobs|.
588
589 Currently polls for results every 5s. When all results are available,
590 @return a list of tuples, one per test: (status, subdir, name, reason)
591 """
Chris Masone6fed6462011-10-20 16:36:43 -0700592 while self._jobs:
593 for job in list(self._jobs):
594 if not self._afe.get_jobs(id=job.id, finished=True):
595 continue
596
597 self._jobs.remove(job)
598
599 entries = self._afe.run('get_host_queue_entries', job=job.id)
600 if reduce(self._collate_aborted, entries, False):
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500601 yield('ABORT', None, job.name)
Chris Masone6fed6462011-10-20 16:36:43 -0700602 else:
603 statuses = self._tko.get_status_counts(job=job.id)
604 for s in filter(self._status_is_relevant, statuses):
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500605 yield(s.status, None, s.test_name, s.reason)
Chris Masone6fed6462011-10-20 16:36:43 -0700606 time.sleep(5)
607
Chris Masone6fed6462011-10-20 16:36:43 -0700608
Chris Masonefef21382012-01-17 11:16:32 -0800609 @staticmethod
610 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
Chris Masone6fed6462011-10-20 16:36:43 -0700611 """
612 Function to scan through all tests and find eligible tests.
613
614 Looks at control files returned by _cf_getter.get_control_file_list()
615 for tests that pass self._predicate().
616
617 @param cf_getter: a control_file_getter.ControlFileGetter used to list
618 and fetch the content of control files
619 @param predicate: a function that should return True when run over a
620 ControlData representation of a control file that should be in
621 this Suite.
622 @param add_experimental: add tests with experimental attribute set.
623
624 @return list of ControlData objects that should be run, with control
625 file text added in |text| attribute.
626 """
627 tests = {}
628 files = cf_getter.get_control_file_list()
629 for file in files:
630 text = cf_getter.get_control_file_contents(file)
631 try:
632 found_test = control_data.parse_control_string(text,
633 raise_warnings=True)
634 if not add_experimental and found_test.experimental:
635 continue
636
637 found_test.text = text
Chris Masonee8a4eff2012-02-28 16:33:43 -0800638 found_test.path = file
Chris Masone6fed6462011-10-20 16:36:43 -0700639 tests[file] = found_test
640 except control_data.ControlVariableException, e:
641 logging.warn("Skipping %s\n%s", file, e)
642 except Exception, e:
643 logging.error("Bad %s\n%s", file, e)
644
645 return [test for test in tests.itervalues() if predicate(test)]