1 #!/usr/bin/env python3.4 2 # 3 # Copyright 2016 - The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 from builtins import str 18 19 import os 20 import random 21 import sys 22 23 from acts import keys 24 from acts import utils 25 26 # An environment variable defining the base location for ACTS logs. 27 _ENV_ACTS_LOGPATH = 'ACTS_LOGPATH' 28 # An environment variable that enables test case failures to log stack traces. 29 _ENV_TEST_FAILURE_TRACEBACKS = 'ACTS_TEST_FAILURE_TRACEBACKS' 30 # An environment variable defining the test search paths for ACTS. 31 _ENV_ACTS_TESTPATHS = 'ACTS_TESTPATHS' 32 _PATH_SEPARATOR = ':' 33 34 35 class ActsConfigError(Exception): 36 """Raised when there is a problem in test configuration file.""" 37 38 39 def _validate_test_config(test_config): 40 """Validates the raw configuration loaded from the config file. 41 42 Making sure all the required fields exist. 43 """ 44 for k in keys.Config.reserved_keys.value: 45 if k not in test_config: 46 raise ActsConfigError( 47 "Required key %s missing in test config." % k) 48 49 50 def _validate_testbed_name(name): 51 """Validates the name of a test bed. 52 53 Since test bed names are used as part of the test run id, it needs to meet 54 certain requirements. 55 56 Args: 57 name: The test bed's name specified in config file. 58 59 Raises: 60 If the name does not meet any criteria, ActsConfigError is raised. 61 """ 62 if not name: 63 raise ActsConfigError("Test bed names can't be empty.") 64 if not isinstance(name, str): 65 raise ActsConfigError("Test bed names have to be string.") 66 for l in name: 67 if l not in utils.valid_filename_chars: 68 raise ActsConfigError( 69 "Char '%s' is not allowed in test bed names." % l) 70 71 72 def _update_file_paths(config, config_path): 73 """ Checks if the path entries are valid. 74 75 If the file path is invalid, assume it is a relative path and append 76 that to the config file path. 77 78 Args: 79 config : the config object to verify. 80 config_path : The path to the config file, which can be used to 81 generate absolute paths from relative paths in configs. 82 83 Raises: 84 If the file path is invalid, ActsConfigError is raised. 85 """ 86 # Check the file_path_keys and update if it is a relative path. 87 for file_path_key in keys.Config.file_path_keys.value: 88 if file_path_key in config: 89 config_file = config[file_path_key] 90 if type(config_file) is str: 91 if not os.path.isfile(config_file): 92 config_file = os.path.join(config_path, config_file) 93 if not os.path.isfile(config_file): 94 raise ActsConfigError("Unable to load config %s from test " 95 "config file.", config_file) 96 config[file_path_key] = config_file 97 98 99 def _validate_testbed_configs(testbed_configs, config_path): 100 """Validates the testbed configurations. 101 102 Args: 103 testbed_configs: A list of testbed configuration json objects. 104 config_path : The path to the config file, which can be used to 105 generate absolute paths from relative paths in configs. 106 107 Raises: 108 If any part of the configuration is invalid, ActsConfigError is raised. 109 """ 110 # Cross checks testbed configs for resource conflicts. 111 for name, config in testbed_configs.items(): 112 _update_file_paths(config, config_path) 113 _validate_testbed_name(name) 114 115 116 def gen_term_signal_handler(test_runners): 117 def termination_sig_handler(signal_num, frame): 118 print('Received sigterm %s.' % signal_num) 119 for t in test_runners: 120 t.stop() 121 sys.exit(1) 122 123 return termination_sig_handler 124 125 126 def _parse_one_test_specifier(item): 127 """Parse one test specifier from command line input. 128 129 This also verifies that the test class name and test case names follow 130 ACTS's naming conventions. A test class name has to end with "Test"; a test 131 case name has to start with "test". 132 133 Args: 134 item: A string that specifies a test class or test cases in one test 135 class to run. 136 137 Returns: 138 A tuple of a string and a list of strings. The string is the test class 139 name, the list of strings is a list of test case names. The list can be 140 None. 141 """ 142 tokens = item.split(':') 143 if len(tokens) > 2: 144 raise ActsConfigError("Syntax error in test specifier %s" % item) 145 if len(tokens) == 1: 146 # This should be considered a test class name 147 test_cls_name = tokens[0] 148 return test_cls_name, None 149 elif len(tokens) == 2: 150 # This should be considered a test class name followed by 151 # a list of test case names. 152 test_cls_name, test_case_names = tokens 153 clean_names = [] 154 for elem in test_case_names.split(','): 155 test_case_name = elem.strip() 156 if not test_case_name.startswith("test_"): 157 raise ActsConfigError( 158 ("Requested test case '%s' in test class " 159 "'%s' does not follow the test case " 160 "naming convention test_*.") % (test_case_name, 161 test_cls_name)) 162 clean_names.append(test_case_name) 163 return test_cls_name, clean_names 164 165 166 def parse_test_list(test_list): 167 """Parse user provided test list into internal format for test_runner. 168 169 Args: 170 test_list: A list of test classes/cases. 171 """ 172 result = [] 173 for elem in test_list: 174 result.append(_parse_one_test_specifier(elem)) 175 return result 176 177 178 def test_randomizer(test_identifiers, test_case_iterations=10): 179 """Generate test lists by randomizing user provided test list. 180 181 Args: 182 test_identifiers: A list of test classes/cases. 183 test_case_iterations: The range of random iterations for each case. 184 Returns: 185 A list of randomized test cases. 186 """ 187 random_tests = [] 188 preflight_tests = [] 189 postflight_tests = [] 190 for test_class, test_cases in test_identifiers: 191 if "Preflight" in test_class: 192 preflight_tests.append((test_class, test_cases)) 193 elif "Postflight" in test_class: 194 postflight_tests.append((test_class, test_cases)) 195 else: 196 for test_case in test_cases: 197 random_tests.append((test_class, 198 [test_case] * random.randrange( 199 1, test_case_iterations + 1))) 200 random.shuffle(random_tests) 201 new_tests = [] 202 previous_class = None 203 for test_class, test_cases in random_tests: 204 if test_class == previous_class: 205 previous_cases = new_tests[-1][1] 206 previous_cases.extend(test_cases) 207 else: 208 new_tests.append((test_class, test_cases)) 209 previous_class = test_class 210 return preflight_tests + new_tests + postflight_tests 211 212 213 def load_test_config_file(test_config_path, 214 tb_filters=None, 215 override_test_path=None, 216 override_log_path=None, 217 override_test_args=None, 218 override_random=None, 219 override_test_case_iterations=None): 220 """Processes the test configuration file provided by the user. 221 222 Loads the configuration file into a json object, unpacks each testbed 223 config into its own json object, and validate the configuration in the 224 process. 225 226 Args: 227 test_config_path: Path to the test configuration file. 228 tb_filters: A subset of test bed names to be pulled from the config 229 file. If None, then all test beds will be selected. 230 override_test_path: If not none the test path to use instead. 231 override_log_path: If not none the log path to use instead. 232 override_test_args: If not none the test args to use instead. 233 override_random: If not None, override the config file value. 234 override_test_case_iterations: If not None, override the config file 235 value. 236 237 Returns: 238 A list of test configuration json objects to be passed to 239 test_runner.TestRunner. 240 """ 241 configs = utils.load_config(test_config_path) 242 if override_test_path: 243 configs[keys.Config.key_test_paths.value] = override_test_path 244 if override_log_path: 245 configs[keys.Config.key_log_path.value] = override_log_path 246 if override_test_args: 247 configs[keys.Config.ikey_cli_args.value] = override_test_args 248 if override_random: 249 configs[keys.Config.key_random.value] = override_random 250 if override_test_case_iterations: 251 configs[keys.Config.key_test_case_iterations.value] = \ 252 override_test_case_iterations 253 254 testbeds = configs[keys.Config.key_testbed.value] 255 if type(testbeds) is list: 256 tb_dict = dict() 257 for testbed in testbeds: 258 tb_dict[testbed[keys.Config.key_testbed_name.value]] = testbed 259 testbeds = tb_dict 260 elif type(testbeds) is dict: 261 # For compatibility, make sure the entry name is the same as 262 # the testbed's "name" entry 263 for name, testbed in testbeds.items(): 264 testbed[keys.Config.key_testbed_name.value] = name 265 266 if tb_filters: 267 tbs = {} 268 for name in tb_filters: 269 if name in testbeds: 270 tbs[name] = testbeds[name] 271 else: 272 raise ActsConfigError( 273 'Expected testbed named "%s", but none was found. Check' 274 'if you have the correct testbed names.' % name) 275 testbeds = tbs 276 277 if (keys.Config.key_log_path.value not in configs 278 and _ENV_ACTS_LOGPATH in os.environ): 279 print('Using environment log path: %s' % 280 (os.environ[_ENV_ACTS_LOGPATH])) 281 configs[keys.Config.key_log_path.value] = os.environ[_ENV_ACTS_LOGPATH] 282 if (keys.Config.key_test_paths.value not in configs 283 and _ENV_ACTS_TESTPATHS in os.environ): 284 print('Using environment test paths: %s' % 285 (os.environ[_ENV_ACTS_TESTPATHS])) 286 configs[keys.Config.key_test_paths.value] = os.environ[ 287 _ENV_ACTS_TESTPATHS].split(_PATH_SEPARATOR) 288 if (keys.Config.key_test_failure_tracebacks not in configs 289 and _ENV_TEST_FAILURE_TRACEBACKS in os.environ): 290 configs[keys.Config.key_test_failure_tracebacks.value] = os.environ[ 291 _ENV_TEST_FAILURE_TRACEBACKS] 292 293 # Add the global paths to the global config. 294 k_log_path = keys.Config.key_log_path.value 295 configs[k_log_path] = utils.abs_path(configs[k_log_path]) 296 297 # TODO: See if there is a better way to do this: b/29836695 298 config_path, _ = os.path.split(utils.abs_path(test_config_path)) 299 configs[keys.Config.key_config_path] = config_path 300 _validate_test_config(configs) 301 _validate_testbed_configs(testbeds, config_path) 302 # Unpack testbeds into separate json objects. 303 configs.pop(keys.Config.key_testbed.value) 304 config_jsons = [] 305 306 for _, original_bed_config in testbeds.items(): 307 new_test_config = dict(configs) 308 new_test_config[keys.Config.key_testbed.value] = original_bed_config 309 # Keys in each test bed config will be copied to a level up to be 310 # picked up for user_params. If the key already exists in the upper 311 # level, the local one defined in test bed config overwrites the 312 # general one. 313 new_test_config.update(original_bed_config) 314 config_jsons.append(new_test_config) 315 return config_jsons 316 317 318 def parse_test_file(fpath): 319 """Parses a test file that contains test specifiers. 320 321 Args: 322 fpath: A string that is the path to the test file to parse. 323 324 Returns: 325 A list of strings, each is a test specifier. 326 """ 327 with open(fpath, 'r') as f: 328 tf = [] 329 for line in f: 330 line = line.strip() 331 if not line: 332 continue 333 if len(tf) and (tf[-1].endswith(':') or tf[-1].endswith(',')): 334 tf[-1] += line 335 else: 336 tf.append(line) 337 return tf 338