blob: 53dd998b88604a52e64fb3776bd2d3379d78834b [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
Alex Miller3a69adc2012-12-19 13:38:31 -08005import hashlib, logging, os, re, traceback
Chris Masone44e4d6c2012-08-15 14:25:53 -07006
7import common
8
Alex Miller3a69adc2012-12-19 13:38:31 -08009from autotest_lib.client.common_lib import control_data
Chris Masone44e4d6c2012-08-15 14:25:53 -070010from autotest_lib.client.common_lib import utils
Chris Masone44e4d6c2012-08-15 14:25:53 -070011from autotest_lib.server.cros.dynamic_suite import constants
12from autotest_lib.server.cros.dynamic_suite import control_file_getter
13from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Alex Miller3a69adc2012-12-19 13:38:31 -080014from autotest_lib.server.cros.dynamic_suite import job_status
Chris Masone44e4d6c2012-08-15 14:25:53 -070015from autotest_lib.server.cros.dynamic_suite.job_status import Status
Chris Masone44e4d6c2012-08-15 14:25:53 -070016
17
18class Suite(object):
19 """
20 A suite of tests, defined by some predicate over control file variables.
21
22 Given a place to search for control files a predicate to match the desired
23 tests, can gather tests and fire off jobs to run them, and then wait for
24 results.
25
26 @var _predicate: a function that should return True when run over a
27 ControlData representation of a control file that should be in
28 this Suite.
29 @var _tag: a string with which to tag jobs run in this suite.
30 @var _build: the build on which we're running this suite.
31 @var _afe: an instance of AFE as defined in server/frontend.py.
32 @var _tko: an instance of TKO as defined in server/frontend.py.
33 @var _jobs: currently scheduled jobs, if any.
34 @var _cf_getter: a control_file_getter.ControlFileGetter
35 """
36
37
38 @staticmethod
Chris Sosaaccb5ce2012-08-30 17:29:15 -070039 def create_ds_getter(build, devserver):
Chris Masone44e4d6c2012-08-15 14:25:53 -070040 """
41 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -070042 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -070043 @return a FileSystemGetter instance that looks under |autotest_dir|.
44 """
Chris Sosaaccb5ce2012-08-30 17:29:15 -070045 return control_file_getter.DevServerGetter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -070046
47
48 @staticmethod
49 def create_fs_getter(autotest_dir):
50 """
51 @param autotest_dir: the place to find autotests.
52 @return a FileSystemGetter instance that looks under |autotest_dir|.
53 """
54 # currently hard-coded places to look for tests.
55 subpaths = ['server/site_tests', 'client/site_tests',
56 'server/tests', 'client/tests']
57 directories = [os.path.join(autotest_dir, p) for p in subpaths]
58 return control_file_getter.FileSystemGetter(directories)
59
60
61 @staticmethod
62 def parse_tag(tag):
Aviv Keshet18308922013-02-19 17:49:49 -080063 """Splits a string on ',' optionally surrounded by whitespace.
64 @param tag: string to split.
65 """
Chris Masone44e4d6c2012-08-15 14:25:53 -070066 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 Millera3a4fe72013-01-22 09:57:47 -0800112 version_prefix=constants.VERSION_PREFIX,
113 file_bugs=False):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700114 """
115 Create a Suite using a predicate based on the SUITE control file var.
116
117 Makes a predicate based on |name| and uses it to instantiate a Suite
118 that looks for tests in |autotest_dir| and will schedule them using
119 |afe|. Pulls control files from the default dev server.
120 Results will be pulled from |tko| upon completion.
121
122 @param name: a value of the SUITE control file variable to search for.
123 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700124 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700125 @param cf_getter: a control_file_getter.ControlFileGetter.
126 If None, default to using a DevServerGetter.
127 @param afe: an instance of AFE as defined in server/frontend.py.
128 @param tko: an instance of TKO as defined in server/frontend.py.
129 @param pool: Specify the pool of machines to use for scheduling
130 purposes.
131 @param results_dir: The directory where the job can write results to.
132 This must be set if you want job_id of sub-jobs
133 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -0800134 @param max_runtime_mins: Maximum suite runtime, in minutes.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800135 @param version_prefix: a string, a prefix to be concatenated with the
136 build name to form a label which the DUT needs
137 to be labeled with to be eligible to run this
138 test.
Alex Millera3a4fe72013-01-22 09:57:47 -0800139 @param file_bugs: True if we should file bugs on test failures for
140 this suite run.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700141 @return a Suite instance.
142 """
143 if cf_getter is None:
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700144 cf_getter = Suite.create_ds_getter(build, devserver)
145
Alex Millera6e33a02013-01-16 15:26:14 -0800146 return Suite(Suite.name_in_tag_predicate(name),
147 name, build, cf_getter, afe, tko, pool, results_dir,
Alex Millera3a4fe72013-01-22 09:57:47 -0800148 max_runtime_mins, version_prefix, file_bugs)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700149
150
Chris Masone8906ab12012-07-23 15:37:56 -0700151 @staticmethod
152 def create_from_name_and_blacklist(name, blacklist, build, devserver,
153 cf_getter=None, afe=None, tko=None,
Simran Basic68cda42012-11-19 17:03:18 -0800154 pool=None, results_dir=None,
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800155 max_runtime_mins=24*60,
Alex Millera3a4fe72013-01-22 09:57:47 -0800156 version_prefix=constants.VERSION_PREFIX,
Aviv Keshet18308922013-02-19 17:49:49 -0800157 file_bugs=False,
158 suite_job_id=None):
Chris Masone8906ab12012-07-23 15:37:56 -0700159 """
160 Create a Suite using a predicate based on the SUITE control file var.
161
162 Makes a predicate based on |name| and uses it to instantiate a Suite
163 that looks for tests in |autotest_dir| and will schedule them using
164 |afe|. Pulls control files from the default dev server.
165 Results will be pulled from |tko| upon completion.
166
167 @param name: a value of the SUITE control file variable to search for.
168 @param blacklist: iterable of control file paths to skip.
169 @param build: the build on which we're running this suite.
170 @param devserver: the devserver which contains the build.
171 @param cf_getter: a control_file_getter.ControlFileGetter.
172 If None, default to using a DevServerGetter.
173 @param afe: an instance of AFE as defined in server/frontend.py.
174 @param tko: an instance of TKO as defined in server/frontend.py.
175 @param pool: Specify the pool of machines to use for scheduling
176 purposes.
177 @param results_dir: The directory where the job can write results to.
178 This must be set if you want job_id of sub-jobs
179 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -0800180 @param max_runtime_mins: Maximum suite runtime, in minutes.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800181 @param version_prefix: a string, a prefix to be concatenated with the
182 build name to form a label which the DUT needs
183 to be labeled with to be eligible to run this
184 test.
Alex Millera3a4fe72013-01-22 09:57:47 -0800185 @param file_bugs: True if we should file bugs on test failures for
186 this suite run.
Aviv Keshet18308922013-02-19 17:49:49 -0800187 @param suite_job_id: Job id that will act as parent id to all sub jobs.
188 Default: None
Chris Masone8906ab12012-07-23 15:37:56 -0700189 @return a Suite instance.
190 """
191 if cf_getter is None:
192 cf_getter = Suite.create_ds_getter(build, devserver)
193
194 def in_tag_not_in_blacklist_predicate(test):
Aviv Keshet18308922013-02-19 17:49:49 -0800195 #pylint: disable-msg=C0111
Chris Masone8906ab12012-07-23 15:37:56 -0700196 return (Suite.name_in_tag_predicate(name)(test) and
197 hasattr(test, 'path') and
198 True not in [b.endswith(test.path) for b in blacklist])
199
200 return Suite(in_tag_not_in_blacklist_predicate,
Simran Basic68cda42012-11-19 17:03:18 -0800201 name, build, cf_getter, afe, tko, pool, results_dir,
Aviv Keshet18308922013-02-19 17:49:49 -0800202 max_runtime_mins, version_prefix, file_bugs, suite_job_id)
Chris Masone8906ab12012-07-23 15:37:56 -0700203
204
Chris Masone44e4d6c2012-08-15 14:25:53 -0700205 def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800206 pool=None, results_dir=None, max_runtime_mins=24*60,
Alex Millera3a4fe72013-01-22 09:57:47 -0800207 version_prefix=constants.VERSION_PREFIX,
Aviv Keshet18308922013-02-19 17:49:49 -0800208 file_bugs=False, suite_job_id=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700209 """
210 Constructor
211
212 @param predicate: a function that should return True when run over a
213 ControlData representation of a control file that should be in
214 this Suite.
215 @param tag: a string with which to tag jobs run in this suite.
216 @param build: the build on which we're running this suite.
217 @param cf_getter: a control_file_getter.ControlFileGetter
218 @param afe: an instance of AFE as defined in server/frontend.py.
219 @param tko: an instance of TKO as defined in server/frontend.py.
220 @param pool: Specify the pool of machines to use for scheduling
221 purposes.
222 @param results_dir: The directory where the job can write results to.
223 This must be set if you want job_id of sub-jobs
224 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -0800225 @param max_runtime_mins: Maximum suite runtime, in minutes.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800226 @param version_prefix: a string, prefix for the database label
227 associated with the build
Aviv Keshet18308922013-02-19 17:49:49 -0800228 @param suite_job_id: Job id that will act as parent id to all sub jobs.
229 Default: None
Chris Masone44e4d6c2012-08-15 14:25:53 -0700230 """
231 self._predicate = predicate
232 self._tag = tag
233 self._build = build
234 self._cf_getter = cf_getter
235 self._results_dir = results_dir
236 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
237 delay_sec=10,
238 debug=False)
239 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
240 delay_sec=10,
241 debug=False)
242 self._pool = pool
243 self._jobs = []
244 self._tests = Suite.find_and_parse_tests(self._cf_getter,
245 self._predicate,
246 add_experimental=True)
Simran Basic68cda42012-11-19 17:03:18 -0800247 self._max_runtime_mins = max_runtime_mins
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800248 self._version_prefix = version_prefix
Alex Millera3a4fe72013-01-22 09:57:47 -0800249 self._file_bugs = file_bugs
Aviv Keshet18308922013-02-19 17:49:49 -0800250 self._suite_job_id = suite_job_id
Alex Millera3a4fe72013-01-22 09:57:47 -0800251
Chris Masone44e4d6c2012-08-15 14:25:53 -0700252
253 @property
254 def tests(self):
255 """
256 A list of ControlData objects in the suite, with added |text| attr.
257 """
258 return self._tests
259
260
261 def stable_tests(self):
262 """
263 |self.tests|, filtered for non-experimental tests.
264 """
265 return filter(lambda t: not t.experimental, self.tests)
266
267
268 def unstable_tests(self):
269 """
270 |self.tests|, filtered for experimental tests.
271 """
272 return filter(lambda t: t.experimental, self.tests)
273
274
275 def _create_job(self, test):
276 """
277 Thin wrapper around frontend.AFE.create_job().
278
279 @param test: ControlData object for a test to run.
280 @return a frontend.Job object with an added test_name member.
281 test_name is used to preserve the higher level TEST_NAME
282 name of the job.
283 """
Chris Masone8906ab12012-07-23 15:37:56 -0700284 job_deps = list(test.dependencies)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700285 if self._pool:
286 meta_hosts = self._pool
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800287 cros_label = self._version_prefix + self._build
Chris Masone44e4d6c2012-08-15 14:25:53 -0700288 job_deps.append(cros_label)
289 else:
290 # No pool specified use any machines with the following label.
Vadim Bendeburyab14bf12012-12-28 13:51:46 -0800291 meta_hosts = self._version_prefix + self._build
Chris Masone44e4d6c2012-08-15 14:25:53 -0700292 test_obj = self._afe.create_job(
293 control_file=test.text,
294 name='/'.join([self._build, self._tag, test.name]),
295 control_type=test.test_type.capitalize(),
296 meta_hosts=[meta_hosts],
297 dependencies=job_deps,
298 keyvals={constants.JOB_BUILD_KEY: self._build,
Simran Basic68cda42012-11-19 17:03:18 -0800299 constants.JOB_SUITE_KEY: self._tag},
Aviv Keshet18308922013-02-19 17:49:49 -0800300 max_runtime_mins=self._max_runtime_mins,
301 parent_job_id=self._suite_job_id)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700302
303 setattr(test_obj, 'test_name', test.name)
304
305 return test_obj
306
307
Alex Miller3a69adc2012-12-19 13:38:31 -0800308 def schedule_and_wait(self, record, add_experimental=True):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700309 """
310 Synchronously run tests in |self.tests|.
311
Alex Miller3a69adc2012-12-19 13:38:31 -0800312 See |schedule| and |wait| for more information.
313
Chris Masone44e4d6c2012-08-15 14:25:53 -0700314 Schedules tests against a device running image |self._build|, and
315 then polls for status, using |record| to print status when each
316 completes.
317
318 Tests returned by self.stable_tests() will always be run, while tests
319 in self.unstable_tests() will only be run if |add_experimental| is true.
320
321 @param record: callable that records job status.
322 prototype:
323 record(base_job.status_log_entry)
Chris Masone8906ab12012-07-23 15:37:56 -0700324 @param manager: a populated HostLockManager instance to handle
325 unlocking DUTs that we already reimaged.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700326 @param add_experimental: schedule experimental tests as well, or not.
327 """
Alex Miller3a69adc2012-12-19 13:38:31 -0800328 # This method still exists for unittesting convenience.
329 self.schedule(record, add_experimental)
330 self.wait(record)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700331
332
Alex Miller3a69adc2012-12-19 13:38:31 -0800333 def schedule(self, record, add_experimental=True):
Aviv Keshet18308922013-02-19 17:49:49 -0800334 #pylint: disable-msg=C0111
Chris Masone44e4d6c2012-08-15 14:25:53 -0700335 """
336 Schedule jobs using |self._afe|.
337
338 frontend.Job objects representing each scheduled job will be put in
339 |self._jobs|.
340
341 @param add_experimental: schedule experimental tests as well, or not.
342 """
Alex Miller3a69adc2012-12-19 13:38:31 -0800343 logging.debug('Discovered %d stable tests.', len(self.stable_tests()))
344 logging.debug('Discovered %d unstable tests.',
345 len(self.unstable_tests()))
Chris Masone44e4d6c2012-08-15 14:25:53 -0700346
Alex Miller3a69adc2012-12-19 13:38:31 -0800347 Status('INFO', 'Start %s' % self._tag).record_result(record)
348 try:
349 for test in self.stable_tests():
350 logging.debug('Scheduling %s', test.name)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700351 self._jobs.append(self._create_job(test))
Alex Miller3a69adc2012-12-19 13:38:31 -0800352
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
359 if self._results_dir:
360 self._remember_scheduled_job_ids()
361 except Exception: # pylint: disable=W0703
362 logging.error(traceback.format_exc())
363 Status('FAIL', self._tag,
364 'Exception while scheduling suite').record_result(record)
365
366
367 def wait(self, record):
368 """
369 Polls for the job statuses, using |record| to print status when each
370 completes.
371
372 @param record: callable that records job status.
373 prototype:
374 record(base_job.status_log_entry)
375 """
Alex Millera3a4fe72013-01-22 09:57:47 -0800376 if self._file_bugs:
377 bug_reporter = reporting.Reporter()
Alex Miller3a69adc2012-12-19 13:38:31 -0800378 try:
379 for result in job_status.wait_for_results(self._afe,
380 self._tko,
381 self._jobs):
382 result.record_all(record)
383 if (self._results_dir and
384 job_status.is_for_infrastructure_fail(result)):
385 self._remember_provided_job_id(result)
Alex Millera3a4fe72013-01-22 09:57:47 -0800386
387 # I'd love to grab the actual tko test object here, as that
388 # includes almost all of the needed information: test name,
389 # status, reason, etc. However, doing so would cause a
390 # bunch of database traffic to grab data that we already
391 # have laying around in memory across several objects here.
392 worse = result.is_worse_than(job_status.Status("WARN", ""))
393 if self._file_bugs and worse:
394 failure = reporting.TestFailure(build=self._build,
395 suite=self._tag,
396 test=result.test_name,
397 reason=result.reason)
398 bug_reporter.report(failure)
Alex Miller3a69adc2012-12-19 13:38:31 -0800399 except Exception: # pylint: disable=W0703
400 logging.error(traceback.format_exc())
401 Status('FAIL', self._tag,
402 'Exception waiting for results').record_result(record)
403
404
405 def abort(self):
406 """
407 Abort all scheduled test jobs.
408 """
409 if self._jobs:
410 job_ids = [job.id for job in self._jobs]
411 self._afe.run('abort_host_queue_entries', job__id__in=job_ids)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700412
413
Chris Masoned9f13c52012-08-29 10:37:08 -0700414 def _remember_scheduled_job_ids(self):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700415 """
416 Record scheduled job ids as keyvals, so they can be referenced later.
417 """
418 for job in self._jobs:
Chris Masoned9f13c52012-08-29 10:37:08 -0700419 self._remember_provided_job_id(job)
420
421
422 def _remember_provided_job_id(self, job):
423 """
424 Record provided job as a suite job keyval, for later referencing.
425
426 @param job: some representation of a job, including id, test_name
427 and owner
428 """
429 if job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -0700430 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -0700431 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700432 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700433 utils.write_keyval(
434 self._results_dir,
435 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
436
437
438 @staticmethod
439 def find_and_parse_tests(cf_getter, predicate, add_experimental=False):
440 """
441 Function to scan through all tests and find eligible tests.
442
443 Looks at control files returned by _cf_getter.get_control_file_list()
444 for tests that pass self._predicate().
445
446 @param cf_getter: a control_file_getter.ControlFileGetter used to list
447 and fetch the content of control files
448 @param predicate: a function that should return True when run over a
449 ControlData representation of a control file that should be in
450 this Suite.
451 @param add_experimental: add tests with experimental attribute set.
452
453 @return list of ControlData objects that should be run, with control
Dan Shief5b53f2013-01-22 10:22:01 -0800454 file text added in |text| attribute. Results are sorted based
455 on the TIME setting in control file, slowest test comes first.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700456 """
457 tests = {}
458 files = cf_getter.get_control_file_list()
459 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
460 for file in filter(lambda f: not matcher.match(f), files):
461 logging.debug('Considering %s', file)
462 text = cf_getter.get_control_file_contents(file)
463 try:
464 found_test = control_data.parse_control_string(
465 text, raise_warnings=True)
466 if not add_experimental and found_test.experimental:
467 continue
468
469 found_test.text = text
470 found_test.path = file
471 tests[file] = found_test
472 except control_data.ControlVariableException, e:
473 logging.warn("Skipping %s\n%s", file, e)
474 except Exception, e:
475 logging.error("Bad %s\n%s", file, e)
Dan Shief5b53f2013-01-22 10:22:01 -0800476 tests = [test for test in tests.itervalues() if predicate(test)]
477 tests.sort(key=lambda t:
478 control_data.ControlData.get_test_time_index(t.time),
479 reverse=True)
480 return tests