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