blob: 27381454658dbfa8b333560f272f8030118fd8a1 [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
Fang Deng443f1952015-01-02 14:51:49 -08005import datetime
6import difflib
7import hashlib
8import logging
9import operator
10import os
11import re
Fang Deng443f1952015-01-02 14:51:49 -080012import sys
Chris Masone44e4d6c2012-08-15 14:25:53 -070013
14import common
15
J. Richard Barnetteb592fbc2014-04-02 10:27:33 -070016from autotest_lib.frontend.afe.json_rpc import proxy
Alex Miller3a69adc2012-12-19 13:38:31 -080017from autotest_lib.client.common_lib import control_data
Fang Denge3bc24b2014-03-17 15:19:46 -070018from autotest_lib.client.common_lib import enum
Dan Shidfea3682014-08-10 23:38:40 -070019from autotest_lib.client.common_lib import error
Simran Basi5ace6f22016-01-06 17:30:44 -080020from autotest_lib.client.common_lib import global_config
Alex Miller7d658cf2013-09-04 16:00:35 -070021from autotest_lib.client.common_lib import priorities
Dan Shidfea3682014-08-10 23:38:40 -070022from autotest_lib.client.common_lib import site_utils
23from autotest_lib.client.common_lib import time_utils
24from autotest_lib.client.common_lib import utils
Fang Denge3bc24b2014-03-17 15:19:46 -070025from autotest_lib.frontend.afe.json_rpc import proxy
Dan Shi36cfd832014-10-10 13:38:51 -070026from autotest_lib.server.cros import provision
Chris Masone44e4d6c2012-08-15 14:25:53 -070027from autotest_lib.server.cros.dynamic_suite import constants
28from autotest_lib.server.cros.dynamic_suite import control_file_getter
29from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Alex Miller3a69adc2012-12-19 13:38:31 -080030from autotest_lib.server.cros.dynamic_suite import job_status
J. Richard Barnettee7b98bb2013-08-21 16:34:16 -070031from autotest_lib.server.cros.dynamic_suite import tools
32from autotest_lib.server.cros.dynamic_suite.job_status import Status
Chris Masone44e4d6c2012-08-15 14:25:53 -070033
Shuqian Zhaoab468812015-04-08 14:40:38 -070034try:
35 from chromite.lib import boolparse_lib
36 from chromite.lib import cros_logging as logging
37except ImportError:
38 print 'Unable to import chromite.'
39 print 'This script must be either:'
40 print ' - Be run in the chroot.'
41 print ' - (not yet supported) be run after running '
42 print ' ../utils/build_externals.py'
Fang Denge3bc24b2014-03-17 15:19:46 -070043
Shuqian Zhao490f78f2016-01-20 13:18:40 -080044_FILE_BUG_SUITES = ['au', 'bvt', 'bvt-cq', 'bvt-inline', 'paygen_au_beta',
45 'paygen_au_canary', 'paygen_au_dev', 'paygen_au_stable',
46 'sanity', 'push_to_prod']
Simran Basi5ace6f22016-01-06 17:30:44 -080047_AUTOTEST_DIR = global_config.global_config.get_config_value(
48 'SCHEDULER', 'drone_installation_directory')
xixuan0f7755d2016-04-18 14:49:12 -070049ENABLE_CONTROLS_IN_BATCH = global_config.global_config.get_config_value(
50 'CROS', 'enable_getting_controls_in_batch', type=bool, default=False)
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -070051
Fang Denge3bc24b2014-03-17 15:19:46 -070052class RetryHandler(object):
53 """Maintain retry information.
54
55 @var _retry_map: A dictionary that stores retry history.
56 The key is afe job id. The value is a dictionary.
57 {job_id: {'state':RetryHandler.States, 'retry_max':int}}
58 - state:
59 The retry state of a job.
60 NOT_ATTEMPTED:
61 We haven't done anything about the job.
62 ATTEMPTED:
63 We've made an attempt to schedule a retry job. The
64 scheduling may or may not be successful, e.g.
65 it might encounter an rpc error. Note failure
66 in scheduling a retry is different from a retry job failure.
67 For each job, we only attempt to schedule a retry once.
68 For example, assume we have a test with JOB_RETRIES=5 and
69 its second retry job failed. When we attempt to create
70 a third retry job to retry the second, we hit an rpc
71 error. In such case, we will give up on all following
72 retries.
73 RETRIED:
74 A retry job has already been successfully
75 scheduled.
76 - retry_max:
77 The maximum of times the job can still
78 be retried, taking into account retries
79 that have occurred.
80 @var _retry_level: A retry might be triggered only if the result
81 is worse than the level.
Fang Deng443f1952015-01-02 14:51:49 -080082 @var _max_retries: Maximum retry limit at suite level.
83 Regardless how many times each individual test
84 has been retried, the total number of retries happening in
85 the suite can't exceed _max_retries.
Fang Denge3bc24b2014-03-17 15:19:46 -070086 """
87
88 States = enum.Enum('NOT_ATTEMPTED', 'ATTEMPTED', 'RETRIED',
89 start_value=1, step=1)
90
Fang Deng443f1952015-01-02 14:51:49 -080091 def __init__(self, initial_jobs_to_tests, retry_level='WARN',
92 max_retries=None):
Fang Denge3bc24b2014-03-17 15:19:46 -070093 """Initialize RetryHandler.
94
95 @param initial_jobs_to_tests: A dictionary that maps a job id to
96 a ControlData object. This dictionary should contain
97 jobs that are originally scheduled by the suite.
98 @param retry_level: A retry might be triggered only if the result is
99 worse than the level.
Fang Deng443f1952015-01-02 14:51:49 -0800100 @param max_retries: Integer, maxmium total retries allowed
101 for the suite. Default to None, no max.
Fang Denge3bc24b2014-03-17 15:19:46 -0700102 """
103 self._retry_map = {}
104 self._retry_level = retry_level
Fang Deng443f1952015-01-02 14:51:49 -0800105 self._max_retries = (max_retries
106 if max_retries is not None else sys.maxint)
Fang Denge3bc24b2014-03-17 15:19:46 -0700107 for job_id, test in initial_jobs_to_tests.items():
108 if test.job_retries > 0:
Allen Lifb89e2b2017-01-03 12:47:58 -0800109 self._add_job(new_job_id=job_id,
110 retry_max=test.job_retries)
Fang Denge3bc24b2014-03-17 15:19:46 -0700111
112
Allen Lifb89e2b2017-01-03 12:47:58 -0800113 def _add_job(self, new_job_id, retry_max):
Fang Denge3bc24b2014-03-17 15:19:46 -0700114 """Add a newly-created job to the retry map.
115
116 @param new_job_id: The afe_job_id of a newly created job.
117 @param retry_max: The maximum of times that we could retry
118 the test if the job fails.
119
120 @raises ValueError if new_job_id is already in retry map.
121
122 """
123 if new_job_id in self._retry_map:
124 raise ValueError('add_job called when job is already in retry map.')
125
126 self._retry_map[new_job_id] = {
127 'state': self.States.NOT_ATTEMPTED,
128 'retry_max': retry_max}
129
130
Allen Li0cd19262017-01-03 12:56:08 -0800131 def _suite_max_reached(self):
Fang Deng443f1952015-01-02 14:51:49 -0800132 """Return whether maximum retry limit for a suite has been reached."""
Fang Denge4326d62015-01-06 13:15:15 -0800133 return self._max_retries <= 0
Fang Deng443f1952015-01-02 14:51:49 -0800134
135
Fang Denge3bc24b2014-03-17 15:19:46 -0700136 def add_retry(self, old_job_id, new_job_id):
137 """Record a retry.
138
139 Update retry map with the retry information.
140
141 @param old_job_id: The afe_job_id of the job that is retried.
142 @param new_job_id: The afe_job_id of the retry job.
143
144 @raises KeyError if old_job_id isn't in the retry map.
145 @raises ValueError if we have already retried or made an attempt
146 to retry the old job.
147
148 """
149 old_record = self._retry_map[old_job_id]
150 if old_record['state'] != self.States.NOT_ATTEMPTED:
151 raise ValueError(
152 'We have already retried or attempted to retry job %d' %
153 old_job_id)
154 old_record['state'] = self.States.RETRIED
Allen Lifb89e2b2017-01-03 12:47:58 -0800155 self._add_job(new_job_id=new_job_id,
156 retry_max=old_record['retry_max'] - 1)
Fang Deng443f1952015-01-02 14:51:49 -0800157 self._max_retries -= 1
Fang Denge3bc24b2014-03-17 15:19:46 -0700158
159
160 def set_attempted(self, job_id):
161 """Set the state of the job to ATTEMPTED.
162
163 @param job_id: afe_job_id of a job.
164
165 @raises KeyError if job_id isn't in the retry map.
166 @raises ValueError if the current state is not NOT_ATTEMPTED.
167
168 """
169 current_state = self._retry_map[job_id]['state']
170 if current_state != self.States.NOT_ATTEMPTED:
171 # We are supposed to retry or attempt to retry each job
172 # only once. Raise an error if this is not the case.
173 raise ValueError('Unexpected state transition: %s -> %s' %
174 (self.States.get_string(current_state),
175 self.States.get_string(self.States.ATTEMPTED)))
176 else:
177 self._retry_map[job_id]['state'] = self.States.ATTEMPTED
178
179
180 def has_following_retry(self, result):
181 """Check whether there will be a following retry.
182
183 We have the following cases for a given job id (result.id),
184 - no retry map entry -> retry not required, no following retry
185 - has retry map entry:
186 - already retried -> has following retry
187 - has not retried
188 (this branch can be handled by checking should_retry(result))
189 - retry_max == 0 --> the last retry job, no more retry
190 - retry_max > 0
191 - attempted, but has failed in scheduling a
192 following retry due to rpc error --> no more retry
193 - has not attempped --> has following retry if test failed.
194
195 @param result: A result, encapsulating the status of the job.
196
197 @returns: True, if there will be a following retry.
198 False otherwise.
199
200 """
Allen Li2ee2a262017-01-03 13:21:10 -0800201 return (result.test_executed
202 and result.id in self._retry_map
203 and (self._retry_map[result.id]['state'] == self.States.RETRIED
204 or self._should_retry(result)))
Allen Li5cb00652017-01-03 13:06:30 -0800205
206
207 def _should_retry(self, result):
208 """Check whether we should retry a job based on its result.
209
Allen Li2ee2a262017-01-03 13:21:10 -0800210 This method only makes sense when called by has_following_retry().
211
Allen Li5cb00652017-01-03 13:06:30 -0800212 We will retry the job that corresponds to the result
213 when all of the following are true.
214 a) The test was actually executed, meaning that if
215 a job was aborted before it could ever reach the state
216 of 'Running', the job will not be retried.
217 b) The result is worse than |self._retry_level| which
218 defaults to 'WARN'.
219 c) The test requires retry, i.e. the job has an entry in the retry map.
220 d) We haven't made any retry attempt yet, i.e. state == NOT_ATTEMPTED
221 Note that if a test has JOB_RETRIES=5, and the second time
222 it was retried it hit an rpc error, we will give up on
223 all following retries.
224 e) The job has not reached its retry max, i.e. retry_max > 0
225
226 @param result: A result, encapsulating the status of the job.
227
228 @returns: True if we should retry the job.
229
230 """
Allen Li2ee2a262017-01-03 13:21:10 -0800231 assert result.test_executed
232 assert result.id in self._retry_map
Allen Li5cb00652017-01-03 13:06:30 -0800233 return (
234 not self._suite_max_reached()
Allen Li5cb00652017-01-03 13:06:30 -0800235 and result.is_worse_than(
236 job_status.Status(self._retry_level, '', 'reason'))
Allen Li5cb00652017-01-03 13:06:30 -0800237 and self._retry_map[result.id]['state'] == self.States.NOT_ATTEMPTED
238 and self._retry_map[result.id]['retry_max'] > 0
239 )
Fang Denge3bc24b2014-03-17 15:19:46 -0700240
241
242 def get_retry_max(self, job_id):
243 """Get the maximum times the job can still be retried.
244
245 @param job_id: afe_job_id of a job.
246
247 @returns: An int, representing the maximum times the job can still be
248 retried.
249 @raises KeyError if job_id isn't in the retry map.
250
251 """
252 return self._retry_map[job_id]['retry_max']
253
254
Allen Li86f8c282017-02-28 13:09:40 -0800255class _ExperimentalTestFilter(object):
256 """Filter experimental tests."""
Allen Li6b161c62017-02-28 13:08:54 -0800257
258
259 def __init__(self, tests, add_experimental=True):
260 """Initialize instance.
261
262 @param tests: iterable of tests (ControlData objects)
263 @param add_experimental: schedule experimental tests as well, or not.
264 """
265 self._tests = list(tests)
266 self._add_experimental = add_experimental
267
268
Allen Li86f8c282017-02-28 13:09:40 -0800269 def get_tests_to_schedule(self):
Allen Li6b161c62017-02-28 13:08:54 -0800270 """Return a list of tests to be scheduled for this suite.
271
272 @returns: list of tests (ControlData objects)
273 """
274 tests = self.stable_tests
275 if self._add_experimental:
276 for test in self.unstable_tests:
277 if not test.name.startswith(constants.EXPERIMENTAL_PREFIX):
278 test.name = constants.EXPERIMENTAL_PREFIX + test.name
279 tests.append(test)
280 return tests
281
282
283 @property
284 def stable_tests(self):
285 """Non-experimental tests.
286
287 @returns: list
288 """
289 return filter(lambda t: not t.experimental, self._tests)
290
291
292 @property
293 def unstable_tests(self):
294 """Experimental tests.
295
296 @returns: list
297 """
298 return filter(lambda t: t.experimental, self._tests)
299
300
Allen Li9ea208e2017-02-28 13:43:11 -0800301def _find_test_control_data_for_suite(
Allen Li066f5872017-02-28 13:30:44 -0800302 cf_getter, suite_name='', add_experimental=False,
303 forgiving_parser=True, run_prod_code=False,
304 test_args=None):
305 """
306 Function to scan through all tests and find all tests.
307
308 When this method is called with a file system ControlFileGetter, or
309 enable_controls_in_batch is set as false, this function will looks at
310 control files returned by cf_getter.get_control_file_list() for tests.
311
312 If cf_getter is a File system ControlFileGetter, it performs a full
313 parse of the root directory associated with the getter. This is the
314 case when it's invoked from suite_preprocessor.
315
316 If cf_getter is a devserver getter it looks up the suite_name in a
317 suite to control file map generated at build time, and parses the
318 relevant control files alone. This lookup happens on the devserver,
319 so as far as this method is concerned, both cases are equivalent. If
320 enable_controls_in_batch is switched on, this function will call
321 cf_getter.get_suite_info() to get a dict of control files and contents
322 in batch.
323
324 @param cf_getter: a control_file_getter.ControlFileGetter used to list
325 and fetch the content of control files
326 @param suite_name: If specified, this method will attempt to restrain
327 the search space to just this suite's control files.
328 @param add_experimental: add tests with experimental attribute set.
329 @param forgiving_parser: If False, will raise ControlVariableExceptions
330 if any are encountered when parsing control
331 files. Note that this can raise an exception
332 for syntax errors in unrelated files, because
333 we parse them before applying the predicate.
334 @param run_prod_code: If true, the suite will run the test code that
335 lives in prod aka the test code currently on the
336 lab servers by disabling SSP for the discovered
337 tests.
338 @param test_args: A dict of args to be seeded in test control file under
339 the name |args_dict|.
340
341 @raises ControlVariableException: If forgiving_parser is False and there
342 is a syntax error in a control file.
343
344 @returns a dictionary of ControlData objects that based on given
345 parameters.
346 """
347 logging.debug('Getting control file list for suite: %s', suite_name)
Allen Li1161bab2017-02-28 13:49:16 -0800348 if _should_batch_with(cf_getter):
Allen Li066f5872017-02-28 13:30:44 -0800349 suite_info = cf_getter.get_suite_info(suite_name=suite_name)
350 files = suite_info.keys()
351 else:
352 files = cf_getter.get_control_file_list(suite_name=suite_name)
353
354
355 logging.debug('Parsing control files ...')
356 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
Allen Lie911e042017-02-28 14:03:25 -0800357 filtered_files = (path for path in files if not matcher.match(path))
Allen Li59664f92017-02-28 14:08:55 -0800358 if _should_batch_with(cf_getter):
359 control_file_texts = _batch_get_control_file_texts(
360 cf_getter, suite_name, filtered_files)
361 else:
362 control_file_texts = _get_control_file_texts(
363 cf_getter, filtered_files)
Allen Li9d0be122017-02-28 14:13:04 -0800364 return _parse_control_file_texts(
365 control_file_texts=control_file_texts,
366 add_experimental=add_experimental,
367 forgiving_parser=forgiving_parser,
368 run_prod_code=run_prod_code,
369 test_args=test_args)
370
371
372def _parse_control_file_texts(control_file_texts, add_experimental=False,
373 forgiving_parser=True, run_prod_code=False,
374 test_args=None):
375 """Parse control file texts.
376
377 @param control_file_texts: iterable of (path, text) pairs
378 @param add_experimental: add tests with experimental attribute set.
379 @param forgiving_parser: If False, will raise ControlVariableExceptions
380 if any are encountered when parsing control
381 files. Note that this can raise an exception
382 for syntax errors in unrelated files, because
383 we parse them before applying the predicate.
384 @param run_prod_code: If true, the suite will run the test code that
385 lives in prod aka the test code currently on the
386 lab servers by disabling SSP for the discovered
387 tests.
388 @param test_args: A dict of args to be seeded in test control file under
389 the name |args_dict|.
390
391 @returns: a dictionary of ControlData objects
392 """
393 tests = {}
Allen Li59664f92017-02-28 14:08:55 -0800394 for file, text in control_file_texts:
Allen Li066f5872017-02-28 13:30:44 -0800395 # Seed test_args into the control file.
396 if test_args:
397 text = tools.inject_vars(test_args, text)
398 try:
399 found_test = control_data.parse_control_string(
400 text, raise_warnings=True, path=file)
401 if not add_experimental and found_test.experimental:
402 continue
403 found_test.text = text
404 if run_prod_code:
405 found_test.require_ssp = False
406 tests[file] = found_test
407 except control_data.ControlVariableException, e:
408 if not forgiving_parser:
409 msg = "Failed parsing %s\n%s" % (file, e)
410 raise control_data.ControlVariableException(msg)
411 logging.warning("Skipping %s\n%s", file, e)
412 except Exception, e:
413 logging.error("Bad %s\n%s", file, e)
414 return tests
415
416
Allen Li59664f92017-02-28 14:08:55 -0800417def _batch_get_control_file_texts(cf_getter, suite_name, paths):
418 """Get control file content for given files.
419
420 @param cf_getter: a control_file_getter.ControlFileGetter used to list
421 and fetch the content of control files
422 @param suite_name: suite name
423 @param paths: iterable of control file paths
424 @returns: generator yielding (path, text) tuples
425 """
426 suite_info = cf_getter.get_suite_info(suite_name=suite_name)
427 for path in paths:
428 yield path, suite_info[path]
429
430
431def _get_control_file_texts(cf_getter, paths):
432 """Get control file content for given files.
433
434 @param cf_getter: a control_file_getter.ControlFileGetter used to list
435 and fetch the content of control files
436 @param paths: iterable of control file paths
437 @returns: generator yielding (path, text) tuples
438 """
439 for path in paths:
440 yield path, cf_getter.get_control_file_contents(path)
441
442
Allen Li1161bab2017-02-28 13:49:16 -0800443def _should_batch_with(cf_getter):
444 """Return whether control files should be fetched in batch.
445
446 This depends on the control file getter and configuration options.
447
448 @param cf_getter: a control_file_getter.ControlFileGetter used to list
449 and fetch the content of control files
450 """
451 return (ENABLE_CONTROLS_IN_BATCH
452 and isinstance(cf_getter, control_file_getter.DevServerGetter))
453
454
Chris Masone44e4d6c2012-08-15 14:25:53 -0700455class Suite(object):
456 """
457 A suite of tests, defined by some predicate over control file variables.
458
459 Given a place to search for control files a predicate to match the desired
460 tests, can gather tests and fire off jobs to run them, and then wait for
461 results.
462
463 @var _predicate: a function that should return True when run over a
464 ControlData representation of a control file that should be in
465 this Suite.
466 @var _tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700467 @var _builds: the builds on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700468 @var _afe: an instance of AFE as defined in server/frontend.py.
469 @var _tko: an instance of TKO as defined in server/frontend.py.
470 @var _jobs: currently scheduled jobs, if any.
Fang Denge3bc24b2014-03-17 15:19:46 -0700471 @var _jobs_to_tests: a dictionary that maps job ids to tests represented
472 ControlData objects.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700473 @var _cf_getter: a control_file_getter.ControlFileGetter
Fang Denge3bc24b2014-03-17 15:19:46 -0700474 @var _retry: a bool value indicating whether jobs should be retried on
475 failure.
476 @var _retry_handler: a RetryHandler object.
477
Chris Masone44e4d6c2012-08-15 14:25:53 -0700478 """
479
480
481 @staticmethod
Allen Li9864ed62016-12-29 16:30:53 -0800482 def _create_ds_getter(build, devserver):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700483 """
484 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700485 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700486 @return a FileSystemGetter instance that looks under |autotest_dir|.
487 """
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700488 return control_file_getter.DevServerGetter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700489
490
491 @staticmethod
492 def create_fs_getter(autotest_dir):
493 """
494 @param autotest_dir: the place to find autotests.
495 @return a FileSystemGetter instance that looks under |autotest_dir|.
496 """
497 # currently hard-coded places to look for tests.
498 subpaths = ['server/site_tests', 'client/site_tests',
499 'server/tests', 'client/tests']
500 directories = [os.path.join(autotest_dir, p) for p in subpaths]
501 return control_file_getter.FileSystemGetter(directories)
502
503
504 @staticmethod
Allen Lif20e17d2017-01-03 18:24:19 -0800505 def name_in_tag_predicate(name):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700506 """Returns predicate that takes a control file and looks for |name|.
507
508 Builds a predicate that takes in a parsed control file (a ControlData)
509 and returns True if the SUITE tag is present and contains |name|.
510
511 @param name: the suite name to base the predicate on.
512 @return a callable that takes a ControlData and looks for |name| in that
513 ControlData object's suite member.
514 """
Allen Li30833702017-01-03 18:34:15 -0800515 return lambda t: name in t.suite_tag_parts
Dan Shi5783f8a2014-12-22 14:34:45 -0800516
517
Allen Lif20e17d2017-01-03 18:24:19 -0800518 @staticmethod
519 def name_in_tag_similarity_predicate(name):
Dan Shi5783f8a2014-12-22 14:34:45 -0800520 """Returns predicate that takes a control file and gets the similarity
521 of the suites in the control file and the given name.
522
523 Builds a predicate that takes in a parsed control file (a ControlData)
524 and returns a list of tuples of (suite name, ratio), where suite name
525 is each suite listed in the control file, and ratio is the similarity
526 between each suite and the given name.
527
528 @param name: the suite name to base the predicate on.
529 @return a callable that takes a ControlData and returns a list of tuples
530 of (suite name, ratio), where suite name is each suite listed in
531 the control file, and ratio is the similarity between each suite
532 and the given name.
533 """
Allen Li30833702017-01-03 18:34:15 -0800534 return lambda t: [(suite,
535 difflib.SequenceMatcher(a=suite, b=name).ratio())
536 for suite in t.suite_tag_parts] or [(None, 0)]
Chris Masone44e4d6c2012-08-15 14:25:53 -0700537
538
539 @staticmethod
Aviv Keshet40222a42013-06-04 16:25:49 -0700540 def test_name_equals_predicate(test_name):
541 """Returns predicate that matched based on a test's name.
542
543 Builds a predicate that takes in a parsed control file (a ControlData)
544 and returns True if the test name is equal to |test_name|.
545
546 @param test_name: the test name to base the predicate on.
547 @return a callable that takes a ControlData and looks for |test_name|
548 in that ControlData's name.
549 """
550 return lambda t: hasattr(t, 'name') and test_name == t.name
551
552
553 @staticmethod
Aviv Kesheta6adc7a2013-08-30 11:13:38 -0700554 def test_name_matches_pattern_predicate(test_name_pattern):
555 """Returns predicate that matches based on a test's name pattern.
556
557 Builds a predicate that takes in a parsed control file (a ControlData)
558 and returns True if the test name matches the given regular expression.
559
560 @param test_name_pattern: regular expression (string) to match against
561 test names.
562 @return a callable that takes a ControlData and returns
563 True if the name fields matches the pattern.
564 """
565 return lambda t: hasattr(t, 'name') and re.match(test_name_pattern,
566 t.name)
567
568
569 @staticmethod
570 def test_file_matches_pattern_predicate(test_file_pattern):
571 """Returns predicate that matches based on a test's file name pattern.
572
573 Builds a predicate that takes in a parsed control file (a ControlData)
574 and returns True if the test's control file name matches the given
575 regular expression.
576
577 @param test_file_pattern: regular expression (string) to match against
578 control file names.
579 @return a callable that takes a ControlData and and returns
580 True if control file name matches the pattern.
581 """
582 return lambda t: hasattr(t, 'path') and re.match(test_file_pattern,
583 t.path)
584
585
586 @staticmethod
Shuqian Zhaoab468812015-04-08 14:40:38 -0700587 def matches_attribute_expression_predicate(test_attr_boolstr):
588 """Returns predicate that matches based on boolean expression of
589 attributes.
590
591 Builds a predicate that takes in a parsed control file (a ControlData)
592 ans returns True if the test attributes satisfy the given attribute
593 boolean expression.
594
595 @param test_attr_boolstr: boolean expression of the attributes to be
596 test, like 'system:all and interval:daily'.
597
598 @return a callable that takes a ControlData and returns True if the test
599 attributes satisfy the given boolean expression.
600 """
601 return lambda t: boolparse_lib.BoolstrResult(
602 test_attr_boolstr, t.attributes)
603
604 @staticmethod
Dan Shi5783f8a2014-12-22 14:34:45 -0800605 def test_name_similarity_predicate(test_name):
606 """Returns predicate that matched based on a test's name.
607
608 Builds a predicate that takes in a parsed control file (a ControlData)
609 and returns a tuple of (test name, ratio), where ratio is the similarity
610 between the test name and the given test_name.
611
612 @param test_name: the test name to base the predicate on.
613 @return a callable that takes a ControlData and returns a tuple of
614 (test name, ratio), where ratio is the similarity between the
615 test name and the given test_name.
616 """
617 return lambda t: ((None, 0) if not hasattr(t, 'name') else
618 (t.name,
619 difflib.SequenceMatcher(a=t.name, b=test_name).ratio()))
620
621
622 @staticmethod
623 def test_file_similarity_predicate(test_file_pattern):
624 """Returns predicate that gets the similarity based on a test's file
625 name pattern.
626
627 Builds a predicate that takes in a parsed control file (a ControlData)
628 and returns a tuple of (file path, ratio), where ratio is the
629 similarity between the test file name and the given test_file_pattern.
630
631 @param test_file_pattern: regular expression (string) to match against
632 control file names.
633 @return a callable that takes a ControlData and and returns a tuple of
634 (file path, ratio), where ratio is the similarity between the
635 test file name and the given test_file_pattern.
636 """
637 return lambda t: ((None, 0) if not hasattr(t, 'path') else
638 (t.path, difflib.SequenceMatcher(a=t.path,
639 b=test_file_pattern).ratio()))
640
641
Allen Li9864ed62016-12-29 16:30:53 -0800642 @classmethod
643 def list_all_suites(cls, build, devserver, cf_getter=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700644 """
645 Parses all ControlData objects with a SUITE tag and extracts all
646 defined suite names.
647
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700648 @param build: the build on which we're running this suite.
649 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700650 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
651 using DevServerGetter.
652
653 @return list of suites
654 """
655 if cf_getter is None:
Allen Li9864ed62016-12-29 16:30:53 -0800656 cf_getter = cls._create_ds_getter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700657
658 suites = set()
Allen Li30833702017-01-03 18:34:15 -0800659 predicate = lambda t: True
Allen Lic9be3662017-01-03 17:56:26 -0800660 for test in cls.find_and_parse_tests(cf_getter, predicate,
Allen Li30833702017-01-03 18:34:15 -0800661 add_experimental=True):
Allen Lif20e17d2017-01-03 18:24:19 -0800662 suites.update(test.suite_tag_parts)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700663 return list(suites)
664
665
666 @staticmethod
Dan Shi36cfd832014-10-10 13:38:51 -0700667 def get_test_source_build(builds, **dargs):
668 """Get the build of test code.
669
670 Get the test source build from arguments. If parameter
671 `test_source_build` is set and has a value, return its value. Otherwise
672 returns the ChromeOS build name if it exists. If ChromeOS build is not
673 specified either, raise SuiteArgumentException.
674
675 @param builds: the builds on which we're running this suite. It's a
676 dictionary of version_prefix:build.
677 @param **dargs: Any other Suite constructor parameters, as described
678 in Suite.__init__ docstring.
679
680 @return: The build contains the test code.
681 @raise: SuiteArgumentException if both test_source_build and ChromeOS
682 build are not specified.
683
684 """
685 if dargs.get('test_source_build', None):
686 return dargs['test_source_build']
687 test_source_build = builds.get(provision.CROS_VERSION_PREFIX, None)
688 if not test_source_build:
689 raise error.SuiteArgumentException(
690 'test_source_build must be specified if CrOS build is not '
691 'specified.')
692 return test_source_build
693
694
Allen Li9864ed62016-12-29 16:30:53 -0800695 @classmethod
696 def create_from_predicates(cls, predicates, builds, board, devserver,
Simran Basi5ace6f22016-01-06 17:30:44 -0800697 cf_getter=None, name='ad_hoc_suite',
698 run_prod_code=False, **dargs):
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700699 """
700 Create a Suite using a given predicate test filters.
701
702 Uses supplied predicate(s) to instantiate a Suite. Looks for tests in
703 |autotest_dir| and will schedule them using |afe|. Pulls control files
704 from the default dev server. Results will be pulled from |tko| upon
705 completion.
706
707 @param predicates: A list of callables that accept ControlData
708 representations of control files. A test will be
Aviv Keshet938a6772013-07-25 14:05:45 -0700709 included in suite if all callables in this list
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700710 return True on the given control file.
Dan Shi36cfd832014-10-10 13:38:51 -0700711 @param builds: the builds on which we're running this suite. It's a
712 dictionary of version_prefix:build.
Alex Millera0913072013-06-12 10:01:51 -0700713 @param board: the board on which we're running this suite.
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700714 @param devserver: the devserver which contains the build.
715 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
716 using DevServerGetter.
717 @param name: name of suite. Defaults to 'ad_hoc_suite'
Simran Basi5ace6f22016-01-06 17:30:44 -0800718 @param run_prod_code: If true, the suite will run the tests that
719 lives in prod aka the test code currently on the
720 lab servers.
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700721 @param **dargs: Any other Suite constructor parameters, as described
722 in Suite.__init__ docstring.
723 @return a Suite instance.
724 """
725 if cf_getter is None:
Simran Basi5ace6f22016-01-06 17:30:44 -0800726 if run_prod_code:
Allen Li9864ed62016-12-29 16:30:53 -0800727 cf_getter = cls.create_fs_getter(_AUTOTEST_DIR)
Simran Basi5ace6f22016-01-06 17:30:44 -0800728 else:
Allen Lic9be3662017-01-03 17:56:26 -0800729 build = cls.get_test_source_build(builds, **dargs)
Allen Li9864ed62016-12-29 16:30:53 -0800730 cf_getter = cls._create_ds_getter(build, devserver)
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700731
Allen Lic9be3662017-01-03 17:56:26 -0800732 return cls(predicates,
733 name, builds, board, cf_getter, run_prod_code, **dargs)
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700734
735
Allen Li9864ed62016-12-29 16:30:53 -0800736 @classmethod
737 def create_from_name(cls, name, builds, board, devserver, cf_getter=None,
Alex Millera0913072013-06-12 10:01:51 -0700738 **dargs):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700739 """
740 Create a Suite using a predicate based on the SUITE control file var.
741
742 Makes a predicate based on |name| and uses it to instantiate a Suite
743 that looks for tests in |autotest_dir| and will schedule them using
744 |afe|. Pulls control files from the default dev server.
745 Results will be pulled from |tko| upon completion.
746
747 @param name: a value of the SUITE control file variable to search for.
Dan Shi36cfd832014-10-10 13:38:51 -0700748 @param builds: the builds on which we're running this suite. It's a
749 dictionary of version_prefix:build.
Alex Millera0913072013-06-12 10:01:51 -0700750 @param board: the board on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700751 @param devserver: the devserver which contains the build.
Aviv Keshet813d6782013-06-04 17:11:03 -0700752 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
753 using DevServerGetter.
754 @param **dargs: Any other Suite constructor parameters, as described
755 in Suite.__init__ docstring.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700756 @return a Suite instance.
757 """
758 if cf_getter is None:
Allen Li9864ed62016-12-29 16:30:53 -0800759 build = cls.get_test_source_build(builds, **dargs)
760 cf_getter = cls._create_ds_getter(build, devserver)
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700761
Allen Lic9be3662017-01-03 17:56:26 -0800762 return cls([cls.name_in_tag_predicate(name)],
763 name, builds, board, cf_getter, **dargs)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700764
765
Allen Li6fff5502016-12-09 18:04:26 -0800766 def __init__(
767 self,
768 predicates,
769 tag,
770 builds,
771 board,
772 cf_getter,
773 run_prod_code=False,
774 afe=None,
775 tko=None,
776 pool=None,
777 results_dir=None,
778 max_runtime_mins=24*60,
779 timeout_mins=24*60,
780 file_bugs=False,
781 file_experimental_bugs=False,
782 suite_job_id=None,
783 ignore_deps=False,
Allen Li493eefa2016-12-09 18:05:35 -0800784 extra_deps=None,
Allen Li6fff5502016-12-09 18:04:26 -0800785 priority=priorities.Priority.DEFAULT,
786 forgiving_parser=True,
787 wait_for_results=True,
788 job_retry=False,
789 max_retries=sys.maxint,
790 offload_failures_only=False,
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800791 test_source_build=None,
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800792 job_keyvals=None,
793 test_args=None
Allen Li6fff5502016-12-09 18:04:26 -0800794 ):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700795 """
796 Constructor
797
Aviv Keshet40222a42013-06-04 16:25:49 -0700798 @param predicates: A list of callables that accept ControlData
799 representations of control files. A test will be
800 included in suite is all callables in this list
801 return True on the given control file.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700802 @param tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700803 @param builds: the builds on which we're running this suite.
Alex Millera0913072013-06-12 10:01:51 -0700804 @param board: the board on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700805 @param cf_getter: a control_file_getter.ControlFileGetter
806 @param afe: an instance of AFE as defined in server/frontend.py.
807 @param tko: an instance of TKO as defined in server/frontend.py.
808 @param pool: Specify the pool of machines to use for scheduling
809 purposes.
Simran Basi5ace6f22016-01-06 17:30:44 -0800810 @param run_prod_code: If true, the suite will run the test code that
811 lives in prod aka the test code currently on the
812 lab servers.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700813 @param results_dir: The directory where the job can write results to.
814 This must be set if you want job_id of sub-jobs
815 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -0800816 @param max_runtime_mins: Maximum suite runtime, in minutes.
Alex Miller028b0312013-09-07 15:25:45 -0700817 @param timeout: Maximum job lifetime, in hours.
Aviv Keshet18308922013-02-19 17:49:49 -0800818 @param suite_job_id: Job id that will act as parent id to all sub jobs.
819 Default: None
Aviv Keshetd7959f32013-05-17 15:58:43 -0700820 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
821 attribute and skip applying of dependency labels.
822 (Default:False)
Alex Miller47a03672013-08-27 09:09:53 -0700823 @param extra_deps: A list of strings which are the extra DEPENDENCIES
824 to add to each test being scheduled.
Alex Miller7d658cf2013-09-04 16:00:35 -0700825 @param priority: Integer priority level. Higher is more important.
Dan Shi95122412013-11-12 16:20:33 -0800826 @param wait_for_results: Set to False to run the suite job without
827 waiting for test jobs to finish. Default is
828 True.
Fang Denge3bc24b2014-03-17 15:19:46 -0700829 @param job_retry: A bool value indicating whether jobs should be retired
830 on failure. If True, the field 'JOB_RETRIES' in
831 control files will be respected. If False, do not
832 retry.
Fang Deng443f1952015-01-02 14:51:49 -0800833 @param max_retries: Maximum retry limit at suite level.
834 Regardless how many times each individual test
835 has been retried, the total number of retries
836 happening in the suite can't exceed _max_retries.
837 Default to sys.maxint.
Simran Basi1e10e922015-04-16 15:09:56 -0700838 @param offload_failures_only: Only enable gs_offloading for failed
839 jobs.
Dan Shi36cfd832014-10-10 13:38:51 -0700840 @param test_source_build: Build that contains the server-side test code.
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800841 @param job_keyvals: General job keyvals to be inserted into keyval file,
842 which will be used by tko/parse later.
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800843 @param test_args: A dict of args passed all the way to each individual
844 test that will be actually ran.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700845 """
Allen Li493eefa2016-12-09 18:05:35 -0800846 if extra_deps is None:
847 extra_deps = []
848
Chris Masone44e4d6c2012-08-15 14:25:53 -0700849 self._tag = tag
Dan Shi36cfd832014-10-10 13:38:51 -0700850 self._builds = builds
Alex Millera0913072013-06-12 10:01:51 -0700851 self._board = board
Chris Masone44e4d6c2012-08-15 14:25:53 -0700852 self._cf_getter = cf_getter
853 self._results_dir = results_dir
854 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
855 delay_sec=10,
856 debug=False)
857 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
858 delay_sec=10,
859 debug=False)
860 self._pool = pool
861 self._jobs = []
Fang Denge3bc24b2014-03-17 15:19:46 -0700862 self._jobs_to_tests = {}
Allen Li7947f732016-12-29 16:44:28 -0800863 self.tests = self.find_and_parse_tests(
Allen Li8a649092016-12-09 18:07:39 -0800864 self._cf_getter,
Allen Lid69b9f02016-12-09 18:15:59 -0800865 lambda control_data: all(f(control_data) for f in predicates),
Allen Li8a649092016-12-09 18:07:39 -0800866 self._tag,
867 add_experimental=True,
868 forgiving_parser=forgiving_parser,
869 run_prod_code=run_prod_code,
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800870 test_args=test_args,
Allen Li8a649092016-12-09 18:07:39 -0800871 )
beeps89f1e062013-09-18 12:00:17 -0700872
Simran Basic68cda42012-11-19 17:03:18 -0800873 self._max_runtime_mins = max_runtime_mins
Simran Basi8705d672013-11-19 15:56:58 -0800874 self._timeout_mins = timeout_mins
Alex Millera3a4fe72013-01-22 09:57:47 -0800875 self._file_bugs = file_bugs
beepsda5b7112013-05-30 11:34:14 -0700876 self._file_experimental_bugs = file_experimental_bugs
Aviv Keshet18308922013-02-19 17:49:49 -0800877 self._suite_job_id = suite_job_id
Aviv Keshetd7959f32013-05-17 15:58:43 -0700878 self._ignore_deps = ignore_deps
Alex Miller47a03672013-08-27 09:09:53 -0700879 self._extra_deps = extra_deps
Alex Miller7d658cf2013-09-04 16:00:35 -0700880 self._priority = priority
Fang Denge3bc24b2014-03-17 15:19:46 -0700881 self._job_retry=job_retry
Fang Deng443f1952015-01-02 14:51:49 -0800882 self._max_retries = max_retries
Fang Denge3bc24b2014-03-17 15:19:46 -0700883 # RetryHandler to be initialized in schedule()
884 self._retry_handler = None
Dan Shi95122412013-11-12 16:20:33 -0800885 self.wait_for_results = wait_for_results
Simran Basi1e10e922015-04-16 15:09:56 -0700886 self._offload_failures_only = offload_failures_only
Dan Shi36cfd832014-10-10 13:38:51 -0700887 self._test_source_build = test_source_build
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800888 self._job_keyvals = job_keyvals
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800889 self._test_args = test_args
Alex Millera3a4fe72013-01-22 09:57:47 -0800890
Chris Masone44e4d6c2012-08-15 14:25:53 -0700891
892 @property
Allen Lidb8eafe2016-12-12 16:33:58 -0800893 def _cros_build(self):
894 """Return the CrOS build or the first build in the builds dict."""
895 # TODO(ayatane): Note that the builds dict isn't ordered. I'm not
896 # sure what the implications of this are, but it's probably not a
897 # good thing.
898 return self._builds.get(provision.CROS_VERSION_PREFIX,
899 self._builds.values()[0])
900
901
Fang Denge3bc24b2014-03-17 15:19:46 -0700902 def _create_job(self, test, retry_for=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700903 """
904 Thin wrapper around frontend.AFE.create_job().
905
906 @param test: ControlData object for a test to run.
Fang Denge3bc24b2014-03-17 15:19:46 -0700907 @param retry_for: If the to-be-created job is a retry for an
908 old job, the afe_job_id of the old job will
909 be passed in as |retry_for|, which will be
910 recorded in the new job's keyvals.
911 @returns: A frontend.Job object with an added test_name member.
912 test_name is used to preserve the higher level TEST_NAME
913 name of the job.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700914 """
Allen Li069fc252016-12-12 16:26:21 -0800915 test_obj = self._afe.create_job(
916 control_file=test.text,
Allen Li468d6152016-12-12 16:35:01 -0800917 name=tools.create_job_name(
918 self._test_source_build or self._cros_build,
919 self._tag,
920 test.name),
Allen Li069fc252016-12-12 16:26:21 -0800921 control_type=test.test_type.capitalize(),
922 meta_hosts=[self._board]*test.sync_count,
Allen Lic68ca4a2016-12-12 17:28:36 -0800923 dependencies=self._create_job_deps(test),
Allen Lia4ae1352016-12-12 16:26:57 -0800924 keyvals=self._create_keyvals_for_test_job(test, retry_for),
Allen Li069fc252016-12-12 16:26:21 -0800925 max_runtime_mins=self._max_runtime_mins,
926 timeout_mins=self._timeout_mins,
927 parent_job_id=self._suite_job_id,
928 test_retry=test.retries,
929 priority=self._priority,
930 synch_count=test.sync_count,
931 require_ssp=test.require_ssp)
932
933 test_obj.test_name = test.name
934 return test_obj
935
936
Allen Lic68ca4a2016-12-12 17:28:36 -0800937 def _create_job_deps(self, test):
938 """Create job deps list for a test job.
939
940 @returns: A list of dependency strings.
941 """
942 if self._ignore_deps:
943 job_deps = []
944 else:
945 job_deps = list(test.dependencies)
946 job_deps.extend(self._extra_deps)
947 if self._pool:
948 job_deps.append(self._pool)
949 job_deps.append(self._board)
950 return job_deps
951
952
Allen Li069fc252016-12-12 16:26:21 -0800953 def _create_keyvals_for_test_job(self, test, retry_for=None):
954 """Create keyvals dict for creating a test job.
955
956 @param test: ControlData object for a test to run.
957 @param retry_for: If the to-be-created job is a retry for an
958 old job, the afe_job_id of the old job will
959 be passed in as |retry_for|, which will be
960 recorded in the new job's keyvals.
961 @returns: A keyvals dict for creating the test job.
962 """
Allen Li015e71b2016-12-12 16:37:25 -0800963 keyvals = {
964 constants.JOB_BUILD_KEY: self._cros_build,
965 constants.JOB_SUITE_KEY: self._tag,
966 constants.JOB_EXPERIMENTAL_KEY: test.experimental,
967 constants.JOB_BUILDS_KEY: self._builds
968 }
Dan Shi36cfd832014-10-10 13:38:51 -0700969 # test_source_build is saved to job_keyvals so scheduler can retrieve
970 # the build name from database when compiling autoserv commandline.
971 # This avoid a database change to add a new field in afe_jobs.
Allen Li015e71b2016-12-12 16:37:25 -0800972 #
Dan Shi36cfd832014-10-10 13:38:51 -0700973 # Only add `test_source_build` to job keyvals if the build is different
974 # from the CrOS build or the job uses more than one build, e.g., both
975 # firmware and CrOS will be updated in the dut.
976 # This is for backwards compatibility, so the update Autotest code can
977 # compile an autoserv command line to run in a SSP container using
978 # previous builds.
979 if (self._test_source_build and
Allen Li015e71b2016-12-12 16:37:25 -0800980 (self._cros_build != self._test_source_build or
981 len(self._builds) > 1)):
982 keyvals[constants.JOB_TEST_SOURCE_BUILD_KEY] = \
983 self._test_source_build
Dan Shidac462f2015-08-14 11:07:32 -0700984 for prefix, build in self._builds.iteritems():
985 if prefix == provision.FW_RW_VERSION_PREFIX:
986 keyvals[constants.FWRW_BUILD]= build
987 elif prefix == provision.FW_RO_VERSION_PREFIX:
988 keyvals[constants.FWRO_BUILD] = build
Allen Li015e71b2016-12-12 16:37:25 -0800989 # Add suite job id to keyvals so tko parser can read it from keyval
990 # file.
Dan Shidac462f2015-08-14 11:07:32 -0700991 if self._suite_job_id:
992 keyvals[constants.PARENT_JOB_ID] = self._suite_job_id
Allen Li015e71b2016-12-12 16:37:25 -0800993 # We drop the old job's id in the new job's keyval file so that
994 # later our tko parser can figure out the retry relationship and
995 # invalidate the results of the old job in tko database.
Fang Denge3bc24b2014-03-17 15:19:46 -0700996 if retry_for:
Fang Denge3bc24b2014-03-17 15:19:46 -0700997 keyvals[constants.RETRY_ORIGINAL_JOB_ID] = retry_for
Simran Basi1e10e922015-04-16 15:09:56 -0700998 if self._offload_failures_only:
999 keyvals[constants.JOB_OFFLOAD_FAILURES_KEY] = True
Allen Li069fc252016-12-12 16:26:21 -08001000 return keyvals
Chris Masone44e4d6c2012-08-15 14:25:53 -07001001
1002
Fang Denge3bc24b2014-03-17 15:19:46 -07001003 def _schedule_test(self, record, test, retry_for=None, ignore_errors=False):
1004 """Schedule a single test and return the job.
1005
Allen Lie79b3cb2016-12-12 18:24:17 -08001006 Schedule a single test by creating a job, and then update relevant
1007 data structures that are used to keep track of all running jobs.
Fang Denge3bc24b2014-03-17 15:19:46 -07001008
Allen Lie79b3cb2016-12-12 18:24:17 -08001009 Emits a TEST_NA status log entry if it failed to schedule the test due
1010 to NoEligibleHostException or a non-existent board label.
1011
1012 Returns a frontend.Job object if the test is successfully scheduled.
1013 If scheduling failed due to NoEligibleHostException or a non-existent
1014 board label, returns None. If ignore_errors is True, all unknown
1015 errors return None, otherwise the errors are raised as-is.
Fang Denge3bc24b2014-03-17 15:19:46 -07001016
1017 @param record: A callable to use for logging.
1018 prototype: record(base_job.status_log_entry)
1019 @param test: ControlData for a test to run.
1020 @param retry_for: If we are scheduling a test to retry an
1021 old job, the afe_job_id of the old job
1022 will be passed in as |retry_for|.
1023 @param ignore_errors: If True, when an rpc error occur, ignore
1024 the error and will return None.
1025 If False, rpc errors will be raised.
1026
Allen Lie79b3cb2016-12-12 18:24:17 -08001027 @returns: A frontend.Job object or None
Fang Denge3bc24b2014-03-17 15:19:46 -07001028 """
1029 msg = 'Scheduling %s' % test.name
1030 if retry_for:
1031 msg = msg + ', to retry afe job %d' % retry_for
1032 logging.debug(msg)
Dan Shidfea3682014-08-10 23:38:40 -07001033 begin_time_str = datetime.datetime.now().strftime(time_utils.TIME_FMT)
Fang Denge3bc24b2014-03-17 15:19:46 -07001034 try:
1035 job = self._create_job(test, retry_for=retry_for)
Allen Li6fd440f2016-12-12 18:40:05 -08001036 except (error.NoEligibleHostException, proxy.ValidationError) as e:
1037 if (isinstance(e, error.NoEligibleHostException)
1038 or (isinstance(e, proxy.ValidationError)
1039 and _is_nonexistent_board_error(e))):
1040 # Treat a dependency on a non-existent board label the same as
1041 # a dependency on a board that exists, but for which there's no
1042 # hardware.
1043 logging.debug('%s not applicable for this board/pool. '
1044 'Emitting TEST_NA.', test.name)
1045 Status('TEST_NA', test.name,
1046 'Skipping: test not supported on this board/pool.',
Allen Li9fcd4b42016-12-12 16:15:14 -08001047 begin_time_str=begin_time_str).record_all(record)
1048 return None
1049 else:
Fang Denge3bc24b2014-03-17 15:19:46 -07001050 raise e
Fang Denge3bc24b2014-03-17 15:19:46 -07001051 except (error.RPCException, proxy.JSONRPCException) as e:
1052 if retry_for:
1053 # Mark that we've attempted to retry the old job.
1054 self._retry_handler.set_attempted(job_id=retry_for)
Allen Li0ba59342016-12-12 15:57:02 -08001055
Fang Denge3bc24b2014-03-17 15:19:46 -07001056 if ignore_errors:
1057 logging.error('Failed to schedule test: %s, Reason: %s',
1058 test.name, e)
Allen Li0ba59342016-12-12 15:57:02 -08001059 return None
Fang Denge3bc24b2014-03-17 15:19:46 -07001060 else:
1061 raise e
1062 else:
1063 self._jobs.append(job)
1064 self._jobs_to_tests[job.id] = test
1065 if retry_for:
1066 # A retry job was just created, record it.
1067 self._retry_handler.add_retry(
1068 old_job_id=retry_for, new_job_id=job.id)
1069 retry_count = (test.job_retries -
1070 self._retry_handler.get_retry_max(job.id))
1071 logging.debug('Job %d created to retry job %d. '
1072 'Have retried for %d time(s)',
1073 job.id, retry_for, retry_count)
Allen Li4df053e2016-12-29 16:05:41 -08001074 self._remember_job_keyval(job)
Fang Denge3bc24b2014-03-17 15:19:46 -07001075 return job
Fang Denge3bc24b2014-03-17 15:19:46 -07001076
1077
Alex Miller3a69adc2012-12-19 13:38:31 -08001078 def schedule(self, record, add_experimental=True):
Aviv Keshet18308922013-02-19 17:49:49 -08001079 #pylint: disable-msg=C0111
Chris Masone44e4d6c2012-08-15 14:25:53 -07001080 """
1081 Schedule jobs using |self._afe|.
1082
1083 frontend.Job objects representing each scheduled job will be put in
1084 |self._jobs|.
1085
Fang Denge3bc24b2014-03-17 15:19:46 -07001086 @param record: A callable to use for logging.
1087 prototype: record(base_job.status_log_entry)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001088 @param add_experimental: schedule experimental tests as well, or not.
Aviv Keshete9170d92013-07-19 11:20:45 -07001089 @returns: The number of tests that were scheduled.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001090 """
Allen Lif4cb5ec2017-01-03 16:58:12 -08001091 scheduled_test_names = []
Allen Li86f8c282017-02-28 13:09:40 -08001092 test_filter = _ExperimentalTestFilter(
Allen Lif4cb5ec2017-01-03 16:58:12 -08001093 tests=self.tests,
1094 add_experimental=add_experimental)
1095 logging.debug('Discovered %d stable tests.',
Allen Li86f8c282017-02-28 13:09:40 -08001096 len(test_filter.stable_tests))
Alex Miller3a69adc2012-12-19 13:38:31 -08001097 logging.debug('Discovered %d unstable tests.',
Allen Li86f8c282017-02-28 13:09:40 -08001098 len(test_filter.unstable_tests))
Chris Masone44e4d6c2012-08-15 14:25:53 -07001099
Alex Miller3a69adc2012-12-19 13:38:31 -08001100 Status('INFO', 'Start %s' % self._tag).record_result(record)
1101 try:
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001102 # Write job_keyvals into keyval file.
1103 if self._job_keyvals:
1104 utils.write_keyval(self._results_dir, self._job_keyvals)
1105
Allen Li86f8c282017-02-28 13:09:40 -08001106 for test in test_filter.get_tests_to_schedule():
Allen Lida905732016-12-12 15:49:16 -08001107 scheduled_job = self._schedule_test(record, test)
1108 if scheduled_job is not None:
Shuqian Zhaocd866f32016-11-29 20:14:34 -08001109 scheduled_test_names.append(test.name)
1110
1111 # Write the num of scheduled tests and name of them to keyval file.
Shuqian Zhaocd866f32016-11-29 20:14:34 -08001112 logging.debug('Scheduled %d tests, writing the total to keyval.',
Allen Lia4d35022016-12-12 15:42:10 -08001113 len(scheduled_test_names))
Allen Lid4d5dda2016-12-12 15:39:11 -08001114 utils.write_keyval(
1115 self._results_dir,
Allen Lidda59b82016-12-12 18:20:04 -08001116 self._make_scheduled_tests_keyvals(scheduled_test_names))
Alex Miller3a69adc2012-12-19 13:38:31 -08001117 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001118 logging.exception('Exception while scheduling suite')
Alex Miller3a69adc2012-12-19 13:38:31 -08001119 Status('FAIL', self._tag,
1120 'Exception while scheduling suite').record_result(record)
1121
Fang Deng7e655a92014-05-23 13:48:11 -07001122 if self._job_retry:
1123 self._retry_handler = RetryHandler(
Fang Deng443f1952015-01-02 14:51:49 -08001124 initial_jobs_to_tests=self._jobs_to_tests,
1125 max_retries=self._max_retries)
Allen Lia4d35022016-12-12 15:42:10 -08001126 return len(scheduled_test_names)
Aviv Keshete9170d92013-07-19 11:20:45 -07001127
Alex Miller3a69adc2012-12-19 13:38:31 -08001128
Allen Lidda59b82016-12-12 18:20:04 -08001129 def _make_scheduled_tests_keyvals(self, scheduled_test_names):
1130 """Make a keyvals dict to write for scheduled test names.
1131
1132 @param scheduled_test_names: A list of scheduled test name strings.
1133
1134 @returns: A keyvals dict.
1135 """
1136 return {
1137 constants.SCHEDULED_TEST_COUNT_KEY: len(scheduled_test_names),
1138 constants.SCHEDULED_TEST_NAMES_KEY: repr(scheduled_test_names),
1139 }
1140
1141
Allen Lid1cbccf2016-12-29 15:12:39 -08001142 def _should_report(self, result):
beepsda5b7112013-05-30 11:34:14 -07001143 """
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -07001144 Returns True if this failure requires to be reported.
beepsda5b7112013-05-30 11:34:14 -07001145
1146 @param result: A result, encapsulating the status of the failed job.
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -07001147 @return: True if we should report this failure.
beepsda5b7112013-05-30 11:34:14 -07001148 """
Allen Licc752292017-01-03 12:44:39 -08001149 if self._has_retry(result):
Fang Denge3bc24b2014-03-17 15:19:46 -07001150 return False
1151
beepsbeefc062013-08-02 11:17:09 -07001152 is_not_experimental = (
1153 constants.EXPERIMENTAL_PREFIX not in result._test_name and
1154 constants.EXPERIMENTAL_PREFIX not in result._job_name)
1155
Alex Millerfcc119b2014-01-15 13:54:58 -08001156 return (self._file_bugs and result.test_executed and
beepsbeefc062013-08-02 11:17:09 -07001157 (is_not_experimental or self._file_experimental_bugs) and
Fang Dengd82c1c72014-07-29 10:43:01 -07001158 not result.is_testna() and
beeps32fa6772014-01-28 13:19:53 -08001159 result.is_worse_than(job_status.Status('GOOD', '', 'reason')))
beepsda5b7112013-05-30 11:34:14 -07001160
1161
Allen Licc752292017-01-03 12:44:39 -08001162 def _has_retry(self, result):
1163 """
1164 Return True if this result gets to retry.
1165
1166 @param result: A result, encapsulating the status of the failed job.
1167 @return: bool
1168 """
1169 return (self._job_retry
1170 and self._retry_handler.has_following_retry(result))
1171
1172
Allen Li18503452016-12-29 14:56:48 -08001173 def wait(self, record, bug_template=None):
Alex Miller3a69adc2012-12-19 13:38:31 -08001174 """
1175 Polls for the job statuses, using |record| to print status when each
1176 completes.
1177
1178 @param record: callable that records job status.
1179 prototype:
1180 record(base_job.status_log_entry)
beepsc8a875b2013-03-25 10:20:38 -07001181 @param bug_template: A template dictionary specifying the default bug
1182 filing options for failures in this suite.
Alex Miller3a69adc2012-12-19 13:38:31 -08001183 """
Dan Shie67bd6a2016-02-17 14:44:07 -08001184 # reporting modules have dependency on external packages, e.g., httplib2
1185 # Such dependency can cause issue to any module tries to import suite.py
1186 # without building site-packages first. Since the reporting modules are
1187 # only used in this function, move the imports here avoid the
1188 # requirement of building site packages to use other functions in this
1189 # module.
1190 from autotest_lib.server.cros.dynamic_suite import reporting
Dan Shie67bd6a2016-02-17 14:44:07 -08001191
Allen Li18503452016-12-29 14:56:48 -08001192 if bug_template is None:
1193 bug_template = {}
1194
Alex Millera3a4fe72013-01-22 09:57:47 -08001195 if self._file_bugs:
1196 bug_reporter = reporting.Reporter()
Allen Li733dab92016-12-29 15:07:50 -08001197 else:
1198 bug_reporter = reporting.NullReporter()
Alex Miller3a69adc2012-12-19 13:38:31 -08001199 try:
Aviv Keshet133beb12013-08-20 14:37:13 -07001200 if self._suite_job_id:
1201 results_generator = job_status.wait_for_child_results(
1202 self._afe, self._tko, self._suite_job_id)
1203 else:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -07001204 logging.warning('Unknown suite_job_id, falling back to less '
Dan Shi08ff1282016-02-18 19:51:16 -08001205 'efficient results_generator.')
Aviv Keshet133beb12013-08-20 14:37:13 -07001206 results_generator = job_status.wait_for_results(self._afe,
1207 self._tko,
1208 self._jobs)
1209 for result in results_generator:
Allen Li26b340d2016-12-29 15:23:01 -08001210 self._record_result(
1211 result=result,
1212 record=record,
1213 results_generator=results_generator,
1214 bug_reporter=bug_reporter,
1215 bug_template=bug_template)
beeps8ead53c2013-04-26 19:12:46 -07001216
Alex Miller3a69adc2012-12-19 13:38:31 -08001217 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001218 logging.exception('Exception waiting for results')
Alex Miller3a69adc2012-12-19 13:38:31 -08001219 Status('FAIL', self._tag,
1220 'Exception waiting for results').record_result(record)
1221
1222
Allen Li26b340d2016-12-29 15:23:01 -08001223 def _record_result(self, result, record, results_generator, bug_reporter,
1224 bug_template):
1225 """
1226 Record a single test job result.
1227
1228 @param result: Status instance for job.
1229 @param record: callable that records job status.
1230 prototype:
1231 record(base_job.status_log_entry)
1232 @param results_generator: Results generator for sending job retries.
1233 @param bug_reporter: Reporter instance for reporting bugs.
1234 @param bug_template: A template dictionary specifying the default bug
1235 filing options for failures in this suite.
1236 """
Allen Li26b340d2016-12-29 15:23:01 -08001237 result.record_all(record)
Allen Li4df053e2016-12-29 16:05:41 -08001238 self._remember_job_keyval(result)
Allen Li26b340d2016-12-29 15:23:01 -08001239
Allen Licc752292017-01-03 12:44:39 -08001240 if self._has_retry(result):
Allen Li26b340d2016-12-29 15:23:01 -08001241 new_job = self._schedule_test(
1242 record=record, test=self._jobs_to_tests[result.id],
1243 retry_for=result.id, ignore_errors=True)
1244 if new_job:
1245 results_generator.send([new_job])
1246
1247 # TODO (fdeng): If the suite times out before a retry could
1248 # finish, we would lose the chance to file a bug for the
1249 # original job.
1250 if self._should_report(result):
Allen Li11308982016-12-29 16:19:55 -08001251 if self._should_file_bugs:
Allen Li47c9fca2016-12-29 16:22:53 -08001252 self._file_bug(result, bug_reporter, bug_template)
Allen Li26b340d2016-12-29 15:23:01 -08001253 else:
Allen Lid5df44b2016-12-29 15:59:06 -08001254 # reporting modules have dependency on external
1255 # packages, e.g., httplib2 Such dependency can cause
1256 # issue to any module tries to import suite.py without
1257 # building site-packages first. Since the reporting
1258 # modules are only used in this function, move the
1259 # imports here avoid the requirement of building site
1260 # packages to use other functions in this module.
1261 from autotest_lib.server.cros.dynamic_suite import reporting
1262
Allen Li7b973112016-12-29 16:17:41 -08001263 reporting.send_email(
1264 self._get_test_bug(result),
1265 self._get_bug_template(result, bug_template))
Allen Li26b340d2016-12-29 15:23:01 -08001266
1267
Allen Lid5df44b2016-12-29 15:59:06 -08001268 def _get_bug_template(self, result, bug_template):
1269 """Get BugTemplate for test job.
1270
1271 @param result: Status instance for job.
1272 @param bug_template: A template dictionary specifying the default bug
1273 filing options for failures in this suite.
1274 @returns: BugTemplate instance
1275 """
1276 # reporting modules have dependency on external packages, e.g., httplib2
1277 # Such dependency can cause issue to any module tries to import suite.py
1278 # without building site-packages first. Since the reporting modules are
1279 # only used in this function, move the imports here avoid the
1280 # requirement of building site packages to use other functions in this
1281 # module.
1282 from autotest_lib.server.cros.dynamic_suite import reporting_utils
1283
1284 # Try to merge with bug template in test control file.
1285 template = reporting_utils.BugTemplate(bug_template)
1286 try:
1287 test_data = self._jobs_to_tests[result.id]
1288 return template.finalize_bug_template(
1289 test_data.bug_template)
1290 except AttributeError:
1291 # Test control file does not have bug template defined.
1292 return template.bug_template
1293 except reporting_utils.InvalidBugTemplateException as e:
1294 logging.error('Merging bug templates failed with '
1295 'error: %s An empty bug template will '
1296 'be used.', e)
1297 return {}
1298
1299
Allen Li003913e2016-12-29 15:53:34 -08001300 def _get_test_bug(self, result):
1301 """Get TestBug for the given result.
1302
1303 @param result: Status instance for a test job.
1304 @returns: TestBug instance.
1305 """
1306 # reporting modules have dependency on external packages, e.g., httplib2
1307 # Such dependency can cause issue to any module tries to import suite.py
1308 # without building site-packages first. Since the reporting modules are
1309 # only used in this function, move the imports here avoid the
1310 # requirement of building site packages to use other functions in this
1311 # module.
1312 from autotest_lib.server.cros.dynamic_suite import reporting
1313
1314 job_views = self._tko.run('get_detailed_test_views',
1315 afe_job_id=result.id)
1316 return reporting.TestBug(self._cros_build,
1317 site_utils.get_chrome_version(job_views),
1318 self._tag,
1319 result)
1320
1321
Allen Li11308982016-12-29 16:19:55 -08001322 @property
1323 def _should_file_bugs(self):
1324 """Return whether bugs should be filed.
1325
1326 @returns: bool
1327 """
1328 # File bug when failure is one of the _FILE_BUG_SUITES,
1329 # otherwise send an email to the owner anc cc.
1330 return self._tag in _FILE_BUG_SUITES
1331
1332
Allen Li47c9fca2016-12-29 16:22:53 -08001333 def _file_bug(self, result, bug_reporter, bug_template):
1334 """File a bug for a test job result.
1335
1336 @param result: Status instance for job.
1337 @param bug_reporter: Reporter instance for reporting bugs.
1338 @param bug_template: A template dictionary specifying the default bug
1339 filing options for failures in this suite.
1340 """
1341 bug_id, bug_count = bug_reporter.report(
1342 self._get_test_bug(result),
1343 self._get_bug_template(result, bug_template))
1344
1345 # We use keyvals to communicate bugs filed with run_suite.
1346 if bug_id is not None:
1347 bug_keyvals = tools.create_bug_keyvals(
1348 result.id, result.test_name,
1349 (bug_id, bug_count))
1350 try:
1351 utils.write_keyval(self._results_dir,
1352 bug_keyvals)
1353 except ValueError:
1354 logging.error('Unable to log bug keyval for:%s',
1355 result.test_name)
1356
1357
Alex Miller3a69adc2012-12-19 13:38:31 -08001358 def abort(self):
1359 """
1360 Abort all scheduled test jobs.
1361 """
1362 if self._jobs:
1363 job_ids = [job.id for job in self._jobs]
1364 self._afe.run('abort_host_queue_entries', job__id__in=job_ids)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001365
1366
Allen Li4df053e2016-12-29 16:05:41 -08001367 def _remember_job_keyval(self, job):
Chris Masoned9f13c52012-08-29 10:37:08 -07001368 """
1369 Record provided job as a suite job keyval, for later referencing.
1370
Allen Li4df053e2016-12-29 16:05:41 -08001371 @param job: some representation of a job that has the attributes:
1372 id, test_name, and owner
Chris Masoned9f13c52012-08-29 10:37:08 -07001373 """
Allen Li3cc73cd2016-12-12 16:02:21 -08001374 if self._results_dir and job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -07001375 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -07001376 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -07001377 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001378 utils.write_keyval(
1379 self._results_dir,
1380 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
1381
Dan Shid1521802013-05-24 13:08:37 -07001382
Allen Lie61acfe2016-12-29 16:27:21 -08001383 @classmethod
1384 def find_and_parse_tests(cls, cf_getter, predicate, suite_name='',
Simran Basi5ace6f22016-01-06 17:30:44 -08001385 add_experimental=False, forgiving_parser=True,
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001386 run_prod_code=False, test_args=None):
Dan Shi5783f8a2014-12-22 14:34:45 -08001387 """
1388 Function to scan through all tests and find eligible tests.
1389
1390 Search through all tests based on given cf_getter, suite_name,
1391 add_experimental and forgiving_parser, return the tests that match
1392 given predicate.
1393
1394 @param cf_getter: a control_file_getter.ControlFileGetter used to list
1395 and fetch the content of control files
Chris Masone44e4d6c2012-08-15 14:25:53 -07001396 @param predicate: a function that should return True when run over a
1397 ControlData representation of a control file that should be in
1398 this Suite.
beepsc594c1c2013-07-09 22:33:18 -07001399 @param suite_name: If specified, this method will attempt to restrain
1400 the search space to just this suite's control files.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001401 @param add_experimental: add tests with experimental attribute set.
beeps89f1e062013-09-18 12:00:17 -07001402 @param forgiving_parser: If False, will raise ControlVariableExceptions
1403 if any are encountered when parsing control
1404 files. Note that this can raise an exception
1405 for syntax errors in unrelated files, because
1406 we parse them before applying the predicate.
Simran Basi5ace6f22016-01-06 17:30:44 -08001407 @param run_prod_code: If true, the suite will run the test code that
1408 lives in prod aka the test code currently on the
1409 lab servers by disabling SSP for the discovered
1410 tests.
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001411 @param test_args: A dict of args to be seeded in test control file.
beeps89f1e062013-09-18 12:00:17 -07001412
1413 @raises ControlVariableException: If forgiving_parser is False and there
1414 is a syntax error in a control file.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001415
1416 @return list of ControlData objects that should be run, with control
Dan Shief5b53f2013-01-22 10:22:01 -08001417 file text added in |text| attribute. Results are sorted based
1418 on the TIME setting in control file, slowest test comes first.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001419 """
Allen Li9ea208e2017-02-28 13:43:11 -08001420 tests = _find_test_control_data_for_suite(
Allen Li20159032017-02-28 13:22:51 -08001421 cf_getter, suite_name, add_experimental, forgiving_parser,
1422 run_prod_code=run_prod_code,
1423 test_args=test_args)
Dan Shi5783f8a2014-12-22 14:34:45 -08001424 logging.debug('Parsed %s control files.', len(tests))
Dan Shief5b53f2013-01-22 10:22:01 -08001425 tests = [test for test in tests.itervalues() if predicate(test)]
1426 tests.sort(key=lambda t:
1427 control_data.ControlData.get_test_time_index(t.time),
1428 reverse=True)
1429 return tests
Dan Shi5783f8a2014-12-22 14:34:45 -08001430
1431
Allen Lie61acfe2016-12-29 16:27:21 -08001432 @classmethod
1433 def find_possible_tests(cls, cf_getter, predicate, suite_name='', count=10):
Dan Shi5783f8a2014-12-22 14:34:45 -08001434 """
1435 Function to scan through all tests and find possible tests.
1436
1437 Search through all tests based on given cf_getter, suite_name,
1438 add_experimental and forgiving_parser. Use the given predicate to
1439 calculate the similarity and return the top 10 matches.
1440
1441 @param cf_getter: a control_file_getter.ControlFileGetter used to list
1442 and fetch the content of control files
1443 @param predicate: a function that should return a tuple of (name, ratio)
1444 when run over a ControlData representation of a control file that
1445 should be in this Suite. `name` is the key to be compared, e.g.,
1446 a suite name or test name. `ratio` is a value between [0,1]
1447 indicating the similarity of `name` and the value to be compared.
1448 @param suite_name: If specified, this method will attempt to restrain
1449 the search space to just this suite's control files.
1450 @param count: Number of suggestions to return, default to 10.
1451
1452 @return list of top names that similar to the given test, sorted by
1453 match ratio.
1454 """
Allen Li9ea208e2017-02-28 13:43:11 -08001455 tests = _find_test_control_data_for_suite(
Allen Li066f5872017-02-28 13:30:44 -08001456 cf_getter, suite_name,
1457 add_experimental=True, forgiving_parser=True)
Dan Shi5783f8a2014-12-22 14:34:45 -08001458 logging.debug('Parsed %s control files.', len(tests))
1459 similarities = {}
1460 for test in tests.itervalues():
1461 ratios = predicate(test)
1462 # Some predicates may return a list of tuples, e.g.,
1463 # name_in_tag_similarity_predicate. Convert all returns to a list.
1464 if not isinstance(ratios, list):
1465 ratios = [ratios]
1466 for name, ratio in ratios:
1467 similarities[name] = ratio
1468 return [s[0] for s in
1469 sorted(similarities.items(), key=operator.itemgetter(1),
1470 reverse=True)][:count]
Allen Li9fcd4b42016-12-12 16:15:14 -08001471
1472
1473def _is_nonexistent_board_error(e):
1474 """Return True if error is caused by nonexistent board label.
1475
1476 As of this writing, the particular case we want looks like this:
1477
1478 1) e.problem_keys is a dictionary
1479 2) e.problem_keys['meta_hosts'] exists as the only key
1480 in the dictionary.
1481 3) e.problem_keys['meta_hosts'] matches this pattern:
1482 "Label "board:.*" not found"
1483
1484 We check for conditions 1) and 2) on the
1485 theory that they're relatively immutable.
1486 We don't check condition 3) because it seems
1487 likely to be a maintenance burden, and for the
1488 times when we're wrong, being right shouldn't
1489 matter enough (we _hope_).
1490
1491 @param e: proxy.ValidationError instance
1492 @returns: boolean
1493 """
1494 return (isinstance(e.problem_keys, dict)
1495 and len(e.problem_keys) == 1
1496 and 'meta_hosts' in e.problem_keys)