blob: d29041f10cbaf944c45b30ec811bf7846f786d19 [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.
Chris Masone6ea0cad2012-07-02 09:43:36 -0700445 results = self.get_results(canary_job)
446
Chris Masonef8b53062012-05-08 22:14:18 -0700447 except error.InadequateHostsException as e:
Chris Masone5374c672012-03-05 15:11:39 -0800448 logging.warning(e)
Chris Masone604baf32012-06-28 08:45:30 -0700449 Status('WARN', REIMAGE_JOB_NAME, str(e),
450 begin_time_str=begin_time_str).record_all(record)
Chris Masone5374c672012-03-05 15:11:39 -0800451 return False
Chris Masone796fcf12012-02-22 16:53:31 -0800452 except Exception as e:
453 # catch Exception so we record the job as terminated no matter what.
454 logging.error(e)
Chris Masone604baf32012-06-28 08:45:30 -0700455 Status('ERROR', REIMAGE_JOB_NAME, str(e),
456 begin_time_str=begin_time_str).record_all(record)
Chris Masone796fcf12012-02-22 16:53:31 -0800457 return False
Chris Masone6fed6462011-10-20 16:36:43 -0700458
Chris Masone6ea0cad2012-07-02 09:43:36 -0700459 self._remember_reimaged_hosts(build, results.keys())
Chris Masoned368cc42012-03-07 15:16:59 -0800460
Chris Masone6ea0cad2012-07-02 09:43:36 -0700461 return job_status.record_and_report_results(results.values(), record)
Chris Masone6fed6462011-10-20 16:36:43 -0700462
Chris Masone604baf32012-06-28 08:45:30 -0700463
Chris Masone6ea0cad2012-07-02 09:43:36 -0700464 def get_results(self, canary_job):
465 """
466 Gather results for |canary_job|, in a map of Statuses indexed by host.
Chris Masone6fed6462011-10-20 16:36:43 -0700467
Chris Masone6ea0cad2012-07-02 09:43:36 -0700468 A host's results will be named REIMAGE_JOB_NAME-<host> in the map, e.g.
469 {'chromeos2-rack1': Status('GOOD', 'try_new_image-chromeos2-rack1')}
470
471 @param canary_job: a completed frontend.Job
472 @return a map of hostname: job_status.Status objects.
473 """
474 return job_status.gather_per_host_results(self._afe,
475 self._tko,
476 [canary_job],
477 REIMAGE_JOB_NAME+'-')
Chris Masone6fed6462011-10-20 16:36:43 -0700478
479
Chris Masone62579122012-03-08 15:18:43 -0800480 def _ensure_enough_hosts(self, board, pool, num):
481 """
482 Determine if there are enough working hosts to run on.
483
484 Raises exception if there are not enough hosts.
485
486 @param board: which kind of devices to reimage.
487 @param pool: the pool of machines to use for scheduling purposes.
488 @param num: how many devices to reimage.
Chris Masonef8b53062012-05-08 22:14:18 -0700489 @raises NoHostsException: if no working hosts.
Chris Masone62579122012-03-08 15:18:43 -0800490 @raises InadequateHostsException: if too few working hosts.
491 """
492 labels = [l for l in [board, pool] if l is not None]
Chris Masone502b71e2012-04-10 10:41:35 -0700493 available = self._count_usable_hosts(labels)
494 if available == 0:
Chris Masonef8b53062012-05-08 22:14:18 -0700495 raise error.NoHostsException('All hosts with %r are dead!' % labels)
Chris Masone502b71e2012-04-10 10:41:35 -0700496 elif num > available:
Chris Masonef8b53062012-05-08 22:14:18 -0700497 raise error.InadequateHostsException(
498 'Too few hosts with %r' % labels)
Chris Masone62579122012-03-08 15:18:43 -0800499
500
Chris Masoned368cc42012-03-07 15:16:59 -0800501 def _wait_for_job_to_start(self, job_id):
502 """
503 Wait for the job specified by |job_id| to start.
504
505 @param job_id: the job ID to poll on.
506 """
507 while len(self._afe.get_jobs(id=job_id, not_yet_run=True)) > 0:
508 time.sleep(10)
509 logging.debug('Re-imaging job running.')
510
511
512 def _wait_for_job_to_finish(self, job_id):
513 """
514 Wait for the job specified by |job_id| to finish.
515
516 @param job_id: the job ID to poll on.
517 """
518 while len(self._afe.get_jobs(id=job_id, finished=True)) == 0:
519 time.sleep(10)
520 logging.debug('Re-imaging job finished.')
521
522
Chris Masone6ea0cad2012-07-02 09:43:36 -0700523 def _remember_reimaged_hosts(self, build, hosts):
Chris Masoned368cc42012-03-07 15:16:59 -0800524 """
525 Remember hosts that were reimaged with |build| as a part |canary_job|.
526
527 @param build: the build that was installed e.g.
528 x86-alex-release/R18-1655.0.0-a1-b1584.
Chris Masone6ea0cad2012-07-02 09:43:36 -0700529 @param hosts: iterable of hostnames.
Chris Masoned368cc42012-03-07 15:16:59 -0800530 """
Chris Masone6ea0cad2012-07-02 09:43:36 -0700531 self._reimaged_hosts[build] = hosts
Chris Masoned368cc42012-03-07 15:16:59 -0800532
533
534 def clear_reimaged_host_state(self, build):
535 """
536 Clear per-host state created in the autotest DB for this job.
537
538 After reimaging a host, we label it and set some host attributes on it
539 that are then used by the suite scheduling code. This call cleans
540 that up.
541
542 @param build: the build whose hosts we want to clean up e.g.
543 x86-alex-release/R18-1655.0.0-a1-b1584.
544 """
Chris Masoned368cc42012-03-07 15:16:59 -0800545 for host in self._reimaged_hosts.get('build', []):
Chris Masone6ea0cad2012-07-02 09:43:36 -0700546 if not host.startswith('hostless'):
547 self._clear_build_state(host)
Chris Masoned368cc42012-03-07 15:16:59 -0800548
549
550 def _clear_build_state(self, machine):
551 """
552 Clear all build-specific labels, attributes from the target.
553
554 @param machine: the host to clear labels, attributes from.
555 """
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700556 self._afe.set_host_attribute(JOB_REPO_URL, None, hostname=machine)
Chris Masoned368cc42012-03-07 15:16:59 -0800557
558
Chris Masone9f13ff22012-03-05 13:45:25 -0800559 def _record_job_if_possible(self, test_name, job):
560 """
561 Record job id as keyval, if possible, so it can be referenced later.
562
563 If |self._results_dir| is None, then this is a NOOP.
Chris Masone5374c672012-03-05 15:11:39 -0800564
565 @param test_name: the test to record id/owner for.
566 @param job: the job object to pull info from.
Chris Masone9f13ff22012-03-05 13:45:25 -0800567 """
568 if self._results_dir:
569 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masone11aae452012-05-21 16:08:39 -0700570 utils.write_keyval(
571 self._results_dir,
572 {hashlib.md5(test_name).hexdigest(): job_id_owner})
Chris Masone9f13ff22012-03-05 13:45:25 -0800573
574
Chris Masone5374c672012-03-05 15:11:39 -0800575 def _count_usable_hosts(self, host_spec):
576 """
577 Given a set of host labels, count the live hosts that have them all.
578
579 @param host_spec: list of labels specifying a set of hosts.
580 @return the number of live hosts that satisfy |host_spec|.
581 """
582 count = 0
583 for h in self._afe.get_hosts(multiple_labels=host_spec):
584 if h.status not in ['Repair Failed', 'Repairing']:
585 count += 1
586 return count
587
588
Chris Masone6fed6462011-10-20 16:36:43 -0700589 def _ensure_version_label(self, name):
590 """
591 Ensure that a label called |name| exists in the autotest DB.
592
593 @param name: the label to check for/create.
594 """
Chris Masone47c9e642012-04-25 14:22:18 -0700595 try:
Chris Masone6fed6462011-10-20 16:36:43 -0700596 self._afe.create_label(name=name)
Chris Masone47c9e642012-04-25 14:22:18 -0700597 except proxy.ValidationError as ve:
598 if ('name' in ve.problem_keys and
599 'This value must be unique' in ve.problem_keys['name']):
600 logging.debug('Version label %s already exists', name)
601 else:
602 raise ve
Chris Masone6fed6462011-10-20 16:36:43 -0700603
604
Chris Masonec43448f2012-05-31 12:55:59 -0700605 def _schedule_reimage_job(self, build, board, pool, num_machines):
Chris Masone6fed6462011-10-20 16:36:43 -0700606 """
607 Schedules the reimaging of |num_machines| |board| devices with |image|.
608
609 Sends an RPC to the autotest frontend to enqueue reimaging jobs on
610 |num_machines| devices of type |board|
611
Chris Masone8abb6fc2012-01-31 09:27:36 -0800612 @param build: the build to install (must be unique).
Chris Masone6fed6462011-10-20 16:36:43 -0700613 @param board: which kind of devices to reimage.
Chris Masonec43448f2012-05-31 12:55:59 -0700614 @param pool: the pool of machines to use for scheduling purposes.
615 @param num_machines: how many devices to reimage.
Chris Masone6fed6462011-10-20 16:36:43 -0700616 @return a frontend.Job object for the reimaging job we scheduled.
617 """
Chris Masone8b764252012-01-17 11:12:51 -0800618 control_file = inject_vars(
Chris Masone8abb6fc2012-01-31 09:27:36 -0800619 {'image_url': _image_url_pattern() % build, 'image_name': build},
Chris Masone6fed6462011-10-20 16:36:43 -0700620 self._cf_getter.get_control_file_contents_by_name('autoupdate'))
Scott Zawalski65650172012-02-16 11:48:26 -0500621 job_deps = []
Chris Masonec43448f2012-05-31 12:55:59 -0700622 if pool:
623 meta_host = pool
Chris Masone5374c672012-03-05 15:11:39 -0800624 board_label = board
Scott Zawalski65650172012-02-16 11:48:26 -0500625 job_deps.append(board_label)
626 else:
627 # No pool specified use board.
Chris Masone5374c672012-03-05 15:11:39 -0800628 meta_host = board
Chris Masone6fed6462011-10-20 16:36:43 -0700629
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800630 return self._afe.create_job(control_file=control_file,
Chris Masone8abb6fc2012-01-31 09:27:36 -0800631 name=build + '-try',
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800632 control_type='Server',
Chris Masone97325362012-04-26 16:19:13 -0700633 priority='Low',
Scott Zawalski65650172012-02-16 11:48:26 -0500634 meta_hosts=[meta_host] * num_machines,
635 dependencies=job_deps)
Chris Masone6fed6462011-10-20 16:36:43 -0700636
637
Chris Masone6fed6462011-10-20 16:36:43 -0700638class Suite(object):
639 """
640 A suite of tests, defined by some predicate over control file variables.
641
642 Given a place to search for control files a predicate to match the desired
643 tests, can gather tests and fire off jobs to run them, and then wait for
644 results.
645
646 @var _predicate: a function that should return True when run over a
647 ControlData representation of a control file that should be in
648 this Suite.
649 @var _tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800650 @var _build: the build on which we're running this suite.
Chris Masone6fed6462011-10-20 16:36:43 -0700651 @var _afe: an instance of AFE as defined in server/frontend.py.
652 @var _tko: an instance of TKO as defined in server/frontend.py.
653 @var _jobs: currently scheduled jobs, if any.
654 @var _cf_getter: a control_file_getter.ControlFileGetter
655 """
656
657
Chris Masonefef21382012-01-17 11:16:32 -0800658 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800659 def create_ds_getter(build):
Chris Masonefef21382012-01-17 11:16:32 -0800660 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800661 @param build: the build on which we're running this suite.
Chris Masonefef21382012-01-17 11:16:32 -0800662 @return a FileSystemGetter instance that looks under |autotest_dir|.
663 """
Chris Masone8b7cd422012-02-22 13:16:11 -0800664 return control_file_getter.DevServerGetter(
665 build, dev_server.DevServer.create())
Chris Masonefef21382012-01-17 11:16:32 -0800666
667
668 @staticmethod
Chris Masoned6f38c82012-02-22 14:53:42 -0800669 def create_fs_getter(autotest_dir):
670 """
671 @param autotest_dir: the place to find autotests.
672 @return a FileSystemGetter instance that looks under |autotest_dir|.
673 """
674 # currently hard-coded places to look for tests.
675 subpaths = ['server/site_tests', 'client/site_tests',
676 'server/tests', 'client/tests']
677 directories = [os.path.join(autotest_dir, p) for p in subpaths]
678 return control_file_getter.FileSystemGetter(directories)
679
680
681 @staticmethod
Zdenek Behan849db052012-02-29 19:16:28 +0100682 def parse_tag(tag):
683 """Splits a string on ',' optionally surrounded by whitespace."""
684 return map(lambda x: x.strip(), tag.split(','))
685
686
687 @staticmethod
Chris Masone84564792012-02-23 10:52:42 -0800688 def name_in_tag_predicate(name):
689 """Returns predicate that takes a control file and looks for |name|.
690
691 Builds a predicate that takes in a parsed control file (a ControlData)
692 and returns True if the SUITE tag is present and contains |name|.
693
694 @param name: the suite name to base the predicate on.
695 @return a callable that takes a ControlData and looks for |name| in that
696 ControlData object's suite member.
697 """
Zdenek Behan849db052012-02-29 19:16:28 +0100698 return lambda t: hasattr(t, 'suite') and \
699 name in Suite.parse_tag(t.suite)
Chris Masone84564792012-02-23 10:52:42 -0800700
Zdenek Behan849db052012-02-29 19:16:28 +0100701
702 @staticmethod
703 def list_all_suites(build, cf_getter=None):
704 """
705 Parses all ControlData objects with a SUITE tag and extracts all
706 defined suite names.
707
708 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
709 using DevServerGetter.
710
711 @return list of suites
712 """
713 if cf_getter is None:
714 cf_getter = Suite.create_ds_getter(build)
715
716 suites = set()
717 predicate = lambda t: hasattr(t, 'suite')
Scott Zawalskif22b75d2012-05-10 16:54:37 -0400718 for test in Suite.find_and_parse_tests(cf_getter, predicate,
719 add_experimental=True):
Zdenek Behan849db052012-02-29 19:16:28 +0100720 suites.update(Suite.parse_tag(test.suite))
721 return list(suites)
Chris Masone84564792012-02-23 10:52:42 -0800722
723
724 @staticmethod
Scott Zawalski9ece6532012-02-28 14:10:47 -0500725 def create_from_name(name, build, cf_getter=None, afe=None, tko=None,
726 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700727 """
728 Create a Suite using a predicate based on the SUITE control file var.
729
730 Makes a predicate based on |name| and uses it to instantiate a Suite
731 that looks for tests in |autotest_dir| and will schedule them using
Chris Masoned6f38c82012-02-22 14:53:42 -0800732 |afe|. Pulls control files from the default dev server.
733 Results will be pulled from |tko| upon completion.
Chris Masone6fed6462011-10-20 16:36:43 -0700734
735 @param name: a value of the SUITE control file variable to search for.
Chris Masone8b7cd422012-02-22 13:16:11 -0800736 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800737 @param cf_getter: a control_file_getter.ControlFileGetter.
738 If None, default to using a DevServerGetter.
Chris Masone6fed6462011-10-20 16:36:43 -0700739 @param afe: an instance of AFE as defined in server/frontend.py.
740 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500741 @param pool: Specify the pool of machines to use for scheduling
Chris Masoned6f38c82012-02-22 14:53:42 -0800742 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500743 @param results_dir: The directory where the job can write results to.
744 This must be set if you want job_id of sub-jobs
745 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700746 @return a Suite instance.
747 """
Chris Masoned6f38c82012-02-22 14:53:42 -0800748 if cf_getter is None:
749 cf_getter = Suite.create_ds_getter(build)
Chris Masone84564792012-02-23 10:52:42 -0800750 return Suite(Suite.name_in_tag_predicate(name),
Scott Zawalski9ece6532012-02-28 14:10:47 -0500751 name, build, cf_getter, afe, tko, pool, results_dir)
Chris Masone6fed6462011-10-20 16:36:43 -0700752
753
Chris Masoned6f38c82012-02-22 14:53:42 -0800754 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Scott Zawalski9ece6532012-02-28 14:10:47 -0500755 pool=None, results_dir=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700756 """
757 Constructor
758
759 @param predicate: a function that should return True when run over a
760 ControlData representation of a control file that should be in
761 this Suite.
762 @param tag: a string with which to tag jobs run in this suite.
Chris Masone8b7cd422012-02-22 13:16:11 -0800763 @param build: the build on which we're running this suite.
Chris Masoned6f38c82012-02-22 14:53:42 -0800764 @param cf_getter: a control_file_getter.ControlFileGetter
Chris Masone6fed6462011-10-20 16:36:43 -0700765 @param afe: an instance of AFE as defined in server/frontend.py.
766 @param tko: an instance of TKO as defined in server/frontend.py.
Scott Zawalski65650172012-02-16 11:48:26 -0500767 @param pool: Specify the pool of machines to use for scheduling
768 purposes.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500769 @param results_dir: The directory where the job can write results to.
770 This must be set if you want job_id of sub-jobs
771 list in the job keyvals.
Chris Masone6fed6462011-10-20 16:36:43 -0700772 """
773 self._predicate = predicate
774 self._tag = tag
Chris Masone8b7cd422012-02-22 13:16:11 -0800775 self._build = build
Chris Masoned6f38c82012-02-22 14:53:42 -0800776 self._cf_getter = cf_getter
Scott Zawalski9ece6532012-02-28 14:10:47 -0500777 self._results_dir = results_dir
Chris Masone8ac66712012-02-15 14:21:02 -0800778 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
779 delay_sec=10,
780 debug=False)
781 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
782 delay_sec=10,
783 debug=False)
Scott Zawalski65650172012-02-16 11:48:26 -0500784 self._pool = pool
Chris Masone6fed6462011-10-20 16:36:43 -0700785 self._jobs = []
Chris Masone6fed6462011-10-20 16:36:43 -0700786 self._tests = Suite.find_and_parse_tests(self._cf_getter,
787 self._predicate,
788 add_experimental=True)
789
790
791 @property
792 def tests(self):
793 """
794 A list of ControlData objects in the suite, with added |text| attr.
795 """
796 return self._tests
797
798
799 def stable_tests(self):
800 """
801 |self.tests|, filtered for non-experimental tests.
802 """
803 return filter(lambda t: not t.experimental, self.tests)
804
805
806 def unstable_tests(self):
807 """
808 |self.tests|, filtered for experimental tests.
809 """
810 return filter(lambda t: t.experimental, self.tests)
811
812
Chris Masone8b7cd422012-02-22 13:16:11 -0800813 def _create_job(self, test):
Chris Masone6fed6462011-10-20 16:36:43 -0700814 """
815 Thin wrapper around frontend.AFE.create_job().
816
817 @param test: ControlData object for a test to run.
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500818 @return a frontend.Job object with an added test_name member.
819 test_name is used to preserve the higher level TEST_NAME
820 name of the job.
Chris Masone6fed6462011-10-20 16:36:43 -0700821 """
Chris Masonec43448f2012-05-31 12:55:59 -0700822 job_deps = [] # TODO(cmasone): init from test.dependencies.
Scott Zawalski65650172012-02-16 11:48:26 -0500823 if self._pool:
Chris Masone5374c672012-03-05 15:11:39 -0800824 meta_hosts = self._pool
Chris Masone8b7cd422012-02-22 13:16:11 -0800825 cros_label = VERSION_PREFIX + self._build
Scott Zawalski65650172012-02-16 11:48:26 -0500826 job_deps.append(cros_label)
827 else:
828 # No pool specified use any machines with the following label.
Chris Masone8b7cd422012-02-22 13:16:11 -0800829 meta_hosts = VERSION_PREFIX + self._build
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500830 test_obj = self._afe.create_job(
Chris Masone6fed6462011-10-20 16:36:43 -0700831 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800832 name='/'.join([self._build, self._tag, test.name]),
Chris Masone6fed6462011-10-20 16:36:43 -0700833 control_type=test.test_type.capitalize(),
Scott Zawalski65650172012-02-16 11:48:26 -0500834 meta_hosts=[meta_hosts],
Chris Masonebafbbb02012-05-16 13:41:36 -0700835 dependencies=job_deps,
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700836 keyvals={JOB_BUILD_KEY: self._build, JOB_SUITE_KEY: self._tag})
Chris Masone6fed6462011-10-20 16:36:43 -0700837
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500838 setattr(test_obj, 'test_name', test.name)
839
840 return test_obj
841
Chris Masone6fed6462011-10-20 16:36:43 -0700842
Chris Masone8b7cd422012-02-22 13:16:11 -0800843 def run_and_wait(self, record, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700844 """
845 Synchronously run tests in |self.tests|.
846
Chris Masone8b7cd422012-02-22 13:16:11 -0800847 Schedules tests against a device running image |self._build|, and
Chris Masone6fed6462011-10-20 16:36:43 -0700848 then polls for status, using |record| to print status when each
849 completes.
850
851 Tests returned by self.stable_tests() will always be run, while tests
852 in self.unstable_tests() will only be run if |add_experimental| is true.
853
Chris Masone6fed6462011-10-20 16:36:43 -0700854 @param record: callable that records job status.
855 prototype:
Chris Masone604baf32012-06-28 08:45:30 -0700856 record(base_job.status_log_entry)
Chris Masone6fed6462011-10-20 16:36:43 -0700857 @param add_experimental: schedule experimental tests as well, or not.
858 """
Chris Masoneed356392012-05-08 14:07:13 -0700859 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
860 logging.debug('Discovered %d unstable tests.',
861 len(self.unstable_tests()))
Chris Masone6fed6462011-10-20 16:36:43 -0700862 try:
Chris Masone604baf32012-06-28 08:45:30 -0700863 Status('INFO', 'Start %s' % self._tag).record_result(record)
Chris Masone8b7cd422012-02-22 13:16:11 -0800864 self.schedule(add_experimental)
Chris Masone6fed6462011-10-20 16:36:43 -0700865 try:
Chris Masone8d6e6412012-06-28 11:20:56 -0700866 for result in job_status.wait_for_results(self._afe,
867 self._tko,
868 self._jobs):
Chris Masone604baf32012-06-28 08:45:30 -0700869 result.record_all(record)
870
Chris Masone6fed6462011-10-20 16:36:43 -0700871 except Exception as e:
Chris Masone99378582012-04-30 13:10:58 -0700872 logging.error(traceback.format_exc())
Chris Masone604baf32012-06-28 08:45:30 -0700873 Status('FAIL', self._tag,
Chris Masone99378582012-04-30 13:10:58 -0700874 'Exception waiting for results').record_result(record)
Chris Masone6fed6462011-10-20 16:36:43 -0700875 except Exception as e:
Chris Masone99378582012-04-30 13:10:58 -0700876 logging.error(traceback.format_exc())
Chris Masone604baf32012-06-28 08:45:30 -0700877 Status('FAIL', self._tag,
Chris Masone99378582012-04-30 13:10:58 -0700878 'Exception while scheduling suite').record_result(record)
Chris Masoneed356392012-05-08 14:07:13 -0700879 # Sanity check
880 tests_at_end = self.find_and_parse_tests(self._cf_getter,
881 self._predicate,
882 add_experimental=True)
883 if len(self.tests) != len(tests_at_end):
884 msg = 'Dev Server enumerated %d tests at start, %d at end.' % (
885 len(self.tests), len(tests_at_end))
Chris Masone604baf32012-06-28 08:45:30 -0700886 Status('FAIL', self._tag, msg).record_result(record)
Chris Masone6fed6462011-10-20 16:36:43 -0700887
888
Chris Masone8b7cd422012-02-22 13:16:11 -0800889 def schedule(self, add_experimental=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700890 """
891 Schedule jobs using |self._afe|.
892
893 frontend.Job objects representing each scheduled job will be put in
894 |self._jobs|.
895
Chris Masone6fed6462011-10-20 16:36:43 -0700896 @param add_experimental: schedule experimental tests as well, or not.
897 """
898 for test in self.stable_tests():
899 logging.debug('Scheduling %s', test.name)
Chris Masone8b7cd422012-02-22 13:16:11 -0800900 self._jobs.append(self._create_job(test))
Chris Masone6fed6462011-10-20 16:36:43 -0700901
902 if add_experimental:
Chris Masone6fed6462011-10-20 16:36:43 -0700903 for test in self.unstable_tests():
Zdenek Behan150fbd62012-04-06 17:20:01 +0200904 logging.debug('Scheduling experimental %s', test.name)
Chris Masoneaa10f8e2012-05-15 13:34:21 -0700905 test.name = EXPERIMENTAL_PREFIX + test.name
Chris Masone8b7cd422012-02-22 13:16:11 -0800906 self._jobs.append(self._create_job(test))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500907 if self._results_dir:
908 self._record_scheduled_jobs()
909
910
911 def _record_scheduled_jobs(self):
912 """
913 Record scheduled job ids as keyvals, so they can be referenced later.
Scott Zawalski9ece6532012-02-28 14:10:47 -0500914 """
915 for job in self._jobs:
916 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masone11aae452012-05-21 16:08:39 -0700917 utils.write_keyval(
918 self._results_dir,
919 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
Chris Masone6fed6462011-10-20 16:36:43 -0700920
921
Chris Masonefef21382012-01-17 11:16:32 -0800922 @staticmethod
923 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
Chris Masone6fed6462011-10-20 16:36:43 -0700924 """
925 Function to scan through all tests and find eligible tests.
926
927 Looks at control files returned by _cf_getter.get_control_file_list()
928 for tests that pass self._predicate().
929
930 @param cf_getter: a control_file_getter.ControlFileGetter used to list
931 and fetch the content of control files
932 @param predicate: a function that should return True when run over a
933 ControlData representation of a control file that should be in
934 this Suite.
935 @param add_experimental: add tests with experimental attribute set.
936
937 @return list of ControlData objects that should be run, with control
938 file text added in |text| attribute.
939 """
940 tests = {}
941 files = cf_getter.get_control_file_list()
Chris Masone75a20612012-05-08 12:37:31 -0700942 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
943 for file in filter(lambda f: not matcher.match(f), files):
Chris Masoneed356392012-05-08 14:07:13 -0700944 logging.debug('Considering %s', file)
Chris Masone6fed6462011-10-20 16:36:43 -0700945 text = cf_getter.get_control_file_contents(file)
946 try:
Chris Masoneed356392012-05-08 14:07:13 -0700947 found_test = control_data.parse_control_string(
948 text, raise_warnings=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700949 if not add_experimental and found_test.experimental:
950 continue
951
952 found_test.text = text
Chris Masonee8a4eff2012-02-28 16:33:43 -0800953 found_test.path = file
Chris Masone6fed6462011-10-20 16:36:43 -0700954 tests[file] = found_test
955 except control_data.ControlVariableException, e:
956 logging.warn("Skipping %s\n%s", file, e)
957 except Exception, e:
958 logging.error("Bad %s\n%s", file, e)
959
960 return [test for test in tests.itervalues() if predicate(test)]