blob: 5504b644f7c111dbbcfac1e1a556ab49734f77b6 [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
Allen Li98a26a42017-02-28 18:43:24 -08007import functools
Fang Deng443f1952015-01-02 14:51:49 -08008import hashlib
9import logging
10import operator
11import os
12import re
Fang Deng443f1952015-01-02 14:51:49 -080013import sys
Allen Li98a26a42017-02-28 18:43:24 -080014import warnings
Chris Masone44e4d6c2012-08-15 14:25:53 -070015
16import common
17
J. Richard Barnetteb592fbc2014-04-02 10:27:33 -070018from autotest_lib.frontend.afe.json_rpc import proxy
Alex Miller3a69adc2012-12-19 13:38:31 -080019from autotest_lib.client.common_lib import control_data
Fang Denge3bc24b2014-03-17 15:19:46 -070020from autotest_lib.client.common_lib import enum
Dan Shidfea3682014-08-10 23:38:40 -070021from autotest_lib.client.common_lib import error
Simran Basi5ace6f22016-01-06 17:30:44 -080022from autotest_lib.client.common_lib import global_config
Alex Miller7d658cf2013-09-04 16:00:35 -070023from autotest_lib.client.common_lib import priorities
Dan Shidfea3682014-08-10 23:38:40 -070024from autotest_lib.client.common_lib import time_utils
25from autotest_lib.client.common_lib import utils
Fang Denge3bc24b2014-03-17 15:19:46 -070026from autotest_lib.frontend.afe.json_rpc import proxy
Dan Shi36cfd832014-10-10 13:38:51 -070027from autotest_lib.server.cros import provision
Chris Masone44e4d6c2012-08-15 14:25:53 -070028from autotest_lib.server.cros.dynamic_suite import constants
29from autotest_lib.server.cros.dynamic_suite import control_file_getter
30from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Alex Miller3a69adc2012-12-19 13:38:31 -080031from autotest_lib.server.cros.dynamic_suite import job_status
J. Richard Barnettee7b98bb2013-08-21 16:34:16 -070032from autotest_lib.server.cros.dynamic_suite import tools
33from autotest_lib.server.cros.dynamic_suite.job_status import Status
Chris Masone44e4d6c2012-08-15 14:25:53 -070034
Shuqian Zhaoab468812015-04-08 14:40:38 -070035try:
36 from chromite.lib import boolparse_lib
37 from chromite.lib import cros_logging as logging
38except ImportError:
39 print 'Unable to import chromite.'
40 print 'This script must be either:'
41 print ' - Be run in the chroot.'
42 print ' - (not yet supported) be run after running '
43 print ' ../utils/build_externals.py'
Fang Denge3bc24b2014-03-17 15:19:46 -070044
Shuqian Zhao490f78f2016-01-20 13:18:40 -080045_FILE_BUG_SUITES = ['au', 'bvt', 'bvt-cq', 'bvt-inline', 'paygen_au_beta',
46 'paygen_au_canary', 'paygen_au_dev', 'paygen_au_stable',
47 'sanity', 'push_to_prod']
Simran Basi5ace6f22016-01-06 17:30:44 -080048_AUTOTEST_DIR = global_config.global_config.get_config_value(
49 'SCHEDULER', 'drone_installation_directory')
xixuan0f7755d2016-04-18 14:49:12 -070050ENABLE_CONTROLS_IN_BATCH = global_config.global_config.get_config_value(
51 'CROS', 'enable_getting_controls_in_batch', type=bool, default=False)
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -070052
Fang Denge3bc24b2014-03-17 15:19:46 -070053class RetryHandler(object):
54 """Maintain retry information.
55
56 @var _retry_map: A dictionary that stores retry history.
57 The key is afe job id. The value is a dictionary.
58 {job_id: {'state':RetryHandler.States, 'retry_max':int}}
59 - state:
60 The retry state of a job.
61 NOT_ATTEMPTED:
62 We haven't done anything about the job.
63 ATTEMPTED:
64 We've made an attempt to schedule a retry job. The
65 scheduling may or may not be successful, e.g.
66 it might encounter an rpc error. Note failure
67 in scheduling a retry is different from a retry job failure.
68 For each job, we only attempt to schedule a retry once.
69 For example, assume we have a test with JOB_RETRIES=5 and
70 its second retry job failed. When we attempt to create
71 a third retry job to retry the second, we hit an rpc
72 error. In such case, we will give up on all following
73 retries.
74 RETRIED:
75 A retry job has already been successfully
76 scheduled.
77 - retry_max:
78 The maximum of times the job can still
79 be retried, taking into account retries
80 that have occurred.
81 @var _retry_level: A retry might be triggered only if the result
82 is worse than the level.
Fang Deng443f1952015-01-02 14:51:49 -080083 @var _max_retries: Maximum retry limit at suite level.
84 Regardless how many times each individual test
85 has been retried, the total number of retries happening in
86 the suite can't exceed _max_retries.
Fang Denge3bc24b2014-03-17 15:19:46 -070087 """
88
89 States = enum.Enum('NOT_ATTEMPTED', 'ATTEMPTED', 'RETRIED',
90 start_value=1, step=1)
91
Fang Deng443f1952015-01-02 14:51:49 -080092 def __init__(self, initial_jobs_to_tests, retry_level='WARN',
93 max_retries=None):
Fang Denge3bc24b2014-03-17 15:19:46 -070094 """Initialize RetryHandler.
95
96 @param initial_jobs_to_tests: A dictionary that maps a job id to
97 a ControlData object. This dictionary should contain
98 jobs that are originally scheduled by the suite.
99 @param retry_level: A retry might be triggered only if the result is
100 worse than the level.
Fang Deng443f1952015-01-02 14:51:49 -0800101 @param max_retries: Integer, maxmium total retries allowed
102 for the suite. Default to None, no max.
Fang Denge3bc24b2014-03-17 15:19:46 -0700103 """
104 self._retry_map = {}
105 self._retry_level = retry_level
Fang Deng443f1952015-01-02 14:51:49 -0800106 self._max_retries = (max_retries
107 if max_retries is not None else sys.maxint)
Fang Denge3bc24b2014-03-17 15:19:46 -0700108 for job_id, test in initial_jobs_to_tests.items():
109 if test.job_retries > 0:
Allen Lifb89e2b2017-01-03 12:47:58 -0800110 self._add_job(new_job_id=job_id,
111 retry_max=test.job_retries)
Fang Denge3bc24b2014-03-17 15:19:46 -0700112
113
Allen Lifb89e2b2017-01-03 12:47:58 -0800114 def _add_job(self, new_job_id, retry_max):
Fang Denge3bc24b2014-03-17 15:19:46 -0700115 """Add a newly-created job to the retry map.
116
117 @param new_job_id: The afe_job_id of a newly created job.
118 @param retry_max: The maximum of times that we could retry
119 the test if the job fails.
120
121 @raises ValueError if new_job_id is already in retry map.
122
123 """
124 if new_job_id in self._retry_map:
125 raise ValueError('add_job called when job is already in retry map.')
126
127 self._retry_map[new_job_id] = {
128 'state': self.States.NOT_ATTEMPTED,
129 'retry_max': retry_max}
130
131
Allen Li0cd19262017-01-03 12:56:08 -0800132 def _suite_max_reached(self):
Fang Deng443f1952015-01-02 14:51:49 -0800133 """Return whether maximum retry limit for a suite has been reached."""
Fang Denge4326d62015-01-06 13:15:15 -0800134 return self._max_retries <= 0
Fang Deng443f1952015-01-02 14:51:49 -0800135
136
Fang Denge3bc24b2014-03-17 15:19:46 -0700137 def add_retry(self, old_job_id, new_job_id):
138 """Record a retry.
139
140 Update retry map with the retry information.
141
142 @param old_job_id: The afe_job_id of the job that is retried.
143 @param new_job_id: The afe_job_id of the retry job.
144
145 @raises KeyError if old_job_id isn't in the retry map.
146 @raises ValueError if we have already retried or made an attempt
147 to retry the old job.
148
149 """
150 old_record = self._retry_map[old_job_id]
151 if old_record['state'] != self.States.NOT_ATTEMPTED:
152 raise ValueError(
153 'We have already retried or attempted to retry job %d' %
154 old_job_id)
155 old_record['state'] = self.States.RETRIED
Allen Lifb89e2b2017-01-03 12:47:58 -0800156 self._add_job(new_job_id=new_job_id,
157 retry_max=old_record['retry_max'] - 1)
Fang Deng443f1952015-01-02 14:51:49 -0800158 self._max_retries -= 1
Fang Denge3bc24b2014-03-17 15:19:46 -0700159
160
161 def set_attempted(self, job_id):
162 """Set the state of the job to ATTEMPTED.
163
164 @param job_id: afe_job_id of a job.
165
166 @raises KeyError if job_id isn't in the retry map.
167 @raises ValueError if the current state is not NOT_ATTEMPTED.
168
169 """
170 current_state = self._retry_map[job_id]['state']
171 if current_state != self.States.NOT_ATTEMPTED:
172 # We are supposed to retry or attempt to retry each job
173 # only once. Raise an error if this is not the case.
174 raise ValueError('Unexpected state transition: %s -> %s' %
175 (self.States.get_string(current_state),
176 self.States.get_string(self.States.ATTEMPTED)))
177 else:
178 self._retry_map[job_id]['state'] = self.States.ATTEMPTED
179
180
181 def has_following_retry(self, result):
182 """Check whether there will be a following retry.
183
184 We have the following cases for a given job id (result.id),
185 - no retry map entry -> retry not required, no following retry
186 - has retry map entry:
187 - already retried -> has following retry
188 - has not retried
189 (this branch can be handled by checking should_retry(result))
190 - retry_max == 0 --> the last retry job, no more retry
191 - retry_max > 0
192 - attempted, but has failed in scheduling a
193 following retry due to rpc error --> no more retry
194 - has not attempped --> has following retry if test failed.
195
196 @param result: A result, encapsulating the status of the job.
197
198 @returns: True, if there will be a following retry.
199 False otherwise.
200
201 """
Allen Li2ee2a262017-01-03 13:21:10 -0800202 return (result.test_executed
203 and result.id in self._retry_map
204 and (self._retry_map[result.id]['state'] == self.States.RETRIED
205 or self._should_retry(result)))
Allen Li5cb00652017-01-03 13:06:30 -0800206
207
208 def _should_retry(self, result):
209 """Check whether we should retry a job based on its result.
210
211 We will retry the job that corresponds to the result
212 when all of the following are true.
213 a) The test was actually executed, meaning that if
214 a job was aborted before it could ever reach the state
215 of 'Running', the job will not be retried.
216 b) The result is worse than |self._retry_level| which
217 defaults to 'WARN'.
218 c) The test requires retry, i.e. the job has an entry in the retry map.
219 d) We haven't made any retry attempt yet, i.e. state == NOT_ATTEMPTED
220 Note that if a test has JOB_RETRIES=5, and the second time
221 it was retried it hit an rpc error, we will give up on
222 all following retries.
223 e) The job has not reached its retry max, i.e. retry_max > 0
224
225 @param result: A result, encapsulating the status of the job.
226
227 @returns: True if we should retry the job.
228
229 """
230 return (
xixuanbf854f82017-04-20 10:40:15 -0700231 result.test_executed
232 and result.id in self._retry_map
233 and not self._suite_max_reached()
Allen Li5cb00652017-01-03 13:06:30 -0800234 and result.is_worse_than(
235 job_status.Status(self._retry_level, '', 'reason'))
Allen Li5cb00652017-01-03 13:06:30 -0800236 and self._retry_map[result.id]['state'] == self.States.NOT_ATTEMPTED
237 and self._retry_map[result.id]['retry_max'] > 0
238 )
Fang Denge3bc24b2014-03-17 15:19:46 -0700239
240
241 def get_retry_max(self, job_id):
242 """Get the maximum times the job can still be retried.
243
244 @param job_id: afe_job_id of a job.
245
246 @returns: An int, representing the maximum times the job can still be
247 retried.
248 @raises KeyError if job_id isn't in the retry map.
249
250 """
251 return self._retry_map[job_id]['retry_max']
252
253
Allen Li86f8c282017-02-28 13:09:40 -0800254class _ExperimentalTestFilter(object):
255 """Filter experimental tests."""
Allen Li6b161c62017-02-28 13:08:54 -0800256
257
258 def __init__(self, tests, add_experimental=True):
259 """Initialize instance.
260
261 @param tests: iterable of tests (ControlData objects)
262 @param add_experimental: schedule experimental tests as well, or not.
263 """
264 self._tests = list(tests)
265 self._add_experimental = add_experimental
266
267
Allen Li86f8c282017-02-28 13:09:40 -0800268 def get_tests_to_schedule(self):
Allen Li6b161c62017-02-28 13:08:54 -0800269 """Return a list of tests to be scheduled for this suite.
270
271 @returns: list of tests (ControlData objects)
272 """
273 tests = self.stable_tests
274 if self._add_experimental:
275 for test in self.unstable_tests:
276 if not test.name.startswith(constants.EXPERIMENTAL_PREFIX):
277 test.name = constants.EXPERIMENTAL_PREFIX + test.name
278 tests.append(test)
279 return tests
280
281
282 @property
283 def stable_tests(self):
284 """Non-experimental tests.
285
286 @returns: list
287 """
288 return filter(lambda t: not t.experimental, self._tests)
289
290
291 @property
292 def unstable_tests(self):
293 """Experimental tests.
294
295 @returns: list
296 """
297 return filter(lambda t: t.experimental, self._tests)
298
299
Allen Lida198fd2017-03-29 17:22:13 -0700300class _SuiteChildJobCreator(object):
301 """Create test jobs for a suite."""
302
Allen Li010c0412017-03-29 17:31:35 -0700303 def __init__(
304 self,
Allen Li55de3402017-03-29 17:48:46 -0700305 tag,
Allen Li27f72a22017-03-29 17:37:43 -0700306 builds,
Allen Li010c0412017-03-29 17:31:35 -0700307 board,
Allen Li388b7a12017-03-29 17:58:23 -0700308 afe=None,
Allen Li388b7a12017-03-29 17:58:23 -0700309 max_runtime_mins=24*60,
310 timeout_mins=24*60,
Allen Li55de3402017-03-29 17:48:46 -0700311 suite_job_id=None,
Allen Li010c0412017-03-29 17:31:35 -0700312 ignore_deps=False,
Allen Li37e1a292017-02-28 18:28:41 -0800313 extra_deps=(),
Allen Li388b7a12017-03-29 17:58:23 -0700314 priority=priorities.Priority.DEFAULT,
Allen Li55de3402017-03-29 17:48:46 -0700315 offload_failures_only=False,
316 test_source_build=None):
Allen Li010c0412017-03-29 17:31:35 -0700317 """
318 Constructor
319
Allen Li55de3402017-03-29 17:48:46 -0700320 @param tag: a string with which to tag jobs run in this suite.
Allen Li27f72a22017-03-29 17:37:43 -0700321 @param builds: the builds on which we're running this suite.
Allen Li010c0412017-03-29 17:31:35 -0700322 @param board: the board on which we're running this suite.
Allen Li388b7a12017-03-29 17:58:23 -0700323 @param afe: an instance of AFE as defined in server/frontend.py.
Allen Li388b7a12017-03-29 17:58:23 -0700324 @param max_runtime_mins: Maximum suite runtime, in minutes.
325 @param timeout_mins: Maximum job lifetime, in minutes.
Allen Li55de3402017-03-29 17:48:46 -0700326 @param suite_job_id: Job id that will act as parent id to all sub jobs.
327 Default: None
Allen Li010c0412017-03-29 17:31:35 -0700328 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
329 attribute and skip applying of dependency labels.
330 (Default:False)
331 @param extra_deps: A list of strings which are the extra DEPENDENCIES
332 to add to each test being scheduled.
Allen Li388b7a12017-03-29 17:58:23 -0700333 @param priority: Integer priority level. Higher is more important.
Allen Li55de3402017-03-29 17:48:46 -0700334 @param offload_failures_only: Only enable gs_offloading for failed
335 jobs.
336 @param test_source_build: Build that contains the server-side test code.
Allen Li010c0412017-03-29 17:31:35 -0700337 """
Allen Li55de3402017-03-29 17:48:46 -0700338 self._tag = tag
Allen Li27f72a22017-03-29 17:37:43 -0700339 self._builds = builds
Allen Li010c0412017-03-29 17:31:35 -0700340 self._board = board
Allen Li388b7a12017-03-29 17:58:23 -0700341 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
342 delay_sec=10,
343 debug=False)
Allen Li388b7a12017-03-29 17:58:23 -0700344 self._max_runtime_mins = max_runtime_mins
345 self._timeout_mins = timeout_mins
Allen Li55de3402017-03-29 17:48:46 -0700346 self._suite_job_id = suite_job_id
Allen Li010c0412017-03-29 17:31:35 -0700347 self._ignore_deps = ignore_deps
Allen Li37e1a292017-02-28 18:28:41 -0800348 self._extra_deps = tuple(extra_deps)
Allen Li388b7a12017-03-29 17:58:23 -0700349 self._priority = priority
Allen Li55de3402017-03-29 17:48:46 -0700350 self._offload_failures_only = offload_failures_only
351 self._test_source_build = test_source_build
Allen Li010c0412017-03-29 17:31:35 -0700352
353
Allen Li27f72a22017-03-29 17:37:43 -0700354 @property
355 def cros_build(self):
356 """Return the CrOS build or the first build in the builds dict."""
357 # TODO(ayatane): Note that the builds dict isn't ordered. I'm not
358 # sure what the implications of this are, but it's probably not a
359 # good thing.
360 return self._builds.get(provision.CROS_VERSION_PREFIX,
361 self._builds.values()[0])
362
363
Allen Li388b7a12017-03-29 17:58:23 -0700364 def create_job(self, test, retry_for=None):
365 """
366 Thin wrapper around frontend.AFE.create_job().
367
368 @param test: ControlData object for a test to run.
369 @param retry_for: If the to-be-created job is a retry for an
370 old job, the afe_job_id of the old job will
371 be passed in as |retry_for|, which will be
372 recorded in the new job's keyvals.
373 @returns: A frontend.Job object with an added test_name member.
374 test_name is used to preserve the higher level TEST_NAME
375 name of the job.
376 """
377 test_obj = self._afe.create_job(
378 control_file=test.text,
379 name=tools.create_job_name(
380 self._test_source_build or self.cros_build,
381 self._tag,
382 test.name),
383 control_type=test.test_type.capitalize(),
384 meta_hosts=[self._board]*test.sync_count,
385 dependencies=self._create_job_deps(test),
386 keyvals=self._create_keyvals_for_test_job(test, retry_for),
387 max_runtime_mins=self._max_runtime_mins,
388 timeout_mins=self._timeout_mins,
389 parent_job_id=self._suite_job_id,
390 test_retry=test.retries,
391 priority=self._priority,
392 synch_count=test.sync_count,
393 require_ssp=test.require_ssp)
394
395 test_obj.test_name = test.name
396 return test_obj
397
398
Allen Li010c0412017-03-29 17:31:35 -0700399 def _create_job_deps(self, test):
400 """Create job deps list for a test job.
401
402 @returns: A list of dependency strings.
403 """
404 if self._ignore_deps:
405 job_deps = []
406 else:
407 job_deps = list(test.dependencies)
408 job_deps.extend(self._extra_deps)
Allen Li010c0412017-03-29 17:31:35 -0700409 return job_deps
410
Allen Lida198fd2017-03-29 17:22:13 -0700411
Allen Li55de3402017-03-29 17:48:46 -0700412 def _create_keyvals_for_test_job(self, test, retry_for=None):
413 """Create keyvals dict for creating a test job.
414
415 @param test: ControlData object for a test to run.
416 @param retry_for: If the to-be-created job is a retry for an
417 old job, the afe_job_id of the old job will
418 be passed in as |retry_for|, which will be
419 recorded in the new job's keyvals.
420 @returns: A keyvals dict for creating the test job.
421 """
422 keyvals = {
423 constants.JOB_BUILD_KEY: self.cros_build,
424 constants.JOB_SUITE_KEY: self._tag,
425 constants.JOB_EXPERIMENTAL_KEY: test.experimental,
426 constants.JOB_BUILDS_KEY: self._builds
427 }
428 # test_source_build is saved to job_keyvals so scheduler can retrieve
429 # the build name from database when compiling autoserv commandline.
430 # This avoid a database change to add a new field in afe_jobs.
431 #
432 # Only add `test_source_build` to job keyvals if the build is different
433 # from the CrOS build or the job uses more than one build, e.g., both
434 # firmware and CrOS will be updated in the dut.
435 # This is for backwards compatibility, so the update Autotest code can
436 # compile an autoserv command line to run in a SSP container using
437 # previous builds.
438 if (self._test_source_build and
439 (self.cros_build != self._test_source_build or
440 len(self._builds) > 1)):
441 keyvals[constants.JOB_TEST_SOURCE_BUILD_KEY] = \
442 self._test_source_build
443 for prefix, build in self._builds.iteritems():
444 if prefix == provision.FW_RW_VERSION_PREFIX:
445 keyvals[constants.FWRW_BUILD]= build
446 elif prefix == provision.FW_RO_VERSION_PREFIX:
447 keyvals[constants.FWRO_BUILD] = build
448 # Add suite job id to keyvals so tko parser can read it from keyval
449 # file.
450 if self._suite_job_id:
451 keyvals[constants.PARENT_JOB_ID] = self._suite_job_id
452 # We drop the old job's id in the new job's keyval file so that
453 # later our tko parser can figure out the retry relationship and
454 # invalidate the results of the old job in tko database.
455 if retry_for:
456 keyvals[constants.RETRY_ORIGINAL_JOB_ID] = retry_for
457 if self._offload_failures_only:
458 keyvals[constants.JOB_OFFLOAD_FAILURES_KEY] = True
459 return keyvals
460
461
Allen Liaed93492017-03-14 13:36:26 -0700462def _get_cf_retriever(cf_getter, forgiving_parser=True, run_prod_code=False,
463 test_args=None):
Allen Liec999112017-03-10 16:46:47 -0800464 """Return the correct _ControlFileRetriever instance.
465
466 If cf_getter is a File system ControlFileGetter, return a
467 _ControlFileRetriever. This performs a full parse of the root
468 directory associated with the getter. This is the case when it's
469 invoked from suite_preprocessor.
470
471 If cf_getter is a devserver getter, return a
472 _BatchControlFileRetriever. This looks up the suite_name in a suite
473 to control file map generated at build time, and parses the relevant
474 control files alone. This lookup happens on the devserver, so as far
475 as this method is concerned, both cases are equivalent. If
476 enable_controls_in_batch is switched on, this function will call
477 cf_getter.get_suite_info() to get a dict of control files and
478 contents in batch.
479 """
Allen Li2d7c9562017-03-10 16:33:31 -0800480 if _should_batch_with(cf_getter):
Allen Li48905292017-03-10 17:06:53 -0800481 cls = _BatchControlFileRetriever
Allen Li2d7c9562017-03-10 16:33:31 -0800482 else:
Allen Li48905292017-03-10 17:06:53 -0800483 cls = _ControlFileRetriever
484 return cls(cf_getter, forgiving_parser, run_prod_code, test_args)
Allen Li65258bf2017-03-10 16:20:11 -0800485
486
Allen Lice93df72017-03-10 16:38:30 -0800487def _should_batch_with(cf_getter):
488 """Return whether control files should be fetched in batch.
489
490 This depends on the control file getter and configuration options.
491
492 @param cf_getter: a control_file_getter.ControlFileGetter used to list
493 and fetch the content of control files
494 """
495 return (ENABLE_CONTROLS_IN_BATCH
496 and isinstance(cf_getter, control_file_getter.DevServerGetter))
497
498
Allen Li574fe4d2017-03-10 16:11:53 -0800499class _ControlFileRetriever(object):
500 """Retrieves control files.
501
502 This returns control data instances, unlike control file getters
503 which simply return the control file text contents.
Allen Li066f5872017-02-28 13:30:44 -0800504 """
Allen Li066f5872017-02-28 13:30:44 -0800505
Allen Liaed93492017-03-14 13:36:26 -0700506 def __init__(self, cf_getter, forgiving_parser=True, run_prod_code=False,
507 test_args=None):
Allen Li36746972017-03-10 16:17:46 -0800508 """Initialize instance.
509
510 @param cf_getter: a control_file_getter.ControlFileGetter used to list
511 and fetch the content of control files
Allen Li574fe4d2017-03-10 16:11:53 -0800512 @param forgiving_parser: If False, will raise ControlVariableExceptions
513 if any are encountered when parsing control
514 files. Note that this can raise an exception
515 for syntax errors in unrelated files, because
516 we parse them before applying the predicate.
Allen Liaed93492017-03-14 13:36:26 -0700517 @param run_prod_code: If true, the retrieved tests will run the test
518 code that lives in prod aka the test code
519 currently on the lab servers by disabling
520 SSP for the discovered tests.
Allen Li574fe4d2017-03-10 16:11:53 -0800521 @param test_args: A dict of args to be seeded in test control file under
522 the name |args_dict|.
Allen Liaed93492017-03-14 13:36:26 -0700523 """
524 self._cf_getter = cf_getter
525 self._forgiving_parser = forgiving_parser
526 self._run_prod_code = run_prod_code
527 self._test_args = test_args
528
529
530 def retrieve_for_suite(self, suite_name=''):
531 """Scan through all tests and find all tests.
532
533 @param suite_name: If specified, this method will attempt to restrain
534 the search space to just this suite's control files.
Allen Li066f5872017-02-28 13:30:44 -0800535
Allen Li574fe4d2017-03-10 16:11:53 -0800536 @raises ControlVariableException: If forgiving_parser is False and there
537 is a syntax error in a control file.
538
539 @returns a dictionary of ControlData objects that based on given
540 parameters.
541 """
Allen Li3466ae82017-03-10 16:36:38 -0800542 control_file_texts = self._get_control_file_texts_for_suite(suite_name)
Allen Liaed93492017-03-14 13:36:26 -0700543 return self._parse_control_file_texts(control_file_texts)
Allen Li9d0be122017-02-28 14:13:04 -0800544
545
Allen Li374c1b62017-03-10 16:29:02 -0800546 def _filter_cf_paths(self, paths):
547 """Remove certain control file paths
548
549 @param paths: Iterable of paths
550 @returns: generator yielding paths
551 """
552 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
553 return (path for path in paths if not matcher.match(path))
554
555
Allen Li3466ae82017-03-10 16:36:38 -0800556 def _get_control_file_texts_for_suite(self, suite_name):
557 """Get control file content for given suite.
558
559 @param suite_name: If specified, this method will attempt to restrain
560 the search space to just this suite's control files.
Allen Li975c9522017-03-10 16:40:52 -0800561 @returns: generator yielding (path, text) tuples
Allen Li3466ae82017-03-10 16:36:38 -0800562 """
Allen Li1abded52017-03-10 16:37:57 -0800563 files = self._cf_getter.get_control_file_list(suite_name=suite_name)
564 filtered_files = self._filter_cf_paths(files)
Allen Li975c9522017-03-10 16:40:52 -0800565 for path in filtered_files:
566 yield path, self._cf_getter.get_control_file_contents(path)
Allen Li3466ae82017-03-10 16:36:38 -0800567
568
Allen Liaed93492017-03-14 13:36:26 -0700569 def _parse_control_file_texts(self, control_file_texts):
Allen Li15e95602017-03-10 16:58:49 -0800570 """Parse control file texts.
571
572 @param control_file_texts: iterable of (path, text) pairs
Allen Li15e95602017-03-10 16:58:49 -0800573 @returns: a dictionary of ControlData objects
574 """
575 tests = {}
576 for path, text in control_file_texts:
577 # Seed test_args into the control file.
Allen Liaed93492017-03-14 13:36:26 -0700578 if self._test_args:
579 text = tools.inject_vars(self._test_args, text)
Allen Li15e95602017-03-10 16:58:49 -0800580 try:
581 found_test = control_data.parse_control_string(
582 text, raise_warnings=True, path=path)
583 except control_data.ControlVariableException, e:
Allen Liaed93492017-03-14 13:36:26 -0700584 if not self._forgiving_parser:
Allen Li15e95602017-03-10 16:58:49 -0800585 msg = "Failed parsing %s\n%s" % (path, e)
586 raise control_data.ControlVariableException(msg)
587 logging.warning("Skipping %s\n%s", path, e)
588 except Exception, e:
589 logging.error("Bad %s\n%s", path, e)
590 else:
591 found_test.text = text
Allen Liaed93492017-03-14 13:36:26 -0700592 if self._run_prod_code:
Allen Li15e95602017-03-10 16:58:49 -0800593 found_test.require_ssp = False
594 tests[path] = found_test
595 return tests
596
597
Allen Lida62c612017-03-10 16:32:38 -0800598class _BatchControlFileRetriever(_ControlFileRetriever):
599 """Subclass that can retrieve suite control files in batch."""
600
601
Allen Li1abded52017-03-10 16:37:57 -0800602 def _get_control_file_texts_for_suite(self, suite_name):
603 """Get control file content for given suite.
604
605 @param suite_name: If specified, this method will attempt to restrain
606 the search space to just this suite's control files.
Allen Li975c9522017-03-10 16:40:52 -0800607 @returns: generator yielding (path, text) tuples
Allen Li1abded52017-03-10 16:37:57 -0800608 """
609 suite_info = self._cf_getter.get_suite_info(suite_name=suite_name)
610 files = suite_info.keys()
611 filtered_files = self._filter_cf_paths(files)
Allen Li975c9522017-03-10 16:40:52 -0800612 for path in filtered_files:
Allen Li1abded52017-03-10 16:37:57 -0800613 yield path, suite_info[path]
614
615
Allen Li1865f632017-03-09 15:58:52 -0800616def get_test_source_build(builds, **dargs):
617 """Get the build of test code.
618
619 Get the test source build from arguments. If parameter
620 `test_source_build` is set and has a value, return its value. Otherwise
621 returns the ChromeOS build name if it exists. If ChromeOS build is not
622 specified either, raise SuiteArgumentException.
623
624 @param builds: the builds on which we're running this suite. It's a
625 dictionary of version_prefix:build.
626 @param **dargs: Any other Suite constructor parameters, as described
627 in Suite.__init__ docstring.
628
629 @return: The build contains the test code.
630 @raise: SuiteArgumentException if both test_source_build and ChromeOS
631 build are not specified.
632
633 """
634 if dargs.get('test_source_build', None):
635 return dargs['test_source_build']
636 test_source_build = builds.get(provision.CROS_VERSION_PREFIX, None)
637 if not test_source_build:
638 raise error.SuiteArgumentException(
639 'test_source_build must be specified if CrOS build is not '
640 'specified.')
641 return test_source_build
642
643
Allen Li122cd092017-03-09 15:56:46 -0800644def list_all_suites(build, devserver, cf_getter=None):
645 """
646 Parses all ControlData objects with a SUITE tag and extracts all
647 defined suite names.
648
649 @param build: the build on which we're running this suite.
650 @param devserver: the devserver which contains the build.
651 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
652 using DevServerGetter.
653
654 @return list of suites
655 """
656 if cf_getter is None:
657 cf_getter = _create_ds_getter(build, devserver)
658
659 suites = set()
660 predicate = lambda t: True
661 for test in find_and_parse_tests(cf_getter, predicate,
662 add_experimental=True):
663 suites.update(test.suite_tag_parts)
664 return list(suites)
665
666
Allen Lid1806ac2017-03-09 15:52:33 -0800667def test_file_similarity_predicate(test_file_pattern):
668 """Returns predicate that gets the similarity based on a test's file
669 name pattern.
670
671 Builds a predicate that takes in a parsed control file (a ControlData)
672 and returns a tuple of (file path, ratio), where ratio is the
673 similarity between the test file name and the given test_file_pattern.
674
675 @param test_file_pattern: regular expression (string) to match against
676 control file names.
677 @return a callable that takes a ControlData and and returns a tuple of
678 (file path, ratio), where ratio is the similarity between the
679 test file name and the given test_file_pattern.
680 """
681 return lambda t: ((None, 0) if not hasattr(t, 'path') else
682 (t.path, difflib.SequenceMatcher(a=t.path,
683 b=test_file_pattern).ratio()))
684
685
Allen Lib5b4a7a2017-03-09 15:50:09 -0800686def test_name_similarity_predicate(test_name):
687 """Returns predicate that matched based on a test's name.
688
689 Builds a predicate that takes in a parsed control file (a ControlData)
690 and returns a tuple of (test name, ratio), where ratio is the similarity
691 between the test name and the given test_name.
692
693 @param test_name: the test name to base the predicate on.
694 @return a callable that takes a ControlData and returns a tuple of
695 (test name, ratio), where ratio is the similarity between the
696 test name and the given test_name.
697 """
698 return lambda t: ((None, 0) if not hasattr(t, 'name') else
699 (t.name,
700 difflib.SequenceMatcher(a=t.name, b=test_name).ratio()))
701
702
Allen Lie37d6ba2017-03-09 15:49:25 -0800703def matches_attribute_expression_predicate(test_attr_boolstr):
704 """Returns predicate that matches based on boolean expression of
705 attributes.
706
707 Builds a predicate that takes in a parsed control file (a ControlData)
708 ans returns True if the test attributes satisfy the given attribute
709 boolean expression.
710
711 @param test_attr_boolstr: boolean expression of the attributes to be
712 test, like 'system:all and interval:daily'.
713
714 @return a callable that takes a ControlData and returns True if the test
715 attributes satisfy the given boolean expression.
716 """
717 return lambda t: boolparse_lib.BoolstrResult(
718 test_attr_boolstr, t.attributes)
719
720
Allen Lif29b48a2017-03-09 15:48:41 -0800721def test_file_matches_pattern_predicate(test_file_pattern):
722 """Returns predicate that matches based on a test's file name pattern.
723
724 Builds a predicate that takes in a parsed control file (a ControlData)
725 and returns True if the test's control file name matches the given
726 regular expression.
727
728 @param test_file_pattern: regular expression (string) to match against
729 control file names.
730 @return a callable that takes a ControlData and and returns
731 True if control file name matches the pattern.
732 """
733 return lambda t: hasattr(t, 'path') and re.match(test_file_pattern,
734 t.path)
735
736
Allen Li1819f522017-03-09 15:47:25 -0800737def test_name_matches_pattern_predicate(test_name_pattern):
738 """Returns predicate that matches based on a test's name pattern.
739
740 Builds a predicate that takes in a parsed control file (a ControlData)
741 and returns True if the test name matches the given regular expression.
742
743 @param test_name_pattern: regular expression (string) to match against
744 test names.
745 @return a callable that takes a ControlData and returns
746 True if the name fields matches the pattern.
747 """
748 return lambda t: hasattr(t, 'name') and re.match(test_name_pattern,
749 t.name)
750
751
Allen Lif8441c82017-03-09 15:46:32 -0800752def test_name_equals_predicate(test_name):
753 """Returns predicate that matched based on a test's name.
754
755 Builds a predicate that takes in a parsed control file (a ControlData)
756 and returns True if the test name is equal to |test_name|.
757
758 @param test_name: the test name to base the predicate on.
759 @return a callable that takes a ControlData and looks for |test_name|
760 in that ControlData's name.
761 """
762 return lambda t: hasattr(t, 'name') and test_name == t.name
763
764
Allen Li6e2fa4f2017-03-09 15:45:43 -0800765def name_in_tag_similarity_predicate(name):
766 """Returns predicate that takes a control file and gets the similarity
767 of the suites in the control file and the given name.
768
769 Builds a predicate that takes in a parsed control file (a ControlData)
770 and returns a list of tuples of (suite name, ratio), where suite name
771 is each suite listed in the control file, and ratio is the similarity
772 between each suite and the given name.
773
774 @param name: the suite name to base the predicate on.
775 @return a callable that takes a ControlData and returns a list of tuples
776 of (suite name, ratio), where suite name is each suite listed in
777 the control file, and ratio is the similarity between each suite
778 and the given name.
779 """
780 return lambda t: [(suite,
781 difflib.SequenceMatcher(a=suite, b=name).ratio())
782 for suite in t.suite_tag_parts] or [(None, 0)]
783
784
Allen Li398ddbd2017-03-09 15:44:25 -0800785def name_in_tag_predicate(name):
786 """Returns predicate that takes a control file and looks for |name|.
787
788 Builds a predicate that takes in a parsed control file (a ControlData)
789 and returns True if the SUITE tag is present and contains |name|.
790
791 @param name: the suite name to base the predicate on.
792 @return a callable that takes a ControlData and looks for |name| in that
793 ControlData object's suite member.
794 """
795 return lambda t: name in t.suite_tag_parts
796
797
Allen Lia640d6d2017-03-09 15:41:35 -0800798def create_fs_getter(autotest_dir):
799 """
800 @param autotest_dir: the place to find autotests.
801 @return a FileSystemGetter instance that looks under |autotest_dir|.
802 """
803 # currently hard-coded places to look for tests.
804 subpaths = ['server/site_tests', 'client/site_tests',
805 'server/tests', 'client/tests']
806 directories = [os.path.join(autotest_dir, p) for p in subpaths]
807 return control_file_getter.FileSystemGetter(directories)
808
809
Allen Li0f915872017-02-28 18:51:04 -0800810def _create_ds_getter(build, devserver):
811 """
812 @param build: the build on which we're running this suite.
813 @param devserver: the devserver which contains the build.
814 @return a FileSystemGetter instance that looks under |autotest_dir|.
815 """
816 return control_file_getter.DevServerGetter(build, devserver)
817
818
Allen Li0b1fa382017-02-28 18:47:16 -0800819def find_and_parse_tests(cf_getter, predicate, suite_name='',
820 add_experimental=False, forgiving_parser=True,
821 run_prod_code=False, test_args=None):
822 """
823 Function to scan through all tests and find eligible tests.
824
825 Search through all tests based on given cf_getter, suite_name,
826 add_experimental and forgiving_parser, return the tests that match
827 given predicate.
828
829 @param cf_getter: a control_file_getter.ControlFileGetter used to list
830 and fetch the content of control files
831 @param predicate: a function that should return True when run over a
832 ControlData representation of a control file that should be in
833 this Suite.
834 @param suite_name: If specified, this method will attempt to restrain
835 the search space to just this suite's control files.
836 @param add_experimental: add tests with experimental attribute set.
837 @param forgiving_parser: If False, will raise ControlVariableExceptions
838 if any are encountered when parsing control
839 files. Note that this can raise an exception
840 for syntax errors in unrelated files, because
841 we parse them before applying the predicate.
842 @param run_prod_code: If true, the suite will run the test code that
843 lives in prod aka the test code currently on the
844 lab servers by disabling SSP for the discovered
845 tests.
846 @param test_args: A dict of args to be seeded in test control file.
847
848 @raises ControlVariableException: If forgiving_parser is False and there
849 is a syntax error in a control file.
850
851 @return list of ControlData objects that should be run, with control
852 file text added in |text| attribute. Results are sorted based
853 on the TIME setting in control file, slowest test comes first.
854 """
Allen Libb60f442017-03-14 12:18:57 -0700855 logging.debug('Getting control file list for suite: %s', suite_name)
Allen Liaed93492017-03-14 13:36:26 -0700856 retriever = _get_cf_retriever(cf_getter,
857 forgiving_parser=forgiving_parser,
858 run_prod_code=run_prod_code,
859 test_args=test_args)
860 tests = retriever.retrieve_for_suite(suite_name)
Allen Li76b8f312017-03-10 16:00:08 -0800861 if not add_experimental:
862 tests = {path: test_data for path, test_data in tests.iteritems()
863 if not test_data.experimental}
Allen Li0b1fa382017-02-28 18:47:16 -0800864 logging.debug('Parsed %s control files.', len(tests))
865 tests = [test for test in tests.itervalues() if predicate(test)]
866 tests.sort(key=lambda t:
867 control_data.ControlData.get_test_time_index(t.time),
868 reverse=True)
869 return tests
870
871
Allen Lida012192017-02-28 18:37:52 -0800872def find_possible_tests(cf_getter, predicate, suite_name='', count=10):
873 """
874 Function to scan through all tests and find possible tests.
875
876 Search through all tests based on given cf_getter, suite_name,
877 add_experimental and forgiving_parser. Use the given predicate to
878 calculate the similarity and return the top 10 matches.
879
880 @param cf_getter: a control_file_getter.ControlFileGetter used to list
881 and fetch the content of control files
882 @param predicate: a function that should return a tuple of (name, ratio)
883 when run over a ControlData representation of a control file that
884 should be in this Suite. `name` is the key to be compared, e.g.,
885 a suite name or test name. `ratio` is a value between [0,1]
886 indicating the similarity of `name` and the value to be compared.
887 @param suite_name: If specified, this method will attempt to restrain
888 the search space to just this suite's control files.
889 @param count: Number of suggestions to return, default to 10.
890
891 @return list of top names that similar to the given test, sorted by
892 match ratio.
893 """
Allen Libb60f442017-03-14 12:18:57 -0700894 logging.debug('Getting control file list for suite: %s', suite_name)
Allen Liaed93492017-03-14 13:36:26 -0700895 retriever = _get_cf_retriever(cf_getter,
896 forgiving_parser=True)
897 tests = retriever.retrieve_for_suite(suite_name)
Allen Lida012192017-02-28 18:37:52 -0800898 logging.debug('Parsed %s control files.', len(tests))
899 similarities = {}
900 for test in tests.itervalues():
901 ratios = predicate(test)
902 # Some predicates may return a list of tuples, e.g.,
903 # name_in_tag_similarity_predicate. Convert all returns to a list.
904 if not isinstance(ratios, list):
905 ratios = [ratios]
906 for name, ratio in ratios:
907 similarities[name] = ratio
908 return [s[0] for s in
909 sorted(similarities.items(), key=operator.itemgetter(1),
910 reverse=True)][:count]
911
912
Allen Li98a26a42017-02-28 18:43:24 -0800913def _deprecated_suite_method(func):
914 """Decorator for deprecated Suite static methods.
915
916 TODO(ayatane): This is used to decorate functions that are called as
917 static methods on Suite.
918 """
919 @functools.wraps(func)
920 def wrapper(*args, **kwargs):
921 warnings.warn('Calling this method from Suite is deprecated')
922 return func(*args, **kwargs)
923 return staticmethod(wrapper)
924
925
Allen Li4b5a24f2017-03-09 16:01:35 -0800926class _BaseSuite(object):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700927 """
928 A suite of tests, defined by some predicate over control file variables.
929
930 Given a place to search for control files a predicate to match the desired
931 tests, can gather tests and fire off jobs to run them, and then wait for
932 results.
933
934 @var _predicate: a function that should return True when run over a
935 ControlData representation of a control file that should be in
936 this Suite.
937 @var _tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700938 @var _builds: the builds on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700939 @var _afe: an instance of AFE as defined in server/frontend.py.
940 @var _tko: an instance of TKO as defined in server/frontend.py.
941 @var _jobs: currently scheduled jobs, if any.
Fang Denge3bc24b2014-03-17 15:19:46 -0700942 @var _jobs_to_tests: a dictionary that maps job ids to tests represented
943 ControlData objects.
Fang Denge3bc24b2014-03-17 15:19:46 -0700944 @var _retry: a bool value indicating whether jobs should be retried on
945 failure.
946 @var _retry_handler: a RetryHandler object.
947
Chris Masone44e4d6c2012-08-15 14:25:53 -0700948 """
949
Dan Shi36cfd832014-10-10 13:38:51 -0700950
Allen Li6fff5502016-12-09 18:04:26 -0800951 def __init__(
952 self,
Allen Li00bbe5b2017-03-09 16:44:30 -0800953 tests,
Allen Li6fff5502016-12-09 18:04:26 -0800954 tag,
955 builds,
956 board,
Allen Li6fff5502016-12-09 18:04:26 -0800957 afe=None,
958 tko=None,
959 pool=None,
960 results_dir=None,
961 max_runtime_mins=24*60,
962 timeout_mins=24*60,
963 file_bugs=False,
964 file_experimental_bugs=False,
965 suite_job_id=None,
966 ignore_deps=False,
Allen Li493eefa2016-12-09 18:05:35 -0800967 extra_deps=None,
Allen Li6fff5502016-12-09 18:04:26 -0800968 priority=priorities.Priority.DEFAULT,
Allen Li6fff5502016-12-09 18:04:26 -0800969 wait_for_results=True,
970 job_retry=False,
971 max_retries=sys.maxint,
972 offload_failures_only=False,
Shuqian Zhaoda1118d2017-02-13 16:22:58 -0800973 test_source_build=None,
Allen Li7f43ef92017-03-09 16:29:48 -0800974 job_keyvals=None
Allen Li6fff5502016-12-09 18:04:26 -0800975 ):
Allen Li7f43ef92017-03-09 16:29:48 -0800976 """Initialize instance.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700977
Allen Li00bbe5b2017-03-09 16:44:30 -0800978 @param tests: Iterable of tests to run.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700979 @param tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700980 @param builds: the builds on which we're running this suite.
Alex Millera0913072013-06-12 10:01:51 -0700981 @param board: the board on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700982 @param afe: an instance of AFE as defined in server/frontend.py.
983 @param tko: an instance of TKO as defined in server/frontend.py.
984 @param pool: Specify the pool of machines to use for scheduling
985 purposes.
986 @param results_dir: The directory where the job can write results to.
987 This must be set if you want job_id of sub-jobs
988 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -0800989 @param max_runtime_mins: Maximum suite runtime, in minutes.
Alex Miller028b0312013-09-07 15:25:45 -0700990 @param timeout: Maximum job lifetime, in hours.
Aviv Keshet18308922013-02-19 17:49:49 -0800991 @param suite_job_id: Job id that will act as parent id to all sub jobs.
992 Default: None
Aviv Keshetd7959f32013-05-17 15:58:43 -0700993 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
994 attribute and skip applying of dependency labels.
995 (Default:False)
Alex Miller47a03672013-08-27 09:09:53 -0700996 @param extra_deps: A list of strings which are the extra DEPENDENCIES
997 to add to each test being scheduled.
Alex Miller7d658cf2013-09-04 16:00:35 -0700998 @param priority: Integer priority level. Higher is more important.
Dan Shi95122412013-11-12 16:20:33 -0800999 @param wait_for_results: Set to False to run the suite job without
1000 waiting for test jobs to finish. Default is
1001 True.
Fang Denge3bc24b2014-03-17 15:19:46 -07001002 @param job_retry: A bool value indicating whether jobs should be retired
1003 on failure. If True, the field 'JOB_RETRIES' in
1004 control files will be respected. If False, do not
1005 retry.
Fang Deng443f1952015-01-02 14:51:49 -08001006 @param max_retries: Maximum retry limit at suite level.
1007 Regardless how many times each individual test
1008 has been retried, the total number of retries
1009 happening in the suite can't exceed _max_retries.
1010 Default to sys.maxint.
Simran Basi1e10e922015-04-16 15:09:56 -07001011 @param offload_failures_only: Only enable gs_offloading for failed
1012 jobs.
Dan Shi36cfd832014-10-10 13:38:51 -07001013 @param test_source_build: Build that contains the server-side test code.
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001014 @param job_keyvals: General job keyvals to be inserted into keyval file,
1015 which will be used by tko/parse later.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001016 """
Allen Li493eefa2016-12-09 18:05:35 -08001017
Allen Li00bbe5b2017-03-09 16:44:30 -08001018 self.tests = list(tests)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001019 self._tag = tag
Dan Shi36cfd832014-10-10 13:38:51 -07001020 self._builds = builds
Chris Masone44e4d6c2012-08-15 14:25:53 -07001021 self._results_dir = results_dir
1022 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
1023 delay_sec=10,
1024 debug=False)
1025 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
1026 delay_sec=10,
1027 debug=False)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001028 self._jobs = []
Fang Denge3bc24b2014-03-17 15:19:46 -07001029 self._jobs_to_tests = {}
beeps89f1e062013-09-18 12:00:17 -07001030
Alex Millera3a4fe72013-01-22 09:57:47 -08001031 self._file_bugs = file_bugs
beepsda5b7112013-05-30 11:34:14 -07001032 self._file_experimental_bugs = file_experimental_bugs
Aviv Keshet18308922013-02-19 17:49:49 -08001033 self._suite_job_id = suite_job_id
Fang Denge3bc24b2014-03-17 15:19:46 -07001034 self._job_retry=job_retry
Fang Deng443f1952015-01-02 14:51:49 -08001035 self._max_retries = max_retries
Fang Denge3bc24b2014-03-17 15:19:46 -07001036 # RetryHandler to be initialized in schedule()
1037 self._retry_handler = None
Dan Shi95122412013-11-12 16:20:33 -08001038 self.wait_for_results = wait_for_results
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001039 self._job_keyvals = job_keyvals
Alex Millera3a4fe72013-01-22 09:57:47 -08001040
Allen Li80dc02c2017-02-28 18:22:16 -08001041 if extra_deps is None:
1042 extra_deps = []
Allen Li3a83fe62017-02-28 18:27:09 -08001043 extra_deps.append(board)
Allen Licceb1832017-02-28 18:25:06 -08001044 if pool:
1045 extra_deps.append(pool)
Allen Li010c0412017-03-29 17:31:35 -07001046 self._job_creator = _SuiteChildJobCreator(
Allen Li55de3402017-03-29 17:48:46 -07001047 tag=tag,
Allen Li27f72a22017-03-29 17:37:43 -07001048 builds=builds,
Allen Li010c0412017-03-29 17:31:35 -07001049 board=board,
Allen Li388b7a12017-03-29 17:58:23 -07001050 afe=afe,
Allen Li388b7a12017-03-29 17:58:23 -07001051 max_runtime_mins=max_runtime_mins,
1052 timeout_mins=timeout_mins,
Allen Li55de3402017-03-29 17:48:46 -07001053 suite_job_id=suite_job_id,
Allen Li010c0412017-03-29 17:31:35 -07001054 ignore_deps=ignore_deps,
1055 extra_deps=extra_deps,
Allen Li388b7a12017-03-29 17:58:23 -07001056 priority=priority,
Allen Li55de3402017-03-29 17:48:46 -07001057 offload_failures_only=offload_failures_only,
1058 test_source_build=test_source_build,
Allen Li010c0412017-03-29 17:31:35 -07001059 )
Allen Lida198fd2017-03-29 17:22:13 -07001060
Chris Masone44e4d6c2012-08-15 14:25:53 -07001061
Fang Denge3bc24b2014-03-17 15:19:46 -07001062 def _schedule_test(self, record, test, retry_for=None, ignore_errors=False):
1063 """Schedule a single test and return the job.
1064
Allen Lie79b3cb2016-12-12 18:24:17 -08001065 Schedule a single test by creating a job, and then update relevant
1066 data structures that are used to keep track of all running jobs.
Fang Denge3bc24b2014-03-17 15:19:46 -07001067
Allen Lie79b3cb2016-12-12 18:24:17 -08001068 Emits a TEST_NA status log entry if it failed to schedule the test due
1069 to NoEligibleHostException or a non-existent board label.
1070
1071 Returns a frontend.Job object if the test is successfully scheduled.
1072 If scheduling failed due to NoEligibleHostException or a non-existent
1073 board label, returns None. If ignore_errors is True, all unknown
1074 errors return None, otherwise the errors are raised as-is.
Fang Denge3bc24b2014-03-17 15:19:46 -07001075
1076 @param record: A callable to use for logging.
1077 prototype: record(base_job.status_log_entry)
1078 @param test: ControlData for a test to run.
1079 @param retry_for: If we are scheduling a test to retry an
1080 old job, the afe_job_id of the old job
1081 will be passed in as |retry_for|.
1082 @param ignore_errors: If True, when an rpc error occur, ignore
1083 the error and will return None.
1084 If False, rpc errors will be raised.
1085
Allen Lie79b3cb2016-12-12 18:24:17 -08001086 @returns: A frontend.Job object or None
Fang Denge3bc24b2014-03-17 15:19:46 -07001087 """
1088 msg = 'Scheduling %s' % test.name
1089 if retry_for:
1090 msg = msg + ', to retry afe job %d' % retry_for
1091 logging.debug(msg)
Dan Shidfea3682014-08-10 23:38:40 -07001092 begin_time_str = datetime.datetime.now().strftime(time_utils.TIME_FMT)
Fang Denge3bc24b2014-03-17 15:19:46 -07001093 try:
Allen Li388b7a12017-03-29 17:58:23 -07001094 job = self._job_creator.create_job(test, retry_for=retry_for)
Allen Li6fd440f2016-12-12 18:40:05 -08001095 except (error.NoEligibleHostException, proxy.ValidationError) as e:
1096 if (isinstance(e, error.NoEligibleHostException)
1097 or (isinstance(e, proxy.ValidationError)
1098 and _is_nonexistent_board_error(e))):
1099 # Treat a dependency on a non-existent board label the same as
1100 # a dependency on a board that exists, but for which there's no
1101 # hardware.
1102 logging.debug('%s not applicable for this board/pool. '
1103 'Emitting TEST_NA.', test.name)
1104 Status('TEST_NA', test.name,
1105 'Skipping: test not supported on this board/pool.',
Allen Li9fcd4b42016-12-12 16:15:14 -08001106 begin_time_str=begin_time_str).record_all(record)
1107 return None
1108 else:
Fang Denge3bc24b2014-03-17 15:19:46 -07001109 raise e
Fang Denge3bc24b2014-03-17 15:19:46 -07001110 except (error.RPCException, proxy.JSONRPCException) as e:
1111 if retry_for:
1112 # Mark that we've attempted to retry the old job.
1113 self._retry_handler.set_attempted(job_id=retry_for)
Allen Li0ba59342016-12-12 15:57:02 -08001114
Fang Denge3bc24b2014-03-17 15:19:46 -07001115 if ignore_errors:
1116 logging.error('Failed to schedule test: %s, Reason: %s',
1117 test.name, e)
Allen Li0ba59342016-12-12 15:57:02 -08001118 return None
Fang Denge3bc24b2014-03-17 15:19:46 -07001119 else:
1120 raise e
1121 else:
1122 self._jobs.append(job)
1123 self._jobs_to_tests[job.id] = test
1124 if retry_for:
1125 # A retry job was just created, record it.
1126 self._retry_handler.add_retry(
1127 old_job_id=retry_for, new_job_id=job.id)
1128 retry_count = (test.job_retries -
1129 self._retry_handler.get_retry_max(job.id))
1130 logging.debug('Job %d created to retry job %d. '
1131 'Have retried for %d time(s)',
1132 job.id, retry_for, retry_count)
Allen Li4df053e2016-12-29 16:05:41 -08001133 self._remember_job_keyval(job)
Fang Denge3bc24b2014-03-17 15:19:46 -07001134 return job
Fang Denge3bc24b2014-03-17 15:19:46 -07001135
1136
Alex Miller3a69adc2012-12-19 13:38:31 -08001137 def schedule(self, record, add_experimental=True):
Aviv Keshet18308922013-02-19 17:49:49 -08001138 #pylint: disable-msg=C0111
Chris Masone44e4d6c2012-08-15 14:25:53 -07001139 """
1140 Schedule jobs using |self._afe|.
1141
1142 frontend.Job objects representing each scheduled job will be put in
1143 |self._jobs|.
1144
Fang Denge3bc24b2014-03-17 15:19:46 -07001145 @param record: A callable to use for logging.
1146 prototype: record(base_job.status_log_entry)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001147 @param add_experimental: schedule experimental tests as well, or not.
Aviv Keshete9170d92013-07-19 11:20:45 -07001148 @returns: The number of tests that were scheduled.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001149 """
Allen Lif4cb5ec2017-01-03 16:58:12 -08001150 scheduled_test_names = []
Allen Li86f8c282017-02-28 13:09:40 -08001151 test_filter = _ExperimentalTestFilter(
Allen Lif4cb5ec2017-01-03 16:58:12 -08001152 tests=self.tests,
1153 add_experimental=add_experimental)
1154 logging.debug('Discovered %d stable tests.',
Allen Li86f8c282017-02-28 13:09:40 -08001155 len(test_filter.stable_tests))
Alex Miller3a69adc2012-12-19 13:38:31 -08001156 logging.debug('Discovered %d unstable tests.',
Allen Li86f8c282017-02-28 13:09:40 -08001157 len(test_filter.unstable_tests))
Chris Masone44e4d6c2012-08-15 14:25:53 -07001158
Alex Miller3a69adc2012-12-19 13:38:31 -08001159 Status('INFO', 'Start %s' % self._tag).record_result(record)
1160 try:
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001161 # Write job_keyvals into keyval file.
1162 if self._job_keyvals:
1163 utils.write_keyval(self._results_dir, self._job_keyvals)
1164
Allen Li86f8c282017-02-28 13:09:40 -08001165 for test in test_filter.get_tests_to_schedule():
Allen Lida905732016-12-12 15:49:16 -08001166 scheduled_job = self._schedule_test(record, test)
1167 if scheduled_job is not None:
Shuqian Zhaocd866f32016-11-29 20:14:34 -08001168 scheduled_test_names.append(test.name)
1169
1170 # Write the num of scheduled tests and name of them to keyval file.
Shuqian Zhaocd866f32016-11-29 20:14:34 -08001171 logging.debug('Scheduled %d tests, writing the total to keyval.',
Allen Lia4d35022016-12-12 15:42:10 -08001172 len(scheduled_test_names))
Allen Lid4d5dda2016-12-12 15:39:11 -08001173 utils.write_keyval(
1174 self._results_dir,
Allen Lidda59b82016-12-12 18:20:04 -08001175 self._make_scheduled_tests_keyvals(scheduled_test_names))
Alex Miller3a69adc2012-12-19 13:38:31 -08001176 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001177 logging.exception('Exception while scheduling suite')
Alex Miller3a69adc2012-12-19 13:38:31 -08001178 Status('FAIL', self._tag,
1179 'Exception while scheduling suite').record_result(record)
1180
Fang Deng7e655a92014-05-23 13:48:11 -07001181 if self._job_retry:
1182 self._retry_handler = RetryHandler(
Fang Deng443f1952015-01-02 14:51:49 -08001183 initial_jobs_to_tests=self._jobs_to_tests,
1184 max_retries=self._max_retries)
Allen Lia4d35022016-12-12 15:42:10 -08001185 return len(scheduled_test_names)
Aviv Keshete9170d92013-07-19 11:20:45 -07001186
Alex Miller3a69adc2012-12-19 13:38:31 -08001187
Allen Lidda59b82016-12-12 18:20:04 -08001188 def _make_scheduled_tests_keyvals(self, scheduled_test_names):
1189 """Make a keyvals dict to write for scheduled test names.
1190
1191 @param scheduled_test_names: A list of scheduled test name strings.
1192
1193 @returns: A keyvals dict.
1194 """
1195 return {
1196 constants.SCHEDULED_TEST_COUNT_KEY: len(scheduled_test_names),
1197 constants.SCHEDULED_TEST_NAMES_KEY: repr(scheduled_test_names),
1198 }
1199
1200
Allen Lid1cbccf2016-12-29 15:12:39 -08001201 def _should_report(self, result):
beepsda5b7112013-05-30 11:34:14 -07001202 """
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -07001203 Returns True if this failure requires to be reported.
beepsda5b7112013-05-30 11:34:14 -07001204
1205 @param result: A result, encapsulating the status of the failed job.
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -07001206 @return: True if we should report this failure.
beepsda5b7112013-05-30 11:34:14 -07001207 """
Allen Licc752292017-01-03 12:44:39 -08001208 if self._has_retry(result):
Fang Denge3bc24b2014-03-17 15:19:46 -07001209 return False
1210
beepsbeefc062013-08-02 11:17:09 -07001211 is_not_experimental = (
1212 constants.EXPERIMENTAL_PREFIX not in result._test_name and
1213 constants.EXPERIMENTAL_PREFIX not in result._job_name)
1214
Alex Millerfcc119b2014-01-15 13:54:58 -08001215 return (self._file_bugs and result.test_executed and
beepsbeefc062013-08-02 11:17:09 -07001216 (is_not_experimental or self._file_experimental_bugs) and
Fang Dengd82c1c72014-07-29 10:43:01 -07001217 not result.is_testna() and
beeps32fa6772014-01-28 13:19:53 -08001218 result.is_worse_than(job_status.Status('GOOD', '', 'reason')))
beepsda5b7112013-05-30 11:34:14 -07001219
1220
Allen Licc752292017-01-03 12:44:39 -08001221 def _has_retry(self, result):
1222 """
1223 Return True if this result gets to retry.
1224
1225 @param result: A result, encapsulating the status of the failed job.
1226 @return: bool
1227 """
1228 return (self._job_retry
1229 and self._retry_handler.has_following_retry(result))
1230
1231
Allen Li18503452016-12-29 14:56:48 -08001232 def wait(self, record, bug_template=None):
Alex Miller3a69adc2012-12-19 13:38:31 -08001233 """
1234 Polls for the job statuses, using |record| to print status when each
1235 completes.
1236
1237 @param record: callable that records job status.
1238 prototype:
1239 record(base_job.status_log_entry)
beepsc8a875b2013-03-25 10:20:38 -07001240 @param bug_template: A template dictionary specifying the default bug
1241 filing options for failures in this suite.
Alex Miller3a69adc2012-12-19 13:38:31 -08001242 """
Dan Shie67bd6a2016-02-17 14:44:07 -08001243 # reporting modules have dependency on external packages, e.g., httplib2
1244 # Such dependency can cause issue to any module tries to import suite.py
1245 # without building site-packages first. Since the reporting modules are
1246 # only used in this function, move the imports here avoid the
1247 # requirement of building site packages to use other functions in this
1248 # module.
1249 from autotest_lib.server.cros.dynamic_suite import reporting
Dan Shie67bd6a2016-02-17 14:44:07 -08001250
Allen Li18503452016-12-29 14:56:48 -08001251 if bug_template is None:
1252 bug_template = {}
1253
Alex Millera3a4fe72013-01-22 09:57:47 -08001254 if self._file_bugs:
1255 bug_reporter = reporting.Reporter()
Allen Li733dab92016-12-29 15:07:50 -08001256 else:
1257 bug_reporter = reporting.NullReporter()
Alex Miller3a69adc2012-12-19 13:38:31 -08001258 try:
Aviv Keshet133beb12013-08-20 14:37:13 -07001259 if self._suite_job_id:
1260 results_generator = job_status.wait_for_child_results(
1261 self._afe, self._tko, self._suite_job_id)
1262 else:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -07001263 logging.warning('Unknown suite_job_id, falling back to less '
Dan Shi08ff1282016-02-18 19:51:16 -08001264 'efficient results_generator.')
Aviv Keshet133beb12013-08-20 14:37:13 -07001265 results_generator = job_status.wait_for_results(self._afe,
1266 self._tko,
1267 self._jobs)
1268 for result in results_generator:
Allen Li26b340d2016-12-29 15:23:01 -08001269 self._record_result(
1270 result=result,
1271 record=record,
1272 results_generator=results_generator,
1273 bug_reporter=bug_reporter,
1274 bug_template=bug_template)
beeps8ead53c2013-04-26 19:12:46 -07001275
Alex Miller3a69adc2012-12-19 13:38:31 -08001276 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001277 logging.exception('Exception waiting for results')
Alex Miller3a69adc2012-12-19 13:38:31 -08001278 Status('FAIL', self._tag,
1279 'Exception waiting for results').record_result(record)
1280
1281
Allen Li26b340d2016-12-29 15:23:01 -08001282 def _record_result(self, result, record, results_generator, bug_reporter,
1283 bug_template):
1284 """
1285 Record a single test job result.
1286
1287 @param result: Status instance for job.
1288 @param record: callable that records job status.
1289 prototype:
1290 record(base_job.status_log_entry)
1291 @param results_generator: Results generator for sending job retries.
1292 @param bug_reporter: Reporter instance for reporting bugs.
1293 @param bug_template: A template dictionary specifying the default bug
1294 filing options for failures in this suite.
1295 """
Allen Li26b340d2016-12-29 15:23:01 -08001296 result.record_all(record)
Allen Li4df053e2016-12-29 16:05:41 -08001297 self._remember_job_keyval(result)
Allen Li26b340d2016-12-29 15:23:01 -08001298
xixuanbf854f82017-04-20 10:40:15 -07001299 if self._job_retry and self._retry_handler._should_retry(result):
Allen Li26b340d2016-12-29 15:23:01 -08001300 new_job = self._schedule_test(
1301 record=record, test=self._jobs_to_tests[result.id],
1302 retry_for=result.id, ignore_errors=True)
1303 if new_job:
1304 results_generator.send([new_job])
1305
1306 # TODO (fdeng): If the suite times out before a retry could
1307 # finish, we would lose the chance to file a bug for the
1308 # original job.
1309 if self._should_report(result):
Allen Li11308982016-12-29 16:19:55 -08001310 if self._should_file_bugs:
Allen Li47c9fca2016-12-29 16:22:53 -08001311 self._file_bug(result, bug_reporter, bug_template)
Allen Li26b340d2016-12-29 15:23:01 -08001312 else:
Allen Lid5df44b2016-12-29 15:59:06 -08001313 # reporting modules have dependency on external
1314 # packages, e.g., httplib2 Such dependency can cause
1315 # issue to any module tries to import suite.py without
1316 # building site-packages first. Since the reporting
1317 # modules are only used in this function, move the
1318 # imports here avoid the requirement of building site
1319 # packages to use other functions in this module.
1320 from autotest_lib.server.cros.dynamic_suite import reporting
1321
Allen Li7b973112016-12-29 16:17:41 -08001322 reporting.send_email(
1323 self._get_test_bug(result),
1324 self._get_bug_template(result, bug_template))
Allen Li26b340d2016-12-29 15:23:01 -08001325
1326
Allen Lid5df44b2016-12-29 15:59:06 -08001327 def _get_bug_template(self, result, bug_template):
1328 """Get BugTemplate for test job.
1329
1330 @param result: Status instance for job.
1331 @param bug_template: A template dictionary specifying the default bug
1332 filing options for failures in this suite.
1333 @returns: BugTemplate instance
1334 """
1335 # reporting modules have dependency on external packages, e.g., httplib2
1336 # Such dependency can cause issue to any module tries to import suite.py
1337 # without building site-packages first. Since the reporting modules are
1338 # only used in this function, move the imports here avoid the
1339 # requirement of building site packages to use other functions in this
1340 # module.
1341 from autotest_lib.server.cros.dynamic_suite import reporting_utils
1342
1343 # Try to merge with bug template in test control file.
1344 template = reporting_utils.BugTemplate(bug_template)
1345 try:
1346 test_data = self._jobs_to_tests[result.id]
1347 return template.finalize_bug_template(
1348 test_data.bug_template)
1349 except AttributeError:
1350 # Test control file does not have bug template defined.
1351 return template.bug_template
1352 except reporting_utils.InvalidBugTemplateException as e:
1353 logging.error('Merging bug templates failed with '
1354 'error: %s An empty bug template will '
1355 'be used.', e)
1356 return {}
1357
1358
Allen Li003913e2016-12-29 15:53:34 -08001359 def _get_test_bug(self, result):
1360 """Get TestBug for the given result.
1361
1362 @param result: Status instance for a test job.
1363 @returns: TestBug instance.
1364 """
1365 # reporting modules have dependency on external packages, e.g., httplib2
1366 # Such dependency can cause issue to any module tries to import suite.py
1367 # without building site-packages first. Since the reporting modules are
1368 # only used in this function, move the imports here avoid the
1369 # requirement of building site packages to use other functions in this
1370 # module.
1371 from autotest_lib.server.cros.dynamic_suite import reporting
1372
1373 job_views = self._tko.run('get_detailed_test_views',
1374 afe_job_id=result.id)
Allen Li27f72a22017-03-29 17:37:43 -07001375 return reporting.TestBug(self._job_creator.cros_build,
Allen Li5ed7e632017-02-03 16:31:33 -08001376 utils.get_chrome_version(job_views),
Allen Li003913e2016-12-29 15:53:34 -08001377 self._tag,
1378 result)
1379
1380
Allen Li11308982016-12-29 16:19:55 -08001381 @property
1382 def _should_file_bugs(self):
1383 """Return whether bugs should be filed.
1384
1385 @returns: bool
1386 """
1387 # File bug when failure is one of the _FILE_BUG_SUITES,
1388 # otherwise send an email to the owner anc cc.
1389 return self._tag in _FILE_BUG_SUITES
1390
1391
Allen Li47c9fca2016-12-29 16:22:53 -08001392 def _file_bug(self, result, bug_reporter, bug_template):
1393 """File a bug for a test job result.
1394
1395 @param result: Status instance for job.
1396 @param bug_reporter: Reporter instance for reporting bugs.
1397 @param bug_template: A template dictionary specifying the default bug
1398 filing options for failures in this suite.
1399 """
1400 bug_id, bug_count = bug_reporter.report(
1401 self._get_test_bug(result),
1402 self._get_bug_template(result, bug_template))
1403
1404 # We use keyvals to communicate bugs filed with run_suite.
1405 if bug_id is not None:
1406 bug_keyvals = tools.create_bug_keyvals(
1407 result.id, result.test_name,
1408 (bug_id, bug_count))
1409 try:
1410 utils.write_keyval(self._results_dir,
1411 bug_keyvals)
1412 except ValueError:
1413 logging.error('Unable to log bug keyval for:%s',
1414 result.test_name)
1415
1416
Alex Miller3a69adc2012-12-19 13:38:31 -08001417 def abort(self):
1418 """
1419 Abort all scheduled test jobs.
1420 """
1421 if self._jobs:
1422 job_ids = [job.id for job in self._jobs]
1423 self._afe.run('abort_host_queue_entries', job__id__in=job_ids)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001424
1425
Allen Li4df053e2016-12-29 16:05:41 -08001426 def _remember_job_keyval(self, job):
Chris Masoned9f13c52012-08-29 10:37:08 -07001427 """
1428 Record provided job as a suite job keyval, for later referencing.
1429
Allen Li4df053e2016-12-29 16:05:41 -08001430 @param job: some representation of a job that has the attributes:
1431 id, test_name, and owner
Chris Masoned9f13c52012-08-29 10:37:08 -07001432 """
Allen Li3cc73cd2016-12-12 16:02:21 -08001433 if self._results_dir and job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -07001434 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -07001435 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -07001436 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001437 utils.write_keyval(
1438 self._results_dir,
1439 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
1440
Dan Shid1521802013-05-24 13:08:37 -07001441
Allen Li4b5a24f2017-03-09 16:01:35 -08001442class Suite(_BaseSuite):
1443 """
1444 A suite of tests, defined by some predicate over control file variables.
1445
1446 Given a place to search for control files a predicate to match the desired
1447 tests, can gather tests and fire off jobs to run them, and then wait for
1448 results.
1449
1450 @var _predicate: a function that should return True when run over a
1451 ControlData representation of a control file that should be in
1452 this Suite.
1453 @var _tag: a string with which to tag jobs run in this suite.
1454 @var _builds: the builds on which we're running this suite.
1455 @var _afe: an instance of AFE as defined in server/frontend.py.
1456 @var _tko: an instance of TKO as defined in server/frontend.py.
1457 @var _jobs: currently scheduled jobs, if any.
1458 @var _jobs_to_tests: a dictionary that maps job ids to tests represented
1459 ControlData objects.
1460 @var _cf_getter: a control_file_getter.ControlFileGetter
1461 @var _retry: a bool value indicating whether jobs should be retried on
1462 failure.
1463 @var _retry_handler: a RetryHandler object.
1464
1465 """
1466
1467 # TODO(ayatane): These methods are kept on the Suite class for
1468 # backward compatibility.
1469 find_and_parse_tests = _deprecated_suite_method(find_and_parse_tests)
1470 find_possible_tests = _deprecated_suite_method(find_possible_tests)
1471 create_fs_getter = _deprecated_suite_method(create_fs_getter)
1472 name_in_tag_predicate = _deprecated_suite_method(name_in_tag_predicate)
1473 name_in_tag_similarity_predicate = _deprecated_suite_method(
1474 name_in_tag_similarity_predicate)
1475 test_name_equals_predicate = _deprecated_suite_method(
1476 test_name_equals_predicate)
1477 test_name_matches_pattern_predicate = _deprecated_suite_method(
1478 test_name_matches_pattern_predicate)
1479 test_file_matches_pattern_predicate = _deprecated_suite_method(
1480 test_file_matches_pattern_predicate)
1481 matches_attribute_expression_predicate = _deprecated_suite_method(
1482 matches_attribute_expression_predicate)
1483 test_name_similarity_predicate = _deprecated_suite_method(
1484 test_name_similarity_predicate)
1485 test_file_similarity_predicate = _deprecated_suite_method(
1486 test_file_similarity_predicate)
1487 list_all_suites = _deprecated_suite_method(list_all_suites)
1488 get_test_source_build = _deprecated_suite_method(get_test_source_build)
1489
1490
Allen Li25bb1c62017-03-09 16:27:00 -08001491 @classmethod
1492 def create_from_predicates(cls, predicates, builds, board, devserver,
1493 cf_getter=None, name='ad_hoc_suite',
1494 run_prod_code=False, **dargs):
1495 """
1496 Create a Suite using a given predicate test filters.
1497
1498 Uses supplied predicate(s) to instantiate a Suite. Looks for tests in
1499 |autotest_dir| and will schedule them using |afe|. Pulls control files
1500 from the default dev server. Results will be pulled from |tko| upon
1501 completion.
1502
1503 @param predicates: A list of callables that accept ControlData
1504 representations of control files. A test will be
1505 included in suite if all callables in this list
1506 return True on the given control file.
1507 @param builds: the builds on which we're running this suite. It's a
1508 dictionary of version_prefix:build.
1509 @param board: the board on which we're running this suite.
1510 @param devserver: the devserver which contains the build.
1511 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
1512 using DevServerGetter.
1513 @param name: name of suite. Defaults to 'ad_hoc_suite'
1514 @param run_prod_code: If true, the suite will run the tests that
1515 lives in prod aka the test code currently on the
1516 lab servers.
1517 @param **dargs: Any other Suite constructor parameters, as described
1518 in Suite.__init__ docstring.
1519 @return a Suite instance.
1520 """
1521 if cf_getter is None:
1522 if run_prod_code:
1523 cf_getter = create_fs_getter(_AUTOTEST_DIR)
1524 else:
1525 build = get_test_source_build(builds, **dargs)
1526 cf_getter = _create_ds_getter(build, devserver)
1527
1528 return cls(predicates,
1529 name, builds, board, cf_getter, run_prod_code, **dargs)
1530
1531
1532 @classmethod
1533 def create_from_name(cls, name, builds, board, devserver, cf_getter=None,
1534 **dargs):
1535 """
1536 Create a Suite using a predicate based on the SUITE control file var.
1537
1538 Makes a predicate based on |name| and uses it to instantiate a Suite
1539 that looks for tests in |autotest_dir| and will schedule them using
1540 |afe|. Pulls control files from the default dev server.
1541 Results will be pulled from |tko| upon completion.
1542
1543 @param name: a value of the SUITE control file variable to search for.
1544 @param builds: the builds on which we're running this suite. It's a
1545 dictionary of version_prefix:build.
1546 @param board: the board on which we're running this suite.
1547 @param devserver: the devserver which contains the build.
1548 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
1549 using DevServerGetter.
1550 @param **dargs: Any other Suite constructor parameters, as described
1551 in Suite.__init__ docstring.
1552 @return a Suite instance.
1553 """
1554 if cf_getter is None:
1555 build = get_test_source_build(builds, **dargs)
1556 cf_getter = _create_ds_getter(build, devserver)
1557
1558 return cls([name_in_tag_predicate(name)],
1559 name, builds, board, cf_getter, **dargs)
1560
1561
Allen Li3b1d4e52017-03-09 16:23:06 -08001562 def __init__(
1563 self,
1564 predicates,
1565 tag,
1566 builds,
1567 board,
1568 cf_getter,
1569 run_prod_code=False,
1570 afe=None,
1571 tko=None,
1572 pool=None,
1573 results_dir=None,
1574 max_runtime_mins=24*60,
1575 timeout_mins=24*60,
1576 file_bugs=False,
1577 file_experimental_bugs=False,
1578 suite_job_id=None,
1579 ignore_deps=False,
1580 extra_deps=None,
1581 priority=priorities.Priority.DEFAULT,
1582 forgiving_parser=True,
1583 wait_for_results=True,
1584 job_retry=False,
1585 max_retries=sys.maxint,
1586 offload_failures_only=False,
1587 test_source_build=None,
Allen Li7f43ef92017-03-09 16:29:48 -08001588 job_keyvals=None,
1589 test_args=None
Allen Li3b1d4e52017-03-09 16:23:06 -08001590 ):
1591 """
1592 Constructor
1593
1594 @param predicates: A list of callables that accept ControlData
1595 representations of control files. A test will be
Allen Li2887e332017-03-09 16:30:36 -08001596 included in suite if all callables in this list
Allen Li3b1d4e52017-03-09 16:23:06 -08001597 return True on the given control file.
1598 @param tag: a string with which to tag jobs run in this suite.
1599 @param builds: the builds on which we're running this suite.
1600 @param board: the board on which we're running this suite.
1601 @param cf_getter: a control_file_getter.ControlFileGetter
1602 @param afe: an instance of AFE as defined in server/frontend.py.
1603 @param tko: an instance of TKO as defined in server/frontend.py.
1604 @param pool: Specify the pool of machines to use for scheduling
1605 purposes.
1606 @param run_prod_code: If true, the suite will run the test code that
1607 lives in prod aka the test code currently on the
1608 lab servers.
1609 @param results_dir: The directory where the job can write results to.
1610 This must be set if you want job_id of sub-jobs
1611 list in the job keyvals.
1612 @param max_runtime_mins: Maximum suite runtime, in minutes.
1613 @param timeout: Maximum job lifetime, in hours.
1614 @param suite_job_id: Job id that will act as parent id to all sub jobs.
1615 Default: None
1616 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
1617 attribute and skip applying of dependency labels.
1618 (Default:False)
1619 @param extra_deps: A list of strings which are the extra DEPENDENCIES
1620 to add to each test being scheduled.
1621 @param priority: Integer priority level. Higher is more important.
1622 @param wait_for_results: Set to False to run the suite job without
1623 waiting for test jobs to finish. Default is
1624 True.
1625 @param job_retry: A bool value indicating whether jobs should be retired
1626 on failure. If True, the field 'JOB_RETRIES' in
1627 control files will be respected. If False, do not
1628 retry.
1629 @param max_retries: Maximum retry limit at suite level.
1630 Regardless how many times each individual test
1631 has been retried, the total number of retries
1632 happening in the suite can't exceed _max_retries.
1633 Default to sys.maxint.
1634 @param offload_failures_only: Only enable gs_offloading for failed
1635 jobs.
1636 @param test_source_build: Build that contains the server-side test code.
1637 @param job_keyvals: General job keyvals to be inserted into keyval file,
1638 which will be used by tko/parse later.
Allen Li7f43ef92017-03-09 16:29:48 -08001639 @param test_args: A dict of args passed all the way to each individual
1640 test that will be actually ran.
Allen Li3b1d4e52017-03-09 16:23:06 -08001641
1642 """
Allen Li00bbe5b2017-03-09 16:44:30 -08001643 tests = find_and_parse_tests(
1644 cf_getter,
1645 _ComposedPredicate(predicates),
1646 tag,
1647 add_experimental=True,
1648 forgiving_parser=forgiving_parser,
1649 run_prod_code=run_prod_code,
1650 test_args=test_args,
1651 )
Allen Li3b1d4e52017-03-09 16:23:06 -08001652 super(Suite, self).__init__(
Allen Li00bbe5b2017-03-09 16:44:30 -08001653 tests=tests,
Allen Li3b1d4e52017-03-09 16:23:06 -08001654 tag=tag,
1655 builds=builds,
1656 board=board,
Allen Li3b1d4e52017-03-09 16:23:06 -08001657 afe=afe,
1658 tko=tko,
1659 pool=pool,
1660 results_dir=results_dir,
1661 max_runtime_mins=max_runtime_mins,
1662 timeout_mins=timeout_mins,
1663 file_bugs=file_bugs,
1664 file_experimental_bugs=file_experimental_bugs,
1665 suite_job_id=suite_job_id,
1666 ignore_deps=ignore_deps,
1667 extra_deps=extra_deps,
1668 priority=priority,
Allen Li3b1d4e52017-03-09 16:23:06 -08001669 wait_for_results=wait_for_results,
1670 job_retry=job_retry,
1671 max_retries=max_retries,
1672 offload_failures_only=offload_failures_only,
1673 test_source_build=test_source_build,
1674 job_keyvals=job_keyvals)
1675
Allen Li4b5a24f2017-03-09 16:01:35 -08001676
Allen Licec26f72017-03-09 16:39:09 -08001677class _ComposedPredicate(object):
1678 """Return the composition of the predicates.
1679
1680 Predicates are functions that take a test control data object and
1681 return True of that test is to be included. The returned
1682 predicate's set is the intersection of all of the input predicates'
1683 sets (it returns True if all predicates return True).
1684 """
1685
1686 def __init__(self, predicates):
1687 """Initialize instance.
1688
1689 @param predicates: Iterable of predicates.
1690 """
1691 self._predicates = list(predicates)
1692
1693 def __repr__(self):
1694 return '{cls}({this._predicates!r})'.format(
1695 cls=type(self).__qualname__,
1696 this=self,
1697 )
1698
1699 def __call__(self, control_data_):
1700 return all(f(control_data_) for f in self._predicates)
1701
1702
Allen Li9fcd4b42016-12-12 16:15:14 -08001703def _is_nonexistent_board_error(e):
1704 """Return True if error is caused by nonexistent board label.
1705
1706 As of this writing, the particular case we want looks like this:
1707
1708 1) e.problem_keys is a dictionary
1709 2) e.problem_keys['meta_hosts'] exists as the only key
1710 in the dictionary.
1711 3) e.problem_keys['meta_hosts'] matches this pattern:
1712 "Label "board:.*" not found"
1713
1714 We check for conditions 1) and 2) on the
1715 theory that they're relatively immutable.
1716 We don't check condition 3) because it seems
1717 likely to be a maintenance burden, and for the
1718 times when we're wrong, being right shouldn't
1719 matter enough (we _hope_).
1720
1721 @param e: proxy.ValidationError instance
1722 @returns: boolean
1723 """
1724 return (isinstance(e.problem_keys, dict)
1725 and len(e.problem_keys) == 1
1726 and 'meta_hosts' in e.problem_keys)