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