1# 2# Copyright (C) 2016 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16 17from future import standard_library 18standard_library.install_aliases() 19 20import copy 21import importlib 22import inspect 23import logging 24import os 25import pkgutil 26import signal 27import sys 28import thread 29import threading 30 31from vts.runners.host import base_test 32from vts.runners.host import config_parser 33from vts.runners.host import keys 34from vts.runners.host import logger 35from vts.runners.host import records 36from vts.runners.host import signals 37from vts.runners.host import utils 38 39 40def main(): 41 """Execute the test class in a test module. 42 43 This is to be used in a test script's main so the script can be executed 44 directly. It will discover all the classes that inherit from BaseTestClass 45 and excute them. all the test results will be aggregated into one. 46 47 A VTS host-driven test case has three args: 48 1st arg: the path of a test case config file. 49 2nd arg: the serial ID of a target device (device config). 50 3rd arg: the path of a test case data dir. 51 52 Returns: 53 The TestResult object that holds the results of the test run. 54 """ 55 test_classes = [] 56 main_module_members = sys.modules["__main__"] 57 for _, module_member in main_module_members.__dict__.items(): 58 if inspect.isclass(module_member): 59 if issubclass(module_member, base_test.BaseTestClass): 60 test_classes.append(module_member) 61 # TODO(angli): Need to handle the case where more than one test class is in 62 # a test script. The challenge is to handle multiple configs and how to do 63 # default config in this case. 64 if len(test_classes) != 1: 65 logging.error("Expected 1 test class per file, found %s.", 66 len(test_classes)) 67 sys.exit(1) 68 test_result = runTestClass(test_classes[0]) 69 return test_result 70 71 72def runTestClass(test_class): 73 """Execute one test class. 74 75 This will create a TestRunner, execute one test run with one test class. 76 77 Args: 78 test_class: The test class to instantiate and execute. 79 80 Returns: 81 The TestResult object that holds the results of the test run. 82 """ 83 test_cls_name = test_class.__name__ 84 if len(sys.argv) < 2: 85 logging.warning("Missing a configuration file. Using the default.") 86 test_configs = [config_parser.GetDefaultConfig(test_cls_name)] 87 else: 88 try: 89 config_path = sys.argv[1] 90 baseline_config = config_parser.GetDefaultConfig(test_cls_name) 91 baseline_config[keys.ConfigKeys.KEY_TESTBED] = [ 92 baseline_config[keys.ConfigKeys.KEY_TESTBED] 93 ] 94 test_configs = config_parser.load_test_config_file( 95 config_path, baseline_config=baseline_config) 96 except IndexError: 97 logging.error("No valid config file found.") 98 sys.exit(1) 99 except Exception as e: 100 logging.error("Unexpected exception") 101 logging.exception(e) 102 103 test_identifiers = [(test_cls_name, None)] 104 105 for config in test_configs: 106 if keys.ConfigKeys.KEY_TEST_MAX_TIMEOUT in config: 107 timeout_sec = int(config[keys.ConfigKeys.KEY_TEST_MAX_TIMEOUT]) / 1000.0 108 else: 109 timeout_sec = 60 * 60 * 3 110 logging.warning("%s unspecified. Set timeout to %s seconds.", 111 keys.ConfigKeys.KEY_TEST_MAX_TIMEOUT, timeout_sec) 112 # The default SIGINT handler sends KeyboardInterrupt to main thread. 113 # On Windows, raising CTRL_C_EVENT, which is received as SIGINT, 114 # has no effect on non-console process. interrupt_main() works but 115 # does not unblock main thread's IO immediately. 116 timeout_func = (raiseSigint if not utils.is_on_windows() else 117 thread.interrupt_main) 118 sig_timer = threading.Timer(timeout_sec, timeout_func) 119 120 tr = TestRunner(config, test_identifiers) 121 tr.parseTestConfig(config) 122 try: 123 sig_timer.start() 124 tr.runTestClass(test_class, None) 125 except KeyboardInterrupt as e: 126 logging.exception("Aborted by timeout or ctrl+C: %s", e) 127 except Exception as e: 128 logging.error("Unexpected exception") 129 logging.exception(e) 130 finally: 131 sig_timer.cancel() 132 tr.stop() 133 return tr.results 134 135 136def raiseSigint(): 137 """Raises SIGINT.""" 138 os.kill(os.getpid(), signal.SIGINT) 139 140 141class TestRunner(object): 142 """The class that instantiates test classes, executes test cases, and 143 report results. 144 145 Attributes: 146 test_run_info: A dictionary containing the information needed by 147 test classes for this test run, including params, 148 controllers, and other objects. All of these will 149 be passed to test classes. 150 test_configs: A dictionary that is the original test configuration 151 passed in by user. 152 id: A string that is the unique identifier of this test run. 153 log_path: A string representing the path of the dir under which 154 all logs from this test run should be written. 155 controller_registry: A dictionary that holds the controller 156 objects used in a test run. 157 controller_destructors: A dictionary that holds the controller 158 distructors. Keys are controllers' names. 159 run_list: A list of tuples specifying what tests to run. 160 results: The test result object used to record the results of 161 this test run. 162 running: A boolean signifies whether this test run is ongoing or 163 not. 164 test_cls_instances: list of test class instances that were executed 165 or scheduled to be executed. 166 """ 167 168 def __init__(self, test_configs, run_list): 169 self.test_run_info = {} 170 self.test_run_info[keys.ConfigKeys.IKEY_DATA_FILE_PATH] = getattr( 171 test_configs, keys.ConfigKeys.IKEY_DATA_FILE_PATH, "./") 172 self.test_configs = test_configs 173 self.testbed_configs = self.test_configs[keys.ConfigKeys.KEY_TESTBED] 174 self.testbed_name = self.testbed_configs[ 175 keys.ConfigKeys.KEY_TESTBED_NAME] 176 start_time = logger.getLogFileTimestamp() 177 self.id = "{}@{}".format(self.testbed_name, start_time) 178 # log_path should be set before parsing configs. 179 l_path = os.path.join(self.test_configs[keys.ConfigKeys.KEY_LOG_PATH], 180 self.testbed_name, start_time) 181 self.log_path = os.path.abspath(l_path) 182 logger.setupTestLogger(self.log_path, self.testbed_name) 183 self.controller_registry = {} 184 self.controller_destructors = {} 185 self.run_list = run_list 186 self.results = records.TestResult() 187 self.running = False 188 self.test_cls_instances = [] 189 190 def __enter__(self): 191 return self 192 193 def __exit__(self, *args): 194 self.stop() 195 196 def importTestModules(self, test_paths): 197 """Imports test classes from test scripts. 198 199 1. Locate all .py files under test paths. 200 2. Import the .py files as modules. 201 3. Find the module members that are test classes. 202 4. Categorize the test classes by name. 203 204 Args: 205 test_paths: A list of directory paths where the test files reside. 206 207 Returns: 208 A dictionary where keys are test class name strings, values are 209 actual test classes that can be instantiated. 210 """ 211 212 def is_testfile_name(name, ext): 213 if ext == ".py": 214 if name.endswith("Test") or name.endswith("_test"): 215 return True 216 return False 217 218 file_list = utils.find_files(test_paths, is_testfile_name) 219 test_classes = {} 220 for path, name, _ in file_list: 221 sys.path.append(path) 222 try: 223 module = importlib.import_module(name) 224 except: 225 for test_cls_name, _ in self.run_list: 226 alt_name = name.replace('_', '').lower() 227 alt_cls_name = test_cls_name.lower() 228 # Only block if a test class on the run list causes an 229 # import error. We need to check against both naming 230 # conventions: AaaBbb and aaa_bbb. 231 if name == test_cls_name or alt_name == alt_cls_name: 232 msg = ("Encountered error importing test class %s, " 233 "abort.") % test_cls_name 234 # This exception is logged here to help with debugging 235 # under py2, because "raise X from Y" syntax is only 236 # supported under py3. 237 logging.exception(msg) 238 raise USERError(msg) 239 continue 240 for member_name in dir(module): 241 if not member_name.startswith("__"): 242 if member_name.endswith("Test"): 243 test_class = getattr(module, member_name) 244 if inspect.isclass(test_class): 245 test_classes[member_name] = test_class 246 return test_classes 247 248 def verifyControllerModule(self, module): 249 """Verifies a module object follows the required interface for 250 controllers. 251 252 Args: 253 module: An object that is a controller module. This is usually 254 imported with import statements or loaded by importlib. 255 256 Raises: 257 ControllerError is raised if the module does not match the vts.runners.host 258 controller interface, or one of the required members is null. 259 """ 260 required_attributes = ("create", "destroy", 261 "VTS_CONTROLLER_CONFIG_NAME") 262 for attr in required_attributes: 263 if not hasattr(module, attr): 264 raise signals.ControllerError( 265 ("Module %s missing required " 266 "controller module attribute %s.") % (module.__name__, 267 attr)) 268 if not getattr(module, attr): 269 raise signals.ControllerError( 270 ("Controller interface %s in %s " 271 "cannot be null.") % (attr, module.__name__)) 272 273 def registerController(self, module, start_services=True): 274 """Registers a controller module for a test run. 275 276 This declares a controller dependency of this test class. If the target 277 module exists and matches the controller interface, the controller 278 module will be instantiated with corresponding configs in the test 279 config file. The module should be imported first. 280 281 Params: 282 module: A module that follows the controller module interface. 283 start_services: boolean, controls whether services (e.g VTS agent) 284 are started on the target. 285 286 Returns: 287 A list of controller objects instantiated from controller_module. 288 289 Raises: 290 ControllerError is raised if no corresponding config can be found, 291 or if the controller module has already been registered. 292 """ 293 logging.info("cwd: %s", os.getcwd()) 294 logging.info("adb devices: %s", module.list_adb_devices()) 295 self.verifyControllerModule(module) 296 module_ref_name = module.__name__.split('.')[-1] 297 if module_ref_name in self.controller_registry: 298 raise signals.ControllerError( 299 ("Controller module %s has already " 300 "been registered. It can not be " 301 "registered again.") % module_ref_name) 302 # Create controller objects. 303 create = module.create 304 module_config_name = module.VTS_CONTROLLER_CONFIG_NAME 305 if module_config_name not in self.testbed_configs: 306 raise signals.ControllerError(("No corresponding config found for" 307 " %s") % module_config_name) 308 try: 309 # Make a deep copy of the config to pass to the controller module, 310 # in case the controller module modifies the config internally. 311 original_config = self.testbed_configs[module_config_name] 312 controller_config = copy.deepcopy(original_config) 313 logging.info("controller_config: %s", controller_config) 314 if "use_vts_agent" not in self.testbed_configs: 315 objects = create(controller_config, start_services) 316 else: 317 objects = create(controller_config, 318 self.testbed_configs["use_vts_agent"]) 319 except: 320 logging.exception(("Failed to initialize objects for controller " 321 "%s, abort!"), module_config_name) 322 raise 323 if not isinstance(objects, list): 324 raise ControllerError(("Controller module %s did not return a list" 325 " of objects, abort.") % module_ref_name) 326 self.controller_registry[module_ref_name] = objects 327 logging.debug("Found %d objects for controller %s", 328 len(objects), module_config_name) 329 destroy_func = module.destroy 330 self.controller_destructors[module_ref_name] = destroy_func 331 return objects 332 333 def unregisterControllers(self): 334 """Destroy controller objects and clear internal registry. 335 336 This will be called at the end of each TestRunner.run call. 337 """ 338 for name, destroy in self.controller_destructors.items(): 339 try: 340 logging.debug("Destroying %s.", name) 341 dut = self.controller_destructors[name][0] 342 destroy(self.controller_registry[name]) 343 except: 344 logging.exception("Exception occurred destroying %s.", name) 345 self.controller_registry = {} 346 self.controller_destructors = {} 347 348 def parseTestConfig(self, test_configs): 349 """Parses the test configuration and unpacks objects and parameters 350 into a dictionary to be passed to test classes. 351 352 Args: 353 test_configs: A json object representing the test configurations. 354 """ 355 self.test_run_info[ 356 keys.ConfigKeys.IKEY_TESTBED_NAME] = self.testbed_name 357 # Unpack other params. 358 self.test_run_info["registerController"] = self.registerController 359 self.test_run_info[keys.ConfigKeys.IKEY_LOG_PATH] = self.log_path 360 user_param_pairs = [] 361 for item in test_configs.items(): 362 if item[0] not in keys.ConfigKeys.RESERVED_KEYS: 363 user_param_pairs.append(item) 364 self.test_run_info[keys.ConfigKeys.IKEY_USER_PARAM] = copy.deepcopy( 365 dict(user_param_pairs)) 366 367 def runTestClass(self, test_cls, test_cases=None): 368 """Instantiates and executes a test class. 369 370 If test_cases is None, the test cases listed by self.tests will be 371 executed instead. If self.tests is empty as well, no test case in this 372 test class will be executed. 373 374 Args: 375 test_cls: The test class to be instantiated and executed. 376 test_cases: List of test case names to execute within the class. 377 378 Returns: 379 A tuple, with the number of cases passed at index 0, and the total 380 number of test cases at index 1. 381 """ 382 self.running = True 383 with test_cls(self.test_run_info) as test_cls_instance: 384 try: 385 if test_cls_instance not in self.test_cls_instances: 386 self.test_cls_instances.append(test_cls_instance) 387 cls_result = test_cls_instance.run(test_cases) 388 except signals.TestAbortAll as e: 389 raise e 390 391 def run(self): 392 """Executes test cases. 393 394 This will instantiate controller and test classes, and execute test 395 classes. This can be called multiple times to repeatly execute the 396 requested test cases. 397 398 A call to TestRunner.stop should eventually happen to conclude the life 399 cycle of a TestRunner. 400 401 Args: 402 test_classes: A dictionary where the key is test class name, and 403 the values are actual test classes. 404 """ 405 if not self.running: 406 self.running = True 407 # Initialize controller objects and pack appropriate objects/params 408 # to be passed to test class. 409 self.parseTestConfig(self.test_configs) 410 t_configs = self.test_configs[keys.ConfigKeys.KEY_TEST_PATHS] 411 test_classes = self.importTestModules(t_configs) 412 logging.debug("Executing run list %s.", self.run_list) 413 try: 414 for test_cls_name, test_case_names in self.run_list: 415 if not self.running: 416 break 417 if test_case_names: 418 logging.debug("Executing test cases %s in test class %s.", 419 test_case_names, test_cls_name) 420 else: 421 logging.debug("Executing test class %s", test_cls_name) 422 try: 423 test_cls = test_classes[test_cls_name] 424 except KeyError: 425 raise USERError( 426 ("Unable to locate class %s in any of the test " 427 "paths specified.") % test_cls_name) 428 try: 429 self.runTestClass(test_cls, test_case_names) 430 except signals.TestAbortAll as e: 431 logging.warning( 432 ("Abort all subsequent test classes. Reason: " 433 "%s"), e) 434 raise 435 except Exception as e: 436 logging.error("Unexpected exception") 437 logging.exception(e) 438 finally: 439 self.unregisterControllers() 440 441 def stop(self): 442 """Releases resources from test run. Should always be called after 443 TestRunner.run finishes. 444 445 This function concludes a test run and writes out a test report. 446 """ 447 if self.running: 448 449 for test_cls_instance in self.test_cls_instances: 450 self.results += test_cls_instance.results 451 452 msg = "\nSummary for test run %s: %s\n" % (self.id, 453 self.results.summary()) 454 self._writeResultsJsonString() 455 logging.info(msg.strip()) 456 logger.killTestLogger(logging.getLogger()) 457 self.running = False 458 459 def _writeResultsJsonString(self): 460 """Writes out a json file with the test result info for easy parsing. 461 """ 462 path = os.path.join(self.log_path, "test_run_summary.json") 463 with open(path, 'w') as f: 464 f.write(self.results.jsonString()) 465