blob: f07bec2ec142d2d1c6bb1827b92de6ce94fc17d3 [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
Allen Lib47f59a2017-03-10 17:50:45 -0800530 def retrieve(self, test_name):
531 """Retrieve a test's control data.
532
533 This ignores forgiving_parser because we cannot return a
534 forgiving value.
535
536 @param test_name: Name of test to retrieve.
537
538 @raises ControlVariableException: There is a syntax error in a
539 control file.
540
541 @returns a ControlData object
542 """
543 path = self._cf_getter.get_control_file_path(test_name)
544 text = self._cf_getter.get_control_file_contents(path)
545 return self._parse_cf_text(path, text)
546
547
Allen Liaed93492017-03-14 13:36:26 -0700548 def retrieve_for_suite(self, suite_name=''):
549 """Scan through all tests and find all tests.
550
551 @param suite_name: If specified, this method will attempt to restrain
552 the search space to just this suite's control files.
Allen Li066f5872017-02-28 13:30:44 -0800553
Allen Li574fe4d2017-03-10 16:11:53 -0800554 @raises ControlVariableException: If forgiving_parser is False and there
555 is a syntax error in a control file.
556
557 @returns a dictionary of ControlData objects that based on given
558 parameters.
559 """
Allen Li73702152017-03-10 17:44:33 -0800560 control_file_texts = self._get_cf_texts_for_suite(suite_name)
561 return self._parse_cf_text_many(control_file_texts)
Allen Li9d0be122017-02-28 14:13:04 -0800562
563
Allen Li374c1b62017-03-10 16:29:02 -0800564 def _filter_cf_paths(self, paths):
565 """Remove certain control file paths
566
567 @param paths: Iterable of paths
568 @returns: generator yielding paths
569 """
570 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
571 return (path for path in paths if not matcher.match(path))
572
573
Allen Li73702152017-03-10 17:44:33 -0800574 def _get_cf_texts_for_suite(self, suite_name):
Allen Li3466ae82017-03-10 16:36:38 -0800575 """Get control file content for given suite.
576
577 @param suite_name: If specified, this method will attempt to restrain
578 the search space to just this suite's control files.
Allen Li975c9522017-03-10 16:40:52 -0800579 @returns: generator yielding (path, text) tuples
Allen Li3466ae82017-03-10 16:36:38 -0800580 """
Allen Li1abded52017-03-10 16:37:57 -0800581 files = self._cf_getter.get_control_file_list(suite_name=suite_name)
582 filtered_files = self._filter_cf_paths(files)
Allen Li975c9522017-03-10 16:40:52 -0800583 for path in filtered_files:
584 yield path, self._cf_getter.get_control_file_contents(path)
Allen Li3466ae82017-03-10 16:36:38 -0800585
586
Allen Li73702152017-03-10 17:44:33 -0800587 def _parse_cf_text_many(self, control_file_texts):
Allen Li15e95602017-03-10 16:58:49 -0800588 """Parse control file texts.
589
590 @param control_file_texts: iterable of (path, text) pairs
Allen Li15e95602017-03-10 16:58:49 -0800591 @returns: a dictionary of ControlData objects
592 """
593 tests = {}
594 for path, text in control_file_texts:
595 # Seed test_args into the control file.
Allen Liaed93492017-03-14 13:36:26 -0700596 if self._test_args:
597 text = tools.inject_vars(self._test_args, text)
Allen Li15e95602017-03-10 16:58:49 -0800598 try:
Allen Li73702152017-03-10 17:44:33 -0800599 found_test = self._parse_cf_text(path, text)
Allen Li15e95602017-03-10 16:58:49 -0800600 except control_data.ControlVariableException, e:
Allen Liaed93492017-03-14 13:36:26 -0700601 if not self._forgiving_parser:
Allen Li15e95602017-03-10 16:58:49 -0800602 msg = "Failed parsing %s\n%s" % (path, e)
603 raise control_data.ControlVariableException(msg)
604 logging.warning("Skipping %s\n%s", path, e)
605 except Exception, e:
606 logging.error("Bad %s\n%s", path, e)
607 else:
Allen Li15e95602017-03-10 16:58:49 -0800608 tests[path] = found_test
609 return tests
610
611
Allen Li73702152017-03-10 17:44:33 -0800612 def _parse_cf_text(self, path, text):
Allen Lif8b0b702017-03-10 17:42:11 -0800613 """Parse control file text.
614
615 This ignores forgiving_parser because we cannot return a
616 forgiving value.
617
618 @param path: path to control file
619 @param text: control file text contents
620 @returns: a ControlData object
Allen Lib47f59a2017-03-10 17:50:45 -0800621
622 @raises ControlVariableException: There is a syntax error in a
623 control file.
Allen Lif8b0b702017-03-10 17:42:11 -0800624 """
625 test = control_data.parse_control_string(
626 text, raise_warnings=True, path=path)
627 test.text = text
628 if self._run_prod_code:
629 test.require_ssp = False
630 return test
631
632
Allen Lida62c612017-03-10 16:32:38 -0800633class _BatchControlFileRetriever(_ControlFileRetriever):
634 """Subclass that can retrieve suite control files in batch."""
635
636
Allen Li73702152017-03-10 17:44:33 -0800637 def _get_cf_texts_for_suite(self, suite_name):
Allen Li1abded52017-03-10 16:37:57 -0800638 """Get control file content for given suite.
639
640 @param suite_name: If specified, this method will attempt to restrain
641 the search space to just this suite's control files.
Allen Li975c9522017-03-10 16:40:52 -0800642 @returns: generator yielding (path, text) tuples
Allen Li1abded52017-03-10 16:37:57 -0800643 """
644 suite_info = self._cf_getter.get_suite_info(suite_name=suite_name)
645 files = suite_info.keys()
646 filtered_files = self._filter_cf_paths(files)
Allen Li975c9522017-03-10 16:40:52 -0800647 for path in filtered_files:
Allen Li1abded52017-03-10 16:37:57 -0800648 yield path, suite_info[path]
649
650
Allen Li1865f632017-03-09 15:58:52 -0800651def get_test_source_build(builds, **dargs):
652 """Get the build of test code.
653
654 Get the test source build from arguments. If parameter
655 `test_source_build` is set and has a value, return its value. Otherwise
656 returns the ChromeOS build name if it exists. If ChromeOS build is not
657 specified either, raise SuiteArgumentException.
658
659 @param builds: the builds on which we're running this suite. It's a
660 dictionary of version_prefix:build.
661 @param **dargs: Any other Suite constructor parameters, as described
662 in Suite.__init__ docstring.
663
664 @return: The build contains the test code.
665 @raise: SuiteArgumentException if both test_source_build and ChromeOS
666 build are not specified.
667
668 """
669 if dargs.get('test_source_build', None):
670 return dargs['test_source_build']
671 test_source_build = builds.get(provision.CROS_VERSION_PREFIX, None)
672 if not test_source_build:
673 raise error.SuiteArgumentException(
674 'test_source_build must be specified if CrOS build is not '
675 'specified.')
676 return test_source_build
677
678
Allen Li122cd092017-03-09 15:56:46 -0800679def list_all_suites(build, devserver, cf_getter=None):
680 """
681 Parses all ControlData objects with a SUITE tag and extracts all
682 defined suite names.
683
684 @param build: the build on which we're running this suite.
685 @param devserver: the devserver which contains the build.
686 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
687 using DevServerGetter.
688
689 @return list of suites
690 """
691 if cf_getter is None:
692 cf_getter = _create_ds_getter(build, devserver)
693
694 suites = set()
695 predicate = lambda t: True
696 for test in find_and_parse_tests(cf_getter, predicate,
697 add_experimental=True):
698 suites.update(test.suite_tag_parts)
699 return list(suites)
700
701
Allen Lid1806ac2017-03-09 15:52:33 -0800702def test_file_similarity_predicate(test_file_pattern):
703 """Returns predicate that gets the similarity based on a test's file
704 name pattern.
705
706 Builds a predicate that takes in a parsed control file (a ControlData)
707 and returns a tuple of (file path, ratio), where ratio is the
708 similarity between the test file name and the given test_file_pattern.
709
710 @param test_file_pattern: regular expression (string) to match against
711 control file names.
712 @return a callable that takes a ControlData and and returns a tuple of
713 (file path, ratio), where ratio is the similarity between the
714 test file name and the given test_file_pattern.
715 """
716 return lambda t: ((None, 0) if not hasattr(t, 'path') else
717 (t.path, difflib.SequenceMatcher(a=t.path,
718 b=test_file_pattern).ratio()))
719
720
Allen Lib5b4a7a2017-03-09 15:50:09 -0800721def test_name_similarity_predicate(test_name):
722 """Returns predicate that matched based on a test's name.
723
724 Builds a predicate that takes in a parsed control file (a ControlData)
725 and returns a tuple of (test name, ratio), where ratio is the similarity
726 between the test name and the given test_name.
727
728 @param test_name: the test name to base the predicate on.
729 @return a callable that takes a ControlData and returns a tuple of
730 (test name, ratio), where ratio is the similarity between the
731 test name and the given test_name.
732 """
733 return lambda t: ((None, 0) if not hasattr(t, 'name') else
734 (t.name,
735 difflib.SequenceMatcher(a=t.name, b=test_name).ratio()))
736
737
Allen Lie37d6ba2017-03-09 15:49:25 -0800738def matches_attribute_expression_predicate(test_attr_boolstr):
739 """Returns predicate that matches based on boolean expression of
740 attributes.
741
742 Builds a predicate that takes in a parsed control file (a ControlData)
743 ans returns True if the test attributes satisfy the given attribute
744 boolean expression.
745
746 @param test_attr_boolstr: boolean expression of the attributes to be
747 test, like 'system:all and interval:daily'.
748
749 @return a callable that takes a ControlData and returns True if the test
750 attributes satisfy the given boolean expression.
751 """
752 return lambda t: boolparse_lib.BoolstrResult(
753 test_attr_boolstr, t.attributes)
754
755
Allen Lif29b48a2017-03-09 15:48:41 -0800756def test_file_matches_pattern_predicate(test_file_pattern):
757 """Returns predicate that matches based on a test's file name pattern.
758
759 Builds a predicate that takes in a parsed control file (a ControlData)
760 and returns True if the test's control file name matches the given
761 regular expression.
762
763 @param test_file_pattern: regular expression (string) to match against
764 control file names.
765 @return a callable that takes a ControlData and and returns
766 True if control file name matches the pattern.
767 """
768 return lambda t: hasattr(t, 'path') and re.match(test_file_pattern,
769 t.path)
770
771
Allen Li1819f522017-03-09 15:47:25 -0800772def test_name_matches_pattern_predicate(test_name_pattern):
773 """Returns predicate that matches based on a test's name pattern.
774
775 Builds a predicate that takes in a parsed control file (a ControlData)
776 and returns True if the test name matches the given regular expression.
777
778 @param test_name_pattern: regular expression (string) to match against
779 test names.
780 @return a callable that takes a ControlData and returns
781 True if the name fields matches the pattern.
782 """
783 return lambda t: hasattr(t, 'name') and re.match(test_name_pattern,
784 t.name)
785
786
Allen Lif8441c82017-03-09 15:46:32 -0800787def test_name_equals_predicate(test_name):
788 """Returns predicate that matched based on a test's name.
789
790 Builds a predicate that takes in a parsed control file (a ControlData)
791 and returns True if the test name is equal to |test_name|.
792
793 @param test_name: the test name to base the predicate on.
794 @return a callable that takes a ControlData and looks for |test_name|
795 in that ControlData's name.
796 """
797 return lambda t: hasattr(t, 'name') and test_name == t.name
798
799
Allen Li6e2fa4f2017-03-09 15:45:43 -0800800def name_in_tag_similarity_predicate(name):
801 """Returns predicate that takes a control file and gets the similarity
802 of the suites in the control file and the given name.
803
804 Builds a predicate that takes in a parsed control file (a ControlData)
805 and returns a list of tuples of (suite name, ratio), where suite name
806 is each suite listed in the control file, and ratio is the similarity
807 between each suite and the given name.
808
809 @param name: the suite name to base the predicate on.
810 @return a callable that takes a ControlData and returns a list of tuples
811 of (suite name, ratio), where suite name is each suite listed in
812 the control file, and ratio is the similarity between each suite
813 and the given name.
814 """
815 return lambda t: [(suite,
816 difflib.SequenceMatcher(a=suite, b=name).ratio())
817 for suite in t.suite_tag_parts] or [(None, 0)]
818
819
Allen Li398ddbd2017-03-09 15:44:25 -0800820def name_in_tag_predicate(name):
821 """Returns predicate that takes a control file and looks for |name|.
822
823 Builds a predicate that takes in a parsed control file (a ControlData)
824 and returns True if the SUITE tag is present and contains |name|.
825
826 @param name: the suite name to base the predicate on.
827 @return a callable that takes a ControlData and looks for |name| in that
828 ControlData object's suite member.
829 """
830 return lambda t: name in t.suite_tag_parts
831
832
Allen Lia640d6d2017-03-09 15:41:35 -0800833def create_fs_getter(autotest_dir):
834 """
835 @param autotest_dir: the place to find autotests.
836 @return a FileSystemGetter instance that looks under |autotest_dir|.
837 """
838 # currently hard-coded places to look for tests.
839 subpaths = ['server/site_tests', 'client/site_tests',
840 'server/tests', 'client/tests']
841 directories = [os.path.join(autotest_dir, p) for p in subpaths]
842 return control_file_getter.FileSystemGetter(directories)
843
844
Allen Li0f915872017-02-28 18:51:04 -0800845def _create_ds_getter(build, devserver):
846 """
847 @param build: the build on which we're running this suite.
848 @param devserver: the devserver which contains the build.
849 @return a FileSystemGetter instance that looks under |autotest_dir|.
850 """
851 return control_file_getter.DevServerGetter(build, devserver)
852
853
Allen Li3adae952017-03-10 17:18:12 -0800854def _non_experimental_tests_predicate(test_data):
855 """Test predicate for non-experimental tests."""
856 return not test_data.experimental
857
858
Allen Li0b1fa382017-02-28 18:47:16 -0800859def find_and_parse_tests(cf_getter, predicate, suite_name='',
860 add_experimental=False, forgiving_parser=True,
861 run_prod_code=False, test_args=None):
862 """
863 Function to scan through all tests and find eligible tests.
864
865 Search through all tests based on given cf_getter, suite_name,
866 add_experimental and forgiving_parser, return the tests that match
867 given predicate.
868
869 @param cf_getter: a control_file_getter.ControlFileGetter used to list
870 and fetch the content of control files
871 @param predicate: a function that should return True when run over a
872 ControlData representation of a control file that should be in
873 this Suite.
874 @param suite_name: If specified, this method will attempt to restrain
875 the search space to just this suite's control files.
876 @param add_experimental: add tests with experimental attribute set.
877 @param forgiving_parser: If False, will raise ControlVariableExceptions
878 if any are encountered when parsing control
879 files. Note that this can raise an exception
880 for syntax errors in unrelated files, because
881 we parse them before applying the predicate.
882 @param run_prod_code: If true, the suite will run the test code that
883 lives in prod aka the test code currently on the
884 lab servers by disabling SSP for the discovered
885 tests.
886 @param test_args: A dict of args to be seeded in test control file.
887
888 @raises ControlVariableException: If forgiving_parser is False and there
889 is a syntax error in a control file.
890
891 @return list of ControlData objects that should be run, with control
892 file text added in |text| attribute. Results are sorted based
893 on the TIME setting in control file, slowest test comes first.
894 """
Allen Libb60f442017-03-14 12:18:57 -0700895 logging.debug('Getting control file list for suite: %s', suite_name)
Allen Liaed93492017-03-14 13:36:26 -0700896 retriever = _get_cf_retriever(cf_getter,
897 forgiving_parser=forgiving_parser,
898 run_prod_code=run_prod_code,
899 test_args=test_args)
900 tests = retriever.retrieve_for_suite(suite_name)
Allen Li0b1fa382017-02-28 18:47:16 -0800901 logging.debug('Parsed %s control files.', len(tests))
Allen Li3adae952017-03-10 17:18:12 -0800902 if not add_experimental:
903 predicate = _ComposedPredicate([predicate,
904 _non_experimental_tests_predicate])
Allen Li0b1fa382017-02-28 18:47:16 -0800905 tests = [test for test in tests.itervalues() if predicate(test)]
906 tests.sort(key=lambda t:
907 control_data.ControlData.get_test_time_index(t.time),
908 reverse=True)
909 return tests
910
911
Allen Lida012192017-02-28 18:37:52 -0800912def find_possible_tests(cf_getter, predicate, suite_name='', count=10):
913 """
914 Function to scan through all tests and find possible tests.
915
916 Search through all tests based on given cf_getter, suite_name,
917 add_experimental and forgiving_parser. Use the given predicate to
918 calculate the similarity and return the top 10 matches.
919
920 @param cf_getter: a control_file_getter.ControlFileGetter used to list
921 and fetch the content of control files
922 @param predicate: a function that should return a tuple of (name, ratio)
923 when run over a ControlData representation of a control file that
924 should be in this Suite. `name` is the key to be compared, e.g.,
925 a suite name or test name. `ratio` is a value between [0,1]
926 indicating the similarity of `name` and the value to be compared.
927 @param suite_name: If specified, this method will attempt to restrain
928 the search space to just this suite's control files.
929 @param count: Number of suggestions to return, default to 10.
930
931 @return list of top names that similar to the given test, sorted by
932 match ratio.
933 """
Allen Libb60f442017-03-14 12:18:57 -0700934 logging.debug('Getting control file list for suite: %s', suite_name)
Allen Li86468342017-03-10 17:12:01 -0800935 tests = _get_cf_retriever(cf_getter).retrieve_for_suite(suite_name)
Allen Lida012192017-02-28 18:37:52 -0800936 logging.debug('Parsed %s control files.', len(tests))
937 similarities = {}
938 for test in tests.itervalues():
939 ratios = predicate(test)
940 # Some predicates may return a list of tuples, e.g.,
941 # name_in_tag_similarity_predicate. Convert all returns to a list.
942 if not isinstance(ratios, list):
943 ratios = [ratios]
944 for name, ratio in ratios:
945 similarities[name] = ratio
946 return [s[0] for s in
947 sorted(similarities.items(), key=operator.itemgetter(1),
948 reverse=True)][:count]
949
950
Allen Li98a26a42017-02-28 18:43:24 -0800951def _deprecated_suite_method(func):
952 """Decorator for deprecated Suite static methods.
953
954 TODO(ayatane): This is used to decorate functions that are called as
955 static methods on Suite.
956 """
957 @functools.wraps(func)
958 def wrapper(*args, **kwargs):
959 warnings.warn('Calling this method from Suite is deprecated')
960 return func(*args, **kwargs)
961 return staticmethod(wrapper)
962
963
Allen Li4b5a24f2017-03-09 16:01:35 -0800964class _BaseSuite(object):
Chris Masone44e4d6c2012-08-15 14:25:53 -0700965 """
966 A suite of tests, defined by some predicate over control file variables.
967
968 Given a place to search for control files a predicate to match the desired
969 tests, can gather tests and fire off jobs to run them, and then wait for
970 results.
971
972 @var _predicate: a function that should return True when run over a
973 ControlData representation of a control file that should be in
974 this Suite.
975 @var _tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -0700976 @var _builds: the builds on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -0700977 @var _afe: an instance of AFE as defined in server/frontend.py.
978 @var _tko: an instance of TKO as defined in server/frontend.py.
979 @var _jobs: currently scheduled jobs, if any.
Fang Denge3bc24b2014-03-17 15:19:46 -0700980 @var _jobs_to_tests: a dictionary that maps job ids to tests represented
981 ControlData objects.
Fang Denge3bc24b2014-03-17 15:19:46 -0700982 @var _retry: a bool value indicating whether jobs should be retried on
983 failure.
984 @var _retry_handler: a RetryHandler object.
985
Chris Masone44e4d6c2012-08-15 14:25:53 -0700986 """
987
Dan Shi36cfd832014-10-10 13:38:51 -0700988
Allen Li6fff5502016-12-09 18:04:26 -0800989 def __init__(
990 self,
Allen Li00bbe5b2017-03-09 16:44:30 -0800991 tests,
Allen Li6fff5502016-12-09 18:04:26 -0800992 tag,
993 builds,
994 board,
Allen Li6fff5502016-12-09 18:04:26 -0800995 afe=None,
996 tko=None,
997 pool=None,
998 results_dir=None,
999 max_runtime_mins=24*60,
1000 timeout_mins=24*60,
1001 file_bugs=False,
1002 file_experimental_bugs=False,
1003 suite_job_id=None,
1004 ignore_deps=False,
Allen Li493eefa2016-12-09 18:05:35 -08001005 extra_deps=None,
Allen Li6fff5502016-12-09 18:04:26 -08001006 priority=priorities.Priority.DEFAULT,
Allen Li6fff5502016-12-09 18:04:26 -08001007 wait_for_results=True,
1008 job_retry=False,
1009 max_retries=sys.maxint,
1010 offload_failures_only=False,
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001011 test_source_build=None,
Allen Li7f43ef92017-03-09 16:29:48 -08001012 job_keyvals=None
Allen Li6fff5502016-12-09 18:04:26 -08001013 ):
Allen Li7f43ef92017-03-09 16:29:48 -08001014 """Initialize instance.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001015
Allen Li00bbe5b2017-03-09 16:44:30 -08001016 @param tests: Iterable of tests to run.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001017 @param tag: a string with which to tag jobs run in this suite.
Dan Shi36cfd832014-10-10 13:38:51 -07001018 @param builds: the builds on which we're running this suite.
Alex Millera0913072013-06-12 10:01:51 -07001019 @param board: the board on which we're running this suite.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001020 @param afe: an instance of AFE as defined in server/frontend.py.
1021 @param tko: an instance of TKO as defined in server/frontend.py.
1022 @param pool: Specify the pool of machines to use for scheduling
1023 purposes.
1024 @param results_dir: The directory where the job can write results to.
1025 This must be set if you want job_id of sub-jobs
1026 list in the job keyvals.
Aviv Keshet18308922013-02-19 17:49:49 -08001027 @param max_runtime_mins: Maximum suite runtime, in minutes.
Alex Miller028b0312013-09-07 15:25:45 -07001028 @param timeout: Maximum job lifetime, in hours.
Aviv Keshet18308922013-02-19 17:49:49 -08001029 @param suite_job_id: Job id that will act as parent id to all sub jobs.
1030 Default: None
Aviv Keshetd7959f32013-05-17 15:58:43 -07001031 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
1032 attribute and skip applying of dependency labels.
1033 (Default:False)
Alex Miller47a03672013-08-27 09:09:53 -07001034 @param extra_deps: A list of strings which are the extra DEPENDENCIES
1035 to add to each test being scheduled.
Alex Miller7d658cf2013-09-04 16:00:35 -07001036 @param priority: Integer priority level. Higher is more important.
Dan Shi95122412013-11-12 16:20:33 -08001037 @param wait_for_results: Set to False to run the suite job without
1038 waiting for test jobs to finish. Default is
1039 True.
Fang Denge3bc24b2014-03-17 15:19:46 -07001040 @param job_retry: A bool value indicating whether jobs should be retired
1041 on failure. If True, the field 'JOB_RETRIES' in
1042 control files will be respected. If False, do not
1043 retry.
Fang Deng443f1952015-01-02 14:51:49 -08001044 @param max_retries: Maximum retry limit at suite level.
1045 Regardless how many times each individual test
1046 has been retried, the total number of retries
1047 happening in the suite can't exceed _max_retries.
1048 Default to sys.maxint.
Simran Basi1e10e922015-04-16 15:09:56 -07001049 @param offload_failures_only: Only enable gs_offloading for failed
1050 jobs.
Dan Shi36cfd832014-10-10 13:38:51 -07001051 @param test_source_build: Build that contains the server-side test code.
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001052 @param job_keyvals: General job keyvals to be inserted into keyval file,
1053 which will be used by tko/parse later.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001054 """
Allen Li493eefa2016-12-09 18:05:35 -08001055
Allen Li00bbe5b2017-03-09 16:44:30 -08001056 self.tests = list(tests)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001057 self._tag = tag
Dan Shi36cfd832014-10-10 13:38:51 -07001058 self._builds = builds
Chris Masone44e4d6c2012-08-15 14:25:53 -07001059 self._results_dir = results_dir
1060 self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
1061 delay_sec=10,
1062 debug=False)
1063 self._tko = tko or frontend_wrappers.RetryingTKO(timeout_min=30,
1064 delay_sec=10,
1065 debug=False)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001066 self._jobs = []
Fang Denge3bc24b2014-03-17 15:19:46 -07001067 self._jobs_to_tests = {}
beeps89f1e062013-09-18 12:00:17 -07001068
Alex Millera3a4fe72013-01-22 09:57:47 -08001069 self._file_bugs = file_bugs
beepsda5b7112013-05-30 11:34:14 -07001070 self._file_experimental_bugs = file_experimental_bugs
Aviv Keshet18308922013-02-19 17:49:49 -08001071 self._suite_job_id = suite_job_id
Fang Denge3bc24b2014-03-17 15:19:46 -07001072 self._job_retry=job_retry
Fang Deng443f1952015-01-02 14:51:49 -08001073 self._max_retries = max_retries
Fang Denge3bc24b2014-03-17 15:19:46 -07001074 # RetryHandler to be initialized in schedule()
1075 self._retry_handler = None
Dan Shi95122412013-11-12 16:20:33 -08001076 self.wait_for_results = wait_for_results
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001077 self._job_keyvals = job_keyvals
Alex Millera3a4fe72013-01-22 09:57:47 -08001078
Allen Li80dc02c2017-02-28 18:22:16 -08001079 if extra_deps is None:
1080 extra_deps = []
Allen Li3a83fe62017-02-28 18:27:09 -08001081 extra_deps.append(board)
Allen Licceb1832017-02-28 18:25:06 -08001082 if pool:
1083 extra_deps.append(pool)
Allen Li010c0412017-03-29 17:31:35 -07001084 self._job_creator = _SuiteChildJobCreator(
Allen Li55de3402017-03-29 17:48:46 -07001085 tag=tag,
Allen Li27f72a22017-03-29 17:37:43 -07001086 builds=builds,
Allen Li010c0412017-03-29 17:31:35 -07001087 board=board,
Allen Li388b7a12017-03-29 17:58:23 -07001088 afe=afe,
Allen Li388b7a12017-03-29 17:58:23 -07001089 max_runtime_mins=max_runtime_mins,
1090 timeout_mins=timeout_mins,
Allen Li55de3402017-03-29 17:48:46 -07001091 suite_job_id=suite_job_id,
Allen Li010c0412017-03-29 17:31:35 -07001092 ignore_deps=ignore_deps,
1093 extra_deps=extra_deps,
Allen Li388b7a12017-03-29 17:58:23 -07001094 priority=priority,
Allen Li55de3402017-03-29 17:48:46 -07001095 offload_failures_only=offload_failures_only,
1096 test_source_build=test_source_build,
Allen Li010c0412017-03-29 17:31:35 -07001097 )
Allen Lida198fd2017-03-29 17:22:13 -07001098
Chris Masone44e4d6c2012-08-15 14:25:53 -07001099
Fang Denge3bc24b2014-03-17 15:19:46 -07001100 def _schedule_test(self, record, test, retry_for=None, ignore_errors=False):
1101 """Schedule a single test and return the job.
1102
Allen Lie79b3cb2016-12-12 18:24:17 -08001103 Schedule a single test by creating a job, and then update relevant
1104 data structures that are used to keep track of all running jobs.
Fang Denge3bc24b2014-03-17 15:19:46 -07001105
Allen Lie79b3cb2016-12-12 18:24:17 -08001106 Emits a TEST_NA status log entry if it failed to schedule the test due
1107 to NoEligibleHostException or a non-existent board label.
1108
1109 Returns a frontend.Job object if the test is successfully scheduled.
1110 If scheduling failed due to NoEligibleHostException or a non-existent
1111 board label, returns None. If ignore_errors is True, all unknown
1112 errors return None, otherwise the errors are raised as-is.
Fang Denge3bc24b2014-03-17 15:19:46 -07001113
1114 @param record: A callable to use for logging.
1115 prototype: record(base_job.status_log_entry)
1116 @param test: ControlData for a test to run.
1117 @param retry_for: If we are scheduling a test to retry an
1118 old job, the afe_job_id of the old job
1119 will be passed in as |retry_for|.
1120 @param ignore_errors: If True, when an rpc error occur, ignore
1121 the error and will return None.
1122 If False, rpc errors will be raised.
1123
Allen Lie79b3cb2016-12-12 18:24:17 -08001124 @returns: A frontend.Job object or None
Fang Denge3bc24b2014-03-17 15:19:46 -07001125 """
1126 msg = 'Scheduling %s' % test.name
1127 if retry_for:
1128 msg = msg + ', to retry afe job %d' % retry_for
1129 logging.debug(msg)
Dan Shidfea3682014-08-10 23:38:40 -07001130 begin_time_str = datetime.datetime.now().strftime(time_utils.TIME_FMT)
Fang Denge3bc24b2014-03-17 15:19:46 -07001131 try:
Allen Li388b7a12017-03-29 17:58:23 -07001132 job = self._job_creator.create_job(test, retry_for=retry_for)
Allen Li6fd440f2016-12-12 18:40:05 -08001133 except (error.NoEligibleHostException, proxy.ValidationError) as e:
1134 if (isinstance(e, error.NoEligibleHostException)
1135 or (isinstance(e, proxy.ValidationError)
1136 and _is_nonexistent_board_error(e))):
1137 # Treat a dependency on a non-existent board label the same as
1138 # a dependency on a board that exists, but for which there's no
1139 # hardware.
1140 logging.debug('%s not applicable for this board/pool. '
1141 'Emitting TEST_NA.', test.name)
1142 Status('TEST_NA', test.name,
1143 'Skipping: test not supported on this board/pool.',
Allen Li9fcd4b42016-12-12 16:15:14 -08001144 begin_time_str=begin_time_str).record_all(record)
1145 return None
1146 else:
Fang Denge3bc24b2014-03-17 15:19:46 -07001147 raise e
Fang Denge3bc24b2014-03-17 15:19:46 -07001148 except (error.RPCException, proxy.JSONRPCException) as e:
1149 if retry_for:
1150 # Mark that we've attempted to retry the old job.
1151 self._retry_handler.set_attempted(job_id=retry_for)
Allen Li0ba59342016-12-12 15:57:02 -08001152
Fang Denge3bc24b2014-03-17 15:19:46 -07001153 if ignore_errors:
1154 logging.error('Failed to schedule test: %s, Reason: %s',
1155 test.name, e)
Allen Li0ba59342016-12-12 15:57:02 -08001156 return None
Fang Denge3bc24b2014-03-17 15:19:46 -07001157 else:
1158 raise e
1159 else:
1160 self._jobs.append(job)
1161 self._jobs_to_tests[job.id] = test
1162 if retry_for:
1163 # A retry job was just created, record it.
1164 self._retry_handler.add_retry(
1165 old_job_id=retry_for, new_job_id=job.id)
1166 retry_count = (test.job_retries -
1167 self._retry_handler.get_retry_max(job.id))
1168 logging.debug('Job %d created to retry job %d. '
1169 'Have retried for %d time(s)',
1170 job.id, retry_for, retry_count)
Allen Li4df053e2016-12-29 16:05:41 -08001171 self._remember_job_keyval(job)
Fang Denge3bc24b2014-03-17 15:19:46 -07001172 return job
Fang Denge3bc24b2014-03-17 15:19:46 -07001173
1174
Alex Miller3a69adc2012-12-19 13:38:31 -08001175 def schedule(self, record, add_experimental=True):
Aviv Keshet18308922013-02-19 17:49:49 -08001176 #pylint: disable-msg=C0111
Chris Masone44e4d6c2012-08-15 14:25:53 -07001177 """
1178 Schedule jobs using |self._afe|.
1179
1180 frontend.Job objects representing each scheduled job will be put in
1181 |self._jobs|.
1182
Fang Denge3bc24b2014-03-17 15:19:46 -07001183 @param record: A callable to use for logging.
1184 prototype: record(base_job.status_log_entry)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001185 @param add_experimental: schedule experimental tests as well, or not.
Aviv Keshete9170d92013-07-19 11:20:45 -07001186 @returns: The number of tests that were scheduled.
Chris Masone44e4d6c2012-08-15 14:25:53 -07001187 """
Allen Lif4cb5ec2017-01-03 16:58:12 -08001188 scheduled_test_names = []
Allen Li86f8c282017-02-28 13:09:40 -08001189 test_filter = _ExperimentalTestFilter(
Allen Lif4cb5ec2017-01-03 16:58:12 -08001190 tests=self.tests,
1191 add_experimental=add_experimental)
1192 logging.debug('Discovered %d stable tests.',
Allen Li86f8c282017-02-28 13:09:40 -08001193 len(test_filter.stable_tests))
Alex Miller3a69adc2012-12-19 13:38:31 -08001194 logging.debug('Discovered %d unstable tests.',
Allen Li86f8c282017-02-28 13:09:40 -08001195 len(test_filter.unstable_tests))
Chris Masone44e4d6c2012-08-15 14:25:53 -07001196
Alex Miller3a69adc2012-12-19 13:38:31 -08001197 Status('INFO', 'Start %s' % self._tag).record_result(record)
1198 try:
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001199 # Write job_keyvals into keyval file.
1200 if self._job_keyvals:
1201 utils.write_keyval(self._results_dir, self._job_keyvals)
1202
Prathmesh Prabhu7295bf32017-06-08 10:44:52 -07001203 tests = test_filter.get_tests_to_schedule()
1204 # TODO(crbug.com/730885): This is a hack to protect tests that are
1205 # not usually retried from getting hit by a provision error when run
1206 # as part of a suite. Remove this hack once provision is separated
1207 # out in its own suite.
1208 self._bump_up_test_retries(tests)
1209 for test in tests:
Allen Lida905732016-12-12 15:49:16 -08001210 scheduled_job = self._schedule_test(record, test)
1211 if scheduled_job is not None:
Shuqian Zhaocd866f32016-11-29 20:14:34 -08001212 scheduled_test_names.append(test.name)
1213
1214 # Write the num of scheduled tests and name of them to keyval file.
Shuqian Zhaocd866f32016-11-29 20:14:34 -08001215 logging.debug('Scheduled %d tests, writing the total to keyval.',
Allen Lia4d35022016-12-12 15:42:10 -08001216 len(scheduled_test_names))
Allen Lid4d5dda2016-12-12 15:39:11 -08001217 utils.write_keyval(
1218 self._results_dir,
Allen Lidda59b82016-12-12 18:20:04 -08001219 self._make_scheduled_tests_keyvals(scheduled_test_names))
Alex Miller3a69adc2012-12-19 13:38:31 -08001220 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001221 logging.exception('Exception while scheduling suite')
Alex Miller3a69adc2012-12-19 13:38:31 -08001222 Status('FAIL', self._tag,
1223 'Exception while scheduling suite').record_result(record)
1224
Fang Deng7e655a92014-05-23 13:48:11 -07001225 if self._job_retry:
1226 self._retry_handler = RetryHandler(
Fang Deng443f1952015-01-02 14:51:49 -08001227 initial_jobs_to_tests=self._jobs_to_tests,
1228 max_retries=self._max_retries)
Allen Lia4d35022016-12-12 15:42:10 -08001229 return len(scheduled_test_names)
Aviv Keshete9170d92013-07-19 11:20:45 -07001230
Alex Miller3a69adc2012-12-19 13:38:31 -08001231
Prathmesh Prabhu7295bf32017-06-08 10:44:52 -07001232 def _bump_up_test_retries(self, tests):
1233 """Bump up individual test retries to match suite retry options."""
1234 if not self._job_retry:
1235 return
1236
1237 for test in tests:
1238 if not test.job_retries:
1239 logging.debug(
1240 'Test %s requested no retries, but suite requires '
1241 'retries. Bumping retries up to 1. '
1242 '(See crbug.com/730885)',
1243 test.name)
1244 test.job_retries = 1
1245
1246
Allen Lidda59b82016-12-12 18:20:04 -08001247 def _make_scheduled_tests_keyvals(self, scheduled_test_names):
1248 """Make a keyvals dict to write for scheduled test names.
1249
1250 @param scheduled_test_names: A list of scheduled test name strings.
1251
1252 @returns: A keyvals dict.
1253 """
1254 return {
1255 constants.SCHEDULED_TEST_COUNT_KEY: len(scheduled_test_names),
1256 constants.SCHEDULED_TEST_NAMES_KEY: repr(scheduled_test_names),
1257 }
1258
1259
Allen Lid1cbccf2016-12-29 15:12:39 -08001260 def _should_report(self, result):
beepsda5b7112013-05-30 11:34:14 -07001261 """
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -07001262 Returns True if this failure requires to be reported.
beepsda5b7112013-05-30 11:34:14 -07001263
1264 @param result: A result, encapsulating the status of the failed job.
Shuqian Zhaoe33ba4a2015-09-11 18:51:43 -07001265 @return: True if we should report this failure.
beepsda5b7112013-05-30 11:34:14 -07001266 """
Allen Licc752292017-01-03 12:44:39 -08001267 if self._has_retry(result):
Fang Denge3bc24b2014-03-17 15:19:46 -07001268 return False
1269
beepsbeefc062013-08-02 11:17:09 -07001270 is_not_experimental = (
1271 constants.EXPERIMENTAL_PREFIX not in result._test_name and
1272 constants.EXPERIMENTAL_PREFIX not in result._job_name)
1273
Alex Millerfcc119b2014-01-15 13:54:58 -08001274 return (self._file_bugs and result.test_executed and
beepsbeefc062013-08-02 11:17:09 -07001275 (is_not_experimental or self._file_experimental_bugs) and
Fang Dengd82c1c72014-07-29 10:43:01 -07001276 not result.is_testna() and
beeps32fa6772014-01-28 13:19:53 -08001277 result.is_worse_than(job_status.Status('GOOD', '', 'reason')))
beepsda5b7112013-05-30 11:34:14 -07001278
1279
Allen Licc752292017-01-03 12:44:39 -08001280 def _has_retry(self, result):
1281 """
1282 Return True if this result gets to retry.
1283
1284 @param result: A result, encapsulating the status of the failed job.
1285 @return: bool
1286 """
1287 return (self._job_retry
1288 and self._retry_handler.has_following_retry(result))
1289
1290
Allen Li18503452016-12-29 14:56:48 -08001291 def wait(self, record, bug_template=None):
Alex Miller3a69adc2012-12-19 13:38:31 -08001292 """
1293 Polls for the job statuses, using |record| to print status when each
1294 completes.
1295
1296 @param record: callable that records job status.
1297 prototype:
1298 record(base_job.status_log_entry)
beepsc8a875b2013-03-25 10:20:38 -07001299 @param bug_template: A template dictionary specifying the default bug
1300 filing options for failures in this suite.
Alex Miller3a69adc2012-12-19 13:38:31 -08001301 """
Dan Shie67bd6a2016-02-17 14:44:07 -08001302 # reporting modules have dependency on external packages, e.g., httplib2
1303 # Such dependency can cause issue to any module tries to import suite.py
1304 # without building site-packages first. Since the reporting modules are
1305 # only used in this function, move the imports here avoid the
1306 # requirement of building site packages to use other functions in this
1307 # module.
1308 from autotest_lib.server.cros.dynamic_suite import reporting
Dan Shie67bd6a2016-02-17 14:44:07 -08001309
Allen Li18503452016-12-29 14:56:48 -08001310 if bug_template is None:
1311 bug_template = {}
1312
Alex Millera3a4fe72013-01-22 09:57:47 -08001313 if self._file_bugs:
1314 bug_reporter = reporting.Reporter()
Allen Li733dab92016-12-29 15:07:50 -08001315 else:
1316 bug_reporter = reporting.NullReporter()
Alex Miller3a69adc2012-12-19 13:38:31 -08001317 try:
Aviv Keshet133beb12013-08-20 14:37:13 -07001318 if self._suite_job_id:
1319 results_generator = job_status.wait_for_child_results(
1320 self._afe, self._tko, self._suite_job_id)
1321 else:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -07001322 logging.warning('Unknown suite_job_id, falling back to less '
Dan Shi08ff1282016-02-18 19:51:16 -08001323 'efficient results_generator.')
Aviv Keshet133beb12013-08-20 14:37:13 -07001324 results_generator = job_status.wait_for_results(self._afe,
1325 self._tko,
1326 self._jobs)
1327 for result in results_generator:
Allen Li26b340d2016-12-29 15:23:01 -08001328 self._record_result(
1329 result=result,
1330 record=record,
1331 results_generator=results_generator,
1332 bug_reporter=bug_reporter,
1333 bug_template=bug_template)
beeps8ead53c2013-04-26 19:12:46 -07001334
Alex Miller3a69adc2012-12-19 13:38:31 -08001335 except Exception: # pylint: disable=W0703
Allen Lib892d9f2016-12-29 15:50:11 -08001336 logging.exception('Exception waiting for results')
Alex Miller3a69adc2012-12-19 13:38:31 -08001337 Status('FAIL', self._tag,
1338 'Exception waiting for results').record_result(record)
1339
1340
Allen Li26b340d2016-12-29 15:23:01 -08001341 def _record_result(self, result, record, results_generator, bug_reporter,
1342 bug_template):
1343 """
1344 Record a single test job result.
1345
1346 @param result: Status instance for job.
1347 @param record: callable that records job status.
1348 prototype:
1349 record(base_job.status_log_entry)
1350 @param results_generator: Results generator for sending job retries.
1351 @param bug_reporter: Reporter instance for reporting bugs.
1352 @param bug_template: A template dictionary specifying the default bug
1353 filing options for failures in this suite.
1354 """
Allen Li26b340d2016-12-29 15:23:01 -08001355 result.record_all(record)
Allen Li4df053e2016-12-29 16:05:41 -08001356 self._remember_job_keyval(result)
Allen Li26b340d2016-12-29 15:23:01 -08001357
xixuanbf854f82017-04-20 10:40:15 -07001358 if self._job_retry and self._retry_handler._should_retry(result):
Allen Li26b340d2016-12-29 15:23:01 -08001359 new_job = self._schedule_test(
1360 record=record, test=self._jobs_to_tests[result.id],
1361 retry_for=result.id, ignore_errors=True)
1362 if new_job:
1363 results_generator.send([new_job])
1364
1365 # TODO (fdeng): If the suite times out before a retry could
1366 # finish, we would lose the chance to file a bug for the
1367 # original job.
1368 if self._should_report(result):
Allen Li11308982016-12-29 16:19:55 -08001369 if self._should_file_bugs:
Allen Li47c9fca2016-12-29 16:22:53 -08001370 self._file_bug(result, bug_reporter, bug_template)
Allen Li26b340d2016-12-29 15:23:01 -08001371 else:
Allen Lid5df44b2016-12-29 15:59:06 -08001372 # reporting modules have dependency on external
1373 # packages, e.g., httplib2 Such dependency can cause
1374 # issue to any module tries to import suite.py without
1375 # building site-packages first. Since the reporting
1376 # modules are only used in this function, move the
1377 # imports here avoid the requirement of building site
1378 # packages to use other functions in this module.
1379 from autotest_lib.server.cros.dynamic_suite import reporting
1380
Allen Li7b973112016-12-29 16:17:41 -08001381 reporting.send_email(
1382 self._get_test_bug(result),
1383 self._get_bug_template(result, bug_template))
Allen Li26b340d2016-12-29 15:23:01 -08001384
1385
Allen Lid5df44b2016-12-29 15:59:06 -08001386 def _get_bug_template(self, result, bug_template):
1387 """Get BugTemplate for test job.
1388
1389 @param result: Status instance for job.
1390 @param bug_template: A template dictionary specifying the default bug
1391 filing options for failures in this suite.
1392 @returns: BugTemplate instance
1393 """
1394 # reporting modules have dependency on external packages, e.g., httplib2
1395 # Such dependency can cause issue to any module tries to import suite.py
1396 # without building site-packages first. Since the reporting modules are
1397 # only used in this function, move the imports here avoid the
1398 # requirement of building site packages to use other functions in this
1399 # module.
1400 from autotest_lib.server.cros.dynamic_suite import reporting_utils
1401
1402 # Try to merge with bug template in test control file.
1403 template = reporting_utils.BugTemplate(bug_template)
1404 try:
1405 test_data = self._jobs_to_tests[result.id]
1406 return template.finalize_bug_template(
1407 test_data.bug_template)
1408 except AttributeError:
1409 # Test control file does not have bug template defined.
1410 return template.bug_template
1411 except reporting_utils.InvalidBugTemplateException as e:
1412 logging.error('Merging bug templates failed with '
1413 'error: %s An empty bug template will '
1414 'be used.', e)
1415 return {}
1416
1417
Allen Li003913e2016-12-29 15:53:34 -08001418 def _get_test_bug(self, result):
1419 """Get TestBug for the given result.
1420
1421 @param result: Status instance for a test job.
1422 @returns: TestBug instance.
1423 """
1424 # reporting modules have dependency on external packages, e.g., httplib2
1425 # Such dependency can cause issue to any module tries to import suite.py
1426 # without building site-packages first. Since the reporting modules are
1427 # only used in this function, move the imports here avoid the
1428 # requirement of building site packages to use other functions in this
1429 # module.
1430 from autotest_lib.server.cros.dynamic_suite import reporting
1431
1432 job_views = self._tko.run('get_detailed_test_views',
1433 afe_job_id=result.id)
Allen Li27f72a22017-03-29 17:37:43 -07001434 return reporting.TestBug(self._job_creator.cros_build,
Allen Li5ed7e632017-02-03 16:31:33 -08001435 utils.get_chrome_version(job_views),
Allen Li003913e2016-12-29 15:53:34 -08001436 self._tag,
1437 result)
1438
1439
Allen Li11308982016-12-29 16:19:55 -08001440 @property
1441 def _should_file_bugs(self):
1442 """Return whether bugs should be filed.
1443
1444 @returns: bool
1445 """
1446 # File bug when failure is one of the _FILE_BUG_SUITES,
1447 # otherwise send an email to the owner anc cc.
1448 return self._tag in _FILE_BUG_SUITES
1449
1450
Allen Li47c9fca2016-12-29 16:22:53 -08001451 def _file_bug(self, result, bug_reporter, bug_template):
1452 """File a bug for a test job result.
1453
1454 @param result: Status instance for job.
1455 @param bug_reporter: Reporter instance for reporting bugs.
1456 @param bug_template: A template dictionary specifying the default bug
1457 filing options for failures in this suite.
1458 """
1459 bug_id, bug_count = bug_reporter.report(
1460 self._get_test_bug(result),
1461 self._get_bug_template(result, bug_template))
1462
1463 # We use keyvals to communicate bugs filed with run_suite.
1464 if bug_id is not None:
1465 bug_keyvals = tools.create_bug_keyvals(
1466 result.id, result.test_name,
1467 (bug_id, bug_count))
1468 try:
1469 utils.write_keyval(self._results_dir,
1470 bug_keyvals)
1471 except ValueError:
1472 logging.error('Unable to log bug keyval for:%s',
1473 result.test_name)
1474
1475
Alex Miller3a69adc2012-12-19 13:38:31 -08001476 def abort(self):
1477 """
1478 Abort all scheduled test jobs.
1479 """
1480 if self._jobs:
1481 job_ids = [job.id for job in self._jobs]
1482 self._afe.run('abort_host_queue_entries', job__id__in=job_ids)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001483
1484
Allen Li4df053e2016-12-29 16:05:41 -08001485 def _remember_job_keyval(self, job):
Chris Masoned9f13c52012-08-29 10:37:08 -07001486 """
1487 Record provided job as a suite job keyval, for later referencing.
1488
Allen Li4df053e2016-12-29 16:05:41 -08001489 @param job: some representation of a job that has the attributes:
1490 id, test_name, and owner
Chris Masoned9f13c52012-08-29 10:37:08 -07001491 """
Allen Li3cc73cd2016-12-12 16:02:21 -08001492 if self._results_dir and job.id and job.owner and job.test_name:
Chris Masone44e4d6c2012-08-15 14:25:53 -07001493 job_id_owner = '%s-%s' % (job.id, job.owner)
Chris Masoned9f13c52012-08-29 10:37:08 -07001494 logging.debug('Adding job keyval for %s=%s',
Chris Sosaaccb5ce2012-08-30 17:29:15 -07001495 job.test_name, job_id_owner)
Chris Masone44e4d6c2012-08-15 14:25:53 -07001496 utils.write_keyval(
1497 self._results_dir,
1498 {hashlib.md5(job.test_name).hexdigest(): job_id_owner})
1499
Dan Shid1521802013-05-24 13:08:37 -07001500
Allen Li4b5a24f2017-03-09 16:01:35 -08001501class Suite(_BaseSuite):
1502 """
1503 A suite of tests, defined by some predicate over control file variables.
1504
1505 Given a place to search for control files a predicate to match the desired
1506 tests, can gather tests and fire off jobs to run them, and then wait for
1507 results.
1508
1509 @var _predicate: a function that should return True when run over a
1510 ControlData representation of a control file that should be in
1511 this Suite.
1512 @var _tag: a string with which to tag jobs run in this suite.
1513 @var _builds: the builds on which we're running this suite.
1514 @var _afe: an instance of AFE as defined in server/frontend.py.
1515 @var _tko: an instance of TKO as defined in server/frontend.py.
1516 @var _jobs: currently scheduled jobs, if any.
1517 @var _jobs_to_tests: a dictionary that maps job ids to tests represented
1518 ControlData objects.
1519 @var _cf_getter: a control_file_getter.ControlFileGetter
1520 @var _retry: a bool value indicating whether jobs should be retried on
1521 failure.
1522 @var _retry_handler: a RetryHandler object.
1523
1524 """
1525
1526 # TODO(ayatane): These methods are kept on the Suite class for
1527 # backward compatibility.
1528 find_and_parse_tests = _deprecated_suite_method(find_and_parse_tests)
1529 find_possible_tests = _deprecated_suite_method(find_possible_tests)
1530 create_fs_getter = _deprecated_suite_method(create_fs_getter)
1531 name_in_tag_predicate = _deprecated_suite_method(name_in_tag_predicate)
1532 name_in_tag_similarity_predicate = _deprecated_suite_method(
1533 name_in_tag_similarity_predicate)
1534 test_name_equals_predicate = _deprecated_suite_method(
1535 test_name_equals_predicate)
1536 test_name_matches_pattern_predicate = _deprecated_suite_method(
1537 test_name_matches_pattern_predicate)
1538 test_file_matches_pattern_predicate = _deprecated_suite_method(
1539 test_file_matches_pattern_predicate)
1540 matches_attribute_expression_predicate = _deprecated_suite_method(
1541 matches_attribute_expression_predicate)
1542 test_name_similarity_predicate = _deprecated_suite_method(
1543 test_name_similarity_predicate)
1544 test_file_similarity_predicate = _deprecated_suite_method(
1545 test_file_similarity_predicate)
1546 list_all_suites = _deprecated_suite_method(list_all_suites)
1547 get_test_source_build = _deprecated_suite_method(get_test_source_build)
1548
1549
Allen Li25bb1c62017-03-09 16:27:00 -08001550 @classmethod
1551 def create_from_predicates(cls, predicates, builds, board, devserver,
1552 cf_getter=None, name='ad_hoc_suite',
1553 run_prod_code=False, **dargs):
1554 """
1555 Create a Suite using a given predicate test filters.
1556
1557 Uses supplied predicate(s) to instantiate a Suite. Looks for tests in
1558 |autotest_dir| and will schedule them using |afe|. Pulls control files
1559 from the default dev server. Results will be pulled from |tko| upon
1560 completion.
1561
1562 @param predicates: A list of callables that accept ControlData
1563 representations of control files. A test will be
1564 included in suite if all callables in this list
1565 return True on the given control file.
1566 @param builds: the builds on which we're running this suite. It's a
1567 dictionary of version_prefix:build.
1568 @param board: the board on which we're running this suite.
1569 @param devserver: the devserver which contains the build.
1570 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
1571 using DevServerGetter.
1572 @param name: name of suite. Defaults to 'ad_hoc_suite'
1573 @param run_prod_code: If true, the suite will run the tests that
1574 lives in prod aka the test code currently on the
1575 lab servers.
1576 @param **dargs: Any other Suite constructor parameters, as described
1577 in Suite.__init__ docstring.
1578 @return a Suite instance.
1579 """
1580 if cf_getter is None:
1581 if run_prod_code:
1582 cf_getter = create_fs_getter(_AUTOTEST_DIR)
1583 else:
1584 build = get_test_source_build(builds, **dargs)
1585 cf_getter = _create_ds_getter(build, devserver)
1586
1587 return cls(predicates,
1588 name, builds, board, cf_getter, run_prod_code, **dargs)
1589
1590
1591 @classmethod
1592 def create_from_name(cls, name, builds, board, devserver, cf_getter=None,
1593 **dargs):
1594 """
1595 Create a Suite using a predicate based on the SUITE control file var.
1596
1597 Makes a predicate based on |name| and uses it to instantiate a Suite
1598 that looks for tests in |autotest_dir| and will schedule them using
1599 |afe|. Pulls control files from the default dev server.
1600 Results will be pulled from |tko| upon completion.
1601
1602 @param name: a value of the SUITE control file variable to search for.
1603 @param builds: the builds on which we're running this suite. It's a
1604 dictionary of version_prefix:build.
1605 @param board: the board on which we're running this suite.
1606 @param devserver: the devserver which contains the build.
1607 @param cf_getter: control_file_getter.ControlFileGetter. Defaults to
1608 using DevServerGetter.
1609 @param **dargs: Any other Suite constructor parameters, as described
1610 in Suite.__init__ docstring.
1611 @return a Suite instance.
1612 """
1613 if cf_getter is None:
1614 build = get_test_source_build(builds, **dargs)
1615 cf_getter = _create_ds_getter(build, devserver)
1616
1617 return cls([name_in_tag_predicate(name)],
1618 name, builds, board, cf_getter, **dargs)
1619
1620
Allen Li3b1d4e52017-03-09 16:23:06 -08001621 def __init__(
1622 self,
1623 predicates,
1624 tag,
1625 builds,
1626 board,
1627 cf_getter,
1628 run_prod_code=False,
1629 afe=None,
1630 tko=None,
1631 pool=None,
1632 results_dir=None,
1633 max_runtime_mins=24*60,
1634 timeout_mins=24*60,
1635 file_bugs=False,
1636 file_experimental_bugs=False,
1637 suite_job_id=None,
1638 ignore_deps=False,
1639 extra_deps=None,
1640 priority=priorities.Priority.DEFAULT,
1641 forgiving_parser=True,
1642 wait_for_results=True,
1643 job_retry=False,
1644 max_retries=sys.maxint,
1645 offload_failures_only=False,
1646 test_source_build=None,
Allen Li7f43ef92017-03-09 16:29:48 -08001647 job_keyvals=None,
1648 test_args=None
Allen Li3b1d4e52017-03-09 16:23:06 -08001649 ):
1650 """
1651 Constructor
1652
1653 @param predicates: A list of callables that accept ControlData
1654 representations of control files. A test will be
Allen Li2887e332017-03-09 16:30:36 -08001655 included in suite if all callables in this list
Allen Li3b1d4e52017-03-09 16:23:06 -08001656 return True on the given control file.
1657 @param tag: a string with which to tag jobs run in this suite.
1658 @param builds: the builds on which we're running this suite.
1659 @param board: the board on which we're running this suite.
1660 @param cf_getter: a control_file_getter.ControlFileGetter
1661 @param afe: an instance of AFE as defined in server/frontend.py.
1662 @param tko: an instance of TKO as defined in server/frontend.py.
1663 @param pool: Specify the pool of machines to use for scheduling
1664 purposes.
1665 @param run_prod_code: If true, the suite will run the test code that
1666 lives in prod aka the test code currently on the
1667 lab servers.
1668 @param results_dir: The directory where the job can write results to.
1669 This must be set if you want job_id of sub-jobs
1670 list in the job keyvals.
1671 @param max_runtime_mins: Maximum suite runtime, in minutes.
1672 @param timeout: Maximum job lifetime, in hours.
1673 @param suite_job_id: Job id that will act as parent id to all sub jobs.
1674 Default: None
1675 @param ignore_deps: True if jobs should ignore the DEPENDENCIES
1676 attribute and skip applying of dependency labels.
1677 (Default:False)
1678 @param extra_deps: A list of strings which are the extra DEPENDENCIES
1679 to add to each test being scheduled.
1680 @param priority: Integer priority level. Higher is more important.
1681 @param wait_for_results: Set to False to run the suite job without
1682 waiting for test jobs to finish. Default is
1683 True.
1684 @param job_retry: A bool value indicating whether jobs should be retired
1685 on failure. If True, the field 'JOB_RETRIES' in
1686 control files will be respected. If False, do not
1687 retry.
1688 @param max_retries: Maximum retry limit at suite level.
1689 Regardless how many times each individual test
1690 has been retried, the total number of retries
1691 happening in the suite can't exceed _max_retries.
1692 Default to sys.maxint.
1693 @param offload_failures_only: Only enable gs_offloading for failed
1694 jobs.
1695 @param test_source_build: Build that contains the server-side test code.
1696 @param job_keyvals: General job keyvals to be inserted into keyval file,
1697 which will be used by tko/parse later.
Allen Li7f43ef92017-03-09 16:29:48 -08001698 @param test_args: A dict of args passed all the way to each individual
1699 test that will be actually ran.
Allen Li3b1d4e52017-03-09 16:23:06 -08001700
1701 """
Allen Li00bbe5b2017-03-09 16:44:30 -08001702 tests = find_and_parse_tests(
1703 cf_getter,
1704 _ComposedPredicate(predicates),
1705 tag,
1706 add_experimental=True,
1707 forgiving_parser=forgiving_parser,
1708 run_prod_code=run_prod_code,
1709 test_args=test_args,
1710 )
Allen Li3b1d4e52017-03-09 16:23:06 -08001711 super(Suite, self).__init__(
Allen Li00bbe5b2017-03-09 16:44:30 -08001712 tests=tests,
Allen Li3b1d4e52017-03-09 16:23:06 -08001713 tag=tag,
1714 builds=builds,
1715 board=board,
Allen Li3b1d4e52017-03-09 16:23:06 -08001716 afe=afe,
1717 tko=tko,
1718 pool=pool,
1719 results_dir=results_dir,
1720 max_runtime_mins=max_runtime_mins,
1721 timeout_mins=timeout_mins,
1722 file_bugs=file_bugs,
1723 file_experimental_bugs=file_experimental_bugs,
1724 suite_job_id=suite_job_id,
1725 ignore_deps=ignore_deps,
1726 extra_deps=extra_deps,
1727 priority=priority,
Allen Li3b1d4e52017-03-09 16:23:06 -08001728 wait_for_results=wait_for_results,
1729 job_retry=job_retry,
1730 max_retries=max_retries,
1731 offload_failures_only=offload_failures_only,
1732 test_source_build=test_source_build,
1733 job_keyvals=job_keyvals)
1734
Allen Li4b5a24f2017-03-09 16:01:35 -08001735
Allen Li44969e32017-05-24 16:47:37 -07001736class ProvisionSuite(_BaseSuite):
1737 """
1738 A suite for provisioning DUTs.
1739
1740 This is done by creating dummy_Pass tests.
1741 """
1742
1743
1744 def __init__(
1745 self,
1746 tag,
1747 builds,
1748 board,
1749 count,
1750 devserver,
1751 cf_getter=None,
1752 run_prod_code=False,
1753 test_args=None,
1754 test_source_build=None,
1755 **suite_args):
1756 """
1757 Constructor
1758
1759 @param tag: a string with which to tag jobs run in this suite.
1760 @param builds: the builds on which we're running this suite.
1761 @param board: the board on which we're running this suite.
1762 @param count: number of dummy tests to make
1763 @param devserver: the devserver which contains the build.
1764 @param cf_getter: a control_file_getter.ControlFileGetter.
1765 @param test_args: A dict of args passed all the way to each individual
1766 test that will be actually ran.
1767 @param test_source_build: Build that contains the server-side test code.
1768 @param suite_args: Various keyword arguments passed to
1769 _BaseSuite constructor.
1770 """
1771 dummy_test = _load_dummy_test(
1772 builds, devserver, cf_getter,
1773 run_prod_code, test_args, test_source_build)
1774
1775 super(ProvisionSuite, self).__init__(
1776 tests=[dummy_test] * count,
1777 tag=tag,
1778 builds=builds,
1779 board=board,
1780 **suite_args)
1781
1782
1783def _load_dummy_test(
1784 builds,
1785 devserver,
1786 cf_getter=None,
1787 run_prod_code=False,
1788 test_args=None,
1789 test_source_build=None):
1790 """
1791 Load and return the dummy pass test.
1792
1793 @param builds: the builds on which we're running this suite.
1794 @param devserver: the devserver which contains the build.
1795 @param cf_getter: a control_file_getter.ControlFileGetter.
1796 @param test_args: A dict of args passed all the way to each individual
1797 test that will be actually ran.
1798 @param test_source_build: Build that contains the server-side test code.
1799 @param suite_args: Various keyword arguments passed to
1800 _BaseSuite constructor.
1801 """
1802 if cf_getter is None:
1803 if run_prod_code:
1804 cf_getter = create_fs_getter(_AUTOTEST_DIR)
1805 else:
1806 build = get_test_source_build(
1807 builds, test_source_build=test_source_build)
1808 cf_getter = _create_ds_getter(build, devserver)
1809 retriever = _get_cf_retriever(cf_getter,
1810 run_prod_code=run_prod_code,
1811 test_args=test_args)
1812 return retriever.retrieve('dummy_Pass')
1813
1814
Allen Licec26f72017-03-09 16:39:09 -08001815class _ComposedPredicate(object):
1816 """Return the composition of the predicates.
1817
1818 Predicates are functions that take a test control data object and
1819 return True of that test is to be included. The returned
1820 predicate's set is the intersection of all of the input predicates'
1821 sets (it returns True if all predicates return True).
1822 """
1823
1824 def __init__(self, predicates):
1825 """Initialize instance.
1826
1827 @param predicates: Iterable of predicates.
1828 """
1829 self._predicates = list(predicates)
1830
1831 def __repr__(self):
1832 return '{cls}({this._predicates!r})'.format(
Allen Li5511bd32017-05-17 16:57:26 -07001833 cls=type(self).__name__,
Allen Licec26f72017-03-09 16:39:09 -08001834 this=self,
1835 )
1836
1837 def __call__(self, control_data_):
1838 return all(f(control_data_) for f in self._predicates)
1839
1840
Allen Li9fcd4b42016-12-12 16:15:14 -08001841def _is_nonexistent_board_error(e):
1842 """Return True if error is caused by nonexistent board label.
1843
1844 As of this writing, the particular case we want looks like this:
1845
1846 1) e.problem_keys is a dictionary
1847 2) e.problem_keys['meta_hosts'] exists as the only key
1848 in the dictionary.
1849 3) e.problem_keys['meta_hosts'] matches this pattern:
1850 "Label "board:.*" not found"
1851
1852 We check for conditions 1) and 2) on the
1853 theory that they're relatively immutable.
1854 We don't check condition 3) because it seems
1855 likely to be a maintenance burden, and for the
1856 times when we're wrong, being right shouldn't
1857 matter enough (we _hope_).
1858
1859 @param e: proxy.ValidationError instance
1860 @returns: boolean
1861 """
1862 return (isinstance(e.problem_keys, dict)
1863 and len(e.problem_keys) == 1
1864 and 'meta_hosts' in e.problem_keys)