test(e2e-chain): scenario model + starter scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
scenarios.py — Declarative scenario matrix for the streaming-chain E2E suite.
|
||||||
|
|
||||||
|
Each scenario describes one full-chain run:
|
||||||
|
MARTe2 App (UDPStreamer) -> StreamHub -> mock client
|
||||||
|
|
||||||
|
The matrix is a *curated covering set*: every configurable option value of the
|
||||||
|
UDPStreamer appears in at least one scenario, plus deliberately chosen
|
||||||
|
high-risk interactions. gen_data.py / gen_cfg.py / client / validate_waveform.py
|
||||||
|
all consume the scenario dict schema documented below.
|
||||||
|
|
||||||
|
Scenario dict schema
|
||||||
|
--------------------
|
||||||
|
{
|
||||||
|
"id": str # unique slug, also the artifact basename
|
||||||
|
"desc": str # human-readable one-liner for the report
|
||||||
|
"network": "unicast" | "multicast"
|
||||||
|
"publishing": "Strict" | "Accumulate" | "Decimate"
|
||||||
|
"ratio": int | None # Decimate only (>=1)
|
||||||
|
"min_refresh_hz": float | None # Accumulate only (>0)
|
||||||
|
"max_payload": int # MaxPayloadSize bytes
|
||||||
|
"ws_port": int # StreamHub WSPort (unique per scenario)
|
||||||
|
"sources": [ # one or more UDPStreamer blocks
|
||||||
|
{
|
||||||
|
"id": str # source/session id (hub Source key)
|
||||||
|
"udp_port": int # UDPStreamer Port (unique per scenario)
|
||||||
|
"data_port": int | None # multicast DATA port
|
||||||
|
"multicast_group": str | None # e.g. "239.0.0.1"
|
||||||
|
"signals": [
|
||||||
|
{
|
||||||
|
"name": str
|
||||||
|
"type": one of TYPE_CODES keys
|
||||||
|
"elements": int # NumberOfElements (1 = scalar)
|
||||||
|
"time_mode": "PacketTime"|"FullArray"|"FirstSample"|"LastSample"
|
||||||
|
"time_signal": str | None # name of the TimeSignal (when needed)
|
||||||
|
"sampling_rate": float | None # Hz, for First/LastSample
|
||||||
|
"quant": "none"|"uint8"|"int8"|"uint16"|"int16"
|
||||||
|
"range_min": float | None # quant only
|
||||||
|
"range_max": float | None # quant only
|
||||||
|
"unit": str | None # e.g. "V", "us", "ns"
|
||||||
|
"formula": "sine"|"ramp"|"counter"|"time_ns"|"time_us"
|
||||||
|
"freq": float | None # sine frequency (Hz over row index)
|
||||||
|
"is_time": bool # True if this signal is a TimeSignal
|
||||||
|
}, ...
|
||||||
|
]
|
||||||
|
}, ...
|
||||||
|
],
|
||||||
|
"oracle": "analytic" | "fed" | "both"
|
||||||
|
"client_checks": subset of ["live","zoom","window","trigger"]
|
||||||
|
"trig_signal": "src:sig" | None # which signal to trigger on
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# MARTe2 TypeDescriptor.all codes: all = (type<<2) | (bits<<6),
|
||||||
|
# BasicType SignedInteger=0, UnsignedInteger=1, Float=2 (verified against
|
||||||
|
# MARTe2 L0Types/BasicType.h and L1Portability/TypeDescriptor.h).
|
||||||
|
TYPE_CODES = {
|
||||||
|
"int8": 0x0200,
|
||||||
|
"uint8": 0x0204,
|
||||||
|
"int16": 0x0400,
|
||||||
|
"uint16": 0x0404,
|
||||||
|
"int32": 0x0800,
|
||||||
|
"uint32": 0x0804,
|
||||||
|
"int64": 0x1000,
|
||||||
|
"uint64": 0x1004,
|
||||||
|
"float32": 0x0808,
|
||||||
|
"float64": 0x1008,
|
||||||
|
}
|
||||||
|
|
||||||
|
# numpy dtype per MARTe type (little-endian).
|
||||||
|
NP_DTYPE = {
|
||||||
|
"int8": "<i1", "uint8": "<u1", "int16": "<i2", "uint16": "<u2",
|
||||||
|
"int32": "<i4", "uint32": "<u4", "int64": "<i8", "uint64": "<u8",
|
||||||
|
"float32": "<f4", "float64": "<f8",
|
||||||
|
}
|
||||||
|
|
||||||
|
FLOAT_TYPES = {"float32", "float64"}
|
||||||
|
QUANT_TYPES = {"none", "uint8", "int8", "uint16", "int16"}
|
||||||
|
QUANT_LEVELS = {"uint8": 255, "int8": 254, "uint16": 65535, "int16": 65534}
|
||||||
|
TIME_MODES = {"PacketTime", "FullArray", "FirstSample", "LastSample"}
|
||||||
|
|
||||||
|
|
||||||
|
def _sig(name, type, elements=1, time_mode="PacketTime", time_signal=None,
|
||||||
|
sampling_rate=None, quant="none", range_min=None, range_max=None,
|
||||||
|
unit=None, formula="sine", freq=1.0, is_time=False):
|
||||||
|
return {
|
||||||
|
"name": name, "type": type, "elements": elements,
|
||||||
|
"time_mode": time_mode, "time_signal": time_signal,
|
||||||
|
"sampling_rate": sampling_rate, "quant": quant,
|
||||||
|
"range_min": range_min, "range_max": range_max, "unit": unit,
|
||||||
|
"formula": formula, "freq": freq, "is_time": is_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_scenario(s):
|
||||||
|
"""Return a list of validity error strings (empty == valid)."""
|
||||||
|
errs = []
|
||||||
|
if s.get("network") not in ("unicast", "multicast"):
|
||||||
|
errs.append("network must be unicast|multicast")
|
||||||
|
if s.get("publishing") not in ("Strict", "Accumulate", "Decimate"):
|
||||||
|
errs.append("publishing invalid")
|
||||||
|
if s["publishing"] == "Decimate" and not (s.get("ratio") and s["ratio"] >= 1):
|
||||||
|
errs.append("Decimate needs ratio>=1")
|
||||||
|
if s["publishing"] == "Accumulate" and not (s.get("min_refresh_hz") and
|
||||||
|
s["min_refresh_hz"] > 0):
|
||||||
|
errs.append("Accumulate needs min_refresh_hz>0")
|
||||||
|
if not s.get("sources"):
|
||||||
|
errs.append("no sources")
|
||||||
|
for src in s.get("sources", []):
|
||||||
|
if s["network"] == "multicast":
|
||||||
|
if not src.get("multicast_group") or not src.get("data_port"):
|
||||||
|
errs.append(f"src {src['id']}: multicast needs group+data_port")
|
||||||
|
names = {sig["name"] for sig in src["signals"]}
|
||||||
|
for sig in src["signals"]:
|
||||||
|
if sig["type"] not in TYPE_CODES:
|
||||||
|
errs.append(f"{sig['name']}: bad type {sig['type']}")
|
||||||
|
if sig["quant"] not in QUANT_TYPES:
|
||||||
|
errs.append(f"{sig['name']}: bad quant {sig['quant']}")
|
||||||
|
if sig["quant"] != "none":
|
||||||
|
if sig["type"] not in FLOAT_TYPES:
|
||||||
|
errs.append(f"{sig['name']}: quant only on float types")
|
||||||
|
if sig["range_min"] is None or sig["range_max"] is None:
|
||||||
|
errs.append(f"{sig['name']}: quant needs range_min/max")
|
||||||
|
if sig["time_mode"] not in TIME_MODES:
|
||||||
|
errs.append(f"{sig['name']}: bad time_mode")
|
||||||
|
if sig["time_mode"] != "PacketTime":
|
||||||
|
if not sig["time_signal"] or sig["time_signal"] not in names:
|
||||||
|
errs.append(f"{sig['name']}: time_mode needs valid time_signal")
|
||||||
|
if sig["time_mode"] == "FullArray":
|
||||||
|
ts = next((x for x in src["signals"]
|
||||||
|
if x["name"] == sig["time_signal"]), None)
|
||||||
|
if ts and ts["elements"] != sig["elements"]:
|
||||||
|
errs.append(f"{sig['name']}: FullArray time_signal length mismatch")
|
||||||
|
if sig["time_mode"] in ("FirstSample", "LastSample"):
|
||||||
|
if not sig["sampling_rate"] or sig["sampling_rate"] <= 0:
|
||||||
|
errs.append(f"{sig['name']}: First/LastSample needs sampling_rate>0")
|
||||||
|
return errs
|
||||||
|
|
||||||
|
|
||||||
|
# ── Starter set (expanded to the full covering matrix in Task 8) ──────────────
|
||||||
|
SCENARIOS = [
|
||||||
|
{
|
||||||
|
"id": "s01_scalar_uint32",
|
||||||
|
"desc": "Single uint32 scalar counter, Strict unicast (type fidelity)",
|
||||||
|
"network": "unicast", "publishing": "Strict",
|
||||||
|
"ratio": None, "min_refresh_hz": None, "max_payload": 1400,
|
||||||
|
"ws_port": 8101,
|
||||||
|
"sources": [{
|
||||||
|
"id": "src", "udp_port": 44610, "data_port": None,
|
||||||
|
"multicast_group": None,
|
||||||
|
"signals": [
|
||||||
|
_sig("Counter", "uint32", 1, formula="counter"),
|
||||||
|
_sig("Sine", "float32", 1, formula="sine", freq=2.0, unit="V"),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
"oracle": "analytic",
|
||||||
|
"client_checks": ["live", "zoom", "window", "trigger"],
|
||||||
|
"trig_signal": "src:Sine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s02_array_float32_fullarray",
|
||||||
|
"desc": "100-elem float32 array, FullArray time mode, uint64 ns time array",
|
||||||
|
"network": "unicast", "publishing": "Strict",
|
||||||
|
"ratio": None, "min_refresh_hz": None, "max_payload": 65507,
|
||||||
|
"ws_port": 8102,
|
||||||
|
"sources": [{
|
||||||
|
"id": "src", "udp_port": 44612, "data_port": None,
|
||||||
|
"multicast_group": None,
|
||||||
|
"signals": [
|
||||||
|
_sig("TimeArr", "uint64", 100, unit="ns",
|
||||||
|
formula="time_ns", is_time=True),
|
||||||
|
_sig("Wave", "float32", 100, time_mode="FullArray",
|
||||||
|
time_signal="TimeArr", formula="sine", freq=5.0, unit="V"),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
"oracle": "both",
|
||||||
|
"client_checks": ["live", "zoom"],
|
||||||
|
"trig_signal": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s03_quant_uint16",
|
||||||
|
"desc": "float32 scalar quantised to uint16 over [-5,5], Strict unicast",
|
||||||
|
"network": "unicast", "publishing": "Strict",
|
||||||
|
"ratio": None, "min_refresh_hz": None, "max_payload": 1400,
|
||||||
|
"ws_port": 8103,
|
||||||
|
"sources": [{
|
||||||
|
"id": "src", "udp_port": 44614, "data_port": None,
|
||||||
|
"multicast_group": None,
|
||||||
|
"signals": [
|
||||||
|
_sig("Sine", "float32", 1, quant="uint16",
|
||||||
|
range_min=-5.0, range_max=5.0, formula="sine",
|
||||||
|
freq=3.0, unit="V"),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
"oracle": "analytic",
|
||||||
|
"client_checks": ["live", "zoom"],
|
||||||
|
"trig_signal": "src:Sine",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ok = True
|
||||||
|
for s in SCENARIOS:
|
||||||
|
errs = validate_scenario(s)
|
||||||
|
print(f"{s['id']:32s} {'OK' if not errs else errs}")
|
||||||
|
ok = ok and not errs
|
||||||
|
print(f"\n{len(SCENARIOS)} scenarios, {'ALL VALID' if ok else 'INVALID PRESENT'}")
|
||||||
Reference in New Issue
Block a user