• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 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
10import datetime
11import logging
12import multiprocessing
13import re
14
15import common
16
17from autotest_lib.client.common_lib import control_data
18from autotest_lib.client.common_lib import error
19from autotest_lib.client.common_lib import global_config
20from autotest_lib.client.common_lib import time_utils
21from autotest_lib.client.common_lib.cros import dev_server
22from autotest_lib.server.cros import provision
23from autotest_lib.server.cros.dynamic_suite import constants
24from autotest_lib.server.cros.dynamic_suite import control_file_getter
25from 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)
29
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
39
40
41def _formatted_now():
42    """Format the current datetime."""
43    return datetime.datetime.now().strftime(time_utils.TIME_FMT)
44
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
110
111
112def stage_build_artifacts(build, hostname=None, artifacts=[]):
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    @param artifacts A list of string artifact name to be staged.
120
121    @raises StageControlFileFailure: if the dev server throws 500 while staging
122        suite control files.
123
124    @return: dev_server.ImageServer instance to use with this build.
125    @return: timings dictionary containing staging start/end times.
126    """
127    timings = {}
128    # Ensure components of |build| necessary for installing images are staged
129    # on the dev server. However set synchronous to False to allow other
130    # components to be downloaded in the background.
131    ds = dev_server.resolve(build, hostname=hostname)
132    ds_name = ds.hostname
133    timings[constants.DOWNLOAD_STARTED_TIME] = _formatted_now()
134    try:
135        artifacts_to_stage = ['test_suites', 'control_files']
136        artifacts_to_stage.extend(artifacts if artifacts else [])
137        ds.stage_artifacts(image=build, artifacts=artifacts_to_stage)
138    except dev_server.DevServerException as e:
139        raise error.StageControlFileFailure(
140                "Failed to stage %s on %s: %s" % (build, ds_name, e))
141    timings[constants.PAYLOAD_FINISHED_TIME] = _formatted_now()
142    return ds, timings
143
144
145def get_control_file_by_build(build, ds, suite_name):
146    """Return control file contents for |suite_name|.
147
148    Query the dev server at |ds| for the control file |suite_name|, included
149    in |build| for |board|.
150
151    @param build: unique name by which to refer to the image from now on.
152    @param ds: a dev_server.DevServer instance to fetch control file with.
153    @param suite_name: canonicalized suite name, e.g. test_suites/control.bvt.
154    @raises ControlFileNotFound if a unique suite control file doesn't exist.
155    @raises NoControlFileList if we can't list the control files at all.
156    @raises ControlFileEmpty if the control file exists on the server, but
157                             can't be read.
158
159    @return the contents of the desired control file.
160    """
161    getter = control_file_getter.DevServerGetter.create(build, ds)
162    devserver_name = ds.hostname
163    # Get the control file for the suite.
164    try:
165        control_file_in = getter.get_control_file_contents_by_name(suite_name)
166    except error.CrosDynamicSuiteException as e:
167        raise type(e)('Failed to get control file for %s '
168                      '(devserver: %s) (error: %s)' %
169                      (build, devserver_name, e))
170    if not control_file_in:
171        raise error.ControlFileEmpty(
172            "Fetching %s returned no data. (devserver: %s)" %
173            (suite_name, devserver_name))
174    # Force control files to only contain ascii characters.
175    try:
176        control_file_in.encode('ascii')
177    except UnicodeDecodeError as e:
178        raise error.ControlFileMalformed(str(e))
179
180    return control_file_in
181
182
183def _should_batch_with(cf_getter):
184    """Return whether control files should be fetched in batch.
185
186    This depends on the control file getter and configuration options.
187
188    If cf_getter is a File system ControlFileGetter, the cf_getter will
189    perform a full parse of the root directory associated with the
190    getter. This is the case when it's invoked from suite_preprocessor.
191
192    If cf_getter is a devserver getter, this will look up the suite_name in a
193    suite to control file map generated at build time, and parses the relevant
194    control files alone. This lookup happens on the devserver, so as far
195    as this method is concerned, both cases are equivalent. If
196    enable_controls_in_batch is switched on, this function will call
197    cf_getter.get_suite_info() to get a dict of control files and
198    contents in batch.
199
200    @param cf_getter: a control_file_getter.ControlFileGetter used to list
201           and fetch the content of control files
202    """
203    return (ENABLE_CONTROLS_IN_BATCH
204            and isinstance(cf_getter, control_file_getter.DevServerGetter))
205
206
207def _get_cf_texts_for_suite_batched(cf_getter, suite_name):
208    """Get control file content for given suite with batched getter.
209
210    See get_cf_texts_for_suite for params & returns.
211    """
212    suite_info = cf_getter.get_suite_info(suite_name=suite_name)
213    files = suite_info.keys()
214    filtered_files = _filter_cf_paths(files)
215    for path in filtered_files:
216        yield path, suite_info[path]
217
218
219def _get_cf_texts_for_suite_unbatched(cf_getter, suite_name):
220    """Get control file content for given suite with unbatched getter.
221
222    See get_cf_texts_for_suite for params & returns.
223    """
224    files = cf_getter.get_control_file_list(suite_name=suite_name)
225    filtered_files = _filter_cf_paths(files)
226    for path in filtered_files:
227        yield path, cf_getter.get_control_file_contents(path)
228
229
230def _filter_cf_paths(paths):
231    """Remove certain control file paths.
232
233    @param paths: Iterable of paths
234    @returns: generator yielding paths
235    """
236    matcher = re.compile(r'[^/]+/(deps|profilers)/.+')
237    return (path for path in paths if not matcher.match(path))
238
239
240def get_cf_texts_for_suite(cf_getter, suite_name):
241    """Get control file content for given suite.
242
243    @param cf_getter: A control file getter object, e.g.
244        a control_file_getter.DevServerGetter object.
245    @param suite_name: If specified, this method will attempt to restrain
246                       the search space to just this suite's control files.
247    @returns: generator yielding (path, text) tuples
248    """
249    if _should_batch_with(cf_getter):
250        return _get_cf_texts_for_suite_batched(cf_getter, suite_name)
251    else:
252        return _get_cf_texts_for_suite_unbatched(cf_getter, suite_name)
253
254
255def parse_cf_text(path, text):
256    """Parse control file text.
257
258    @param path: path to control file
259    @param text: control file text contents
260
261    @returns: a ControlData object
262
263    @raises ControlVariableException: There is a syntax error in a
264                                      control file.
265    """
266    test = control_data.parse_control_string(
267            text, raise_warnings=True, path=path)
268    test.text = text
269    return test
270
271def parse_cf_text_process(data):
272    """Worker process for parsing control file text
273
274    @param data: Tuple of path, text, forgiving_error, and test_args.
275
276    @returns: Tuple of the path and test ControlData
277
278    @raises ControlVariableException: If forgiving_error is false parsing
279                                      exceptions are raised instead of logged.
280    """
281    path, text, forgiving_error, test_args = data
282
283    if test_args:
284        text = tools.inject_vars(test_args, text)
285
286    try:
287        found_test = parse_cf_text(path, text)
288    except control_data.ControlVariableException, e:
289        if not forgiving_error:
290            msg = "Failed parsing %s\n%s" % (path, e)
291            raise control_data.ControlVariableException(msg)
292        logging.warning("Skipping %s\n%s", path, e)
293    except Exception, e:
294        logging.error("Bad %s\n%s", path, e)
295        import traceback
296        logging.error(traceback.format_exc())
297    else:
298        return (path, found_test)
299
300
301def get_process_limit():
302    """Limit the number of CPUs to use.
303
304    On a server many autotest instances can run in parallel. Avoid that
305    each of them requests all the CPUs at the same time causing a spike.
306    """
307    return min(8, multiprocessing.cpu_count())
308
309
310def parse_cf_text_many(control_file_texts,
311                       forgiving_error=False,
312                       test_args=None):
313    """Parse control file texts.
314
315    @param control_file_texts: iterable of (path, text) pairs
316    @param test_args: The test args to be injected into test control file.
317
318    @returns: a dictionary of ControlData objects
319    """
320    tests = {}
321
322    control_file_texts_all = list(control_file_texts)
323    if control_file_texts_all:
324        # Construct input data for worker processes. Each row contains the
325        # path, text, forgiving_error configuration, and test arguments.
326        paths, texts = zip(*control_file_texts_all)
327        worker_data = zip(paths, texts, [forgiving_error] * len(paths),
328                          [test_args] * len(paths))
329        pool = multiprocessing.Pool(processes=get_process_limit())
330        result_list = pool.map(parse_cf_text_process, worker_data)
331        pool.close()
332        pool.join()
333
334        # Convert [(path, test), ...] to {path: test, ...}
335        tests = dict(result_list)
336
337    return tests
338
339
340def retrieve_control_data_for_test(cf_getter, test_name):
341    """Retrieve a test's control file.
342
343    @param cf_getter: a control_file_getter.ControlFileGetter object to
344                      list and fetch the control files' content.
345    @param test_name: Name of test to retrieve.
346
347    @raises ControlVariableException: There is a syntax error in a
348                                      control file.
349
350    @returns a ControlData object
351    """
352    path = cf_getter.get_control_file_path(test_name)
353    text = cf_getter.get_control_file_contents(path)
354    return parse_cf_text(path, text)
355
356
357def retrieve_for_suite(cf_getter, suite_name='', forgiving_error=False,
358                       test_args=None):
359    """Scan through all tests and find all tests.
360
361    @param suite_name: If specified, retrieve this suite's control file.
362
363    @raises ControlVariableException: If forgiving_parser is False and there
364                                      is a syntax error in a control file.
365
366    @returns a dictionary of ControlData objects that based on given
367             parameters.
368    """
369    control_file_texts = get_cf_texts_for_suite(cf_getter, suite_name)
370    return parse_cf_text_many(control_file_texts,
371                              forgiving_error=forgiving_error,
372                              test_args=test_args)
373
374
375def filter_tests(tests, predicate=lambda t: True):
376    """Filter child tests with predicates.
377
378    @tests: A dict of ControlData objects as tests.
379    @predicate: A test filter. By default it's None.
380
381    @returns a list of ControlData objects as tests.
382    """
383    logging.info('Parsed %s child test control files.', len(tests))
384    tests = [test for test in tests.itervalues() if predicate(test)]
385    tests.sort(key=lambda t:
386               control_data.ControlData.get_test_time_index(t.time),
387               reverse=True)
388    return tests
389
390
391def name_in_tag_predicate(name):
392    """Returns predicate that takes a control file and looks for |name|.
393
394    Builds a predicate that takes in a parsed control file (a ControlData)
395    and returns True if the SUITE tag is present and contains |name|.
396
397    @param name: the suite name to base the predicate on.
398    @return a callable that takes a ControlData and looks for |name| in that
399            ControlData object's suite member.
400    """
401    return lambda t: name in t.suite_tag_parts
402
403
404def test_name_in_list_predicate(name_list):
405    """Returns a predicate that matches control files by test name.
406
407    The returned predicate returns True for control files whose test name
408    is present in name_list.
409    """
410    name_set = set(name_list)
411    return lambda t: t.name in name_set
412