• 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):
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
140
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
178
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
268def parse_cf_text_process(data):
269    """Worker process for parsing control file text
270
271    @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,
309                       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 = {}
318
319    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
334    return tests
335
336
337def 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
354def 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)
370
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
386
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
399