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