1#!/usr/bin/env python3 2# 3# Copyright 2018 - 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. 16import logging 17import os 18 19from acts import utils 20from acts.config import entries 21 22from acts.config.config_sources.cli_config_source import CliConfigSource 23from acts.config.config_sources.env_config_source import EnvConfigSource 24from acts.config.config_sources.file_config_source import FileConfigSource 25from acts.config.config_wrapper import ConfigWrapper 26from acts.keys import Config 27 28 29class ActsConfigError(Exception): 30 """Raised when there is a problem in test configuration file.""" 31 32 33class ConfigGenerator(object): 34 """A class for generating the master configuration for running tests.""" 35 36 def __init__(self): 37 """Initializes the ConfigGenerator.""" 38 self._master_config = {} 39 40 def _get_test_config(self, testbed_name): 41 """Returns a test config for a given testbed.""" 42 test_config = dict(self._master_config) 43 testbed_data = ( 44 self._master_config[Config.key_testbed.value][testbed_name]) 45 46 test_config[Config.key_testbed.value] = testbed_data 47 # Keys in each test bed config will be copied to a level up to be 48 # picked up for user_params. If the key already exists in the upper 49 # level, the local one defined in test bed config overwrites the 50 # general one. 51 test_config.update(testbed_data) 52 return ConfigWrapper(test_config) 53 54 def generate_configs(self): 55 """Returns the configuration to be used for the tests and framework.""" 56 config_entries = entries.config_entries 57 for config_src in self._get_config_sources(): 58 for key, value in config_src.gather_configs(config_entries).items(): 59 if key not in self._master_config: 60 self._master_config[key] = value 61 62 self._post_process_configs() 63 64 generated_testbeds = [] 65 for name in self._master_config.get( 66 Config.key_testbeds_under_test.value, 67 self._master_config[Config.key_testbed.value].keys()): 68 generated_testbeds.append(self._get_test_config(name)) 69 return generated_testbeds 70 71 def _get_config_sources(self): 72 """Returns the sources in the order of most-to-least significant. 73 74 Values from ConfigSources returned earlier should not be overwritten by 75 the values obtained by later sources. 76 """ 77 yield CliConfigSource() 78 yield FileConfigSource( 79 self._master_config[Config.key_config_full_path.value]) 80 yield EnvConfigSource() 81 82 def _post_process_configs(self): 83 """Does post processing on the configs. 84 85 This function will handle obtaining additional values from the configs, 86 well as handle the testbed config creation. 87 """ 88 # Sets the config directory. 89 # TODO(b/29836695): Remove after the key has been deprecated. 90 config_dir = os.path.dirname( 91 self._master_config[Config.key_config_full_path.value]) 92 self._master_config[Config.key_config_path] = config_dir 93 94 # Normalizes the "testbed" field to be a dictionary if not already. 95 if type(self._master_config[Config.key_testbed.value]) is list: 96 self._master_config[Config.key_testbed.value] = ( 97 utils.list_of_dict_to_dict_of_dict( 98 self._master_config[Config.key_testbed.value], 99 Config.key_testbed_name.value)) 100 101 # For backwards compatibility, makes sure the name of each testbed name 102 # is available to the testbed data. 103 for name, testbed in ( 104 self._master_config[Config.key_testbed.value].items()): 105 if Config.key_testbed_name.value not in testbed: 106 testbed[Config.key_testbed_name.value] = name 107 108 # Validates the given configs. 109 _validate_test_config(self._master_config) 110 _validate_testbed_configs(self._master_config[Config.key_testbed.value], 111 config_dir) 112 113 114def _validate_test_config(test_config): 115 """Validates the raw configuration loaded from the config file. 116 117 Makes sure all the required fields exist. 118 """ 119 for k in Config.reserved_keys.value: 120 if k not in test_config: 121 raise ActsConfigError( 122 'Required key "%s" is missing from the test config.' % k) 123 124 125def _validate_testbed_configs(testbed_configs, config_path): 126 """Validates the testbed configurations. 127 128 Args: 129 testbed_configs: A list of testbed configuration json objects. 130 config_path : The path to the config file, which can be used to 131 generate absolute paths from relative paths in configs. 132 133 Raises: 134 If any part of the configuration is invalid, ActsConfigError is raised. 135 """ 136 for name, config in testbed_configs.items(): 137 _validate_testbed_name(name) 138 # TODO(b/78189048): Remove this after deprecating relative paths. 139 _update_file_paths(config, config_path) 140 141 142def _update_file_paths(config, config_path): 143 """ Checks if the path entries are valid. 144 145 If the file path is invalid, assume it is a relative path and append 146 that to the config file path. 147 148 Args: 149 config : the config object to verify. 150 config_path : The path to the config file, which can be used to 151 generate absolute paths from relative paths in configs. 152 153 Raises: 154 If the file path is invalid, ActsConfigError is raised. 155 """ 156 # Check the file_path_keys and update if it is a relative path. 157 for file_path_key in Config.file_path_keys.value: 158 if file_path_key in config: 159 config_file = config[file_path_key] 160 if type(config_file) is str: 161 if os.path.isabs(config_file): 162 continue 163 logging.warning( 164 'Relative paths for key "%s" will be deprecated shortly. ' 165 'Please update your configuration to use an absolute path.', 166 file_path_key 167 ) 168 config_file = os.path.join(config_path, config_file) 169 if not os.path.isfile(config_file): 170 raise ActsConfigError('Unable to load config %s from test ' 171 'config file.', config_file) 172 config[file_path_key] = config_file 173 174 175def _validate_testbed_name(name): 176 """Validates the name of a test bed. 177 178 Since test bed names are used as part of the test run id, it needs to meet 179 certain requirements. 180 181 Args: 182 name: The test bed's name specified in config file. 183 184 Raises: 185 If the name does not meet any criteria, ActsConfigError is raised. 186 """ 187 if not name: 188 raise ActsConfigError('Testbed names cannot be empty.') 189 if not isinstance(name, str): 190 raise ActsConfigError('Testbed names have to be string.') 191 invalid_chars = set(name) - set(utils.valid_filename_chars) 192 if invalid_chars: 193 raise ActsConfigError( 194 'Testbed "%s" has following disallowed characters: %s' % 195 (name, set(name) - set(utils.valid_filename_chars))) 196