1# vim: set syntax=python ts=4 : 2# 3# Copyright (c) 2018-2022 Intel Corporation 4# SPDX-License-Identifier: Apache-2.0 5 6import scl 7import warnings 8from typing import Union 9from twisterlib.error import ConfigurationError 10 11def extract_fields_from_arg_list(target_fields: set, arg_list: Union[str, list]): 12 """ 13 Given a list of "FIELD=VALUE" args, extract values of args with a 14 given field name and return the remaining args separately. 15 """ 16 extracted_fields = {f : list() for f in target_fields} 17 other_fields = [] 18 19 if isinstance(arg_list, str): 20 args = arg_list.strip().split() 21 else: 22 args = arg_list 23 24 for field in args: 25 try: 26 name, val = field.split("=", 1) 27 except ValueError: 28 # Can't parse this. Just pass it through 29 other_fields.append(field) 30 continue 31 32 if name in target_fields: 33 extracted_fields[name].append(val.strip('\'"')) 34 else: 35 # Move to other_fields 36 other_fields.append(field) 37 38 return extracted_fields, " ".join(other_fields) 39 40class TwisterConfigParser: 41 """Class to read testsuite yaml files with semantic checking 42 """ 43 44 testsuite_valid_keys = {"tags": {"type": "set", "required": False}, 45 "type": {"type": "str", "default": "integration"}, 46 "extra_args": {"type": "list"}, 47 "extra_configs": {"type": "list"}, 48 "extra_conf_files": {"type": "list", "default": []}, 49 "extra_overlay_confs" : {"type": "list", "default": []}, 50 "extra_dtc_overlay_files": {"type": "list", "default": []}, 51 "build_only": {"type": "bool", "default": False}, 52 "build_on_all": {"type": "bool", "default": False}, 53 "skip": {"type": "bool", "default": False}, 54 "slow": {"type": "bool", "default": False}, 55 "timeout": {"type": "int", "default": 60}, 56 "min_ram": {"type": "int", "default": 8}, 57 "modules": {"type": "list", "default": []}, 58 "depends_on": {"type": "set"}, 59 "min_flash": {"type": "int", "default": 32}, 60 "arch_allow": {"type": "set"}, 61 "arch_exclude": {"type": "set"}, 62 "extra_sections": {"type": "list", "default": []}, 63 "integration_platforms": {"type": "list", "default": []}, 64 "ignore_faults": {"type": "bool", "default": False }, 65 "ignore_qemu_crash": {"type": "bool", "default": False }, 66 "testcases": {"type": "list", "default": []}, 67 "platform_type": {"type": "list", "default": []}, 68 "platform_exclude": {"type": "set"}, 69 "platform_allow": {"type": "set"}, 70 "platform_key": {"type": "list", "default": []}, 71 "toolchain_exclude": {"type": "set"}, 72 "toolchain_allow": {"type": "set"}, 73 "filter": {"type": "str"}, 74 "levels": {"type": "list", "default": []}, 75 "harness": {"type": "str", "default": "test"}, 76 "harness_config": {"type": "map", "default": {}}, 77 "seed": {"type": "int", "default": 0}, 78 "sysbuild": {"type": "bool", "default": False} 79 } 80 81 def __init__(self, filename, schema): 82 """Instantiate a new TwisterConfigParser object 83 84 @param filename Source .yaml file to read 85 """ 86 self.data = {} 87 self.schema = schema 88 self.filename = filename 89 self.scenarios = {} 90 self.common = {} 91 92 def load(self): 93 self.data = scl.yaml_load_verify(self.filename, self.schema) 94 95 if 'tests' in self.data: 96 self.scenarios = self.data['tests'] 97 if 'common' in self.data: 98 self.common = self.data['common'] 99 100 def _cast_value(self, value, typestr): 101 if isinstance(value, str): 102 v = value.strip() 103 if typestr == "str": 104 return v 105 106 elif typestr == "float": 107 return float(value) 108 109 elif typestr == "int": 110 return int(value) 111 112 elif typestr == "bool": 113 return value 114 115 elif typestr.startswith("list"): 116 if isinstance(value, list): 117 return value 118 elif isinstance(value, str): 119 vs = v.split() 120 121 if len(vs) > 1: 122 warnings.warn( 123 "Space-separated lists are deprecated, use YAML lists instead", 124 DeprecationWarning) 125 126 if len(typestr) > 4 and typestr[4] == ":": 127 return [self._cast_value(vsi, typestr[5:]) for vsi in vs] 128 else: 129 return vs 130 else: 131 raise ValueError 132 133 elif typestr.startswith("set"): 134 if isinstance(value, list): 135 return set(value) 136 elif isinstance(value, str): 137 vs = v.split() 138 139 if len(vs) > 1: 140 warnings.warn( 141 "Space-separated lists are deprecated, use YAML lists instead", 142 DeprecationWarning) 143 144 if len(typestr) > 3 and typestr[3] == ":": 145 return {self._cast_value(vsi, typestr[4:]) for vsi in vs} 146 else: 147 return set(vs) 148 else: 149 raise ValueError 150 151 elif typestr.startswith("map"): 152 return value 153 else: 154 raise ConfigurationError( 155 self.filename, "unknown type '%s'" % value) 156 157 def get_scenario(self, name): 158 """Get a dictionary representing the keys/values within a scenario 159 160 @param name The scenario in the .yaml file to retrieve data from 161 @return A dictionary containing the scenario key-value pairs with 162 type conversion and default values filled in per valid_keys 163 """ 164 165 # "CONF_FILE", "OVERLAY_CONFIG", and "DTC_OVERLAY_FILE" fields from each 166 # of the extra_args lines 167 extracted_common = {} 168 extracted_testsuite = {} 169 170 d = {} 171 for k, v in self.common.items(): 172 if k == "extra_args": 173 # Pull out these fields and leave the rest 174 extracted_common, d[k] = extract_fields_from_arg_list( 175 {"CONF_FILE", "OVERLAY_CONFIG", "DTC_OVERLAY_FILE"}, v 176 ) 177 else: 178 d[k] = v 179 180 for k, v in self.scenarios[name].items(): 181 if k == "extra_args": 182 # Pull out these fields and leave the rest 183 extracted_testsuite, v = extract_fields_from_arg_list( 184 {"CONF_FILE", "OVERLAY_CONFIG", "DTC_OVERLAY_FILE"}, v 185 ) 186 if k in d: 187 if k == "filter": 188 d[k] = "(%s) and (%s)" % (d[k], v) 189 elif k not in ("extra_conf_files", "extra_overlay_confs", 190 "extra_dtc_overlay_files"): 191 if isinstance(d[k], str) and isinstance(v, list): 192 d[k] = d[k].split() + v 193 elif isinstance(d[k], list) and isinstance(v, str): 194 d[k] += v.split() 195 elif isinstance(d[k], list) and isinstance(v, list): 196 d[k] += v 197 elif isinstance(d[k], str) and isinstance(v, str): 198 d[k] += " " + v 199 else: 200 # replace value if not str/list (e.g. integer) 201 d[k] = v 202 else: 203 d[k] = v 204 205 # Compile conf files in to a single list. The order to apply them is: 206 # (1) CONF_FILEs extracted from common['extra_args'] 207 # (2) common['extra_conf_files'] 208 # (3) CONF_FILES extracted from scenarios[name]['extra_args'] 209 # (4) scenarios[name]['extra_conf_files'] 210 d["extra_conf_files"] = \ 211 extracted_common.get("CONF_FILE", []) + \ 212 self.common.get("extra_conf_files", []) + \ 213 extracted_testsuite.get("CONF_FILE", []) + \ 214 self.scenarios[name].get("extra_conf_files", []) 215 216 # Repeat the above for overlay confs and DTC overlay files 217 d["extra_overlay_confs"] = \ 218 extracted_common.get("OVERLAY_CONFIG", []) + \ 219 self.common.get("extra_overlay_confs", []) + \ 220 extracted_testsuite.get("OVERLAY_CONFIG", []) + \ 221 self.scenarios[name].get("extra_overlay_confs", []) 222 223 d["extra_dtc_overlay_files"] = \ 224 extracted_common.get("DTC_OVERLAY_FILE", []) + \ 225 self.common.get("extra_dtc_overlay_files", []) + \ 226 extracted_testsuite.get("DTC_OVERLAY_FILE", []) + \ 227 self.scenarios[name].get("extra_dtc_overlay_files", []) 228 229 if any({len(x) > 0 for x in extracted_common.values()}) or \ 230 any({len(x) > 0 for x in extracted_testsuite.values()}): 231 warnings.warn( 232 "Do not specify CONF_FILE, OVERLAY_CONFIG, or DTC_OVERLAY_FILE " 233 "in extra_args. This feature is deprecated and will soon " 234 "result in an error. Use extra_conf_files, extra_overlay_confs " 235 "or extra_dtc_overlay_files YAML fields instead", 236 DeprecationWarning 237 ) 238 239 for k, kinfo in self.testsuite_valid_keys.items(): 240 if k not in d: 241 if "required" in kinfo: 242 required = kinfo["required"] 243 else: 244 required = False 245 246 if required: 247 raise ConfigurationError( 248 self.filename, 249 "missing required value for '%s' in test '%s'" % 250 (k, name)) 251 else: 252 if "default" in kinfo: 253 default = kinfo["default"] 254 else: 255 default = self._cast_value("", kinfo["type"]) 256 d[k] = default 257 else: 258 try: 259 d[k] = self._cast_value(d[k], kinfo["type"]) 260 except ValueError: 261 raise ConfigurationError( 262 self.filename, "bad %s value '%s' for key '%s' in name '%s'" % 263 (kinfo["type"], d[k], k, name)) 264 265 return d 266