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