diff --git a/AGENTS.md b/AGENTS.md index 1a74526..1da0914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,8 +127,6 @@ cd Test/E2E/suite && python3 -m unittest tests_py | Script | What it does | |---|---| | `./run_streamhub.sh` | Launch MARTe2 app (UDPStreamer) + StreamHub, optionally web UI (`-w`) and ImGui client (`-g`). Ports documented in its header. | -| `./run_combined_test.sh` | Combined streaming + debug integration test. `-d` auto-starts the debugger web UI. | -| `./run_e2e_test.sh` | Older standalone StreamHub E2E (`-s` skips build). | | `./Test/E2E/suite/run_e2e.sh` | **Streaming-chain E2E suite**: per-scenario generates data + cfgs, runs MARTe2+StreamHub, drives the Go `chain-client` (live/zoom/window/trigger), validates the recorded waveform against an analytic/fed oracle, runs unit suites + coverage, builds a Typst PDF report. Flags: `--skip-build`, `--only `, `--cpp-coverage`, `--pdf-only`, `--stress`. | | `./Test/E2E/suite/run_stress.sh` | **Capacity/stress harness**: sweeps one load axis at a time (signal size/count, subscriber fan-out, source count, WS-client count, zoom rate), gates on survival+liveness (hard) and RSS+zoom-p95 latency (soft). Flags: `--skip-build`, `--only `, `--axis `. | diff --git a/CLAUDE.md b/CLAUDE.md index 72213b6..47d2863 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ cd Client/streamhub && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --buil cd Client/streamhub-qt && cmake -B build && cmake --build build ``` -End-to-end demo scripts (build + launch full stack, see headers for ports/options): `./run_combined_test.sh`, `./run_streamhub.sh`. +End-to-end demo script (build + launch full stack, see header for ports/options): `./run_streamhub.sh`. **Streaming-chain E2E suite** (`Test/E2E/suite/`): `./run_e2e.sh [--skip-build] [--only ] [--cpp-coverage]` drives the full chain per scenario (`scenarios.py`) — generates typed/shaped input + both cfgs, runs MARTe2+StreamHub, records via the Go `chain-client` (live/zoom/window/trigger), and validates the recorded waveform against an analytic/fed oracle (`validate_waveform.py`: fidelity gates correctness, sine shape-fit is a gross-sanity gate + tracked metric pending Phase-A timestamp calibration). It then runs the unit suites + coverage (`collect.py`: C++ GTest, Go, Python; `--cpp-coverage` does an instrumented `--coverage` rebuild, captures with lcov restricted to `Source/*`+`Test/*`, then restores the clean build), consolidates everything into `report_data.json` with per-field progression/regression vs the previous run and trend plots (`report_build.py`, history in `Build/x86-linux/E2E/chain/history.jsonl`), and compiles a Typst PDF (`E2E_Report.typ`). Python framework unit tests: `python3 -m unittest tests_py` (in `Test/E2E/suite/`). diff --git a/Test/E2E/datasources/E2E_Report.typ b/Test/E2E/datasources/E2E_Report.typ deleted file mode 100644 index 883ff3d..0000000 --- a/Test/E2E/datasources/E2E_Report.typ +++ /dev/null @@ -1,413 +0,0 @@ -// UDPStreamerClient — E2E Test Report -// Author: Martino Ferrari -// Date: June 2026 -#set document( - title: "UDPStreamerClient — End-to-End Test Report", - author: "Martino Ferrari", - date: datetime(year: 2026, month: 6, day: 24), -) -#set page(numbering: "1 / 1", margin: (left: 2.5cm, right: 2.5cm, top: 2cm, bottom: 2cm)) -#set heading(numbering: "1.") -#set par(justify: true) -#show link: underline -#show raw.where(block: true): set block(inset: 8pt, radius: 4pt, fill: luma(240)) -#set table(stroke: 0.5pt, inset: 8pt) - -// ── Live validation data (emitted by validate_binary.py --json) ── -#let uni = json("e2e_unicast.json") -#let multi = json("e2e_multicast.json") -#let fidx(v) = if v < 0 { [—] } else { [#v] } -#let pct(n, d) = if d > 0 { [#(calc.round(100 * n / d, digits: 1))%] } else { [—] } -#let status-badge(d) = { - let c = if d.passed { green.darken(20%) } else { red.darken(10%) } - text(fill: c, weight: "bold")[#d.status] -} -// Validation metrics table for one mode's json record. -#let metrics-table(d) = table( - columns: (auto, auto, auto), - align: (left, right, left), - [*Metric*], [*Value*], [*Notes*], - [Output rows], [#d.n_rows_out], [Cycles captured by `FileWriter`], - [Matching rows], [#d.matching_rows (#pct(d.matching_rows, d.n_rows_out))], [Non-zero rows equal to an input row], - [Zero rows], [#d.zero_rows (#pct(d.zero_rows, d.n_rows_out))], [Startup transient before first `DATA`], - [Mismatching rows], [#d.mismatching_rows (#pct(d.mismatching_rows, d.n_rows_out))], [Non-zero rows matching no input → corruption], - [First matching row], [#fidx(d.first_matching_row)], [Index of first transported row], - [First zero row], [#fidx(d.first_zero_row)], [Index of first all-zero row], - [First mismatching row], [#fidx(d.first_mismatch_row)], [`—` when no corruption], - [Status], [#status-badge(d)], [#d.message], -) - -// ── Title page ── -#align(center)[ - #v(4cm) - #text(size: 28pt, weight: "bold")[UDPStreamerClient] - #v(0.5cm) - #text(size: 18pt)[End-to-End Test Report] - #v(1.5cm) - #text(size: 11pt, fill: luma(120))[ - MARTe2 Input DataSource for receiving signal data from UDPStreamer server \ - Unicast and multicast modes with event-driven thread triggering - ] - #v(3cm) - #text(size: 10pt)[Martino Ferrari — June 2026] -] -#pagebreak() - -#outline(indent: 1.5em, depth: 3) -#pagebreak() - -// ═══════════════════════════════════════ -// 1. Architecture -// ═══════════════════════════════════════ -= Architecture Overview - -== End-to-End Dataflow - -#figure( - caption: [Pipeline from binary file input to binary file output across two MARTe2 threads.], - { - set text(size: 9pt) - grid( - columns: (1fr, 1fr, 1fr, 1fr, 1fr), - rows: (auto, auto, auto, auto, auto, auto, auto, auto, auto), - gutter: 4pt, - // Header row - grid.cell(colspan: 5, align(center)[*Thread 1 — 1 kHz, CPU 0x1*]), - grid.cell(colspan: 5, align(center)[#line(length: 100%)]), - // Row 1: sources - align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`LinuxTimer`\ Counter, Time]], - align(center)[#text(fill: luma(140))[→]], - align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`FileReader`\ `Signal[10000]`]], - align(center)[], - align(center)[], - // Row 2: IOGAM - grid.cell(colspan: 5, align(center)[ - #block(fill: luma(210), inset: 6pt, radius: 4pt, width: 100%)[ - *IOGAM* `ReaderGAM` \ - _Input:_ `Counter, Time, Signal` from `DDB` + `FileReaderDS` \ - _Output:_ `Counter, Time, Signal` to `DDB` + `Streamer` - ] - ]), - grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↕ memcpy]]), - // Row 3: UDPStreamer - grid.cell(colspan: 5, align(center)[ - #block(fill: luma(200), inset: 8pt, radius: 4pt, width: 100%)[ - *UDPStreamer* (port `44600`)\ - `Synchronise()` copies `memory` → `readyBuffer` → posts `dataSem`\ - `Execute()` (background) waits on `dataSem`, serializes, sends UDP - ] - ]), - grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↓ UDP datagrams ↓]]), - // Row 4: Network - grid.cell(colspan: 5)[#block(fill: luma(235), inset: 6pt, radius: 3pt, width: 100%)[#align(center)[*Network* — localhost loopback, unicast or multicast]]], - grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↓ UDP datagrams ↓]]), - // Row 5: UDPStreamerClient - grid.cell(colspan: 5, align(center)[ - #block(fill: luma(200), inset: 8pt, radius: 4pt, width: 100%)[ - *UDPStreamerClient* (owns a shared `UDPSClient` — same receiver as the StreamHub hub)\ - `UDPSClient` background thread receives UDP, reassembles fragments, auto-reconnects,\ - then invokes `OnUDPSConfig()` / `OnUDPSData()` → decode to `scratchBuffer` → `readyBuffer`, post `dataSem`\ - `Synchronise()` (RT) blocks on `dataSem.ResetWait()` — _no `LinuxTimer` needed_ - ] - ]), - grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↕ memcpy]]), - // Row 6: IOGAM - grid.cell(colspan: 5, align(center)[ - #block(fill: luma(210), inset: 6pt, radius: 4pt, width: 100%)[ - *IOGAM* `ClientGAM` \ - _Input:_ `Signal` from `ClientDS` \ - _Output:_ `Signal` to `FileWriterDS` - ] - ]), - grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↕ async write]]), - // Row 7: FileWriter - grid.cell(colspan: 5, align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`FileWriter`\ async flush to binary file]]), - // Footer - grid.cell(colspan: 5, align(center)[#line(length: 100%)]), - grid.cell(colspan: 5, align(center)[*Thread 2 — Event-driven, CPU 0x2*]), - ) - }, -) - -== Event-Driven Thread Trigger - -Thread 2 does _not_ use a `LinuxTimer`. Execution is driven entirely by data arrival -via the `EventSem` pattern (also used by `SDNSubscriber`, `NI6368ADC`, `UARTDataSource`). -Crucially, `UDPStreamerClient` does *not* reimplement the network stack: it owns a shared -`MARTe::UDPSClient` (the very same receiver the StreamHub hub uses) and only implements the -`UDPSClientListener` callbacks. Transport, fragment reassembly, multicast join and -auto-reconnect are therefore identical to the hub by construction, with the wire format -shared through `Common/UDP/UDPSProtocol.h`. - -#enum( - numbering: "1.", - [`UDPSClient` background thread (`SingleThreadService`) receives datagrams, reassembles fragments and auto-reconnects], - [On a complete payload it invokes the listener: `OnUDPSConfig()` validates the server CONFIG against the local signals; `OnUDPSData()` decodes one snapshot], - [`OnUDPSData()` decodes (incl. dequantisation / accumulate) into a private `scratchBuffer`, then copies to `readyBuffer` under `FastPollingMutexSem`], - [Posts `EventSem dataSem` to wake the real-time thread], - [`UDPStreamerClient::Synchronise()` (RT) blocks on `dataSem.ResetWait(10 ms)`, copies `readyBuffer` to `memory`], - [GAM executes, data flows to `FileWriter`], -) - -#pagebreak() - -// ═══════════════════════════════════════ -// 2. Latency Budget -// ═══════════════════════════════════════ -= Latency Budget - -#figure( - image("latency_budget.png", width: 100%), - caption: [Estimated per-cycle latency. Total: 54 ms → ∼18 Hz max throughput. Bottlenecks: `FileWriter` async flush (50 ms) and poll sleeps (2 ms).], -) - -== Breakdown - -#table( - columns: (auto, auto, auto), - [*Stage*], [*Latency (ms)*], [*Notes*], - [`FileReader::Synchronise()`], [1.0], [Blocking read from OS buffer], - [`IOGAM` (memcpy)], [0.1], [24 KB copy (6100 float32)], - [`UDPStreamer::Synchronise()`], [1.0], [Copy `memory` → `readyBuffer` + post semaphore], - [`UDPStreamer::Execute()` (bg)], [1.0], [`Sleep::MSec(1)` poll interval], - [Network (localhost)], [0.05], [Loopback, negligible], - [`UDPSClient` receiver (bg)], [1.0], [`select()` timeout + decode in `OnUDPSData()`], - [`UDPStreamerClient::Synchronise()`], [0.01], [`ResetWait(10ms)`, copy, return], - [`IOGAM` (memcpy)], [0.1], [24 KB copy (6100 floats)], - [`FileWriter` (async flush)], [50.0], [Disk I/O, buffer count configurable], - [*Total*], [*54.3*], [*18 Hz max throughput*], -) - -== Observations - -#list( - tight: false, - [1 ms poll sleeps in both `Execute()` loops minimize software latency. Total poll overhead: 2 ms.], - [`FileWriter` async flush dominates at 50 ms; reducing `NumberOfBuffers` or using CSV format lowers this.], - [Maximum theoretical throughput with zero sleeps and sync FileWriter: ∼500 Hz (limited by 24 KB memcpy).], - [The `EventSem` pattern eliminates timer jitter — cycle rate exactly matches network data rate.], -) - -#pagebreak() - -// ═══════════════════════════════════════ -// 3. Test Results -// ═══════════════════════════════════════ -= End-to-End Test Results - -== Input Data - -Multi-signal test file with three channels of different sizes to verify -no data scrambling across UDP transport: - -#table( - columns: (auto, auto, auto, auto), - [*Signal*], [*Type*], [*Elements*], [*Value Range*], - [`Signal_100`], [`float32`], [`100`], [`(row*1000 + col) / 100.0`], - [`Signal_1K`], [`float32`], [`1000`], [`(row*500 + col) / 50.0`], - [`Signal_5K`], [`float32`], [`5000`], [`(row*200 + col) / 20.0`], -) - -Format: MARTe2 binary (42 B signal descriptor) + 6100 floats per row (24.4 KB/row). -100 rows total, 2.44 MB data. - -#figure( - image("e2e_plots.png", width: 100%), - caption: [3×3 grid: Input, the matching received Output, and their Difference per signal. The plot picks the first non-zero output row that matches an input row (skipping startup zero rows), so the near-zero Difference column confirms lossless, unscrambled transport.], -) - -== Latency Distribution - -#figure( - image("latency_histogram.png", width: 100%), - caption: [Left: End-to-end latency histogram (median 54 ms, P95 103 ms, P99 129 ms). Right: Per-component boxplot showing `FileWriter` async flush dominates the distribution.], -) - -== Unicast Test - -#table( - columns: (auto, auto), - [*Parameter*], [*Value*], - [Configuration], [`E2ETest.cfg`], - [Signals], [`3` (100 / 1000 / 5000 float32)], - [Server port], [`44600`], - [`MaxPayloadSize`], [`65507` (UDP max, no fragmentation)], - [`PublishingMode`], [`Strict`], - [Client thread], [Event-driven (no `LinuxTimer`)], -) - -#block(fill: luma(240), inset: 10pt, radius: 4pt)[ - *Status*: #status-badge(uni) --- #uni.message -] - -#metrics-table(uni) - -== Multicast Test - -#table( - columns: (auto, auto), - [*Parameter*], [*Value*], - [Configuration], [`E2EMulticastTest.cfg`], - [Signals], [`3` (100 / 1000 / 5000 float32)], - [Server], [TCP control on `44600`, UDP DATA on `239.0.0.1:44610`], - [`MaxPayloadSize`], [`65507` (UDP max, no fragmentation)], - [`PublishingMode`], [`Strict`], - [Client thread], [Event-driven (no `LinuxTimer`)], -) - -#block(fill: luma(240), inset: 10pt, radius: 4pt)[ - *Status*: #status-badge(multi) --- #multi.message -] - -#metrics-table(multi) - -== Result Interpretation - -Both transports *pass*: every non-zero output row is byte-identical to an input row -(*zero mismatching rows*), confirming the `UDPSClient`-based transport is lossless and -does not scramble the three different-sized signals -(#uni.matching_rows of #uni.n_rows_out rows matched for unicast, -#multi.matching_rows of #multi.n_rows_out for multicast). - -The only non-matching rows are the leading all-zero rows (#uni.zero_rows for unicast; -first real match at row #uni.first_matching_row). These are an expected start-up -transient: `FileWriter` begins capturing cycles the instant the application reaches -`Running`, a few cycles before the client has received its first `CONFIG` + `DATA`, -so the `MemoryDataSourceI` signal memory is still zero-initialised. Once data arrives -the output tracks the input exactly, hence *zero* mismatching rows. - -=== Pass / Fail Criteria - -`validate_binary.py` sorts every output row into exactly one bucket --- *zero* -(all-zero startup), *matching* (equals some input row) or *mismatching* (non-zero but -matches no input row) --- and fails on genuine corruption: - -#table( - columns: (auto, auto), - [*Condition*], [*Verdict*], - [Signal count / per-signal size / row size differ, or a file is unreadable/empty], [*FAIL*], - [`matching == 0` (nothing transported, incl. all-zero output)], [*FAIL*], - [`mismatching > 0` (a non-zero row matches no input row)], [*FAIL* --- corruption], - [`matching > 0`, `mismatching == 0`, with some zero rows], [*PASS* (WARN)], - [`matching == n_rows_out`], [*PASS*], -) -#pagebreak() - -// ═══════════════════════════════════════ -// 4. Implementation -// ═══════════════════════════════════════ -= Implementation Summary - -== Source Code - -#table( - columns: (auto, auto, auto), - [*File*], [*Lines*], [*Description*], - [`UDPStreamerClient.h`], [`204`], [Class + `UDPStreamerClientSignal` metadata declaration], - [`UDPStreamerClient.cpp`], [`564`], [CONFIG/DATA decode, double-buffering, `Synchronise()`], - [`Makefile.inc`], [`60`], [Includes + links `-lUDPStream`, `-lMARTe2`], - [`Makefile.gcc`], [`25`], [GCC compiler rules], - [`Makefile.cov`], [`25`], [Coverage rules], - [*Total*], [*878*], [], -) - -The transport, fragment reassembly, multicast and auto-reconnect logic is *not* counted -here: it lives in the shared `Source/Components/Interfaces/UDPStream/UDPSClient` library -that the StreamHub hub also uses, so the DataSource itself stays thin. - -== Protocol Support - -#table( - columns: (auto, auto, auto), - [*Packet*], [*Direction*], [*Status*], - [`CONNECT` (3)], [Client → Server], [✓], - [`CONFIG` (1)], [Server → Client], [✓ parse + validate], - [`DATA` (0)], [Server → Client], [✓ deserialize + dequantize + accumulate], - [`DISCONNECT` (4)], [Bidirectional], [✓], - [`ACK` (2)], [Client → Server], [✓ optional], -) - -== Features - -#table( - columns: (auto, auto), - [*Feature*], [*Status*], - [Reuses StreamHub hub code base (shared `UDPSClient`)], [✓], - [Unicast mode], [✓], - [Multicast mode (TCP control + UDP DATA join)], [✓], - [Fragment reassembly (delegated to `UDPSClient`)], [✓], - [Auto-reconnect on silence (delegated to `UDPSClient`)], [✓], - [CONFIG validation against local signals], [✓], - [Dequantization (uint8 / int8 / uint16 / int16)], [✓], - [Accumulate mode batch deserialization], [✓], - [Event-driven thread trigger (`EventSem`, no `LinuxTimer`)], [✓], - [RT-safe double buffering (`FastPollingMutexSem`)], [✓], - [`CLASS_REGISTER("1.0")`], [✓], - [`MemoryMapSynchronisedInputBroker`], [✓], - [Integrated into root `Makefile.gcc` `core`/`clean`], [✓], -) - -== Test Infrastructure - -#table( - columns: (auto, auto), - [*File*], [*Description*], - [`E2ETest.cfg`], [Unicast MARTe2 config with 3 multi-size signals], - [`E2EMulticastTest.cfg`], [Multicast MARTe2 config (`239.0.0.1:44610`)], - [`run_e2e_report.sh`], [Builds, runs unicast+multicast, validates, plots, compiles this report], - [`validate_binary.py`], [Row-bucket comparison + `--json` metrics export], - [`gen_test_data.py`], [Multi-signal binary file generator], -) - -== Build - -#block(fill: luma(235), inset: 10pt, radius: 4pt)[ -```sh -# Built as part of the library via the repo root (Interfaces/UDPStream first, -# since UDPStreamerClient links -lUDPStream): -$ make -f Makefile.gcc core - -# Or the component on its own: -$ make -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc -g++ -std=c++98 -Wall -Werror -Wno-invalid-offsetof \ - -fPIC -fno-strict-aliasing -frtti -pthread -g \ - -I. -I$ROOT/Common/UDP \ - -I$ROOT/Source/Components/Interfaces/UDPStream \ - UDPStreamerClient.cpp -o UDPStreamerClient.o -g++ -shared UDPStreamerClient.o \ - -L$ROOT/Build/x86-linux/Components/Interfaces/UDPStream -lUDPStream \ - -L$MARTe2_DIR/Build/x86-linux/Core -lMARTe2 -o UDPStreamerClient.so -``` -] - -Builds clean under `-Werror`; the DataSource reuses the hub's `UDPSClient` rather than -duplicating any socket code. - -#pagebreak() - -// ═══════════════════════════════════════ -// 5. Next Steps -// ═══════════════════════════════════════ -= Next Steps - -== Short-Term - -#list( - [*Reduce poll latency*: Lower `RECV_TIMEOUT_MS` from 10 to 1 ms. Lower `ResetWait` timeout from 1000 to 100 ms.], - [*Add GTest unit tests*: Fragment reassembly (2/5/100 fragments), dequantization accuracy, CONFIG parsing, accumulate mode.], -) - -== Medium-Term - -#list( - [*Benchmark throughput*: Measure with varying signal sizes (100 / 1K / 10K / 100K floats) and plot curve.], - [*Multicast multi-client*: Verify multiple `UDPStreamerClient` instances join same group simultaneously.], - [*Remove poll sleeps entirely*: Use continuous `select()` with zero timeout + `EventSem` back-pressure.], -) - -== Long-Term - -#list( - [*CI integration*: Add E2E test runner with automated comparison and regression detection.], - [*Performance profiling*: Identify exact memcpy and serialization costs with `perf`.], -) diff --git a/Test/E2E/datasources/run_e2e_report.sh b/Test/E2E/datasources/run_e2e_report.sh deleted file mode 100755 index 03f4ab5..0000000 --- a/Test/E2E/datasources/run_e2e_report.sh +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env bash -# run_e2e_report.sh — End-to-end test + report generation -# -# Usage: ./run_e2e_report.sh [--skip-tests] [--pdf-only] -# -# Steps: -# 1. Generate multi-signal test data -# 2. Build UDPStreamer + UDPStreamerClient -# 3. Run unicast and multicast E2E tests -# 4. Compare output against input -# 5. Generate plots (input/output/diff, latency budget, latency histogram) -# 6. Compile Typst report → PDF -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -TARGET=x86-linux -BUILD_DIR="${REPO_ROOT}/Build/${TARGET}" -# Generated artifacts (plots, copied template, PDF) go here — never in the source tree. -OUT_DIR="${BUILD_DIR}/E2E/datasources" -mkdir -p "${OUT_DIR}" - -SKIP_TESTS=0 -PDF_ONLY=0 -for arg in "$@"; do - case "$arg" in - --skip-tests) SKIP_TESTS=1 ;; - --pdf-only) PDF_ONLY=1 ;; - --help|-h) - echo "Usage: $0 [--skip-tests] [--pdf-only]" - echo " --skip-tests Skip E2E tests, only generate plots + PDF" - echo " --pdf-only Only compile Typst → PDF (requires existing plots)" - exit 0 ;; - esac -done - -# ── Load environment ───────────────────────────────────────────────────────── -ENV_SCRIPT="${REPO_ROOT}/env.sh" -if [ ! -f "${ENV_SCRIPT}" ]; then - echo "ERROR: ${ENV_SCRIPT} not found." >&2 - exit 1 -fi -source "${ENV_SCRIPT}" - -COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components" -export LD_LIBRARY_PATH="\ -${BUILD_DIR}/Components/DataSources/UDPStreamerClient:\ -${BUILD_DIR}/Components/DataSources/UDPStreamer:\ -${BUILD_DIR}/Components/Interfaces/UDPStream:\ -${MARTe2_DIR}/Build/${TARGET}/Core:\ -${COMP}/DataSources/LinuxTimer:\ -${COMP}/DataSources/LoggerDataSource:\ -${COMP}/DataSources/FileDataSource:\ -${COMP}/GAMs/IOGAM:\ -${LD_LIBRARY_PATH}" - -MARTE_APP="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex" -INPUT="/tmp/udpstreamer_test_input.bin" -OUTPUT_U="/tmp/udpstreamer_test_output.bin" -OUTPUT_M="/tmp/udpstreamer_test_output_multicast.bin" - -echo "==========================================" -echo " UDPStreamer E2E Test & Report Generator" -echo "==========================================" - -# ── Step 1-2: Generate data + build ────────────────────────────────────────── -if [ "${PDF_ONLY}" -eq 0 ]; then - echo "" - echo "── Step 1: Generating test data ──" - python3 "${SCRIPT_DIR}/gen_test_data.py" - - echo "" - echo "── Step 2: Building components ──" - make -C "${REPO_ROOT}/Source/Components/Interfaces/UDPStream" \ - -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2 - make -C "${REPO_ROOT}/Source/Components/DataSources/UDPStreamerClient" \ - -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2 - make -C "${REPO_ROOT}/Source/Components/DataSources/UDPStreamer" \ - -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2 -fi - -# ── Step 3: Run E2E tests ──────────────────────────────────────────────────── -run_test() { - local name="$1" cfg="$2" output="$3" - echo "" - echo "── Test: ${name} ──" - rm -f "${output}" - if [ ! -x "${MARTE_APP}" ]; then - echo " SKIP: MARTeApp.ex not found"; return 0 - fi - timeout 6 "${MARTE_APP}" -l RealTimeLoader -f "${cfg}" -s Running 2>&1 | grep -E "^\[" > /tmp/e2e_log_${name}.txt & - local pid=$! - sleep 5 - kill "${pid}" 2>/dev/null || true - wait "${pid}" 2>/dev/null || true - # Show log messages (reader/client first-element values) - grep -E "Log100_|Log1K_|Log5K_" /tmp/e2e_log_${name}.txt 2>/dev/null | head -20 || true - echo " Done." -} - -if [ "${SKIP_TESTS}" -eq 0 ] && [ "${PDF_ONLY}" -eq 0 ]; then - echo "" - echo "── Step 3: Running E2E tests ──" - run_test "Unicast" "${SCRIPT_DIR}/E2ETest.cfg" "${OUTPUT_U}" - run_test "Multicast" "${SCRIPT_DIR}/E2EMulticastTest.cfg" "${OUTPUT_M}" - - # ── Step 3b: Validate ── - echo "" - echo "── Results ──" - RESULTS="${OUT_DIR}/e2e_results.txt" - : > "${RESULTS}" - for label in unicast multicast; do - [ "$label" = "unicast" ] && out="${OUTPUT_U}" || out="${OUTPUT_M}" - python3 "${SCRIPT_DIR}/validate_binary.py" "${INPUT}" "${out}" --label "${label}" \ - --json "${OUT_DIR}/e2e_${label}.json" 2>&1 | tee -a "${RESULTS}" || true - done - echo " Results saved to ${RESULTS} (+ e2e_unicast.json, e2e_multicast.json)" -fi - -# ── Step 4: Generate plots ─────────────────────────────────────────────────── -echo "" -echo "── Step 4: Generating plots ──" -cd "${OUT_DIR}" - -python3 << 'PLOT_EOF' -import struct, os, numpy as np -import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt -from matplotlib.gridspec import GridSpec - -INPUT="/tmp/udpstreamer_test_input.bin" -OUTPUT_U="/tmp/udpstreamer_test_output.bin" - -def read_binary(fn): - if not os.path.exists(fn): return None,None - with open(fn,'rb') as f: - ns=struct.unpack('=1 else f'{l*1000:.0f} µs',va='center',fontsize=9,fontweight='bold') - ax.text(0.2,b.get_y()+b.get_height()/2,comps[len(comps)-int(b.get_y()+b.get_height())],va='center',fontsize=8,color='white',fontweight='bold') -ax.set_xlabel('Latency (ms)',fontsize=11) -ax.set_title('UDPStreamer E2E Latency Budget',fontsize=12,fontweight='bold') -t=sum(lats) -ax.text(0.15,-0.4,f'Total: {t:.1f} ms | Max throughput: {1000/t:.0f} Hz',fontsize=11,fontweight='bold',transform=ax.get_xaxis_transform()) -plt.tight_layout(); plt.savefig('latency_budget.png',dpi=150); plt.close() -print(' ✓ latency_budget.png') -print(' All plots generated.') -PLOT_EOF - -# ── Step 5: Compile Typst → PDF (optional) ────────────────────────────────── -echo "" -echo "── Step 5: Compiling Typst report ──" -if [ ! -f "${SCRIPT_DIR}/E2E_Report.typ" ]; then - echo " SKIP: E2E_Report.typ template not present." -elif ! command -v typst >/dev/null 2>&1; then - echo " SKIP: typst not installed." -else - # Compile from the build dir so the template's relative image() paths - # resolve against the freshly generated PNGs; keep the source .typ pristine. - cp "${SCRIPT_DIR}/E2E_Report.typ" "${OUT_DIR}/E2E_Report.typ" - typst compile "${OUT_DIR}/E2E_Report.typ" "${OUT_DIR}/E2E_Report.pdf" 2>&1 - if [ -f "${OUT_DIR}/E2E_Report.pdf" ]; then - SIZE=$(ls -lh "${OUT_DIR}/E2E_Report.pdf" | awk '{print $5}') - echo " ✓ Report generated: ${OUT_DIR}/E2E_Report.pdf (${SIZE})" - else - echo " ✗ Typst compilation failed" - fi -fi - -echo "" -echo "==========================================" -echo " Done — artifacts in ${OUT_DIR}" -echo "==========================================" diff --git a/Test/E2E/recorder/run_recorder_e2e.sh b/Test/E2E/recorder/run_recorder_e2e.sh deleted file mode 100755 index ee90d60..0000000 --- a/Test/E2E/recorder/run_recorder_e2e.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -# run_recorder_e2e.sh — End-to-end test for the StreamHub binary recorder. -# -# Data path: -# FileReader(/tmp/udpstreamer_test_input.bin) -# -> IOGAM -> UDPStreamer(:44600) [MARTe2 app, separate process] -# -> UDPS -> StreamHub UDPSClient -# -> BinaryRecorder -> /tmp/streamhub_rec_e2e/e2e_*.bin -# -# The recorder writes FileWriter-compatible binary files for un-quantized -# float32 signals, so each recorded row is byte-identical to a streamed row. -# validate_binary.py confirms every non-zero recorded row matches an input row. -# -# Usage: ./run_recorder_e2e.sh [--skip-build] -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -TARGET=x86-linux -BUILD_DIR="${REPO_ROOT}/Build/${TARGET}" -OUT_DIR="${BUILD_DIR}/E2E/recorder" -mkdir -p "${OUT_DIR}" - -VALIDATOR="${SCRIPT_DIR}/../datasources/validate_binary.py" -GEN_DATA="${SCRIPT_DIR}/../datasources/gen_test_data.py" -INPUT="/tmp/udpstreamer_test_input.bin" -REC_DIR="/tmp/streamhub_rec_e2e" - -SKIP_BUILD=0 -for arg in "$@"; do - case "$arg" in - --skip-build) SKIP_BUILD=1 ;; - --help|-h) echo "Usage: $0 [--skip-build]"; exit 0 ;; - esac -done - -# ── Load environment ───────────────────────────────────────────────────────── -ENV_SCRIPT="${REPO_ROOT}/env.sh" -if [ ! -f "${ENV_SCRIPT}" ]; then - echo "ERROR: ${ENV_SCRIPT} not found." >&2 - exit 1 -fi -source "${ENV_SCRIPT}" - -COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components" -export LD_LIBRARY_PATH="\ -${BUILD_DIR}/Components/DataSources/UDPStreamer:\ -${BUILD_DIR}/Components/Interfaces/UDPStream:\ -${MARTe2_DIR}/Build/${TARGET}/Core:\ -${COMP}/DataSources/LinuxTimer:\ -${COMP}/DataSources/FileDataSource:\ -${COMP}/GAMs/IOGAM:\ -${LD_LIBRARY_PATH:-}" - -MARTE_APP="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex" -STREAMHUB_EX="${BUILD_DIR}/StreamHub/StreamHub.ex" - -echo "==========================================" -echo " StreamHub Recorder E2E Test" -echo "==========================================" - -# ── Step 1: Generate test data ─────────────────────────────────────────────── -echo "" -echo "── Step 1: Generating test data ──" -python3 "${GEN_DATA}" - -# ── Step 2: Build components ────────────────────────────────────────────────── -if [ "${SKIP_BUILD}" -eq 0 ]; then - echo "" - echo "── Step 2: Building components ──" - make -C "${REPO_ROOT}/Source/Components/Interfaces/UDPStream" \ - -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2 - make -C "${REPO_ROOT}/Source/Components/DataSources/UDPStreamer" \ - -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2 - make -C "${REPO_ROOT}/Source/Applications/StreamHub" \ - -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -2 -fi - -if [ ! -x "${MARTE_APP}" ]; then - echo "ERROR: MARTeApp.ex not found at ${MARTE_APP}" >&2 - exit 1 -fi -if [ ! -x "${STREAMHUB_EX}" ]; then - echo "ERROR: StreamHub.ex not found at ${STREAMHUB_EX}" >&2 - exit 1 -fi - -# ── Step 3: Run the stack ───────────────────────────────────────────────────── -echo "" -echo "── Step 3: Running StreamHub + UDPStreamer ──" -rm -rf "${REC_DIR}" -mkdir -p "${REC_DIR}" - -HUB_LOG="${OUT_DIR}/streamhub.log" -APP_LOG="${OUT_DIR}/marte.log" - -# Start StreamHub first so it is ready to receive the CONFIG packet. -"${STREAMHUB_EX}" -cfg "${SCRIPT_DIR}/StreamHubRec.cfg" > "${HUB_LOG}" 2>&1 & -HUB_PID=$! -sleep 1 - -cleanup() { - kill "${HUB_PID}" 2>/dev/null || true - kill "${APP_PID}" 2>/dev/null || true - wait "${HUB_PID}" 2>/dev/null || true - wait "${APP_PID}" 2>/dev/null || true -} -trap cleanup EXIT - -timeout 8 "${MARTE_APP}" -l RealTimeLoader -f "${SCRIPT_DIR}/RecorderStreamer.cfg" \ - -s Running > "${APP_LOG}" 2>&1 & -APP_PID=$! - -# Let data flow, then stop the streamer and give the push thread time to flush. -sleep 7 -kill "${APP_PID}" 2>/dev/null || true -wait "${APP_PID}" 2>/dev/null || true -sleep 2 -kill "${HUB_PID}" 2>/dev/null || true -wait "${HUB_PID}" 2>/dev/null || true -trap - EXIT - -echo " Done. StreamHub log: ${HUB_LOG}" - -# ── Step 4: Validate the recorded file ─────────────────────────────────────── -echo "" -echo "── Step 4: Validating recorded output ──" -# The recorder names files __.bin; pick the newest. -REC_FILE="$(ls -t "${REC_DIR}"/*.bin 2>/dev/null | head -1 || true)" -if [ -z "${REC_FILE}" ]; then - echo " ✗ FAIL: no recorded .bin file in ${REC_DIR}" - echo " --- StreamHub log tail ---" - tail -20 "${HUB_LOG}" || true - exit 1 -fi -echo " Recorded file: ${REC_FILE} ($(stat -c%s "${REC_FILE}") B)" - -python3 "${VALIDATOR}" "${INPUT}" "${REC_FILE}" --label "recorder" \ - --json "${OUT_DIR}/recorder_e2e.json" - -echo "" -echo "==========================================" -echo " Done — artifacts in ${OUT_DIR}" -echo "==========================================" diff --git a/Test/E2E/streamhub/go.mod b/Test/E2E/streamhub/go.mod deleted file mode 100644 index 8d8d160..0000000 --- a/Test/E2E/streamhub/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module streamhub-e2e - -go 1.21 - -require github.com/gorilla/websocket v1.5.1 - -require golang.org/x/net v0.17.0 // indirect diff --git a/Test/E2E/streamhub/go.sum b/Test/E2E/streamhub/go.sum deleted file mode 100644 index 272772f..0000000 --- a/Test/E2E/streamhub/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= diff --git a/Test/E2E/streamhub/main.go b/Test/E2E/streamhub/main.go deleted file mode 100644 index 082a4fe..0000000 --- a/Test/E2E/streamhub/main.go +++ /dev/null @@ -1,561 +0,0 @@ -// Command streamhub-e2e is an end-to-end test client for the C++ StreamHub. -// -// It connects to a running StreamHub WebSocket endpoint (with at least one -// connected UDPStreamer source, e.g. the stack launched by run_e2e_test.sh) -// and verifies the full protocol: -// -// 1. "sources" event with at least one connected source -// 2. "config" event per source with at least one signal -// 3. binary v1 data pushes: parseable, per-signal monotonic time, -// timestamps within a few seconds of wall clock (Unix time base) -// 4. "stats" event with a positive receive rate -// 5. WS zoom round-trip: reqId echoed, points returned in [t0,t1] -// 6. hub-side trigger: setTrigger+arm → triggerState(armed) → binary v2 -// capture frame with the latched pre/post window -// -// Exit code 0 on success; 1 with a FAIL message otherwise. -package main - -import ( - "encoding/binary" - "encoding/json" - "flag" - "fmt" - "log" - "math" - "os" - "time" - - "github.com/gorilla/websocket" -) - -var hub = flag.String("hub", "127.0.0.1:8090", "StreamHub host:port") -var timeout = flag.Duration("timeout", 30*time.Second, "overall test timeout") -var verbose = flag.Bool("v", false, "log every received event") - -// --------------------------------------------------------------------------- -// Wire types (subset of the StreamHub JSON protocol) -// --------------------------------------------------------------------------- - -type sourceInfo struct { - ID string `json:"id"` - Label string `json:"label"` - Addr string `json:"addr"` - State string `json:"state"` -} - -type signalInfo struct { - Name string `json:"name"` - TypeCode uint32 `json:"typeCode"` - NumRows uint32 `json:"numRows"` - NumCols uint32 `json:"numCols"` - TimeMode int `json:"timeMode"` - Rate float64 `json:"samplingRate"` -} - -type statInfo struct { - State string `json:"state"` - TotalReceived uint64 `json:"totalReceived"` - RateHz float64 `json:"rateHz"` - CycleHist []f64 `json:"cycleHist"` -} - -type f64 = float64 - -type zoomPoints struct { - T []float64 `json:"t"` - V []float64 `json:"v"` -} - -type historyInfoMsg struct { - Enabled bool `json:"enabled"` - DurationHours float64 `json:"durationHours"` - Decimation uint32 `json:"decimation"` - Signals map[string]struct { - T0 float64 `json:"t0"` - T1 float64 `json:"t1"` - Count uint32 `json:"count"` - Capacity uint32 `json:"capacity"` - } `json:"signals"` -} - -type event struct { - Type string `json:"type"` - Sources json.RawMessage `json:"sources"` - SourceID string `json:"sourceId"` - Signals json.RawMessage `json:"signals"` - ReqID uint32 `json:"reqId"` - State string `json:"state"` - TrigTime float64 `json:"trigTime"` -} - -// Parsed binary v1 push frame: sourceId → signal → samples. -type pushFrame struct { - sourceID string - signals map[string]zoomPoints -} - -// Parsed binary v2 capture frame. -type captureFrame struct { - trigTime, preSec, postSec float64 - signals map[string]zoomPoints -} - -// --------------------------------------------------------------------------- -// Binary parsers -// --------------------------------------------------------------------------- - -func parsePush(b []byte) (*pushFrame, error) { - if len(b) < 2 || b[0] != 1 { - return nil, fmt.Errorf("not a v1 frame") - } - idLen := int(b[1]) - off := 2 - if len(b) < off+idLen+4 { - return nil, fmt.Errorf("truncated header") - } - f := &pushFrame{sourceID: string(b[off : off+idLen]), - signals: map[string]zoomPoints{}} - off += idLen - nSig := int(binary.LittleEndian.Uint32(b[off:])) - off += 4 - for s := 0; s < nSig; s++ { - if len(b) < off+2 { - return nil, fmt.Errorf("truncated keyLen (sig %d)", s) - } - keyLen := int(binary.LittleEndian.Uint16(b[off:])) - off += 2 - if len(b) < off+keyLen+4 { - return nil, fmt.Errorf("truncated key (sig %d)", s) - } - key := string(b[off : off+keyLen]) - off += keyLen - n := int(binary.LittleEndian.Uint32(b[off:])) - off += 4 - if len(b) < off+16*n { - return nil, fmt.Errorf("truncated data (sig %s n=%d)", key, n) - } - pts := zoomPoints{T: make([]float64, n), V: make([]float64, n)} - for i := 0; i < n; i++ { - pts.T[i] = math.Float64frombits(binary.LittleEndian.Uint64(b[off+8*i:])) - } - off += 8 * n - for i := 0; i < n; i++ { - pts.V[i] = math.Float64frombits(binary.LittleEndian.Uint64(b[off+8*i:])) - } - off += 8 * n - f.signals[key] = pts - } - return f, nil -} - -func parseCapture(b []byte) (*captureFrame, error) { - if len(b) < 1+24+4 || b[0] != 2 { - return nil, fmt.Errorf("not a v2 frame") - } - rdF64 := func(off int) float64 { - return math.Float64frombits(binary.LittleEndian.Uint64(b[off:])) - } - f := &captureFrame{ - trigTime: rdF64(1), preSec: rdF64(9), postSec: rdF64(17), - signals: map[string]zoomPoints{}, - } - off := 25 - nSig := int(binary.LittleEndian.Uint32(b[off:])) - off += 4 - for s := 0; s < nSig; s++ { - keyLen := int(binary.LittleEndian.Uint16(b[off:])) - off += 2 - key := string(b[off : off+keyLen]) - off += keyLen - n := int(binary.LittleEndian.Uint32(b[off:])) - off += 4 - if len(b) < off+16*n { - return nil, fmt.Errorf("truncated capture (sig %s n=%d)", key, n) - } - pts := zoomPoints{T: make([]float64, n), V: make([]float64, n)} - for i := 0; i < n; i++ { - pts.T[i] = math.Float64frombits(binary.LittleEndian.Uint64(b[off+8*i:])) - } - off += 8 * n - for i := 0; i < n; i++ { - pts.V[i] = math.Float64frombits(binary.LittleEndian.Uint64(b[off+8*i:])) - } - off += 8 * n - f.signals[key] = pts - } - return f, nil -} - -// --------------------------------------------------------------------------- -// Test driver -// --------------------------------------------------------------------------- - -type client struct { - ws *websocket.Conn - deadline time.Time - - sources []sourceInfo - configs map[string][]signalInfo // sourceId → signals - pushes []*pushFrame - stats map[string]statInfo - zooms map[uint32]map[string]zoomPoints - histZooms map[uint32]map[string]zoomPoints - historyInfo *historyInfoMsg - trigSt []string // observed triggerState sequence - captures []*captureFrame -} - -func (c *client) send(v interface{}) { - b, _ := json.Marshal(v) - if err := c.ws.WriteMessage(websocket.TextMessage, b); err != nil { - fail("ws write: %v", err) - } -} - -// pump reads one WS message (with a short read deadline) and dispatches it. -func (c *client) pump() { - c.ws.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) - mt, data, err := c.ws.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err) { - fail("ws closed: %v", err) - } - return // read timeout — fine - } - switch mt { - case websocket.BinaryMessage: - if len(data) == 0 { - return - } - switch data[0] { - case 1: - if f, err := parsePush(data); err == nil { - c.pushes = append(c.pushes, f) - } else { - fail("bad v1 frame: %v", err) - } - case 2: - if f, err := parseCapture(data); err == nil { - c.captures = append(c.captures, f) - } else { - fail("bad v2 frame: %v", err) - } - default: - fail("unknown binary frame version %d", data[0]) - } - case websocket.TextMessage: - var ev event - if err := json.Unmarshal(data, &ev); err != nil { - fail("bad JSON event: %v (%.120s)", err, data) - } - if *verbose { - log.Printf("event %-12s %.160s", ev.Type, data) - } - switch ev.Type { - case "sources": - var srcs []sourceInfo - if err := json.Unmarshal(ev.Sources, &srcs); err == nil { - c.sources = srcs - } - case "config": - var sigs []signalInfo - if err := json.Unmarshal(ev.Signals, &sigs); err == nil { - c.configs[ev.SourceID] = sigs - } else { - log.Printf("config parse error: %v (%.200s)", err, data) - } - case "stats": - var st map[string]statInfo - if err := json.Unmarshal(ev.Sources, &st); err == nil { - c.stats = st - } - case "zoom": - var body struct { - Signals map[string]zoomPoints `json:"signals"` - } - if err := json.Unmarshal(data, &body); err == nil { - c.zooms[ev.ReqID] = body.Signals - } - case "historyZoom": - var body struct { - Signals map[string]zoomPoints `json:"signals"` - } - if err := json.Unmarshal(data, &body); err == nil { - c.histZooms[ev.ReqID] = body.Signals - } - case "historyInfo": - var hi historyInfoMsg - if err := json.Unmarshal(data, &hi); err == nil { - c.historyInfo = &hi - } - case "triggerState": - c.trigSt = append(c.trigSt, ev.State) - } - } -} - -// waitFor pumps messages until cond() or the step deadline expires. -func (c *client) waitFor(what string, d time.Duration, cond func() bool) { - end := time.Now().Add(d) - if end.After(c.deadline) { - end = c.deadline - } - for time.Now().Before(end) { - if cond() { - log.Printf("OK %s", what) - return - } - c.pump() - } - fail("timeout waiting for %s", what) -} - -func fail(format string, args ...interface{}) { - fmt.Printf("FAIL "+format+"\n", args...) - os.Exit(1) -} - -func main() { - flag.Parse() - url := "ws://" + *hub + "/ws" - log.Printf("connecting to %s", url) - - ws, _, err := websocket.DefaultDialer.Dial(url, nil) - if err != nil { - fail("dial %s: %v", url, err) - } - defer ws.Close() - - c := &client{ - ws: ws, - deadline: time.Now().Add(*timeout), - configs: map[string][]signalInfo{}, - zooms: map[uint32]map[string]zoomPoints{}, - histZooms: map[uint32]map[string]zoomPoints{}, - } - - // ── 1. sources ──────────────────────────────────────────────────────── - c.send(map[string]interface{}{"type": "getSources"}) - c.waitFor("sources event with a connected source", 10*time.Second, func() bool { - for _, s := range c.sources { - if s.State == "connected" { - return true - } - } - return false - }) - - // ── 2. config per connected source ─────────────────────────────────── - for _, s := range c.sources { - log.Printf("source %s (%s): state=%s", s.ID, s.Label, s.State) - c.send(map[string]interface{}{"type": "getConfig", "sourceId": s.ID}) - } - c.waitFor("config with signals for every connected source", 10*time.Second, func() bool { - for _, s := range c.sources { - if s.State != "connected" { - continue - } - if len(c.configs[s.ID]) == 0 { - return false - } - } - return len(c.sources) > 0 - }) - - // ── 3. binary pushes: wall-clock time base + monotonicity ──────────── - c.waitFor("binary v1 data pushes (>=10 frames)", 10*time.Second, func() bool { - return len(c.pushes) >= 10 - }) - now := float64(time.Now().UnixNano()) / 1e9 - seen := map[string][]float64{} // last times per src:sig - for _, f := range c.pushes { - for key, pts := range f.signals { - full := f.sourceID + ":" + key - for i, t := range pts.T { - if math.Abs(t-now) > 30.0 { - fail("timestamp not wall-clock: %s t=%.3f now=%.3f", full, t, now) - } - prev := seen[full] - if len(prev) > 0 && t < prev[len(prev)-1]-1e-9 { - fail("non-monotonic time on %s: %.9f after %.9f (i=%d)", - full, t, prev[len(prev)-1], i) - } - seen[full] = append(seen[full], t) - } - } - } - if len(seen) == 0 { - fail("pushes contained no signal data") - } - log.Printf("OK wall-clock & monotonic time on %d signal streams", len(seen)) - - // ── 4. stats ────────────────────────────────────────────────────────── - c.send(map[string]interface{}{"type": "getStats"}) - c.waitFor("stats with positive rate", 10*time.Second, func() bool { - for _, st := range c.stats { - if st.State == "connected" && st.RateHz > 0 && st.TotalReceived > 0 { - return true - } - } - return false - }) - - // ── 5. zoom round-trip ─────────────────────────────────────────────── - // Use the busiest streamed signal and the time range we actually saw. - var zoomKey string - var zMax int - for k, ts := range seen { - if len(ts) > zMax { - zMax, zoomKey = len(ts), k - } - } - ts := seen[zoomKey] - t1 := ts[len(ts)-1] - t0 := t1 - 0.5 - const reqID = 4242 - c.send(map[string]interface{}{ - "type": "zoom", "reqId": reqID, "t0": t0, "t1": t1, "n": 200, - "signals": zoomKey, - }) - c.waitFor(fmt.Sprintf("zoom reply (reqId=%d, %s)", reqID, zoomKey), - 10*time.Second, func() bool { - sigs, ok := c.zooms[reqID] - if !ok { - return false - } - pts, ok := sigs[zoomKey] - if !ok || len(pts.T) < 2 { - fail("zoom reply missing %s (got %d signals)", zoomKey, len(sigs)) - } - for _, t := range pts.T { - if t < t0-1e-6 || t > t1+1e-6 { - fail("zoom point outside range: t=%.9f not in [%.9f,%.9f]", t, t0, t1) - } - } - return true - }) - - // ── 5b. historyInfo — check the hub broadcast it on connect ───────── - if c.historyInfo != nil && c.historyInfo.Enabled { - log.Printf("OK historyInfo: enabled, %.1fh, decimation=%d, %d signals", - c.historyInfo.DurationHours, c.historyInfo.Decimation, - len(c.historyInfo.Signals)) - - // ── 5c. historyZoom round-trip ────────────────────────────────── - const hReqID = 4243 - c.send(map[string]interface{}{ - "type": "historyZoom", "reqId": hReqID, - "t0": t0, "t1": t1, "n": 200, - "signals": zoomKey, - }) - c.waitFor(fmt.Sprintf("historyZoom reply (reqId=%d, %s)", hReqID, zoomKey), - 10*time.Second, func() bool { - sigs, ok := c.histZooms[hReqID] - if !ok { - return false - } - pts, ok := sigs[zoomKey] - if !ok || len(pts.T) < 1 { - // History data may still be sparse right after startup - return true - } - for _, ht := range pts.T { - if ht < t0-1e-6 || ht > t1+1e-6 { - fail("historyZoom point outside range: t=%.9f not in [%.9f,%.9f]", ht, t0, t1) - } - } - return true - }) - } else { - log.Println(" (history not enabled — skipping historyZoom test)") - } - - // ── 6. trigger: arm → capture ──────────────────────────────────────── - // Trigger on an *oscillating* signal at its mean observed value: a - // monotonic ramp (counter, time array) crosses its past mean only once, - // before arming, so a rising edge would never fire on it. Pick the - // busiest signal whose last push frame is non-monotonic (a sine). - lastVals := map[string][]float64{} - for _, f := range c.pushes { - for name, pts := range f.signals { - if len(pts.V) >= 4 { - lastVals[f.sourceID+":"+name] = pts.V - } - } - } - trigKey := "" - tMaxPts := 0 - for k, vs := range lastVals { - monotonic := true - for i := 1; i < len(vs); i++ { - if vs[i] < vs[i-1] { - monotonic = false - break - } - } - if !monotonic && len(seen[k]) > tMaxPts { - tMaxPts, trigKey = len(seen[k]), k - } - } - if trigKey == "" { - fail("no oscillating signal found for trigger test") - } - vals := lastVals[trigKey] - mean := 0.0 - for _, v := range vals { - mean += v - } - mean /= float64(len(vals)) - log.Printf(" trigger signal %s, threshold %.6g", trigKey, mean) - - c.send(map[string]interface{}{ - "type": "setTrigger", "signal": trigKey, "edge": "rising", - "threshold": mean, "windowSec": 0.1, "prePercent": 20.0, - "mode": "single", - }) - c.send(map[string]interface{}{"type": "arm"}) - // The trigger can fire within microseconds of arming (5 MS/s sine), so - // the broadcast emitted by the arm command may already say "collecting" - // or even "triggered" — any of these proves the arm was accepted. - c.waitFor("triggerState: armed/collecting/triggered", 5*time.Second, func() bool { - for _, s := range c.trigSt { - if s == "armed" || s == "collecting" || s == "triggered" { - return true - } - } - return false - }) - c.waitFor("binary v2 capture frame", 15*time.Second, func() bool { - return len(c.captures) > 0 - }) - - cap0 := c.captures[0] - if math.Abs(cap0.preSec-0.02) > 1e-9 || math.Abs(cap0.postSec-0.08) > 1e-9 { - fail("capture window mismatch: pre=%.6f post=%.6f (want 0.02/0.08)", - cap0.preSec, cap0.postSec) - } - pts, ok := cap0.signals[trigKey] - if !ok || len(pts.T) == 0 { - fail("capture missing trigger signal %s (%d signals)", trigKey, len(cap0.signals)) - } - for _, t := range pts.T { - if t < cap0.trigTime-cap0.preSec-1e-3 || t > cap0.trigTime+cap0.postSec+1e-3 { - fail("capture point outside window: t=%.9f trig=%.9f", t, cap0.trigTime) - } - } - log.Printf("OK capture: trig=%.6f pre=%.3fs post=%.3fs %d signals", - cap0.trigTime, cap0.preSec, cap0.postSec, len(cap0.signals)) - - c.waitFor("triggerState: triggered", 5*time.Second, func() bool { - for _, s := range c.trigSt { - if s == "triggered" { - return true - } - } - return false - }) - c.send(map[string]interface{}{"type": "disarm"}) - - fmt.Println("PASS streamhub-e2e: all checks passed") -} diff --git a/Test/E2E/streamhub/streamhub-e2e b/Test/E2E/streamhub/streamhub-e2e deleted file mode 100755 index ca78207..0000000 Binary files a/Test/E2E/streamhub/streamhub-e2e and /dev/null differ diff --git a/run_combined_test.sh b/run_combined_test.sh deleted file mode 100755 index 4aeb2e5..0000000 --- a/run_combined_test.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env bash -# run_combined_test.sh — launch the combined integration test -# -# Usage: -# ./run_combined_test.sh [OPTIONS] -# -# Options: -# -m Override MARTe2 installation dir (default: $MARTe2_DIR) -# -c Override MARTe2-components dir -# -b Build target (default: x86-linux) -# -s Skip building — run with whatever is already built -# -d Start debugger web UI (marte2debugger) automatically -# -h Show this help -# -# Ports used: -# 44500/tcp+udp UDPStreamer1 control + data (scalar signals) -# 44501/tcp UDPStreamer2 control (packed arrays FirstSample/LastSample) -# 44502/tcp UDPStreamer3 control (packed arrays FullArray) -# 44503/udp UDPStreamer1 multicast data (239.0.0.1) -# 8080/tcp DebugService control -# 8081/udp DebugService UDPS stream -# 8082/tcp DebugService log -# 9090/tcp TCPLogger -# 7777/tcp Debugger web UI (if -d is given) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CFG="${SCRIPT_DIR}/Test/Configurations/combined_test.cfg" - -BUILD_TARGET="${TARGET:-x86-linux}" -SKIP_BUILD=0 -START_UI=0 - -# ── Parse arguments ─────────────────────────────────────────────────────────── -while getopts "m:c:b:sdh" opt; do - case "$opt" in - m) MARTe2_DIR="$OPTARG" ;; - c) MARTe2_Components_DIR="$OPTARG" ;; - b) BUILD_TARGET="$OPTARG" ;; - s) SKIP_BUILD=1 ;; - d) START_UI=1 ;; - h) - sed -n '2,40p' "$0" | grep '^#' | sed 's/^# \?//' - exit 0 - ;; - *) echo "Unknown option: -$OPTARG" >&2; exit 1 ;; - esac -done - -# ── Validate environment ────────────────────────────────────────────────────── -if [[ -z "${MARTe2_DIR:-}" ]]; then - echo "ERROR: MARTe2_DIR is not set. Source env.sh first or pass -m ." - echo " source ${SCRIPT_DIR}/env.sh" - exit 1 -fi - -if [[ -z "${MARTe2_Components_DIR:-}" ]]; then - echo "ERROR: MARTe2_Components_DIR is not set. Source env.sh first or pass -c ." - exit 1 -fi - -MARTE2_BIN="${MARTe2_DIR}/Build/${BUILD_TARGET}/App/MARTeApp.ex" -if [[ ! -x "$MARTE2_BIN" ]]; then - # Some builds use MARTe2.sh as the launcher wrapper - MARTE2_BIN="${MARTe2_DIR}/Build/${BUILD_TARGET}/App/MARTe2.sh" -fi -if [[ ! -x "$MARTE2_BIN" ]]; then - echo "ERROR: MARTe2 executable not found at ${MARTe2_DIR}/Build/${BUILD_TARGET}/App/" - exit 1 -fi - -# ── Build ───────────────────────────────────────────────────────────────────── -if [[ "$SKIP_BUILD" -eq 0 ]]; then - echo "==> Building components (TARGET=${BUILD_TARGET})..." - make -C "${SCRIPT_DIR}" -f Makefile.gcc TARGET="${BUILD_TARGET}" 2>&1 | tail -20 - echo "==> Build done." -fi - -# ── Library path ───────────────────────────────────────────────────────────── -BUILD_DIR="${SCRIPT_DIR}/Build/${BUILD_TARGET}" - -export LD_LIBRARY_PATH="\ -${MARTe2_DIR}/Build/${BUILD_TARGET}/Core:\ -${MARTe2_Components_DIR}/Build/${BUILD_TARGET}/Components/DataSources/LinuxTimer:\ -${MARTe2_Components_DIR}/Build/${BUILD_TARGET}/Components/DataSources/LoggerDataSource:\ -${MARTe2_Components_DIR}/Build/${BUILD_TARGET}/Components/GAMs/IOGAM:\ -${MARTe2_Components_DIR}/Build/${BUILD_TARGET}/Components/GAMs/WaveformGAM:\ -${BUILD_DIR}/Components/DataSources/UDPStreamer:\ -${BUILD_DIR}/Components/GAMs/SineArrayGAM:\ -${BUILD_DIR}/Components/GAMs/TimeArrayGAM:\ -${BUILD_DIR}/Components/Interfaces/DebugService:\ -${BUILD_DIR}/Components/Interfaces/TCPLogger:\ -${LD_LIBRARY_PATH:-}" - -# ── Loader parameters ───────────────────────────────────────────────────────── -LOADER_PARAMS=( - -l RealTimeLoader - -f "${CFG}" - -s Running - -m StateMachine:START -) - -# ── Optionally start the web UI ─────────────────────────────────────────────── -UI_PID="" -if [[ "$START_UI" -eq 1 ]]; then - UI_BIN="${SCRIPT_DIR}/Client/debugger/marte2debugger" - echo "==> Building debugger web UI..." - (cd "${SCRIPT_DIR}/Client/debugger" && go build -o marte2debugger .) - # Check if port 7777 is already in use - if ss -tlnH sport = :7777 2>/dev/null | grep -q 7777; then - echo "==> Debugger web UI already running on http://localhost:7777" - else - echo "==> Starting debugger web UI on http://localhost:7777 ..." - "${UI_BIN}" -addr :7777 & - UI_PID="$!" - sleep 0.5 - fi - echo " Open http://localhost:7777 in your browser." - echo " In the connection panel set:" - echo " Host=127.0.0.1 TCP=8080 UDP=8081 Log=9090" - echo "" -fi - -# ── Cleanup handler ─────────────────────────────────────────────────────────── -cleanup() { - echo "" - echo "==> Shutting down..." - if [[ -n "$UI_PID" ]] && kill -0 "$UI_PID" 2>/dev/null; then - kill "$UI_PID" - fi -} -trap cleanup EXIT INT TERM - -# ── Launch MARTe2 ───────────────────────────────────────────────────────────── -echo "==> Launching MARTe2..." -echo " Config : ${CFG}" -echo " State : Running" -echo " Streams: UDPStreamer1=44500 UDPStreamer2=44501 UDPStreamer3=44502" -echo " Debug : TCP=8080 UDP=8081 Log=9090 (TcpLogger auto-injected)" -echo "" -echo " Press Ctrl-C to stop." -echo "" - -exec "${MARTE2_BIN}" "${LOADER_PARAMS[@]}" diff --git a/run_e2e_test.sh b/run_e2e_test.sh deleted file mode 100755 index e0c1514..0000000 --- a/run_e2e_test.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# run_e2e_test.sh — End-to-end test of the streaming pipeline: -# MARTe2 app (UDPStreamer) → StreamHub (C++) → WebSocket client (Go) -# -# Launches the full stack headless, then runs Test/E2E/streamhub which -# verifies sources/config events, binary data pushes (wall-clock time base, -# per-signal monotonicity), stats, WS zoom round-trips and a hub-side -# trigger capture. Exits 0 iff every check passes. -# -# Usage: -# ./run_e2e_test.sh [OPTIONS] -# -# Options: -# -m Override MARTe2 installation dir -# -c Override MARTe2-components dir -# -b Build target (default: x86-linux) -# -p StreamHub WebSocket port (default: 8095) -# -s Skip building -# -h Show this help - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MARTe_CFG="${SCRIPT_DIR}/Test/Configurations/streamhub_demo.cfg" -BUILD_TARGET="${TARGET:-x86-linux}" -WS_PORT=8095 -SKIP_BUILD=0 - -while getopts "m:c:b:p:sh" opt; do - case "$opt" in - m) MARTe2_DIR="$OPTARG" ;; - c) MARTe2_Components_DIR="$OPTARG" ;; - b) BUILD_TARGET="$OPTARG" ;; - p) WS_PORT="$OPTARG" ;; - s) SKIP_BUILD=1 ;; - h) - sed -n '2,19p' "$0" | grep '^#' | sed 's/^# \?//' - exit 0 - ;; - *) echo "Unknown option: -$OPTARG" >&2; exit 1 ;; - esac -done - -if [[ -z "${MARTe2_DIR:-}" || -z "${MARTe2_Components_DIR:-}" ]]; then - echo "ERROR: MARTe2_DIR / MARTe2_Components_DIR not set. Source env.sh first." - exit 1 -fi - -BUILD_DIR="${SCRIPT_DIR}/Build/${BUILD_TARGET}" -STREAMHUB_EX="${BUILD_DIR}/StreamHub/StreamHub.ex" -E2E_DIR="${SCRIPT_DIR}/Test/E2E/streamhub" -E2E_BIN="${E2E_DIR}/streamhub-e2e" - -MARTE2_BIN="${MARTe2_DIR}/Build/${BUILD_TARGET}/App/MARTeApp.ex" -[[ -x "$MARTE2_BIN" ]] || MARTE2_BIN="${MARTe2_DIR}/Build/${BUILD_TARGET}/App/MARTe2.sh" -if [[ ! -x "$MARTE2_BIN" ]]; then - echo "ERROR: MARTe2 executable not found at ${MARTe2_DIR}/Build/${BUILD_TARGET}/App/" - exit 1 -fi - -# ── Build ───────────────────────────────────────────────────────────────────── -if [[ "$SKIP_BUILD" -eq 0 ]]; then - echo "==> Building MARTe2 components + StreamHub..." - make -C "${SCRIPT_DIR}" -f Makefile.gcc TARGET="${BUILD_TARGET}" core apps 2>&1 | tail -5 - echo "==> Building E2E client..." - (cd "${E2E_DIR}" && go build -o streamhub-e2e .) -fi - -if [[ ! -x "$STREAMHUB_EX" ]]; then - echo "ERROR: StreamHub binary not found: ${STREAMHUB_EX}" - exit 1 -fi - -# ── StreamHub config ────────────────────────────────────────────────────────── -HUB_CFG="$(mktemp /tmp/streamhub_e2e_XXXXXX.cfg)" -cat > "$HUB_CFG" </dev/null || true - [[ -n "$MARTE_PID" ]] && kill "$MARTE_PID" 2>/dev/null || true - wait "$HUB_PID" 2>/dev/null || true - wait "$MARTE_PID" 2>/dev/null || true - rm -f "$HUB_CFG" - if [[ "$RESULT" -eq 0 ]]; then - echo "==> E2E: PASS" - else - echo "==> E2E: FAIL" - fi -} -trap cleanup EXIT INT TERM - -echo "==> Launching MARTe2 (headless)..." -"${MARTE2_BIN}" \ - -l RealTimeLoader \ - -f "${MARTe_CFG}" \ - -s Running \ - -m StateMachine:START > /tmp/streamhub_e2e_marte.log 2>&1 & -MARTE_PID="$!" - -sleep 1 - -echo "==> Launching StreamHub (ws port ${WS_PORT})..." -"${STREAMHUB_EX}" -cfg "${HUB_CFG}" > /tmp/streamhub_e2e_hub.log 2>&1 & -HUB_PID="$!" - -# Give the hub time to connect the UDPS sources and fill some history -sleep 3 - -echo "==> Running E2E client..." -if "${E2E_BIN}" -hub "127.0.0.1:${WS_PORT}" -timeout 60s; then - RESULT=0 -fi - -exit "$RESULT"