blob: fdf4d8aeb7b1e815713ab6a74f6687bc25e4d615 [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 Masone8d6e6412012-06-28 11:20:56 -070011from autotest_lib.server.cros import 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 Masonec43448f2012-05-31 12:55:59 -0700273 reimager = Reimager(job.autodir, results_dir=job.resultdir)
Chris Masoned368cc42012-03-07 15:16:59 -0800274
Chris Masone604baf32012-06-28 08:45:30 -0700275 if skip_reimage or reimager.attempt(build, board, pool, job.record_entry,
Chris Masonec43448f2012-05-31 12:55:59 -0700276 check_hosts, num=num):
Chris Sosa6b288c82012-03-29 15:31:06 -0700277 # Ensure that the image's artifacts have completed downloading.
Chris Masonef70650c2012-05-16 08:52:12 -0700278 try:
279 ds = dev_server.DevServer.create()
280 ds.finish_download(build)
281 except dev_server.DevServerException as e:
282 raise error.AsynchronousBuildFailure(e)
283
Chris Masone8d6e6412012-06-28 11:20:56 -0700284 timestamp = datetime.datetime.now().strftime(job_status.TIME_FMT)
Chris Masonea8066a92012-05-01 16:52:31 -0700285 utils.write_keyval(job.resultdir,
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700286 {ARTIFACT_FINISHED_TIME: timestamp})
Chris Sosa6b288c82012-03-29 15:31:06 -0700287
Chris Masoneab3e7332012-02-29 18:54:58 -0800288 suite = Suite.create_from_name(name, build, pool=pool,
289 results_dir=job.resultdir)
Chris Masone99378582012-04-30 13:10:58 -0700290 suite.run_and_wait(job.record_entry, add_experimental=add_experimental)
Chris Masoneab3e7332012-02-29 18:54:58 -0800291
Chris Masoned368cc42012-03-07 15:16:59 -0800292 reimager.clear_reimaged_host_state(build)
293
Chris Masoneab3e7332012-02-29 18:54:58 -0800294
295def _vet_reimage_and_run_args(build=None, board=None, name=None, job=None,
Chris Masone62579122012-03-08 15:18:43 -0800296 pool=None, num=None, check_hosts=True,
297 skip_reimage=False, add_experimental=True,
298 **dargs):
Chris Masoneab3e7332012-02-29 18:54:58 -0800299 """
300 Vets arguments for reimage_and_run().
301
302 Currently required args:
303 @param build: the build to install e.g.
304 x86-alex-release/R18-1655.0.0-a1-b1584.
305 @param board: which kind of devices to reimage.
306 @param name: a value of the SUITE control file variable to search for.
307 @param job: an instance of client.common_lib.base_job representing the
308 currently running suite job.
309
310 Currently supported optional args:
311 @param pool: specify the pool of machines to use for scheduling purposes.
312 Default: None
313 @param num: how many devices to reimage.
314 Default in global_config
Chris Masone62579122012-03-08 15:18:43 -0800315 @param check_hosts: require appropriate hosts to be available now.
Chris Masoneab3e7332012-02-29 18:54:58 -0800316 @param skip_reimage: skip reimaging, used for testing purposes.
317 Default: False
318 @param add_experimental: schedule experimental tests as well, or not.
319 Default: True
320 @return a tuple of args set to provided (or default) values.
321 """
322 required_keywords = {'build': str,
323 'board': str,
324 'name': str,
325 'job': base_job.base_job}
326 for key, expected in required_keywords.iteritems():
327 value = locals().get(key)
328 if not value or not isinstance(value, expected):
Chris Masonef8b53062012-05-08 22:14:18 -0700329 raise error.SuiteArgumentException(
330 "reimage_and_run() needs %s=<%r>" % (key, expected))
Chris Masone62579122012-03-08 15:18:43 -0800331 return (build, board, name, job, pool, num, check_hosts, skip_reimage,
332 add_experimental)
Chris Masoneab3e7332012-02-29 18:54:58 -0800333
334
Chris Masone8b764252012-01-17 11:12:51 -0800335def inject_vars(vars, control_file_in):
336 """
Chris Masoneab3e7332012-02-29 18:54:58 -0800337 Inject the contents of |vars| into |control_file_in|.
Chris Masone8b764252012-01-17 11:12:51 -0800338
339 @param vars: a dict to shoehorn into the provided control file string.
340 @param control_file_in: the contents of a control file to munge.
341 @return the modified control file string.
342 """
343 control_file = ''
344 for key, value in vars.iteritems():
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800345 # None gets injected as 'None' without this check; same for digits.
346 if isinstance(value, str):
347 control_file += "%s='%s'\n" % (key, value)
348 else:
349 control_file += "%s=%r\n" % (key, value)
Chris Masone8b764252012-01-17 11:12:51 -0800350 return control_file + control_file_in
351
352
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800353def _image_url_pattern():
354 return CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
355
356
357def _package_url_pattern():
358 return CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
359
Chris Masone6fed6462011-10-20 16:36:43 -0700360
Chris Masoneab3e7332012-02-29 18:54:58 -0800361def skip_reimage(g):
362 return g.get('SKIP_IMAGE')
363
364
Chris Masone6fed6462011-10-20 16:36:43 -0700365class Reimager(object):
366 """
367 A class that can run jobs to reimage devices.
368
369 @var _afe: a frontend.AFE instance used to talk to autotest.
370 @var _tko: a frontend.TKO instance used to query the autotest results db.
371 @var _cf_getter: a ControlFileGetter used to get the AU control file.
372 """
373
374
Chris Masonec43448f2012-05-31 12:55:59 -0700375 def __init__(self, autotest_dir, afe=None, tko=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700376 """
377 Constructor
378
379 @param autotest_dir: the place to find autotests.
380 @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.
Chris Masone9f13ff22012-03-05 13:45:25 -0800382 @param results_dir: The directory where the job can write results to.
383 This must be set if you want job_id of sub-jobs
384 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700385 """
Chris Masone8ac66712012-02-15 14:21:02 -0800386 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
387 delay_sec=10,
388 debug=False)
389 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
390 delay_sec=10,
391 debug=False)
Chris Masone9f13ff22012-03-05 13:45:25 -0800392 self._results_dir = results_dir
Chris Masoned368cc42012-03-07 15:16:59 -0800393 self._reimaged_hosts = {}
Chris Masone6fed6462011-10-20 16:36:43 -0700394 self._cf_getter = control_file_getter.FileSystemGetter(
395 [os.path.join(autotest_dir, 'server/site_tests')])
396
397
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800398 def skip(self, g):
Chris Masoneab3e7332012-02-29 18:54:58 -0800399 """Deprecated in favor of dynamic_suite.skip_reimage()."""
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800400 return 'SKIP_IMAGE' in g and g['SKIP_IMAGE']
401
402
Chris Masonec43448f2012-05-31 12:55:59 -0700403 def attempt(self, build, board, pool, record, check_hosts, num=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700404 """
405 Synchronously attempt to reimage some machines.
406
407 Fire off attempts to reimage |num| machines of type |board|, using an
Chris Masone8abb6fc2012-01-31 09:27:36 -0800408 image at |url| called |build|. Wait for completion, polling every
Chris Masone6fed6462011-10-20 16:36:43 -0700409 10s, and log results with |record| upon completion.
410
Chris Masone8abb6fc2012-01-31 09:27:36 -0800411 @param build: the build to install e.g.
412 x86-alex-release/R18-1655.0.0-a1-b1584.
Chris Masone6fed6462011-10-20 16:36:43 -0700413 @param board: which kind of devices to reimage.
Chris Masonec43448f2012-05-31 12:55:59 -0700414 @param pool: Specify the pool of machines to use for scheduling
415 purposes.
Chris Masone6fed6462011-10-20 16:36:43 -0700416 @param record: callable that records job status.
Chris Masone604baf32012-06-28 08:45:30 -0700417 prototype:
418 record(base_job.status_log_entry)
Chris Masone62579122012-03-08 15:18:43 -0800419 @param check_hosts: require appropriate hosts to be available now.
Chris Masone5552dd72012-02-15 15:01:04 -0800420 @param num: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700421 @return True if all reimaging jobs succeed, false otherwise.
422 """
Chris Masone5552dd72012-02-15 15:01:04 -0800423 if not num:
424 num = CONFIG.get_config_value('CROS', 'sharding_factor', type=int)
Scott Zawalski65650172012-02-16 11:48:26 -0500425 logging.debug("scheduling reimaging across %d machines", num)
Chris Masone604baf32012-06-28 08:45:30 -0700426 begin_time_str = datetime.datetime.now().strftime(job_status.TIME_FMT)
Chris Masone796fcf12012-02-22 16:53:31 -0800427 try:
Chris Masone62579122012-03-08 15:18:43 -0800428 self._ensure_version_label(VERSION_PREFIX + build)
429
430 if check_hosts:
Chris Masonec43448f2012-05-31 12:55:59 -0700431 # TODO make DEPENDENCIES-aware
432 self._ensure_enough_hosts(board, pool, num)
Chris Masone5374c672012-03-05 15:11:39 -0800433
Chris Masoned368cc42012-03-07 15:16:59 -0800434 # Schedule job and record job metadata.
Chris Masonec43448f2012-05-31 12:55:59 -0700435 # TODO make DEPENDENCIES-aware
436 canary_job = self._schedule_reimage_job(build, board, pool, num)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700437 self._record_job_if_possible(REIMAGE_JOB_NAME, canary_job)
Chris Masoned368cc42012-03-07 15:16:59 -0800438 logging.debug('Created re-imaging job: %d', canary_job.id)
439
440 # Poll until reimaging is complete.
441 self._wait_for_job_to_start(canary_job.id)
442 self._wait_for_job_to_finish(canary_job.id)
443
444 # Gather job results.
445 canary_job.result = self._afe.poll_job_results(self._tko,
446 canary_job,
447 0)
Chris Masonef8b53062012-05-08 22:14:18 -0700448 except error.InadequateHostsException as e:
Chris Masone5374c672012-03-05 15:11:39 -0800449 logging.warning(e)
Chris Masone604baf32012-06-28 08:45:30 -0700450 Status('WARN', REIMAGE_JOB_NAME, str(e),
451 begin_time_str=begin_time_str).record_all(record)
Chris Masone5374c672012-03-05 15:11:39 -0800452 return False
Chris Masone796fcf12012-02-22 16:53:31 -0800453 except Exception as e:
454 # catch Exception so we record the job as terminated no matter what.
455 logging.error(e)
Chris Masone604baf32012-06-28 08:45:30 -0700456 Status('ERROR', REIMAGE_JOB_NAME, str(e),
457 begin_time_str=begin_time_str).record_all(record)
Chris Masone796fcf12012-02-22 16:53:31 -0800458 return False
Chris Masone6fed6462011-10-20 16:36:43 -0700459
Chris Masoned368cc42012-03-07 15:16:59 -0800460 self._remember_reimaged_hosts(build, canary_job)
461
462 if canary_job.result is True:
463 self._report_results(canary_job, record)
Chris Masone6fed6462011-10-20 16:36:43 -0700464 return True
465
Chris Masoned368cc42012-03-07 15:16:59 -0800466 if canary_job.result is None:
Chris Masone604baf32012-06-28 08:45:30 -0700467 Status('FAIL', canary_job.name,
468 'reimaging tasks did not run',
469 begin_time_str=begin_time_str).record_all(record)
470
Chris Masoned368cc42012-03-07 15:16:59 -0800471 else: # canary_job.result is False
472 self._report_results(canary_job, record)
Chris Masone6fed6462011-10-20 16:36:43 -0700473
Chris Masone6fed6462011-10-20 16:36:43 -0700474 return False
475
476
Chris Masone62579122012-03-08 15:18:43 -0800477 def _ensure_enough_hosts(self, board, pool, num):
478 """
479 Determine if there are enough working hosts to run on.
480
481 Raises exception if there are not enough hosts.
482
483 @param board: which kind of devices to reimage.
484 @param pool: the pool of machines to use for scheduling purposes.
485 @param num: how many devices to reimage.
Chris Masonef8b53062012-05-08 22:14:18 -0700486 @raises NoHostsException: if no working hosts.
Chris Masone62579122012-03-08 15:18:43 -0800487 @raises InadequateHostsException: if too few working hosts.
488 """
489 labels = [l for l in [board, pool] if l is not None]
Chris Masone502b71e2012-04-10 10:41:35 -0700490 available = self._count_usable_hosts(labels)
491 if available == 0:
Chris Masonef8b53062012-05-08 22:14:18 -0700492 raise error.NoHostsException('All hosts with %r are dead!' % labels)
Chris Masone502b71e2012-04-10 10:41:35 -0700493 elif num > available:
Chris Masonef8b53062012-05-08 22:14:18 -0700494 raise error.InadequateHostsException(
495 'Too few hosts with %r' % labels)
Chris Masone62579122012-03-08 15:18:43 -0800496
497
Chris Masoned368cc42012-03-07 15:16:59 -0800498 def _wait_for_job_to_start(self, job_id):
499 """
500 Wait for the job specified by |job_id| to start.
501
502 @param job_id: the job ID to poll on.
503 """
504 while len(self._afe.get_jobs(id=job_id, not_yet_run=True)) > 0:
505 time.sleep(10)
506 logging.debug('Re-imaging job running.')
507
508
509 def _wait_for_job_to_finish(self, job_id):
510 """
511 Wait for the job specified by |job_id| to finish.
512
513 @param job_id: the job ID to poll on.
514 """
515 while len(self._afe.get_jobs(id=job_id, finished=True)) == 0:
516 time.sleep(10)
517 logging.debug('Re-imaging job finished.')
518
519
520 def _remember_reimaged_hosts(self, build, canary_job):
521 """
522 Remember hosts that were reimaged with |build| as a part |canary_job|.
523
524 @param build: the build that was installed e.g.
525 x86-alex-release/R18-1655.0.0-a1-b1584.
526 @param canary_job: a completed frontend.Job object, possibly populated
527 by frontend.AFE.poll_job_results.
528 """
529 if not hasattr(canary_job, 'results_platform_map'):
530 return
531 if not self._reimaged_hosts.get('build'):
532 self._reimaged_hosts[build] = []
533 for platform in canary_job.results_platform_map:
534 for host in canary_job.results_platform_map[platform]['Total']:
535 self._reimaged_hosts[build].append(host)
536
537
538 def clear_reimaged_host_state(self, build):
539 """
540 Clear per-host state created in the autotest DB for this job.
541
542 After reimaging a host, we label it and set some host attributes on it
543 that are then used by the suite scheduling code. This call cleans
544 that up.
545
546 @param build: the build whose hosts we want to clean up e.g.
547 x86-alex-release/R18-1655.0.0-a1-b1584.
548 """
Chris Masoned368cc42012-03-07 15:16:59 -0800549 for host in self._reimaged_hosts.get('build', []):
550 self._clear_build_state(host)
551
552
553 def _clear_build_state(self, machine):
554 """
555 Clear all build-specific labels, attributes from the target.
556
557 @param machine: the host to clear labels, attributes from.
558 """
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700559 self._afe.set_host_attribute(JOB_REPO_URL, None, hostname=machine)
Chris Masoned368cc42012-03-07 15:16:59 -0800560
561
Chris Masone9f13ff22012-03-05 13:45:25 -0800562 def _record_job_if_possible(self, test_name, job):
563 """
564 Record job id as keyval, if possible, so it can be referenced later.
565
566 If |self._results_dir| is None, then this is a NOOP.
Chris Masone5374c672012-03-05 15:11:39 -0800567
568 @param test_name: the test to record id/owner for.
569 @param job: the job object to pull info from.
Chris Masone9f13ff22012-03-05 13:45:25 -0800570 """
571 if self._results_dir:
572 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masone11aae452012-05-21 16:08:39 -0700573 utils.write_keyval(
574 self._results_dir,
575 {hashlib.md5(test_name).hexdigest(): job_id_owner})
Chris Masone9f13ff22012-03-05 13:45:25 -0800576
577
Chris Masone5374c672012-03-05 15:11:39 -0800578 def _count_usable_hosts(self, host_spec):
579 """
580 Given a set of host labels, count the live hosts that have them all.
581
582 @param host_spec: list of labels specifying a set of hosts.
583 @return the number of live hosts that satisfy |host_spec|.
584 """
585 count = 0
586 for h in self._afe.get_hosts(multiple_labels=host_spec):
587 if h.status not in ['Repair Failed', 'Repairing']:
588 count += 1
589 return count
590
591
Chris Masone6fed6462011-10-20 16:36:43 -0700592 def _ensure_version_label(self, name):
593 """
594 Ensure that a label called |name| exists in the autotest DB.
595
596 @param name: the label to check for/create.
597 """
Chris Masone47c9e642012-04-25 14:22:18 -0700598 try:
Chris Masone6fed6462011-10-20 16:36:43 -0700599 self._afe.create_label(name=name)
Chris Masone47c9e642012-04-25 14:22:18 -0700600 except proxy.ValidationError as ve:
601 if ('name' in ve.problem_keys and
602 'This value must be unique' in ve.problem_keys['name']):
603 logging.debug('Version label %s already exists', name)
604 else:
605 raise ve
Chris Masone6fed6462011-10-20 16:36:43 -0700606
607
Chris Masonec43448f2012-05-31 12:55:59 -0700608 def _schedule_reimage_job(self, build, board, pool, num_machines):
Chris Masone6fed6462011-10-20 16:36:43 -0700609 """
610 Schedules the reimaging of |num_machines| |board| devices with |image|.
611
612 Sends an RPC to the autotest frontend to enqueue reimaging jobs on
613 |num_machines| devices of type |board|
614
Chris Masone8abb6fc2012-01-31 09:27:36 -0800615 @param build: the build to install (must be unique).
Chris Masone6fed6462011-10-20 16:36:43 -0700616 @param board: which kind of devices to reimage.
Chris Masonec43448f2012-05-31 12:55:59 -0700617 @param pool: the pool of machines to use for scheduling purposes.
618 @param num_machines: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700619 @return a frontend.Job object for the reimaging job we scheduled.
620 """
Chris Masone8b764252012-01-17 11:12:51 -0800621 control_file = inject_vars(
Chris Masone8abb6fc2012-01-31 09:27:36 -0800622 {'image_url': _image_url_pattern() % build, 'image_name': build},
Chris Masone6fed6462011-10-20 16:36:43 -0700623 self._cf_getter.get_control_file_contents_by_name('autoupdate'))
Scott Zawalski65650172012-02-16 11:48:26 -0500624 job_deps = []
Chris Masonec43448f2012-05-31 12:55:59 -0700625 if pool:
626 meta_host = pool
Chris Masone5374c672012-03-05 15:11:39 -0800627 board_label = board
Scott Zawalski65650172012-02-16 11:48:26 -0500628 job_deps.append(board_label)
629 else:
630 # No pool specified use board.
Chris Masone5374c672012-03-05 15:11:39 -0800631 meta_host = board
Chris Masone6fed6462011-10-20 16:36:43 -0700632
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800633 return self._afe.create_job(control_file=control_file,
Chris Masone8abb6fc2012-01-31 09:27:36 -0800634 name=build + '-try',
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800635 control_type='Server',
Chris Masone97325362012-04-26 16:19:13 -0700636 priority='Low',
Scott Zawalski65650172012-02-16 11:48:26 -0500637 meta_hosts=[meta_host] * num_machines,
638 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700639
640
641 def _report_results(self, job, record):
642 """
643 Record results from a completed frontend.Job object.
644
645 @param job: a completed frontend.Job object populated by
646 frontend.AFE.poll_job_results.
647 @param record: callable that records job status.
648 prototype:
Chris Masone604baf32012-06-28 08:45:30 -0700649 record(base_job.status_log_entry)
Chris Masone6fed6462011-10-20 16:36:43 -0700650 """
Chris Masone604baf32012-06-28 08:45:30 -0700651 status_map = {'Failed': 'FAIL', 'Aborted': 'ABORT', 'Completed': 'GOOD'}
Chris Masone6fed6462011-10-20 16:36:43 -0700652 for platform in job.results_platform_map:
653 for status in job.results_platform_map[platform]:
654 if status == 'Total':
655 continue
656 for host in job.results_platform_map[platform][status]:
657 if host not in job.test_status:
Chris Masone604baf32012-06-28 08:45:30 -0700658 Status('ERROR', host,
659 'Job failed to run.').record_all(record)
660
661 elif status in status_map:
662 for test_status in (job.test_status[host].fail +
663 job.test_status[host].good):
664 result = Status(status_map[status],
665 '%s-%s' % (REIMAGE_JOB_NAME, host),
666 test_status.reason,
667 test_status.test_started_time,
668 test_status.test_finished_time)
669 result.record_all(record)
670 else:
671 logging.error('Unknown status ' + status)
Chris Masone6fed6462011-10-20 16:36:43 -0700672
673
674class Suite(object):
675 """
676 A suite of tests, defined by some predicate over control file variables.
677
678 Given a place to search for control files a predicate to match the desired
679 tests, can gather tests and fire off jobs to run them, and then wait for
680 results.
681
682 @var _predicate: a function that should return True when run over a
683 ControlData representation of a control file that should be in
684 this Suite.
685 @var _tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800686 @var _build: the build on which we're running this suite.
Chris Masone6fed6462011-10-20 16:36:43 -0700687 @var _afe: an instance of AFE as defined in server/frontend.py.
688 @var _tko: an instance of TKO as defined in server/frontend.py.
689 @var _jobs: currently scheduled jobs, if any.
690 @var _cf_getter: a control_file_getter.ControlFileGetter
691 """
692
693
Chris Masonefef21382012-01-17 11:16:32 -0800694 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800695 def create_ds_getter(build):
Chris Masonefef21382012-01-17 11:16:32 -0800696 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800697 @param build: the build on which we're running this suite.
Chris Masonefef21382012-01-17 11:16:32 -0800698 @return a FileSystemGetter instance that looks under |autotest_dir|.
699 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800700 return control_file_getter.DevServerGetter(
701 build, dev_server.DevServer.create())
Chris Masonefef21382012-01-17 11:16:32 -0800702
703
704 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800705 def create_fs_getter(autotest_dir):
706 """
707 @param autotest_dir: the place to find autotests.
708 @return a FileSystemGetter instance that looks under |autotest_dir|.
709 """
710 # currently hard-coded places to look for tests.
711 subpaths = ['server/site_tests', 'client/site_tests',
712 'server/tests', 'client/tests']
713 directories = [os.path.join(autotest_dir, p) for p in subpaths]
714 return control_file_getter.FileSystemGetter(directories)
715
716
717 @staticmethod
Zdenek Behan849db052012-02-29 19:16:28 +0100718 def parse_tag(tag):
719 """Splits a string on ',' optionally surrounded by whitespace."""
720 return map(lambda x: x.strip(), tag.split(','))
721
722
723 @staticmethod
Chris Masone84564792012-02-23 10:52:42 -0800724 def name_in_tag_predicate(name):
725 """Returns predicate that takes a control file and looks for |name|.
726
727 Builds a predicate that takes in a parsed control file (a ControlData)
728 and returns True if the SUITE tag is present and contains |name|.
729
730 @param name: the suite name to base the predicate on.
731 @return a callable that takes a ControlData and looks for |name| in that
732 ControlData object's suite member.
733 """
Zdenek Behan849db052012-02-29 19:16:28 +0100734 return lambda t: hasattr(t, 'suite') and \
735 name in Suite.parse_tag(t.suite)
Chris Masone84564792012-02-23 10:52:42 -0800736
Zdenek Behan849db052012-02-29 19:16:28 +0100737
738 @staticmethod
739 def list_all_suites(build, cf_getter=None):
740 """
741 Parses all ControlData objects with a SUITE tag and extracts all
742 defined suite names.
743
744 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
745 using DevServerGetter.
746
747 @return list of suites
748 """
749 if cf_getter is None:
750 cf_getter = Suite.create_ds_getter(build)
751
752 suites = set()
753 predicate = lambda t: hasattr(t, 'suite')
Scott Zawalskif22b75d2012-05-10 16:54:37 -0400754 for test in Suite.find_and_parse_tests(cf_getter, predicate,
755 add_experimental=True):
Zdenek Behan849db052012-02-29 19:16:28 +0100756 suites.update(Suite.parse_tag(test.suite))
757 return list(suites)
Chris Masone84564792012-02-23 10:52:42 -0800758
759
760 @staticmethod
Scott Zawalski9ece6532012-02-28 14:10:47 -0500761 def create_from_name(name, build, cf_getter=None, afe=None, tko=None,
762 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700763 """
764 Create a Suite using a predicate based on the SUITE control file var.
765
766 Makes a predicate based on |name| and uses it to instantiate a Suite
767 that looks for tests in |autotest_dir| and will schedule them using
Chris Masoned6f38c82012-02-22 14:53:42 -0800768 |afe|. Pulls control files from the default dev server.
769 Results will be pulled from |tko| upon completion.
Chris Masone6fed6462011-10-20 16:36:43 -0700770
771 @param name: a value of the SUITE control file variable to search for.
Chris Masone8b7cd422012-02-22 13:16:11 -0800772 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800773 @param cf_getter: a control_file_getter.ControlFileGetter.
774 If None, default to using a DevServerGetter.
Chris Masone6fed6462011-10-20 16:36:43 -0700775 @param afe: an instance of AFE as defined in server/frontend.py.
776 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500777 @param pool: Specify the pool of machines to use for scheduling
Chris Masoned6f38c82012-02-22 14:53:42 -0800778 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500779 @param results_dir: The directory where the job can write results to.
780 This must be set if you want job_id of sub-jobs
781 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700782 @return a Suite instance.
783 """
Chris Masoned6f38c82012-02-22 14:53:42 -0800784 if cf_getter is None:
785 cf_getter = Suite.create_ds_getter(build)
Chris Masone84564792012-02-23 10:52:42 -0800786 return Suite(Suite.name_in_tag_predicate(name),
Scott Zawalski9ece6532012-02-28 14:10:47 -0500787 name, build, cf_getter, afe, tko, pool, results_dir)
Chris Masone6fed6462011-10-20 16:36:43 -0700788
789
Chris Masoned6f38c82012-02-22 14:53:42 -0800790 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Scott Zawalski9ece6532012-02-28 14:10:47 -0500791 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700792 """
793 Constructor
794
795 @param predicate: a function that should return True when run over a
796 ControlData representation of a control file that should be in
797 this Suite.
798 @param tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800799 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800800 @param cf_getter: a control_file_getter.ControlFileGetter
Chris Masone6fed6462011-10-20 16:36:43 -0700801 @param afe: an instance of AFE as defined in server/frontend.py.
802 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500803 @param pool: Specify the pool of machines to use for scheduling
804 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500805 @param results_dir: The directory where the job can write results to.
806 This must be set if you want job_id of sub-jobs
807 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700808 """
809 self._predicate = predicate
810 self._tag = tag
Chris Masone8b7cd422012-02-22 13:16:11 -0800811 self._build = build
Chris Masoned6f38c82012-02-22 14:53:42 -0800812 self._cf_getter = cf_getter
Scott Zawalski9ece6532012-02-28 14:10:47 -0500813 self._results_dir = results_dir
Chris Masone8ac66712012-02-15 14:21:02 -0800814 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
815 delay_sec=10,
816 debug=False)
817 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
818 delay_sec=10,
819 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500820 self._pool = pool
Chris Masone6fed6462011-10-20 16:36:43 -0700821 self._jobs = []
Chris Masone6fed6462011-10-20 16:36:43 -0700822 self._tests = Suite.find_and_parse_tests(self._cf_getter,
823 self._predicate,
824 add_experimental=True)
825
826
827 @property
828 def tests(self):
829 """
830 A list of ControlData objects in the suite, with added |text| attr.
831 """
832 return self._tests
833
834
835 def stable_tests(self):
836 """
837 |self.tests|, filtered for non-experimental tests.
838 """
839 return filter(lambda t: not t.experimental, self.tests)
840
841
842 def unstable_tests(self):
843 """
844 |self.tests|, filtered for experimental tests.
845 """
846 return filter(lambda t: t.experimental, self.tests)
847
848
Chris Masone8b7cd422012-02-22 13:16:11 -0800849 def _create_job(self, test):
Chris Masone6fed6462011-10-20 16:36:43 -0700850 """
851 Thin wrapper around frontend.AFE.create_job().
852
853 @param test: ControlData object for a test to run.
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500854 @return a frontend.Job object with an added test_name member.
855 test_name is used to preserve the higher level TEST_NAME
856 name of the job.
Chris Masone6fed6462011-10-20 16:36:43 -0700857 """
Chris Masonec43448f2012-05-31 12:55:59 -0700858 job_deps = [] # TODO(cmasone): init from test.dependencies.
Scott Zawalski65650172012-02-16 11:48:26 -0500859 if self._pool:
Chris Masone5374c672012-03-05 15:11:39 -0800860 meta_hosts = self._pool
Chris Masone8b7cd422012-02-22 13:16:11 -0800861 cros_label = VERSION_PREFIX + self._build
Scott Zawalski65650172012-02-16 11:48:26 -0500862 job_deps.append(cros_label)
863 else:
864 # No pool specified use any machines with the following label.
Chris Masone8b7cd422012-02-22 13:16:11 -0800865 meta_hosts = VERSION_PREFIX + self._build
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500866 test_obj = self._afe.create_job(
Chris Masone6fed6462011-10-20 16:36:43 -0700867 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800868 name='/'.join([self._build, self._tag, test.name]),
Chris Masone6fed6462011-10-20 16:36:43 -0700869 control_type=test.test_type.capitalize(),
Scott Zawalski65650172012-02-16 11:48:26 -0500870 meta_hosts=[meta_hosts],
Chris Masonebafbbb02012-05-16 13:41:36 -0700871 dependencies=job_deps,
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700872 keyvals={JOB_BUILD_KEY: self._build, JOB_SUITE_KEY: self._tag})
Chris Masone6fed6462011-10-20 16:36:43 -0700873
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500874 setattr(test_obj, 'test_name', test.name)
875
876 return test_obj
877
Chris Masone6fed6462011-10-20 16:36:43 -0700878
Chris Masone8b7cd422012-02-22 13:16:11 -0800879 def run_and_wait(self, record, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700880 """
881 Synchronously run tests in |self.tests|.
882
Chris Masone8b7cd422012-02-22 13:16:11 -0800883 Schedules tests against a device running image |self._build|, and
Chris Masone6fed6462011-10-20 16:36:43 -0700884 then polls for status, using |record| to print status when each
885 completes.
886
887 Tests returned by self.stable_tests() will always be run, while tests
888 in self.unstable_tests() will only be run if |add_experimental| is true.
889
Chris Masone6fed6462011-10-20 16:36:43 -0700890 @param record: callable that records job status.
891 prototype:
Chris Masone604baf32012-06-28 08:45:30 -0700892 record(base_job.status_log_entry)
Chris Masone6fed6462011-10-20 16:36:43 -0700893 @param add_experimental: schedule experimental tests as well, or not.
894 """
Chris Masoneed356392012-05-08 14:07:13 -0700895 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
896 logging.debug('Discovered %d unstable tests.',
897 len(self.unstable_tests()))
Chris Masone6fed6462011-10-20 16:36:43 -0700898 try:
Chris Masone604baf32012-06-28 08:45:30 -0700899 Status('INFO', 'Start %s' % self._tag).record_result(record)
Chris Masone8b7cd422012-02-22 13:16:11 -0800900 self.schedule(add_experimental)
Chris Masone6fed6462011-10-20 16:36:43 -0700901 try:
Chris Masone8d6e6412012-06-28 11:20:56 -0700902 for result in job_status.wait_for_results(self._afe,
903 self._tko,
904 self._jobs):
Chris Masone604baf32012-06-28 08:45:30 -0700905 result.record_all(record)
906
Chris Masone6fed6462011-10-20 16:36:43 -0700907 except Exception as e:
Chris Masone99378582012-04-30 13:10:58 -0700908 logging.error(traceback.format_exc())
Chris Masone604baf32012-06-28 08:45:30 -0700909 Status('FAIL', self._tag,
Chris Masone99378582012-04-30 13:10:58 -0700910 'Exception waiting for results').record_result(record)
Chris Masone6fed6462011-10-20 16:36:43 -0700911 except Exception as e:
Chris Masone99378582012-04-30 13:10:58 -0700912 logging.error(traceback.format_exc())
Chris Masone604baf32012-06-28 08:45:30 -0700913 Status('FAIL', self._tag,
Chris Masone99378582012-04-30 13:10:58 -0700914 'Exception while scheduling suite').record_result(record)
Chris Masoneed356392012-05-08 14:07:13 -0700915 # Sanity check
916 tests_at_end = self.find_and_parse_tests(self._cf_getter,
917 self._predicate,
918 add_experimental=True)
919 if len(self.tests) != len(tests_at_end):
920 msg = 'Dev Server enumerated %d tests at start, %d at end.' % (
921 len(self.tests), len(tests_at_end))
Chris Masone604baf32012-06-28 08:45:30 -0700922 Status('FAIL', self._tag, msg).record_result(record)
Chris Masone6fed6462011-10-20 16:36:43 -0700923
924
Chris Masone8b7cd422012-02-22 13:16:11 -0800925 def schedule(self, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700926 """
927 Schedule jobs using |self._afe|.
928
929 frontend.Job objects representing each scheduled job will be put in
930 |self._jobs|.
931
Chris Masone6fed6462011-10-20 16:36:43 -0700932 @param add_experimental: schedule experimental tests as well, or not.
933 """
934 for test in self.stable_tests():
935 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800936 self._jobs.append(self._create_job(test))
Chris Masone6fed6462011-10-20 16:36:43 -0700937
938 if add_experimental:
Chris Masone6fed6462011-10-20 16:36:43 -0700939 for test in self.unstable_tests():
Zdenek Behan150fbd62012-04-06 17:20:01 +0200940 logging.debug('Scheduling experimental %s', test.name)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700941 test.name = EXPERIMENTAL_PREFIX + test.name
Chris Masone8b7cd422012-02-22 13:16:11 -0800942 self._jobs.append(self._create_job(test))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500943 if self._results_dir:
944 self._record_scheduled_jobs()
945
946
947 def _record_scheduled_jobs(self):
948 """
949 Record scheduled job ids as keyvals, so they can be referenced later.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500950 """
951 for job in self._jobs:
952 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masone11aae452012-05-21 16:08:39 -0700953 utils.write_keyval(
954 self._results_dir,
955 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
Chris Masone6fed6462011-10-20 16:36:43 -0700956
957
Chris Masonefef21382012-01-17 11:16:32 -0800958 @staticmethod
959 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
Chris Masone6fed6462011-10-20 16:36:43 -0700960 """
961 Function to scan through all tests and find eligible tests.
962
963 Looks at control files returned by _cf_getter.get_control_file_list()
964 for tests that pass self._predicate().
965
966 @param cf_getter: a control_file_getter.ControlFileGetter used to list
967 and fetch the content of control files
968 @param predicate: a function that should return True when run over a
969 ControlData representation of a control file that should be in
970 this Suite.
971 @param add_experimental: add tests with experimental attribute set.
972
973 @return list of ControlData objects that should be run, with control
974 file text added in |text| attribute.
975 """
976 tests = {}
977 files = cf_getter.get_control_file_list()
Chris Masone75a20612012-05-08 12:37:31 -0700978 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
979 for file in filter(lambda f: not matcher.match(f), files):
Chris Masoneed356392012-05-08 14:07:13 -0700980 logging.debug('Considering %s', file)
Chris Masone6fed6462011-10-20 16:36:43 -0700981 text = cf_getter.get_control_file_contents(file)
982 try:
Chris Masoneed356392012-05-08 14:07:13 -0700983 found_test = control_data.parse_control_string(
984 text, raise_warnings=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700985 if not add_experimental and found_test.experimental:
986 continue
987
988 found_test.text = text
Chris Masonee8a4eff2012-02-28 16:33:43 -0800989 found_test.path = file
Chris Masone6fed6462011-10-20 16:36:43 -0700990 tests[file] = found_test
991 except control_data.ControlVariableException, e:
992 logging.warn("Skipping %s\n%s", file, e)
993 except Exception, e:
994 logging.error("Bad %s\n%s", file, e)
995
996 return [test for test in tests.itervalues() if predicate(test)]