83d0a060fe
Adds a trimmed debug_e2e.cfg (DebugService on 8080/8081, TcpLogger on
9090) and two new scenarios (s55_debug_force_trace_break, kind=debug;
s56_tcplogger_delivery, kind=tcplogger) reusing it, with matching
run_e2e.sh scenario-list/dispatch wiring and debugclient build steps.
Also fixes a real bug found while wiring s56: debugclient's tcplogger
check was tautological (it matched MarteController's own local
"CMD"-level echo of the outgoing command, which contains the same text
as the triggered event, instead of a line actually delivered over the
real TCPLogger TCP socket) and its trigger command (an invalid FORCE)
never reaches DebugServiceBase's REPORT_ERROR at all. Switched the
trigger to a MSG-to-missing-destination command (which does call
REPORT_ERROR) and the match to require the real log text
("not found in ORD"), recorded only after a connect-settle baseline —
verified with a positive run (real Warning-level TCPLogger line
received) and a negative control (LogPort=0 disables TcpLogger and the
scenario correctly FAILs).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
672 lines
33 KiB
Python
672 lines
33 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
|
|
"known_issue": str | None # if set, the scenario exercises a
|
|
# documented, not-yet-fixed chain gap:
|
|
# a FAIL is reclassified XFAIL (does not
|
|
# break the baseline) and a PASS becomes
|
|
# XPASS (the bug is unexpectedly fixed —
|
|
# time to drop the marker). The string is
|
|
# the human-readable reason.
|
|
}
|
|
"""
|
|
|
|
# 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",
|
|
}
|
|
|
|
# Wire constraints shared with the C++ UDPS layer. The UDPStreamer fragments the
|
|
# serialised packet into chunks of at most MaxPayloadSize *payload* bytes and
|
|
# prepends a UDPS_HEADER_SIZE-byte header, so the datagram it hands to sendto() is
|
|
# MaxPayloadSize+UDPS_HEADER_SIZE bytes. That must not exceed the maximum UDP
|
|
# payload or sendto() fails with EMSGSIZE.
|
|
UDPS_HEADER_SIZE = 17 # bytes; mirrors Common/UDP/UDPSProtocol.h
|
|
MAX_UDP_PAYLOAD = 65507 # 65535 - 20 (IP) - 8 (UDP)
|
|
|
|
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 geometry(s):
|
|
"""Resolve a scenario's producer geometry (per-scenario override or global).
|
|
|
|
Returns (row_dt, num_rows, producer_hz, loop_hz). Every data/config/validator
|
|
consumer reads geometry through here so the override cannot drift apart from
|
|
the seamless-loop fundamental used by the sine constraint.
|
|
"""
|
|
row_dt = s.get("row_dt") or ROW_DT
|
|
num_rows = s.get("num_rows") or NUM_ROWS
|
|
producer_hz = s.get("producer_hz") or int(round(1.0 / row_dt))
|
|
loop_hz = 1.0 / (num_rows * row_dt)
|
|
return row_dt, num_rows, producer_hz, loop_hz
|
|
|
|
|
|
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 = []
|
|
kind = s.get("kind", "chain")
|
|
if kind == "direct":
|
|
if "cfg" not in s:
|
|
errs.append(f"direct scenario {s.get('id')} missing cfg")
|
|
return errs
|
|
if kind == "recorder":
|
|
if "hub_cfg" not in s or "marte_cfg" not in s:
|
|
errs.append(f"recorder scenario {s.get('id')} missing hub_cfg/marte_cfg")
|
|
return errs
|
|
if kind in ("debug", "tcplogger"):
|
|
if not all(k in s for k in ("cfg", "cmd_port", "udp_port", "log_port")):
|
|
errs.append(f"{kind} scenario {s.get('id')} missing cfg/cmd_port/udp_port/log_port")
|
|
return errs
|
|
if kind != "chain":
|
|
errs.append(f"unknown scenario kind: {kind}")
|
|
return errs
|
|
row_dt, num_rows, _producer_hz, loop_hz = geometry(s)
|
|
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")
|
|
max_payload = s.get("max_payload")
|
|
if max_payload is not None and max_payload + UDPS_HEADER_SIZE > MAX_UDP_PAYLOAD:
|
|
errs.append(
|
|
f"max_payload {max_payload} + {UDPS_HEADER_SIZE}B header exceeds the "
|
|
f"{MAX_UDP_PAYLOAD}B UDP datagram limit (sendto EMSGSIZE); "
|
|
f"cap at {MAX_UDP_PAYLOAD - UDPS_HEADER_SIZE}")
|
|
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")
|
|
else:
|
|
# The per-packet sample window must fit inside one producer
|
|
# cycle, or successive packets' expanded timestamps overlap
|
|
# and the hub's binary-search ring (which assumes a sorted
|
|
# time axis) is corrupted — the s17/s18 failure class.
|
|
window = (sig["elements"] - 1) / sig["sampling_rate"]
|
|
if window > row_dt + 1e-12:
|
|
errs.append(
|
|
f"{sig['name']}: {sig['time_mode']} window "
|
|
f"{window * 1e3:.4f} ms > row period {row_dt * 1e3:.4f} ms "
|
|
f"(non-monotonic ring); raise producer rate or "
|
|
f"sampling_rate, or lower elements")
|
|
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
|
|
|
|
|
|
# ── Curated covering matrix ───────────────────────────────────────────────────
|
|
# The first three scenarios are referenced positionally by tests_py.py
|
|
# (SCENARIOS[0..2]) and are kept verbatim. The remainder is built with the helpers
|
|
# below, which auto-allocate unique WS/UDP/DATA ports so the matrix can grow
|
|
# without manual bookkeeping. Coverage goal: every UDPStreamer option *value*
|
|
# (all 10 types, scalar+array shapes, all four TimeModes, all five quant kinds,
|
|
# the three publishing modes, unicast+multicast, fragmentation via small payload,
|
|
# multi-source) appears at least once, plus deliberately chosen high-risk
|
|
# interactions (decimate+quant+array, accumulate+fullarray, multicast+decimate,
|
|
# fragmentation+decimate). Non-sine formulas (counter/ramp) are preferred for
|
|
# pure type/shape coverage because only the fidelity oracle gates them; sine is
|
|
# used where the shape metric should be tracked. MCAST_GROUP must have a route on
|
|
# the test host or those scenarios report SKIP (the orchestrator probes it).
|
|
import itertools
|
|
|
|
MCAST_GROUP = "239.0.7.7"
|
|
_ws = itertools.count(8104)
|
|
_udp = itertools.count(44616, 2)
|
|
_data = itertools.count(45616, 2)
|
|
|
|
|
|
def _src(sid, signals, multicast=False):
|
|
return {
|
|
"id": sid, "udp_port": next(_udp),
|
|
"data_port": next(_data) if multicast else None,
|
|
"multicast_group": MCAST_GROUP if multicast else None,
|
|
"signals": signals,
|
|
}
|
|
|
|
|
|
def mk(sid, desc, sources, network="unicast", publishing="Strict",
|
|
ratio=None, min_refresh_hz=None, max_payload=1400,
|
|
oracle="analytic", checks=("live", "zoom"), trig=None,
|
|
known_issue=None, row_dt=None, num_rows=None, producer_hz=None):
|
|
# row_dt / num_rows / producer_hz override the global producer geometry for
|
|
# this scenario only (default None == use the suite-wide NUM_ROWS / ROW_DT /
|
|
# 1 kHz). They are kept together so the per-cycle wall gap (1/producer_hz),
|
|
# the encoded time-step (row_dt) and the seamless-loop fundamental
|
|
# (1/(num_rows*row_dt)) stay mutually consistent.
|
|
return {
|
|
"id": sid, "desc": desc, "network": network, "publishing": publishing,
|
|
"ratio": ratio, "min_refresh_hz": min_refresh_hz,
|
|
"max_payload": max_payload, "ws_port": next(_ws),
|
|
"sources": sources, "oracle": oracle,
|
|
"client_checks": list(checks), "trig_signal": trig,
|
|
"known_issue": known_issue,
|
|
"row_dt": row_dt, "num_rows": num_rows, "producer_hz": producer_hz,
|
|
}
|
|
|
|
|
|
_STARTERS = [
|
|
{
|
|
"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": MAX_UDP_PAYLOAD - UDPS_HEADER_SIZE,
|
|
"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",
|
|
},
|
|
]
|
|
|
|
# ── Type fidelity: one scalar per remaining MARTe type (fidelity-only) ────────
|
|
_TYPES = [
|
|
mk("s04_int8_scalar", "int8 scalar counter, type fidelity",
|
|
[_src("src", [_sig("Cnt", "int8", 1, formula="counter")])]),
|
|
mk("s05_uint8_scalar", "uint8 scalar counter, type fidelity",
|
|
[_src("src", [_sig("Cnt", "uint8", 1, formula="counter")])]),
|
|
mk("s06_int16_scalar", "int16 scalar ramp, type fidelity",
|
|
[_src("src", [_sig("Ramp", "int16", 1, formula="ramp")])]),
|
|
mk("s07_uint16_scalar", "uint16 scalar ramp, type fidelity",
|
|
[_src("src", [_sig("Ramp", "uint16", 1, formula="ramp")])]),
|
|
mk("s08_int32_scalar", "int32 scalar counter, type fidelity",
|
|
[_src("src", [_sig("Cnt", "int32", 1, formula="counter")])]),
|
|
mk("s09_int64_scalar", "int64 scalar counter, type fidelity",
|
|
[_src("src", [_sig("Cnt", "int64", 1, formula="counter")])]),
|
|
mk("s10_uint64_scalar", "uint64 scalar counter, type fidelity",
|
|
[_src("src", [_sig("Cnt", "uint64", 1, formula="counter")])]),
|
|
mk("s11_float64_scalar", "float64 scalar sine 5 Hz (double-precision path)",
|
|
[_src("src", [_sig("Sine", "float64", 1, formula="sine", freq=5.0,
|
|
unit="V")])],
|
|
checks=("live", "zoom", "trigger"), trig="src:Sine"),
|
|
]
|
|
|
|
# ── Array shapes (NumberOfElements) ──────────────────────────────────────────
|
|
_ARRAYS = [
|
|
mk("s12_f32_arr8", "float32 8-elem array sine 5 Hz",
|
|
[_src("src", [_sig("Wave", "float32", 8, formula="sine", freq=5.0)])]),
|
|
mk("s13_f32_arr32", "float32 32-elem array sine 10 Hz",
|
|
[_src("src", [_sig("Wave", "float32", 32, formula="sine", freq=10.0)])]),
|
|
mk("s14_f64_arr64", "float64 64-elem array ramp",
|
|
[_src("src", [_sig("Ramp", "float64", 64, formula="ramp")])]),
|
|
mk("s15_i16_arr16", "int16 16-elem array counter",
|
|
[_src("src", [_sig("Cnt", "int16", 16, formula="counter")])]),
|
|
mk("s16_f32_arr256", "float32 256-elem array sine 5 Hz (large frame)",
|
|
[_src("src", [_sig("Wave", "float32", 256, formula="sine", freq=5.0)])],
|
|
max_payload=MAX_UDP_PAYLOAD - UDPS_HEADER_SIZE),
|
|
mk("s39_uint8_arr32", "uint8 32-elem array counter (wrap fidelity)",
|
|
[_src("src", [_sig("Cnt", "uint8", 32, formula="counter")])]),
|
|
mk("s40_int8_arr16", "int8 16-elem array counter (wrap fidelity)",
|
|
[_src("src", [_sig("Cnt", "int8", 16, formula="counter")])]),
|
|
]
|
|
|
|
# ── Time modes (each non-PacketTime mode needs a TimeSignal in the source) ────
|
|
_TIMEMODES = [
|
|
# sampling_rate = elements/ROW_DT (8/0.001 = 8000) so the per-cycle window
|
|
# of 8 samples fills exactly one 1 kHz producer cycle. A smaller rate makes
|
|
# each cycle's window wider than the inter-cycle gap, so successive windows
|
|
# overlap and the published timestamps stop being monotonic — which corrupts
|
|
# the hub's binary-search range/zoom queries (they assume a sorted ring).
|
|
mk("s17_lastsample", "float32 8-elem LastSample, uint64 ns scalar anchor",
|
|
[_src("src", [
|
|
_sig("Tns", "uint64", 1, unit="ns", formula="time_ns", is_time=True),
|
|
_sig("Data", "float32", 8, time_mode="LastSample",
|
|
time_signal="Tns", sampling_rate=8000.0, formula="counter"),
|
|
])]),
|
|
mk("s18_firstsample", "float32 8-elem FirstSample, uint32 us scalar anchor",
|
|
[_src("src", [
|
|
_sig("Tus", "uint32", 1, unit="us", formula="time_us", is_time=True),
|
|
_sig("Data", "float32", 8, time_mode="FirstSample",
|
|
time_signal="Tus", sampling_rate=8000.0, formula="counter"),
|
|
])]),
|
|
mk("s19_fullarray_f64", "float64 50-elem FullArray sine 5 Hz, uint64 ns time",
|
|
[_src("src", [
|
|
_sig("TimeArr", "uint64", 50, unit="ns", formula="time_ns",
|
|
is_time=True),
|
|
_sig("Wave", "float64", 50, time_mode="FullArray",
|
|
time_signal="TimeArr", formula="sine", freq=5.0, unit="V"),
|
|
])],
|
|
oracle="both"),
|
|
mk("s43_fullarray_quant", "float32 16-elem FullArray quant uint16 sine 5 Hz",
|
|
[_src("src", [
|
|
_sig("TimeArr", "uint64", 16, unit="ns", formula="time_ns",
|
|
is_time=True),
|
|
_sig("Wave", "float32", 16, time_mode="FullArray",
|
|
time_signal="TimeArr", quant="uint16", range_min=-1.0,
|
|
range_max=1.0, formula="sine", freq=5.0, unit="V"),
|
|
])]),
|
|
]
|
|
|
|
# ── Quantization (each QuantizedType, on float signals) ──────────────────────
|
|
_QUANT = [
|
|
mk("s20_quant_uint8", "float32 scalar quant uint8 [-1,1] sine 5 Hz",
|
|
[_src("src", [_sig("Sine", "float32", 1, quant="uint8", range_min=-1.0,
|
|
range_max=1.0, formula="sine", freq=5.0)])]),
|
|
mk("s21_quant_int8", "float32 scalar quant int8 [-10,10] sine 5 Hz",
|
|
[_src("src", [_sig("Sine", "float32", 1, quant="int8", range_min=-10.0,
|
|
range_max=10.0, formula="sine", freq=5.0)])]),
|
|
mk("s22_quant_int16", "float32 scalar quant int16 [-100,100] ramp",
|
|
[_src("src", [_sig("Ramp", "float32", 1, quant="int16", range_min=-100.0,
|
|
range_max=100.0, formula="ramp")])]),
|
|
mk("s23_quant_f64_arr", "float64 16-elem quant uint16 [-2,2] sine 5 Hz",
|
|
[_src("src", [_sig("Wave", "float64", 16, quant="uint16", range_min=-2.0,
|
|
range_max=2.0, formula="sine", freq=5.0)])]),
|
|
]
|
|
|
|
# ── Publishing modes (Strict covered; Accumulate + Decimate here) ────────────
|
|
_PUBLISH = [
|
|
mk("s24_accumulate", "float32 scalar sine 5 Hz, Accumulate @50 Hz refresh",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)])],
|
|
publishing="Accumulate", min_refresh_hz=50.0),
|
|
mk("s25_decimate4", "float32 scalar sine 5 Hz, Decimate ratio 4",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)])],
|
|
publishing="Decimate", ratio=4),
|
|
mk("s26_decimate10_arr", "float32 8-elem counter, Decimate ratio 10",
|
|
[_src("src", [_sig("Cnt", "float32", 8, formula="counter")])],
|
|
publishing="Decimate", ratio=10),
|
|
mk("s46_accumulate_arr",
|
|
"Accumulate @200 Hz: accumulated scalar sine + 16-elem array passenger",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0),
|
|
_sig("Wave", "float32", 16, formula="sine", freq=5.0)])],
|
|
publishing="Accumulate", min_refresh_hz=200.0),
|
|
mk("s45_decimate_multisig", "Decimate ratio 2 over a 2-signal source",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0),
|
|
_sig("Cnt", "uint32", 1, formula="counter")])],
|
|
publishing="Decimate", ratio=2),
|
|
]
|
|
|
|
# ── Payload size / fragmentation (small MaxPayloadSize vs large array) ───────
|
|
_PAYLOAD = [
|
|
mk("s27_frag_f64_128", "float64 128-elem ramp, MaxPayload 512 (fragmented)",
|
|
[_src("src", [_sig("Ramp", "float64", 128, formula="ramp")])],
|
|
max_payload=512),
|
|
mk("s28_frag_f32_100", "float32 100-elem sine 5 Hz, MaxPayload 256 (fragmented)",
|
|
[_src("src", [_sig("Wave", "float32", 100, formula="sine", freq=5.0)])],
|
|
max_payload=256),
|
|
mk("s48_f64_arr_big_payload", "float64 100-elem ramp, MaxPayload 65490 (single frame)",
|
|
[_src("src", [_sig("Ramp", "float64", 100, formula="ramp")])],
|
|
max_payload=MAX_UDP_PAYLOAD - UDPS_HEADER_SIZE),
|
|
]
|
|
|
|
# ── Multicast ────────────────────────────────────────────────────────────────
|
|
_MCAST = [
|
|
mk("s29_mcast_scalar", "multicast float32 scalar sine 5 Hz",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)],
|
|
multicast=True)],
|
|
network="multicast"),
|
|
mk("s30_mcast_arr_fullarray", "multicast float32 32-elem FullArray sine 5 Hz",
|
|
[_src("src", [
|
|
_sig("TimeArr", "uint64", 32, unit="ns", formula="time_ns",
|
|
is_time=True),
|
|
_sig("Wave", "float32", 32, time_mode="FullArray",
|
|
time_signal="TimeArr", formula="sine", freq=5.0),
|
|
], multicast=True)],
|
|
network="multicast"),
|
|
mk("s47_mcast_multisrc", "multicast, two sources (scalar each)",
|
|
[_src("a", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)],
|
|
multicast=True),
|
|
_src("b", [_sig("Cnt", "uint32", 1, formula="counter")],
|
|
multicast=True)],
|
|
network="multicast"),
|
|
]
|
|
|
|
# ── Multiple sources (independent UDPStreamer blocks, one hub) ───────────────
|
|
_MULTISRC = [
|
|
mk("s31_two_src", "two unicast sources: float32 sine + uint32 counter",
|
|
[_src("a", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)]),
|
|
_src("b", [_sig("Cnt", "uint32", 1, formula="counter")])]),
|
|
mk("s32_three_src", "three unicast sources: int16 ramp / float64 sine / uint8 counter",
|
|
[_src("a", [_sig("Ramp", "int16", 1, formula="ramp")]),
|
|
_src("b", [_sig("Sine", "float64", 1, formula="sine", freq=5.0)]),
|
|
_src("c", [_sig("Cnt", "uint8", 1, formula="counter")])]),
|
|
]
|
|
|
|
# ── High-risk interactions ───────────────────────────────────────────────────
|
|
_INTERACT = [
|
|
mk("s33_dec_arr_quant", "Decimate 2 + 16-elem quant uint16 sine 5 Hz",
|
|
[_src("src", [_sig("Wave", "float32", 16, quant="uint16", range_min=-1.0,
|
|
range_max=1.0, formula="sine", freq=5.0)])],
|
|
publishing="Decimate", ratio=2),
|
|
mk("s34_acc_fullarray",
|
|
"Accumulate @100 Hz: accumulated scalar + 32-elem FullArray sine passenger",
|
|
[_src("src", [
|
|
_sig("Sine", "float32", 1, formula="sine", freq=5.0),
|
|
_sig("TimeArr", "uint64", 32, unit="ns", formula="time_ns",
|
|
is_time=True),
|
|
_sig("Wave", "float32", 32, time_mode="FullArray",
|
|
time_signal="TimeArr", formula="sine", freq=5.0),
|
|
])],
|
|
publishing="Accumulate", min_refresh_hz=100.0),
|
|
mk("s35_mcast_decimate", "multicast + Decimate ratio 5, float32 scalar sine 5 Hz",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)],
|
|
multicast=True)],
|
|
network="multicast", publishing="Decimate", ratio=5),
|
|
mk("s36_big_frag_dec", "float64 64-elem ramp, MaxPayload 256 + Decimate 4",
|
|
[_src("src", [_sig("Ramp", "float64", 64, formula="ramp")])],
|
|
max_payload=256, publishing="Decimate", ratio=4),
|
|
mk("s49_mixed_quant_raw", "one source: quant uint8 sine + raw float32 sine",
|
|
[_src("src", [
|
|
_sig("Q", "float32", 1, quant="uint8", range_min=-1.0, range_max=1.0,
|
|
formula="sine", freq=5.0),
|
|
_sig("Raw", "float32", 1, formula="sine", freq=5.0),
|
|
])]),
|
|
]
|
|
|
|
# ── Trigger + client-check coverage (rising/falling/both swept by the client) ─
|
|
_TRIGGER = [
|
|
mk("s37_trig_ramp_i32", "trigger on int32 ramp scalar",
|
|
[_src("src", [_sig("Ramp", "int32", 1, formula="ramp")])],
|
|
checks=("live", "trigger"), trig="src:Ramp"),
|
|
mk("s38_trig_f64_sine", "trigger on float64 sine 5 Hz scalar",
|
|
[_src("src", [_sig("Sine", "float64", 1, formula="sine", freq=5.0)])],
|
|
checks=("live", "zoom", "trigger"), trig="src:Sine"),
|
|
mk("s44_window_check", "float32 sine 5 Hz scalar, window time-range check",
|
|
[_src("src", [_sig("Sine", "float32", 1, formula="sine", freq=5.0)])],
|
|
checks=("live", "zoom", "window")),
|
|
mk("s50_trig_quant", "trigger on quantised uint16 sine 10 Hz",
|
|
[_src("src", [_sig("Sine", "float32", 1, quant="uint16", range_min=-5.0,
|
|
range_max=5.0, formula="sine", freq=10.0)])],
|
|
checks=("live", "trigger"), trig="src:Sine"),
|
|
]
|
|
|
|
# ── Misc scalar coverage (unit annotation, counter on float) ─────────────────
|
|
_MISC = [
|
|
mk("s41_f32_unit", "float32 scalar ramp with Unit=V",
|
|
[_src("src", [_sig("Ramp", "float32", 1, formula="ramp", unit="V")])]),
|
|
mk("s42_f64_counter", "float64 scalar counter (large integer values)",
|
|
[_src("src", [_sig("Cnt", "float64", 1, formula="counter")])]),
|
|
]
|
|
|
|
# ── High sample-rate / high-throughput stress ────────────────────────────────
|
|
# Eight signals, each a 1000-element float32 array carrying 1 ms worth of 1 MSps
|
|
# data, published once per 1 kHz producer cycle. Aggregate wire rate is
|
|
# 8 x 1 MSps x 4 B = 32 MB/s. A single scalar uint64 ns anchor (FirstSample) is
|
|
# shared by all eight data signals; the per-packet sample window
|
|
# (1000 / 1 MHz = 1 ms) equals the 1 kHz inter-packet gap so the expanded
|
|
# per-sample timestamps stay monotonic for the hub's binary-search ring. Ramp
|
|
# formula keeps the gate fidelity-only (robust to the UDP loss expected at rate).
|
|
#
|
|
# Packet sizing — DATA payload is 8 (ns anchor) + 8 x 1000 x 4 = 32008 B, which
|
|
# is deliberately kept under the 64 KB single-datagram ceiling. That ceiling is
|
|
# the UDPSClient reassembly buffer (UDPSReassemblySlot::payload[65535]) plus its
|
|
# offset overflow guard: a packet whose payload exceeds ~64 KB can never be
|
|
# reassembled (fragment 1 lands past the buffer and is dropped), so high-rate
|
|
# streaming must use single-datagram packets. max_payload is set to the maximum
|
|
# valid value (UDP limit - 17 B header) so the 32 KB packet is sent unfragmented.
|
|
_HIGHRATE = [
|
|
mk("s51_8x1msps_100hz",
|
|
"8x float32 10k-elem arrays @1 MSps, FirstSample, 100 Hz packets (~32 MB/s, "
|
|
"320 KB/cycle fragmented)",
|
|
[_src("src", [
|
|
_sig("Tns", "uint64", 1, unit="ns", formula="time_ns", is_time=True),
|
|
] + [
|
|
_sig(f"S{i}", "float32", 10000, time_mode="FirstSample",
|
|
time_signal="Tns", sampling_rate=1.0e6, formula="ramp")
|
|
for i in range(8)
|
|
])],
|
|
max_payload=40000, row_dt=0.01, num_rows=50, producer_hz=100,
|
|
checks=("live", "zoom")),
|
|
]
|
|
|
|
SCENARIOS = (_STARTERS + _TYPES + _ARRAYS + _TIMEMODES + _QUANT + _PUBLISH +
|
|
_PAYLOAD + _MCAST + _MULTISRC + _INTERACT + _TRIGGER + _MISC +
|
|
_HIGHRATE)
|
|
|
|
for _s in SCENARIOS:
|
|
_s.setdefault("kind", "chain")
|
|
|
|
# ── Non-chain scenario kinds ──────────────────────────────────────────────────
|
|
# "direct" and "recorder" scenarios exercise the other two data paths described
|
|
# in ARCHITECTURE.md (direct UDPStreamer<->UDPStreamerClient, and StreamHub's
|
|
# BinaryRecorder disk sink) and intentionally do not carry the chain-only keys
|
|
# (sources/network/oracle/...): they have their own orchestration in run_e2e.sh.
|
|
_DIRECT = [
|
|
{
|
|
"id": "s52_direct_unicast",
|
|
"desc": "Direct UDPStreamer->UDPStreamerClient round-trip, unicast",
|
|
"kind": "direct",
|
|
"cfg": "Test/E2E/datasources/E2ETest.cfg",
|
|
"client_checks": [],
|
|
"known_issue": None,
|
|
},
|
|
{
|
|
"id": "s53_direct_multicast",
|
|
"desc": "Direct UDPStreamer->UDPStreamerClient round-trip, multicast",
|
|
"kind": "direct",
|
|
"cfg": "Test/E2E/datasources/E2EMulticastTest.cfg",
|
|
"client_checks": [],
|
|
"known_issue": None,
|
|
},
|
|
]
|
|
|
|
_RECORDER = [
|
|
{
|
|
"id": "s54_recorder",
|
|
"desc": "StreamHub BinaryRecorder disk-output round-trip",
|
|
"kind": "recorder",
|
|
"hub_cfg": "Test/E2E/recorder/StreamHubRec.cfg",
|
|
"marte_cfg": "Test/E2E/recorder/RecorderStreamer.cfg",
|
|
"client_checks": [],
|
|
"known_issue": None,
|
|
},
|
|
]
|
|
|
|
_DEBUG = [
|
|
{
|
|
"id": "s55_debug_force_trace_break",
|
|
"desc": "DebugService FORCE/TRACE/BREAK over real TCP 8080 + UDP 8081",
|
|
"kind": "debug",
|
|
"cfg": "Test/E2E/suite/debug_e2e.cfg",
|
|
"cmd_port": 8080, "udp_port": 8081, "log_port": 9090,
|
|
"client_checks": [],
|
|
"known_issue": None,
|
|
},
|
|
]
|
|
|
|
_TCPLOGGER = [
|
|
{
|
|
"id": "s56_tcplogger_delivery",
|
|
"desc": "TCPLogger delivers a log line for a triggered DebugService event",
|
|
"kind": "tcplogger",
|
|
"cfg": "Test/E2E/suite/debug_e2e.cfg",
|
|
"cmd_port": 8080, "udp_port": 8081, "log_port": 9090,
|
|
"client_checks": [],
|
|
"known_issue": None,
|
|
},
|
|
]
|
|
|
|
SCENARIOS = SCENARIOS + _DIRECT + _RECORDER + _DEBUG + _TCPLOGGER
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
ok = True
|
|
seen_ids, seen_ws, seen_udp = set(), set(), set()
|
|
for s in SCENARIOS:
|
|
errs = validate_scenario(s)
|
|
# uniqueness invariants the orchestrator relies on
|
|
if s["id"] in seen_ids:
|
|
errs.append("duplicate id")
|
|
seen_ids.add(s["id"])
|
|
if s["kind"] == "chain":
|
|
if s["ws_port"] in seen_ws:
|
|
errs.append(f"duplicate ws_port {s['ws_port']}")
|
|
seen_ws.add(s["ws_port"])
|
|
for src in s["sources"]:
|
|
if src["udp_port"] in seen_udp:
|
|
errs.append(f"duplicate udp_port {src['udp_port']}")
|
|
seen_udp.add(src["udp_port"])
|
|
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'}")
|
|
sys.exit(0 if ok else 1)
|