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('Required key %s missing in test config.' % 43 required_key) 44 45 46def _validate_testbed_name(name): 47 """Validates the name of a test bed. 48 49 Since test bed names are used as part of the test run id, it needs to meet 50 certain requirements. 51 52 Args: 53 name: The test bed's name specified in config file. 54 55 Raises: 56 MoblyConfigError: The name does not meet any criteria. 57 """ 58 if not name: 59 raise MoblyConfigError("Test bed names can't be empty.") 60 name = str(name) 61 for char in name: 62 if char not in utils.valid_filename_chars: 63 raise MoblyConfigError('Char "%s" is not allowed in test bed names.' % 64 char) 65 66 67def _validate_testbed_configs(testbed_configs): 68 """Validates the testbed configurations. 69 70 Args: 71 testbed_configs: A list of testbed configuration dicts. 72 73 Raises: 74 MoblyConfigError: Some parts of the configuration is invalid. 75 """ 76 seen_names = set() 77 # Cross checks testbed configs for resource conflicts. 78 for config in testbed_configs: 79 # Check for conflicts between multiple concurrent testbed configs. 80 # No need to call it if there's only one testbed config. 81 name = config[keys.Config.key_testbed_name.value] 82 _validate_testbed_name(name) 83 # Test bed names should be unique. 84 if name in seen_names: 85 raise MoblyConfigError('Duplicate testbed name %s found.' % name) 86 seen_names.add(name) 87 88 89def load_test_config_file(test_config_path, tb_filters=None): 90 """Processes the test configuration file provied by user. 91 92 Loads the configuration file into a dict, unpacks each testbed 93 config into its own dict, and validate the configuration in the 94 process. 95 96 Args: 97 test_config_path: Path to the test configuration file. 98 tb_filters: A subset of test bed names to be pulled from the config 99 file. If None, then all test beds will be selected. 100 101 Returns: 102 A list of test configuration dicts to be passed to 103 test_runner.TestRunner. 104 """ 105 configs = _load_config_file(test_config_path) 106 if tb_filters: 107 tbs = [] 108 for tb in configs[keys.Config.key_testbed.value]: 109 if tb[keys.Config.key_testbed_name.value] in tb_filters: 110 tbs.append(tb) 111 if len(tbs) != len(tb_filters): 112 raise MoblyConfigError( 113 'Expect to find %d test bed configs, found %d. Check if' 114 ' you have the correct test bed names.' % (len(tb_filters), len(tbs))) 115 configs[keys.Config.key_testbed.value] = tbs 116 mobly_params = configs.get(keys.Config.key_mobly_params.value, {}) 117 # Decide log path. 118 log_path = mobly_params.get(keys.Config.key_log_path.value, _DEFAULT_LOG_PATH) 119 if ENV_MOBLY_LOGPATH in os.environ: 120 log_path = os.environ[ENV_MOBLY_LOGPATH] 121 log_path = utils.abs_path(log_path) 122 # Validate configs 123 _validate_test_config(configs) 124 _validate_testbed_configs(configs[keys.Config.key_testbed.value]) 125 # Transform config dict from user-facing key mapping to internal config object. 126 test_configs = [] 127 for original_bed_config in configs[keys.Config.key_testbed.value]: 128 test_run_config = TestRunConfig() 129 test_run_config.testbed_name = original_bed_config[ 130 keys.Config.key_testbed_name.value] 131 # Deprecated, use testbed_name 132 test_run_config.test_bed_name = test_run_config.testbed_name 133 test_run_config.log_path = log_path 134 test_run_config.controller_configs = original_bed_config.get( 135 keys.Config.key_testbed_controllers.value, {}) 136 test_run_config.user_params = original_bed_config.get( 137 keys.Config.key_testbed_test_params.value, {}) 138 test_configs.append(test_run_config) 139 return test_configs 140 141 142def _load_config_file(path): 143 """Loads a test config file. 144 145 The test config file has to be in YAML format. 146 147 Args: 148 path: A string that is the full path to the config file, including the 149 file name. 150 151 Returns: 152 A dict that represents info in the config file. 153 """ 154 with io.open(utils.abs_path(path), 'r', encoding='utf-8') as f: 155 conf = yaml.safe_load(f) 156 return conf 157 158 159class TestRunConfig: 160 """The data class that holds all the information needed for a test run. 161 162 Attributes: 163 log_path: string, specifies the root directory for all logs written by 164 a test run. 165 test_bed_name: [Deprecated, use 'testbed_name' instead] 166 string, the name of the test bed used by a test run. 167 testbed_name: string, the name of the test bed used by a test run. 168 controller_configs: dict, configs used for instantiating controller 169 objects. 170 user_params: dict, all the parameters to be consumed by the test logic. 171 summary_writer: records.TestSummaryWriter, used to write elements to 172 the test result summary file. 173 test_class_name_suffix: string, suffix to append to the class name for 174 reporting. This is used for differentiating the same class 175 executed with different parameters in a suite. 176 """ 177 178 def __init__(self): 179 self.log_path = _DEFAULT_LOG_PATH 180 # Deprecated, use 'testbed_name' 181 self.test_bed_name = None 182 self.testbed_name = None 183 self.controller_configs = {} 184 self.user_params = {} 185 self.summary_writer = None 186 self.test_class_name_suffix = None 187 188 def copy(self): 189 """Returns a deep copy of the current config. 190 """ 191 return copy.deepcopy(self) 192 193 def __str__(self): 194 content = dict(self.__dict__) 195 content.pop('summary_writer') 196 return pprint.pformat(content) 197