test(streamhub): add binary recorder E2E scenario
End-to-end test streaming three float32 signals via a MARTe2 UDPStreamer app into a standalone StreamHub with the recorder enabled, then validating the recorded FileWriter-compatible .bin against the streamed input with validate_binary.py. All recorded rows are byte-identical to input rows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Recorder E2E — FileReader -> IOGAM -> UDPStreamer (port 44600).
|
||||
*
|
||||
* Streams the three test signals (Signal_100, Signal_1K, Signal_5K, all
|
||||
* float32, un-quantized) over UDPS so a separate StreamHub process can
|
||||
* record them to disk. Signal names MUST match the binary file header
|
||||
* produced by gen_test_data.py.
|
||||
*/
|
||||
$RecorderStreamer = {
|
||||
Class = RealTimeApplication
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+TimerGAM = { Class = IOGAM InputSignals = { Counter = { DataSource = ReaderTimer Type = uint32 } Time = { Frequency = 10 DataSource = ReaderTimer Type = uint32 } } OutputSignals = { Counter = { DataSource = DDB Type = uint32 } Time = { DataSource = DDB Type = uint32 } } }
|
||||
+ReaderGAM = { Class = IOGAM InputSignals = { Signal_100 = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { DataSource = FileReaderDS Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } OutputSignals = { Signal_100 = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { DataSource = Streamer Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
|
||||
}
|
||||
+Data = {
|
||||
Class = ReferenceContainer DefaultDataSource = DDB
|
||||
+DDB = { Class = GAMDataSource }
|
||||
+ReaderTimer = { Class = LinuxTimer SleepNature = "Default" Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }
|
||||
+FileReaderDS = { Class = FileReader Filename = "/tmp/udpstreamer_test_input.bin" Interpolate = "no" FileFormat = "binary" }
|
||||
+Streamer = { Class = UDPStreamer Port = 44600 MaxPayloadSize = 65507 PublishingMode = "Strict" Signals = { Signal_100 = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 100 } Signal_1K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 1000 } Signal_5K = { Type = float32 NumberOfDimensions = 1 NumberOfElements = 5000 } } }
|
||||
+Timings = { Class = TimingDataSource }
|
||||
}
|
||||
+States = { Class = ReferenceContainer +Running = { Class = RealTimeState +Threads = { Class = ReferenceContainer +ReaderThread = { Class = RealTimeThread CPUs = 0x1 Functions = {TimerGAM ReaderGAM} } } } }
|
||||
+Scheduler = { Class = GAMScheduler TimingDataSource = Timings }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Recorder E2E — StreamHub config.
|
||||
*
|
||||
* One source consuming the UDPS stream emitted by RecorderStreamer.cfg
|
||||
* (UDPStreamer on port 44600). The +Recorder block is enabled and
|
||||
* auto-starts, writing FileWriter-compatible binary files to
|
||||
* /tmp/streamhub_rec_e2e. Signals="all" records every signal the source
|
||||
* reports, in source order (Signal_100, Signal_1K, Signal_5K).
|
||||
*/
|
||||
Hub = {
|
||||
WSPort = 8095
|
||||
MaxPoints = 200000
|
||||
PushRate = 30
|
||||
MaxPushPoints = 2000
|
||||
RingTemporal = 1000000
|
||||
RingScalar = 100000
|
||||
+Recorder = {
|
||||
Enabled = 1
|
||||
AutoStart = 1
|
||||
Directory = "/tmp/streamhub_rec_e2e"
|
||||
MaxFileMB = 256
|
||||
KeepFiles = 8
|
||||
StagingMB = 16
|
||||
FlushIntervalSec = 1
|
||||
MinDiskFreeMB = 200
|
||||
Signals = "all"
|
||||
}
|
||||
Sources = {
|
||||
e2e = {
|
||||
Label = "Recorder E2E source"
|
||||
Addr = "127.0.0.1"
|
||||
Port = 44600
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/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 <sourceId>_<UTCstamp>_<seq>.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 "=========================================="
|
||||
Reference in New Issue
Block a user