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