@h2loop/autosar 1.0.1 → 1.2.0
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/README.md +11 -1
- package/agents/autosar-integrator.md +45 -2
- package/commands/autosar-trace.md +2 -2
- package/package.json +4 -1
- package/plugins/index.js +55 -23
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +351 -0
- package/python/ecuc_engine/arxml_edit.py +387 -0
- package/python/ecuc_engine/audit.py +352 -50
- package/python/ecuc_engine/cli.py +125 -5
- package/python/ecuc_engine/codegen/__init__.py +233 -0
- package/python/ecuc_engine/codegen/binding.py +305 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
- package/python/ecuc_engine/codegen/cdiff.py +395 -0
- package/python/ecuc_engine/codegen/cwriter.py +150 -0
- package/python/ecuc_engine/codegen/emit.py +463 -0
- package/python/ecuc_engine/codegen/handles.py +176 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +264 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/com.py +145 -0
- package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
- package/python/ecuc_engine/codegen/modules/dem.py +22 -0
- package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
- package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
- package/python/ecuc_engine/codegen/registry.py +208 -0
- package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
- package/python/ecuc_engine/context.py +481 -0
- package/python/ecuc_engine/csrc.py +189 -0
- package/python/ecuc_engine/defs.py +30 -0
- package/python/ecuc_engine/ecucjson.py +75 -22
- package/python/ecuc_engine/find.py +164 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +4 -1
- package/python/ecuc_engine/probe.py +229 -0
- package/python/ecuc_engine/rte_api.py +935 -0
- package/python/ecuc_engine/rules/__init__.py +5 -3
- package/python/ecuc_engine/rules/mem.py +84 -0
- package/python/ecuc_engine/rules/nvm.py +131 -0
- package/python/ecuc_engine/rules/pdur.py +87 -0
- package/python/ecuc_engine/rules/system.py +162 -0
- package/python/ecuc_engine/specids.py +163 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/values.py +26 -0
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +78 -6
- package/skills/autosar/references/codegen.md +58 -0
- package/skills/autosar/references/ecuc-model.md +4 -1
- package/skills/autosar/references/project-layout.md +10 -1
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -74,9 +74,11 @@ class RuleContext:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def run_rules(project: Project) -> Iterable[dict]:
|
|
77
|
-
from . import com, handles # noqa: F401 (registers the packs)
|
|
77
|
+
from . import com, handles, mem, nvm, pdur, system # noqa: F401 (registers the packs)
|
|
78
78
|
ctx = RuleContext(project)
|
|
79
79
|
for r in RULES:
|
|
80
|
-
for
|
|
80
|
+
for item in r.fn(ctx):
|
|
81
|
+
e, line, msg, def_ref = item[:4]
|
|
82
|
+
sev = item[4] if len(item) > 4 else r.severity # a rule may downgrade one finding (e.g. NOT CHECKED -> info)
|
|
81
83
|
yield {"file": project.rel(e.file), "line": line, "path": e.path, "rule": r.id,
|
|
82
|
-
"severity":
|
|
84
|
+
"severity": sev, "message": f"{msg} [{r.cite}]", "defRef": def_ref}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""H2L-MEM-001: do the Fee/Ea blocks fit the memory they are configured to use?
|
|
2
|
+
|
|
3
|
+
Followed generically: each Fee/Ea block -> its address-area reference (MemAcc address area) -> sub address
|
|
4
|
+
areas -> sector batch (number of sectors x erase sector size). Per-block cost = blockSize + block overhead,
|
|
5
|
+
rounded up to the virtual page size (FeeBlockOverhead/FeeVirtualPageSize from the Fee config when present),
|
|
6
|
+
times the number of NV instances NvM keeps (NvMNvBlockNum of the referencing NvM block). Fee/Ea need spare
|
|
7
|
+
space for sector switching / garbage collection, so the check compares against half the area (warning).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from . import rule
|
|
12
|
+
from ..values import parse_int
|
|
13
|
+
|
|
14
|
+
CITE = ("ECUC_Fee_00148 FeeBlockSize, FeeBlockOverhead/FeeVirtualPageSize; ECUC_NvM_00479 NvMNvBlockNum; MemAcc "
|
|
15
|
+
"address area / Mem sector batch sizes (SWS_MemAcc, SWS_Mem)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _up(n: int, page: int) -> int:
|
|
19
|
+
return n if page <= 1 else ((n + page - 1) // page) * page
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _first(ctx, rel):
|
|
23
|
+
c = ctx.containers(rel)
|
|
24
|
+
return c[0] if c else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@rule("H2L-MEM-001", "warning", CITE, "Fee/Ea blocks fit their configured memory area")
|
|
28
|
+
def capacity(ctx):
|
|
29
|
+
for mod, blkdef, size_p in (("Fee", "Fee/FeeBlockConfiguration", "FeeBlockSize"),
|
|
30
|
+
("Ea", "Ea/EaBlockConfiguration", "EaBlockSize")):
|
|
31
|
+
blocks = ctx.containers(blkdef)
|
|
32
|
+
if not blocks:
|
|
33
|
+
continue
|
|
34
|
+
gen = _first(ctx, f"{mod}/{mod}General")
|
|
35
|
+
pub = _first(ctx, f"{mod}/{mod}PublishedInformation")
|
|
36
|
+
page = parse_int(ctx.text(gen, f"{mod}VirtualPageSize") or "") if gen is not None else None
|
|
37
|
+
over = parse_int(ctx.text(pub, f"{mod}BlockOverhead") or "") if pub is not None else None
|
|
38
|
+
instances: dict = {}
|
|
39
|
+
for nb in ctx.containers("NvM/NvMBlockDescriptor"):
|
|
40
|
+
stack = [nb]
|
|
41
|
+
n = parse_int(ctx.text(nb, "NvMNvBlockNum") or "") or 1
|
|
42
|
+
while stack:
|
|
43
|
+
e = stack.pop()
|
|
44
|
+
for g in ctx.p.find(e.path):
|
|
45
|
+
stack.extend(g.children)
|
|
46
|
+
for pv in g.params:
|
|
47
|
+
if pv.is_ref and pv.value:
|
|
48
|
+
instances[pv.value.strip()] = max(instances.get(pv.value.strip(), 1), n)
|
|
49
|
+
per_area: dict = {}
|
|
50
|
+
for b in blocks:
|
|
51
|
+
size = parse_int(ctx.text(b, size_p) or "")
|
|
52
|
+
if size is None:
|
|
53
|
+
continue
|
|
54
|
+
area = None
|
|
55
|
+
for g in ctx.p.find(b.path):
|
|
56
|
+
for pv in g.params:
|
|
57
|
+
t = ctx.target(pv) if pv.is_ref else None
|
|
58
|
+
if t is not None and "AddressArea" in (t.def_ref or ""):
|
|
59
|
+
area = t
|
|
60
|
+
cost = _up(size + (over or 0), page or 1) * instances.get(b.path, 1)
|
|
61
|
+
key = area.path if area is not None else None
|
|
62
|
+
per_area.setdefault(key, [0, 0, area])
|
|
63
|
+
per_area[key][0] += cost
|
|
64
|
+
per_area[key][1] += 1
|
|
65
|
+
for key, (need, count, area) in per_area.items():
|
|
66
|
+
if area is None:
|
|
67
|
+
continue
|
|
68
|
+
cap, parts = 0, []
|
|
69
|
+
for sub in ctx.p.find(area.path)[0].children:
|
|
70
|
+
ns = parse_int(ctx.text(sub, f"MemAccNumberOfSectors") or "")
|
|
71
|
+
batch = ctx.target(ctx.value(sub, "MemAccSectorBatchRef"))
|
|
72
|
+
ss = parse_int(ctx.text(batch, "MemEraseSectorSize") or "") if batch is not None else None
|
|
73
|
+
if ns and ss:
|
|
74
|
+
cap += ns * ss
|
|
75
|
+
parts.append(f"{sub.name}: {ns} x {ss} B")
|
|
76
|
+
if not cap:
|
|
77
|
+
continue
|
|
78
|
+
usable = cap // 2
|
|
79
|
+
if need > usable:
|
|
80
|
+
anchor = ctx.value(blocks[0], size_p)
|
|
81
|
+
yield blocks[0], anchor.node.line if anchor else blocks[0].line, (
|
|
82
|
+
f"{count} {mod} block(s) on {area.name} need {need} B (size + overhead {over or 0} B, rounded to "
|
|
83
|
+
f"{page or 1} B pages, x NV instances) but the area holds {cap} B ({', '.join(parts)}); with space "
|
|
84
|
+
f"for sector switching only ~{usable} B are usable"), anchor.def_ref if anchor else None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""NvM block length vs the C object behind its permanent RAM block, and the Fee/Ea block that stores it.
|
|
2
|
+
|
|
3
|
+
NvMRamBlockDataAddress names a C object; its size comes from a compile-only sizeof probe built with the
|
|
4
|
+
project's own compiler/flags (ecuc_engine/probe.py). NvMNvBlockLength is the NV *data* length
|
|
5
|
+
(ECUC_NvM_00479: "NV block data length in bytes"; CRC is managed by NvM on top). The Fee/Ea block is found by
|
|
6
|
+
following the block's references to a FeeBlockConfiguration / EaBlockConfiguration.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from . import rule
|
|
14
|
+
from .. import paths, probe
|
|
15
|
+
from ..values import parse_int
|
|
16
|
+
|
|
17
|
+
CITE = ("ECUC_NvM_00479 NvMNvBlockLength (NV block data length), ECUC_NvM_00482 NvMRamBlockDataAddress, "
|
|
18
|
+
"ECUC_Fee_00148 FeeBlockSize / ECUC_Ea_00128 EaBlockSize")
|
|
19
|
+
SYMBOL = re.compile(r"^[&(\s]*(?:\(\s*\w+\s*\*?\s*\)\s*)?&?\s*([A-Za-z_]\w*)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _storage_blocks(ctx, blk):
|
|
23
|
+
"""Fee/Ea block configurations referenced (at any depth) from an NvM block descriptor."""
|
|
24
|
+
out = []
|
|
25
|
+
stack = [blk]
|
|
26
|
+
while stack:
|
|
27
|
+
e = stack.pop()
|
|
28
|
+
for g in ctx.p.find(e.path):
|
|
29
|
+
stack.extend(g.children)
|
|
30
|
+
for pv in g.params:
|
|
31
|
+
if pv.is_ref and pv.value:
|
|
32
|
+
t = ctx.target(pv)
|
|
33
|
+
if t is not None and (t.def_ref or "").endswith(("/FeeBlockConfiguration", "/EaBlockConfiguration")):
|
|
34
|
+
out.append(t)
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def blocks_with_ram(ctx):
|
|
39
|
+
for blk in ctx.containers("NvM/NvMBlockDescriptor"):
|
|
40
|
+
addr = (ctx.text(blk, "NvMRamBlockDataAddress") or "").strip()
|
|
41
|
+
m = SYMBOL.match(addr)
|
|
42
|
+
if m:
|
|
43
|
+
yield blk, addr, m.group(1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def probe_sizes(ctx):
|
|
47
|
+
"""{block path: (size|None, Decl|None)} and notes; cached on the context."""
|
|
48
|
+
if getattr(ctx, "_nvm_sizes", None) is not None:
|
|
49
|
+
return ctx._nvm_sizes
|
|
50
|
+
root = ctx.p.root
|
|
51
|
+
tree = paths.walk(root, paths.is_c).files
|
|
52
|
+
from ..audit import is_generated
|
|
53
|
+
own = [f for f in tree if not is_generated(f)]
|
|
54
|
+
headers = [f for f in own if f.lower().endswith(".h")] + [f for f in tree if f.lower().endswith(".h") and f not in own]
|
|
55
|
+
cfiles = [f for f in own if f.lower().endswith(".c")]
|
|
56
|
+
decls, where = {}, {}
|
|
57
|
+
for blk, _addr, sym in blocks_with_ram(ctx):
|
|
58
|
+
d = probe.find_decl(sym, cfiles, headers)
|
|
59
|
+
where[blk.path] = d
|
|
60
|
+
if d is not None:
|
|
61
|
+
if not d.header:
|
|
62
|
+
d.header = probe.type_header(d.type, headers)
|
|
63
|
+
if d.header is None and d.type not in ("uint8", "uint16", "uint32", "sint8", "sint16", "sint32", "boolean",
|
|
64
|
+
"char", "int", "short", "long", "float", "double"):
|
|
65
|
+
continue
|
|
66
|
+
decls[blk.path] = d
|
|
67
|
+
ctx._nvm_probed = set(decls)
|
|
68
|
+
found, notes, tc = probe.sizes(root, probe.load_cfg(root), decls) if decls else ({}, [], None)
|
|
69
|
+
ctx._nvm_sizes = ({k: (found.get(k), where.get(k)) for k in where}, notes, tc)
|
|
70
|
+
return ctx._nvm_sizes
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@rule("H2L-NVM-001", "error", CITE, "NvM block length / Fee-Ea block size match the RAM block's C object size")
|
|
74
|
+
def nvm_length_vs_c(ctx):
|
|
75
|
+
"""Every NvM block descriptor gets an outcome: a size verdict (error on mismatch), or a NOT CHECKED info that
|
|
76
|
+
says why. A summary info is always emitted, so the rule never yields nothing when NvM is configured."""
|
|
77
|
+
blocks = ctx.containers("NvM/NvMBlockDescriptor")
|
|
78
|
+
if not blocks:
|
|
79
|
+
return
|
|
80
|
+
first = blocks[0]
|
|
81
|
+
with_ram = list(blocks_with_ram(ctx))
|
|
82
|
+
if not with_ram:
|
|
83
|
+
yield first, first.line, (f"NOT CHECKED: none of the {len(blocks)} NvM block(s) has an NvMRamBlockDataAddress "
|
|
84
|
+
"naming a C object"), None, "info"
|
|
85
|
+
return
|
|
86
|
+
table, notes, tc = probe_sizes(ctx)
|
|
87
|
+
for n in notes:
|
|
88
|
+
yield first, first.line, f"NOT CHECKED: {n}", None, "info"
|
|
89
|
+
checked = mism = 0
|
|
90
|
+
unchecked = []
|
|
91
|
+
for blk, addr, sym in with_ram:
|
|
92
|
+
size, decl = table.get(blk.path, (None, None))
|
|
93
|
+
if size is None:
|
|
94
|
+
why = ("not declared in any C source" if decl is None else
|
|
95
|
+
f"{decl.type} has no header defining it" if blk.path not in getattr(ctx, "_nvm_probed", {blk.path})
|
|
96
|
+
else "compiler did not report its size")
|
|
97
|
+
unchecked.append(blk.name)
|
|
98
|
+
yield blk, blk.line, f"NOT CHECKED: {blk.name}: sizeof({sym}) undetermined ({why})", None, "info"
|
|
99
|
+
continue
|
|
100
|
+
checked += 1
|
|
101
|
+
where = f"{os.path.relpath(decl.file, ctx.p.root)}:{decl.line}" if decl else "?"
|
|
102
|
+
ln = parse_int(ctx.text(blk, "NvMNvBlockLength") or "")
|
|
103
|
+
pv = ctx.value(blk, "NvMNvBlockLength")
|
|
104
|
+
crc = (ctx.text(blk, "NvMBlockUseCrc") or "").lower() == "true"
|
|
105
|
+
tail = f" (RAM block {addr}: {decl.type if decl else '?'} at {where}, sizeof = {size} via {tc.origin if tc else '?'}" \
|
|
106
|
+
f"{'; CRC ' + (ctx.text(blk, 'NvMBlockCrcType') or '') + ' is added by NvM on top' if crc else ''})"
|
|
107
|
+
if ln is not None and ln != size:
|
|
108
|
+
mism += 1
|
|
109
|
+
yield blk, pv.node.line, f"{blk.name}: NvMNvBlockLength={ln} but sizeof({sym}) = {size}" + tail, pv.def_ref
|
|
110
|
+
for st in _storage_blocks(ctx, blk):
|
|
111
|
+
sname = "FeeBlockSize" if (st.def_ref or "").endswith("/FeeBlockConfiguration") else "EaBlockSize"
|
|
112
|
+
bs = parse_int(ctx.text(st, sname) or "")
|
|
113
|
+
spv = ctx.value(st, sname)
|
|
114
|
+
if bs is not None and bs < size:
|
|
115
|
+
mism += 1
|
|
116
|
+
yield st, spv.node.line, (f"{st.name}: {sname}={bs} cannot hold the {size}-byte RAM block of {blk.name}"
|
|
117
|
+
+ tail), spv.def_ref
|
|
118
|
+
yield first, first.line, (f"sizeof check: {checked} of {len(with_ram)} RAM block(s) sized via "
|
|
119
|
+
f"{tc.origin if tc else 'no probe'}, {mism} mismatch(es), {len(unchecked)} NOT CHECKED"), None, "info"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@rule("H2L-NVM-000", "info", "NvM RAM block coverage", "RAM blocks whose C object could not be found")
|
|
123
|
+
def nvm_unresolved(ctx):
|
|
124
|
+
if not ctx.containers("NvM/NvMBlockDescriptor"):
|
|
125
|
+
return
|
|
126
|
+
table, _notes, _tc = probe_sizes(ctx)
|
|
127
|
+
miss = [p for p, (s, d) in table.items() if d is None]
|
|
128
|
+
if miss:
|
|
129
|
+
first = ctx.containers("NvM/NvMBlockDescriptor")[0]
|
|
130
|
+
yield first, first.line, (f"{len(miss)} NvM RAM block symbol(s) not declared in any C source "
|
|
131
|
+
f"(e.g. {', '.join(miss[:3])}); H2L-NVM-001 skipped for them"), None
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""PduR routing vs the System / ECU extract.
|
|
2
|
+
|
|
3
|
+
A routing path forwards one I-PDU: its source and destination EcuC Pdus must stand for the same system
|
|
4
|
+
I-PDU (the same IPdu triggered on two channels, or an I-PDU with the same I-SIGNAL content), or for a
|
|
5
|
+
source/target pair of an extract GATEWAY I-PDU-MAPPING when the extract has one.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from . import rule
|
|
10
|
+
from .system import channels, foreign_targets, system_ipdus
|
|
11
|
+
|
|
12
|
+
CITE = ("SWS_PduR routing path (PduRSrcPdu/PduRDestPdu); System Template Gateway.iPduMappings / IPdu triggered on "
|
|
13
|
+
"source and target channels")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _pdu(ctx, container, ref_name):
|
|
17
|
+
return ctx.target(ctx.value(container, ref_name)) if container is not None else None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _gateway_map(ctx) -> dict:
|
|
21
|
+
"""source PDU-TRIGGERING -> {target PDU-TRIGGERING} from extract I-PDU-MAPPINGs (if any)."""
|
|
22
|
+
out: dict = {}
|
|
23
|
+
for m in ctx.p.system.tags("I-PDU-MAPPING"):
|
|
24
|
+
src = m.values.get("SOURCE-I-PDU-REF", [])
|
|
25
|
+
tgt = m.values.get("TARGET-I-PDU-REF", [])
|
|
26
|
+
for s in src:
|
|
27
|
+
out.setdefault(s, set()).update(tgt)
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _paths(ctx):
|
|
32
|
+
for rp in ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
|
|
33
|
+
src = ctx.target(ctx.value(rp, "PduRSrcPduRRef"))
|
|
34
|
+
dests = [ctx.target(pv) for pv in ctx.values(rp, "PduRDestPduRRef")]
|
|
35
|
+
yield rp, src, [d for d in dests if d is not None]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@rule("H2L-PDUR-001", "error", CITE, "a routing path's destination carries the same I-PDU as its source")
|
|
39
|
+
def route_vs_extract(ctx):
|
|
40
|
+
if not ctx.p.system.by_path:
|
|
41
|
+
return
|
|
42
|
+
gw = _gateway_map(ctx)
|
|
43
|
+
sysi = ctx.p.system
|
|
44
|
+
for rp, src, dests in _paths(ctx):
|
|
45
|
+
spdu = _pdu(ctx, src, "PduRSrcPduRef")
|
|
46
|
+
s_ip = system_ipdus(ctx, spdu)
|
|
47
|
+
if not s_ip:
|
|
48
|
+
continue
|
|
49
|
+
s_sig = {x for ip in s_ip for x in sysi.signals_of_ipdu(ip)}
|
|
50
|
+
for d in dests:
|
|
51
|
+
dpdu = _pdu(ctx, d, "PduRDestPduRef")
|
|
52
|
+
d_ip = system_ipdus(ctx, dpdu)
|
|
53
|
+
if not d_ip or set(d_ip) & set(s_ip):
|
|
54
|
+
continue
|
|
55
|
+
d_sig = {x for ip in d_ip for x in sysi.signals_of_ipdu(ip)}
|
|
56
|
+
if s_sig and s_sig == d_sig:
|
|
57
|
+
continue
|
|
58
|
+
src_trig = [t.path for _pv, t in foreign_targets(ctx, spdu)]
|
|
59
|
+
if any(t in gw for t in src_trig):
|
|
60
|
+
allowed = set().union(*(gw[t] for t in src_trig if t in gw))
|
|
61
|
+
dst_trig = {t.path for _pv, t in foreign_targets(ctx, dpdu)}
|
|
62
|
+
if allowed & dst_trig:
|
|
63
|
+
continue
|
|
64
|
+
pv = ctx.value(rp, "PduRDestPduRRef")
|
|
65
|
+
others = [o.name for o, s2, ds in _paths(ctx) if o.path != rp.path and any(x.path == d.path for x in ds)]
|
|
66
|
+
yield rp, pv.node.line, (
|
|
67
|
+
f"{rp.name}: source {spdu.name if spdu else src.name} carries system I-PDU {', '.join(s_ip)} "
|
|
68
|
+
f"(channel {', '.join(channels(ctx, spdu)) or '?'}) but destination {d.name} -> "
|
|
69
|
+
f"{dpdu.name if dpdu else '?'} carries {', '.join(d_ip)} (channel {', '.join(channels(ctx, dpdu)) or '?'}), "
|
|
70
|
+
f"with different I-SIGNALs ({len(s_sig)} vs {len(d_sig)}, {len(s_sig & d_sig)} shared)"
|
|
71
|
+
+ (f"; that destination is also the target of {', '.join(others)}" if others else "")), pv.def_ref
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@rule("H2L-PDUR-002", "warning", "SWS_PduR routing path (one source per destination PDU)",
|
|
75
|
+
"a destination PDU is fed by more than one routing path")
|
|
76
|
+
def fan_in(ctx):
|
|
77
|
+
feeds: dict = {}
|
|
78
|
+
for rp, src, dests in _paths(ctx):
|
|
79
|
+
for d in dests:
|
|
80
|
+
feeds.setdefault(d.path, []).append((rp, src))
|
|
81
|
+
for dpath, rps in feeds.items():
|
|
82
|
+
srcs = {s.path for _r, s in rps if s is not None}
|
|
83
|
+
if len(srcs) > 1:
|
|
84
|
+
for rp, _s in rps:
|
|
85
|
+
pv = ctx.value(rp, "PduRDestPduRRef")
|
|
86
|
+
yield rp, pv.node.line, (f"{rp.name}: destination {dpath} is also fed by "
|
|
87
|
+
f"{', '.join(r.name for r, _ in rps if r.path != rp.path)} (different sources)"), pv.def_ref
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""ECU configuration vs the System / ECU extract (the ECUC must agree with the extract it was derived from).
|
|
2
|
+
|
|
3
|
+
Links are discovered from the configuration's own foreign references and the extract's metaclasses:
|
|
4
|
+
ComSignal --(any foreign ref: I-SIGNAL-TO-I-PDU-MAPPING | I-SIGNAL | SYSTEM-SIGNAL)--> mapping;
|
|
5
|
+
EcuC Pdu --(any foreign ref: PDU-TRIGGERING | *FRAME-TRIGGERING | *I-PDU)--> system I-PDU.
|
|
6
|
+
No element names are assumed.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from . import rule
|
|
13
|
+
from ..values import parse_int
|
|
14
|
+
|
|
15
|
+
ENDIAN = {"MOST-SIGNIFICANT-BYTE-LAST": "LITTLE_ENDIAN", "MOST-SIGNIFICANT-BYTE-FIRST": "BIG_ENDIAN", "OPAQUE": "OPAQUE"}
|
|
16
|
+
CITE_SIG = ("System Template ISignalToIPduMapping.startPosition/packingByteOrder, ISignal.length vs "
|
|
17
|
+
"ECUC_Com_00259 ComBitPosition, ECUC_Com_00158 ComBitSize, ECUC_Com_00157 ComSignalEndianness")
|
|
18
|
+
CITE_PDU = "System Template IPdu.length vs ECUC_EcuC_00003 PduLength"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def foreign_targets(ctx, e):
|
|
22
|
+
"""(param value, system Ident) for every reference of an ECUC element that lands in the system index."""
|
|
23
|
+
out = []
|
|
24
|
+
for g in ctx.p.find(e.path):
|
|
25
|
+
for pv in g.params:
|
|
26
|
+
if pv.is_ref and pv.value:
|
|
27
|
+
t = ctx.p.system.get(pv.value)
|
|
28
|
+
if t is not None:
|
|
29
|
+
out.append((pv, t))
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def system_ipdus(ctx, pdu) -> list:
|
|
34
|
+
"""EcuC Pdu container -> system I-PDU paths it stands for."""
|
|
35
|
+
if pdu is None:
|
|
36
|
+
return []
|
|
37
|
+
out = []
|
|
38
|
+
for _pv, t in foreign_targets(ctx, pdu):
|
|
39
|
+
if t.tag.endswith("TRIGGERING"):
|
|
40
|
+
out.extend(ctx.p.system.ipdus_of_triggering(t.path))
|
|
41
|
+
elif t.tag.endswith("PDU"):
|
|
42
|
+
out.append(t.path)
|
|
43
|
+
return sorted(set(out))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def channels(ctx, pdu) -> list:
|
|
47
|
+
if pdu is None:
|
|
48
|
+
return []
|
|
49
|
+
return sorted({ctx.p.system.channel_of(t.path) for _pv, t in foreign_targets(ctx, pdu)
|
|
50
|
+
if t.tag.endswith("TRIGGERING") and ctx.p.system.channel_of(t.path)})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def owning_ipdus(ctx):
|
|
54
|
+
"""ComSignal path -> list of ComIPdu Elems that reference it."""
|
|
55
|
+
own: dict = {}
|
|
56
|
+
for ip in ctx.containers("Com/ComConfig/ComIPdu"):
|
|
57
|
+
for pv in ctx.values(ip, "ComIPduSignalRef"):
|
|
58
|
+
if pv.value:
|
|
59
|
+
own.setdefault(pv.value.strip(), []).append(ip)
|
|
60
|
+
return own
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def mappings_for(ctx, sig, sys_ipdus: list) -> list:
|
|
64
|
+
sysi = ctx.p.system
|
|
65
|
+
out = []
|
|
66
|
+
for _pv, t in foreign_targets(ctx, sig):
|
|
67
|
+
if t.tag == "I-SIGNAL-TO-I-PDU-MAPPING":
|
|
68
|
+
out.append(t)
|
|
69
|
+
elif t.tag in ("I-SIGNAL", "SYSTEM-SIGNAL"):
|
|
70
|
+
isigs = [t.path] if t.tag == "I-SIGNAL" else [i.path for i in sysi.refs_to(t.path) if i.tag == "I-SIGNAL"]
|
|
71
|
+
for isig in isigs:
|
|
72
|
+
for m in sysi.refs_to(isig):
|
|
73
|
+
if m.tag == "I-SIGNAL-TO-I-PDU-MAPPING" and (not sys_ipdus or m.parent in sys_ipdus):
|
|
74
|
+
out.append(m)
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _motorola_msb(lsb: int, size: int) -> int:
|
|
79
|
+
b = lsb
|
|
80
|
+
for _ in range(size - 1):
|
|
81
|
+
b = b + 1 if b % 8 != 7 else b - 15
|
|
82
|
+
return b
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _line(m) -> str:
|
|
86
|
+
return f"{m.file.rsplit('/', 1)[-1]}:{m.line}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@rule("H2L-SYS-001", "error", CITE_SIG, "ComSignal layout and I-PDU agree with the system extract mapping")
|
|
90
|
+
def signal_vs_extract(ctx):
|
|
91
|
+
if not ctx.p.system.by_path:
|
|
92
|
+
return
|
|
93
|
+
own = owning_ipdus(ctx)
|
|
94
|
+
for sig in ctx.containers("Com/ComConfig/ComSignal") + ctx.containers("Com/ComConfig/ComSignalGroup/ComGroupSignal"):
|
|
95
|
+
ipdus = own.get(sig.path, [])
|
|
96
|
+
sys_ipdus = sorted({x for ip in ipdus for x in system_ipdus(ctx, ctx.target(ctx.value(ip, "ComPduIdRef")))})
|
|
97
|
+
maps = mappings_for(ctx, sig, sys_ipdus)
|
|
98
|
+
if not maps:
|
|
99
|
+
continue # no link: counted by the info rule below
|
|
100
|
+
m = maps[0]
|
|
101
|
+
isig = ctx.p.system.get(m.one("I-SIGNAL-REF"))
|
|
102
|
+
pos, size = parse_int(ctx.text(sig, "ComBitPosition") or ""), parse_int(ctx.text(sig, "ComBitSize") or "")
|
|
103
|
+
if size is None:
|
|
104
|
+
n = parse_int(ctx.text(sig, "ComSignalLength") or "")
|
|
105
|
+
size = n * 8 if n is not None else None
|
|
106
|
+
start = parse_int(m.one("START-POSITION") or "")
|
|
107
|
+
length = parse_int(isig.one("LENGTH") or "") if isig is not None else None
|
|
108
|
+
order = m.one("PACKING-BYTE-ORDER")
|
|
109
|
+
endian = (ctx.text(sig, "ComSignalEndianness") or "").strip()
|
|
110
|
+
anchor = ctx.value(sig, "ComBitPosition") or ctx.value(sig, "ComBitSize")
|
|
111
|
+
line = anchor.node.line if anchor is not None else sig.line
|
|
112
|
+
ev = f"extract {m.path} ({_line(m)}): START-POSITION={start}, PACKING-BYTE-ORDER={order}, " \
|
|
113
|
+
f"I-SIGNAL LENGTH={length}"
|
|
114
|
+
if pos is not None and start is not None and size:
|
|
115
|
+
ok = pos == start
|
|
116
|
+
if order == "MOST-SIGNIFICANT-BYTE-FIRST":
|
|
117
|
+
ok = ok or start == _motorola_msb(pos, size) or pos == _motorola_msb(start, size)
|
|
118
|
+
if not ok:
|
|
119
|
+
yield sig, line, f"{sig.name}: ComBitPosition={pos} but the system extract maps it at START-POSITION={start}; {ev}", \
|
|
120
|
+
ctx.value(sig, "ComBitPosition").def_ref
|
|
121
|
+
if size is not None and length is not None and size != length:
|
|
122
|
+
yield sig, line, f"{sig.name}: ComBitSize={size} but I-SIGNAL LENGTH={length} bits; {ev}", \
|
|
123
|
+
(ctx.value(sig, "ComBitSize") or ctx.value(sig, "ComSignalLength")).def_ref
|
|
124
|
+
if order in ENDIAN and endian and ENDIAN[order] != endian:
|
|
125
|
+
yield sig, line, f"{sig.name}: ComSignalEndianness={endian} but PACKING-BYTE-ORDER={order} ({ENDIAN[order]}); {ev}", \
|
|
126
|
+
ctx.value(sig, "ComSignalEndianness").def_ref
|
|
127
|
+
if sys_ipdus and m.parent not in sys_ipdus:
|
|
128
|
+
yield sig, sig.line, (f"{sig.name} is in ComIPdu {', '.join(i.name for i in ipdus)} (system I-PDU "
|
|
129
|
+
f"{', '.join(sys_ipdus)}) but the extract maps its I-SIGNAL into {m.parent} ({_line(m)})"), sig.def_ref
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@rule("H2L-SYS-002", "error", CITE_PDU, "EcuC PduLength agrees with the system I-PDU LENGTH")
|
|
133
|
+
def pdu_length_vs_extract(ctx):
|
|
134
|
+
if not ctx.p.system.by_path:
|
|
135
|
+
return
|
|
136
|
+
for pdu in ctx.containers("EcuC/EcucConfigSet/EcucPduCollection/Pdu"):
|
|
137
|
+
length = parse_int(ctx.text(pdu, "PduLength") or "")
|
|
138
|
+
if length is None:
|
|
139
|
+
continue
|
|
140
|
+
for ip in system_ipdus(ctx, pdu):
|
|
141
|
+
si = ctx.p.system.get(ip)
|
|
142
|
+
sl = parse_int(si.one("LENGTH") or "") if si is not None else None
|
|
143
|
+
if sl is not None and sl != length:
|
|
144
|
+
pv = ctx.value(pdu, "PduLength")
|
|
145
|
+
yield pdu, pv.node.line, (f"{pdu.name}: PduLength={length} but system I-PDU {ip} has LENGTH={sl} "
|
|
146
|
+
f"({_line(si)})"), pv.def_ref
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@rule("H2L-SYS-000", "info", "ECU extract link coverage", "how much of the config could be linked to the extract")
|
|
150
|
+
def coverage(ctx):
|
|
151
|
+
first: Optional[object] = next((m for m in ctx.p.modules if (m.def_ref or "").endswith("/Com")), None)
|
|
152
|
+
if first is None:
|
|
153
|
+
return
|
|
154
|
+
if not ctx.p.system.by_path:
|
|
155
|
+
yield first, first.line, ("no System/ECU extract loaded (no non-ECUC .arxml found or configured as 'input'); "
|
|
156
|
+
"H2L-SYS/H2L-PDUR cross-checks were skipped"), None
|
|
157
|
+
return
|
|
158
|
+
sigs = ctx.containers("Com/ComConfig/ComSignal")
|
|
159
|
+
unlinked = [s for s in sigs if not mappings_for(ctx, s, [])]
|
|
160
|
+
if unlinked:
|
|
161
|
+
yield first, first.line, (f"{len(unlinked)} of {len(sigs)} ComSignal(s) have no reference into the loaded "
|
|
162
|
+
f"extract (e.g. {', '.join(s.path for s in unlinked[:3])}); H2L-SYS-001 skipped for them"), None
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Requirement-ID check (autosar_spec_id_check) against the user's own AUTOSAR spec texts.
|
|
2
|
+
|
|
3
|
+
No requirement IDs or spec text are bundled with the pack: AUTOSAR documents are licensed to the user, so the
|
|
4
|
+
index is built at run time from text exports the project points at (`.hydron/autosar.json` "specDocs" globs, or
|
|
5
|
+
$AUTOSAR_SPEC_DOCS, an os.pathsep-separated list of files/dirs/globs; .txt and .md files). The index is cached
|
|
6
|
+
per file set (size + mtime) under $XDG_CACHE_HOME/h2loop-autosar.
|
|
7
|
+
|
|
8
|
+
An ID is "defined" where a line starts with "[ID]" alone (optionally after a label such as "Signature:", as the
|
|
9
|
+
RTE spec writes API requirements), followed within a few lines by the requirement
|
|
10
|
+
bracket "⌈" or "Upstream requirements" (how SWS/TPS/SRS/ECUC requirements are introduced), and "mentioned" when
|
|
11
|
+
it only occurs in running text or trace tables.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from . import paths
|
|
22
|
+
|
|
23
|
+
ID = re.compile(r"\b([A-Z][A-Za-z]{1,7}_[A-Za-z0-9]+_\d{3,6})\b")
|
|
24
|
+
# "SWS_NvM_00898/00899" -> both IDs
|
|
25
|
+
SHORTHAND = re.compile(r"\b([A-Z][A-Za-z]{1,7}_[A-Za-z0-9]+_)(\d{3,6})((?:/\d{3,6})+)")
|
|
26
|
+
DEF_LINE = re.compile(r"^\s*(?:[A-Z][A-Za-z ]{0,30}:\s*)?\[([A-Z][A-Za-z]{1,7}_[A-Za-z0-9]+_\d{3,6})\]")
|
|
27
|
+
INDEX_VERSION = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def expand_ids(text: str) -> list:
|
|
31
|
+
"""Every requirement ID written in text, with "X_Mod_00001/00002" shorthand expanded, in order, unique."""
|
|
32
|
+
out: list = []
|
|
33
|
+
for m in SHORTHAND.finditer(text):
|
|
34
|
+
out.append(m.group(1) + m.group(2))
|
|
35
|
+
out.extend(m.group(1) + n for n in m.group(3).strip("/").split("/"))
|
|
36
|
+
out.extend(m.group(1) for m in ID.finditer(text))
|
|
37
|
+
seen: set = set()
|
|
38
|
+
return [x for x in out if not (x in seen or seen.add(x))]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_doc(f: str) -> bool:
|
|
42
|
+
return f.lower().endswith((".txt", ".md"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def doc_files(root: str, docs: Optional[list] = None) -> tuple:
|
|
46
|
+
"""(files, origin) of the spec text docs to index."""
|
|
47
|
+
pats, origin = list(docs or []), "argument"
|
|
48
|
+
if not pats:
|
|
49
|
+
cfg = os.path.join(root, ".hydron", "autosar.json")
|
|
50
|
+
try:
|
|
51
|
+
with open(cfg) as fh:
|
|
52
|
+
spec = json.load(fh).get("specDocs")
|
|
53
|
+
if spec:
|
|
54
|
+
pats, origin = ([spec] if isinstance(spec, str) else list(spec)), ".hydron/autosar.json specDocs"
|
|
55
|
+
except (OSError, ValueError, AttributeError):
|
|
56
|
+
pass
|
|
57
|
+
if not pats and os.environ.get("AUTOSAR_SPEC_DOCS"):
|
|
58
|
+
pats, origin = os.environ["AUTOSAR_SPEC_DOCS"].split(os.pathsep), "AUTOSAR_SPEC_DOCS"
|
|
59
|
+
files: list = []
|
|
60
|
+
for p in paths.patterns(root, [x for x in pats if x]):
|
|
61
|
+
files.extend(f for f in p.expand(_is_doc) if _is_doc(f) and os.path.isfile(f))
|
|
62
|
+
return sorted({os.path.abspath(f) for f in files}), origin if pats else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _cache_path(files: list) -> str:
|
|
66
|
+
h = hashlib.sha1(str(INDEX_VERSION).encode())
|
|
67
|
+
for f in files:
|
|
68
|
+
st = os.stat(f)
|
|
69
|
+
h.update(f"{f}\0{st.st_size}\0{int(st.st_mtime)}\n".encode())
|
|
70
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
|
|
71
|
+
return os.path.join(base, "h2loop-autosar", f"specids-{h.hexdigest()[:16]}.json")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_index(files: list) -> dict:
|
|
75
|
+
"""ID -> [file index, line, defined] (the defining line when there is one, else the first mention)."""
|
|
76
|
+
idx: dict = {}
|
|
77
|
+
for fi, f in enumerate(files):
|
|
78
|
+
with open(f, encoding="utf-8", errors="replace") as fh:
|
|
79
|
+
lines = fh.readlines()
|
|
80
|
+
for i, line in enumerate(lines):
|
|
81
|
+
d = DEF_LINE.match(line)
|
|
82
|
+
if d and len(ID.findall(line)) == 1 and any(
|
|
83
|
+
"⌈" in x or "Upstream requirements" in x for x in lines[i: i + 5]):
|
|
84
|
+
cur = idx.get(d.group(1))
|
|
85
|
+
if cur is None or not cur[2]:
|
|
86
|
+
idx[d.group(1)] = [fi, i + 1, True]
|
|
87
|
+
for m in ID.finditer(line):
|
|
88
|
+
idx.setdefault(m.group(1), [fi, i + 1, False])
|
|
89
|
+
return idx
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_index(files: list) -> dict:
|
|
93
|
+
cache = _cache_path(files)
|
|
94
|
+
try:
|
|
95
|
+
with open(cache) as fh:
|
|
96
|
+
data = json.load(fh)
|
|
97
|
+
if data.get("files") == files:
|
|
98
|
+
return data["ids"]
|
|
99
|
+
except (OSError, ValueError):
|
|
100
|
+
pass
|
|
101
|
+
ids = build_index(files)
|
|
102
|
+
try:
|
|
103
|
+
os.makedirs(os.path.dirname(cache), exist_ok=True)
|
|
104
|
+
tmp = cache + ".tmp"
|
|
105
|
+
with open(tmp, "w") as fh:
|
|
106
|
+
json.dump({"files": files, "ids": ids}, fh)
|
|
107
|
+
os.replace(tmp, cache)
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
110
|
+
return ids
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _snippet(f: str, line: int, max_lines: int = 14, max_chars: int = 700) -> str:
|
|
114
|
+
out: list = []
|
|
115
|
+
with open(f, encoding="utf-8", errors="replace") as fh:
|
|
116
|
+
for ln, text in enumerate(fh, 1):
|
|
117
|
+
if ln < line:
|
|
118
|
+
continue
|
|
119
|
+
out.append(text.strip())
|
|
120
|
+
if "⌋" in text or len(out) >= max_lines:
|
|
121
|
+
break
|
|
122
|
+
s = re.sub(r"\s+", " ", " ".join(out)).strip()
|
|
123
|
+
return s if len(s) <= max_chars else s[: max_chars - 1] + "…"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def check_ids(root: str, ids: list, docs: Optional[list] = None) -> dict:
|
|
127
|
+
files, origin = doc_files(root, docs)
|
|
128
|
+
wanted: list = []
|
|
129
|
+
for raw in ids:
|
|
130
|
+
got = expand_ids(str(raw))
|
|
131
|
+
wanted.extend(got or [str(raw).strip().strip("[]")])
|
|
132
|
+
if not files:
|
|
133
|
+
return {"error": "no AUTOSAR spec text docs configured, so no requirement ID can be confirmed. Set "
|
|
134
|
+
"`specDocs` (globs of .txt/.md spec exports) in .hydron/autosar.json or $AUTOSAR_SPEC_DOCS. "
|
|
135
|
+
"Until then cite only IDs that appear verbatim in a tool result or a spec text you read; "
|
|
136
|
+
"otherwise describe the rule without an ID.",
|
|
137
|
+
"ids": wanted}
|
|
138
|
+
idx = load_index(files)
|
|
139
|
+
lower: dict = {}
|
|
140
|
+
for k in idx:
|
|
141
|
+
lower.setdefault(k.lower(), k)
|
|
142
|
+
results = []
|
|
143
|
+
for i in wanted:
|
|
144
|
+
hit = idx.get(i)
|
|
145
|
+
if hit is None:
|
|
146
|
+
alt = lower.get(i.lower())
|
|
147
|
+
r = {"id": i, "status": "NOT-FOUND"}
|
|
148
|
+
if not ID.fullmatch(i):
|
|
149
|
+
r["status"] = "MALFORMED"
|
|
150
|
+
if alt:
|
|
151
|
+
r["didYouMean"] = alt
|
|
152
|
+
results.append(r)
|
|
153
|
+
continue
|
|
154
|
+
f = files[hit[0]]
|
|
155
|
+
r = {"id": i, "status": "DEFINED" if hit[2] else "MENTIONED-ONLY",
|
|
156
|
+
"doc": os.path.basename(f), "line": hit[1]}
|
|
157
|
+
if hit[2]:
|
|
158
|
+
r["text"] = _snippet(f, hit[1])
|
|
159
|
+
results.append(r)
|
|
160
|
+
counts: dict = {}
|
|
161
|
+
for r in results:
|
|
162
|
+
counts[r["status"]] = counts.get(r["status"], 0) + 1
|
|
163
|
+
return {"docs": len(files), "docsOrigin": origin, "indexedIds": len(idx), "counts": counts, "results": results}
|