blob: 1430039a0dbaf02fe4a08e917979f7eda2e18175 [file] [log] [blame]
Xixuan Wu7cc10e52018-04-25 17:04:51 -07001# Copyright 2018 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
5"""Shared functions by dynamic_suite/suite.py & skylab_suite/cros_suite.py."""
6
7from __future__ import division
8from __future__ import print_function
9
Xixuan Wu48c45b92018-04-26 11:09:35 -070010import datetime
Xixuan Wu92249042018-04-30 17:17:10 -070011import logging
Ed Baker8280cb32018-09-25 08:42:53 -070012import multiprocessing
Xixuan Wu7cc10e52018-04-25 17:04:51 -070013import re
14
15import common
16
Xixuan Wu92249042018-04-30 17:17:10 -070017from autotest_lib.client.common_lib import control_data
Xixuan Wu7cc10e52018-04-25 17:04:51 -070018from autotest_lib.client.common_lib import error
Xixuan Wu92249042018-04-30 17:17:10 -070019from autotest_lib.client.common_lib import global_config
Xixuan Wu48c45b92018-04-26 11:09:35 -070020from autotest_lib.client.common_lib import time_utils
21from autotest_lib.client.common_lib.cros import dev_server
Xixuan Wu7cc10e52018-04-25 17:04:51 -070022from autotest_lib.server.cros import provision
Xixuan Wu48c45b92018-04-26 11:09:35 -070023from autotest_lib.server.cros.dynamic_suite import constants
Xixuan Wue3e362f2018-04-26 16:34:28 -070024from autotest_lib.server.cros.dynamic_suite import control_file_getter
Xixuan Wu92249042018-04-30 17:17:10 -070025from autotest_lib.server.cros.dynamic_suite import tools
26
27ENABLE_CONTROLS_IN_BATCH = global_config.global_config.get_config_value(
28 'CROS', 'enable_getting_controls_in_batch', type=bool, default=False)
Xixuan Wue3e362f2018-04-26 16:34:28 -070029
30
31def canonicalize_suite_name(suite_name):
32 """Canonicalize the suite's name.
33
34 @param suite_name: the name of the suite.
35 """
36 # Do not change this naming convention without updating
37 # site_utils.parse_job_name.
38 return 'test_suites/control.%s' % suite_name
Xixuan Wu48c45b92018-04-26 11:09:35 -070039
40
41def _formatted_now():
42 """Format the current datetime."""
43 return datetime.datetime.now().strftime(time_utils.TIME_FMT)
Xixuan Wu7cc10e52018-04-25 17:04:51 -070044
45
46def make_builds_from_options(options):
47 """Create a dict of builds for creating a suite job.
48
49 The returned dict maps version label prefixes to build names. Together,
50 each key-value pair describes a complete label.
51
52 @param options: SimpleNamespace from argument parsing.
53
54 @return: dict mapping version label prefixes to build names
55 """
56 builds = {}
57 build_prefix = None
58 if options.build:
59 build_prefix = provision.get_version_label_prefix(options.build)
60 builds[build_prefix] = options.build
61
62 if options.cheets_build:
63 builds[provision.CROS_ANDROID_VERSION_PREFIX] = options.cheets_build
64 if build_prefix == provision.CROS_VERSION_PREFIX:
65 builds[build_prefix] += provision.CHEETS_SUFFIX
66
67 if options.firmware_rw_build:
68 builds[provision.FW_RW_VERSION_PREFIX] = options.firmware_rw_build
69
70 if options.firmware_ro_build:
71 builds[provision.FW_RO_VERSION_PREFIX] = options.firmware_ro_build
72
73 return builds
74
75
76def get_test_source_build(builds, **dargs):
77 """Get the build of test code.
78
79 Get the test source build from arguments. If parameter
80 `test_source_build` is set and has a value, return its value. Otherwise
81 returns the ChromeOS build name if it exists. If ChromeOS build is not
82 specified either, raise SuiteArgumentException.
83
84 @param builds: the builds on which we're running this suite. It's a
85 dictionary of version_prefix:build.
86 @param **dargs: Any other Suite constructor parameters, as described
87 in Suite.__init__ docstring.
88
89 @return: The build contains the test code.
90 @raise: SuiteArgumentException if both test_source_build and ChromeOS
91 build are not specified.
92
93 """
94 if dargs.get('test_source_build', None):
95 return dargs['test_source_build']
96
97 cros_build = builds.get(provision.CROS_VERSION_PREFIX, None)
98 if cros_build.endswith(provision.CHEETS_SUFFIX):
99 test_source_build = re.sub(
100 provision.CHEETS_SUFFIX + '$', '', cros_build)
101 else:
102 test_source_build = cros_build
103
104 if not test_source_build:
105 raise error.SuiteArgumentException(
106 'test_source_build must be specified if CrOS build is not '
107 'specified.')
108
109 return test_source_build
Xixuan Wu48c45b92018-04-26 11:09:35 -0700110
111
112def stage_build_artifacts(build, hostname=None):
113 """
114 Ensure components of |build| necessary for installing images are staged.
115
116 @param build image we want to stage.
117 @param hostname hostname of a dut may run test on. This is to help to locate
118 a devserver closer to duts if needed. Default is None.
119
120 @raises StageControlFileFailure: if the dev server throws 500 while staging
121 suite control files.
122
123 @return: dev_server.ImageServer instance to use with this build.
124 @return: timings dictionary containing staging start/end times.
125 """
126 timings = {}
127 # Ensure components of |build| necessary for installing images are staged
128 # on the dev server. However set synchronous to False to allow other
129 # components to be downloaded in the background.
130 ds = dev_server.resolve(build, hostname=hostname)
131 ds_name = ds.hostname
132 timings[constants.DOWNLOAD_STARTED_TIME] = _formatted_now()
133 try:
134 ds.stage_artifacts(image=build, artifacts=['test_suites'])
135 except dev_server.DevServerException as e:
136 raise error.StageControlFileFailure(
137 "Failed to stage %s on %s: %s" % (build, ds_name, e))
138 timings[constants.PAYLOAD_FINISHED_TIME] = _formatted_now()
139 return ds, timings
Xixuan Wue3e362f2018-04-26 16:34:28 -0700140
141
142def get_control_file_by_build(build, ds, suite_name):
143 """Return control file contents for |suite_name|.
144
145 Query the dev server at |ds| for the control file |suite_name|, included
146 in |build| for |board|.
147
148 @param build: unique name by which to refer to the image from now on.
149 @param ds: a dev_server.DevServer instance to fetch control file with.
150 @param suite_name: canonicalized suite name, e.g. test_suites/control.bvt.
151 @raises ControlFileNotFound if a unique suite control file doesn't exist.
152 @raises NoControlFileList if we can't list the control files at all.
153 @raises ControlFileEmpty if the control file exists on the server, but
154 can't be read.
155
156 @return the contents of the desired control file.
157 """
158 getter = control_file_getter.DevServerGetter.create(build, ds)
159 devserver_name = ds.hostname
160 # Get the control file for the suite.
161 try:
162 control_file_in = getter.get_control_file_contents_by_name(suite_name)
163 except error.CrosDynamicSuiteException as e:
164 raise type(e)('Failed to get control file for %s '
165 '(devserver: %s) (error: %s)' %
166 (build, devserver_name, e))
167 if not control_file_in:
168 raise error.ControlFileEmpty(
169 "Fetching %s returned no data. (devserver: %s)" %
170 (suite_name, devserver_name))
171 # Force control files to only contain ascii characters.
172 try:
173 control_file_in.encode('ascii')
174 except UnicodeDecodeError as e:
175 raise error.ControlFileMalformed(str(e))
176
177 return control_file_in
Xixuan Wu92249042018-04-30 17:17:10 -0700178
179
180def _should_batch_with(cf_getter):
181 """Return whether control files should be fetched in batch.
182
183 This depends on the control file getter and configuration options.
184
185 If cf_getter is a File system ControlFileGetter, the cf_getter will
186 perform a full parse of the root directory associated with the
187 getter. This is the case when it's invoked from suite_preprocessor.
188
189 If cf_getter is a devserver getter, this will look up the suite_name in a
190 suite to control file map generated at build time, and parses the relevant
191 control files alone. This lookup happens on the devserver, so as far
192 as this method is concerned, both cases are equivalent. If
193 enable_controls_in_batch is switched on, this function will call
194 cf_getter.get_suite_info() to get a dict of control files and
195 contents in batch.
196
197 @param cf_getter: a control_file_getter.ControlFileGetter used to list
198 and fetch the content of control files
199 """
200 return (ENABLE_CONTROLS_IN_BATCH
201 and isinstance(cf_getter, control_file_getter.DevServerGetter))
202
203
204def _get_cf_texts_for_suite_batched(cf_getter, suite_name):
205 """Get control file content for given suite with batched getter.
206
207 See get_cf_texts_for_suite for params & returns.
208 """
209 suite_info = cf_getter.get_suite_info(suite_name=suite_name)
210 files = suite_info.keys()
211 filtered_files = _filter_cf_paths(files)
212 for path in filtered_files:
213 yield path, suite_info[path]
214
215
216def _get_cf_texts_for_suite_unbatched(cf_getter, suite_name):
217 """Get control file content for given suite with unbatched getter.
218
219 See get_cf_texts_for_suite for params & returns.
220 """
221 files = cf_getter.get_control_file_list(suite_name=suite_name)
222 filtered_files = _filter_cf_paths(files)
223 for path in filtered_files:
224 yield path, cf_getter.get_control_file_contents(path)
225
226
227def _filter_cf_paths(paths):
228 """Remove certain control file paths.
229
230 @param paths: Iterable of paths
231 @returns: generator yielding paths
232 """
233 matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
234 return (path for path in paths if not matcher.match(path))
235
236
237def get_cf_texts_for_suite(cf_getter, suite_name):
238 """Get control file content for given suite.
239
240 @param cf_getter: A control file getter object, e.g.
241 a control_file_getter.DevServerGetter object.
242 @param suite_name: If specified, this method will attempt to restrain
243 the search space to just this suite's control files.
244 @returns: generator yielding (path, text) tuples
245 """
246 if _should_batch_with(cf_getter):
247 return _get_cf_texts_for_suite_batched(cf_getter, suite_name)
248 else:
249 return _get_cf_texts_for_suite_unbatched(cf_getter, suite_name)
250
251
252def parse_cf_text(path, text):
253 """Parse control file text.
254
255 @param path: path to control file
256 @param text: control file text contents
257
258 @returns: a ControlData object
259
260 @raises ControlVariableException: There is a syntax error in a
261 control file.
262 """
263 test = control_data.parse_control_string(
264 text, raise_warnings=True, path=path)
265 test.text = text
266 return test
267
Ed Baker8280cb32018-09-25 08:42:53 -0700268def parse_cf_text_process(data):
269 """Worker process for parsing control file text
Xixuan Wu92249042018-04-30 17:17:10 -0700270
Ed Baker8280cb32018-09-25 08:42:53 -0700271 @param data: Tuple of path, text, forgiving_error, and test_args.
272
273 @returns: Tuple of the path and test ControlData
274
275 @raises ControlVariableException: If forgiving_error is false parsing
276 exceptions are raised instead of logged.
277 """
278 path, text, forgiving_error, test_args = data
279
280 if test_args:
281 text = tools.inject_vars(test_args, text)
282
283 try:
284 found_test = parse_cf_text(path, text)
285 except control_data.ControlVariableException, e:
286 if not forgiving_error:
287 msg = "Failed parsing %s\n%s" % (path, e)
288 raise control_data.ControlVariableException(msg)
289 logging.warning("Skipping %s\n%s", path, e)
290 except Exception, e:
291 logging.error("Bad %s\n%s", path, e)
292 import traceback
293 logging.error(traceback.format_exc())
294 else:
295 return (path, found_test)
296
297
298def get_process_limit():
299 """Limit the number of CPUs to use.
300
301 On a server many autotest instances can run in parallel. Avoid that
302 each of them requests all the CPUs at the same time causing a spike.
303 """
304 return min(8, multiprocessing.cpu_count())
305
306
307def parse_cf_text_many(control_file_texts,
308 forgiving_error=False,
Xixuan Wu92249042018-04-30 17:17:10 -0700309 test_args=None):
310 """Parse control file texts.
311
312 @param control_file_texts: iterable of (path, text) pairs
313 @param test_args: The test args to be injected into test control file.
314
315 @returns: a dictionary of ControlData objects
316 """
317 tests = {}
Xixuan Wu92249042018-04-30 17:17:10 -0700318
Ed Baker8280cb32018-09-25 08:42:53 -0700319 control_file_texts_all = list(control_file_texts)
320 if control_file_texts_all:
321 # Construct input data for worker processes. Each row contains the
322 # path, text, forgiving_error configuration, and test arguments.
323 paths, texts = zip(*control_file_texts_all)
324 worker_data = zip(paths, texts, [forgiving_error] * len(paths),
325 [test_args] * len(paths))
326 pool = multiprocessing.Pool(processes=get_process_limit())
327 result_list = pool.map(parse_cf_text_process, worker_data)
328 pool.close()
329 pool.join()
330
331 # Convert [(path, test), ...] to {path: test, ...}
332 tests = dict(result_list)
333
Xixuan Wu92249042018-04-30 17:17:10 -0700334 return tests
335
336
Xixuan Wu9af22652018-05-14 10:50:54 -0700337def retrieve_control_data_for_test(cf_getter, test_name):
338 """Retrieve a test's control file.
339
340 @param cf_getter: a control_file_getter.ControlFileGetter object to
341 list and fetch the control files' content.
342 @param test_name: Name of test to retrieve.
343
344 @raises ControlVariableException: There is a syntax error in a
345 control file.
346
347 @returns a ControlData object
348 """
349 path = cf_getter.get_control_file_path(test_name)
350 text = cf_getter.get_control_file_contents(path)
351 return parse_cf_text(path, text)
352
353
Xixuan Wu92249042018-04-30 17:17:10 -0700354def retrieve_for_suite(cf_getter, suite_name='', forgiving_error=False,
355 test_args=None):
356 """Scan through all tests and find all tests.
357
358 @param suite_name: If specified, retrieve this suite's control file.
359
360 @raises ControlVariableException: If forgiving_parser is False and there
361 is a syntax error in a control file.
362
363 @returns a dictionary of ControlData objects that based on given
364 parameters.
365 """
366 control_file_texts = get_cf_texts_for_suite(cf_getter, suite_name)
367 return parse_cf_text_many(control_file_texts,
368 forgiving_error=forgiving_error,
369 test_args=test_args)
Xixuan Wud9648532018-05-04 18:06:53 -0700370
371
372def filter_tests(tests, predicate=lambda t: True):
373 """Filter child tests with predicates.
374
375 @tests: A dict of ControlData objects as tests.
376 @predicate: A test filter. By default it's None.
377
378 @returns a list of ControlData objects as tests.
379 """
380 logging.info('Parsed %s child test control files.', len(tests))
381 tests = [test for test in tests.itervalues() if predicate(test)]
382 tests.sort(key=lambda t:
383 control_data.ControlData.get_test_time_index(t.time),
384 reverse=True)
385 return tests
Xixuan Wu81b71cb2019-01-10 16:00:30 -0800386
387
388def name_in_tag_predicate(name):
389 """Returns predicate that takes a control file and looks for |name|.
390
391 Builds a predicate that takes in a parsed control file (a ControlData)
392 and returns True if the SUITE tag is present and contains |name|.
393
394 @param name: the suite name to base the predicate on.
395 @return a callable that takes a ControlData and looks for |name| in that
396 ControlData object's suite member.
397 """
398 return lambda t: name in t.suite_tag_parts