vss-tools 0.5.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vspec/__init__.py +996 -0
- vspec/loggingconfig.py +22 -0
- vspec/model/__init__.py +7 -0
- vspec/model/constants.py +307 -0
- vspec/model/exceptions.py +29 -0
- vspec/model/vsstree.py +602 -0
- vspec/py.typed +0 -0
- vspec/utils/__init__.py +23 -0
- vspec/utils/idgen_utils.py +97 -0
- vspec/utils/stringstyle.py +25 -0
- vspec/utils/vss2id_val.py +165 -0
- vspec/vspec2vss_config.py +53 -0
- vspec/vspec2x.py +191 -0
- vspec/vss2x.py +38 -0
- vspec/vssexporters/__init__.py +7 -0
- vspec/vssexporters/vss2binary.py +147 -0
- vspec/vssexporters/vss2csv.py +82 -0
- vspec/vssexporters/vss2ddsidl.py +295 -0
- vspec/vssexporters/vss2franca.py +98 -0
- vspec/vssexporters/vss2graphql.py +143 -0
- vspec/vssexporters/vss2id.py +172 -0
- vspec/vssexporters/vss2json.py +116 -0
- vspec/vssexporters/vss2jsonschema.py +159 -0
- vspec/vssexporters/vss2protobuf.py +171 -0
- vspec/vssexporters/vss2yaml.py +122 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2csv.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2ddsidl.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2franca.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2graphql.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2id.py +28 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2json.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2jsonschema.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2protobuf.py +29 -0
- vss_tools-0.5.0.dev0.data/scripts/vspec2yaml.py +29 -0
- vss_tools-0.5.0.dev0.dist-info/LICENSE +373 -0
- vss_tools-0.5.0.dev0.dist-info/METADATA +65 -0
- vss_tools-0.5.0.dev0.dist-info/RECORD +39 -0
- vss_tools-0.5.0.dev0.dist-info/WHEEL +5 -0
- vss_tools-0.5.0.dev0.dist-info/top_level.txt +1 -0
vspec/__init__.py
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
# Copyright (c) 2016 Contributors to COVESA
|
|
2
|
+
#
|
|
3
|
+
# This program and the accompanying materials are made available under the
|
|
4
|
+
# terms of the Mozilla Public License 2.0 which is available at
|
|
5
|
+
# https://www.mozilla.org/en-US/MPL/2.0/
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# VSpec file parser.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
import os
|
|
15
|
+
import uuid
|
|
16
|
+
import sys
|
|
17
|
+
import collections
|
|
18
|
+
import logging
|
|
19
|
+
from copy import deepcopy
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
from anytree import (Resolver, LevelOrderIter, PreOrderIter, RenderTree) # type: ignore[import]
|
|
23
|
+
|
|
24
|
+
from .model.vsstree import VSSNode
|
|
25
|
+
from .model.exceptions import ImpossibleMergeException, IncompleteElementException
|
|
26
|
+
from .model.constants import VSSTreeType, VSSUnitCollection, VSSQuantityCollection
|
|
27
|
+
|
|
28
|
+
nestable_types = set(["branch", "struct"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VSpecError(Exception):
|
|
32
|
+
def __init__(self, *args, **kwargs):
|
|
33
|
+
self.file_name = args[0]
|
|
34
|
+
self.line_nr = args[1]
|
|
35
|
+
self.message = args[2]
|
|
36
|
+
Exception.__init__(self, *args, **kwargs)
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return "{}: {}: {}".format(self.file_name, self.line_nr, self.message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Try to open a file name that can reside
|
|
43
|
+
# in any directory listed in incude_paths.
|
|
44
|
+
# If successful, read context and return file
|
|
45
|
+
#
|
|
46
|
+
def search_and_read(file_name, include_paths):
|
|
47
|
+
# If absolute path, then ignore include paths
|
|
48
|
+
if file_name[0] == '/':
|
|
49
|
+
with open(file_name, "r") as fp:
|
|
50
|
+
text = fp.read()
|
|
51
|
+
fp.close()
|
|
52
|
+
return os.path.dirname(file_name), text
|
|
53
|
+
|
|
54
|
+
for directory in include_paths:
|
|
55
|
+
try:
|
|
56
|
+
path = "{}/{}".format(directory, file_name)
|
|
57
|
+
with open(path, "r") as fp:
|
|
58
|
+
text = fp.read()
|
|
59
|
+
fp.close()
|
|
60
|
+
return os.path.dirname(path), text
|
|
61
|
+
except IOError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# We failed, raise last exception we ran into.
|
|
65
|
+
raise VSpecError(file_name, 0, "File error")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def convert_yaml_to_list(raw_yaml):
|
|
69
|
+
if isinstance(raw_yaml, list):
|
|
70
|
+
return raw_yaml
|
|
71
|
+
|
|
72
|
+
# Sort the dictionary according to line number.
|
|
73
|
+
# The reason is that when the YAML file is loaded
|
|
74
|
+
# the object order is not preserved in the created
|
|
75
|
+
# dictionary
|
|
76
|
+
raw_yaml = collections.OrderedDict(
|
|
77
|
+
sorted(raw_yaml.items(), key=lambda x: x[1]['$line$']))
|
|
78
|
+
lst = []
|
|
79
|
+
for elem in raw_yaml:
|
|
80
|
+
if isinstance(raw_yaml[elem], dict):
|
|
81
|
+
raw_yaml[elem]['$name$'] = elem
|
|
82
|
+
lst.append(raw_yaml[elem])
|
|
83
|
+
|
|
84
|
+
return lst
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_tree(
|
|
88
|
+
file_name,
|
|
89
|
+
include_paths,
|
|
90
|
+
tree_type: VSSTreeType,
|
|
91
|
+
break_on_name_style_violation=False,
|
|
92
|
+
expand_inst=True,
|
|
93
|
+
data_type_tree: Optional[VSSNode] = None):
|
|
94
|
+
if expand_inst and tree_type == VSSTreeType.DATA_TYPE_TREE:
|
|
95
|
+
logging.error("Instance expansion is not supported for VSS type tree.")
|
|
96
|
+
sys.exit(-1)
|
|
97
|
+
|
|
98
|
+
flat_model = load_flat_model(file_name, "", include_paths, tree_type)
|
|
99
|
+
absolute_path_flat_model = create_absolute_paths(flat_model)
|
|
100
|
+
deep_model = create_nested_model(absolute_path_flat_model, file_name)
|
|
101
|
+
cleanup_deep_model(deep_model)
|
|
102
|
+
dict_tree = deep_model["children"]
|
|
103
|
+
tree = render_tree(
|
|
104
|
+
dict_tree,
|
|
105
|
+
tree_type,
|
|
106
|
+
break_on_name_style_violation=break_on_name_style_violation)
|
|
107
|
+
if expand_inst:
|
|
108
|
+
expand_tree_instances(tree)
|
|
109
|
+
return tree
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_type_usage(tree: VSSNode, tree_type: VSSTreeType, type_tree: Optional[VSSNode] = None):
|
|
113
|
+
"""
|
|
114
|
+
Check usages of types within the tree.
|
|
115
|
+
This methods shall be called after overlays (or additional type files) have been merged.
|
|
116
|
+
"""
|
|
117
|
+
if tree_type == VSSTreeType.DATA_TYPE_TREE:
|
|
118
|
+
check_data_type_references(tree)
|
|
119
|
+
elif tree_type == VSSTreeType.SIGNAL_TREE:
|
|
120
|
+
check_data_type_references_across_trees(tree, type_tree)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load_flat_model(file_name, prefix, include_paths, tree_type: VSSTreeType):
|
|
124
|
+
# Hooks into YAML parser to add line numbers
|
|
125
|
+
# and file name into each element
|
|
126
|
+
def yaml_compose_node(parent, index):
|
|
127
|
+
# the line number where the previous token has ended (plus empty lines)
|
|
128
|
+
line = loader.line
|
|
129
|
+
try:
|
|
130
|
+
node = yaml.composer.Composer.compose_node(loader, parent, index)
|
|
131
|
+
except yaml.scanner.ScannerError as e:
|
|
132
|
+
raise VSpecError(file_name, line + 1, e)
|
|
133
|
+
except yaml.parser.ParserError as e:
|
|
134
|
+
raise VSpecError(file_name, line + 1, e)
|
|
135
|
+
|
|
136
|
+
if node.value == '$include$':
|
|
137
|
+
node.value = f'$include${load_flat_model.include_index}'
|
|
138
|
+
load_flat_model.include_index = load_flat_model.include_index + 1
|
|
139
|
+
|
|
140
|
+
# Avoid having root-level line numbers as non-dictionary entries
|
|
141
|
+
if parent:
|
|
142
|
+
node.__line__ = line + 1
|
|
143
|
+
node.__file_name__ = file_name
|
|
144
|
+
else:
|
|
145
|
+
node.__line__ = None
|
|
146
|
+
node.__file_name = None
|
|
147
|
+
return node
|
|
148
|
+
|
|
149
|
+
def yaml_construct_mapping(node, deep=True):
|
|
150
|
+
mapping = yaml.constructor.Constructor.construct_mapping(
|
|
151
|
+
loader, node, deep=deep)
|
|
152
|
+
|
|
153
|
+
# Replace
|
|
154
|
+
# { 'Vehicle.Speed': { 'datatype': 'boolean', 'type': 'sensor' }}
|
|
155
|
+
# with
|
|
156
|
+
# { '$name$': 'Vehicle.Speed', 'datatype': 'boolean', 'type': 'sensor' }
|
|
157
|
+
|
|
158
|
+
for key, val in list(mapping.items()):
|
|
159
|
+
if key[0] == '$':
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
if val is None:
|
|
163
|
+
mapping['$name$'] = key
|
|
164
|
+
del mapping[key]
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
# Add line number and file name to element.
|
|
168
|
+
if node.__line__ is not None:
|
|
169
|
+
mapping['$line$'] = node.__line__
|
|
170
|
+
mapping['$file_name$'] = node.__file_name__
|
|
171
|
+
|
|
172
|
+
return mapping
|
|
173
|
+
|
|
174
|
+
directory, text = search_and_read(file_name, include_paths)
|
|
175
|
+
|
|
176
|
+
# Do a trial pasing of the file to find out if it is list- or
|
|
177
|
+
# object-formatted.
|
|
178
|
+
loader = yaml.Loader(text)
|
|
179
|
+
loader.compose_node = yaml_compose_node # type: ignore[assignment]
|
|
180
|
+
|
|
181
|
+
loader.construct_mapping = yaml_construct_mapping # type: ignore[assignment]
|
|
182
|
+
test_yaml = loader.get_data()
|
|
183
|
+
|
|
184
|
+
# Depending on if this is a list or an object, expand
|
|
185
|
+
# the #include diretives differently
|
|
186
|
+
#
|
|
187
|
+
if isinstance(test_yaml, list):
|
|
188
|
+
text = yamilify_includes(text, True)
|
|
189
|
+
else:
|
|
190
|
+
text = yamilify_includes(text, False)
|
|
191
|
+
|
|
192
|
+
# Re-initialize loader with the new text hosting the
|
|
193
|
+
# yamilified includes.
|
|
194
|
+
loader = yaml.Loader(text)
|
|
195
|
+
loader.compose_node = yaml_compose_node # type: ignore[assignment]
|
|
196
|
+
|
|
197
|
+
loader.construct_mapping = yaml_construct_mapping # type: ignore[assignment]
|
|
198
|
+
raw_yaml = loader.get_data()
|
|
199
|
+
|
|
200
|
+
# Check for file with no objects.
|
|
201
|
+
if not raw_yaml:
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
raw_yaml = convert_yaml_to_list(raw_yaml)
|
|
205
|
+
|
|
206
|
+
# Sanity check of loaded code
|
|
207
|
+
check_yaml_usage(raw_yaml, file_name)
|
|
208
|
+
|
|
209
|
+
# Recursively expand all include files.
|
|
210
|
+
if directory not in include_paths:
|
|
211
|
+
include_paths = [directory] + include_paths
|
|
212
|
+
expanded_includes = expand_includes(
|
|
213
|
+
raw_yaml, prefix, include_paths, tree_type)
|
|
214
|
+
|
|
215
|
+
flat_model = cleanup_flat_entries(expanded_includes, tree_type)
|
|
216
|
+
|
|
217
|
+
return flat_model
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cleanup_flat_entries(flat_model, tree_type: VSSTreeType):
|
|
221
|
+
"""
|
|
222
|
+
# 1. Check that the declared type is part of the available types for the tree.
|
|
223
|
+
# 2. Check that allowed values are provided as arrays.
|
|
224
|
+
"""
|
|
225
|
+
available_types = tree_type.available_types()
|
|
226
|
+
|
|
227
|
+
# Traverse the flat list of the parsed specification
|
|
228
|
+
for elem in flat_model:
|
|
229
|
+
# Is this an include element?
|
|
230
|
+
if "type" not in elem:
|
|
231
|
+
raise VSpecError(elem["$file_name$"], elem["$line$"], "No type specified!")
|
|
232
|
+
|
|
233
|
+
# Check, with case sensitivity that we do have
|
|
234
|
+
# a validated type.
|
|
235
|
+
if not elem["type"] in available_types:
|
|
236
|
+
raise VSpecError(elem["$file_name$"], elem["$line$"],
|
|
237
|
+
"Unknown type: {}".format(elem["type"]))
|
|
238
|
+
|
|
239
|
+
if "allowed" in elem and not isinstance(elem["allowed"], list):
|
|
240
|
+
raise VSpecError(elem["$file_name$"], elem["$line$"],
|
|
241
|
+
"Allowed values are not represented as array.")
|
|
242
|
+
|
|
243
|
+
return flat_model
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
#
|
|
247
|
+
# Delete parser-specific elements
|
|
248
|
+
#
|
|
249
|
+
# Parser metadata is cleaned in two steps. An initial step just after vspec files are parsed.
|
|
250
|
+
# Then some data is removed but not all as it is used for error messages.
|
|
251
|
+
# That needs to be removed in a second step, just before exporting.
|
|
252
|
+
#
|
|
253
|
+
def cleanup_deep_model(deep_model):
|
|
254
|
+
|
|
255
|
+
if "$line$" in deep_model:
|
|
256
|
+
del deep_model["$line$"]
|
|
257
|
+
|
|
258
|
+
if "$prefix$" in deep_model:
|
|
259
|
+
del deep_model["$prefix$"]
|
|
260
|
+
|
|
261
|
+
if "$name$" in deep_model:
|
|
262
|
+
del deep_model['$name$']
|
|
263
|
+
|
|
264
|
+
# children as of today exists only for branches and structs
|
|
265
|
+
if "children" in deep_model:
|
|
266
|
+
children = deep_model["children"]
|
|
267
|
+
for child in deep_model["children"]:
|
|
268
|
+
cleanup_deep_model(children[child])
|
|
269
|
+
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
#
|
|
273
|
+
# Meta data on extended attributes needs to be cleaned as part of the second cleaning step.
|
|
274
|
+
# as it is not included in first step.
|
|
275
|
+
#
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def clean_metadata(node):
|
|
279
|
+
|
|
280
|
+
if isinstance(node, VSSNode):
|
|
281
|
+
clean_metadata(node.extended_attributes)
|
|
282
|
+
for child in node.children:
|
|
283
|
+
clean_metadata(child)
|
|
284
|
+
elif isinstance(node, dict):
|
|
285
|
+
for k in list(node.keys()):
|
|
286
|
+
clean_metadata(node[k])
|
|
287
|
+
if k in ["$file_name$", "$line$"]:
|
|
288
|
+
del node[k]
|
|
289
|
+
elif isinstance(node, list):
|
|
290
|
+
for elem in node:
|
|
291
|
+
clean_metadata(elem)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def verify_mandatory_attributes(node, abort_on_unknown_attribute: bool):
|
|
295
|
+
"""
|
|
296
|
+
Verify that mandatory attributes are present.
|
|
297
|
+
Need to be checked first after overlays (if any) have been applied, as attributes are not
|
|
298
|
+
mandatory in individual files but only in the final tree
|
|
299
|
+
"""
|
|
300
|
+
if isinstance(node, VSSNode):
|
|
301
|
+
if node.delete:
|
|
302
|
+
logging.info(
|
|
303
|
+
f"Node {node.qualified_name()} will be deleted. Please note, that if {node.qualified_name()} "
|
|
304
|
+
f"is a branch all subsequent nodes will also be deleted irrespective of their 'delete' value."
|
|
305
|
+
)
|
|
306
|
+
node.parent = None
|
|
307
|
+
node.children = []
|
|
308
|
+
node.verify_attributes(abort_on_unknown_attribute)
|
|
309
|
+
for child in node.children:
|
|
310
|
+
verify_mandatory_attributes(child, abort_on_unknown_attribute)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
#
|
|
314
|
+
# Verify that we are using correct YAML in the model
|
|
315
|
+
#
|
|
316
|
+
def check_yaml_usage(flat_model, file_name):
|
|
317
|
+
for elem in flat_model:
|
|
318
|
+
if isinstance(elem, list):
|
|
319
|
+
raise VSpecError(
|
|
320
|
+
file_name,
|
|
321
|
+
0,
|
|
322
|
+
"Element {} is not a list entry. (Did you forget a ':'?)".format(elem))
|
|
323
|
+
|
|
324
|
+
# FIXME:
|
|
325
|
+
# Add more usage checks, such as absence of nested models.
|
|
326
|
+
# and mutually exclusive elements.
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# Expand yaml include elements (inserted by yamilify_include())
|
|
330
|
+
#
|
|
331
|
+
def expand_includes(flat_model, prefix, include_paths, tree_type: VSSTreeType):
|
|
332
|
+
# Build up a new spec model based on the old one, but
|
|
333
|
+
# with expanded include directives.
|
|
334
|
+
|
|
335
|
+
new_flat_model: List[VSSNode] = []
|
|
336
|
+
|
|
337
|
+
# Traverse the flat list of the parsed specification
|
|
338
|
+
for elem in flat_model:
|
|
339
|
+
# Is this an include element?
|
|
340
|
+
if elem['$name$'][0:9] == "$include$":
|
|
341
|
+
include_prefix = elem.get("prefix", "")
|
|
342
|
+
|
|
343
|
+
if include_prefix != "":
|
|
344
|
+
prefix_found = False
|
|
345
|
+
for dict_elem in new_flat_model:
|
|
346
|
+
if dict_elem["$name$"] == include_prefix:
|
|
347
|
+
prefix_found = True
|
|
348
|
+
break
|
|
349
|
+
if not prefix_found:
|
|
350
|
+
# Printing line number does not make sense, as we work on a
|
|
351
|
+
# modified tree
|
|
352
|
+
logging.warning(
|
|
353
|
+
f"No branch matching prefix {include_prefix} for #include in {elem['$file_name$']}")
|
|
354
|
+
|
|
355
|
+
# Append include prefix to our current prefix.
|
|
356
|
+
# Make sure we do not start new prefix with a "."
|
|
357
|
+
if prefix != "":
|
|
358
|
+
if include_prefix != "":
|
|
359
|
+
include_prefix = "{}.{}".format(prefix, include_prefix)
|
|
360
|
+
else:
|
|
361
|
+
include_prefix = prefix
|
|
362
|
+
|
|
363
|
+
# Recursively load included file
|
|
364
|
+
inc_elem = load_flat_model(
|
|
365
|
+
elem["file"], include_prefix, include_paths, tree_type)
|
|
366
|
+
|
|
367
|
+
# Add the loaded elements at the end of the new spec model
|
|
368
|
+
new_flat_model.extend(inc_elem)
|
|
369
|
+
else:
|
|
370
|
+
# Add a prefix to the element
|
|
371
|
+
elem["$prefix$"] = prefix
|
|
372
|
+
# Add the existing elements at the end of the new spec model
|
|
373
|
+
new_flat_model.append(elem)
|
|
374
|
+
|
|
375
|
+
return new_flat_model
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def expand_tree_instances(tree: VSSNode):
|
|
379
|
+
tree_node: VSSNode
|
|
380
|
+
|
|
381
|
+
def rollout_list(instance_entry):
|
|
382
|
+
'''
|
|
383
|
+
Converts "Prefix[1,n] to [Prefix1, Prefix2, ..., Prefixn]"
|
|
384
|
+
'''
|
|
385
|
+
prefix = ""
|
|
386
|
+
if "[" in instance_entry: # if so unroll
|
|
387
|
+
unrolled_items = []
|
|
388
|
+
prefix = instance_entry[:instance_entry.find("[")]
|
|
389
|
+
start = instance_entry[instance_entry.find(
|
|
390
|
+
"[") + 1:instance_entry.find(",")]
|
|
391
|
+
end = instance_entry[instance_entry.find(
|
|
392
|
+
",") + 1:instance_entry.find("]")]
|
|
393
|
+
for i in range(int(start), int(end) + 1):
|
|
394
|
+
unrolled_items.append(f"{prefix}{i}")
|
|
395
|
+
else: # if not, add
|
|
396
|
+
unrolled_items = instance_entry
|
|
397
|
+
return unrolled_items, prefix
|
|
398
|
+
|
|
399
|
+
def is_instance_branch(node, unrolled_instances, prefix):
|
|
400
|
+
'''
|
|
401
|
+
Check if node is a branch that has the same name as an instance for parent node
|
|
402
|
+
'''
|
|
403
|
+
for instance in unrolled_instances:
|
|
404
|
+
if isinstance(instance, list):
|
|
405
|
+
# This means the element of the instances is a list, e.g. it was
|
|
406
|
+
# something like Row[1,4]
|
|
407
|
+
for element in instance:
|
|
408
|
+
if element == node.name:
|
|
409
|
+
return True
|
|
410
|
+
else:
|
|
411
|
+
# in this case our instances have been a simple list of elements
|
|
412
|
+
# (as opposed to a list of lists), e.g. ['Left', 'Right'], so we just compare by name
|
|
413
|
+
if instance == node.name:
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
# Now we try to be smarter - check if it seems to be an instance by prefix, e.g. Pos3 and prefix is Pos
|
|
417
|
+
# This is only for specifying instances outside specified range, e.g.
|
|
418
|
+
# specifying Row5 while instance is specified as Row[1,4]
|
|
419
|
+
if prefix != "" and prefix in node.name:
|
|
420
|
+
number = node.name.split(prefix, 1)[1]
|
|
421
|
+
return number.isnumeric()
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
def create_instantiated_branch(branch_name, parent, nodes_to_expand):
|
|
425
|
+
# Check if the branch we want to create as part of expansion (e.g.
|
|
426
|
+
# Row1, Pos2, Left, ...) already exist
|
|
427
|
+
old_node = None
|
|
428
|
+
for child in parent.children:
|
|
429
|
+
if child.name == branch_name:
|
|
430
|
+
old_node = child
|
|
431
|
+
instantiated_branch = VSSNode(
|
|
432
|
+
branch_name,
|
|
433
|
+
# autopep8: off
|
|
434
|
+
{"type": "branch",
|
|
435
|
+
"description": parent.description,
|
|
436
|
+
"comment": parent.comment,
|
|
437
|
+
"$file_name$": "Generated"
|
|
438
|
+
},
|
|
439
|
+
# autopep8: on
|
|
440
|
+
VSSTreeType.SIGNAL_TREE.available_types(),
|
|
441
|
+
parent)
|
|
442
|
+
if old_node is not None:
|
|
443
|
+
# If it exist we take the new one as default (to give e.g. default descriptions and comments)
|
|
444
|
+
# Then merge anything from the old (expanded) instance above, to get e.g. updated comment
|
|
445
|
+
# Finally remove the old node by removing parent
|
|
446
|
+
instantiated_branch.merge(old_node)
|
|
447
|
+
for child in old_node.children:
|
|
448
|
+
child.parent = instantiated_branch
|
|
449
|
+
old_node.parent = None
|
|
450
|
+
|
|
451
|
+
# Deep copy needed so that we can change attributes/dict/children
|
|
452
|
+
# independently
|
|
453
|
+
for expand_node in deepcopy(nodes_to_expand):
|
|
454
|
+
# Check if this branch/signal already exists in the instantiated
|
|
455
|
+
# branch
|
|
456
|
+
for existing_item in instantiated_branch.children:
|
|
457
|
+
if expand_node.name == existing_item.name:
|
|
458
|
+
# A child with the same name already exists
|
|
459
|
+
# Typical use-case is that a single instance of this signal has been re-defined in an overlay
|
|
460
|
+
# Then data from the overlay (for example A.B.Row2.Column2.Sig)
|
|
461
|
+
# shall have precedence over the expanded instance
|
|
462
|
+
# This is handled by removing the old node from tree and
|
|
463
|
+
# instead merging it to the new node
|
|
464
|
+
existing_item.parent = None
|
|
465
|
+
expand_node.merge(existing_item)
|
|
466
|
+
break
|
|
467
|
+
expand_node.parent = instantiated_branch
|
|
468
|
+
return instantiated_branch
|
|
469
|
+
|
|
470
|
+
# Checking each node for instances and expand them
|
|
471
|
+
# The walking order makes sure, we do not need to recurse
|
|
472
|
+
for tree_node in PreOrderIter(tree):
|
|
473
|
+
if tree_node.has_instances():
|
|
474
|
+
# print(f"This node has instances: {tree_node.qualified_name()}, they are *{tree_node.instances}*")
|
|
475
|
+
|
|
476
|
+
# Instances can be many things: A string Row[1,4] that is shorthand for a list,
|
|
477
|
+
# a simple list of of strings ['Left', 'Right'],
|
|
478
|
+
# or a list of lists ['Row[1,4]', ['Left', 'Right']], which expresses
|
|
479
|
+
# multidimensional instances, where the n'th entry will be expanded at the
|
|
480
|
+
# n'th level under the current node, i.e. in the example above you expect
|
|
481
|
+
# children branches Row1, Row2, Row3, Row4, where each of them has a "left"
|
|
482
|
+
# and a "right" child.
|
|
483
|
+
# See
|
|
484
|
+
# https://covesa.github.io/vehicle_signal_specification/rule_set/instances/
|
|
485
|
+
# for an explanation.
|
|
486
|
+
# This is a bit painful
|
|
487
|
+
|
|
488
|
+
unrolled_instances = []
|
|
489
|
+
array_prefix = ""
|
|
490
|
+
|
|
491
|
+
# Instances are a list in .vspec e.g. ["left", "right"]
|
|
492
|
+
if isinstance(tree_node.instances, list):
|
|
493
|
+
for instance_entry in tree_node.instances:
|
|
494
|
+
# check every entry whether it is shorthand for a list
|
|
495
|
+
# e.g. Prefix[1,3]
|
|
496
|
+
unrolled_items, tmpprefix = rollout_list(instance_entry)
|
|
497
|
+
if array_prefix == "":
|
|
498
|
+
# For "smart" comparison only use prefix from first
|
|
499
|
+
# level
|
|
500
|
+
array_prefix = tmpprefix
|
|
501
|
+
unrolled_instances.append(unrolled_items)
|
|
502
|
+
|
|
503
|
+
# it is not a list, e.g. instance in vspec is just Sensor[1,10]
|
|
504
|
+
else:
|
|
505
|
+
unrolled_items, array_prefix = rollout_list(
|
|
506
|
+
tree_node.instances)
|
|
507
|
+
unrolled_instances.append(unrolled_items)
|
|
508
|
+
|
|
509
|
+
# When a node has instances we need to decide what to do with the children
|
|
510
|
+
# The default behavior is to duplicate each child under each created instance,
|
|
511
|
+
# but there are exceptions.
|
|
512
|
+
#
|
|
513
|
+
# The first exception is nodes marked in vspec to be excluded from instantiation
|
|
514
|
+
# (instantiate: False)
|
|
515
|
+
#
|
|
516
|
+
# The other exception is nodes/branches that actually already are expanded.
|
|
517
|
+
# This shall typically only occur when analyzing overlays, where you may find
|
|
518
|
+
# a signal specified as e.g. Vehicle.Cabin.Door.Row2.Right.Window.Tint
|
|
519
|
+
# which then shall not be copied for each instance of Door as it is valid only
|
|
520
|
+
# for one of the instances
|
|
521
|
+
|
|
522
|
+
nodes_to_stay = [] # < nodes excluded from instantiation
|
|
523
|
+
nodes_to_expand = [] # < nodes shall use the instances as parent
|
|
524
|
+
|
|
525
|
+
for child in tree_node.children:
|
|
526
|
+
if is_instance_branch(child, unrolled_instances, array_prefix):
|
|
527
|
+
# Child is already an instance, for example Row2.Right.X
|
|
528
|
+
nodes_to_stay.append(child)
|
|
529
|
+
elif child.is_instantiated():
|
|
530
|
+
# Child has an explicit or implicit "instantiate:true",
|
|
531
|
+
# this is the default
|
|
532
|
+
nodes_to_expand.append(child)
|
|
533
|
+
else:
|
|
534
|
+
nodes_to_stay.append(child)
|
|
535
|
+
|
|
536
|
+
tree_node.children = deepcopy(nodes_to_stay)
|
|
537
|
+
|
|
538
|
+
# now iterate over instances
|
|
539
|
+
for instance in unrolled_instances:
|
|
540
|
+
if isinstance(instance, list):
|
|
541
|
+
# This means the element of the instances is a list, e.g. it was
|
|
542
|
+
# something like Row[1,4]
|
|
543
|
+
# In that case the expectation is, that further elements in the
|
|
544
|
+
# unrolled instances list will be expanded under it.
|
|
545
|
+
# We will only expand the first layer, and come back later, e.g.
|
|
546
|
+
# instances are ['Row[1,2]', 'Pos[1,3]']. In that case we will
|
|
547
|
+
# add Row1 and Row2 branches under the current node and duplicate
|
|
548
|
+
# the subtree, and we set instances property of the new "RowX"
|
|
549
|
+
# branches to 'Pos[1,3]'
|
|
550
|
+
# The PreOrderIter will pass the newly created "RowX" instances
|
|
551
|
+
# next so they will be expanded
|
|
552
|
+
|
|
553
|
+
for element in instance:
|
|
554
|
+
instantiated_branch = create_instantiated_branch(
|
|
555
|
+
element, tree_node, nodes_to_expand)
|
|
556
|
+
|
|
557
|
+
if len(unrolled_instances) > 1:
|
|
558
|
+
# We need to expand more (see above). PreOrderIter
|
|
559
|
+
# allows us to do this without recursing
|
|
560
|
+
instantiated_branch.instances = unrolled_instances[1:]
|
|
561
|
+
# break outer for loop,
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
else:
|
|
565
|
+
# in this case our instances have been a simple list of elements
|
|
566
|
+
# (as opposed to a list of lists), e.g. ['Left', 'Right'], so we
|
|
567
|
+
# just add them all in parallel as childs of the current
|
|
568
|
+
# loop
|
|
569
|
+
create_instantiated_branch(
|
|
570
|
+
instance, tree_node, nodes_to_expand)
|
|
571
|
+
|
|
572
|
+
# As instance expansions moves signals in the tree, we need to recreate
|
|
573
|
+
# UUIDs
|
|
574
|
+
create_tree_uuids(tree)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
#
|
|
578
|
+
# Take the flat model created by _load() and merge all $prefix$ with its name
|
|
579
|
+
# I.e: $prefix$ = "Cabin.Doors.1"
|
|
580
|
+
# name = "Window.Pos"
|
|
581
|
+
# -> name = "Cabin.Doors.1.Window.Pos"
|
|
582
|
+
#
|
|
583
|
+
# $prefix$ is deleted
|
|
584
|
+
#
|
|
585
|
+
#
|
|
586
|
+
def create_absolute_paths(flat_model):
|
|
587
|
+
for elem in flat_model:
|
|
588
|
+
# Create a list of path components to the given element
|
|
589
|
+
#
|
|
590
|
+
# $prefix$='body.door.front.left' name='lock' ->
|
|
591
|
+
# [ 'body', 'door', 'front', 'left', 'lock' ]
|
|
592
|
+
name = elem['$name$']
|
|
593
|
+
|
|
594
|
+
if elem["$prefix$"] == "":
|
|
595
|
+
new_name = name
|
|
596
|
+
else:
|
|
597
|
+
new_name = "{}.{}".format(elem["$prefix$"], name)
|
|
598
|
+
|
|
599
|
+
elem['$name$'] = new_name
|
|
600
|
+
del elem["$prefix$"]
|
|
601
|
+
|
|
602
|
+
return flat_model
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
#
|
|
606
|
+
# Take the flat model with absolute signal names parsed from the vspec
|
|
607
|
+
# file and create a nested variant where each component of a prefix
|
|
608
|
+
# becomes a branch.
|
|
609
|
+
#
|
|
610
|
+
|
|
611
|
+
def create_nested_model(flat_model, file_name):
|
|
612
|
+
deep_model = {
|
|
613
|
+
"children": {},
|
|
614
|
+
"type": "branch",
|
|
615
|
+
"$file_name$": file_name,
|
|
616
|
+
"$line$": 0
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# Traverse the flat list of the parsed specification
|
|
620
|
+
for elem in flat_model:
|
|
621
|
+
# Create children for branch type objects
|
|
622
|
+
if elem["type"] in nestable_types:
|
|
623
|
+
elem["children"] = {}
|
|
624
|
+
|
|
625
|
+
# Create a list of path components to the given element
|
|
626
|
+
# name='body.door.front.left.lock' ->
|
|
627
|
+
# [ 'body', 'door', 'front', 'left', 'lock' ]
|
|
628
|
+
name_list = elem['$name$'].split(".")
|
|
629
|
+
|
|
630
|
+
# Extract name
|
|
631
|
+
name = name_list[-1]
|
|
632
|
+
|
|
633
|
+
# Locate the correct branch in the tree
|
|
634
|
+
parent_branch = find_branch_or_struct(deep_model, name_list[:-1], 0)
|
|
635
|
+
|
|
636
|
+
# If an element with name is already in the parent branch
|
|
637
|
+
# we update its fields with the fields from the new element
|
|
638
|
+
if name in parent_branch["children"]:
|
|
639
|
+
old_elem = parent_branch["children"][name]
|
|
640
|
+
# never update the type
|
|
641
|
+
elem.pop("type", None)
|
|
642
|
+
# concatenate file names
|
|
643
|
+
fname = "{}:{}".format(
|
|
644
|
+
old_elem["$file_name$"], elem["$file_name$"])
|
|
645
|
+
old_elem.update(elem)
|
|
646
|
+
old_elem["$file_name$"] = fname
|
|
647
|
+
else:
|
|
648
|
+
parent_branch["children"][name] = elem
|
|
649
|
+
|
|
650
|
+
return deep_model
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# Find the given prefix somewhere under the tree rooted in elem.
|
|
654
|
+
# If the branch referred to by prefix is not found, create it implicitly
|
|
655
|
+
# if autocreate is True. Note that structs are not auto-created.
|
|
656
|
+
def find_branch_or_struct(elem, name_list, index, autocreate=True):
|
|
657
|
+
# Have we reached the end of the name list
|
|
658
|
+
if len(name_list) == index:
|
|
659
|
+
if (elem['type'] not in nestable_types):
|
|
660
|
+
raise VSpecError(
|
|
661
|
+
elem.get(
|
|
662
|
+
"$file_name$", "??"), elem.get(
|
|
663
|
+
"$line$", "??"), "Not a branch or struct: {}.".format(
|
|
664
|
+
elem['$name$']))
|
|
665
|
+
|
|
666
|
+
return elem
|
|
667
|
+
|
|
668
|
+
if (elem['type'] not in nestable_types):
|
|
669
|
+
raise VSpecError(elem.get("$file_name$", "??"),
|
|
670
|
+
elem.get("$line$", "??"),
|
|
671
|
+
"{} is not a branch or struct.".format(list_to_path(name_list[:index])))
|
|
672
|
+
|
|
673
|
+
children = elem["children"]
|
|
674
|
+
|
|
675
|
+
if name_list[index] not in children:
|
|
676
|
+
if autocreate and elem['type'] != "struct":
|
|
677
|
+
logging.info(f"Autocreating implicit branch {name_list[index]}")
|
|
678
|
+
|
|
679
|
+
# If we are above Vehicle (e.g. vehicle not defined), we are
|
|
680
|
+
# missing a name
|
|
681
|
+
if "$name$" not in elem:
|
|
682
|
+
elem['$name$'] = ""
|
|
683
|
+
# autopep8: off
|
|
684
|
+
newelem = {'type': elem['type'],
|
|
685
|
+
'children': {},
|
|
686
|
+
'$line$': '0',
|
|
687
|
+
'$file_name$':
|
|
688
|
+
'<generated>',
|
|
689
|
+
'$name$': f"{elem['$name$']}.{name_list[index]}"}
|
|
690
|
+
# autopep8: on
|
|
691
|
+
children[name_list[index]] = newelem
|
|
692
|
+
# Search again
|
|
693
|
+
find_branch_or_struct(elem, name_list, index, autocreate)
|
|
694
|
+
else:
|
|
695
|
+
raise VSpecError(
|
|
696
|
+
elem.get(
|
|
697
|
+
"$file_name$", "??"), elem.get(
|
|
698
|
+
"$line$", "??"), "Missing branch: {} in {}.".format(
|
|
699
|
+
name_list[index], list_to_path(name_list)))
|
|
700
|
+
|
|
701
|
+
# Traverse all children, looking for the
|
|
702
|
+
# Move on to next element in prefix.
|
|
703
|
+
return find_branch_or_struct(
|
|
704
|
+
children[name_list[index]], name_list, index + 1, autocreate)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def list_to_path(name_list):
|
|
708
|
+
path = ""
|
|
709
|
+
for name in name_list:
|
|
710
|
+
if path == "":
|
|
711
|
+
path = name
|
|
712
|
+
else:
|
|
713
|
+
path = "{}.{}".format(path, name)
|
|
714
|
+
|
|
715
|
+
return path
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
#
|
|
719
|
+
# Convert
|
|
720
|
+
# #include door.vspec, body.door.front.left
|
|
721
|
+
# to
|
|
722
|
+
# - $include$:
|
|
723
|
+
# file: door.vspec
|
|
724
|
+
# prefix: body.door.front.left
|
|
725
|
+
#
|
|
726
|
+
# This yaml version of the include directive will
|
|
727
|
+
# then be further processed to actually include
|
|
728
|
+
# the given file.
|
|
729
|
+
#
|
|
730
|
+
def yamilify_includes(text, is_list):
|
|
731
|
+
|
|
732
|
+
# Logic below expects a new line after "#include", to support vspec files where there
|
|
733
|
+
# is no newline at end we add a newline here
|
|
734
|
+
text += '\n'
|
|
735
|
+
|
|
736
|
+
while True:
|
|
737
|
+
st_index = text.find("\n#include")
|
|
738
|
+
if st_index == -1:
|
|
739
|
+
return text
|
|
740
|
+
|
|
741
|
+
end_index = text.find("\n", st_index + 1)
|
|
742
|
+
if end_index == -1:
|
|
743
|
+
return text
|
|
744
|
+
|
|
745
|
+
include_arg = text[st_index + 10:end_index].split()
|
|
746
|
+
if len(include_arg) == 2:
|
|
747
|
+
[include_file, include_prefix] = include_arg
|
|
748
|
+
else:
|
|
749
|
+
include_prefix = '""'
|
|
750
|
+
[include_file] = include_arg
|
|
751
|
+
|
|
752
|
+
if is_list:
|
|
753
|
+
fmt_str = """{}
|
|
754
|
+
|
|
755
|
+
- $name$: $include$
|
|
756
|
+
file: {}
|
|
757
|
+
prefix: {}
|
|
758
|
+
{}"""
|
|
759
|
+
else:
|
|
760
|
+
fmt_str = """{}
|
|
761
|
+
|
|
762
|
+
$include$:
|
|
763
|
+
file: {}
|
|
764
|
+
prefix: {}
|
|
765
|
+
{}"""
|
|
766
|
+
|
|
767
|
+
text = fmt_str.format(
|
|
768
|
+
text[:st_index], include_file, include_prefix, text[end_index:])
|
|
769
|
+
|
|
770
|
+
return text
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def render_tree(
|
|
774
|
+
tree_dict,
|
|
775
|
+
tree_type: VSSTreeType,
|
|
776
|
+
break_on_name_style_violation=False) -> VSSNode:
|
|
777
|
+
if len(tree_dict) != 1:
|
|
778
|
+
for item in tree_dict.keys():
|
|
779
|
+
logging.info(f"Found root node {item}")
|
|
780
|
+
raise Exception(
|
|
781
|
+
f"Invalid VSS model, must have single root node, found {len(tree_dict)}")
|
|
782
|
+
|
|
783
|
+
root_element_name = next(iter(tree_dict.keys()))
|
|
784
|
+
root_element = tree_dict[root_element_name]
|
|
785
|
+
if root_element['type'] != 'branch':
|
|
786
|
+
raise Exception(
|
|
787
|
+
f"Invalid VSS model, root must be branch, found {root_element_name} of type {root_element['type']}")
|
|
788
|
+
|
|
789
|
+
tree_root = VSSNode(
|
|
790
|
+
root_element_name,
|
|
791
|
+
root_element,
|
|
792
|
+
tree_type.available_types(),
|
|
793
|
+
break_on_name_style_violation=break_on_name_style_violation)
|
|
794
|
+
|
|
795
|
+
if "children" in root_element.keys():
|
|
796
|
+
child_nodes = root_element["children"]
|
|
797
|
+
render_subtree(
|
|
798
|
+
child_nodes,
|
|
799
|
+
tree_type,
|
|
800
|
+
tree_root,
|
|
801
|
+
break_on_name_style_violation=break_on_name_style_violation)
|
|
802
|
+
|
|
803
|
+
create_tree_uuids(tree_root)
|
|
804
|
+
return tree_root
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def render_subtree(
|
|
808
|
+
subtree,
|
|
809
|
+
tree_type: VSSTreeType,
|
|
810
|
+
parent,
|
|
811
|
+
break_on_name_style_violation=False):
|
|
812
|
+
for element_name in subtree:
|
|
813
|
+
current_element = subtree[element_name]
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
new_element = VSSNode(
|
|
817
|
+
element_name,
|
|
818
|
+
current_element,
|
|
819
|
+
tree_type.available_types(),
|
|
820
|
+
parent=parent,
|
|
821
|
+
break_on_name_style_violation=break_on_name_style_violation)
|
|
822
|
+
except IncompleteElementException as e:
|
|
823
|
+
logging.error(f"Invalid VSS: {e}")
|
|
824
|
+
logging.error("Terminating.")
|
|
825
|
+
sys.exit(-1)
|
|
826
|
+
if "children" in current_element.keys():
|
|
827
|
+
child_nodes = current_element["children"]
|
|
828
|
+
render_subtree(
|
|
829
|
+
child_nodes,
|
|
830
|
+
tree_type,
|
|
831
|
+
new_element,
|
|
832
|
+
break_on_name_style_violation=break_on_name_style_violation)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def merge_elem(base, overlay_element):
|
|
836
|
+
r = Resolver()
|
|
837
|
+
element_name = "/" + overlay_element.qualified_name("/")
|
|
838
|
+
|
|
839
|
+
if not VSSNode.node_exists(base, element_name):
|
|
840
|
+
# The node in the overlay does not exist, so we connect it
|
|
841
|
+
# print(f"Not exists {overlay_element.qualified_name()} does not exist, creating.")
|
|
842
|
+
new_parent_name = "/" + overlay_element.parent.qualified_name("/")
|
|
843
|
+
new_parent = r.get(base, new_parent_name)
|
|
844
|
+
overlay_element.parent = new_parent
|
|
845
|
+
|
|
846
|
+
else:
|
|
847
|
+
# else we merge the node. The merge function of VSSNode is not recursive
|
|
848
|
+
# so children in base will not be overwritten
|
|
849
|
+
# print(f"Merging {overlay_element.qualified_name()}")
|
|
850
|
+
other_node: VSSNode = r.get(base, element_name)
|
|
851
|
+
try:
|
|
852
|
+
other_node.merge(overlay_element)
|
|
853
|
+
except ImpossibleMergeException as e:
|
|
854
|
+
logging.error(f"Merging impossible: {e}")
|
|
855
|
+
sys.exit(-1)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def merge_tree(base: VSSNode, overlay: VSSNode):
|
|
859
|
+
overlay_element: VSSNode
|
|
860
|
+
for overlay_element in LevelOrderIter(overlay):
|
|
861
|
+
merge_elem(base, overlay_element)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def create_tree_uuids(root: VSSNode):
|
|
865
|
+
VSS_NAMESPACE = "vehicle_signal_specification"
|
|
866
|
+
namespace_uuid = uuid.uuid5(uuid.NAMESPACE_OID, VSS_NAMESPACE)
|
|
867
|
+
vss_element: VSSNode
|
|
868
|
+
for vss_element in PreOrderIter(root):
|
|
869
|
+
vss_element.uuid = uuid.uuid5(
|
|
870
|
+
namespace_uuid, vss_element.qualified_name()).hex
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def load_quantities(vspec_file: str, quantity_files: List[str]):
|
|
874
|
+
"""
|
|
875
|
+
Reset the list of quantities defined (if any)
|
|
876
|
+
and add new quantities
|
|
877
|
+
"""
|
|
878
|
+
VSSQuantityCollection.reset_quantities()
|
|
879
|
+
|
|
880
|
+
total_nbr_quantities = 0
|
|
881
|
+
if not quantity_files:
|
|
882
|
+
# Search for a file quantities.yaml in same directory as vspec file
|
|
883
|
+
vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
|
|
884
|
+
default_vss_quantity_file = vspec_dir + os.path.sep + 'quantities.yaml'
|
|
885
|
+
if os.path.exists(default_vss_quantity_file):
|
|
886
|
+
total_nbr_quantities = VSSQuantityCollection.load_config_file(default_vss_quantity_file)
|
|
887
|
+
logging.info(f"Added {total_nbr_quantities} quantities from {default_vss_quantity_file}")
|
|
888
|
+
else:
|
|
889
|
+
for quantity_file in quantity_files:
|
|
890
|
+
nbr_quantities = VSSQuantityCollection.load_config_file(quantity_file)
|
|
891
|
+
if (nbr_quantities == 0):
|
|
892
|
+
logging.warning(f"Warning: No quantities found in {quantity_file}")
|
|
893
|
+
else:
|
|
894
|
+
logging.info(f"Added {nbr_quantities} quantities from {quantity_file}")
|
|
895
|
+
total_nbr_quantities += nbr_quantities
|
|
896
|
+
|
|
897
|
+
if (total_nbr_quantities == 0):
|
|
898
|
+
logging.info("No quantities defined!")
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def load_units(vspec_file: str, unit_files: List[str]):
|
|
902
|
+
"""
|
|
903
|
+
Reset the list of units defined (if any)
|
|
904
|
+
and add new units
|
|
905
|
+
"""
|
|
906
|
+
VSSUnitCollection.reset_units()
|
|
907
|
+
|
|
908
|
+
total_nbr_units = 0
|
|
909
|
+
if not unit_files:
|
|
910
|
+
# Search for a file units.yaml in same directory as vspec file
|
|
911
|
+
vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
|
|
912
|
+
default_vss_unit_file = vspec_dir + os.path.sep + 'units.yaml'
|
|
913
|
+
if os.path.exists(default_vss_unit_file):
|
|
914
|
+
total_nbr_units = VSSUnitCollection.load_config_file(default_vss_unit_file)
|
|
915
|
+
logging.info(f"Added {total_nbr_units} units from {default_vss_unit_file}")
|
|
916
|
+
else:
|
|
917
|
+
for unit_file in unit_files:
|
|
918
|
+
nbr_units = VSSUnitCollection.load_config_file(unit_file)
|
|
919
|
+
if (nbr_units == 0):
|
|
920
|
+
logging.warning(f"Warning: No units found in {unit_file}")
|
|
921
|
+
else:
|
|
922
|
+
logging.info(f"Added {nbr_units} units from {unit_file}")
|
|
923
|
+
total_nbr_units += nbr_units
|
|
924
|
+
|
|
925
|
+
if (total_nbr_units == 0):
|
|
926
|
+
logging.error("No units defined! Terminating!")
|
|
927
|
+
sys.exit(-1)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def check_data_type_references(tree: VSSNode):
|
|
931
|
+
"""
|
|
932
|
+
Check that the data type names referenced by tree nodes exist in the tree.
|
|
933
|
+
"""
|
|
934
|
+
errors: List[str] = []
|
|
935
|
+
check_data_type_references_recursive(
|
|
936
|
+
tree, [
|
|
937
|
+
str(attr) for attr in VSSNode.get_tree_attrs(
|
|
938
|
+
tree, lambda n: n.qualified_name(), lambda n: n.is_struct())], errors)
|
|
939
|
+
if len(errors) > 0:
|
|
940
|
+
error_str = ",".join(errors)
|
|
941
|
+
logging.error(
|
|
942
|
+
f"{len(errors)} data type reference errors detected. Errors: {error_str}")
|
|
943
|
+
sys.exit(-1)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def check_data_type_references_recursive(
|
|
947
|
+
node: VSSNode, data_type_qualified_names: List[str], errors: List[str]):
|
|
948
|
+
"""
|
|
949
|
+
Check that the data type names referenced by tree nodes exist in the tree.
|
|
950
|
+
"""
|
|
951
|
+
if node.is_property() or node.is_signal():
|
|
952
|
+
if node.datatype is None:
|
|
953
|
+
undecorated_data_type = node.data_type_str.replace('[]', '')
|
|
954
|
+
if undecorated_data_type not in data_type_qualified_names:
|
|
955
|
+
errors.append(
|
|
956
|
+
f"Data Type reference invalid. Node Name: {node.name}. "
|
|
957
|
+
f"Node Qualified Name: {node.qualified_name()}. "
|
|
958
|
+
f"Data Type: {undecorated_data_type}")
|
|
959
|
+
|
|
960
|
+
separator = "."
|
|
961
|
+
# is the property circularly refereing to the struct in which it is defined?
|
|
962
|
+
member_of = separator.join(node.qualified_name(separator).split(separator)[:-1])
|
|
963
|
+
if member_of == undecorated_data_type:
|
|
964
|
+
errors.append(
|
|
965
|
+
"Circular reference detected in data type. "
|
|
966
|
+
f"Data Type: {member_of}. Property: {node.name}")
|
|
967
|
+
|
|
968
|
+
for n in node.children:
|
|
969
|
+
check_data_type_references_recursive(
|
|
970
|
+
n, data_type_qualified_names, errors)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def check_data_type_references_across_trees(
|
|
974
|
+
signal_root: VSSNode, data_type_root: Optional[VSSNode]):
|
|
975
|
+
"""
|
|
976
|
+
Check that the data type names referenced by nodes in the signal tree are present in the data types tree.
|
|
977
|
+
"""
|
|
978
|
+
nonexistant_types = []
|
|
979
|
+
for _, _, node in RenderTree(signal_root):
|
|
980
|
+
# signals with user-defined data types
|
|
981
|
+
if node.is_signal() and node.datatype is None:
|
|
982
|
+
if data_type_root is None or (not node.does_attribute_exist(
|
|
983
|
+
data_type_root,
|
|
984
|
+
lambda n: n.base_data_type_str(),
|
|
985
|
+
lambda n: n.qualified_name(),
|
|
986
|
+
lambda n: n.is_struct())):
|
|
987
|
+
nonexistant_types.append(f'{node.data_type_str}')
|
|
988
|
+
|
|
989
|
+
if len(nonexistant_types) > 0:
|
|
990
|
+
error_str = ", ".join(nonexistant_types)
|
|
991
|
+
logging.error(
|
|
992
|
+
f"Following types were referenced in signals but have not been defined: {error_str}")
|
|
993
|
+
sys.exit(-1)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
load_flat_model.include_index = 1 # type: ignore[attr-defined]
|