blob: 05eab950d7cf09a320fd9df96c8db74b05b44bf8 [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,
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800111 max_runtime_mins=24*60,
Alex Millera6e33a02013-01-16 15:26:14 -0800112 version_prefix=constants.VERSION_PREFIX):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700113 """
114 Create a Suite using a predicate based on the SUITE control file var.
115
116 Makes a predicate based on |name| and uses it to instantiate a Suite
117 that looks for tests in |autotest_dir| and will schedule them using
118 |afe|. Pulls control files from the default dev server.
119 Results will be pulled from |tko| upon completion.
120
121 @param name: a value of the SUITE control file variable to search for.
122 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700123 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700124 @param cf_getter: a control_file_getter.ControlFileGetter.
125 If None, default to using a DevServerGetter.
126 @param afe: an instance of AFE as defined in server/frontend.py.
127 @param tko: an instance of TKO as defined in server/frontend.py.
128 @param pool: Specify the pool of machines to use for scheduling
129 purposes.
130 @param results_dir: The directory where the job can write results to.
131 This must be set if you want job_id of sub-jobs
132 list in the job keyvals.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800133 @param version_prefix: a string, a prefix to be concatenated with the
134 build name to form a label which the DUT needs
135 to be labeled with to be eligible to run this
136 test.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700137 @return a Suite instance.
138 """
139 if cf_getter is None:
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700140 cf_getter = Suite.create_ds_getter(build, devserver)
141
Alex Millera6e33a02013-01-16 15:26:14 -0800142 return Suite(Suite.name_in_tag_predicate(name),
143 name, build, cf_getter, afe, tko, pool, results_dir,
144 version_prefix=version_prefix)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700145
146
Chris Masone8906ab12012-07-23 15:37:56 -0700147 @staticmethod
148 def create_from_name_and_blacklist(name, blacklist, build, devserver,
149 cf_getter=None, afe=None, tko=None,
Simran Basic68cda42012-11-19 17:03:18 -0800150 pool=None, results_dir=None,
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800151 max_runtime_mins=24*60,
Alex Millera6e33a02013-01-16 15:26:14 -0800152 version_prefix=constants.VERSION_PREFIX):
Chris Masone8906ab12012-07-23 15:37:56 -0700153 """
154 Create a Suite using a predicate based on the SUITE control file var.
155
156 Makes a predicate based on |name| and uses it to instantiate a Suite
157 that looks for tests in |autotest_dir| and will schedule them using
158 |afe|. Pulls control files from the default dev server.
159 Results will be pulled from |tko| upon completion.
160
161 @param name: a value of the SUITE control file variable to search for.
162 @param blacklist: iterable of control file paths to skip.
163 @param build: the build on which we're running this suite.
164 @param devserver: the devserver which contains the build.
165 @param cf_getter: a control_file_getter.ControlFileGetter.
166 If None, default to using a DevServerGetter.
167 @param afe: an instance of AFE as defined in server/frontend.py.
168 @param tko: an instance of TKO as defined in server/frontend.py.
169 @param pool: Specify the pool of machines to use for scheduling
170 purposes.
171 @param results_dir: The directory where the job can write results to.
172 This must be set if you want job_id of sub-jobs
173 list in the job keyvals.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800174 @param version_prefix: a string, a prefix to be concatenated with the
175 build name to form a label which the DUT needs
176 to be labeled with to be eligible to run this
177 test.
Chris Masone8906ab12012-07-23 15:37:56 -0700178 @return a Suite instance.
179 """
180 if cf_getter is None:
181 cf_getter = Suite.create_ds_getter(build, devserver)
182
183 def in_tag_not_in_blacklist_predicate(test):
184 return (Suite.name_in_tag_predicate(name)(test) and
185 hasattr(test, 'path') and
186 True not in [b.endswith(test.path) for b in blacklist])
187
188 return Suite(in_tag_not_in_blacklist_predicate,
Simran Basic68cda42012-11-19 17:03:18 -0800189 name, build, cf_getter, afe, tko, pool, results_dir,
Alex Millera6e33a02013-01-16 15:26:14 -0800190 max_runtime_mins, version_prefix)
Chris Masone8906ab12012-07-23 15:37:56 -0700191
192
Chris Masone44e4d6c2012-08-15 14:25:53 -0700193 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800194 pool=None, results_dir=None, max_runtime_mins=24*60,
Alex Millera6e33a02013-01-16 15:26:14 -0800195 version_prefix=constants.VERSION_PREFIX):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700196 """
197 Constructor
198
199 @param predicate: a function that should return True when run over a
200 ControlData representation of a control file that should be in
201 this Suite.
202 @param tag: a string with which to tag jobs run in this suite.
203 @param build: the build on which we're running this suite.
204 @param cf_getter: a control_file_getter.ControlFileGetter
205 @param afe: an instance of AFE as defined in server/frontend.py.
206 @param tko: an instance of TKO as defined in server/frontend.py.
207 @param pool: Specify the pool of machines to use for scheduling
208 purposes.
209 @param results_dir: The directory where the job can write results to.
210 This must be set if you want job_id of sub-jobs
211 list in the job keyvals.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800212 @param version_prefix: a string, prefix for the database label
213 associated with the build
Chris Masone44e4d6c2012-08-15 14:25:53 -0700214 """
215 self._predicate = predicate
216 self._tag = tag
217 self._build = build
218 self._cf_getter = cf_getter
219 self._results_dir = results_dir
220 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
221 delay_sec=10,
222 debug=False)
223 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
224 delay_sec=10,
225 debug=False)
226 self._pool = pool
227 self._jobs = []
228 self._tests = Suite.find_and_parse_tests(self._cf_getter,
229 self._predicate,
230 add_experimental=True)
Simran Basic68cda42012-11-19 17:03:18 -0800231 self._max_runtime_mins = max_runtime_mins
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800232 self._version_prefix = version_prefix
Chris Masone44e4d6c2012-08-15 14:25:53 -0700233
234 @property
235 def tests(self):
236 """
237 A list of ControlData objects in the suite, with added |text| attr.
238 """
239 return self._tests
240
241
242 def stable_tests(self):
243 """
244 |self.tests|, filtered for non-experimental tests.
245 """
246 return filter(lambda t: not t.experimental, self.tests)
247
248
249 def unstable_tests(self):
250 """
251 |self.tests|, filtered for experimental tests.
252 """
253 return filter(lambda t: t.experimental, self.tests)
254
255
256 def _create_job(self, test):
257 """
258 Thin wrapper around frontend.AFE.create_job().
259
260 @param test: ControlData object for a test to run.
261 @return a frontend.Job object with an added test_name member.
262 test_name is used to preserve the higher level TEST_NAME
263 name of the job.
264 """
Chris Masone8906ab12012-07-23 15:37:56 -0700265 job_deps = list(test.dependencies)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700266 if self._pool:
267 meta_hosts = self._pool
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800268 cros_label = self._version_prefix + self._build
Chris Masone44e4d6c2012-08-15 14:25:53 -0700269 job_deps.append(cros_label)
270 else:
271 # No pool specified use any machines with the following label.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800272 meta_hosts = self._version_prefix + self._build
Chris Masone44e4d6c2012-08-15 14:25:53 -0700273 test_obj = self._afe.create_job(
274 control_file=test.text,
275 name='/'.join([self._build, self._tag, test.name]),
276 control_type=test.test_type.capitalize(),
277 meta_hosts=[meta_hosts],
278 dependencies=job_deps,
279 keyvals={constants.JOB_BUILD_KEY: self._build,
Simran Basic68cda42012-11-19 17:03:18 -0800280 constants.JOB_SUITE_KEY: self._tag},
281 max_runtime_mins=self._max_runtime_mins)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700282
283 setattr(test_obj, 'test_name', test.name)
284
285 return test_obj
286
287
288 def run_and_wait(self, record, manager, add_experimental=True):
289 """
290 Synchronously run tests in |self.tests|.
291
292 Schedules tests against a device running image |self._build|, and
293 then polls for status, using |record| to print status when each
294 completes.
295
296 Tests returned by self.stable_tests() will always be run, while tests
297 in self.unstable_tests() will only be run if |add_experimental| is true.
298
299 @param record: callable that records job status.
300 prototype:
301 record(base_job.status_log_entry)
Chris Masone8906ab12012-07-23 15:37:56 -0700302 @param manager: a populated HostLockManager instance to handle
303 unlocking DUTs that we already reimaged.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700304 @param add_experimental: schedule experimental tests as well, or not.
305 """
306 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
307 logging.debug('Discovered %d unstable tests.',
308 len(self.unstable_tests()))
309 try:
310 Status('INFO', 'Start %s' % self._tag).record_result(record)
311 self.schedule(add_experimental)
312 # Unlock all hosts, so test jobs can be run on them.
313 manager.unlock()
314 try:
315 for result in job_status.wait_for_results(self._afe,
316 self._tko,
317 self._jobs):
318 result.record_all(record)
Chris Masoned9f13c52012-08-29 10:37:08 -0700319 if (self._results_dir and
320 job_status.is_for_infrastructure_fail(result)):
321 self._remember_provided_job_id(result)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700322
323 except Exception as e:
324 logging.error(traceback.format_exc())
325 Status('FAIL', self._tag,
326 'Exception waiting for results').record_result(record)
327 except Exception as e:
328 logging.error(traceback.format_exc())
329 Status('FAIL', self._tag,
330 'Exception while scheduling suite').record_result(record)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700331
332
333 def schedule(self, add_experimental=True):
334 """
335 Schedule jobs using |self._afe|.
336
337 frontend.Job objects representing each scheduled job will be put in
338 |self._jobs|.
339
340 @param add_experimental: schedule experimental tests as well, or not.
341 """
342 for test in self.stable_tests():
343 logging.debug('Scheduling %s', test.name)
344 self._jobs.append(self._create_job(test))
345
346 if add_experimental:
347 for test in self.unstable_tests():
348 logging.debug('Scheduling experimental %s', test.name)
349 test.name = constants.EXPERIMENTAL_PREFIX + test.name
350 self._jobs.append(self._create_job(test))
351 if self._results_dir:
Chris Masoned9f13c52012-08-29 10:37:08 -0700352 self._remember_scheduled_job_ids()
Chris Masone44e4d6c2012-08-15 14:25:53 -0700353
354
Chris Masoned9f13c52012-08-29 10:37:08 -0700355 def _remember_scheduled_job_ids(self):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700356 """
357 Record scheduled job ids as keyvals, so they can be referenced later.
358 """
359 for job in self._jobs:
Chris Masoned9f13c52012-08-29 10:37:08 -0700360 self._remember_provided_job_id(job)
361
362
363 def _remember_provided_job_id(self, job):
364 """
365 Record provided job as a suite job keyval, for later referencing.
366
367 @param job: some representation of a job, including id, test_name
368 and owner
369 """
370 if job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -0700371 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -0700372 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700373 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700374 utils.write_keyval(
375 self._results_dir,
376 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
377
378
379 @staticmethod
380 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
381 """
382 Function to scan through all tests and find eligible tests.
383
384 Looks at control files returned by _cf_getter.get_control_file_list()
385 for tests that pass self._predicate().
386
387 @param cf_getter: a control_file_getter.ControlFileGetter used to list
388 and fetch the content of control files
389 @param predicate: a function that should return True when run over a
390 ControlData representation of a control file that should be in
391 this Suite.
392 @param add_experimental: add tests with experimental attribute set.
393
394 @return list of ControlData objects that should be run, with control
395 file text added in |text| attribute.
396 """
397 tests = {}
398 files = cf_getter.get_control_file_list()
399 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
400 for file in filter(lambda f: not matcher.match(f), files):
401 logging.debug('Considering %s', file)
402 text = cf_getter.get_control_file_contents(file)
403 try:
404 found_test = control_data.parse_control_string(
405 text, raise_warnings=True)
406 if not add_experimental and found_test.experimental:
407 continue
408
409 found_test.text = text
410 found_test.path = file
411 tests[file] = found_test
412 except control_data.ControlVariableException, e:
413 logging.warn("Skipping %s\n%s", file, e)
414 except Exception, e:
415 logging.error("Bad %s\n%s", file, e)
416
417 return [test for test in tests.itervalues() if predicate(test)]