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 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