1# Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD 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 15""" 16Processing case config files. 17This is mainly designed for CI, we need to auto create and assign test jobs. 18 19Template Config File:: 20 21 TestConfig: 22 app: 23 package: ttfw_idf 24 class: Example 25 dut: 26 path: 27 class: 28 config_file: /somewhere/config_file_for_runner 29 test_name: CI_test_job_1 30 31 Filter: 32 chip: ESP32 33 env_tag: default 34 35 CaseConfig: 36 - name: test_examples_protocol_https_request 37 # optional 38 extra_data: some extra data passed to case with kwarg extra_data 39 overwrite: # overwrite test configs 40 app: 41 package: ttfw_idf 42 class: Example 43 - name: xxx 44""" 45import importlib 46 47import yaml 48 49try: 50 from yaml import CLoader as Loader 51except ImportError: 52 from yaml import Loader as Loader 53 54from . import TestCase 55 56 57def _convert_to_lower_case_bytes(item): 58 """ 59 bot filter is always lower case string. 60 this function will convert to all string to lower case. 61 Note: Unicode strings are converted to bytes. 62 """ 63 if isinstance(item, (tuple, list)): 64 output = [_convert_to_lower_case_bytes(v) for v in item] 65 elif isinstance(item, type(b'')): 66 output = item.lower() 67 elif isinstance(item, type(u'')): 68 output = item.encode().lower() 69 else: 70 output = item 71 return output 72 73 74def _filter_one_case(test_method, case_filter): 75 """ Apply filter for one case (the filter logic is the same as described in ``filter_test_cases``) """ 76 filter_result = True 77 # filter keys are lower case. Do map lower case keys with original keys. 78 key_mapping = {x.lower(): x for x in test_method.case_info.keys()} 79 80 for orig_key in case_filter: 81 key = key_mapping[orig_key] 82 if key in test_method.case_info: 83 # the filter key is both in case and filter 84 # we need to check if they match 85 filter_item = _convert_to_lower_case_bytes(case_filter[orig_key]) 86 accepted_item = _convert_to_lower_case_bytes(test_method.case_info[key]) 87 88 if isinstance(filter_item, (tuple, list)) \ 89 and isinstance(accepted_item, (tuple, list)): 90 # both list/tuple, check if they have common item 91 filter_result = True if set(filter_item) & set(accepted_item) else False 92 elif isinstance(filter_item, (tuple, list)): 93 # filter item list/tuple, check if case accepted value in filter item list/tuple 94 filter_result = True if accepted_item in filter_item else False 95 elif isinstance(accepted_item, (tuple, list)): 96 # accepted item list/tuple, check if case filter value is in accept item list/tuple 97 filter_result = True if filter_item in accepted_item else False 98 else: 99 if type(filter_item) != type(accepted_item): 100 # This will catch silent ignores of test cases when Unicode and bytes are compared 101 raise AssertionError(filter_item, '!=', accepted_item) 102 # both string/int, just do string compare 103 filter_result = (filter_item == accepted_item) 104 else: 105 # key in filter only, which means the case supports all values for this filter key, match succeed 106 pass 107 if not filter_result: 108 # match failed 109 break 110 return filter_result 111 112 113def filter_test_cases(test_methods, case_filter): 114 """ 115 filter test case. filter logic: 116 117 1. if filter key both in case attribute and filter: 118 * if both value is string/int, then directly compare 119 * if one is list/tuple, the other one is string/int, then check if string/int is in list/tuple 120 * if both are list/tuple, then check if they have common item 121 2. if only case attribute or filter have the key, filter succeed 122 3. will do case insensitive compare for string 123 124 for example, the following are match succeed scenarios 125 (the rule is symmetric, result is same if exchange values for user filter and case attribute): 126 127 * user case filter is ``chip: ["esp32", "esp32c"]``, case doesn't have ``chip`` attribute 128 * user case filter is ``chip: ["esp32", "esp32c"]``, case attribute is ``chip: "esp32"`` 129 * user case filter is ``chip: "esp32"``, case attribute is ``chip: "esp32"`` 130 131 :param test_methods: a list of test methods functions 132 :param case_filter: case filter 133 :return: filtered test methods 134 """ 135 filtered_test_methods = [] 136 for test_method in test_methods: 137 if _filter_one_case(test_method, case_filter): 138 filtered_test_methods.append(test_method) 139 return filtered_test_methods 140 141 142class Parser(object): 143 DEFAULT_CONFIG = { 144 'TestConfig': dict(), 145 'Filter': dict(), 146 'CaseConfig': [{'extra_data': None}], 147 } 148 149 @classmethod 150 def parse_config_file(cls, config_file): 151 """ 152 parse from config file and then update to default config. 153 154 :param config_file: config file path 155 :return: configs 156 """ 157 configs = cls.DEFAULT_CONFIG.copy() 158 if config_file: 159 with open(config_file, 'r') as f: 160 configs.update(yaml.load(f, Loader=Loader)) 161 return configs 162 163 @classmethod 164 def handle_overwrite_args(cls, overwrite): 165 """ 166 handle overwrite configs. import module from path and then get the required class. 167 168 :param overwrite: overwrite args 169 :return: dict of (original key: class) 170 """ 171 output = dict() 172 for key in overwrite: 173 module = importlib.import_module(overwrite[key]['package']) 174 output[key] = module.__getattribute__(overwrite[key]['class']) 175 return output 176 177 @classmethod 178 def apply_config(cls, test_methods, config_file): 179 """ 180 apply config for test methods 181 182 :param test_methods: a list of test methods functions 183 :param config_file: case filter file 184 :return: filtered cases 185 """ 186 configs = cls.parse_config_file(config_file) 187 test_case_list = [] 188 for _config in configs['CaseConfig']: 189 _filter = configs['Filter'].copy() 190 _overwrite = cls.handle_overwrite_args(_config.pop('overwrite', dict())) 191 _extra_data = _config.pop('extra_data', None) 192 _filter.update(_config) 193 194 # Try get target from yml 195 try: 196 _target = _filter['target'] 197 except KeyError: 198 _target = None 199 else: 200 _overwrite.update({'target': _target}) 201 202 for test_method in test_methods: 203 if _filter_one_case(test_method, _filter): 204 try: 205 dut_dict = test_method.case_info['dut_dict'] 206 except (AttributeError, KeyError): 207 dut_dict = None 208 209 if dut_dict and _target: 210 dut = test_method.case_info.get('dut') 211 if _target.upper() in dut_dict: 212 if dut and dut in dut_dict.values(): # don't overwrite special cases 213 _overwrite.update({'dut': dut_dict[_target.upper()]}) 214 else: 215 raise ValueError('target {} is not in the specified dut_dict'.format(_target)) 216 test_case_list.append(TestCase.TestCase(test_method, _extra_data, **_overwrite)) 217 return test_case_list 218 219 220class Generator(object): 221 """ Case config file generator """ 222 223 def __init__(self): 224 self.default_config = { 225 'TestConfig': dict(), 226 'Filter': dict(), 227 } 228 229 def set_default_configs(self, test_config, case_filter): 230 """ 231 :param test_config: "TestConfig" value 232 :param case_filter: "Filter" value 233 :return: None 234 """ 235 self.default_config = {'TestConfig': test_config, 'Filter': case_filter} 236 237 def generate_config(self, case_configs, output_file): 238 """ 239 :param case_configs: "CaseConfig" value 240 :param output_file: output file path 241 :return: None 242 """ 243 config = self.default_config.copy() 244 config.update({'CaseConfig': case_configs}) 245 with open(output_file, 'w') as f: 246 yaml.dump(config, f) 247