Home | History | Annotate | Download | only in acts
      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 future import standard_library
     18 standard_library.install_aliases()
     19 
     20 import argparse
     21 import copy
     22 import importlib
     23 import inspect
     24 import logging
     25 import os
     26 import pkgutil
     27 import sys
     28 
     29 from acts import base_test
     30 from acts import config_parser
     31 from acts import keys
     32 from acts import logger
     33 from acts import records
     34 from acts import signals
     35 from acts import utils
     36 
     37 
     38 def main():
     39     """Execute the test class in a test module.
     40 
     41     This is the default entry point for running a test script file directly.
     42     In this case, only one test class in a test script is allowed.
     43 
     44     To make your test script executable, add the following to your file:
     45 
     46         from acts import test_runner
     47         ...
     48         if __name__ == "__main__":
     49             test_runner.main()
     50 
     51     If you want to implement your own cli entry point, you could use function
     52     execute_one_test_class(test_class, test_config, test_identifier)
     53     """
     54     # Parse cli args.
     55     parser = argparse.ArgumentParser(description="ACTS Test Executable.")
     56     parser.add_argument(
     57         '-c',
     58         '--config',
     59         nargs=1,
     60         type=str,
     61         required=True,
     62         metavar="<PATH>",
     63         help="Path to the test configuration file.")
     64     parser.add_argument(
     65         '--test_case',
     66         nargs='+',
     67         type=str,
     68         metavar="[test_a test_b...]",
     69         help="A list of test case names in the test script.")
     70     parser.add_argument(
     71         '-tb',
     72         '--test_bed',
     73         nargs='+',
     74         type=str,
     75         metavar="[<TEST BED NAME1> <TEST BED NAME2> ...]",
     76         help="Specify which test beds to run tests on.")
     77     args = parser.parse_args(sys.argv[1:])
     78     # Load test config file.
     79     test_configs = config_parser.load_test_config_file(args.config[0],
     80                                                        args.test_bed)
     81     # Find the test class in the test script.
     82     test_class = _find_test_class()
     83     test_class_name = test_class.__name__
     84     # Parse test case specifiers if exist.
     85     test_case_names = None
     86     if args.test_case:
     87         test_case_names = args.test_case
     88     test_identifier = [(test_class_name, test_case_names)]
     89     # Execute the test class with configs.
     90     ok = True
     91     for config in test_configs:
     92         try:
     93             result = execute_one_test_class(test_class, config,
     94                                             test_identifier)
     95             ok = result and ok
     96         except signals.TestAbortAll:
     97             pass
     98         except:
     99             logging.exception("Error occurred when executing test bed %s",
    100                               config[keys.Config.key_testbed.value])
    101             ok = False
    102     if not ok:
    103         sys.exit(1)
    104 
    105 
    106 def _find_test_class():
    107     """Finds the test class in a test script.
    108 
    109     Walk through module memebers and find the subclass of BaseTestClass. Only
    110     one subclass is allowed in a test script.
    111 
    112     Returns:
    113         The test class in the test module.
    114     """
    115     test_classes = []
    116     main_module_members = sys.modules["__main__"]
    117     for _, module_member in main_module_members.__dict__.items():
    118         if inspect.isclass(module_member):
    119             if issubclass(module_member, base_test.BaseTestClass):
    120                 test_classes.append(module_member)
    121     if len(test_classes) != 1:
    122         logging.error("Expected 1 test class per file, found %s.",
    123                       [t.__name__ for t in test_classes])
    124         sys.exit(1)
    125     return test_classes[0]
    126 
    127 
    128 def execute_one_test_class(test_class, test_config, test_identifier):
    129     """Executes one specific test class.
    130 
    131     You could call this function in your own cli test entry point if you choose
    132     not to use act.py or test_runner.main.
    133 
    134     Args:
    135         test_class: A subclass of acts.base_test.BaseTestClass that has the test
    136                     logic to be executed.
    137         test_config: A dict representing one set of configs for a test run.
    138         test_identifier: A list of tuples specifying which test cases to run in
    139                          the test class.
    140 
    141     Returns:
    142         True if all tests passed without any error, False otherwise.
    143 
    144     Raises:
    145         If signals.TestAbortAll is raised by a test run, pipe it through.
    146     """
    147     tr = TestRunner(test_config, test_identifier)
    148     try:
    149         tr.run(test_class)
    150         return tr.results.is_all_pass
    151     except signals.TestAbortAll:
    152         raise
    153     except:
    154         logging.exception("Exception when executing %s.", tr.testbed_name)
    155     finally:
    156         tr.stop()
    157 
    158 
    159 class TestRunner(object):
    160     """The class that instantiates test classes, executes test cases, and
    161     report results.
    162 
    163     Attributes:
    164         self.test_run_info: A dictionary containing the information needed by
    165                             test classes for this test run, including params,
    166                             controllers, and other objects. All of these will
    167                             be passed to test classes.
    168         self.test_configs: A dictionary that is the original test configuration
    169                            passed in by user.
    170         self.id: A string that is the unique identifier of this test run.
    171         self.log_path: A string representing the path of the dir under which
    172                        all logs from this test run should be written.
    173         self.log: The logger object used throughout this test run.
    174         self.controller_registry: A dictionary that holds the controller
    175                                   objects used in a test run.
    176         self.controller_destructors: A dictionary that holds the controller
    177                                      distructors. Keys are controllers' names.
    178         self.test_classes: A dictionary where we can look up the test classes
    179                            by name to instantiate.
    180         self.run_list: A list of tuples specifying what tests to run.
    181         self.results: The test result object used to record the results of
    182                       this test run.
    183         self.running: A boolean signifies whether this test run is ongoing or
    184                       not.
    185     """
    186 
    187     def __init__(self, test_configs, run_list):
    188         self.test_run_info = {}
    189         self.test_configs = test_configs
    190         self.testbed_configs = self.test_configs[keys.Config.key_testbed.value]
    191         self.testbed_name = self.testbed_configs[
    192             keys.Config.key_testbed_name.value]
    193         start_time = logger.get_log_file_timestamp()
    194         self.id = "{}@{}".format(self.testbed_name, start_time)
    195         # log_path should be set before parsing configs.
    196         l_path = os.path.join(
    197             self.test_configs[keys.Config.key_log_path.value],
    198             self.testbed_name, start_time)
    199         self.log_path = os.path.abspath(l_path)
    200         logger.setup_test_logger(self.log_path, self.testbed_name)
    201         self.log = logging.getLogger()
    202         self.controller_registry = {}
    203         self.controller_destructors = {}
    204         if self.test_configs.get(keys.Config.key_random.value):
    205             test_case_iterations = self.test_configs.get(
    206                 keys.Config.key_test_case_iterations.value, 10)
    207             self.log.info(
    208                 "Campaign randomizer is enabled with test_case_iterations %s",
    209                 test_case_iterations)
    210             self.run_list = config_parser.test_randomizer(
    211                 run_list, test_case_iterations=test_case_iterations)
    212             self.write_test_campaign()
    213         else:
    214             self.run_list = run_list
    215         self.results = records.TestResult()
    216         self.running = False
    217 
    218     def import_test_modules(self, test_paths):
    219         """Imports test classes from test scripts.
    220 
    221         1. Locate all .py files under test paths.
    222         2. Import the .py files as modules.
    223         3. Find the module members that are test classes.
    224         4. Categorize the test classes by name.
    225 
    226         Args:
    227             test_paths: A list of directory paths where the test files reside.
    228 
    229         Returns:
    230             A dictionary where keys are test class name strings, values are
    231             actual test classes that can be instantiated.
    232         """
    233 
    234         def is_testfile_name(name, ext):
    235             if ext == ".py":
    236                 if name.endswith("Test") or name.endswith("_test"):
    237                     return True
    238             return False
    239 
    240         file_list = utils.find_files(test_paths, is_testfile_name)
    241         test_classes = {}
    242         for path, name, _ in file_list:
    243             sys.path.append(path)
    244             try:
    245                 module = importlib.import_module(name)
    246             except:
    247                 for test_cls_name, _ in self.run_list:
    248                     alt_name = name.replace('_', '').lower()
    249                     alt_cls_name = test_cls_name.lower()
    250                     # Only block if a test class on the run list causes an
    251                     # import error. We need to check against both naming
    252                     # conventions: AaaBbb and aaa_bbb.
    253                     if name == test_cls_name or alt_name == alt_cls_name:
    254                         msg = ("Encountered error importing test class %s, "
    255                                "abort.") % test_cls_name
    256                         # This exception is logged here to help with debugging
    257                         # under py2, because "raise X from Y" syntax is only
    258                         # supported under py3.
    259                         self.log.exception(msg)
    260                         raise ValueError(msg)
    261                 continue
    262             for member_name in dir(module):
    263                 if not member_name.startswith("__"):
    264                     if member_name.endswith("Test"):
    265                         test_class = getattr(module, member_name)
    266                         if inspect.isclass(test_class):
    267                             test_classes[member_name] = test_class
    268         return test_classes
    269 
    270     def _import_builtin_controllers(self):
    271         """Import built-in controller modules.
    272 
    273         Go through the testbed configs, find any built-in controller configs
    274         and import the corresponding controller module from acts.controllers
    275         package.
    276 
    277         TODO(angli): Remove this when all scripts change to explicitly declare
    278                      controller dependency.
    279 
    280         Returns:
    281             A list of controller modules.
    282         """
    283         builtin_controllers = []
    284         for ctrl_name in keys.Config.builtin_controller_names.value:
    285             if ctrl_name in self.testbed_configs:
    286                 module_name = keys.get_module_name(ctrl_name)
    287                 module = importlib.import_module("acts.controllers.%s" %
    288                                                  module_name)
    289                 builtin_controllers.append(module)
    290         return builtin_controllers
    291 
    292     @staticmethod
    293     def verify_controller_module(module):
    294         """Verifies a module object follows the required interface for
    295         controllers.
    296 
    297         Args:
    298             module: An object that is a controller module. This is usually
    299                     imported with import statements or loaded by importlib.
    300 
    301         Raises:
    302             ControllerError is raised if the module does not match the ACTS
    303             controller interface, or one of the required members is null.
    304         """
    305         required_attributes = ("create", "destroy",
    306                                "ACTS_CONTROLLER_CONFIG_NAME")
    307         for attr in required_attributes:
    308             if not hasattr(module, attr):
    309                 raise signals.ControllerError(
    310                     ("Module %s missing required "
    311                      "controller module attribute %s.") % (module.__name__,
    312                                                            attr))
    313             if not getattr(module, attr):
    314                 raise signals.ControllerError(
    315                     "Controller interface %s in %s cannot be null." % (
    316                      attr, module.__name__))
    317 
    318     def register_controller(self, module, required=True):
    319         """Registers an ACTS controller module for a test run.
    320 
    321         An ACTS controller module is a Python lib that can be used to control
    322         a device, service, or equipment. To be ACTS compatible, a controller
    323         module needs to have the following members:
    324 
    325             def create(configs):
    326                 [Required] Creates controller objects from configurations.
    327                 Args:
    328                     configs: A list of serialized data like string/dict. Each
    329                              element of the list is a configuration for a
    330                              controller object.
    331                 Returns:
    332                     A list of objects.
    333 
    334             def destroy(objects):
    335                 [Required] Destroys controller objects created by the create
    336                 function. Each controller object shall be properly cleaned up
    337                 and all the resources held should be released, e.g. memory
    338                 allocation, sockets, file handlers etc.
    339                 Args:
    340                     A list of controller objects created by the create function.
    341 
    342             def get_info(objects):
    343                 [Optional] Gets info from the controller objects used in a test
    344                 run. The info will be included in test_result_summary.json under
    345                 the key "ControllerInfo". Such information could include unique
    346                 ID, version, or anything that could be useful for describing the
    347                 test bed and debugging.
    348                 Args:
    349                     objects: A list of controller objects created by the create
    350                              function.
    351                 Returns:
    352                     A list of json serializable objects, each represents the
    353                     info of a controller object. The order of the info object
    354                     should follow that of the input objects.
    355 
    356         Registering a controller module declares a test class's dependency the
    357         controller. If the module config exists and the module matches the
    358         controller interface, controller objects will be instantiated with
    359         corresponding configs. The module should be imported first.
    360 
    361         Args:
    362             module: A module that follows the controller module interface.
    363             required: A bool. If True, failing to register the specified
    364                       controller module raises exceptions. If False, returns
    365                       None upon failures.
    366 
    367         Returns:
    368             A list of controller objects instantiated from controller_module, or
    369             None.
    370 
    371         Raises:
    372             When required is True, ControllerError is raised if no corresponding
    373             config can be found.
    374             Regardless of the value of "required", ControllerError is raised if
    375             the controller module has already been registered or any other error
    376             occurred in the registration process.
    377         """
    378         TestRunner.verify_controller_module(module)
    379         try:
    380             # If this is a builtin controller module, use the default ref name.
    381             module_ref_name = module.ACTS_CONTROLLER_REFERENCE_NAME
    382             builtin = True
    383         except AttributeError:
    384             # Or use the module's name
    385             builtin = False
    386             module_ref_name = module.__name__.split('.')[-1]
    387         if module_ref_name in self.controller_registry:
    388             raise signals.ControllerError(
    389                 ("Controller module %s has already been registered. It can not"
    390                  " be registered again.") % module_ref_name)
    391         # Create controller objects.
    392         create = module.create
    393         module_config_name = module.ACTS_CONTROLLER_CONFIG_NAME
    394         if module_config_name not in self.testbed_configs:
    395             if required:
    396                 raise signals.ControllerError(
    397                     "No corresponding config found for %s" %
    398                     module_config_name)
    399             self.log.warning(
    400                 "No corresponding config found for optional controller %s",
    401                 module_config_name)
    402             return None
    403         try:
    404             # Make a deep copy of the config to pass to the controller module,
    405             # in case the controller module modifies the config internally.
    406             original_config = self.testbed_configs[module_config_name]
    407             controller_config = copy.deepcopy(original_config)
    408             objects = create(controller_config)
    409         except:
    410             self.log.exception(
    411                 "Failed to initialize objects for controller %s, abort!",
    412                 module_config_name)
    413             raise
    414         if not isinstance(objects, list):
    415             raise signals.ControllerError(
    416                 "Controller module %s did not return a list of objects, abort."
    417                 % module_ref_name)
    418         self.controller_registry[module_ref_name] = objects
    419         # Collect controller information and write to test result.
    420         # Implementation of "get_info" is optional for a controller module.
    421         if hasattr(module, "get_info"):
    422             controller_info = module.get_info(objects)
    423             self.log.info("Controller %s: %s", module_config_name,
    424                           controller_info)
    425             self.results.add_controller_info(module_config_name,
    426                                              controller_info)
    427         else:
    428             self.log.warning("No controller info obtained for %s",
    429                              module_config_name)
    430         # TODO(angli): After all tests move to register_controller, stop
    431         # tracking controller objs in test_run_info.
    432         if builtin:
    433             self.test_run_info[module_ref_name] = objects
    434         self.log.debug("Found %d objects for controller %s", len(objects),
    435                       module_config_name)
    436         destroy_func = module.destroy
    437         self.controller_destructors[module_ref_name] = destroy_func
    438         return objects
    439 
    440     def unregister_controllers(self):
    441         """Destroy controller objects and clear internal registry.
    442 
    443         This will be called at the end of each TestRunner.run call.
    444         """
    445         for name, destroy in self.controller_destructors.items():
    446             try:
    447                 self.log.debug("Destroying %s.", name)
    448                 destroy(self.controller_registry[name])
    449             except:
    450                 self.log.exception("Exception occurred destroying %s.", name)
    451         self.controller_registry = {}
    452         self.controller_destructors = {}
    453 
    454     def parse_config(self, test_configs):
    455         """Parses the test configuration and unpacks objects and parameters
    456         into a dictionary to be passed to test classes.
    457 
    458         Args:
    459             test_configs: A json object representing the test configurations.
    460         """
    461         self.test_run_info[
    462             keys.Config.ikey_testbed_name.value] = self.testbed_name
    463         # Unpack other params.
    464         self.test_run_info["register_controller"] = self.register_controller
    465         self.test_run_info[keys.Config.ikey_logpath.value] = self.log_path
    466         self.test_run_info[keys.Config.ikey_logger.value] = self.log
    467         cli_args = test_configs.get(keys.Config.ikey_cli_args.value)
    468         self.test_run_info[keys.Config.ikey_cli_args.value] = cli_args
    469         user_param_pairs = []
    470         for item in test_configs.items():
    471             if item[0] not in keys.Config.reserved_keys.value:
    472                 user_param_pairs.append(item)
    473         self.test_run_info[keys.Config.ikey_user_param.value] = copy.deepcopy(
    474             dict(user_param_pairs))
    475 
    476     def set_test_util_logs(self, module=None):
    477         """Sets the log object to each test util module.
    478 
    479         This recursively include all modules under acts.test_utils and sets the
    480         main test logger to each module.
    481 
    482         Args:
    483             module: A module under acts.test_utils.
    484         """
    485         # Initial condition of recursion.
    486         if not module:
    487             module = importlib.import_module("acts.test_utils")
    488         # Somehow pkgutil.walk_packages is not working for me.
    489         # Using iter_modules for now.
    490         pkg_iter = pkgutil.iter_modules(module.__path__, module.__name__ + '.')
    491         for _, module_name, ispkg in pkg_iter:
    492             m = importlib.import_module(module_name)
    493             if ispkg:
    494                 self.set_test_util_logs(module=m)
    495             else:
    496                 self.log.debug("Setting logger to test util module %s",
    497                                module_name)
    498                 setattr(m, "log", self.log)
    499 
    500     def run_test_class(self, test_cls_name, test_cases=None):
    501         """Instantiates and executes a test class.
    502 
    503         If test_cases is None, the test cases listed by self.tests will be
    504         executed instead. If self.tests is empty as well, no test case in this
    505         test class will be executed.
    506 
    507         Args:
    508             test_cls_name: Name of the test class to execute.
    509             test_cases: List of test case names to execute within the class.
    510 
    511         Raises:
    512             ValueError is raised if the requested test class could not be found
    513             in the test_paths directories.
    514         """
    515         try:
    516             test_cls = self.test_classes[test_cls_name]
    517         except KeyError:
    518             self.log.info(
    519                 "Cannot find test class %s skipping for now." % test_cls_name)
    520             record = records.TestResultRecord("*all*", test_cls_name)
    521             record.test_skip(signals.TestSkip("Test class does not exist."))
    522             self.results.add_record(record)
    523             return
    524         if self.test_configs.get(keys.Config.key_random.value) or (
    525                 "Preflight" in test_cls_name) or "Postflight" in test_cls_name:
    526             test_case_iterations = 1
    527         else:
    528             test_case_iterations = self.test_configs.get(
    529                 keys.Config.key_test_case_iterations.value, 1)
    530 
    531         with test_cls(self.test_run_info) as test_cls_instance:
    532             try:
    533                 cls_result = test_cls_instance.run(test_cases,
    534                                                    test_case_iterations)
    535                 self.results += cls_result
    536             except signals.TestAbortAll as e:
    537                 self.results += e.results
    538                 raise e
    539 
    540     def run(self, test_class=None):
    541         """Executes test cases.
    542 
    543         This will instantiate controller and test classes, and execute test
    544         classes. This can be called multiple times to repeatly execute the
    545         requested test cases.
    546 
    547         A call to TestRunner.stop should eventually happen to conclude the life
    548         cycle of a TestRunner.
    549 
    550         Args:
    551             test_class: The python module of a test class. If provided, run this
    552                         class; otherwise, import modules in under test_paths
    553                         based on run_list.
    554         """
    555         if not self.running:
    556             self.running = True
    557         # Initialize controller objects and pack appropriate objects/params
    558         # to be passed to test class.
    559         self.parse_config(self.test_configs)
    560         if test_class:
    561             self.test_classes = {test_class.__name__: test_class}
    562         else:
    563             t_paths = self.test_configs[keys.Config.key_test_paths.value]
    564             self.test_classes = self.import_test_modules(t_paths)
    565         self.log.debug("Executing run list %s.", self.run_list)
    566         for test_cls_name, test_case_names in self.run_list:
    567             if not self.running:
    568                 break
    569             if test_case_names:
    570                 self.log.debug("Executing test cases %s in test class %s.",
    571                                test_case_names, test_cls_name)
    572             else:
    573                 self.log.debug("Executing test class %s", test_cls_name)
    574             try:
    575                 # Import and register the built-in controller modules specified
    576                 # in testbed config.
    577                 for module in self._import_builtin_controllers():
    578                     self.register_controller(module)
    579                 self.run_test_class(test_cls_name, test_case_names)
    580             except signals.TestAbortAll as e:
    581                 self.log.warning(
    582                     "Abort all subsequent test classes. Reason: %s", e)
    583                 raise
    584             finally:
    585                 self.unregister_controllers()
    586 
    587     def stop(self):
    588         """Releases resources from test run. Should always be called after
    589         TestRunner.run finishes.
    590 
    591         This function concludes a test run and writes out a test report.
    592         """
    593         if self.running:
    594             msg = "\nSummary for test run %s: %s\n" % (
    595                 self.id, self.results.summary_str())
    596             self._write_results_json_str()
    597             self.log.info(msg.strip())
    598             logger.kill_test_logger(self.log)
    599             self.running = False
    600 
    601     def _write_results_json_str(self):
    602         """Writes out a json file with the test result info for easy parsing.
    603 
    604         TODO(angli): This should be replaced by standard log record mechanism.
    605         """
    606         path = os.path.join(self.log_path, "test_run_summary.json")
    607         with open(path, 'w') as f:
    608             f.write(self.results.json_str())
    609 
    610     def write_test_campaign(self):
    611         """Log test campaign file."""
    612         path = os.path.join(self.log_path, "test_campaign.log")
    613         with open(path, 'w') as f:
    614             for test_class, test_cases in self.run_list:
    615                 f.write("%s:\n%s" % (test_class, ",\n".join(test_cases)))
    616                 f.write("\n\n")
    617