blob: 86e935d74ef78ae714019afc4163b8aad89460d2 [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
Chris Masone11aae452012-05-21 16:08:39 -07006import compiler, datetime, hashlib, logging, os, random, re, time, traceback
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 Masone275ec902012-07-10 15:28:34 -070011from autotest_lib.server.cros import host_lock_manager, job_status
Chris Masone604baf32012-06-28 08:45:30 -070012from autotest_lib.server.cros.job_status import Status
Chris Masone6fed6462011-10-20 16:36:43 -070013from autotest_lib.server import frontend
Chris Masonef8b53062012-05-08 22:14:18 -070014from autotest_lib.frontend.afe.json_rpc import proxy
Chris Masone6fed6462011-10-20 16:36:43 -070015
Chris Masone6cfb7122012-05-02 11:36:28 -070016"""CrOS dynamic test suite generation and execution module.
17
18This module implements runtime-generated test suites for CrOS.
19Design doc: http://goto.google.com/suitesv2
20
21Individual tests can declare themselves as a part of one or more
22suites, and the code here enables control files to be written
23that can refer to these "dynamic suites" by name. We also provide
24support for reimaging devices with a given build and running a
25dynamic suite across all reimaged devices.
26
27The public API for defining a suite includes one method: reimage_and_run().
28A suite control file can be written by importing this module and making
29an appropriate call to this single method. In normal usage, this control
30file will be run in a 'hostless' server-side autotest job, scheduling
31sub-jobs to do the needed reimaging and test running.
32
33Example control file:
34
35import common
36from autotest_lib.server.cros import dynamic_suite
37
38dynamic_suite.reimage_and_run(
39 build=build, board=board, name='bvt', job=job, pool=pool,
40 check_hosts=check_hosts, add_experimental=True, num=4,
41 skip_reimage=dynamic_suite.skip_reimage(globals()))
42
43This will -- at runtime -- find all control files that contain "bvt"
44in their "SUITE=" clause, schedule jobs to reimage 4 devices in the
45specified pool of the specified board with the specified build and,
46upon completion of those jobs, schedule and wait for jobs that run all
47the tests it discovered across those 4 machines.
48
49Suites can be run by using the atest command-line tool:
50 atest suite create -b <board> -i <build/name> <suite>
51e.g.
52 atest suite create -b x86-mario -i x86-mario/R20-2203.0.0 bvt
53
54-------------------------------------------------------------------------
55Implementation details
56
57In addition to the create_suite_job() RPC defined in the autotest frontend,
58there are two main classes defined here: Suite and Reimager.
59
60A Suite instance represents a single test suite, defined by some predicate
61run over all known control files. The simplest example is creating a Suite
62by 'name'.
63
64The Reimager class provides support for reimaging a heterogenous set
65of devices with an appropriate build, in preparation for a test run.
66One could use a single Reimager, followed by the instantiation and use
67of multiple Suite objects.
68
69create_suite_job() takes the parameters needed to define a suite run (board,
70build to test, machine pool, and which suite to run), ensures important
71preconditions are met, finds the appropraite suite control file, and then
72schedules the hostless job that will do the rest of the work.
73
74reimage_and_run() works by creating a Reimager, using it to perform the
75requested installs, and then instantiating a Suite and running it on the
76machines that were just reimaged. We'll go through this process in stages.
77
78- create_suite_job()
79The primary role of create_suite_job() is to ensure that the required
80artifacts for the build to be tested are staged on the dev server. This
81includes payloads required to autoupdate machines to the desired build, as
82well as the autotest control files appropriate for that build. Then, the
83RPC pulls the control file for the suite to be run from the dev server and
84uses it to create the suite job with the autotest frontend.
85
86 +----------------+
87 | Google Storage | Client
88 +----------------+ |
89 | ^ | create_suite_job()
90 payloads/ | | |
91 control files | | request |
92 V | V
93 +-------------+ download request +--------------------------+
94 | |<----------------------| |
95 | Dev Server | | Autotest Frontend (AFE) |
96 | |---------------------->| |
97 +-------------+ suite control file +--------------------------+
98 |
99 V
100 Suite Job (hostless)
101
102- The Reimaging process
103In short, the Reimager schedules and waits for a number of autoupdate 'test'
104jobs that perform image installation and make sure the device comes back up.
105It labels the machines that it reimages with the newly-installed CrOS version,
106so that later steps in the can refer to the machines by version and board,
107instead of having to keep track of hostnames or some such.
108
109The number of machines to use is called the 'sharding_factor', and the default
110is defined in the [CROS] section of global_config.ini. This can be overridden
111by passing a 'num=N' parameter to reimage_and_run() as shown in the example
112above.
113
114Step by step:
1151) Schedule autoupdate 'tests' across N devices of the appropriate board.
116 - Technically, one job that has N tests across N hosts.
117 - This 'test' is in server/site_tests/autoupdate/
118 - The control file is modified at runtime to inject the name of the build
119 to install, and the URL to get said build from.
120 - This is the _TOT_ version of the autoupdate test; it must be able to run
121 successfully on all currently supported branches at all times.
1222) Wait for this job to get kicked off and run to completion.
1233) Label successfully reimaged devices with a 'cros-version' label
124 - This is actually done by the autoupdate 'test' control file.
1254) Add a host attribute ('job_repo_url') to each reimaged host indicating
126 the URL where packages should be downloaded for subsequent tests
127 - This is actually done by the autoupdate 'test' control file
128 - This information is consumed in server/site_autotest.py
129 - job_repo_url points to some location on the dev server, where build
130 artifacts are staged -- including autotest packages.
1315) Return success or failure.
132
133 +------------+ +--------------------------+
134 | | | |
135 | Dev Server | | Autotest Frontend (AFE) |
136 | | | [Suite Job] |
137 +------------+ +--------------------------+
138 | payloads | | | |
139 V V autoupdate test | | |
140 +--------+ +--------+ <-----+----------------+ | |
141 | Host 1 |<------| Host 2 |-------+ | |
142 +--------+ +--------+ label | |
143 VersLabel VersLabel <-----------------------+ |
144 job_repo_url job_repo_url <-----------------------------+
145 host-attribute
146
147To sum up, after re-imaging, we have the following assumptions:
148- |num| devices of type |board| have |build| installed.
149- These devices are labeled appropriately
150- They have a host attribute called 'job_repo_url' dictating where autotest
151 packages can be downloaded for test runs.
152
153
154- Running Suites
155A Suite instance uses the labels created by the Reimager to schedule test jobs
156across all the hosts that were just reimaged. It then waits for all these jobs.
157
158Step by step:
1591) At instantiation time, find all appropriate control files for this suite
160 that were included in the build to be tested. To do this, we consult the
161 Dev Server, where all these control files are staged.
162
163 +------------+ control files? +--------------------------+
164 | |<----------------------| |
165 | Dev Server | | Autotest Frontend (AFE) |
166 | |---------------------->| [Suite Job] |
167 +------------+ control files! +--------------------------+
168
1692) Now that the Suite instance exists, it schedules jobs for every control
170 file it deemed appropriate, to be run on the hosts that were labeled
171 by the Reimager. We stuff keyvals into these jobs, indicating what
172 build they were testing and which suite they were for.
173
174 +--------------------------+ Job for VersLabel +--------+
175 | |------------------------>| Host 1 | VersLabel
176 | Autotest Frontend (AFE) | +--------+ +--------+
177 | [Suite Job] |----------->| Host 2 |
178 +--------------------------+ Job for +--------+
179 | ^ VersLabel VersLabel
180 | |
181 +----------------+
182 One job per test
183 {'build': build/name,
184 'suite': suite_name}
185
1863) Now that all jobs are scheduled, they'll be doled out as labeled hosts
187 finish their assigned work and become available again.
1884) As we clean up each job, we check to see if any crashes occurred. If they
189 did, we look at the 'build' keyval in the job to see which build's debug
190 symbols we'll need to symbolicate the crash dump we just found.
1915) Using this info, we tell the Dev Server to stage the required debug symbols.
192 Once that's done, we ask the dev server to use those symbols to symbolicate
193 the crash dump in question.
194
195 +----------------+
196 | Google Storage |
197 +----------------+
198 | ^
199 symbols! | | symbols?
200 V |
201 +------------+ stage symbols for build +--------------------------+
202 | |<--------------------------| |
203 | | | |
204 | Dev Server | dump to symbolicate | Autotest Frontend (AFE) |
205 | |<--------------------------| [Suite Job] |
206 | |-------------------------->| |
207 +------------+ symbolicated dump +--------------------------+
208
2096) As jobs finish, we record their success or failure in the status of the suite
210 job. We also record a 'job keyval' in the suite job for each test, noting
211 the job ID and job owner. This can be used to refer to test logs later.
2127) Once all jobs are complete, status is recorded for the suite job, and the
213 job_repo_url host attribute is removed from all hosts used by the suite.
214
215"""
216
Chris Masone6fed6462011-10-20 16:36:43 -0700217
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700218# Job keyvals for finding debug symbols when processing crash dumps.
219JOB_BUILD_KEY = 'build'
220JOB_SUITE_KEY = 'suite'
221
222# Job attribute and label names
223JOB_REPO_URL = 'job_repo_url'
Scott Zawalski65650172012-02-16 11:48:26 -0500224VERSION_PREFIX = 'cros-version:'
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700225EXPERIMENTAL_PREFIX = 'experimental_'
226REIMAGE_JOB_NAME = 'try_new_image'
227
228# Timings
229ARTIFACT_FINISHED_TIME = 'artifact_finished_time'
230DOWNLOAD_STARTED_TIME = 'download_started_time'
231PAYLOAD_FINISHED_TIME = 'payload_finished_time'
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700232
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800233CONFIG = global_config.global_config
234
235
Chris Masonef8b53062012-05-08 22:14:18 -0700236# Relevant CrosDynamicSuiteExceptions are defined in client/common_lib/error.py.
Chris Masone502b71e2012-04-10 10:41:35 -0700237
238
Chris Masoneab3e7332012-02-29 18:54:58 -0800239def reimage_and_run(**dargs):
240 """
241 Backward-compatible API for dynamic_suite.
242
243 Will re-image a number of devices (of the specified board) with the
244 provided build, and then run the indicated test suite on them.
245 Guaranteed to be compatible with any build from stable to dev.
246
247 Currently required args:
248 @param build: the build to install e.g.
249 x86-alex-release/R18-1655.0.0-a1-b1584.
250 @param board: which kind of devices to reimage.
251 @param name: a value of the SUITE control file variable to search for.
252 @param job: an instance of client.common_lib.base_job representing the
253 currently running suite job.
254
255 Currently supported optional args:
256 @param pool: specify the pool of machines to use for scheduling purposes.
257 Default: None
258 @param num: how many devices to reimage.
259 Default in global_config
Chris Masone62579122012-03-08 15:18:43 -0800260 @param check_hosts: require appropriate hosts to be available now.
Chris Masoneab3e7332012-02-29 18:54:58 -0800261 @param skip_reimage: skip reimaging, used for testing purposes.
262 Default: False
263 @param add_experimental: schedule experimental tests as well, or not.
264 Default: True
Chris Sosa6b288c82012-03-29 15:31:06 -0700265 @raises AsynchronousBuildFailure: if there was an issue finishing staging
266 from the devserver.
Chris Masoneab3e7332012-02-29 18:54:58 -0800267 """
Chris Masone62579122012-03-08 15:18:43 -0800268 (build, board, name, job, pool, num, check_hosts, skip_reimage,
269 add_experimental) = _vet_reimage_and_run_args(**dargs)
Chris Masone5374c672012-03-05 15:11:39 -0800270 board = 'board:%s' % board
271 if pool:
272 pool = 'pool:%s' % pool
Chris Masoned368cc42012-03-07 15:16:59 -0800273
Chris Masone275ec902012-07-10 15:28:34 -0700274 afe = frontend_wrappers.RetryingAFE(timeout_min=30, delay_sec=10,
275 debug=False)
276 tko = frontend_wrappers.RetryingTKO(timeout_min=30, delay_sec=10,
277 debug=False)
278 manager = host_lock_manager.HostLockManager(afe=afe)
279 reimager = Reimager(job.autodir, afe, tko, results_dir=job.resultdir)
Chris Masone275ec902012-07-10 15:28:34 -0700280 try:
281 if skip_reimage or reimager.attempt(build, board, pool,
282 job.record_entry, check_hosts,
283 manager, num=num):
284 # Ensure that the image's artifacts have completed downloading.
285 try:
286 ds = dev_server.DevServer.create()
287 ds.finish_download(build)
288 except dev_server.DevServerException as e:
289 raise error.AsynchronousBuildFailure(e)
Chris Sosa6b288c82012-03-29 15:31:06 -0700290
Chris Masone275ec902012-07-10 15:28:34 -0700291 timestamp = datetime.datetime.now().strftime(job_status.TIME_FMT)
292 utils.write_keyval(job.resultdir,
293 {ARTIFACT_FINISHED_TIME: timestamp})
294
295 suite = Suite.create_from_name(name, build, afe=afe, tko=tko,
296 pool=pool, results_dir=job.resultdir)
297 suite.run_and_wait(job.record_entry, manager, add_experimental)
298 finally:
299 manager.unlock()
Chris Masoneab3e7332012-02-29 18:54:58 -0800300
Chris Masoned368cc42012-03-07 15:16:59 -0800301 reimager.clear_reimaged_host_state(build)
302
Chris Masoneab3e7332012-02-29 18:54:58 -0800303
304def _vet_reimage_and_run_args(build=None, board=None, name=None, job=None,
Chris Masone62579122012-03-08 15:18:43 -0800305 pool=None, num=None, check_hosts=True,
306 skip_reimage=False, add_experimental=True,
307 **dargs):
Chris Masoneab3e7332012-02-29 18:54:58 -0800308 """
309 Vets arguments for reimage_and_run().
310
311 Currently required args:
312 @param build: the build to install e.g.
313 x86-alex-release/R18-1655.0.0-a1-b1584.
314 @param board: which kind of devices to reimage.
315 @param name: a value of the SUITE control file variable to search for.
316 @param job: an instance of client.common_lib.base_job representing the
317 currently running suite job.
318
319 Currently supported optional args:
320 @param pool: specify the pool of machines to use for scheduling purposes.
321 Default: None
322 @param num: how many devices to reimage.
323 Default in global_config
Chris Masone62579122012-03-08 15:18:43 -0800324 @param check_hosts: require appropriate hosts to be available now.
Chris Masoneab3e7332012-02-29 18:54:58 -0800325 @param skip_reimage: skip reimaging, used for testing purposes.
326 Default: False
327 @param add_experimental: schedule experimental tests as well, or not.
328 Default: True
329 @return a tuple of args set to provided (or default) values.
330 """
331 required_keywords = {'build': str,
332 'board': str,
333 'name': str,
334 'job': base_job.base_job}
335 for key, expected in required_keywords.iteritems():
336 value = locals().get(key)
337 if not value or not isinstance(value, expected):
Chris Masonef8b53062012-05-08 22:14:18 -0700338 raise error.SuiteArgumentException(
339 "reimage_and_run() needs %s=<%r>" % (key, expected))
Chris Masone62579122012-03-08 15:18:43 -0800340 return (build, board, name, job, pool, num, check_hosts, skip_reimage,
341 add_experimental)
Chris Masoneab3e7332012-02-29 18:54:58 -0800342
343
Chris Masone8b764252012-01-17 11:12:51 -0800344def inject_vars(vars, control_file_in):
345 """
Chris Masoneab3e7332012-02-29 18:54:58 -0800346 Inject the contents of |vars| into |control_file_in|.
Chris Masone8b764252012-01-17 11:12:51 -0800347
348 @param vars: a dict to shoehorn into the provided control file string.
349 @param control_file_in: the contents of a control file to munge.
350 @return the modified control file string.
351 """
352 control_file = ''
353 for key, value in vars.iteritems():
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800354 # None gets injected as 'None' without this check; same for digits.
355 if isinstance(value, str):
356 control_file += "%s='%s'\n" % (key, value)
357 else:
358 control_file += "%s=%r\n" % (key, value)
Chris Masone8b764252012-01-17 11:12:51 -0800359 return control_file + control_file_in
360
361
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800362def _image_url_pattern():
363 return CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
364
365
366def _package_url_pattern():
367 return CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
368
Chris Masone6fed6462011-10-20 16:36:43 -0700369
Chris Sosadfe70422012-07-25 16:23:55 -0700370def get_package_url(build):
371 """Returns the package url for the given build."""
372 devserver_url = dev_server.DevServer.devserver_url_for_build(build)
373 return _package_url_pattern() % (devserver_url, build)
374
375
Chris Masoneab3e7332012-02-29 18:54:58 -0800376def skip_reimage(g):
377 return g.get('SKIP_IMAGE')
378
379
Chris Masone6fed6462011-10-20 16:36:43 -0700380class Reimager(object):
381 """
382 A class that can run jobs to reimage devices.
383
384 @var _afe: a frontend.AFE instance used to talk to autotest.
385 @var _tko: a frontend.TKO instance used to query the autotest results db.
386 @var _cf_getter: a ControlFileGetter used to get the AU control file.
387 """
388
389
Chris Masonec43448f2012-05-31 12:55:59 -0700390 def __init__(self, autotest_dir, afe=None, tko=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700391 """
392 Constructor
393
394 @param autotest_dir: the place to find autotests.
395 @param afe: an instance of AFE as defined in server/frontend.py.
396 @param tko: an instance of TKO as defined in server/frontend.py.
Chris Masone9f13ff22012-03-05 13:45:25 -0800397 @param results_dir: The directory where the job can write results to.
398 This must be set if you want job_id of sub-jobs
399 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700400 """
Chris Masone8ac66712012-02-15 14:21:02 -0800401 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
402 delay_sec=10,
403 debug=False)
404 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
405 delay_sec=10,
406 debug=False)
Chris Masone9f13ff22012-03-05 13:45:25 -0800407 self._results_dir = results_dir
Chris Masoned368cc42012-03-07 15:16:59 -0800408 self._reimaged_hosts = {}
Chris Masone6fed6462011-10-20 16:36:43 -0700409 self._cf_getter = control_file_getter.FileSystemGetter(
410 [os.path.join(autotest_dir, 'server/site_tests')])
411
412
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800413 def skip(self, g):
Chris Masoneab3e7332012-02-29 18:54:58 -0800414 """Deprecated in favor of dynamic_suite.skip_reimage()."""
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800415 return 'SKIP_IMAGE' in g and g['SKIP_IMAGE']
416
417
Chris Masone275ec902012-07-10 15:28:34 -0700418 def attempt(self, build, board, pool, record, check_hosts,
419 manager, num=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700420 """
421 Synchronously attempt to reimage some machines.
422
423 Fire off attempts to reimage |num| machines of type |board|, using an
Chris Masone8abb6fc2012-01-31 09:27:36 -0800424 image at |url| called |build|. Wait for completion, polling every
Chris Masone6fed6462011-10-20 16:36:43 -0700425 10s, and log results with |record| upon completion.
426
Chris Masone8abb6fc2012-01-31 09:27:36 -0800427 @param build: the build to install e.g.
428 x86-alex-release/R18-1655.0.0-a1-b1584.
Chris Masone6fed6462011-10-20 16:36:43 -0700429 @param board: which kind of devices to reimage.
Chris Masonec43448f2012-05-31 12:55:59 -0700430 @param pool: Specify the pool of machines to use for scheduling
431 purposes.
Chris Masone6fed6462011-10-20 16:36:43 -0700432 @param record: callable that records job status.
Chris Masone604baf32012-06-28 08:45:30 -0700433 prototype:
434 record(base_job.status_log_entry)
Chris Masone62579122012-03-08 15:18:43 -0800435 @param check_hosts: require appropriate hosts to be available now.
Chris Masone275ec902012-07-10 15:28:34 -0700436 @param manager: an as-yet-unused HostLockManager instance to handle
437 locking DUTs that we decide to reimage.
Chris Masone5552dd72012-02-15 15:01:04 -0800438 @param num: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700439 @return True if all reimaging jobs succeed, false otherwise.
440 """
Chris Masone5552dd72012-02-15 15:01:04 -0800441 if not num:
442 num = CONFIG.get_config_value('CROS', 'sharding_factor', type=int)
Scott Zawalski65650172012-02-16 11:48:26 -0500443 logging.debug("scheduling reimaging across %d machines", num)
Chris Masone604baf32012-06-28 08:45:30 -0700444 begin_time_str = datetime.datetime.now().strftime(job_status.TIME_FMT)
Chris Masone796fcf12012-02-22 16:53:31 -0800445 try:
Chris Masone62579122012-03-08 15:18:43 -0800446 self._ensure_version_label(VERSION_PREFIX + build)
447
448 if check_hosts:
Chris Masonec43448f2012-05-31 12:55:59 -0700449 # TODO make DEPENDENCIES-aware
450 self._ensure_enough_hosts(board, pool, num)
Chris Masone5374c672012-03-05 15:11:39 -0800451
Chris Masoned368cc42012-03-07 15:16:59 -0800452 # Schedule job and record job metadata.
Chris Masonec43448f2012-05-31 12:55:59 -0700453 # TODO make DEPENDENCIES-aware
454 canary_job = self._schedule_reimage_job(build, board, pool, num)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700455 self._record_job_if_possible(REIMAGE_JOB_NAME, canary_job)
Chris Masonee1056d72012-07-24 15:22:40 -0700456 logging.info('Created re-imaging job: %d', canary_job.id)
Chris Masoned368cc42012-03-07 15:16:59 -0800457
Chris Masone517ef482012-07-23 15:36:36 -0700458 job_status.wait_for_jobs_to_start(self._afe, [canary_job])
Chris Masone275ec902012-07-10 15:28:34 -0700459 logging.debug('Re-imaging job running.')
460
461 hosts = job_status.wait_for_and_lock_job_hosts(self._afe,
Chris Masone517ef482012-07-23 15:36:36 -0700462 [canary_job],
463 manager)
Chris Masonee1056d72012-07-24 15:22:40 -0700464 logging.info('%r locked for reimaging.', hosts)
Chris Masone275ec902012-07-10 15:28:34 -0700465
Chris Masone517ef482012-07-23 15:36:36 -0700466 job_status.wait_for_jobs_to_finish(self._afe, [canary_job])
Chris Masone275ec902012-07-10 15:28:34 -0700467 logging.debug('Re-imaging job finished.')
Chris Masoned368cc42012-03-07 15:16:59 -0800468
469 # Gather job results.
Chris Masone6ea0cad2012-07-02 09:43:36 -0700470 results = self.get_results(canary_job)
Chris Masone275ec902012-07-10 15:28:34 -0700471 self._reimaged_hosts[build] = results.keys()
Chris Masone6ea0cad2012-07-02 09:43:36 -0700472
Chris Masonef8b53062012-05-08 22:14:18 -0700473 except error.InadequateHostsException as e:
Chris Masone5374c672012-03-05 15:11:39 -0800474 logging.warning(e)
Chris Masone604baf32012-06-28 08:45:30 -0700475 Status('WARN', REIMAGE_JOB_NAME, str(e),
476 begin_time_str=begin_time_str).record_all(record)
Chris Masone5374c672012-03-05 15:11:39 -0800477 return False
Chris Masone796fcf12012-02-22 16:53:31 -0800478 except Exception as e:
479 # catch Exception so we record the job as terminated no matter what.
480 logging.error(e)
Chris Masone604baf32012-06-28 08:45:30 -0700481 Status('ERROR', REIMAGE_JOB_NAME, str(e),
482 begin_time_str=begin_time_str).record_all(record)
Chris Masone796fcf12012-02-22 16:53:31 -0800483 return False
Chris Masone6fed6462011-10-20 16:36:43 -0700484
Chris Masone6ea0cad2012-07-02 09:43:36 -0700485 return job_status.record_and_report_results(results.values(), record)
Chris Masone6fed6462011-10-20 16:36:43 -0700486
Chris Masone604baf32012-06-28 08:45:30 -0700487
Chris Masone6ea0cad2012-07-02 09:43:36 -0700488 def get_results(self, canary_job):
489 """
490 Gather results for |canary_job|, in a map of Statuses indexed by host.
Chris Masone6fed6462011-10-20 16:36:43 -0700491
Chris Masone6ea0cad2012-07-02 09:43:36 -0700492 A host's results will be named REIMAGE_JOB_NAME-<host> in the map, e.g.
493 {'chromeos2-rack1': Status('GOOD', 'try_new_image-chromeos2-rack1')}
494
495 @param canary_job: a completed frontend.Job
496 @return a map of hostname: job_status.Status objects.
497 """
498 return job_status.gather_per_host_results(self._afe,
499 self._tko,
500 [canary_job],
Chris Sosadfe70422012-07-25 16:23:55 -0700501 REIMAGE_JOB_NAME + '-')
Chris Masone6fed6462011-10-20 16:36:43 -0700502
503
Chris Masone62579122012-03-08 15:18:43 -0800504 def _ensure_enough_hosts(self, board, pool, num):
505 """
506 Determine if there are enough working hosts to run on.
507
508 Raises exception if there are not enough hosts.
509
510 @param board: which kind of devices to reimage.
511 @param pool: the pool of machines to use for scheduling purposes.
512 @param num: how many devices to reimage.
Chris Masonef8b53062012-05-08 22:14:18 -0700513 @raises NoHostsException: if no working hosts.
Chris Masone62579122012-03-08 15:18:43 -0800514 @raises InadequateHostsException: if too few working hosts.
515 """
516 labels = [l for l in [board, pool] if l is not None]
Chris Masone502b71e2012-04-10 10:41:35 -0700517 available = self._count_usable_hosts(labels)
518 if available == 0:
Chris Masonef8b53062012-05-08 22:14:18 -0700519 raise error.NoHostsException('All hosts with %r are dead!' % labels)
Chris Masone502b71e2012-04-10 10:41:35 -0700520 elif num > available:
Chris Masonef8b53062012-05-08 22:14:18 -0700521 raise error.InadequateHostsException(
522 'Too few hosts with %r' % labels)
Chris Masone62579122012-03-08 15:18:43 -0800523
524
Chris Masoned368cc42012-03-07 15:16:59 -0800525 def clear_reimaged_host_state(self, build):
526 """
527 Clear per-host state created in the autotest DB for this job.
528
529 After reimaging a host, we label it and set some host attributes on it
530 that are then used by the suite scheduling code. This call cleans
531 that up.
532
533 @param build: the build whose hosts we want to clean up e.g.
534 x86-alex-release/R18-1655.0.0-a1-b1584.
535 """
Chris Masoned368cc42012-03-07 15:16:59 -0800536 for host in self._reimaged_hosts.get('build', []):
Chris Masone6ea0cad2012-07-02 09:43:36 -0700537 if not host.startswith('hostless'):
538 self._clear_build_state(host)
Chris Masoned368cc42012-03-07 15:16:59 -0800539
540
541 def _clear_build_state(self, machine):
542 """
543 Clear all build-specific labels, attributes from the target.
544
545 @param machine: the host to clear labels, attributes from.
546 """
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700547 self._afe.set_host_attribute(JOB_REPO_URL, None, hostname=machine)
Chris Masoned368cc42012-03-07 15:16:59 -0800548
549
Chris Masone9f13ff22012-03-05 13:45:25 -0800550 def _record_job_if_possible(self, test_name, job):
551 """
552 Record job id as keyval, if possible, so it can be referenced later.
553
554 If |self._results_dir| is None, then this is a NOOP.
Chris Masone5374c672012-03-05 15:11:39 -0800555
556 @param test_name: the test to record id/owner for.
557 @param job: the job object to pull info from.
Chris Masone9f13ff22012-03-05 13:45:25 -0800558 """
559 if self._results_dir:
560 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masone11aae452012-05-21 16:08:39 -0700561 utils.write_keyval(
562 self._results_dir,
563 {hashlib.md5(test_name).hexdigest(): job_id_owner})
Chris Masone9f13ff22012-03-05 13:45:25 -0800564
565
Chris Masone5374c672012-03-05 15:11:39 -0800566 def _count_usable_hosts(self, host_spec):
567 """
568 Given a set of host labels, count the live hosts that have them all.
569
570 @param host_spec: list of labels specifying a set of hosts.
571 @return the number of live hosts that satisfy |host_spec|.
572 """
573 count = 0
574 for h in self._afe.get_hosts(multiple_labels=host_spec):
575 if h.status not in ['Repair Failed', 'Repairing']:
576 count += 1
577 return count
578
579
Chris Masone6fed6462011-10-20 16:36:43 -0700580 def _ensure_version_label(self, name):
581 """
582 Ensure that a label called |name| exists in the autotest DB.
583
584 @param name: the label to check for/create.
585 """
Chris Masone47c9e642012-04-25 14:22:18 -0700586 try:
Chris Masone6fed6462011-10-20 16:36:43 -0700587 self._afe.create_label(name=name)
Chris Masone47c9e642012-04-25 14:22:18 -0700588 except proxy.ValidationError as ve:
589 if ('name' in ve.problem_keys and
590 'This value must be unique' in ve.problem_keys['name']):
591 logging.debug('Version label %s already exists', name)
592 else:
593 raise ve
Chris Masone6fed6462011-10-20 16:36:43 -0700594
595
Chris Masonec43448f2012-05-31 12:55:59 -0700596 def _schedule_reimage_job(self, build, board, pool, num_machines):
Chris Masone6fed6462011-10-20 16:36:43 -0700597 """
598 Schedules the reimaging of |num_machines| |board| devices with |image|.
599
600 Sends an RPC to the autotest frontend to enqueue reimaging jobs on
601 |num_machines| devices of type |board|
602
Chris Masone8abb6fc2012-01-31 09:27:36 -0800603 @param build: the build to install (must be unique).
Chris Masone6fed6462011-10-20 16:36:43 -0700604 @param board: which kind of devices to reimage.
Chris Masonec43448f2012-05-31 12:55:59 -0700605 @param pool: the pool of machines to use for scheduling purposes.
606 @param num_machines: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700607 @return a frontend.Job object for the reimaging job we scheduled.
608 """
Chris Sosadfe70422012-07-25 16:23:55 -0700609 image_url = _image_url_pattern() % (
610 dev_server.DevServer.devserver_url_for_build(build), build)
Chris Masone8b764252012-01-17 11:12:51 -0800611 control_file = inject_vars(
Chris Sosadfe70422012-07-25 16:23:55 -0700612 dict(image_url=image_url, image_name=build),
Chris Masone6fed6462011-10-20 16:36:43 -0700613 self._cf_getter.get_control_file_contents_by_name('autoupdate'))
Scott Zawalski65650172012-02-16 11:48:26 -0500614 job_deps = []
Chris Masonec43448f2012-05-31 12:55:59 -0700615 if pool:
616 meta_host = pool
Chris Masone5374c672012-03-05 15:11:39 -0800617 board_label = board
Scott Zawalski65650172012-02-16 11:48:26 -0500618 job_deps.append(board_label)
619 else:
620 # No pool specified use board.
Chris Masone5374c672012-03-05 15:11:39 -0800621 meta_host = board
Chris Masone6fed6462011-10-20 16:36:43 -0700622
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800623 return self._afe.create_job(control_file=control_file,
Chris Masone8abb6fc2012-01-31 09:27:36 -0800624 name=build + '-try',
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800625 control_type='Server',
Chris Masone97325362012-04-26 16:19:13 -0700626 priority='Low',
Scott Zawalski65650172012-02-16 11:48:26 -0500627 meta_hosts=[meta_host] * num_machines,
628 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700629
630
Chris Masone6fed6462011-10-20 16:36:43 -0700631class Suite(object):
632 """
633 A suite of tests, defined by some predicate over control file variables.
634
635 Given a place to search for control files a predicate to match the desired
636 tests, can gather tests and fire off jobs to run them, and then wait for
637 results.
638
639 @var _predicate: a function that should return True when run over a
640 ControlData representation of a control file that should be in
641 this Suite.
642 @var _tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800643 @var _build: the build on which we're running this suite.
Chris Masone6fed6462011-10-20 16:36:43 -0700644 @var _afe: an instance of AFE as defined in server/frontend.py.
645 @var _tko: an instance of TKO as defined in server/frontend.py.
646 @var _jobs: currently scheduled jobs, if any.
647 @var _cf_getter: a control_file_getter.ControlFileGetter
648 """
649
650
Chris Masonefef21382012-01-17 11:16:32 -0800651 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800652 def create_ds_getter(build):
Chris Masonefef21382012-01-17 11:16:32 -0800653 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800654 @param build: the build on which we're running this suite.
Chris Masonefef21382012-01-17 11:16:32 -0800655 @return a FileSystemGetter instance that looks under |autotest_dir|.
656 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800657 return control_file_getter.DevServerGetter(
658 build, dev_server.DevServer.create())
Chris Masonefef21382012-01-17 11:16:32 -0800659
660
661 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800662 def create_fs_getter(autotest_dir):
663 """
664 @param autotest_dir: the place to find autotests.
665 @return a FileSystemGetter instance that looks under |autotest_dir|.
666 """
667 # currently hard-coded places to look for tests.
668 subpaths = ['server/site_tests', 'client/site_tests',
669 'server/tests', 'client/tests']
670 directories = [os.path.join(autotest_dir, p) for p in subpaths]
671 return control_file_getter.FileSystemGetter(directories)
672
673
674 @staticmethod
Zdenek Behan849db052012-02-29 19:16:28 +0100675 def parse_tag(tag):
676 """Splits a string on ',' optionally surrounded by whitespace."""
677 return map(lambda x: x.strip(), tag.split(','))
678
679
680 @staticmethod
Chris Masone84564792012-02-23 10:52:42 -0800681 def name_in_tag_predicate(name):
682 """Returns predicate that takes a control file and looks for |name|.
683
684 Builds a predicate that takes in a parsed control file (a ControlData)
685 and returns True if the SUITE tag is present and contains |name|.
686
687 @param name: the suite name to base the predicate on.
688 @return a callable that takes a ControlData and looks for |name| in that
689 ControlData object's suite member.
690 """
Zdenek Behan849db052012-02-29 19:16:28 +0100691 return lambda t: hasattr(t, 'suite') and \
692 name in Suite.parse_tag(t.suite)
Chris Masone84564792012-02-23 10:52:42 -0800693
Zdenek Behan849db052012-02-29 19:16:28 +0100694
695 @staticmethod
696 def list_all_suites(build, cf_getter=None):
697 """
698 Parses all ControlData objects with a SUITE tag and extracts all
699 defined suite names.
700
701 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
702 using DevServerGetter.
703
704 @return list of suites
705 """
706 if cf_getter is None:
707 cf_getter = Suite.create_ds_getter(build)
708
709 suites = set()
710 predicate = lambda t: hasattr(t, 'suite')
Scott Zawalskif22b75d2012-05-10 16:54:37 -0400711 for test in Suite.find_and_parse_tests(cf_getter, predicate,
712 add_experimental=True):
Zdenek Behan849db052012-02-29 19:16:28 +0100713 suites.update(Suite.parse_tag(test.suite))
714 return list(suites)
Chris Masone84564792012-02-23 10:52:42 -0800715
716
717 @staticmethod
Scott Zawalski9ece6532012-02-28 14:10:47 -0500718 def create_from_name(name, build, cf_getter=None, afe=None, tko=None,
719 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700720 """
721 Create a Suite using a predicate based on the SUITE control file var.
722
723 Makes a predicate based on |name| and uses it to instantiate a Suite
724 that looks for tests in |autotest_dir| and will schedule them using
Chris Masoned6f38c82012-02-22 14:53:42 -0800725 |afe|. Pulls control files from the default dev server.
726 Results will be pulled from |tko| upon completion.
Chris Masone6fed6462011-10-20 16:36:43 -0700727
728 @param name: a value of the SUITE control file variable to search for.
Chris Masone8b7cd422012-02-22 13:16:11 -0800729 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800730 @param cf_getter: a control_file_getter.ControlFileGetter.
731 If None, default to using a DevServerGetter.
Chris Masone6fed6462011-10-20 16:36:43 -0700732 @param afe: an instance of AFE as defined in server/frontend.py.
733 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500734 @param pool: Specify the pool of machines to use for scheduling
Chris Masoned6f38c82012-02-22 14:53:42 -0800735 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500736 @param results_dir: The directory where the job can write results to.
737 This must be set if you want job_id of sub-jobs
738 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700739 @return a Suite instance.
740 """
Chris Masoned6f38c82012-02-22 14:53:42 -0800741 if cf_getter is None:
742 cf_getter = Suite.create_ds_getter(build)
Chris Masone84564792012-02-23 10:52:42 -0800743 return Suite(Suite.name_in_tag_predicate(name),
Scott Zawalski9ece6532012-02-28 14:10:47 -0500744 name, build, cf_getter, afe, tko, pool, results_dir)
Chris Masone6fed6462011-10-20 16:36:43 -0700745
746
Chris Masoned6f38c82012-02-22 14:53:42 -0800747 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Scott Zawalski9ece6532012-02-28 14:10:47 -0500748 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700749 """
750 Constructor
751
752 @param predicate: a function that should return True when run over a
753 ControlData representation of a control file that should be in
754 this Suite.
755 @param tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800756 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800757 @param cf_getter: a control_file_getter.ControlFileGetter
Chris Masone6fed6462011-10-20 16:36:43 -0700758 @param afe: an instance of AFE as defined in server/frontend.py.
759 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500760 @param pool: Specify the pool of machines to use for scheduling
761 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500762 @param results_dir: The directory where the job can write results to.
763 This must be set if you want job_id of sub-jobs
764 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700765 """
766 self._predicate = predicate
767 self._tag = tag
Chris Masone8b7cd422012-02-22 13:16:11 -0800768 self._build = build
Chris Masoned6f38c82012-02-22 14:53:42 -0800769 self._cf_getter = cf_getter
Scott Zawalski9ece6532012-02-28 14:10:47 -0500770 self._results_dir = results_dir
Chris Masone8ac66712012-02-15 14:21:02 -0800771 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
772 delay_sec=10,
773 debug=False)
774 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
775 delay_sec=10,
776 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500777 self._pool = pool
Chris Masone6fed6462011-10-20 16:36:43 -0700778 self._jobs = []
Chris Masone6fed6462011-10-20 16:36:43 -0700779 self._tests = Suite.find_and_parse_tests(self._cf_getter,
780 self._predicate,
781 add_experimental=True)
782
783
784 @property
785 def tests(self):
786 """
787 A list of ControlData objects in the suite, with added |text| attr.
788 """
789 return self._tests
790
791
792 def stable_tests(self):
793 """
794 |self.tests|, filtered for non-experimental tests.
795 """
796 return filter(lambda t: not t.experimental, self.tests)
797
798
799 def unstable_tests(self):
800 """
801 |self.tests|, filtered for experimental tests.
802 """
803 return filter(lambda t: t.experimental, self.tests)
804
805
Chris Masone8b7cd422012-02-22 13:16:11 -0800806 def _create_job(self, test):
Chris Masone6fed6462011-10-20 16:36:43 -0700807 """
808 Thin wrapper around frontend.AFE.create_job().
809
810 @param test: ControlData object for a test to run.
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500811 @return a frontend.Job object with an added test_name member.
812 test_name is used to preserve the higher level TEST_NAME
813 name of the job.
Chris Masone6fed6462011-10-20 16:36:43 -0700814 """
Chris Masonec43448f2012-05-31 12:55:59 -0700815 job_deps = [] # TODO(cmasone): init from test.dependencies.
Scott Zawalski65650172012-02-16 11:48:26 -0500816 if self._pool:
Chris Masone5374c672012-03-05 15:11:39 -0800817 meta_hosts = self._pool
Chris Masone8b7cd422012-02-22 13:16:11 -0800818 cros_label = VERSION_PREFIX + self._build
Scott Zawalski65650172012-02-16 11:48:26 -0500819 job_deps.append(cros_label)
820 else:
821 # No pool specified use any machines with the following label.
Chris Masone8b7cd422012-02-22 13:16:11 -0800822 meta_hosts = VERSION_PREFIX + self._build
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500823 test_obj = self._afe.create_job(
Chris Masone6fed6462011-10-20 16:36:43 -0700824 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800825 name='/'.join([self._build, self._tag, test.name]),
Chris Masone6fed6462011-10-20 16:36:43 -0700826 control_type=test.test_type.capitalize(),
Scott Zawalski65650172012-02-16 11:48:26 -0500827 meta_hosts=[meta_hosts],
Chris Masonebafbbb02012-05-16 13:41:36 -0700828 dependencies=job_deps,
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700829 keyvals={JOB_BUILD_KEY: self._build, JOB_SUITE_KEY: self._tag})
Chris Masone6fed6462011-10-20 16:36:43 -0700830
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500831 setattr(test_obj, 'test_name', test.name)
832
833 return test_obj
834
Chris Masone6fed6462011-10-20 16:36:43 -0700835
Chris Masone275ec902012-07-10 15:28:34 -0700836 def run_and_wait(self, record, manager, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700837 """
838 Synchronously run tests in |self.tests|.
839
Chris Masone8b7cd422012-02-22 13:16:11 -0800840 Schedules tests against a device running image |self._build|, and
Chris Masone6fed6462011-10-20 16:36:43 -0700841 then polls for status, using |record| to print status when each
842 completes.
843
844 Tests returned by self.stable_tests() will always be run, while tests
845 in self.unstable_tests() will only be run if |add_experimental| is true.
846
Chris Masone6fed6462011-10-20 16:36:43 -0700847 @param record: callable that records job status.
848 prototype:
Chris Masone604baf32012-06-28 08:45:30 -0700849 record(base_job.status_log_entry)
Chris Masone6fed6462011-10-20 16:36:43 -0700850 @param add_experimental: schedule experimental tests as well, or not.
851 """
Chris Masoneed356392012-05-08 14:07:13 -0700852 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
853 logging.debug('Discovered %d unstable tests.',
854 len(self.unstable_tests()))
Chris Masone6fed6462011-10-20 16:36:43 -0700855 try:
Chris Masone604baf32012-06-28 08:45:30 -0700856 Status('INFO', 'Start %s' % self._tag).record_result(record)
Chris Masone8b7cd422012-02-22 13:16:11 -0800857 self.schedule(add_experimental)
Chris Masone275ec902012-07-10 15:28:34 -0700858 # Unlock all hosts, so test jobs can be run on them.
859 manager.unlock()
Chris Masone6fed6462011-10-20 16:36:43 -0700860 try:
Chris Masone8d6e6412012-06-28 11:20:56 -0700861 for result in job_status.wait_for_results(self._afe,
862 self._tko,
863 self._jobs):
Chris Masone604baf32012-06-28 08:45:30 -0700864 result.record_all(record)
865
Chris Masone6fed6462011-10-20 16:36:43 -0700866 except Exception as e:
Chris Masone99378582012-04-30 13:10:58 -0700867 logging.error(traceback.format_exc())
Chris Masone604baf32012-06-28 08:45:30 -0700868 Status('FAIL', self._tag,
Chris Masone99378582012-04-30 13:10:58 -0700869 'Exception waiting for results').record_result(record)
Chris Masone6fed6462011-10-20 16:36:43 -0700870 except Exception as e:
Chris Masone99378582012-04-30 13:10:58 -0700871 logging.error(traceback.format_exc())
Chris Masone604baf32012-06-28 08:45:30 -0700872 Status('FAIL', self._tag,
Chris Masone99378582012-04-30 13:10:58 -0700873 'Exception while scheduling suite').record_result(record)
Chris Masoneed356392012-05-08 14:07:13 -0700874 # Sanity check
875 tests_at_end = self.find_and_parse_tests(self._cf_getter,
876 self._predicate,
877 add_experimental=True)
878 if len(self.tests) != len(tests_at_end):
879 msg = 'Dev Server enumerated %d tests at start, %d at end.' % (
880 len(self.tests), len(tests_at_end))
Chris Masone604baf32012-06-28 08:45:30 -0700881 Status('FAIL', self._tag, msg).record_result(record)
Chris Masone6fed6462011-10-20 16:36:43 -0700882
883
Chris Masone8b7cd422012-02-22 13:16:11 -0800884 def schedule(self, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700885 """
886 Schedule jobs using |self._afe|.
887
888 frontend.Job objects representing each scheduled job will be put in
889 |self._jobs|.
890
Chris Masone6fed6462011-10-20 16:36:43 -0700891 @param add_experimental: schedule experimental tests as well, or not.
892 """
893 for test in self.stable_tests():
894 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800895 self._jobs.append(self._create_job(test))
Chris Masone6fed6462011-10-20 16:36:43 -0700896
897 if add_experimental:
Chris Masone6fed6462011-10-20 16:36:43 -0700898 for test in self.unstable_tests():
Zdenek Behan150fbd62012-04-06 17:20:01 +0200899 logging.debug('Scheduling experimental %s', test.name)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700900 test.name = EXPERIMENTAL_PREFIX + test.name
Chris Masone8b7cd422012-02-22 13:16:11 -0800901 self._jobs.append(self._create_job(test))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500902 if self._results_dir:
903 self._record_scheduled_jobs()
904
905
906 def _record_scheduled_jobs(self):
907 """
908 Record scheduled job ids as keyvals, so they can be referenced later.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500909 """
910 for job in self._jobs:
911 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masone11aae452012-05-21 16:08:39 -0700912 utils.write_keyval(
913 self._results_dir,
914 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
Chris Masone6fed6462011-10-20 16:36:43 -0700915
916
Chris Masonefef21382012-01-17 11:16:32 -0800917 @staticmethod
918 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
Chris Masone6fed6462011-10-20 16:36:43 -0700919 """
920 Function to scan through all tests and find eligible tests.
921
922 Looks at control files returned by _cf_getter.get_control_file_list()
923 for tests that pass self._predicate().
924
925 @param cf_getter: a control_file_getter.ControlFileGetter used to list
926 and fetch the content of control files
927 @param predicate: a function that should return True when run over a
928 ControlData representation of a control file that should be in
929 this Suite.
930 @param add_experimental: add tests with experimental attribute set.
931
932 @return list of ControlData objects that should be run, with control
933 file text added in |text| attribute.
934 """
935 tests = {}
936 files = cf_getter.get_control_file_list()
Chris Masone75a20612012-05-08 12:37:31 -0700937 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
938 for file in filter(lambda f: not matcher.match(f), files):
Chris Masoneed356392012-05-08 14:07:13 -0700939 logging.debug('Considering %s', file)
Chris Masone6fed6462011-10-20 16:36:43 -0700940 text = cf_getter.get_control_file_contents(file)
941 try:
Chris Masoneed356392012-05-08 14:07:13 -0700942 found_test = control_data.parse_control_string(
943 text, raise_warnings=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700944 if not add_experimental and found_test.experimental:
945 continue
946
947 found_test.text = text
Chris Masonee8a4eff2012-02-28 16:33:43 -0800948 found_test.path = file
Chris Masone6fed6462011-10-20 16:36:43 -0700949 tests[file] = found_test
950 except control_data.ControlVariableException, e:
951 logging.warn("Skipping %s\n%s", file, e)
952 except Exception, e:
953 logging.error("Bad %s\n%s", file, e)
954
955 return [test for test in tests.itervalues() if predicate(test)]