@anik-2/autosar 0.0.0-stage → 0.1.1
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.
- package/.hydron-plugin/plugin.json +3 -0
- package/README.md +55 -2
- package/agents/autosar-integrator.md +44 -0
- package/commands/autosar-trace.md +11 -0
- package/commands/autosar-validate.md +12 -0
- package/package.json +44 -4
- package/plugins/index.js +21 -0
- package/python/ecuc_engine/__init__.py +2 -0
- package/python/ecuc_engine/__main__.py +5 -0
- package/python/ecuc_engine/cli.py +111 -0
- package/python/ecuc_engine/defs.py +330 -0
- package/python/ecuc_engine/diff.py +111 -0
- package/python/ecuc_engine/ecucjson.py +154 -0
- package/python/ecuc_engine/edit.py +429 -0
- package/python/ecuc_engine/model.py +330 -0
- package/python/ecuc_engine/rules/__init__.py +82 -0
- package/python/ecuc_engine/rules/com.py +107 -0
- package/python/ecuc_engine/rules/handles.py +42 -0
- package/python/ecuc_engine/validate.py +227 -0
- package/python/ecuc_engine/values.py +146 -0
- package/python/ecuc_engine/xmltree.py +141 -0
- package/python/pyproject.toml +13 -0
- package/skills/autosar/SKILL.md +70 -0
- package/skills/autosar/references/config-classes.md +23 -0
- package/skills/autosar/references/ecuc-model.md +42 -0
- package/skills/autosar/references/project-layout.md +29 -0
- package/skills/autosar/references/symbolic-names.md +25 -0
- package/skills/autosar/references/workflow.md +29 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""ECUC parameter-definition model (StMD and vendor VSMD alike).
|
|
2
|
+
|
|
3
|
+
Schema-free: every def kind is recognised by its tag suffix, so the loader works across AUTOSAR
|
|
4
|
+
releases (R4.0 ... R25-11) without an XSD. A def is addressed by its AUTOSAR path, the chain of
|
|
5
|
+
SHORT-NAMEs from the root package, which is exactly what a value's DEFINITION-REF holds.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import os
|
|
11
|
+
import pickle
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from . import ENGINE_VERSION
|
|
17
|
+
|
|
18
|
+
MODULE_TAG = "ECUC-MODULE-DEF"
|
|
19
|
+
CONTAINER_TAGS = {"ECUC-PARAM-CONF-CONTAINER-DEF", "ECUC-CHOICE-CONTAINER-DEF"}
|
|
20
|
+
CHOICE_TAG = "ECUC-CHOICE-CONTAINER-DEF"
|
|
21
|
+
COLLECTIONS_CONTAINER = {"CONTAINERS", "SUB-CONTAINERS", "CHOICES"}
|
|
22
|
+
|
|
23
|
+
NUMERICAL_KINDS = {"INTEGER", "FLOAT", "BOOLEAN"}
|
|
24
|
+
TEXTUAL_KINDS = {"ENUMERATION", "STRING", "FUNCTION-NAME", "LINKER-SYMBOL", "MULTILINE-STRING"}
|
|
25
|
+
ECUC_REF_KINDS = {"REFERENCE", "CHOICE-REFERENCE", "SYMBOLIC-NAME-REFERENCE"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _local(tag: str) -> str:
|
|
29
|
+
return tag.rsplit("}", 1)[-1]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def param_kind(tag: str) -> Optional[str]:
|
|
33
|
+
"""ECUC-INTEGER-PARAM-DEF -> INTEGER; ECUC-FUNCTION-NAME-DEF -> FUNCTION-NAME; refs -> e.g. FOREIGN-REFERENCE."""
|
|
34
|
+
if not tag.startswith("ECUC-") or not tag.endswith("-DEF"):
|
|
35
|
+
return None
|
|
36
|
+
core = tag[5:-4]
|
|
37
|
+
if core.endswith("-PARAM"):
|
|
38
|
+
return core[:-6]
|
|
39
|
+
if core in ("FUNCTION-NAME", "LINKER-SYMBOL"):
|
|
40
|
+
return core
|
|
41
|
+
if core.endswith("-REFERENCE") or core == "REFERENCE":
|
|
42
|
+
return core
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_ref_kind(kind: str) -> bool:
|
|
47
|
+
return kind.endswith("REFERENCE")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class DefNode:
|
|
52
|
+
path: str
|
|
53
|
+
tag: str
|
|
54
|
+
name: str
|
|
55
|
+
file: str
|
|
56
|
+
lower: int = 1
|
|
57
|
+
upper: Optional[int] = 1 # None = infinite
|
|
58
|
+
kind: Optional[str] = None # params/refs only
|
|
59
|
+
min: Optional[str] = None
|
|
60
|
+
max: Optional[str] = None
|
|
61
|
+
min_open: bool = False
|
|
62
|
+
max_open: bool = False
|
|
63
|
+
literals: list = field(default_factory=list)
|
|
64
|
+
default: Optional[str] = None
|
|
65
|
+
regex: Optional[str] = None
|
|
66
|
+
min_len: Optional[int] = None
|
|
67
|
+
max_len: Optional[int] = None
|
|
68
|
+
dest_refs: list = field(default_factory=list)
|
|
69
|
+
dest_type: Optional[str] = None
|
|
70
|
+
with_auto: bool = False
|
|
71
|
+
symbolic_name: bool = False
|
|
72
|
+
obsolete: bool = False
|
|
73
|
+
trace: Optional[str] = None
|
|
74
|
+
containers: list = field(default_factory=list) # child container def paths (incl. choices)
|
|
75
|
+
params: list = field(default_factory=list) # child param def paths
|
|
76
|
+
refs: list = field(default_factory=list) # child ref def paths
|
|
77
|
+
parent: Optional[str] = None
|
|
78
|
+
module: Optional[str] = None # module def path
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_container(self) -> bool:
|
|
82
|
+
return self.tag in CONTAINER_TAGS or self.tag == MODULE_TAG
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def mult(self) -> str:
|
|
86
|
+
return f"{self.lower}..{'*' if self.upper is None else self.upper}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _int(s: Optional[str], default):
|
|
90
|
+
try:
|
|
91
|
+
return int(s.strip()) if s is not None else default
|
|
92
|
+
except ValueError:
|
|
93
|
+
return default
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DefIndex:
|
|
97
|
+
def __init__(self):
|
|
98
|
+
self.by_path: dict[str, DefNode] = {}
|
|
99
|
+
self.modules: dict[str, DefNode] = {} # module def path -> node
|
|
100
|
+
self.files: list[str] = []
|
|
101
|
+
|
|
102
|
+
def __bool__(self):
|
|
103
|
+
return bool(self.modules)
|
|
104
|
+
|
|
105
|
+
def get(self, path: Optional[str]) -> Optional[DefNode]:
|
|
106
|
+
return self.by_path.get(path) if path else None
|
|
107
|
+
|
|
108
|
+
def module_of(self, def_path: str) -> Optional[DefNode]:
|
|
109
|
+
best = None
|
|
110
|
+
for mp, m in self.modules.items():
|
|
111
|
+
if (def_path == mp or def_path.startswith(mp + "/")) and (best is None or len(mp) > len(best.path)):
|
|
112
|
+
best = m
|
|
113
|
+
return best
|
|
114
|
+
|
|
115
|
+
def rel(self, def_path: str) -> str:
|
|
116
|
+
"""Release/vendor-neutral form: '<ModuleName>/<rest>' (so /MICROSAR/Com/X == /AUTOSAR/EcucDefs/Com/X)."""
|
|
117
|
+
m = self.module_of(def_path)
|
|
118
|
+
if m is not None:
|
|
119
|
+
return m.name + def_path[len(m.path):]
|
|
120
|
+
parts = def_path.strip("/").split("/")
|
|
121
|
+
# Unknown module: fall back to dropping the conventional package prefix.
|
|
122
|
+
return "/".join(parts[2:]) if len(parts) > 2 else def_path
|
|
123
|
+
|
|
124
|
+
def merge(self, other: "DefIndex"):
|
|
125
|
+
self.by_path.update(other.by_path)
|
|
126
|
+
self.modules.update(other.modules)
|
|
127
|
+
self.files.extend(other.files)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _cache_path(path: str) -> str:
|
|
131
|
+
st = os.stat(path)
|
|
132
|
+
key = f"{ENGINE_VERSION}|{os.path.abspath(path)}|{st.st_size}|{st.st_mtime_ns}"
|
|
133
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
|
|
134
|
+
return os.path.join(base, "hydron-autosar", "defs-" + hashlib.sha256(key.encode()).hexdigest()[:24] + ".pkl")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def load_def_file(path: str, use_cache: bool = True) -> DefIndex:
|
|
138
|
+
cp = None
|
|
139
|
+
if use_cache:
|
|
140
|
+
try:
|
|
141
|
+
cp = _cache_path(path)
|
|
142
|
+
with open(cp, "rb") as f:
|
|
143
|
+
return pickle.load(f)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
idx = _parse_def_file(path)
|
|
147
|
+
if cp:
|
|
148
|
+
try:
|
|
149
|
+
os.makedirs(os.path.dirname(cp), exist_ok=True)
|
|
150
|
+
tmp = cp + f".{os.getpid()}.tmp"
|
|
151
|
+
with open(tmp, "wb") as f:
|
|
152
|
+
pickle.dump(idx, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
153
|
+
os.replace(tmp, cp)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return idx
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _parse_def_file(path: str) -> DefIndex:
|
|
160
|
+
idx = DefIndex()
|
|
161
|
+
idx.files.append(path)
|
|
162
|
+
root = ET.parse(path).getroot()
|
|
163
|
+
|
|
164
|
+
def short(el) -> Optional[str]:
|
|
165
|
+
for c in el:
|
|
166
|
+
if _local(c.tag) == "SHORT-NAME":
|
|
167
|
+
return (c.text or "").strip()
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
skip = {"DESC", "INTRODUCTION", "ADMIN-DATA", "ANNOTATIONS", "LONG-NAME", "VALUE-CONFIG-CLASSES",
|
|
171
|
+
"MULTIPLICITY-CONFIG-CLASSES"}
|
|
172
|
+
|
|
173
|
+
def walk(el, prefix: str, module: Optional[DefNode], parent: Optional[DefNode]):
|
|
174
|
+
tag = _local(el.tag)
|
|
175
|
+
sn = short(el)
|
|
176
|
+
path = prefix
|
|
177
|
+
if sn is not None:
|
|
178
|
+
path = f"{prefix}/{sn}"
|
|
179
|
+
is_def = tag == MODULE_TAG or tag in CONTAINER_TAGS or (param_kind(tag) is not None and parent is not None)
|
|
180
|
+
if not is_def:
|
|
181
|
+
parent = None # some other identifiable (package, collection, value element)
|
|
182
|
+
else:
|
|
183
|
+
node = _make_def(el, tag, sn, path, path_file=path_file)
|
|
184
|
+
node.parent = parent.path if parent else None
|
|
185
|
+
if tag == MODULE_TAG:
|
|
186
|
+
module = node
|
|
187
|
+
idx.modules[path] = node
|
|
188
|
+
node.module = module.path if module else None
|
|
189
|
+
idx.by_path[path] = node
|
|
190
|
+
if parent is not None:
|
|
191
|
+
if node.is_container:
|
|
192
|
+
parent.containers.append(path)
|
|
193
|
+
elif is_ref_kind(node.kind or ""):
|
|
194
|
+
parent.refs.append(path)
|
|
195
|
+
else:
|
|
196
|
+
parent.params.append(path)
|
|
197
|
+
if node.kind: # params/refs hold no nested defs
|
|
198
|
+
return
|
|
199
|
+
parent = node
|
|
200
|
+
for c in el:
|
|
201
|
+
if _local(c.tag) not in skip:
|
|
202
|
+
walk(c, path, module, parent)
|
|
203
|
+
|
|
204
|
+
path_file = path
|
|
205
|
+
walk(root, "", None, None)
|
|
206
|
+
return idx
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _make_def(el, tag: str, sn: str, path: str, path_file: str) -> DefNode:
|
|
210
|
+
d = DefNode(path=path, tag=tag, name=sn, file=path_file)
|
|
211
|
+
d.kind = param_kind(tag) if tag not in CONTAINER_TAGS and tag != MODULE_TAG else None
|
|
212
|
+
direct = {}
|
|
213
|
+
for c in el:
|
|
214
|
+
direct.setdefault(_local(c.tag), c)
|
|
215
|
+
d.lower = _int(direct["LOWER-MULTIPLICITY"].text if "LOWER-MULTIPLICITY" in direct else None, 1)
|
|
216
|
+
inf = direct.get("UPPER-MULTIPLICITY-INFINITE")
|
|
217
|
+
if inf is not None and (inf.text or "").strip().lower() == "true":
|
|
218
|
+
d.upper = None
|
|
219
|
+
else:
|
|
220
|
+
d.upper = _int(direct["UPPER-MULTIPLICITY"].text if "UPPER-MULTIPLICITY" in direct else None, 1)
|
|
221
|
+
tr = direct.get("RELATED-TRACE-ITEM-REF")
|
|
222
|
+
if tr is not None:
|
|
223
|
+
d.trace = (tr.text or "").strip()
|
|
224
|
+
for c in el.iter():
|
|
225
|
+
t = _local(c.tag)
|
|
226
|
+
if t == "LIFE-CYCLE-INFO" or t == "LIFE-CYCLE-STATE-REF":
|
|
227
|
+
if "obsolete" in (c.text or "").lower():
|
|
228
|
+
d.obsolete = True
|
|
229
|
+
if d.kind is None:
|
|
230
|
+
return d
|
|
231
|
+
# Params/refs carry no nested identifiables except enum literals: a descendant scan is safe.
|
|
232
|
+
for c in el.iter():
|
|
233
|
+
t = _local(c.tag)
|
|
234
|
+
txt = (c.text or "").strip()
|
|
235
|
+
if t == "MIN" and d.min is None:
|
|
236
|
+
d.min, d.min_open = txt, (c.get("INTERVAL-TYPE") or "").upper() == "OPEN"
|
|
237
|
+
elif t == "MAX" and d.max is None:
|
|
238
|
+
d.max, d.max_open = txt, (c.get("INTERVAL-TYPE") or "").upper() == "OPEN"
|
|
239
|
+
elif t == "DEFAULT-VALUE" and d.default is None:
|
|
240
|
+
d.default = txt
|
|
241
|
+
elif t == "ECUC-ENUMERATION-LITERAL-DEF":
|
|
242
|
+
for s in c:
|
|
243
|
+
if _local(s.tag) == "SHORT-NAME":
|
|
244
|
+
d.literals.append((s.text or "").strip())
|
|
245
|
+
elif t == "REGULAR-EXPRESSION" and d.regex is None:
|
|
246
|
+
d.regex = txt
|
|
247
|
+
elif t == "MIN-LENGTH":
|
|
248
|
+
d.min_len = _int(txt, None)
|
|
249
|
+
elif t == "MAX-LENGTH":
|
|
250
|
+
d.max_len = _int(txt, None)
|
|
251
|
+
elif t in ("DESTINATION-REF",):
|
|
252
|
+
d.dest_refs.append(txt)
|
|
253
|
+
elif t == "DESTINATION-TYPE":
|
|
254
|
+
d.dest_type = txt
|
|
255
|
+
elif t == "WITH-AUTO":
|
|
256
|
+
d.with_auto = txt.lower() == "true"
|
|
257
|
+
elif t == "SYMBOLIC-NAME-VALUE":
|
|
258
|
+
d.symbolic_name = txt.lower() == "true"
|
|
259
|
+
return d
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def load_defs(paths: list[str], use_cache: bool = True) -> DefIndex:
|
|
263
|
+
idx = DefIndex()
|
|
264
|
+
for p in paths:
|
|
265
|
+
idx.merge(load_def_file(p, use_cache=use_cache))
|
|
266
|
+
return idx
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def describe(d: DefNode, defs: "DefIndex") -> dict:
|
|
270
|
+
"""Compact schema view of a def, for the agent (what may be set/added under it)."""
|
|
271
|
+
out: dict = {"@def": d.path, "@tag": d.tag, "@mult": d.mult}
|
|
272
|
+
if d.trace:
|
|
273
|
+
out["@trace"] = d.trace
|
|
274
|
+
if d.tag == CHOICE_TAG:
|
|
275
|
+
out["@choice"] = "exactly one of the sub-containers"
|
|
276
|
+
if d.kind:
|
|
277
|
+
out.update(_param_summary(d))
|
|
278
|
+
return out
|
|
279
|
+
params = {}
|
|
280
|
+
for p in d.params + d.refs:
|
|
281
|
+
pd = defs.by_path[p]
|
|
282
|
+
params[pd.name] = _param_line(pd)
|
|
283
|
+
if params:
|
|
284
|
+
out["params"] = params
|
|
285
|
+
subs = {}
|
|
286
|
+
for cpath in d.containers:
|
|
287
|
+
cd = defs.by_path[cpath]
|
|
288
|
+
subs[cd.name] = f"[{cd.mult}]" + (" choice" if cd.tag == CHOICE_TAG else "")
|
|
289
|
+
if subs:
|
|
290
|
+
out["containers"] = subs
|
|
291
|
+
return out
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _param_summary(d: DefNode) -> dict:
|
|
295
|
+
s: dict = {"type": d.kind, "mult": d.mult}
|
|
296
|
+
if d.min is not None or d.max is not None:
|
|
297
|
+
s["range"] = f"{'(' if d.min_open else '['}{d.min},{d.max}{')' if d.max_open else ']'}"
|
|
298
|
+
if d.literals:
|
|
299
|
+
s["literals"] = d.literals
|
|
300
|
+
if d.default is not None:
|
|
301
|
+
s["default"] = d.default
|
|
302
|
+
if d.regex:
|
|
303
|
+
s["regex"] = d.regex
|
|
304
|
+
if d.dest_refs:
|
|
305
|
+
s["dest"] = d.dest_refs
|
|
306
|
+
if d.dest_type:
|
|
307
|
+
s["destType"] = d.dest_type
|
|
308
|
+
if d.symbolic_name:
|
|
309
|
+
s["symbolicName"] = True
|
|
310
|
+
if d.obsolete:
|
|
311
|
+
s["obsolete"] = True
|
|
312
|
+
return s
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _param_line(d: DefNode) -> str:
|
|
316
|
+
bits = [d.kind or "?", f"[{d.mult}]"]
|
|
317
|
+
if d.min is not None or d.max is not None:
|
|
318
|
+
bits.append(f"{'(' if d.min_open else '['}{d.min}..{d.max}{')' if d.max_open else ']'}")
|
|
319
|
+
if d.literals:
|
|
320
|
+
lits = d.literals if len(d.literals) <= 12 else d.literals[:12] + [f"+{len(d.literals) - 12}"]
|
|
321
|
+
bits.append("{" + "|".join(lits) + "}")
|
|
322
|
+
if d.default is not None:
|
|
323
|
+
bits.append(f"default={d.default}")
|
|
324
|
+
if d.dest_refs:
|
|
325
|
+
bits.append("-> " + ",".join(r.rsplit("/", 1)[-1] for r in d.dest_refs))
|
|
326
|
+
if d.dest_type:
|
|
327
|
+
bits.append("-> " + d.dest_type)
|
|
328
|
+
if d.obsolete:
|
|
329
|
+
bits.append("OBSOLETE")
|
|
330
|
+
return " ".join(bits)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Semantic ARXML diff by AUTOSAR path.
|
|
2
|
+
|
|
3
|
+
Ignores UUIDs, whitespace, comments, element order and split-file layout. ECUC elements are compared
|
|
4
|
+
parameter by parameter (by def name); other identifiables by a canonical form of their own content
|
|
5
|
+
(nested identifiables are compared separately under their own paths).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .model import MODULE_VALUES, CONTAINER_VALUE, _walk_arxml, build_file_model
|
|
13
|
+
from .values import parse_int, parse_float
|
|
14
|
+
from .xmltree import Node
|
|
15
|
+
|
|
16
|
+
IGNORED_ATTRS = {"UUID", "T", "S"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _norm(raw: Optional[str], numerical: bool):
|
|
20
|
+
if raw is None:
|
|
21
|
+
return None
|
|
22
|
+
t = raw.strip()
|
|
23
|
+
if numerical:
|
|
24
|
+
lo = t.lower()
|
|
25
|
+
if lo in ("true", "false"):
|
|
26
|
+
return lo
|
|
27
|
+
i = parse_int(t)
|
|
28
|
+
if i is not None:
|
|
29
|
+
return str(i)
|
|
30
|
+
f = parse_float(t)
|
|
31
|
+
if f is not None:
|
|
32
|
+
return repr(f)
|
|
33
|
+
return t
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _canon(node: Node, top: bool = True) -> str:
|
|
37
|
+
if not top and node.short_name is not None:
|
|
38
|
+
return "" # nested identifiable: diffed under its own path
|
|
39
|
+
attrs = ",".join(f"{k}={v}" for k, v in sorted(node.attrs.items()) if k not in IGNORED_ATTRS and not k.startswith("xmlns"))
|
|
40
|
+
kids = sorted(c for c in (_canon(ch, False) for ch in node.children) if c)
|
|
41
|
+
txt = " ".join(node.text.split()) if not node.children else ""
|
|
42
|
+
return f"<{node.tag}[{attrs}]{txt}{''.join(kids)}>"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _side(target: str) -> dict:
|
|
46
|
+
files = [target] if os.path.isfile(target) else sorted(_walk_arxml(target))
|
|
47
|
+
out: dict = {}
|
|
48
|
+
for f in files:
|
|
49
|
+
with open(f, "rb") as fh:
|
|
50
|
+
fm = build_file_model(f, fh.read())
|
|
51
|
+
|
|
52
|
+
def visit(node: Node, prefix: str):
|
|
53
|
+
sn = node.short_name
|
|
54
|
+
p = prefix
|
|
55
|
+
if sn is not None:
|
|
56
|
+
p = f"{prefix}/{sn}"
|
|
57
|
+
if node.tag in (MODULE_VALUES, CONTAINER_VALUE):
|
|
58
|
+
ent = out.setdefault(p, {"tag": node.tag, "file": f, "line": node.line, "ecuc": True,
|
|
59
|
+
"def": node.child_text("DEFINITION-REF"), "values": {}})
|
|
60
|
+
for coll in node.children:
|
|
61
|
+
if coll.tag in ("PARAMETER-VALUES", "REFERENCE-VALUES"):
|
|
62
|
+
for pv in coll.children:
|
|
63
|
+
name = (pv.child_text("DEFINITION-REF") or "?").rsplit("/", 1)[-1]
|
|
64
|
+
vn = pv.child("VALUE") or pv.child("VALUE-REF")
|
|
65
|
+
v = _norm(vn.text if vn is not None else None, pv.tag == "ECUC-NUMERICAL-PARAM-VALUE")
|
|
66
|
+
if vn is None and pv.child("VALUE-IREF") is not None:
|
|
67
|
+
v = _canon(pv.child("VALUE-IREF"))
|
|
68
|
+
ent["values"].setdefault(name, []).append(v)
|
|
69
|
+
for coll in node.children:
|
|
70
|
+
if coll.tag in ("CONTAINERS", "SUB-CONTAINERS"):
|
|
71
|
+
for c in coll.children:
|
|
72
|
+
visit(c, p)
|
|
73
|
+
return
|
|
74
|
+
out.setdefault(p, {"tag": node.tag, "file": f, "line": node.line, "ecuc": False, "canon": _canon(node)})
|
|
75
|
+
for c in node.children:
|
|
76
|
+
visit(c, p)
|
|
77
|
+
|
|
78
|
+
visit(fm.root, "")
|
|
79
|
+
for ent in out.values():
|
|
80
|
+
if ent.get("ecuc"):
|
|
81
|
+
ent["values"] = {k: sorted(v, key=lambda x: (x is None, str(x))) for k, v in ent["values"].items()}
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def diff(a: str, b: str, rel=lambda f: f) -> dict:
|
|
86
|
+
A, B = _side(a), _side(b)
|
|
87
|
+
added = [{"path": p, "tag": B[p]["tag"], "at": f"{rel(B[p]['file'])}:{B[p]['line']}"} for p in sorted(set(B) - set(A))]
|
|
88
|
+
removed = [{"path": p, "tag": A[p]["tag"], "at": f"{rel(A[p]['file'])}:{A[p]['line']}"} for p in sorted(set(A) - set(B))]
|
|
89
|
+
changed = []
|
|
90
|
+
for p in sorted(set(A) & set(B)):
|
|
91
|
+
x, y = A[p], B[p]
|
|
92
|
+
ch: dict = {}
|
|
93
|
+
if x["tag"] != y["tag"]:
|
|
94
|
+
ch["tag"] = [x["tag"], y["tag"]]
|
|
95
|
+
if x.get("ecuc") and y.get("ecuc"):
|
|
96
|
+
if x["def"] != y["def"]:
|
|
97
|
+
ch["definitionRef"] = [x["def"], y["def"]]
|
|
98
|
+
vals = {}
|
|
99
|
+
for k in sorted(set(x["values"]) | set(y["values"])):
|
|
100
|
+
va, vb = x["values"].get(k), y["values"].get(k)
|
|
101
|
+
if va != vb:
|
|
102
|
+
un = lambda v: v[0] if v and len(v) == 1 else v # noqa: E731
|
|
103
|
+
vals[k] = [un(va), un(vb)]
|
|
104
|
+
if vals:
|
|
105
|
+
ch["values"] = vals
|
|
106
|
+
elif x.get("canon") != y.get("canon"):
|
|
107
|
+
ch["content"] = "changed"
|
|
108
|
+
if ch:
|
|
109
|
+
changed.append({"path": p, "at": f"{rel(y['file'])}:{y['line']}", **ch})
|
|
110
|
+
return {"added": added, "removed": removed, "changed": changed,
|
|
111
|
+
"summary": {"added": len(added), "removed": len(removed), "changed": len(changed)}}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""ECUC JSON: the compact view the agent reads.
|
|
2
|
+
|
|
3
|
+
Keyed by def names, not XML structure:
|
|
4
|
+
{"Com": {"@path": "/Pkg/Com", "@def": "/AUTOSAR/EcucDefs/Com", "@at": "Config/Com.arxml:12",
|
|
5
|
+
"ComConfig": {"@path": ...,
|
|
6
|
+
"ComIPdu:DoorStatus_Tx": {"@path": ..., "ComIPduDirection": "SEND",
|
|
7
|
+
"ComPduIdRef": "->/EcuC/EcucPduCollection/DoorStatus_Tx",
|
|
8
|
+
"ComSignal:DoorLatch": {...}}}}}
|
|
9
|
+
A container key is "<DefName>:<ShortName>", shortened to "<ShortName>" when the two are equal.
|
|
10
|
+
Multi-valued parameters are arrays. References are "->/AUTOSAR/path".
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from . import defs as D
|
|
17
|
+
from .model import Elem, Project
|
|
18
|
+
from .validate import _resolve_def
|
|
19
|
+
from .values import to_json
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def key_of(e: Elem) -> str:
|
|
23
|
+
dn = (e.def_ref or "?").rsplit("/", 1)[-1]
|
|
24
|
+
return e.name if dn == e.name else f"{dn}:{e.name}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render(project: Project, group: list, depth: int, top: bool = True) -> dict:
|
|
28
|
+
defs = project.defs
|
|
29
|
+
e0 = group[0]
|
|
30
|
+
out: dict = {"@path": e0.path}
|
|
31
|
+
if top:
|
|
32
|
+
out["@def"] = e0.def_ref
|
|
33
|
+
out["@at"] = ", ".join(f"{project.rel(e.file)}:{e.line}" for e in group)
|
|
34
|
+
vals: dict = {}
|
|
35
|
+
for e in group:
|
|
36
|
+
for pv in e.params:
|
|
37
|
+
pd = _resolve_def(defs, pv.def_ref)
|
|
38
|
+
v = to_json(pd, pv.value, pv.is_ref, pv.tag == "ECUC-NUMERICAL-PARAM-VALUE")
|
|
39
|
+
name = pv.name
|
|
40
|
+
multi = pd is not None and (pd.upper is None or pd.upper > 1)
|
|
41
|
+
if name in vals:
|
|
42
|
+
if not isinstance(vals[name], list) or not multi:
|
|
43
|
+
vals[name] = vals[name] if isinstance(vals[name], list) else [vals[name]]
|
|
44
|
+
vals[name].append(v)
|
|
45
|
+
else:
|
|
46
|
+
vals[name] = [v] if multi else v
|
|
47
|
+
out.update(vals)
|
|
48
|
+
seen = set()
|
|
49
|
+
for e in group:
|
|
50
|
+
for c in e.children:
|
|
51
|
+
if c.path in seen:
|
|
52
|
+
continue
|
|
53
|
+
seen.add(c.path)
|
|
54
|
+
cg = project.find(c.path)
|
|
55
|
+
if depth <= 0:
|
|
56
|
+
n_p = sum(len(x.params) for x in cg)
|
|
57
|
+
n_c = sum(len(x.children) for x in cg)
|
|
58
|
+
out[key_of(c)] = f"… {n_p} values, {n_c} sub-containers (read {c.path})"
|
|
59
|
+
else:
|
|
60
|
+
out[key_of(c)] = render(project, cg, depth - 1, top=False)
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read(project: Project, path: Optional[str], depth: int = 2, schema: bool = False) -> dict:
|
|
65
|
+
res: dict = {"defsLoaded": len(project.defs.modules)}
|
|
66
|
+
if project.warnings:
|
|
67
|
+
res["warnings"] = project.warnings
|
|
68
|
+
if not path:
|
|
69
|
+
mods: dict = {}
|
|
70
|
+
for m in project.modules:
|
|
71
|
+
g = project.find(m.path)
|
|
72
|
+
ent = mods.setdefault(m.path, {"module": m.name, "path": m.path, "def": m.def_ref, "files": [],
|
|
73
|
+
"containers": 0, "values": 0})
|
|
74
|
+
ent["files"].append(f"{project.rel(m.file)}:{m.line}")
|
|
75
|
+
ent["containers"] += sum(1 for _ in _walk(m)) - 1
|
|
76
|
+
ent["values"] += sum(len(x.params) for x in _walk(m))
|
|
77
|
+
res["modules"] = list(mods.values())
|
|
78
|
+
res["hint"] = "read a module or container by its AUTOSAR path, e.g. path=" + (
|
|
79
|
+
res["modules"][0]["path"] if res["modules"] else "/Pkg/Module")
|
|
80
|
+
return res
|
|
81
|
+
resolved = resolve(project, path)
|
|
82
|
+
if isinstance(resolved, dict):
|
|
83
|
+
res.update(resolved)
|
|
84
|
+
return res
|
|
85
|
+
if resolved != path:
|
|
86
|
+
res["resolved"] = f"{path} -> {resolved}"
|
|
87
|
+
path = resolved
|
|
88
|
+
group = project.find(path)
|
|
89
|
+
if group:
|
|
90
|
+
res["ecuc"] = {key_of(group[0]): render(project, group, depth)}
|
|
91
|
+
if schema:
|
|
92
|
+
d = _resolve_def(project.defs, group[0].def_ref)
|
|
93
|
+
res["schema"] = D.describe(d, project.defs) if d else None
|
|
94
|
+
return res
|
|
95
|
+
# a def path? describe the definition
|
|
96
|
+
d = project.defs.get(path)
|
|
97
|
+
if d is not None:
|
|
98
|
+
res["schema"] = D.describe(d, project.defs)
|
|
99
|
+
return res
|
|
100
|
+
# parent/param form: /.../Container/ParamName
|
|
101
|
+
parent, _, last = path.rstrip("/").rpartition("/")
|
|
102
|
+
pg = project.find(parent)
|
|
103
|
+
if pg:
|
|
104
|
+
hits = [pv for e in pg for pv in e.params if pv.name == last]
|
|
105
|
+
if hits:
|
|
106
|
+
pd = _resolve_def(project.defs, hits[0].def_ref)
|
|
107
|
+
res["param"] = {"@path": path, "@def": hits[0].def_ref,
|
|
108
|
+
"@at": [f"{project.rel(hits[0].node and pg[0].file)}:{h.node.line}" for h in hits],
|
|
109
|
+
"value": [to_json(pd, h.value, h.is_ref, h.tag == "ECUC-NUMERICAL-PARAM-VALUE") for h in hits]}
|
|
110
|
+
if pd is not None:
|
|
111
|
+
res["schema"] = D.describe(pd, project.defs)
|
|
112
|
+
return res
|
|
113
|
+
res["error"] = f"no ECUC element or definition at {path}"
|
|
114
|
+
near = [p for p in project.by_path if p.rsplit("/", 1)[-1] == last][:10]
|
|
115
|
+
if near:
|
|
116
|
+
res["didYouMean"] = near
|
|
117
|
+
return res
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _walk(e: Elem):
|
|
121
|
+
stack = [e]
|
|
122
|
+
while stack:
|
|
123
|
+
x = stack.pop()
|
|
124
|
+
yield x
|
|
125
|
+
stack.extend(x.children)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve(project: Project, q: str):
|
|
129
|
+
"""Exact path, def path, or param path pass through. Otherwise resolve a SHORT-NAME or partial
|
|
130
|
+
path ("ComConfig/DoorLatch", "DoorLatch", "DoorLatch/ComBitPosition") against value paths.
|
|
131
|
+
Returns the path, or {"error", "candidates"} when ambiguous / not found."""
|
|
132
|
+
q = q.strip()
|
|
133
|
+
if project.find(q) or project.defs.get(q):
|
|
134
|
+
return q
|
|
135
|
+
parent, _, last = q.rstrip("/").rpartition("/")
|
|
136
|
+
if parent.startswith("/") and project.find(parent):
|
|
137
|
+
return q
|
|
138
|
+
tail = q.strip("/")
|
|
139
|
+
paths = list(project.by_path)
|
|
140
|
+
hits = [p for p in paths if p == "/" + tail or p.endswith("/" + tail)]
|
|
141
|
+
param = None
|
|
142
|
+
if not hits and "/" in tail: # last segment may be a parameter
|
|
143
|
+
head, _, param = tail.rpartition("/")
|
|
144
|
+
hits = [p for p in paths if p.endswith("/" + head)]
|
|
145
|
+
hits = [p for p in hits if any(pv.name == param for e in project.find(p) for pv in e.params)]
|
|
146
|
+
if not hits:
|
|
147
|
+
low = tail.lower().rsplit("/", 1)[-1]
|
|
148
|
+
sugg = sorted(p for p in paths if low in p.rsplit("/", 1)[-1].lower())[:15]
|
|
149
|
+
return {"error": f"no ECUC element matches '{q}'", "didYouMean": sugg} if sugg else \
|
|
150
|
+
{"error": f"no ECUC element or definition at {q}; call ecuc_read with no path to list modules"}
|
|
151
|
+
if len(hits) > 1:
|
|
152
|
+
return {"error": f"'{q}' is ambiguous ({len(hits)} matches); pass one of these full paths",
|
|
153
|
+
"didYouMean": sorted(hits)[:25]}
|
|
154
|
+
return hits[0] + (f"/{param}" if param else "")
|