| # vim: set syntax=python ts=4 : |
| # |
| # Copyright (c) 2018-2022 Intel Corporation |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import copy |
| import scl |
| import warnings |
| from typing import Union |
| from twisterlib.error import ConfigurationError |
| |
| def extract_fields_from_arg_list(target_fields: set, arg_list: Union[str, list]): |
| """ |
| Given a list of "FIELD=VALUE" args, extract values of args with a |
| given field name and return the remaining args separately. |
| """ |
| extracted_fields = {f : list() for f in target_fields} |
| other_fields = [] |
| |
| if isinstance(arg_list, str): |
| args = arg_list.strip().split() |
| else: |
| args = arg_list |
| |
| for field in args: |
| try: |
| name, val = field.split("=", 1) |
| except ValueError: |
| # Can't parse this. Just pass it through |
| other_fields.append(field) |
| continue |
| |
| if name in target_fields: |
| extracted_fields[name].append(val.strip('\'"')) |
| else: |
| # Move to other_fields |
| other_fields.append(field) |
| |
| return extracted_fields, other_fields |
| |
| class TwisterConfigParser: |
| """Class to read testsuite yaml files with semantic checking |
| """ |
| |
| testsuite_valid_keys = {"tags": {"type": "set", "required": False}, |
| "type": {"type": "str", "default": "integration"}, |
| "extra_args": {"type": "list"}, |
| "extra_configs": {"type": "list"}, |
| "extra_conf_files": {"type": "list", "default": []}, |
| "extra_overlay_confs" : {"type": "list", "default": []}, |
| "extra_dtc_overlay_files": {"type": "list", "default": []}, |
| "required_snippets": {"type": "list"}, |
| "build_only": {"type": "bool", "default": False}, |
| "build_on_all": {"type": "bool", "default": False}, |
| "skip": {"type": "bool", "default": False}, |
| "slow": {"type": "bool", "default": False}, |
| "timeout": {"type": "int", "default": 60}, |
| "min_ram": {"type": "int", "default": 8}, |
| "modules": {"type": "list", "default": []}, |
| "depends_on": {"type": "set"}, |
| "min_flash": {"type": "int", "default": 32}, |
| "arch_allow": {"type": "set"}, |
| "arch_exclude": {"type": "set"}, |
| "extra_sections": {"type": "list", "default": []}, |
| "integration_platforms": {"type": "list", "default": []}, |
| "ignore_faults": {"type": "bool", "default": False }, |
| "ignore_qemu_crash": {"type": "bool", "default": False }, |
| "testcases": {"type": "list", "default": []}, |
| "platform_type": {"type": "list", "default": []}, |
| "platform_exclude": {"type": "set"}, |
| "platform_allow": {"type": "set"}, |
| "platform_key": {"type": "list", "default": []}, |
| "simulation_exclude": {"type": "list", "default": []}, |
| "toolchain_exclude": {"type": "set"}, |
| "toolchain_allow": {"type": "set"}, |
| "filter": {"type": "str"}, |
| "levels": {"type": "list", "default": []}, |
| "harness": {"type": "str", "default": "test"}, |
| "harness_config": {"type": "map", "default": {}}, |
| "seed": {"type": "int", "default": 0}, |
| "sysbuild": {"type": "bool", "default": False} |
| } |
| |
| def __init__(self, filename, schema): |
| """Instantiate a new TwisterConfigParser object |
| |
| @param filename Source .yaml file to read |
| """ |
| self.data = {} |
| self.schema = schema |
| self.filename = filename |
| self.scenarios = {} |
| self.common = {} |
| |
| def load(self): |
| self.data = scl.yaml_load_verify(self.filename, self.schema) |
| |
| if 'tests' in self.data: |
| self.scenarios = self.data['tests'] |
| if 'common' in self.data: |
| self.common = self.data['common'] |
| |
| def _cast_value(self, value, typestr): |
| if isinstance(value, str): |
| v = value.strip() |
| if typestr == "str": |
| return v |
| |
| elif typestr == "float": |
| return float(value) |
| |
| elif typestr == "int": |
| return int(value) |
| |
| elif typestr == "bool": |
| return value |
| |
| elif typestr.startswith("list"): |
| if isinstance(value, list): |
| return value |
| elif isinstance(value, str): |
| vs = v.split() |
| |
| if len(vs) > 1: |
| warnings.warn( |
| "Space-separated lists are deprecated, use YAML lists instead", |
| DeprecationWarning) |
| |
| if len(typestr) > 4 and typestr[4] == ":": |
| return [self._cast_value(vsi, typestr[5:]) for vsi in vs] |
| else: |
| return vs |
| else: |
| raise ValueError |
| |
| elif typestr.startswith("set"): |
| if isinstance(value, list): |
| return set(value) |
| elif isinstance(value, str): |
| vs = v.split() |
| |
| if len(vs) > 1: |
| warnings.warn( |
| "Space-separated lists are deprecated, use YAML lists instead", |
| DeprecationWarning) |
| |
| if len(typestr) > 3 and typestr[3] == ":": |
| return {self._cast_value(vsi, typestr[4:]) for vsi in vs} |
| else: |
| return set(vs) |
| else: |
| raise ValueError |
| |
| elif typestr.startswith("map"): |
| return value |
| else: |
| raise ConfigurationError( |
| self.filename, "unknown type '%s'" % value) |
| |
| def get_scenario(self, name): |
| """Get a dictionary representing the keys/values within a scenario |
| |
| @param name The scenario in the .yaml file to retrieve data from |
| @return A dictionary containing the scenario key-value pairs with |
| type conversion and default values filled in per valid_keys |
| """ |
| |
| # "CONF_FILE", "OVERLAY_CONFIG", and "DTC_OVERLAY_FILE" fields from each |
| # of the extra_args lines |
| extracted_common = {} |
| extracted_testsuite = {} |
| |
| d = {} |
| for k, v in self.common.items(): |
| if k == "extra_args": |
| # Pull out these fields and leave the rest |
| extracted_common, d[k] = extract_fields_from_arg_list( |
| {"CONF_FILE", "OVERLAY_CONFIG", "DTC_OVERLAY_FILE"}, v |
| ) |
| else: |
| # Copy common value to avoid mutating it with test specific values below |
| d[k] = copy.copy(v) |
| |
| for k, v in self.scenarios[name].items(): |
| if k == "extra_args": |
| # Pull out these fields and leave the rest |
| extracted_testsuite, v = extract_fields_from_arg_list( |
| {"CONF_FILE", "OVERLAY_CONFIG", "DTC_OVERLAY_FILE"}, v |
| ) |
| if k in d: |
| if k == "filter": |
| d[k] = "(%s) and (%s)" % (d[k], v) |
| elif k not in ("extra_conf_files", "extra_overlay_confs", |
| "extra_dtc_overlay_files"): |
| if isinstance(d[k], str) and isinstance(v, list): |
| d[k] = d[k].split() + v |
| elif isinstance(d[k], list) and isinstance(v, str): |
| d[k] += v.split() |
| elif isinstance(d[k], list) and isinstance(v, list): |
| d[k] += v |
| elif isinstance(d[k], str) and isinstance(v, str): |
| d[k] += " " + v |
| else: |
| # replace value if not str/list (e.g. integer) |
| d[k] = v |
| else: |
| d[k] = v |
| |
| # Compile conf files in to a single list. The order to apply them is: |
| # (1) CONF_FILEs extracted from common['extra_args'] |
| # (2) common['extra_conf_files'] |
| # (3) CONF_FILES extracted from scenarios[name]['extra_args'] |
| # (4) scenarios[name]['extra_conf_files'] |
| d["extra_conf_files"] = \ |
| extracted_common.get("CONF_FILE", []) + \ |
| self.common.get("extra_conf_files", []) + \ |
| extracted_testsuite.get("CONF_FILE", []) + \ |
| self.scenarios[name].get("extra_conf_files", []) |
| |
| # Repeat the above for overlay confs and DTC overlay files |
| d["extra_overlay_confs"] = \ |
| extracted_common.get("OVERLAY_CONFIG", []) + \ |
| self.common.get("extra_overlay_confs", []) + \ |
| extracted_testsuite.get("OVERLAY_CONFIG", []) + \ |
| self.scenarios[name].get("extra_overlay_confs", []) |
| |
| d["extra_dtc_overlay_files"] = \ |
| extracted_common.get("DTC_OVERLAY_FILE", []) + \ |
| self.common.get("extra_dtc_overlay_files", []) + \ |
| extracted_testsuite.get("DTC_OVERLAY_FILE", []) + \ |
| self.scenarios[name].get("extra_dtc_overlay_files", []) |
| |
| if any({len(x) > 0 for x in extracted_common.values()}) or \ |
| any({len(x) > 0 for x in extracted_testsuite.values()}): |
| warnings.warn( |
| "Do not specify CONF_FILE, OVERLAY_CONFIG, or DTC_OVERLAY_FILE " |
| "in extra_args. This feature is deprecated and will soon " |
| "result in an error. Use extra_conf_files, extra_overlay_confs " |
| "or extra_dtc_overlay_files YAML fields instead", |
| DeprecationWarning |
| ) |
| |
| for k, kinfo in self.testsuite_valid_keys.items(): |
| if k not in d: |
| if "required" in kinfo: |
| required = kinfo["required"] |
| else: |
| required = False |
| |
| if required: |
| raise ConfigurationError( |
| self.filename, |
| "missing required value for '%s' in test '%s'" % |
| (k, name)) |
| else: |
| if "default" in kinfo: |
| default = kinfo["default"] |
| else: |
| default = self._cast_value("", kinfo["type"]) |
| d[k] = default |
| else: |
| try: |
| d[k] = self._cast_value(d[k], kinfo["type"]) |
| except ValueError: |
| raise ConfigurationError( |
| self.filename, "bad %s value '%s' for key '%s' in name '%s'" % |
| (kinfo["type"], d[k], k, name)) |
| |
| return d |