Files
MARTe-Integrated-Components/Test/E2E/chain/scenarios.py
T
Martino Ferrari f256a4def5 test(e2e-chain): orchestrator + end-to-end fixes (Task 7)
Full-chain orchestrator (run_chain_e2e.sh) now runs the starter set green
(3 pass). Fixes found bringing the chain up end-to-end:

- client: gorilla/websocket cannot survive a read deadline (next ReadMessage
  panics "repeated read on failed connection"); replace the poll-with-deadline
  loop with a background reader goroutine + mutex-guarded state.
- orchestrator: guard env.sh's unbound LD_LIBRARY_PATH under set -u.
- scenarios/gen_data: centralize NUM_ROWS/ROW_DT and enforce sine freq to be a
  multiple of the buffer fundamental (LOOP_HZ=5 Hz) so the looped FileReader
  buffer is a seamless waveform; align starter freqs (5/5/10 Hz).
- gen_cfg: FileReader allows exactly one consuming Function, so route tapped
  (oracle=fed/both) sources through the DDB (ReaderGAM->DDB, then StreamGAM and
  TapGAM both read DDB) instead of a second FileReader consumer.
- validate_waveform: fidelity gates correctness (bit-exact / within one quant
  level); sine shape becomes a gross frequency-sanity gate (corr>=0.5) plus a
  tracked corr/nRMSE quality metric, since per-sample wall-clock calibration
  (Phase-A) and FULL_ARRAY packed timestamps (Phase-A4) are still pending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-25 13:11:35 +02:00

225 lines
10 KiB
Python

#!/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"}
# Producer buffer geometry — the single source of truth shared with gen_data.py.
# The MARTe FileReader loops this finite buffer (EOF=Rewind), so the streamed
# signal is only a continuous waveform if the buffer holds an integer number of
# periods. The buffer fundamental LOOP_HZ = 1/(NUM_ROWS*ROW_DT) is therefore the
# smallest sine frequency that loops seamlessly; every sine freq must be a
# positive integer multiple of it or the analytic shape oracle is invalid.
NUM_ROWS = 200 # producer cycles written to the FileReader input
ROW_DT = 1.0e-3 # seconds per producer cycle (row); 1 kHz producer
LOOP_HZ = 1.0 / (NUM_ROWS * ROW_DT) # 5.0 Hz buffer fundamental
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")
if sig["formula"] == "sine" and sig.get("freq"):
ratio = sig["freq"] / LOOP_HZ
if abs(ratio - round(ratio)) > 1e-9 or round(ratio) < 1:
errs.append(f"{sig['name']}: sine freq {sig['freq']} must be a "
f"positive multiple of LOOP_HZ={LOOP_HZ} (seamless loop)")
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=5.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=10.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'}")