blob: 8d8e7b7a367b7663a6f8352c4fb450f70943bce9 [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
Alex Miller24ebb102012-10-29 09:59:50 -070017from autotest_lib.server.cros.dynamic_suite import reporting
Chris Masone44e4d6c2012-08-15 14:25:53 -070018from autotest_lib.server import frontend
19
20
21class Suite(object):
22 """
23 A suite of tests, defined by some predicate over control file variables.
24
25 Given a place to search for control files a predicate to match the desired
26 tests, can gather tests and fire off jobs to run them, and then wait for
27 results.
28
29 @var _predicate: a function that should return True when run over a
30 ControlData representation of a control file that should be in
31 this Suite.
32 @var _tag: a string with which to tag jobs run in this suite.
33 @var _build: the build on which we're running this suite.
34 @var _afe: an instance of AFE as defined in server/frontend.py.
35 @var _tko: an instance of TKO as defined in server/frontend.py.
36 @var _jobs: currently scheduled jobs, if any.
37 @var _cf_getter: a control_file_getter.ControlFileGetter
38 """
39
40
41 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -070042 def create_ds_getter(build, devserver):
Chris Masone44e4d6c2012-08-15 14:25:53 -070043 """
44 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -070045 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -070046 @return a FileSystemGetter instance that looks under |autotest_dir|.
47 """
Chris Sosaaccb5ce2012-08-30 17:29:15 -070048 return control_file_getter.DevServerGetter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -070049
50
51 @staticmethod
52 def create_fs_getter(autotest_dir):
53 """
54 @param autotest_dir: the place to find autotests.
55 @return a FileSystemGetter instance that looks under |autotest_dir|.
56 """
57 # currently hard-coded places to look for tests.
58 subpaths = ['server/site_tests', 'client/site_tests',
59 'server/tests', 'client/tests']
60 directories = [os.path.join(autotest_dir, p) for p in subpaths]
61 return control_file_getter.FileSystemGetter(directories)
62
63
64 @staticmethod
65 def parse_tag(tag):
66 """Splits a string on ',' optionally surrounded by whitespace."""
67 return map(lambda x: x.strip(), tag.split(','))
68
69
70 @staticmethod
71 def name_in_tag_predicate(name):
72 """Returns predicate that takes a control file and looks for |name|.
73
74 Builds a predicate that takes in a parsed control file (a ControlData)
75 and returns True if the SUITE tag is present and contains |name|.
76
77 @param name: the suite name to base the predicate on.
78 @return a callable that takes a ControlData and looks for |name| in that
79 ControlData object's suite member.
80 """
81 return lambda t: hasattr(t, 'suite') and \
82 name in Suite.parse_tag(t.suite)
83
84
85 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -070086 def list_all_suites(build, devserver, cf_getter=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -070087 """
88 Parses all ControlData objects with a SUITE tag and extracts all
89 defined suite names.
90
Chris Sosaaccb5ce2012-08-30 17:29:15 -070091 @param build: the build on which we're running this suite.
92 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -070093 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
94 using DevServerGetter.
95
96 @return list of suites
97 """
98 if cf_getter is None:
Chris Sosaaccb5ce2012-08-30 17:29:15 -070099 cf_getter = Suite.create_ds_getter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700100
101 suites = set()
102 predicate = lambda t: hasattr(t, 'suite')
103 for test in Suite.find_and_parse_tests(cf_getter, predicate,
104 add_experimental=True):
105 suites.update(Suite.parse_tag(test.suite))
106 return list(suites)
107
108
109 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700110 def create_from_name(name, build, devserver, cf_getter=None, afe=None,
Simran Basic68cda42012-11-19 17:03:18 -0800111 tko=None, pool=None, results_dir=None,
Alex Miller24ebb102012-10-29 09:59:50 -0700112 max_runtime_mins=24*60, file_bugs=False):
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.
Alex Miller24ebb102012-10-29 09:59:50 -0700133 @param file_bugs: True if we should file bugs on test failures for
134 this suite run.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700135 @return a Suite instance.
136 """
137 if cf_getter is None:
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700138 cf_getter = Suite.create_ds_getter(build, devserver)
139
Alex Miller24ebb102012-10-29 09:59:50 -0700140 return Suite(Suite.name_in_tag_predicate(name), name, build, cf_getter,
141 afe, tko, pool, results_dir, file_bugs)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700142
143
Chris Masone8906ab12012-07-23 15:37:56 -0700144 @staticmethod
145 def create_from_name_and_blacklist(name, blacklist, build, devserver,
146 cf_getter=None, afe=None, tko=None,
Simran Basic68cda42012-11-19 17:03:18 -0800147 pool=None, results_dir=None,
Alex Miller24ebb102012-10-29 09:59:50 -0700148 max_runtime_mins=24*60, file_bugs=False):
Chris Masone8906ab12012-07-23 15:37:56 -0700149 """
150 Create a Suite using a predicate based on the SUITE control file var.
151
152 Makes a predicate based on |name| and uses it to instantiate a Suite
153 that looks for tests in |autotest_dir| and will schedule them using
154 |afe|. Pulls control files from the default dev server.
155 Results will be pulled from |tko| upon completion.
156
157 @param name: a value of the SUITE control file variable to search for.
158 @param blacklist: iterable of control file paths to skip.
159 @param build: the build on which we're running this suite.
160 @param devserver: the devserver which contains the build.
161 @param cf_getter: a control_file_getter.ControlFileGetter.
162 If None, default to using a DevServerGetter.
163 @param afe: an instance of AFE as defined in server/frontend.py.
164 @param tko: an instance of TKO as defined in server/frontend.py.
165 @param pool: Specify the pool of machines to use for scheduling
166 purposes.
167 @param results_dir: The directory where the job can write results to.
168 This must be set if you want job_id of sub-jobs
169 list in the job keyvals.
Alex Miller24ebb102012-10-29 09:59:50 -0700170 @param file_bugs: True if we should file bugs on test failures for
171 this suite run.
Chris Masone8906ab12012-07-23 15:37:56 -0700172 @return a Suite instance.
173 """
174 if cf_getter is None:
175 cf_getter = Suite.create_ds_getter(build, devserver)
176
177 def in_tag_not_in_blacklist_predicate(test):
178 return (Suite.name_in_tag_predicate(name)(test) and
179 hasattr(test, 'path') and
180 True not in [b.endswith(test.path) for b in blacklist])
181
182 return Suite(in_tag_not_in_blacklist_predicate,
Simran Basic68cda42012-11-19 17:03:18 -0800183 name, build, cf_getter, afe, tko, pool, results_dir,
Alex Miller24ebb102012-10-29 09:59:50 -0700184 max_runtime_mins, file_bugs)
Chris Masone8906ab12012-07-23 15:37:56 -0700185
186
Chris Masone44e4d6c2012-08-15 14:25:53 -0700187 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Alex Miller24ebb102012-10-29 09:59:50 -0700188 pool=None, results_dir=None, max_runtime_mins=24*60,
189 file_bugs=False):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700190 """
191 Constructor
192
193 @param predicate: a function that should return True when run over a
194 ControlData representation of a control file that should be in
195 this Suite.
196 @param tag: a string with which to tag jobs run in this suite.
197 @param build: the build on which we're running this suite.
198 @param cf_getter: a control_file_getter.ControlFileGetter
199 @param afe: an instance of AFE as defined in server/frontend.py.
200 @param tko: an instance of TKO as defined in server/frontend.py.
201 @param pool: Specify the pool of machines to use for scheduling
202 purposes.
203 @param results_dir: The directory where the job can write results to.
204 This must be set if you want job_id of sub-jobs
205 list in the job keyvals.
206 """
207 self._predicate = predicate
208 self._tag = tag
209 self._build = build
210 self._cf_getter = cf_getter
211 self._results_dir = results_dir
212 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
213 delay_sec=10,
214 debug=False)
215 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
216 delay_sec=10,
217 debug=False)
218 self._pool = pool
219 self._jobs = []
220 self._tests = Suite.find_and_parse_tests(self._cf_getter,
221 self._predicate,
222 add_experimental=True)
Simran Basic68cda42012-11-19 17:03:18 -0800223 self._max_runtime_mins = max_runtime_mins
Alex Miller24ebb102012-10-29 09:59:50 -0700224 self._file_bugs = file_bugs
Chris Masone44e4d6c2012-08-15 14:25:53 -0700225
226
227 @property
228 def tests(self):
229 """
230 A list of ControlData objects in the suite, with added |text| attr.
231 """
232 return self._tests
233
234
235 def stable_tests(self):
236 """
237 |self.tests|, filtered for non-experimental tests.
238 """
239 return filter(lambda t: not t.experimental, self.tests)
240
241
242 def unstable_tests(self):
243 """
244 |self.tests|, filtered for experimental tests.
245 """
246 return filter(lambda t: t.experimental, self.tests)
247
248
249 def _create_job(self, test):
250 """
251 Thin wrapper around frontend.AFE.create_job().
252
253 @param test: ControlData object for a test to run.
254 @return a frontend.Job object with an added test_name member.
255 test_name is used to preserve the higher level TEST_NAME
256 name of the job.
257 """
Chris Masone8906ab12012-07-23 15:37:56 -0700258 job_deps = list(test.dependencies)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700259 if self._pool:
260 meta_hosts = self._pool
261 cros_label = constants.VERSION_PREFIX + self._build
262 job_deps.append(cros_label)
263 else:
264 # No pool specified use any machines with the following label.
265 meta_hosts = constants.VERSION_PREFIX + self._build
266 test_obj = self._afe.create_job(
267 control_file=test.text,
268 name='/'.join([self._build, self._tag, test.name]),
269 control_type=test.test_type.capitalize(),
270 meta_hosts=[meta_hosts],
271 dependencies=job_deps,
272 keyvals={constants.JOB_BUILD_KEY: self._build,
Simran Basic68cda42012-11-19 17:03:18 -0800273 constants.JOB_SUITE_KEY: self._tag},
274 max_runtime_mins=self._max_runtime_mins)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700275
276 setattr(test_obj, 'test_name', test.name)
277
278 return test_obj
279
280
281 def run_and_wait(self, record, manager, add_experimental=True):
282 """
283 Synchronously run tests in |self.tests|.
284
285 Schedules tests against a device running image |self._build|, and
286 then polls for status, using |record| to print status when each
287 completes.
288
289 Tests returned by self.stable_tests() will always be run, while tests
290 in self.unstable_tests() will only be run if |add_experimental| is true.
291
292 @param record: callable that records job status.
293 prototype:
294 record(base_job.status_log_entry)
Chris Masone8906ab12012-07-23 15:37:56 -0700295 @param manager: a populated HostLockManager instance to handle
296 unlocking DUTs that we already reimaged.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700297 @param add_experimental: schedule experimental tests as well, or not.
298 """
299 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
300 logging.debug('Discovered %d unstable tests.',
301 len(self.unstable_tests()))
302 try:
Alex Miller24ebb102012-10-29 09:59:50 -0700303 if self._file_bugs:
304 bug_reporter = reporting.Reporter()
Chris Masone44e4d6c2012-08-15 14:25:53 -0700305 Status('INFO', 'Start %s' % self._tag).record_result(record)
306 self.schedule(add_experimental)
307 # Unlock all hosts, so test jobs can be run on them.
308 manager.unlock()
309 try:
310 for result in job_status.wait_for_results(self._afe,
311 self._tko,
312 self._jobs):
313 result.record_all(record)
Chris Masoned9f13c52012-08-29 10:37:08 -0700314 if (self._results_dir and
315 job_status.is_for_infrastructure_fail(result)):
316 self._remember_provided_job_id(result)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700317
Alex Miller24ebb102012-10-29 09:59:50 -0700318 # I'd love to grab the actual tko test object here, as that
319 # includes almost all of the needed information: test name,
320 # status, reason, etc. However, doing so would cause a
321 # bunch of database traffic to grab data that we already
322 # have laying around in memory across several objects here.
323 worse = result.is_worse_than(job_status.Status("WARN", ""))
324 if self._file_bugs and worse:
325 failure = reporting.TestFailure(build=self._build,
326 suite=self._tag,
327 test=result.test_name,
328 reason=result.reason)
329 bug_reporter.report(failure)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700330 except Exception as e:
331 logging.error(traceback.format_exc())
332 Status('FAIL', self._tag,
333 'Exception waiting for results').record_result(record)
334 except Exception as e:
335 logging.error(traceback.format_exc())
336 Status('FAIL', self._tag,
337 'Exception while scheduling suite').record_result(record)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700338
339
340 def schedule(self, add_experimental=True):
341 """
342 Schedule jobs using |self._afe|.
343
344 frontend.Job objects representing each scheduled job will be put in
345 |self._jobs|.
346
347 @param add_experimental: schedule experimental tests as well, or not.
348 """
349 for test in self.stable_tests():
350 logging.debug('Scheduling %s', test.name)
351 self._jobs.append(self._create_job(test))
352
353 if add_experimental:
354 for test in self.unstable_tests():
355 logging.debug('Scheduling experimental %s', test.name)
356 test.name = constants.EXPERIMENTAL_PREFIX + test.name
357 self._jobs.append(self._create_job(test))
358 if self._results_dir:
Chris Masoned9f13c52012-08-29 10:37:08 -0700359 self._remember_scheduled_job_ids()
Chris Masone44e4d6c2012-08-15 14:25:53 -0700360
361
Chris Masoned9f13c52012-08-29 10:37:08 -0700362 def _remember_scheduled_job_ids(self):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700363 """
364 Record scheduled job ids as keyvals, so they can be referenced later.
365 """
366 for job in self._jobs:
Chris Masoned9f13c52012-08-29 10:37:08 -0700367 self._remember_provided_job_id(job)
368
369
370 def _remember_provided_job_id(self, job):
371 """
372 Record provided job as a suite job keyval, for later referencing.
373
374 @param job: some representation of a job, including id, test_name
375 and owner
376 """
377 if job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -0700378 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -0700379 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700380 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700381 utils.write_keyval(
382 self._results_dir,
383 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
384
385
386 @staticmethod
387 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
388 """
389 Function to scan through all tests and find eligible tests.
390
391 Looks at control files returned by _cf_getter.get_control_file_list()
392 for tests that pass self._predicate().
393
394 @param cf_getter: a control_file_getter.ControlFileGetter used to list
395 and fetch the content of control files
396 @param predicate: a function that should return True when run over a
397 ControlData representation of a control file that should be in
398 this Suite.
399 @param add_experimental: add tests with experimental attribute set.
400
401 @return list of ControlData objects that should be run, with control
402 file text added in |text| attribute.
403 """
404 tests = {}
405 files = cf_getter.get_control_file_list()
406 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
407 for file in filter(lambda f: not matcher.match(f), files):
408 logging.debug('Considering %s', file)
409 text = cf_getter.get_control_file_contents(file)
410 try:
411 found_test = control_data.parse_control_string(
412 text, raise_warnings=True)
413 if not add_experimental and found_test.experimental:
414 continue
415
416 found_test.text = text
417 found_test.path = file
418 tests[file] = found_test
419 except control_data.ControlVariableException, e:
420 logging.warn("Skipping %s\n%s", file, e)
421 except Exception, e:
422 logging.error("Bad %s\n%s", file, e)
423
424 return [test for test in tests.itervalues() if predicate(test)]