blob: 308449d176e6ecc40bdd37355e9f9a2bcca0a9e6 [file] [log] [blame]
Chris Masone44e4d6c2012-08-15 14:25:53 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import compiler, hashlib, logging, os, re, traceback, signal
6
7import common
8
9from autotest_lib.client.common_lib import base_job, control_data
10from autotest_lib.client.common_lib import utils
11from autotest_lib.client.common_lib.cros import dev_server
12from autotest_lib.server.cros.dynamic_suite import constants
13from autotest_lib.server.cros.dynamic_suite import control_file_getter
14from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
15from autotest_lib.server.cros.dynamic_suite import host_lock_manager, job_status
16from autotest_lib.server.cros.dynamic_suite.job_status import Status
17from autotest_lib.server import frontend
18
19
20class Suite(object):
21 """
22 A suite of tests, defined by some predicate over control file variables.
23
24 Given a place to search for control files a predicate to match the desired
25 tests, can gather tests and fire off jobs to run them, and then wait for
26 results.
27
28 @var _predicate: a function that should return True when run over a
29 ControlData representation of a control file that should be in
30 this Suite.
31 @var _tag: a string with which to tag jobs run in this suite.
32 @var _build: the build on which we're running this suite.
33 @var _afe: an instance of AFE as defined in server/frontend.py.
34 @var _tko: an instance of TKO as defined in server/frontend.py.
35 @var _jobs: currently scheduled jobs, if any.
36 @var _cf_getter: a control_file_getter.ControlFileGetter
37 """
38
39
40 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -070041 def create_ds_getter(build, devserver):
Chris Masone44e4d6c2012-08-15 14:25:53 -070042 """
43 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -070044 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -070045 @return a FileSystemGetter instance that looks under |autotest_dir|.
46 """
Chris Sosaaccb5ce2012-08-30 17:29:15 -070047 return control_file_getter.DevServerGetter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -070048
49
50 @staticmethod
51 def create_fs_getter(autotest_dir):
52 """
53 @param autotest_dir: the place to find autotests.
54 @return a FileSystemGetter instance that looks under |autotest_dir|.
55 """
56 # currently hard-coded places to look for tests.
57 subpaths = ['server/site_tests', 'client/site_tests',
58 'server/tests', 'client/tests']
59 directories = [os.path.join(autotest_dir, p) for p in subpaths]
60 return control_file_getter.FileSystemGetter(directories)
61
62
63 @staticmethod
64 def parse_tag(tag):
65 """Splits a string on ',' optionally surrounded by whitespace."""
66 return map(lambda x: x.strip(), tag.split(','))
67
68
69 @staticmethod
70 def name_in_tag_predicate(name):
71 """Returns predicate that takes a control file and looks for |name|.
72
73 Builds a predicate that takes in a parsed control file (a ControlData)
74 and returns True if the SUITE tag is present and contains |name|.
75
76 @param name: the suite name to base the predicate on.
77 @return a callable that takes a ControlData and looks for |name| in that
78 ControlData object's suite member.
79 """
80 return lambda t: hasattr(t, 'suite') and \
81 name in Suite.parse_tag(t.suite)
82
83
84 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -070085 def list_all_suites(build, devserver, cf_getter=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -070086 """
87 Parses all ControlData objects with a SUITE tag and extracts all
88 defined suite names.
89
Chris Sosaaccb5ce2012-08-30 17:29:15 -070090 @param build: the build on which we're running this suite.
91 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -070092 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
93 using DevServerGetter.
94
95 @return list of suites
96 """
97 if cf_getter is None:
Chris Sosaaccb5ce2012-08-30 17:29:15 -070098 cf_getter = Suite.create_ds_getter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -070099
100 suites = set()
101 predicate = lambda t: hasattr(t, 'suite')
102 for test in Suite.find_and_parse_tests(cf_getter, predicate,
103 add_experimental=True):
104 suites.update(Suite.parse_tag(test.suite))
105 return list(suites)
106
107
108 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700109 def create_from_name(name, build, devserver, cf_getter=None, afe=None,
Simran Basic68cda42012-11-19 17:03:18 -0800110 tko=None, pool=None, results_dir=None,
Alex Millerb4d60ca2012-12-18 09:28:30 -0800111 max_runtime_mins=24*60):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700112 """
113 Create a Suite using a predicate based on the SUITE control file var.
114
115 Makes a predicate based on |name| and uses it to instantiate a Suite
116 that looks for tests in |autotest_dir| and will schedule them using
117 |afe|. Pulls control files from the default dev server.
118 Results will be pulled from |tko| upon completion.
119
120 @param name: a value of the SUITE control file variable to search for.
121 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700122 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700123 @param cf_getter: a control_file_getter.ControlFileGetter.
124 If None, default to using a DevServerGetter.
125 @param afe: an instance of AFE as defined in server/frontend.py.
126 @param tko: an instance of TKO as defined in server/frontend.py.
127 @param pool: Specify the pool of machines to use for scheduling
128 purposes.
129 @param results_dir: The directory where the job can write results to.
130 This must be set if you want job_id of sub-jobs
131 list in the job keyvals.
132 @return a Suite instance.
133 """
134 if cf_getter is None:
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700135 cf_getter = Suite.create_ds_getter(build, devserver)
136
Alex Millerb4d60ca2012-12-18 09:28:30 -0800137 return Suite(Suite.name_in_tag_predicate(name),
138 name, build, cf_getter, afe, tko, pool, results_dir)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700139
140
Chris Masone8906ab12012-07-23 15:37:56 -0700141 @staticmethod
142 def create_from_name_and_blacklist(name, blacklist, build, devserver,
143 cf_getter=None, afe=None, tko=None,
Simran Basic68cda42012-11-19 17:03:18 -0800144 pool=None, results_dir=None,
Alex Millerb4d60ca2012-12-18 09:28:30 -0800145 max_runtime_mins=24*60):
Chris Masone8906ab12012-07-23 15:37:56 -0700146 """
147 Create a Suite using a predicate based on the SUITE control file var.
148
149 Makes a predicate based on |name| and uses it to instantiate a Suite
150 that looks for tests in |autotest_dir| and will schedule them using
151 |afe|. Pulls control files from the default dev server.
152 Results will be pulled from |tko| upon completion.
153
154 @param name: a value of the SUITE control file variable to search for.
155 @param blacklist: iterable of control file paths to skip.
156 @param build: the build on which we're running this suite.
157 @param devserver: the devserver which contains the build.
158 @param cf_getter: a control_file_getter.ControlFileGetter.
159 If None, default to using a DevServerGetter.
160 @param afe: an instance of AFE as defined in server/frontend.py.
161 @param tko: an instance of TKO as defined in server/frontend.py.
162 @param pool: Specify the pool of machines to use for scheduling
163 purposes.
164 @param results_dir: The directory where the job can write results to.
165 This must be set if you want job_id of sub-jobs
166 list in the job keyvals.
167 @return a Suite instance.
168 """
169 if cf_getter is None:
170 cf_getter = Suite.create_ds_getter(build, devserver)
171
172 def in_tag_not_in_blacklist_predicate(test):
173 return (Suite.name_in_tag_predicate(name)(test) and
174 hasattr(test, 'path') and
175 True not in [b.endswith(test.path) for b in blacklist])
176
177 return Suite(in_tag_not_in_blacklist_predicate,
Simran Basic68cda42012-11-19 17:03:18 -0800178 name, build, cf_getter, afe, tko, pool, results_dir,
Alex Millerb4d60ca2012-12-18 09:28:30 -0800179 max_runtime_mins)
Chris Masone8906ab12012-07-23 15:37:56 -0700180
181
Chris Masone44e4d6c2012-08-15 14:25:53 -0700182 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Alex Millerb4d60ca2012-12-18 09:28:30 -0800183 pool=None, results_dir=None, max_runtime_mins=24*60):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700184 """
185 Constructor
186
187 @param predicate: a function that should return True when run over a
188 ControlData representation of a control file that should be in
189 this Suite.
190 @param tag: a string with which to tag jobs run in this suite.
191 @param build: the build on which we're running this suite.
192 @param cf_getter: a control_file_getter.ControlFileGetter
193 @param afe: an instance of AFE as defined in server/frontend.py.
194 @param tko: an instance of TKO as defined in server/frontend.py.
195 @param pool: Specify the pool of machines to use for scheduling
196 purposes.
197 @param results_dir: The directory where the job can write results to.
198 This must be set if you want job_id of sub-jobs
199 list in the job keyvals.
200 """
201 self._predicate = predicate
202 self._tag = tag
203 self._build = build
204 self._cf_getter = cf_getter
205 self._results_dir = results_dir
206 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
207 delay_sec=10,
208 debug=False)
209 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
210 delay_sec=10,
211 debug=False)
212 self._pool = pool
213 self._jobs = []
214 self._tests = Suite.find_and_parse_tests(self._cf_getter,
215 self._predicate,
216 add_experimental=True)
Simran Basic68cda42012-11-19 17:03:18 -0800217 self._max_runtime_mins = max_runtime_mins
Chris Masone44e4d6c2012-08-15 14:25:53 -0700218
219
220 @property
221 def tests(self):
222 """
223 A list of ControlData objects in the suite, with added |text| attr.
224 """
225 return self._tests
226
227
228 def stable_tests(self):
229 """
230 |self.tests|, filtered for non-experimental tests.
231 """
232 return filter(lambda t: not t.experimental, self.tests)
233
234
235 def unstable_tests(self):
236 """
237 |self.tests|, filtered for experimental tests.
238 """
239 return filter(lambda t: t.experimental, self.tests)
240
241
242 def _create_job(self, test):
243 """
244 Thin wrapper around frontend.AFE.create_job().
245
246 @param test: ControlData object for a test to run.
247 @return a frontend.Job object with an added test_name member.
248 test_name is used to preserve the higher level TEST_NAME
249 name of the job.
250 """
Chris Masone8906ab12012-07-23 15:37:56 -0700251 job_deps = list(test.dependencies)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700252 if self._pool:
253 meta_hosts = self._pool
254 cros_label = constants.VERSION_PREFIX + self._build
255 job_deps.append(cros_label)
256 else:
257 # No pool specified use any machines with the following label.
258 meta_hosts = constants.VERSION_PREFIX + self._build
259 test_obj = self._afe.create_job(
260 control_file=test.text,
261 name='/'.join([self._build, self._tag, test.name]),
262 control_type=test.test_type.capitalize(),
263 meta_hosts=[meta_hosts],
264 dependencies=job_deps,
265 keyvals={constants.JOB_BUILD_KEY: self._build,
Simran Basic68cda42012-11-19 17:03:18 -0800266 constants.JOB_SUITE_KEY: self._tag},
267 max_runtime_mins=self._max_runtime_mins)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700268
269 setattr(test_obj, 'test_name', test.name)
270
271 return test_obj
272
273
274 def run_and_wait(self, record, manager, add_experimental=True):
275 """
276 Synchronously run tests in |self.tests|.
277
278 Schedules tests against a device running image |self._build|, and
279 then polls for status, using |record| to print status when each
280 completes.
281
282 Tests returned by self.stable_tests() will always be run, while tests
283 in self.unstable_tests() will only be run if |add_experimental| is true.
284
285 @param record: callable that records job status.
286 prototype:
287 record(base_job.status_log_entry)
Chris Masone8906ab12012-07-23 15:37:56 -0700288 @param manager: a populated HostLockManager instance to handle
289 unlocking DUTs that we already reimaged.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700290 @param add_experimental: schedule experimental tests as well, or not.
291 """
292 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
293 logging.debug('Discovered %d unstable tests.',
294 len(self.unstable_tests()))
295 try:
296 Status('INFO', 'Start %s' % self._tag).record_result(record)
297 self.schedule(add_experimental)
298 # Unlock all hosts, so test jobs can be run on them.
299 manager.unlock()
300 try:
301 for result in job_status.wait_for_results(self._afe,
302 self._tko,
303 self._jobs):
304 result.record_all(record)
Chris Masoned9f13c52012-08-29 10:37:08 -0700305 if (self._results_dir and
306 job_status.is_for_infrastructure_fail(result)):
307 self._remember_provided_job_id(result)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700308
309 except Exception as e:
310 logging.error(traceback.format_exc())
311 Status('FAIL', self._tag,
312 'Exception waiting for results').record_result(record)
313 except Exception as e:
314 logging.error(traceback.format_exc())
315 Status('FAIL', self._tag,
316 'Exception while scheduling suite').record_result(record)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700317
318
319 def schedule(self, add_experimental=True):
320 """
321 Schedule jobs using |self._afe|.
322
323 frontend.Job objects representing each scheduled job will be put in
324 |self._jobs|.
325
326 @param add_experimental: schedule experimental tests as well, or not.
327 """
328 for test in self.stable_tests():
329 logging.debug('Scheduling %s', test.name)
330 self._jobs.append(self._create_job(test))
331
332 if add_experimental:
333 for test in self.unstable_tests():
334 logging.debug('Scheduling experimental %s', test.name)
335 test.name = constants.EXPERIMENTAL_PREFIX + test.name
336 self._jobs.append(self._create_job(test))
337 if self._results_dir:
Chris Masoned9f13c52012-08-29 10:37:08 -0700338 self._remember_scheduled_job_ids()
Chris Masone44e4d6c2012-08-15 14:25:53 -0700339
340
Chris Masoned9f13c52012-08-29 10:37:08 -0700341 def _remember_scheduled_job_ids(self):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700342 """
343 Record scheduled job ids as keyvals, so they can be referenced later.
344 """
345 for job in self._jobs:
Chris Masoned9f13c52012-08-29 10:37:08 -0700346 self._remember_provided_job_id(job)
347
348
349 def _remember_provided_job_id(self, job):
350 """
351 Record provided job as a suite job keyval, for later referencing.
352
353 @param job: some representation of a job, including id, test_name
354 and owner
355 """
356 if job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -0700357 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -0700358 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700359 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700360 utils.write_keyval(
361 self._results_dir,
362 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
363
364
365 @staticmethod
366 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
367 """
368 Function to scan through all tests and find eligible tests.
369
370 Looks at control files returned by _cf_getter.get_control_file_list()
371 for tests that pass self._predicate().
372
373 @param cf_getter: a control_file_getter.ControlFileGetter used to list
374 and fetch the content of control files
375 @param predicate: a function that should return True when run over a
376 ControlData representation of a control file that should be in
377 this Suite.
378 @param add_experimental: add tests with experimental attribute set.
379
380 @return list of ControlData objects that should be run, with control
381 file text added in |text| attribute.
382 """
383 tests = {}
384 files = cf_getter.get_control_file_list()
385 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
386 for file in filter(lambda f: not matcher.match(f), files):
387 logging.debug('Considering %s', file)
388 text = cf_getter.get_control_file_contents(file)
389 try:
390 found_test = control_data.parse_control_string(
391 text, raise_warnings=True)
392 if not add_experimental and found_test.experimental:
393 continue
394
395 found_test.text = text
396 found_test.path = file
397 tests[file] = found_test
398 except control_data.ControlVariableException, e:
399 logging.warn("Skipping %s\n%s", file, e)
400 except Exception, e:
401 logging.error("Bad %s\n%s", file, e)
402
403 return [test for test in tests.itervalues() if predicate(test)]