blob: b12dc2040d97e7b2e31fa5afc21ead1bcdb9e1d1 [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 Li6b161c62017-02-28 13:08:54 -0800255class _DynamicSuiteDiscoverer(object):
256 """Test discoverer for dynamic suite tests."""
257
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
269 def discover_tests(self):
270 """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
Chris Masone44e4d6c2012-08-15 14:25:53 -0700301class Suite(object):
302 """
303 A suite of tests, defined by some predicate over control file variables.
304
305 Given a place to search for control files a predicate to match the desired
306 tests, can gather tests and fire off jobs to run them, and then wait for
307 results.
308
309 @var _predicate: a function that should return True when run over a
310 ControlData representation of a control file that should be in
311 this Suite.
312 @var _tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700313 @var _builds: the builds on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700314 @var _afe: an instance of AFE as defined in server/frontend.py.
315 @var _tko: an instance of TKO as defined in server/frontend.py.
316 @var _jobs: currently scheduled jobs, if any.
Fang Denge3bc24b2014-03-17 15:19:46 -0700317 @var _jobs_to_tests: a dictionary that maps job ids to tests represented
318 ControlData objects.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700319 @var _cf_getter: a control_file_getter.ControlFileGetter
Fang Denge3bc24b2014-03-17 15:19:46 -0700320 @var _retry: a bool value indicating whether jobs should be retried on
321 failure.
322 @var _retry_handler: a RetryHandler object.
323
Chris Masone44e4d6c2012-08-15 14:25:53 -0700324 """
325
326
327 @staticmethod
Allen Li9864ed62016-12-29 16:30:53 -0800328 def _create_ds_getter(build, devserver):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700329 """
330 @param build: the build on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700331 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700332 @return a FileSystemGetter instance that looks under |autotest_dir|.
333 """
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700334 return control_file_getter.DevServerGetter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700335
336
337 @staticmethod
338 def create_fs_getter(autotest_dir):
339 """
340 @param autotest_dir: the place to find autotests.
341 @return a FileSystemGetter instance that looks under |autotest_dir|.
342 """
343 # currently hard-coded places to look for tests.
344 subpaths = ['server/site_tests', 'client/site_tests',
345 'server/tests', 'client/tests']
346 directories = [os.path.join(autotest_dir, p) for p in subpaths]
347 return control_file_getter.FileSystemGetter(directories)
348
349
350 @staticmethod
Allen Lif20e17d2017-01-03 18:24:19 -0800351 def name_in_tag_predicate(name):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700352 """Returns predicate that takes a control file and looks for |name|.
353
354 Builds a predicate that takes in a parsed control file (a ControlData)
355 and returns True if the SUITE tag is present and contains |name|.
356
357 @param name: the suite name to base the predicate on.
358 @return a callable that takes a ControlData and looks for |name| in that
359 ControlData object's suite member.
360 """
Allen Li30833702017-01-03 18:34:15 -0800361 return lambda t: name in t.suite_tag_parts
Dan Shi5783f8a2014-12-22 14:34:45 -0800362
363
Allen Lif20e17d2017-01-03 18:24:19 -0800364 @staticmethod
365 def name_in_tag_similarity_predicate(name):
Dan Shi5783f8a2014-12-22 14:34:45 -0800366 """Returns predicate that takes a control file and gets the similarity
367 of the suites in the control file and the given name.
368
369 Builds a predicate that takes in a parsed control file (a ControlData)
370 and returns a list of tuples of (suite name, ratio), where suite name
371 is each suite listed in the control file, and ratio is the similarity
372 between each suite and the given name.
373
374 @param name: the suite name to base the predicate on.
375 @return a callable that takes a ControlData and returns a list of tuples
376 of (suite name, ratio), where suite name is each suite listed in
377 the control file, and ratio is the similarity between each suite
378 and the given name.
379 """
Allen Li30833702017-01-03 18:34:15 -0800380 return lambda t: [(suite,
381 difflib.SequenceMatcher(a=suite, b=name).ratio())
382 for suite in t.suite_tag_parts] or [(None, 0)]
Chris Masone44e4d6c2012-08-15 14:25:53 -0700383
384
385 @staticmethod
Aviv Keshet40222a42013-06-04 16:25:49 -0700386 def test_name_equals_predicate(test_name):
387 """Returns predicate that matched based on a test's name.
388
389 Builds a predicate that takes in a parsed control file (a ControlData)
390 and returns True if the test name is equal to |test_name|.
391
392 @param test_name: the test name to base the predicate on.
393 @return a callable that takes a ControlData and looks for |test_name|
394 in that ControlData's name.
395 """
396 return lambda t: hasattr(t, 'name') and test_name == t.name
397
398
399 @staticmethod
Aviv Kesheta6adc7a2013-08-30 11:13:38 -0700400 def test_name_matches_pattern_predicate(test_name_pattern):
401 """Returns predicate that matches based on a test's name pattern.
402
403 Builds a predicate that takes in a parsed control file (a ControlData)
404 and returns True if the test name matches the given regular expression.
405
406 @param test_name_pattern: regular expression (string) to match against
407 test names.
408 @return a callable that takes a ControlData and returns
409 True if the name fields matches the pattern.
410 """
411 return lambda t: hasattr(t, 'name') and re.match(test_name_pattern,
412 t.name)
413
414
415 @staticmethod
416 def test_file_matches_pattern_predicate(test_file_pattern):
417 """Returns predicate that matches based on a test's file name pattern.
418
419 Builds a predicate that takes in a parsed control file (a ControlData)
420 and returns True if the test's control file name matches the given
421 regular expression.
422
423 @param test_file_pattern: regular expression (string) to match against
424 control file names.
425 @return a callable that takes a ControlData and and returns
426 True if control file name matches the pattern.
427 """
428 return lambda t: hasattr(t, 'path') and re.match(test_file_pattern,
429 t.path)
430
431
432 @staticmethod
Shuqian Zhaoab468812015-04-08 14:40:38 -0700433 def matches_attribute_expression_predicate(test_attr_boolstr):
434 """Returns predicate that matches based on boolean expression of
435 attributes.
436
437 Builds a predicate that takes in a parsed control file (a ControlData)
438 ans returns True if the test attributes satisfy the given attribute
439 boolean expression.
440
441 @param test_attr_boolstr: boolean expression of the attributes to be
442 test, like 'system:all and interval:daily'.
443
444 @return a callable that takes a ControlData and returns True if the test
445 attributes satisfy the given boolean expression.
446 """
447 return lambda t: boolparse_lib.BoolstrResult(
448 test_attr_boolstr, t.attributes)
449
450 @staticmethod
Dan Shi5783f8a2014-12-22 14:34:45 -0800451 def test_name_similarity_predicate(test_name):
452 """Returns predicate that matched based on a test's name.
453
454 Builds a predicate that takes in a parsed control file (a ControlData)
455 and returns a tuple of (test name, ratio), where ratio is the similarity
456 between the test name and the given test_name.
457
458 @param test_name: the test name to base the predicate on.
459 @return a callable that takes a ControlData and returns a tuple of
460 (test name, ratio), where ratio is the similarity between the
461 test name and the given test_name.
462 """
463 return lambda t: ((None, 0) if not hasattr(t, 'name') else
464 (t.name,
465 difflib.SequenceMatcher(a=t.name, b=test_name).ratio()))
466
467
468 @staticmethod
469 def test_file_similarity_predicate(test_file_pattern):
470 """Returns predicate that gets the similarity based on a test's file
471 name pattern.
472
473 Builds a predicate that takes in a parsed control file (a ControlData)
474 and returns a tuple of (file path, ratio), where ratio is the
475 similarity between the test file name and the given test_file_pattern.
476
477 @param test_file_pattern: regular expression (string) to match against
478 control file names.
479 @return a callable that takes a ControlData and and returns a tuple of
480 (file path, ratio), where ratio is the similarity between the
481 test file name and the given test_file_pattern.
482 """
483 return lambda t: ((None, 0) if not hasattr(t, 'path') else
484 (t.path, difflib.SequenceMatcher(a=t.path,
485 b=test_file_pattern).ratio()))
486
487
Allen Li9864ed62016-12-29 16:30:53 -0800488 @classmethod
489 def list_all_suites(cls, build, devserver, cf_getter=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700490 """
491 Parses all ControlData objects with a SUITE tag and extracts all
492 defined suite names.
493
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700494 @param build: the build on which we're running this suite.
495 @param devserver: the devserver which contains the build.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700496 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
497 using DevServerGetter.
498
499 @return list of suites
500 """
501 if cf_getter is None:
Allen Li9864ed62016-12-29 16:30:53 -0800502 cf_getter = cls._create_ds_getter(build, devserver)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700503
504 suites = set()
Allen Li30833702017-01-03 18:34:15 -0800505 predicate = lambda t: True
Allen Lic9be3662017-01-03 17:56:26 -0800506 for test in cls.find_and_parse_tests(cf_getter, predicate,
Allen Li30833702017-01-03 18:34:15 -0800507 add_experimental=True):
Allen Lif20e17d2017-01-03 18:24:19 -0800508 suites.update(test.suite_tag_parts)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700509 return list(suites)
510
511
512 @staticmethod
Dan Shi36cfd832014-10-10 13:38:51 -0700513 def get_test_source_build(builds, **dargs):
514 """Get the build of test code.
515
516 Get the test source build from arguments. If parameter
517 `test_source_build` is set and has a value, return its value. Otherwise
518 returns the ChromeOS build name if it exists. If ChromeOS build is not
519 specified either, raise SuiteArgumentException.
520
521 @param builds: the builds on which we're running this suite. It's a
522 dictionary of version_prefix:build.
523 @param **dargs: Any other Suite constructor parameters, as described
524 in Suite.__init__ docstring.
525
526 @return: The build contains the test code.
527 @raise: SuiteArgumentException if both test_source_build and ChromeOS
528 build are not specified.
529
530 """
531 if dargs.get('test_source_build', None):
532 return dargs['test_source_build']
533 test_source_build = builds.get(provision.CROS_VERSION_PREFIX, None)
534 if not test_source_build:
535 raise error.SuiteArgumentException(
536 'test_source_build must be specified if CrOS build is not '
537 'specified.')
538 return test_source_build
539
540
Allen Li9864ed62016-12-29 16:30:53 -0800541 @classmethod
542 def create_from_predicates(cls, predicates, builds, board, devserver,
Simran Basi5ace6f22016-01-06 17:30:44 -0800543 cf_getter=None, name='ad_hoc_suite',
544 run_prod_code=False, **dargs):
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700545 """
546 Create a Suite using a given predicate test filters.
547
548 Uses supplied predicate(s) to instantiate a Suite. Looks for tests in
549 |autotest_dir| and will schedule them using |afe|. Pulls control files
550 from the default dev server. Results will be pulled from |tko| upon
551 completion.
552
553 @param predicates: A list of callables that accept ControlData
554 representations of control files. A test will be
Aviv Keshet938a6772013-07-25 14:05:45 -0700555 included in suite if all callables in this list
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700556 return True on the given control file.
Dan Shi36cfd832014-10-10 13:38:51 -0700557 @param builds: the builds on which we're running this suite. It's a
558 dictionary of version_prefix:build.
Alex Millera0913072013-06-12 10:01:51 -0700559 @param board: the board on which we're running this suite.
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700560 @param devserver: the devserver which contains the build.
561 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
562 using DevServerGetter.
563 @param name: name of suite. Defaults to 'ad_hoc_suite'
Simran Basi5ace6f22016-01-06 17:30:44 -0800564 @param run_prod_code: If true, the suite will run the tests that
565 lives in prod aka the test code currently on the
566 lab servers.
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700567 @param **dargs: Any other Suite constructor parameters, as described
568 in Suite.__init__ docstring.
569 @return a Suite instance.
570 """
571 if cf_getter is None:
Simran Basi5ace6f22016-01-06 17:30:44 -0800572 if run_prod_code:
Allen Li9864ed62016-12-29 16:30:53 -0800573 cf_getter = cls.create_fs_getter(_AUTOTEST_DIR)
Simran Basi5ace6f22016-01-06 17:30:44 -0800574 else:
Allen Lic9be3662017-01-03 17:56:26 -0800575 build = cls.get_test_source_build(builds, **dargs)
Allen Li9864ed62016-12-29 16:30:53 -0800576 cf_getter = cls._create_ds_getter(build, devserver)
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700577
Allen Lic9be3662017-01-03 17:56:26 -0800578 return cls(predicates,
579 name, builds, board, cf_getter, run_prod_code, **dargs)
Aviv Keshet69ebb6c2013-06-11 13:58:44 -0700580
581
Allen Li9864ed62016-12-29 16:30:53 -0800582 @classmethod
583 def create_from_name(cls, name, builds, board, devserver, cf_getter=None,
Alex Millera0913072013-06-12 10:01:51 -0700584 **dargs):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700585 """
586 Create a Suite using a predicate based on the SUITE control file var.
587
588 Makes a predicate based on |name| and uses it to instantiate a Suite
589 that looks for tests in |autotest_dir| and will schedule them using
590 |afe|. Pulls control files from the default dev server.
591 Results will be pulled from |tko| upon completion.
592
593 @param name: a value of the SUITE control file variable to search for.
Dan Shi36cfd832014-10-10 13:38:51 -0700594 @param builds: the builds on which we're running this suite. It's a
595 dictionary of version_prefix:build.
Alex Millera0913072013-06-12 10:01:51 -0700596 @param board: the board on which we're running this suite.
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700597 @param devserver: the devserver which contains the build.
Aviv Keshet813d6782013-06-04 17:11:03 -0700598 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
599 using DevServerGetter.
600 @param **dargs: Any other Suite constructor parameters, as described
601 in Suite.__init__ docstring.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700602 @return a Suite instance.
603 """
604 if cf_getter is None:
Allen Li9864ed62016-12-29 16:30:53 -0800605 build = cls.get_test_source_build(builds, **dargs)
606 cf_getter = cls._create_ds_getter(build, devserver)
Chris Sosaaccb5ce2012-08-30 17:29:15 -0700607
Allen Lic9be3662017-01-03 17:56:26 -0800608 return cls([cls.name_in_tag_predicate(name)],
609 name, builds, board, cf_getter, **dargs)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700610
611
Allen Li6fff5502016-12-09 18:04:26 -0800612 def __init__(
613 self,
614 predicates,
615 tag,
616 builds,
617 board,
618 cf_getter,
619 run_prod_code=False,
620 afe=None,
621 tko=None,
622 pool=None,
623 results_dir=None,
624 max_runtime_mins=24*60,
625 timeout_mins=24*60,
626 file_bugs=False,
627 file_experimental_bugs=False,
628 suite_job_id=None,
629 ignore_deps=False,
Allen Li493eefa2016-12-09 18:05:35 -0800630 extra_deps=None,
Allen Li6fff5502016-12-09 18:04:26 -0800631 priority=priorities.Priority.DEFAULT,
632 forgiving_parser=True,
633 wait_for_results=True,
634 job_retry=False,
635 max_retries=sys.maxint,
636 offload_failures_only=False,
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800637 test_source_build=None,
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800638 job_keyvals=None,
639 test_args=None
Allen Li6fff5502016-12-09 18:04:26 -0800640 ):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700641 """
642 Constructor
643
Aviv Keshet40222a42013-06-04 16:25:49 -0700644 @param predicates: A list of callables that accept ControlData
645 representations of control files. A test will be
646 included in suite is all callables in this list
647 return True on the given control file.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700648 @param tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700649 @param builds: the builds on which we're running this suite.
Alex Millera0913072013-06-12 10:01:51 -0700650 @param board: the board on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700651 @param cf_getter: a control_file_getter.ControlFileGetter
652 @param afe: an instance of AFE as defined in server/frontend.py.
653 @param tko: an instance of TKO as defined in server/frontend.py.
654 @param pool: Specify the pool of machines to use for scheduling
655 purposes.
Simran Basi5ace6f22016-01-06 17:30:44 -0800656 @param run_prod_code: If true, the suite will run the test code that
657 lives in prod aka the test code currently on the
658 lab servers.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700659 @param results_dir: The directory where the job can write results to.
660 This must be set if you want job_id of sub-jobs
661 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -0800662 @param max_runtime_mins: Maximum suite runtime, in minutes.
Alex Miller028b0312013-09-07 15:25:45 -0700663 @param timeout: Maximum job lifetime, in hours.
Aviv Keshet18308922013-02-19 17:49:49 -0800664 @param suite_job_id: Job id that will act as parent id to all sub jobs.
665 Default: None
Aviv Keshetd7959f32013-05-17 15:58:43 -0700666 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
667 attribute and skip applying of dependency labels.
668 (Default:False)
Alex Miller47a03672013-08-27 09:09:53 -0700669 @param extra_deps: A list of strings which are the extra DEPENDENCIES
670 to add to each test being scheduled.
Alex Miller7d658cf2013-09-04 16:00:35 -0700671 @param priority: Integer priority level. Higher is more important.
Dan Shi95122412013-11-12 16:20:33 -0800672 @param wait_for_results: Set to False to run the suite job without
673 waiting for test jobs to finish. Default is
674 True.
Fang Denge3bc24b2014-03-17 15:19:46 -0700675 @param job_retry: A bool value indicating whether jobs should be retired
676 on failure. If True, the field 'JOB_RETRIES' in
677 control files will be respected. If False, do not
678 retry.
Fang Deng443f1952015-01-02 14:51:49 -0800679 @param max_retries: Maximum retry limit at suite level.
680 Regardless how many times each individual test
681 has been retried, the total number of retries
682 happening in the suite can't exceed _max_retries.
683 Default to sys.maxint.
Simran Basi1e10e922015-04-16 15:09:56 -0700684 @param offload_failures_only: Only enable gs_offloading for failed
685 jobs.
Dan Shi36cfd832014-10-10 13:38:51 -0700686 @param test_source_build: Build that contains the server-side test code.
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800687 @param job_keyvals: General job keyvals to be inserted into keyval file,
688 which will be used by tko/parse later.
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800689 @param test_args: A dict of args passed all the way to each individual
690 test that will be actually ran.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700691 """
Allen Li493eefa2016-12-09 18:05:35 -0800692 if extra_deps is None:
693 extra_deps = []
694
Chris Masone44e4d6c2012-08-15 14:25:53 -0700695 self._tag = tag
Dan Shi36cfd832014-10-10 13:38:51 -0700696 self._builds = builds
Alex Millera0913072013-06-12 10:01:51 -0700697 self._board = board
Chris Masone44e4d6c2012-08-15 14:25:53 -0700698 self._cf_getter = cf_getter
699 self._results_dir = results_dir
700 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
701 delay_sec=10,
702 debug=False)
703 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
704 delay_sec=10,
705 debug=False)
706 self._pool = pool
707 self._jobs = []
Fang Denge3bc24b2014-03-17 15:19:46 -0700708 self._jobs_to_tests = {}
Allen Li7947f732016-12-29 16:44:28 -0800709 self.tests = self.find_and_parse_tests(
Allen Li8a649092016-12-09 18:07:39 -0800710 self._cf_getter,
Allen Lid69b9f02016-12-09 18:15:59 -0800711 lambda control_data: all(f(control_data) for f in predicates),
Allen Li8a649092016-12-09 18:07:39 -0800712 self._tag,
713 add_experimental=True,
714 forgiving_parser=forgiving_parser,
715 run_prod_code=run_prod_code,
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800716 test_args=test_args,
Allen Li8a649092016-12-09 18:07:39 -0800717 )
beeps89f1e062013-09-18 12:00:17 -0700718
Simran Basic68cda42012-11-19 17:03:18 -0800719 self._max_runtime_mins = max_runtime_mins
Simran Basi8705d672013-11-19 15:56:58 -0800720 self._timeout_mins = timeout_mins
Alex Millera3a4fe72013-01-22 09:57:47 -0800721 self._file_bugs = file_bugs
beepsda5b7112013-05-30 11:34:14 -0700722 self._file_experimental_bugs = file_experimental_bugs
Aviv Keshet18308922013-02-19 17:49:49 -0800723 self._suite_job_id = suite_job_id
Aviv Keshetd7959f32013-05-17 15:58:43 -0700724 self._ignore_deps = ignore_deps
Alex Miller47a03672013-08-27 09:09:53 -0700725 self._extra_deps = extra_deps
Alex Miller7d658cf2013-09-04 16:00:35 -0700726 self._priority = priority
Fang Denge3bc24b2014-03-17 15:19:46 -0700727 self._job_retry=job_retry
Fang Deng443f1952015-01-02 14:51:49 -0800728 self._max_retries = max_retries
Fang Denge3bc24b2014-03-17 15:19:46 -0700729 # RetryHandler to be initialized in schedule()
730 self._retry_handler = None
Dan Shi95122412013-11-12 16:20:33 -0800731 self.wait_for_results = wait_for_results
Simran Basi1e10e922015-04-16 15:09:56 -0700732 self._offload_failures_only = offload_failures_only
Dan Shi36cfd832014-10-10 13:38:51 -0700733 self._test_source_build = test_source_build
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800734 self._job_keyvals = job_keyvals
Shuqian Zhaoed0da862017-03-06 14:47:13 -0800735 self._test_args = test_args
Alex Millera3a4fe72013-01-22 09:57:47 -0800736
Chris Masone44e4d6c2012-08-15 14:25:53 -0700737
738 @property
Allen Lidb8eafe2016-12-12 16:33:58 -0800739 def _cros_build(self):
740 """Return the CrOS build or the first build in the builds dict."""
741 # TODO(ayatane): Note that the builds dict isn't ordered. I'm not
742 # sure what the implications of this are, but it's probably not a
743 # good thing.
744 return self._builds.get(provision.CROS_VERSION_PREFIX,
745 self._builds.values()[0])
746
747
Fang Denge3bc24b2014-03-17 15:19:46 -0700748 def _create_job(self, test, retry_for=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700749 """
750 Thin wrapper around frontend.AFE.create_job().
751
752 @param test: ControlData object for a test to run.
Fang Denge3bc24b2014-03-17 15:19:46 -0700753 @param retry_for: If the to-be-created job is a retry for an
754 old job, the afe_job_id of the old job will
755 be passed in as |retry_for|, which will be
756 recorded in the new job's keyvals.
757 @returns: A frontend.Job object with an added test_name member.
758 test_name is used to preserve the higher level TEST_NAME
759 name of the job.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700760 """
Allen Li069fc252016-12-12 16:26:21 -0800761 test_obj = self._afe.create_job(
762 control_file=test.text,
Allen Li468d6152016-12-12 16:35:01 -0800763 name=tools.create_job_name(
764 self._test_source_build or self._cros_build,
765 self._tag,
766 test.name),
Allen Li069fc252016-12-12 16:26:21 -0800767 control_type=test.test_type.capitalize(),
768 meta_hosts=[self._board]*test.sync_count,
Allen Lic68ca4a2016-12-12 17:28:36 -0800769 dependencies=self._create_job_deps(test),
Allen Lia4ae1352016-12-12 16:26:57 -0800770 keyvals=self._create_keyvals_for_test_job(test, retry_for),
Allen Li069fc252016-12-12 16:26:21 -0800771 max_runtime_mins=self._max_runtime_mins,
772 timeout_mins=self._timeout_mins,
773 parent_job_id=self._suite_job_id,
774 test_retry=test.retries,
775 priority=self._priority,
776 synch_count=test.sync_count,
777 require_ssp=test.require_ssp)
778
779 test_obj.test_name = test.name
780 return test_obj
781
782
Allen Lic68ca4a2016-12-12 17:28:36 -0800783 def _create_job_deps(self, test):
784 """Create job deps list for a test job.
785
786 @returns: A list of dependency strings.
787 """
788 if self._ignore_deps:
789 job_deps = []
790 else:
791 job_deps = list(test.dependencies)
792 job_deps.extend(self._extra_deps)
793 if self._pool:
794 job_deps.append(self._pool)
795 job_deps.append(self._board)
796 return job_deps
797
798
Allen Li069fc252016-12-12 16:26:21 -0800799 def _create_keyvals_for_test_job(self, test, retry_for=None):
800 """Create keyvals dict for creating a test job.
801
802 @param test: ControlData object for a test to run.
803 @param retry_for: If the to-be-created job is a retry for an
804 old job, the afe_job_id of the old job will
805 be passed in as |retry_for|, which will be
806 recorded in the new job's keyvals.
807 @returns: A keyvals dict for creating the test job.
808 """
Allen Li015e71b2016-12-12 16:37:25 -0800809 keyvals = {
810 constants.JOB_BUILD_KEY: self._cros_build,
811 constants.JOB_SUITE_KEY: self._tag,
812 constants.JOB_EXPERIMENTAL_KEY: test.experimental,
813 constants.JOB_BUILDS_KEY: self._builds
814 }
Dan Shi36cfd832014-10-10 13:38:51 -0700815 # test_source_build is saved to job_keyvals so scheduler can retrieve
816 # the build name from database when compiling autoserv commandline.
817 # This avoid a database change to add a new field in afe_jobs.
Allen Li015e71b2016-12-12 16:37:25 -0800818 #
Dan Shi36cfd832014-10-10 13:38:51 -0700819 # Only add `test_source_build` to job keyvals if the build is different
820 # from the CrOS build or the job uses more than one build, e.g., both
821 # firmware and CrOS will be updated in the dut.
822 # This is for backwards compatibility, so the update Autotest code can
823 # compile an autoserv command line to run in a SSP container using
824 # previous builds.
825 if (self._test_source_build and
Allen Li015e71b2016-12-12 16:37:25 -0800826 (self._cros_build != self._test_source_build or
827 len(self._builds) > 1)):
828 keyvals[constants.JOB_TEST_SOURCE_BUILD_KEY] = \
829 self._test_source_build
Dan Shidac462f2015-08-14 11:07:32 -0700830 for prefix, build in self._builds.iteritems():
831 if prefix == provision.FW_RW_VERSION_PREFIX:
832 keyvals[constants.FWRW_BUILD]= build
833 elif prefix == provision.FW_RO_VERSION_PREFIX:
834 keyvals[constants.FWRO_BUILD] = build
Allen Li015e71b2016-12-12 16:37:25 -0800835 # Add suite job id to keyvals so tko parser can read it from keyval
836 # file.
Dan Shidac462f2015-08-14 11:07:32 -0700837 if self._suite_job_id:
838 keyvals[constants.PARENT_JOB_ID] = self._suite_job_id
Allen Li015e71b2016-12-12 16:37:25 -0800839 # We drop the old job's id in the new job's keyval file so that
840 # later our tko parser can figure out the retry relationship and
841 # invalidate the results of the old job in tko database.
Fang Denge3bc24b2014-03-17 15:19:46 -0700842 if retry_for:
Fang Denge3bc24b2014-03-17 15:19:46 -0700843 keyvals[constants.RETRY_ORIGINAL_JOB_ID] = retry_for
Simran Basi1e10e922015-04-16 15:09:56 -0700844 if self._offload_failures_only:
845 keyvals[constants.JOB_OFFLOAD_FAILURES_KEY] = True
Allen Li069fc252016-12-12 16:26:21 -0800846 return keyvals
Chris Masone44e4d6c2012-08-15 14:25:53 -0700847
848
Fang Denge3bc24b2014-03-17 15:19:46 -0700849 def _schedule_test(self, record, test, retry_for=None, ignore_errors=False):
850 """Schedule a single test and return the job.
851
Allen Lie79b3cb2016-12-12 18:24:17 -0800852 Schedule a single test by creating a job, and then update relevant
853 data structures that are used to keep track of all running jobs.
Fang Denge3bc24b2014-03-17 15:19:46 -0700854
Allen Lie79b3cb2016-12-12 18:24:17 -0800855 Emits a TEST_NA status log entry if it failed to schedule the test due
856 to NoEligibleHostException or a non-existent board label.
857
858 Returns a frontend.Job object if the test is successfully scheduled.
859 If scheduling failed due to NoEligibleHostException or a non-existent
860 board label, returns None. If ignore_errors is True, all unknown
861 errors return None, otherwise the errors are raised as-is.
Fang Denge3bc24b2014-03-17 15:19:46 -0700862
863 @param record: A callable to use for logging.
864 prototype: record(base_job.status_log_entry)
865 @param test: ControlData for a test to run.
866 @param retry_for: If we are scheduling a test to retry an
867 old job, the afe_job_id of the old job
868 will be passed in as |retry_for|.
869 @param ignore_errors: If True, when an rpc error occur, ignore
870 the error and will return None.
871 If False, rpc errors will be raised.
872
Allen Lie79b3cb2016-12-12 18:24:17 -0800873 @returns: A frontend.Job object or None
Fang Denge3bc24b2014-03-17 15:19:46 -0700874 """
875 msg = 'Scheduling %s' % test.name
876 if retry_for:
877 msg = msg + ', to retry afe job %d' % retry_for
878 logging.debug(msg)
Dan Shidfea3682014-08-10 23:38:40 -0700879 begin_time_str = datetime.datetime.now().strftime(time_utils.TIME_FMT)
Fang Denge3bc24b2014-03-17 15:19:46 -0700880 try:
881 job = self._create_job(test, retry_for=retry_for)
Allen Li6fd440f2016-12-12 18:40:05 -0800882 except (error.NoEligibleHostException, proxy.ValidationError) as e:
883 if (isinstance(e, error.NoEligibleHostException)
884 or (isinstance(e, proxy.ValidationError)
885 and _is_nonexistent_board_error(e))):
886 # Treat a dependency on a non-existent board label the same as
887 # a dependency on a board that exists, but for which there's no
888 # hardware.
889 logging.debug('%s not applicable for this board/pool. '
890 'Emitting TEST_NA.', test.name)
891 Status('TEST_NA', test.name,
892 'Skipping: test not supported on this board/pool.',
Allen Li9fcd4b42016-12-12 16:15:14 -0800893 begin_time_str=begin_time_str).record_all(record)
894 return None
895 else:
Fang Denge3bc24b2014-03-17 15:19:46 -0700896 raise e
Fang Denge3bc24b2014-03-17 15:19:46 -0700897 except (error.RPCException, proxy.JSONRPCException) as e:
898 if retry_for:
899 # Mark that we've attempted to retry the old job.
900 self._retry_handler.set_attempted(job_id=retry_for)
Allen Li0ba59342016-12-12 15:57:02 -0800901
Fang Denge3bc24b2014-03-17 15:19:46 -0700902 if ignore_errors:
903 logging.error('Failed to schedule test: %s, Reason: %s',
904 test.name, e)
Allen Li0ba59342016-12-12 15:57:02 -0800905 return None
Fang Denge3bc24b2014-03-17 15:19:46 -0700906 else:
907 raise e
908 else:
909 self._jobs.append(job)
910 self._jobs_to_tests[job.id] = test
911 if retry_for:
912 # A retry job was just created, record it.
913 self._retry_handler.add_retry(
914 old_job_id=retry_for, new_job_id=job.id)
915 retry_count = (test.job_retries -
916 self._retry_handler.get_retry_max(job.id))
917 logging.debug('Job %d created to retry job %d. '
918 'Have retried for %d time(s)',
919 job.id, retry_for, retry_count)
Allen Li4df053e2016-12-29 16:05:41 -0800920 self._remember_job_keyval(job)
Fang Denge3bc24b2014-03-17 15:19:46 -0700921 return job
Fang Denge3bc24b2014-03-17 15:19:46 -0700922
923
Alex Miller3a69adc2012-12-19 13:38:31 -0800924 def schedule(self, record, add_experimental=True):
Aviv Keshet18308922013-02-19 17:49:49 -0800925 #pylint: disable-msg=C0111
Chris Masone44e4d6c2012-08-15 14:25:53 -0700926 """
927 Schedule jobs using |self._afe|.
928
929 frontend.Job objects representing each scheduled job will be put in
930 |self._jobs|.
931
Fang Denge3bc24b2014-03-17 15:19:46 -0700932 @param record: A callable to use for logging.
933 prototype: record(base_job.status_log_entry)
Chris Masone44e4d6c2012-08-15 14:25:53 -0700934 @param add_experimental: schedule experimental tests as well, or not.
Aviv Keshete9170d92013-07-19 11:20:45 -0700935 @returns: The number of tests that were scheduled.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700936 """
Allen Lif4cb5ec2017-01-03 16:58:12 -0800937 scheduled_test_names = []
938 discoverer = _DynamicSuiteDiscoverer(
939 tests=self.tests,
940 add_experimental=add_experimental)
941 logging.debug('Discovered %d stable tests.',
942 len(discoverer.stable_tests))
Alex Miller3a69adc2012-12-19 13:38:31 -0800943 logging.debug('Discovered %d unstable tests.',
Allen Lif4cb5ec2017-01-03 16:58:12 -0800944 len(discoverer.unstable_tests))
Chris Masone44e4d6c2012-08-15 14:25:53 -0700945
Alex Miller3a69adc2012-12-19 13:38:31 -0800946 Status('INFO', 'Start %s' % self._tag).record_result(record)
947 try:
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800948 # Write job_keyvals into keyval file.
949 if self._job_keyvals:
950 utils.write_keyval(self._results_dir, self._job_keyvals)
951
Allen Lif4cb5ec2017-01-03 16:58:12 -0800952 for test in discoverer.discover_tests():
Allen Lida905732016-12-12 15:49:16 -0800953 scheduled_job = self._schedule_test(record, test)
954 if scheduled_job is not None:
Shuqian Zhaocd866f32016-11-29 20:14:34 -0800955 scheduled_test_names.append(test.name)
956
957 # Write the num of scheduled tests and name of them to keyval file.
Shuqian Zhaocd866f32016-11-29 20:14:34 -0800958 logging.debug('Scheduled %d tests, writing the total to keyval.',
Allen Lia4d35022016-12-12 15:42:10 -0800959 len(scheduled_test_names))
Allen Lid4d5dda2016-12-12 15:39:11 -0800960 utils.write_keyval(
961 self._results_dir,
Allen Lidda59b82016-12-12 18:20:04 -0800962 self._make_scheduled_tests_keyvals(scheduled_test_names))
Alex Miller3a69adc2012-12-19 13:38:31 -0800963 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -0800964 logging.exception('Exception while scheduling suite')
Alex Miller3a69adc2012-12-19 13:38:31 -0800965 Status('FAIL', self._tag,
966 'Exception while scheduling suite').record_result(record)
967
Fang Deng7e655a92014-05-23 13:48:11 -0700968 if self._job_retry:
969 self._retry_handler = RetryHandler(
Fang Deng443f1952015-01-02 14:51:49 -0800970 initial_jobs_to_tests=self._jobs_to_tests,
971 max_retries=self._max_retries)
Allen Lia4d35022016-12-12 15:42:10 -0800972 return len(scheduled_test_names)
Aviv Keshete9170d92013-07-19 11:20:45 -0700973
Alex Miller3a69adc2012-12-19 13:38:31 -0800974
Allen Lidda59b82016-12-12 18:20:04 -0800975 def _make_scheduled_tests_keyvals(self, scheduled_test_names):
976 """Make a keyvals dict to write for scheduled test names.
977
978 @param scheduled_test_names: A list of scheduled test name strings.
979
980 @returns: A keyvals dict.
981 """
982 return {
983 constants.SCHEDULED_TEST_COUNT_KEY: len(scheduled_test_names),
984 constants.SCHEDULED_TEST_NAMES_KEY: repr(scheduled_test_names),
985 }
986
987
Allen Lid1cbccf2016-12-29 15:12:39 -0800988 def _should_report(self, result):
beepsda5b7112013-05-30 11:34:14 -0700989 """
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -0700990 Returns True if this failure requires to be reported.
beepsda5b7112013-05-30 11:34:14 -0700991
992 @param result: A result, encapsulating the status of the failed job.
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -0700993 @return: True if we should report this failure.
beepsda5b7112013-05-30 11:34:14 -0700994 """
Allen Licc752292017-01-03 12:44:39 -0800995 if self._has_retry(result):
Fang Denge3bc24b2014-03-17 15:19:46 -0700996 return False
997
beepsbeefc062013-08-02 11:17:09 -0700998 is_not_experimental = (
999 constants.EXPERIMENTAL_PREFIX not in result._test_name and
1000 constants.EXPERIMENTAL_PREFIX not in result._job_name)
1001
Alex Millerfcc119b2014-01-15 13:54:58 -08001002 return (self._file_bugs and result.test_executed and
beepsbeefc062013-08-02 11:17:09 -07001003 (is_not_experimental or self._file_experimental_bugs) and
Fang Dengd82c1c72014-07-29 10:43:01 -07001004 not result.is_testna() and
beeps32fa6772014-01-28 13:19:53 -08001005 result.is_worse_than(job_status.Status('GOOD', '', 'reason')))
beepsda5b7112013-05-30 11:34:14 -07001006
1007
Allen Licc752292017-01-03 12:44:39 -08001008 def _has_retry(self, result):
1009 """
1010 Return True if this result gets to retry.
1011
1012 @param result: A result, encapsulating the status of the failed job.
1013 @return: bool
1014 """
1015 return (self._job_retry
1016 and self._retry_handler.has_following_retry(result))
1017
1018
Allen Li18503452016-12-29 14:56:48 -08001019 def wait(self, record, bug_template=None):
Alex Miller3a69adc2012-12-19 13:38:31 -08001020 """
1021 Polls for the job statuses, using |record| to print status when each
1022 completes.
1023
1024 @param record: callable that records job status.
1025 prototype:
1026 record(base_job.status_log_entry)
beepsc8a875b2013-03-25 10:20:38 -07001027 @param bug_template: A template dictionary specifying the default bug
1028 filing options for failures in this suite.
Alex Miller3a69adc2012-12-19 13:38:31 -08001029 """
Dan Shie67bd6a2016-02-17 14:44:07 -08001030 # reporting modules have dependency on external packages, e.g., httplib2
1031 # Such dependency can cause issue to any module tries to import suite.py
1032 # without building site-packages first. Since the reporting modules are
1033 # only used in this function, move the imports here avoid the
1034 # requirement of building site packages to use other functions in this
1035 # module.
1036 from autotest_lib.server.cros.dynamic_suite import reporting
Dan Shie67bd6a2016-02-17 14:44:07 -08001037
Allen Li18503452016-12-29 14:56:48 -08001038 if bug_template is None:
1039 bug_template = {}
1040
Alex Millera3a4fe72013-01-22 09:57:47 -08001041 if self._file_bugs:
1042 bug_reporter = reporting.Reporter()
Allen Li733dab92016-12-29 15:07:50 -08001043 else:
1044 bug_reporter = reporting.NullReporter()
Alex Miller3a69adc2012-12-19 13:38:31 -08001045 try:
Aviv Keshet133beb12013-08-20 14:37:13 -07001046 if self._suite_job_id:
1047 results_generator = job_status.wait_for_child_results(
1048 self._afe, self._tko, self._suite_job_id)
1049 else:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -07001050 logging.warning('Unknown suite_job_id, falling back to less '
Dan Shi08ff1282016-02-18 19:51:16 -08001051 'efficient results_generator.')
Aviv Keshet133beb12013-08-20 14:37:13 -07001052 results_generator = job_status.wait_for_results(self._afe,
1053 self._tko,
1054 self._jobs)
1055 for result in results_generator:
Allen Li26b340d2016-12-29 15:23:01 -08001056 self._record_result(
1057 result=result,
1058 record=record,
1059 results_generator=results_generator,
1060 bug_reporter=bug_reporter,
1061 bug_template=bug_template)
beeps8ead53c2013-04-26 19:12:46 -07001062
Alex Miller3a69adc2012-12-19 13:38:31 -08001063 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001064 logging.exception('Exception waiting for results')
Alex Miller3a69adc2012-12-19 13:38:31 -08001065 Status('FAIL', self._tag,
1066 'Exception waiting for results').record_result(record)
1067
1068
Allen Li26b340d2016-12-29 15:23:01 -08001069 def _record_result(self, result, record, results_generator, bug_reporter,
1070 bug_template):
1071 """
1072 Record a single test job result.
1073
1074 @param result: Status instance for job.
1075 @param record: callable that records job status.
1076 prototype:
1077 record(base_job.status_log_entry)
1078 @param results_generator: Results generator for sending job retries.
1079 @param bug_reporter: Reporter instance for reporting bugs.
1080 @param bug_template: A template dictionary specifying the default bug
1081 filing options for failures in this suite.
1082 """
Allen Li26b340d2016-12-29 15:23:01 -08001083 result.record_all(record)
Allen Li4df053e2016-12-29 16:05:41 -08001084 self._remember_job_keyval(result)
Allen Li26b340d2016-12-29 15:23:01 -08001085
Allen Licc752292017-01-03 12:44:39 -08001086 if self._has_retry(result):
Allen Li26b340d2016-12-29 15:23:01 -08001087 new_job = self._schedule_test(
1088 record=record, test=self._jobs_to_tests[result.id],
1089 retry_for=result.id, ignore_errors=True)
1090 if new_job:
1091 results_generator.send([new_job])
1092
1093 # TODO (fdeng): If the suite times out before a retry could
1094 # finish, we would lose the chance to file a bug for the
1095 # original job.
1096 if self._should_report(result):
Allen Li11308982016-12-29 16:19:55 -08001097 if self._should_file_bugs:
Allen Li47c9fca2016-12-29 16:22:53 -08001098 self._file_bug(result, bug_reporter, bug_template)
Allen Li26b340d2016-12-29 15:23:01 -08001099 else:
Allen Lid5df44b2016-12-29 15:59:06 -08001100 # reporting modules have dependency on external
1101 # packages, e.g., httplib2 Such dependency can cause
1102 # issue to any module tries to import suite.py without
1103 # building site-packages first. Since the reporting
1104 # modules are only used in this function, move the
1105 # imports here avoid the requirement of building site
1106 # packages to use other functions in this module.
1107 from autotest_lib.server.cros.dynamic_suite import reporting
1108
Allen Li7b973112016-12-29 16:17:41 -08001109 reporting.send_email(
1110 self._get_test_bug(result),
1111 self._get_bug_template(result, bug_template))
Allen Li26b340d2016-12-29 15:23:01 -08001112
1113
Allen Lid5df44b2016-12-29 15:59:06 -08001114 def _get_bug_template(self, result, bug_template):
1115 """Get BugTemplate for test job.
1116
1117 @param result: Status instance for job.
1118 @param bug_template: A template dictionary specifying the default bug
1119 filing options for failures in this suite.
1120 @returns: BugTemplate instance
1121 """
1122 # reporting modules have dependency on external packages, e.g., httplib2
1123 # Such dependency can cause issue to any module tries to import suite.py
1124 # without building site-packages first. Since the reporting modules are
1125 # only used in this function, move the imports here avoid the
1126 # requirement of building site packages to use other functions in this
1127 # module.
1128 from autotest_lib.server.cros.dynamic_suite import reporting_utils
1129
1130 # Try to merge with bug template in test control file.
1131 template = reporting_utils.BugTemplate(bug_template)
1132 try:
1133 test_data = self._jobs_to_tests[result.id]
1134 return template.finalize_bug_template(
1135 test_data.bug_template)
1136 except AttributeError:
1137 # Test control file does not have bug template defined.
1138 return template.bug_template
1139 except reporting_utils.InvalidBugTemplateException as e:
1140 logging.error('Merging bug templates failed with '
1141 'error: %s An empty bug template will '
1142 'be used.', e)
1143 return {}
1144
1145
Allen Li003913e2016-12-29 15:53:34 -08001146 def _get_test_bug(self, result):
1147 """Get TestBug for the given result.
1148
1149 @param result: Status instance for a test job.
1150 @returns: TestBug instance.
1151 """
1152 # reporting modules have dependency on external packages, e.g., httplib2
1153 # Such dependency can cause issue to any module tries to import suite.py
1154 # without building site-packages first. Since the reporting modules are
1155 # only used in this function, move the imports here avoid the
1156 # requirement of building site packages to use other functions in this
1157 # module.
1158 from autotest_lib.server.cros.dynamic_suite import reporting
1159
1160 job_views = self._tko.run('get_detailed_test_views',
1161 afe_job_id=result.id)
1162 return reporting.TestBug(self._cros_build,
1163 site_utils.get_chrome_version(job_views),
1164 self._tag,
1165 result)
1166
1167
Allen Li11308982016-12-29 16:19:55 -08001168 @property
1169 def _should_file_bugs(self):
1170 """Return whether bugs should be filed.
1171
1172 @returns: bool
1173 """
1174 # File bug when failure is one of the _FILE_BUG_SUITES,
1175 # otherwise send an email to the owner anc cc.
1176 return self._tag in _FILE_BUG_SUITES
1177
1178
Allen Li47c9fca2016-12-29 16:22:53 -08001179 def _file_bug(self, result, bug_reporter, bug_template):
1180 """File a bug for a test job result.
1181
1182 @param result: Status instance for job.
1183 @param bug_reporter: Reporter instance for reporting bugs.
1184 @param bug_template: A template dictionary specifying the default bug
1185 filing options for failures in this suite.
1186 """
1187 bug_id, bug_count = bug_reporter.report(
1188 self._get_test_bug(result),
1189 self._get_bug_template(result, bug_template))
1190
1191 # We use keyvals to communicate bugs filed with run_suite.
1192 if bug_id is not None:
1193 bug_keyvals = tools.create_bug_keyvals(
1194 result.id, result.test_name,
1195 (bug_id, bug_count))
1196 try:
1197 utils.write_keyval(self._results_dir,
1198 bug_keyvals)
1199 except ValueError:
1200 logging.error('Unable to log bug keyval for:%s',
1201 result.test_name)
1202
1203
Alex Miller3a69adc2012-12-19 13:38:31 -08001204 def abort(self):
1205 """
1206 Abort all scheduled test jobs.
1207 """
1208 if self._jobs:
1209 job_ids = [job.id for job in self._jobs]
1210 self._afe.run('abort_host_queue_entries', job__id__in=job_ids)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001211
1212
Allen Li4df053e2016-12-29 16:05:41 -08001213 def _remember_job_keyval(self, job):
Chris Masoned9f13c52012-08-29 10:37:08 -07001214 """
1215 Record provided job as a suite job keyval, for later referencing.
1216
Allen Li4df053e2016-12-29 16:05:41 -08001217 @param job: some representation of a job that has the attributes:
1218 id, test_name, and owner
Chris Masoned9f13c52012-08-29 10:37:08 -07001219 """
Allen Li3cc73cd2016-12-12 16:02:21 -08001220 if self._results_dir and job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -07001221 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -07001222 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -07001223 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001224 utils.write_keyval(
1225 self._results_dir,
1226 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
1227
Dan Shid1521802013-05-24 13:08:37 -07001228
Chris Masone44e4d6c2012-08-15 14:25:53 -07001229 @staticmethod
Allen Lie61acfe2016-12-29 16:27:21 -08001230 def _find_all_tests(cf_getter, suite_name='', add_experimental=False,
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001231 forgiving_parser=True, run_prod_code=False,
1232 test_args=None):
Chris Masone44e4d6c2012-08-15 14:25:53 -07001233 """
Dan Shi5783f8a2014-12-22 14:34:45 -08001234 Function to scan through all tests and find all tests.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001235
xixuan0f7755d2016-04-18 14:49:12 -07001236 When this method is called with a file system ControlFileGetter, or
1237 enable_controls_in_batch is set as false, this function will looks at
Allen Lid69b9f02016-12-09 18:15:59 -08001238 control files returned by cf_getter.get_control_file_list() for tests.
xixuan0f7755d2016-04-18 14:49:12 -07001239
1240 If cf_getter is a File system ControlFileGetter, it performs a full
1241 parse of the root directory associated with the getter. This is the
1242 case when it's invoked from suite_preprocessor.
1243
1244 If cf_getter is a devserver getter it looks up the suite_name in a
1245 suite to control file map generated at build time, and parses the
1246 relevant control files alone. This lookup happens on the devserver,
1247 so as far as this method is concerned, both cases are equivalent. If
1248 enable_controls_in_batch is switched on, this function will call
1249 cf_getter.get_suite_info() to get a dict of control files and contents
1250 in batch.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001251
1252 @param cf_getter: a control_file_getter.ControlFileGetter used to list
1253 and fetch the content of control files
Dan Shi5783f8a2014-12-22 14:34:45 -08001254 @param suite_name: If specified, this method will attempt to restrain
1255 the search space to just this suite's control files.
1256 @param add_experimental: add tests with experimental attribute set.
1257 @param forgiving_parser: If False, will raise ControlVariableExceptions
1258 if any are encountered when parsing control
1259 files. Note that this can raise an exception
1260 for syntax errors in unrelated files, because
1261 we parse them before applying the predicate.
Simran Basi5ace6f22016-01-06 17:30:44 -08001262 @param run_prod_code: If true, the suite will run the test code that
1263 lives in prod aka the test code currently on the
1264 lab servers by disabling SSP for the discovered
1265 tests.
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001266 @param test_args: A dict of args to be seeded in test control file under
1267 the name |args_dict|.
Dan Shi5783f8a2014-12-22 14:34:45 -08001268
1269 @raises ControlVariableException: If forgiving_parser is False and there
1270 is a syntax error in a control file.
1271
1272 @returns a dictionary of ControlData objects that based on given
1273 parameters.
1274 """
1275 logging.debug('Getting control file list for suite: %s', suite_name)
1276 tests = {}
xixuan0f7755d2016-04-18 14:49:12 -07001277 use_batch = (ENABLE_CONTROLS_IN_BATCH and hasattr(
1278 cf_getter, '_dev_server'))
1279 if use_batch:
1280 suite_info = cf_getter.get_suite_info(suite_name=suite_name)
1281 files = suite_info.keys()
1282 else:
1283 files = cf_getter.get_control_file_list(suite_name=suite_name)
1284
Dan Shi5783f8a2014-12-22 14:34:45 -08001285
1286 logging.debug('Parsing control files ...')
1287 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
1288 for file in filter(lambda f: not matcher.match(f), files):
xixuan0f7755d2016-04-18 14:49:12 -07001289 if use_batch:
1290 text = suite_info[file]
1291 else:
1292 text = cf_getter.get_control_file_contents(file)
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001293 # Seed test_args into the control file.
1294 if test_args:
1295 text = tools.inject_vars(test_args, text)
Dan Shi5783f8a2014-12-22 14:34:45 -08001296 try:
1297 found_test = control_data.parse_control_string(
Christopher Wiley10439d82016-03-07 12:45:26 -08001298 text, raise_warnings=True, path=file)
Dan Shi5783f8a2014-12-22 14:34:45 -08001299 if not add_experimental and found_test.experimental:
1300 continue
1301 found_test.text = text
Simran Basi5ace6f22016-01-06 17:30:44 -08001302 if run_prod_code:
1303 found_test.require_ssp = False
Dan Shi5783f8a2014-12-22 14:34:45 -08001304 tests[file] = found_test
1305 except control_data.ControlVariableException, e:
1306 if not forgiving_parser:
1307 msg = "Failed parsing %s\n%s" % (file, e)
1308 raise control_data.ControlVariableException(msg)
1309 logging.warning("Skipping %s\n%s", file, e)
1310 except Exception, e:
1311 logging.error("Bad %s\n%s", file, e)
1312 return tests
1313
1314
Allen Lie61acfe2016-12-29 16:27:21 -08001315 @classmethod
1316 def find_and_parse_tests(cls, cf_getter, predicate, suite_name='',
Simran Basi5ace6f22016-01-06 17:30:44 -08001317 add_experimental=False, forgiving_parser=True,
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001318 run_prod_code=False, test_args=None):
Dan Shi5783f8a2014-12-22 14:34:45 -08001319 """
1320 Function to scan through all tests and find eligible tests.
1321
1322 Search through all tests based on given cf_getter, suite_name,
1323 add_experimental and forgiving_parser, return the tests that match
1324 given predicate.
1325
1326 @param cf_getter: a control_file_getter.ControlFileGetter used to list
1327 and fetch the content of control files
Chris Masone44e4d6c2012-08-15 14:25:53 -07001328 @param predicate: a function that should return True when run over a
1329 ControlData representation of a control file that should be in
1330 this Suite.
beepsc594c1c2013-07-09 22:33:18 -07001331 @param suite_name: If specified, this method will attempt to restrain
1332 the search space to just this suite's control files.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001333 @param add_experimental: add tests with experimental attribute set.
beeps89f1e062013-09-18 12:00:17 -07001334 @param forgiving_parser: If False, will raise ControlVariableExceptions
1335 if any are encountered when parsing control
1336 files. Note that this can raise an exception
1337 for syntax errors in unrelated files, because
1338 we parse them before applying the predicate.
Simran Basi5ace6f22016-01-06 17:30:44 -08001339 @param run_prod_code: If true, the suite will run the test code that
1340 lives in prod aka the test code currently on the
1341 lab servers by disabling SSP for the discovered
1342 tests.
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001343 @param test_args: A dict of args to be seeded in test control file.
beeps89f1e062013-09-18 12:00:17 -07001344
1345 @raises ControlVariableException: If forgiving_parser is False and there
1346 is a syntax error in a control file.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001347
1348 @return list of ControlData objects that should be run, with control
Dan Shief5b53f2013-01-22 10:22:01 -08001349 file text added in |text| attribute. Results are sorted based
1350 on the TIME setting in control file, slowest test comes first.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001351 """
Allen Lie61acfe2016-12-29 16:27:21 -08001352 tests = cls._find_all_tests(cf_getter, suite_name, add_experimental,
1353 forgiving_parser,
Shuqian Zhaoed0da862017-03-06 14:47:13 -08001354 run_prod_code=run_prod_code,
1355 test_args=test_args)
Dan Shi5783f8a2014-12-22 14:34:45 -08001356 logging.debug('Parsed %s control files.', len(tests))
Dan Shief5b53f2013-01-22 10:22:01 -08001357 tests = [test for test in tests.itervalues() if predicate(test)]
1358 tests.sort(key=lambda t:
1359 control_data.ControlData.get_test_time_index(t.time),
1360 reverse=True)
1361 return tests
Dan Shi5783f8a2014-12-22 14:34:45 -08001362
1363
Allen Lie61acfe2016-12-29 16:27:21 -08001364 @classmethod
1365 def find_possible_tests(cls, cf_getter, predicate, suite_name='', count=10):
Dan Shi5783f8a2014-12-22 14:34:45 -08001366 """
1367 Function to scan through all tests and find possible tests.
1368
1369 Search through all tests based on given cf_getter, suite_name,
1370 add_experimental and forgiving_parser. Use the given predicate to
1371 calculate the similarity and return the top 10 matches.
1372
1373 @param cf_getter: a control_file_getter.ControlFileGetter used to list
1374 and fetch the content of control files
1375 @param predicate: a function that should return a tuple of (name, ratio)
1376 when run over a ControlData representation of a control file that
1377 should be in this Suite. `name` is the key to be compared, e.g.,
1378 a suite name or test name. `ratio` is a value between [0,1]
1379 indicating the similarity of `name` and the value to be compared.
1380 @param suite_name: If specified, this method will attempt to restrain
1381 the search space to just this suite's control files.
1382 @param count: Number of suggestions to return, default to 10.
1383
1384 @return list of top names that similar to the given test, sorted by
1385 match ratio.
1386 """
Allen Lie61acfe2016-12-29 16:27:21 -08001387 tests = cls._find_all_tests(cf_getter, suite_name,
1388 add_experimental=True,
1389 forgiving_parser=True)
Dan Shi5783f8a2014-12-22 14:34:45 -08001390 logging.debug('Parsed %s control files.', len(tests))
1391 similarities = {}
1392 for test in tests.itervalues():
1393 ratios = predicate(test)
1394 # Some predicates may return a list of tuples, e.g.,
1395 # name_in_tag_similarity_predicate. Convert all returns to a list.
1396 if not isinstance(ratios, list):
1397 ratios = [ratios]
1398 for name, ratio in ratios:
1399 similarities[name] = ratio
1400 return [s[0] for s in
1401 sorted(similarities.items(), key=operator.itemgetter(1),
1402 reverse=True)][:count]
Allen Li9fcd4b42016-12-12 16:15:14 -08001403
1404
1405def _is_nonexistent_board_error(e):
1406 """Return True if error is caused by nonexistent board label.
1407
1408 As of this writing, the particular case we want looks like this:
1409
1410 1) e.problem_keys is a dictionary
1411 2) e.problem_keys['meta_hosts'] exists as the only key
1412 in the dictionary.
1413 3) e.problem_keys['meta_hosts'] matches this pattern:
1414 "Label "board:.*" not found"
1415
1416 We check for conditions 1) and 2) on the
1417 theory that they're relatively immutable.
1418 We don't check condition 3) because it seems
1419 likely to be a maintenance burden, and for the
1420 times when we're wrong, being right shouldn't
1421 matter enough (we _hope_).
1422
1423 @param e: proxy.ValidationError instance
1424 @returns: boolean
1425 """
1426 return (isinstance(e.problem_keys, dict)
1427 and len(e.problem_keys) == 1
1428 and 'meta_hosts' in e.problem_keys)