wireforge 0.1.0__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.
- wireforge/__init__.py +3 -0
- wireforge/_data/cmake/WireForgeConfig.cmake +29 -0
- wireforge/_data/cmake/WireForgeConfigVersion.cmake +15 -0
- wireforge/_data/cmake/WireForgeGenerate.cmake +64 -0
- wireforge/_data/runtime/LICENSE +21 -0
- wireforge/_data/runtime/include/wireforge/array_access.hpp +35 -0
- wireforge/_data/runtime/include/wireforge/bit_utils.hpp +226 -0
- wireforge/_data/runtime/include/wireforge/byte_io.hpp +387 -0
- wireforge/_data/runtime/include/wireforge/checksum.hpp +201 -0
- wireforge/_data/runtime/include/wireforge/codec_support.hpp +412 -0
- wireforge/_data/runtime/include/wireforge/config.hpp +41 -0
- wireforge/_data/runtime/include/wireforge/delegate.hpp +101 -0
- wireforge/_data/runtime/include/wireforge/dispatcher.hpp +339 -0
- wireforge/_data/runtime/include/wireforge/expr_support.hpp +38 -0
- wireforge/_data/runtime/include/wireforge/frame_decoder.hpp +262 -0
- wireforge/_data/runtime/include/wireforge/frame_layout.hpp +20 -0
- wireforge/_data/runtime/include/wireforge/link.hpp +223 -0
- wireforge/_data/runtime/include/wireforge/lock.hpp +146 -0
- wireforge/_data/runtime/include/wireforge/ring_buffer.hpp +226 -0
- wireforge/_data/runtime/include/wireforge/span.hpp +116 -0
- wireforge/_data/runtime/include/wireforge/static_vector.hpp +264 -0
- wireforge/_data/runtime/include/wireforge/status.hpp +75 -0
- wireforge/_data/runtime/include/wireforge/transport.hpp +95 -0
- wireforge/_data/runtime/include/wireforge/validation.hpp +153 -0
- wireforge/_data/runtime/include/wireforge/version.hpp +22 -0
- wireforge/_data/runtime/requirements.yaml +438 -0
- wireforge/_data/schema/deviations.schema.json +47 -0
- wireforge/_data/schema/fragment.schema.json +79 -0
- wireforge/_data/schema/llr_ids.schema.json +25 -0
- wireforge/_data/schema/project.schema.json +1240 -0
- wireforge/_data/schema/runtime_requirements.schema.json +34 -0
- wireforge/_data/ui/assets/index-6Pomu6U4.js +44 -0
- wireforge/_data/ui/assets/index-GVdDyUAG.css +1 -0
- wireforge/_data/ui/index.html +13 -0
- wireforge/bundle.py +177 -0
- wireforge/ci_check.py +155 -0
- wireforge/cli.py +617 -0
- wireforge/codec.py +897 -0
- wireforge/compat.py +536 -0
- wireforge/coverage_points.py +64 -0
- wireforge/cpp.py +273 -0
- wireforge/cpp_codec.py +440 -0
- wireforge/cpp_interface.py +874 -0
- wireforge/cpp_tests.py +1993 -0
- wireforge/differential.py +586 -0
- wireforge/docs_markdown.py +72 -0
- wireforge/expr.py +598 -0
- wireforge/findings.py +187 -0
- wireforge/fmt.py +351 -0
- wireforge/generate.py +644 -0
- wireforge/golden.py +253 -0
- wireforge/hlr_import.py +112 -0
- wireforge/impact.py +249 -0
- wireforge/importers.py +402 -0
- wireforge/issues.py +27 -0
- wireforge/layout.py +681 -0
- wireforge/lifecycle.py +1266 -0
- wireforge/lifecycle_results.py +205 -0
- wireforge/lifecycle_rules.py +78 -0
- wireforge/lifecycle_runtime.py +375 -0
- wireforge/llr.py +693 -0
- wireforge/llr_ids.py +104 -0
- wireforge/loader.py +422 -0
- wireforge/mcp_server.py +357 -0
- wireforge/model.py +322 -0
- wireforge/plain.py +15 -0
- wireforge/py.typed +0 -0
- wireforge/reqif.py +289 -0
- wireforge/resources.py +43 -0
- wireforge/results.py +377 -0
- wireforge/runtime_reqs.py +147 -0
- wireforge/schema.py +151 -0
- wireforge/server/__init__.py +1 -0
- wireforge/server/app.py +619 -0
- wireforge/server/oidc.py +99 -0
- wireforge/server/storage.py +146 -0
- wireforge/service.py +1354 -0
- wireforge/templates/command/project.yaml +38 -0
- wireforge/templates/raw/project.yaml +28 -0
- wireforge/templates/telemetry/project.yaml +51 -0
- wireforge/test_cases.py +475 -0
- wireforge/test_project.py +478 -0
- wireforge/trace.py +442 -0
- wireforge/validate.py +1241 -0
- wireforge/vcs.py +44 -0
- wireforge/vectors.py +222 -0
- wireforge/versioning.py +137 -0
- wireforge/wireshark.py +792 -0
- wireforge/wireshark_runtime.lua +725 -0
- wireforge-0.1.0.dist-info/METADATA +91 -0
- wireforge-0.1.0.dist-info/RECORD +96 -0
- wireforge-0.1.0.dist-info/WHEEL +5 -0
- wireforge-0.1.0.dist-info/entry_points.txt +2 -0
- wireforge-0.1.0.dist-info/licenses/LICENSE +98 -0
- wireforge-0.1.0.dist-info/licenses/runtime/LICENSE +21 -0
- wireforge-0.1.0.dist-info/top_level.txt +1 -0
wireforge/llr_ids.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Stable numbers for the low-level requirements (`llr_ids.lock`).
|
|
2
|
+
|
|
3
|
+
A low-level requirement is identified by a key made of names (`SensorReading.temperature.RANGE`).
|
|
4
|
+
Customers' tools want ids that never change, so a project can pin a number to each key in an
|
|
5
|
+
optional lock file. The file is append-only: a number is never changed, moved or given again; a key
|
|
6
|
+
that disappears is moved to `retired`, and a renamed element keeps its number (`rename`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
13
|
+
|
|
14
|
+
from ruamel.yaml import YAML
|
|
15
|
+
|
|
16
|
+
from wireforge import schema
|
|
17
|
+
from wireforge.model import IdMap, Source
|
|
18
|
+
from wireforge.plain import to_plain
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
LOCK_FILE = "llr_ids.lock"
|
|
25
|
+
DEFAULT_FORMAT = "SWLLR-{n:05d}"
|
|
26
|
+
FIRST_NUMBER = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LockError(ValueError):
|
|
30
|
+
"""The lock file is invalid or an operation on it is not allowed."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load(project_dir: Path) -> IdMap | None:
|
|
34
|
+
"""Read the lock file of a project directory (None without one)."""
|
|
35
|
+
path = project_dir / LOCK_FILE
|
|
36
|
+
if not path.exists():
|
|
37
|
+
return None
|
|
38
|
+
document = schema.load_yaml(path)
|
|
39
|
+
issues = schema.validate_document(
|
|
40
|
+
document, "llr_ids.schema.json", file=str(path), source=document
|
|
41
|
+
)
|
|
42
|
+
if issues:
|
|
43
|
+
raise LockError("\n".join(str(issue) for issue in issues))
|
|
44
|
+
plain = cast("dict[str, Any]", to_plain(document))
|
|
45
|
+
return IdMap(
|
|
46
|
+
plain["format"],
|
|
47
|
+
plain["next"],
|
|
48
|
+
dict(plain["ids"]),
|
|
49
|
+
dict(plain.get("retired", {})),
|
|
50
|
+
Source(str(path), 1, ""),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def number(ids: IdMap, n: int) -> str:
|
|
55
|
+
"""Return the displayed id of the number `n`."""
|
|
56
|
+
return ids.format.format(n=n)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def assign(keys: Iterable[str], current: IdMap | None, *, text: str = DEFAULT_FORMAT) -> IdMap:
|
|
60
|
+
"""Return the lock for `keys`: existing numbers kept, new keys numbered in order.
|
|
61
|
+
|
|
62
|
+
Keys that are no longer derived move to `retired` with their number.
|
|
63
|
+
"""
|
|
64
|
+
wanted = list(dict.fromkeys(keys))
|
|
65
|
+
base = current or IdMap(text, FIRST_NUMBER, {}, {}, Source("", None, ""))
|
|
66
|
+
ids = {key: value for key, value in base.ids.items() if key in set(wanted)}
|
|
67
|
+
retired = dict(base.retired)
|
|
68
|
+
retired.update({key: value for key, value in base.ids.items() if key not in set(wanted)})
|
|
69
|
+
following = base.next
|
|
70
|
+
for key in wanted:
|
|
71
|
+
if key not in ids:
|
|
72
|
+
ids[key] = number(base, following)
|
|
73
|
+
following += 1
|
|
74
|
+
return IdMap(base.format, following, ids, retired, base.source)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rename(current: IdMap, old: str, new: str) -> IdMap:
|
|
78
|
+
"""Move a number to a new key (an element was renamed); the new key must not be numbered."""
|
|
79
|
+
if old not in current.ids:
|
|
80
|
+
message = f"{old!r} has no number"
|
|
81
|
+
raise LockError(message)
|
|
82
|
+
if new in current.ids:
|
|
83
|
+
message = f"{new!r} already has the number {current.ids[new]}"
|
|
84
|
+
raise LockError(message)
|
|
85
|
+
ids = {(new if key == old else key): value for key, value in current.ids.items()}
|
|
86
|
+
return IdMap(current.format, current.next, ids, dict(current.retired), current.source)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def dump(ids: IdMap) -> str:
|
|
90
|
+
"""Return the canonical text of the lock file."""
|
|
91
|
+
document: dict[str, Any] = {
|
|
92
|
+
"schema_version": 1,
|
|
93
|
+
"format": ids.format,
|
|
94
|
+
"next": ids.next,
|
|
95
|
+
"ids": dict(ids.ids),
|
|
96
|
+
}
|
|
97
|
+
if ids.retired:
|
|
98
|
+
document["retired"] = dict(ids.retired)
|
|
99
|
+
yaml = YAML(typ="rt")
|
|
100
|
+
yaml.default_flow_style = False
|
|
101
|
+
yaml.width = 4096
|
|
102
|
+
out = io.StringIO()
|
|
103
|
+
yaml.dump(document, out)
|
|
104
|
+
return out.getvalue()
|
wireforge/loader.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Project loader: root file + fragments -> `wireforge.model.Project`.
|
|
2
|
+
|
|
3
|
+
Structural problems (YAML, JSON Schema, version stamps) stop the load; semantic rules are checked
|
|
4
|
+
afterwards by `wireforge.validate`. Duplicate names across files are reported here, because
|
|
5
|
+
merging is where they become visible. The `requirements` catalog is a section like the others and
|
|
6
|
+
`reqs` lists are read into the elements that carry them; unknown ids are left to the validator.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
14
|
+
|
|
15
|
+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
16
|
+
from ruamel.yaml.error import YAMLError
|
|
17
|
+
|
|
18
|
+
from wireforge import llr_ids, schema, versioning
|
|
19
|
+
from wireforge.issues import Issue, errors_only
|
|
20
|
+
from wireforge.model import (
|
|
21
|
+
Checksum,
|
|
22
|
+
ChecksumUse,
|
|
23
|
+
Define,
|
|
24
|
+
Enum,
|
|
25
|
+
Field,
|
|
26
|
+
Framing,
|
|
27
|
+
Interface,
|
|
28
|
+
InterfaceMessage,
|
|
29
|
+
MessageInfo,
|
|
30
|
+
Occurs,
|
|
31
|
+
Profile,
|
|
32
|
+
Project,
|
|
33
|
+
Requirement,
|
|
34
|
+
Source,
|
|
35
|
+
Struct,
|
|
36
|
+
UserConstant,
|
|
37
|
+
UserHeader,
|
|
38
|
+
UserType,
|
|
39
|
+
Vector,
|
|
40
|
+
)
|
|
41
|
+
from wireforge.plain import to_plain
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
SECTIONS = (
|
|
47
|
+
"requirements",
|
|
48
|
+
"defines",
|
|
49
|
+
"profiles",
|
|
50
|
+
"checksums",
|
|
51
|
+
"enums",
|
|
52
|
+
"types",
|
|
53
|
+
"user_types",
|
|
54
|
+
"messages",
|
|
55
|
+
"interfaces",
|
|
56
|
+
"vectors",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class LoadResult:
|
|
62
|
+
"""Outcome of `load_project`: the model (None after structural errors) and all issues."""
|
|
63
|
+
|
|
64
|
+
project: Project | None
|
|
65
|
+
issues: list[Issue]
|
|
66
|
+
documents: dict[str, CommentedMap] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _mapping(node: object) -> dict[str, Any]:
|
|
70
|
+
"""Plain copy of a YAML mapping (the JSON Schema already checked its shape)."""
|
|
71
|
+
return cast("dict[str, Any]", to_plain(node))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _escape(part: str | int) -> str:
|
|
75
|
+
return str(part).replace("~", "~0").replace("/", "~1")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class _Where:
|
|
80
|
+
"""A YAML node plus its file and JSON pointer, used to derive child sources."""
|
|
81
|
+
|
|
82
|
+
file: str
|
|
83
|
+
node: Any
|
|
84
|
+
pointer: str
|
|
85
|
+
|
|
86
|
+
def child(self, key: str | int) -> _Where:
|
|
87
|
+
return _Where(self.file, self.node[key], f"{self.pointer}/{_escape(key)}")
|
|
88
|
+
|
|
89
|
+
def source_of(self, key: str | int) -> Source:
|
|
90
|
+
line: int | None = None
|
|
91
|
+
try:
|
|
92
|
+
if isinstance(self.node, CommentedMap):
|
|
93
|
+
line = self.node.lc.key(key)[0] + 1
|
|
94
|
+
elif isinstance(self.node, CommentedSeq) and isinstance(key, int):
|
|
95
|
+
line = self.node.lc.item(key)[0] + 1
|
|
96
|
+
except (KeyError, IndexError, TypeError):
|
|
97
|
+
line = None
|
|
98
|
+
return Source(self.file, line, f"{self.pointer}/{_escape(key)}")
|
|
99
|
+
|
|
100
|
+
def items(self) -> list[tuple[str, _Where, Source]]:
|
|
101
|
+
return [(str(key), self.child(key), self.source_of(key)) for key in self.node]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _occurs(raw: dict[str, Any] | None) -> Occurs | None:
|
|
105
|
+
if raw is None:
|
|
106
|
+
return None
|
|
107
|
+
return Occurs(
|
|
108
|
+
max=raw["max"], min=raw.get("min"), count=raw.get("count"), fill=bool(raw.get("fill"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _checksum_use(raw: dict[str, Any] | None) -> ChecksumUse | None:
|
|
113
|
+
if raw is None:
|
|
114
|
+
return None
|
|
115
|
+
covers = raw["covers"]
|
|
116
|
+
return ChecksumUse(
|
|
117
|
+
use=raw["use"], scope=covers.get("scope"), first=covers.get("from"), last=covers.get("to")
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _reqs(raw: dict[str, Any]) -> tuple[str, ...]:
|
|
122
|
+
return tuple(raw.get("reqs", ()))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _field(raw: dict[str, Any], source: Source) -> Field:
|
|
126
|
+
return Field(
|
|
127
|
+
name=raw["name"],
|
|
128
|
+
type=raw["type"],
|
|
129
|
+
source=source,
|
|
130
|
+
bits=raw.get("bits"),
|
|
131
|
+
min=raw.get("min"),
|
|
132
|
+
max=raw.get("max"),
|
|
133
|
+
default=raw.get("default"),
|
|
134
|
+
const=raw.get("const"),
|
|
135
|
+
reserved=bool(raw.get("reserved", False)),
|
|
136
|
+
occurs=_occurs(raw.get("occurs")),
|
|
137
|
+
checksum=_checksum_use(raw.get("checksum")),
|
|
138
|
+
role=raw.get("role"),
|
|
139
|
+
condition=raw.get("if"),
|
|
140
|
+
description=raw.get("description"),
|
|
141
|
+
units=raw.get("units"),
|
|
142
|
+
reqs=_reqs(raw),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _fields(where: _Where) -> tuple[Field, ...]:
|
|
147
|
+
fields = where.child("fields")
|
|
148
|
+
return tuple(
|
|
149
|
+
_field(_mapping(fields.node[index]), fields.source_of(index))
|
|
150
|
+
for index in range(len(fields.node))
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _struct(name: str, where: _Where, source: Source, *, message: bool) -> Struct:
|
|
155
|
+
raw = _mapping(where.node)
|
|
156
|
+
info = None
|
|
157
|
+
if message:
|
|
158
|
+
info = MessageInfo(
|
|
159
|
+
id=raw.get("id"),
|
|
160
|
+
endian=raw.get("endian"),
|
|
161
|
+
bit_order=raw.get("bit_order"),
|
|
162
|
+
range_check=raw.get("range_check"),
|
|
163
|
+
)
|
|
164
|
+
return Struct(
|
|
165
|
+
name,
|
|
166
|
+
_fields(where),
|
|
167
|
+
source,
|
|
168
|
+
condition=raw.get("if"),
|
|
169
|
+
message=info,
|
|
170
|
+
description=raw.get("description"),
|
|
171
|
+
reqs=_reqs(raw),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _checksum(name: str, where: _Where, source: Source) -> Checksum:
|
|
176
|
+
raw = _mapping(where.node)
|
|
177
|
+
return Checksum(
|
|
178
|
+
name=name,
|
|
179
|
+
algo=raw["algo"],
|
|
180
|
+
source=source,
|
|
181
|
+
width=raw.get("width"),
|
|
182
|
+
poly=raw.get("poly"),
|
|
183
|
+
init=raw.get("init", 0),
|
|
184
|
+
refin=raw.get("refin", False),
|
|
185
|
+
refout=raw.get("refout", False),
|
|
186
|
+
xorout=raw.get("xorout", 0),
|
|
187
|
+
check=raw.get("check"),
|
|
188
|
+
impl=raw.get("impl"),
|
|
189
|
+
include=raw.get("include"),
|
|
190
|
+
size_bytes=raw.get("size_bytes"),
|
|
191
|
+
reqs=_reqs(raw),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _user_type(name: str, where: _Where, source: Source) -> UserType:
|
|
196
|
+
raw = _mapping(where.node)
|
|
197
|
+
return UserType(
|
|
198
|
+
name=name,
|
|
199
|
+
cpp=raw["cpp"],
|
|
200
|
+
size_bytes=raw["size_bytes"],
|
|
201
|
+
source=source,
|
|
202
|
+
include=raw.get("include"),
|
|
203
|
+
style=raw.get("style", "quote"),
|
|
204
|
+
description=raw.get("description"),
|
|
205
|
+
reqs=_reqs(raw),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _interface_messages(where: _Where) -> tuple[InterfaceMessage, ...]:
|
|
210
|
+
entries = where.child("messages")
|
|
211
|
+
result = []
|
|
212
|
+
for index in range(len(entries.node)):
|
|
213
|
+
source = entries.source_of(index)
|
|
214
|
+
if isinstance(entries.node[index], str):
|
|
215
|
+
result.append(InterfaceMessage(str(entries.node[index]), source))
|
|
216
|
+
else:
|
|
217
|
+
raw = _mapping(entries.node[index])
|
|
218
|
+
result.append(
|
|
219
|
+
InterfaceMessage(
|
|
220
|
+
raw["name"],
|
|
221
|
+
source,
|
|
222
|
+
id=raw.get("id"),
|
|
223
|
+
condition=raw.get("if"),
|
|
224
|
+
reqs=_reqs(raw),
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return tuple(result)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _interface(name: str, where: _Where, source: Source) -> Interface:
|
|
231
|
+
raw = _mapping(where.node)
|
|
232
|
+
header_raw = raw.get("header", "none")
|
|
233
|
+
header = None if header_raw == "none" else _fields(where.child("header"))
|
|
234
|
+
trailer = _fields(where.child("trailer")) if "trailer" in raw else ()
|
|
235
|
+
framing_raw = raw.get("framing", {"kind": "none"})
|
|
236
|
+
framing = Framing(
|
|
237
|
+
kind=framing_raw["kind"],
|
|
238
|
+
frame_size=framing_raw.get("frame_size"),
|
|
239
|
+
max_frame_size=framing_raw.get("max_frame_size"),
|
|
240
|
+
length_counts=framing_raw.get("length_counts", "payload"),
|
|
241
|
+
resync=bool(framing_raw.get("resync", False)),
|
|
242
|
+
reqs=_reqs(framing_raw),
|
|
243
|
+
)
|
|
244
|
+
return Interface(
|
|
245
|
+
name=name,
|
|
246
|
+
version=raw["version"],
|
|
247
|
+
direction=raw["direction"],
|
|
248
|
+
source=source,
|
|
249
|
+
messages=_interface_messages(where),
|
|
250
|
+
header=header,
|
|
251
|
+
trailer=trailer,
|
|
252
|
+
framing=framing,
|
|
253
|
+
buffers=raw.get("buffers", {}),
|
|
254
|
+
endian=raw.get("endian"),
|
|
255
|
+
bit_order=raw.get("bit_order"),
|
|
256
|
+
allow_trailing_bytes=bool(raw.get("allow_trailing_bytes", False)),
|
|
257
|
+
description=raw.get("description"),
|
|
258
|
+
reqs=_reqs(raw),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _requirement(name: str, raw: dict[str, Any], source: Source) -> Requirement:
|
|
263
|
+
return Requirement(
|
|
264
|
+
name,
|
|
265
|
+
raw["text"],
|
|
266
|
+
source,
|
|
267
|
+
reference=raw.get("source"),
|
|
268
|
+
verification=raw.get("verification", "test"),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _vector(name: str, raw: dict[str, Any], source: Source) -> Vector:
|
|
273
|
+
text = raw["bytes"]
|
|
274
|
+
try:
|
|
275
|
+
data: bytes | None = bytes.fromhex(re.sub(r"[\s_]", "", text))
|
|
276
|
+
except ValueError:
|
|
277
|
+
data = None
|
|
278
|
+
expect = raw["expect"]
|
|
279
|
+
return Vector(
|
|
280
|
+
name=name,
|
|
281
|
+
message=raw["message"],
|
|
282
|
+
text=text,
|
|
283
|
+
data=data,
|
|
284
|
+
source=source,
|
|
285
|
+
interface=raw.get("interface"),
|
|
286
|
+
profile=raw.get("profile"),
|
|
287
|
+
fields=expect.get("fields"),
|
|
288
|
+
status=expect.get("status"),
|
|
289
|
+
direction=raw.get("direction", "decode"),
|
|
290
|
+
reqs=_reqs(raw),
|
|
291
|
+
description=raw.get("description"),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _add_entry(project: Project, section: str, name: str, where: _Where, source: Source) -> None:
|
|
296
|
+
if section == "requirements":
|
|
297
|
+
project.requirements[name] = _requirement(name, _mapping(where.node), source)
|
|
298
|
+
elif section == "defines":
|
|
299
|
+
project.defines[name] = Define(name, int(where.node), source)
|
|
300
|
+
elif section == "profiles":
|
|
301
|
+
project.profiles[name] = Profile(name, _mapping(where.node)["defines"], source)
|
|
302
|
+
elif section == "checksums":
|
|
303
|
+
project.checksums[name] = _checksum(name, where, source)
|
|
304
|
+
elif section == "enums":
|
|
305
|
+
raw = _mapping(where.node)
|
|
306
|
+
project.enums[name] = Enum(name, raw["type"], raw["values"], source, reqs=_reqs(raw))
|
|
307
|
+
elif section == "types":
|
|
308
|
+
project.types[name] = _struct(name, where, source, message=False)
|
|
309
|
+
elif section == "user_types":
|
|
310
|
+
project.user_types[name] = _user_type(name, where, source)
|
|
311
|
+
elif section == "messages":
|
|
312
|
+
project.messages[name] = _struct(name, where, source, message=True)
|
|
313
|
+
elif section == "vectors":
|
|
314
|
+
project.vectors[name] = _vector(name, _mapping(where.node), source)
|
|
315
|
+
else:
|
|
316
|
+
project.interfaces[name] = _interface(name, where, source)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _user_includes(project: Project, root: _Where) -> None:
|
|
320
|
+
if "user_includes" not in root.node:
|
|
321
|
+
return
|
|
322
|
+
includes = root.child("user_includes")
|
|
323
|
+
raw = _mapping(includes.node)
|
|
324
|
+
for index, directory in enumerate(raw.get("include_dirs", [])):
|
|
325
|
+
project.include_dirs.append((directory, includes.child("include_dirs").source_of(index)))
|
|
326
|
+
for index, header in enumerate(raw.get("headers", [])):
|
|
327
|
+
source = includes.child("headers").source_of(index)
|
|
328
|
+
style = header.get("style", "quote")
|
|
329
|
+
project.headers.append(UserHeader(header["path"], source, style, header.get("if")))
|
|
330
|
+
if "constants" in raw:
|
|
331
|
+
for name, _, source in includes.child("constants").items():
|
|
332
|
+
constant = raw["constants"][name]
|
|
333
|
+
project.constants[name] = UserConstant(
|
|
334
|
+
name,
|
|
335
|
+
constant["cpp"],
|
|
336
|
+
source,
|
|
337
|
+
value=constant.get("value"),
|
|
338
|
+
profile_values=constant.get("profile_values", {}),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _merge(project: Project, document: _Where, seen: dict[tuple[str, str], Source]) -> list[Issue]:
|
|
343
|
+
issues = []
|
|
344
|
+
for section in SECTIONS:
|
|
345
|
+
if section not in document.node:
|
|
346
|
+
continue
|
|
347
|
+
for name, where, source in document.child(section).items():
|
|
348
|
+
previous = seen.get((section, name))
|
|
349
|
+
if previous is not None:
|
|
350
|
+
where_first = f"{previous.file}:{previous.line}"
|
|
351
|
+
message = f"{section} entry {name!r} is already defined at {where_first}"
|
|
352
|
+
issues.append(source.issue("SR-1", message))
|
|
353
|
+
continue
|
|
354
|
+
seen[(section, name)] = source
|
|
355
|
+
_add_entry(project, section, name, where, source)
|
|
356
|
+
return issues
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _load_checked(path: Path, schema_file: str) -> tuple[CommentedMap | None, list[Issue]]:
|
|
360
|
+
try:
|
|
361
|
+
document = schema.load_yaml(path)
|
|
362
|
+
except (YAMLError, OSError) as error:
|
|
363
|
+
return None, [Issue(str(path), None, "", f"cannot load YAML: {error}")]
|
|
364
|
+
if not isinstance(document, CommentedMap):
|
|
365
|
+
return None, [Issue(str(path), 1, "", "top level must be a mapping")]
|
|
366
|
+
issues = versioning.check_document(document, file=str(path), line=1)
|
|
367
|
+
if not errors_only(issues):
|
|
368
|
+
issues += schema.validate_document(document, schema_file, file=str(path), source=document)
|
|
369
|
+
return (None if errors_only(issues) else document), issues
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _fragment_paths(root: Path, document: CommentedMap) -> list[Path]:
|
|
373
|
+
found: list[Path] = []
|
|
374
|
+
for pattern in document.get("include", []):
|
|
375
|
+
for path in sorted(root.parent.glob(str(pattern))):
|
|
376
|
+
if path.resolve() != root.resolve() and path not in found:
|
|
377
|
+
found.append(path)
|
|
378
|
+
return found
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _numbers(project: Project, directory: Path, issues: list[Issue]) -> None:
|
|
382
|
+
"""Read the optional lock file with the numbers of the low-level requirements."""
|
|
383
|
+
try:
|
|
384
|
+
project.llr_ids = llr_ids.load(directory)
|
|
385
|
+
except llr_ids.LockError as error:
|
|
386
|
+
issues.append(Issue(str(directory / llr_ids.LOCK_FILE), None, "", str(error)))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def load_project(location: Path) -> LoadResult:
|
|
390
|
+
"""Load a project directory (or its root file), merging every included fragment."""
|
|
391
|
+
root = location / schema.ROOT_FILE_NAME if location.is_dir() else location
|
|
392
|
+
document, issues = _load_checked(root, "project.schema.json")
|
|
393
|
+
if document is None:
|
|
394
|
+
return LoadResult(None, issues)
|
|
395
|
+
documents = {str(root): document}
|
|
396
|
+
for path in _fragment_paths(root, document):
|
|
397
|
+
fragment, fragment_issues = _load_checked(path, "fragment.schema.json")
|
|
398
|
+
issues += fragment_issues
|
|
399
|
+
if fragment is not None:
|
|
400
|
+
documents[str(path)] = fragment
|
|
401
|
+
if errors_only(issues):
|
|
402
|
+
return LoadResult(None, issues, documents)
|
|
403
|
+
top = _mapping(document)
|
|
404
|
+
project = Project(
|
|
405
|
+
name=top["project"],
|
|
406
|
+
source=Source(str(root), 1, ""),
|
|
407
|
+
version=top.get("version"),
|
|
408
|
+
namespace=top.get("namespace"),
|
|
409
|
+
defaults=top.get("defaults", {}),
|
|
410
|
+
files=list(documents),
|
|
411
|
+
)
|
|
412
|
+
seen: dict[tuple[str, str], Source] = {}
|
|
413
|
+
for file, loaded in documents.items():
|
|
414
|
+
issues += _merge(project, _Where(file, loaded, ""), seen)
|
|
415
|
+
_user_includes(project, _Where(str(root), document, ""))
|
|
416
|
+
_numbers(project, root.parent, issues)
|
|
417
|
+
for loaded in documents.values():
|
|
418
|
+
if "requirements_source" in loaded:
|
|
419
|
+
project.requirements_source = {
|
|
420
|
+
str(key): str(value) for key, value in loaded["requirements_source"].items()
|
|
421
|
+
}
|
|
422
|
+
return LoadResult(project, issues, documents)
|