1# Copyright 2016 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from builtins import str 16 17import copy 18import io 19import pprint 20import os 21import yaml 22 23from mobly import keys 24from mobly import utils 25 26# An environment variable defining the base location for Mobly logs. 27ENV_MOBLY_LOGPATH = 'MOBLY_LOGPATH' 28_DEFAULT_LOG_PATH = '/tmp/logs/mobly/' 29 30 31class MoblyConfigError(Exception): 32 """Raised when there is a problem in test configuration file.""" 33 34 35def _validate_test_config(test_config): 36 """Validates the raw configuration loaded from the config file. 37 38 Making sure the required key 'TestBeds' is present. 39 """ 40 required_key = keys.Config.key_testbed.value 41 if required_key not in test_config: 42 raise MoblyConfigError( 43 'Required key %s missing in test config.' % required_key 44 ) 45 46 47def _validate_testbed_name(name): 48 """Validates the name of a test bed. 49 50 Since test bed names are used as part of the test run id, it needs to meet 51 certain requirements. 52 53 Args: 54 name: The test bed's name specified in config file. 55 56 Raises: 57 MoblyConfigError: The name does not meet any criteria. 58 """ 59 if not name: 60 raise MoblyConfigError("Test bed names can't be empty.") 61 name = str(name) 62 for char in name: 63 if char not in utils.valid_filename_chars: 64 raise MoblyConfigError( 65 'Char "%s" is not allowed in test bed names.' % char 66 ) 67 68 69def _validate_testbed_configs(testbed_configs): 70 """Validates the testbed configurations. 71 72 Args: 73 testbed_configs: A list of testbed configuration dicts. 74 75 Raises: 76 MoblyConfigError: Some parts of the configuration is invalid. 77 """ 78 seen_names = set() 79 # Cross checks testbed configs for resource conflicts. 80 for config in testbed_configs: 81 # Check for conflicts between multiple concurrent testbed configs. 82 # No need to call it if there's only one testbed config. 83 name = config[keys.Config.key_testbed_name.value] 84 _validate_testbed_name(name) 85 # Test bed names should be unique. 86 if name in seen_names: 87 raise MoblyConfigError('Duplicate testbed name %s found.' % name) 88 seen_names.add(name) 89 90 91def load_test_config_file(test_config_path, tb_filters=None): 92 """Processes the test configuration file provied by user. 93 94 Loads the configuration file into a dict, unpacks each testbed 95 config into its own dict, and validate the configuration in the 96 process. 97 98 Args: 99 test_config_path: Path to the test configuration file. 100 tb_filters: A subset of test bed names to be pulled from the config 101 file. If None, then all test beds will be selected. 102 103 Returns: 104 A list of test configuration dicts to be passed to 105 test_runner.TestRunner. 106 """ 107 configs = _load_config_file(test_config_path) 108 if tb_filters: 109 tbs = [] 110 for tb in configs[keys.Config.key_testbed.value]: 111 if tb[keys.Config.key_testbed_name.value] in tb_filters: 112 tbs.append(tb) 113 if len(tbs) != len(tb_filters): 114 raise MoblyConfigError( 115 'Expect to find %d test bed configs, found %d. Check if' 116 ' you have the correct test bed names.' % (len(tb_filters), len(tbs)) 117 ) 118 configs[keys.Config.key_testbed.value] = tbs 119 mobly_params = configs.get(keys.Config.key_mobly_params.value, {}) 120 # Decide log path. 121 log_path = mobly_params.get(keys.Config.key_log_path.value, _DEFAULT_LOG_PATH) 122 if ENV_MOBLY_LOGPATH in os.environ: 123 log_path = os.environ[ENV_MOBLY_LOGPATH] 124 log_path = utils.abs_path(log_path) 125 # Validate configs 126 _validate_test_config(configs) 127 _validate_testbed_configs(configs[keys.Config.key_testbed.value]) 128 # Transform config dict from user-facing key mapping to internal config object. 129 test_configs = [] 130 for original_bed_config in configs[keys.Config.key_testbed.value]: 131 test_run_config = TestRunConfig() 132 test_run_config.testbed_name = original_bed_config[ 133 keys.Config.key_testbed_name.value 134 ] 135 # Deprecated, use testbed_name 136 test_run_config.test_bed_name = test_run_config.testbed_name 137 test_run_config.log_path = log_path 138 test_run_config.controller_configs = original_bed_config.get( 139 keys.Config.key_testbed_controllers.value, {} 140 ) 141 test_run_config.user_params = original_bed_config.get( 142 keys.Config.key_testbed_test_params.value, {} 143 ) 144 test_configs.append(test_run_config) 145 return test_configs 146 147 148def _load_config_file(path): 149 """Loads a test config file. 150 151 The test config file has to be in YAML format. 152 153 Args: 154 path: A string that is the full path to the config file, including the 155 file name. 156 157 Returns: 158 A dict that represents info in the config file. 159 """ 160 with io.open(utils.abs_path(path), 'r', encoding='utf-8') as f: 161 conf = yaml.safe_load(f) 162 return conf 163 164 165class TestRunConfig: 166 """The data class that holds all the information needed for a test run. 167 168 Attributes: 169 log_path: string, specifies the root directory for all logs written by 170 a test run. 171 test_bed_name: [Deprecated, use 'testbed_name' instead] 172 string, the name of the test bed used by a test run. 173 testbed_name: string, the name of the test bed used by a test run. 174 controller_configs: dict, configs used for instantiating controller 175 objects. 176 user_params: dict, all the parameters to be consumed by the test logic. 177 summary_writer: records.TestSummaryWriter, used to write elements to 178 the test result summary file. 179 test_class_name_suffix: string, suffix to append to the class name for 180 reporting. This is used for differentiating the same class 181 executed with different parameters in a suite. 182 """ 183 184 def __init__(self): 185 self.log_path = _DEFAULT_LOG_PATH 186 # Deprecated, use 'testbed_name' 187 self.test_bed_name = None 188 self.testbed_name = None 189 self.controller_configs = {} 190 self.user_params = {} 191 self.summary_writer = None 192 self.test_class_name_suffix = None 193 194 def copy(self): 195 """Returns a deep copy of the current config.""" 196 return copy.deepcopy(self) 197 198 def __str__(self): 199 content = dict(self.__dict__) 200 content.pop('summary_writer') 201 return pprint.pformat(content) 202