#!/usr/bin/env bash # run_chain_e2e.sh — Full-chain E2E orchestrator for the streaming chain # # MARTe2 app (FileReader -> IOGAM -> UDPStreamer) # -> UDPS -> StreamHub -> chain-client (record + zoom/window/trigger) # -> validate_waveform.py -> plots.py -> results.json [-> PDF] # # Per scenario (scenarios.py) it generates input data + both cfgs, runs the # two-process stack, drives the mock client, validates the recorded waveform # against the analytic/fed oracle, renders plots, and aggregates results.json. # Artifacts: Build/x86-linux/E2E/chain/ (report) and /tmp/chain_e2e/ (scratch). # # Usage: ./run_chain_e2e.sh [--skip-build] [--only ] [--pdf-only] set -u 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/chain" WORK="/tmp/chain_e2e" mkdir -p "${OUT_DIR}" "${WORK}" SKIP_BUILD=0 ONLY="" PDF_ONLY=0 while [ $# -gt 0 ]; do case "$1" in --skip-build) SKIP_BUILD=1 ;; --only) shift; ONLY="$1" ;; --pdf-only) PDF_ONLY=1 ;; --help|-h) echo "Usage: $0 [--skip-build] [--only ] [--pdf-only]"; exit 0 ;; *) echo "unknown arg $1" >&2; exit 2 ;; esac shift done ENV_SCRIPT="${REPO_ROOT}/env.sh" [ -f "${ENV_SCRIPT}" ] || { echo "ERROR: ${ENV_SCRIPT} not found" >&2; exit 1; } : "${LD_LIBRARY_PATH:=}" # env.sh appends to it under our set -u 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" CLIENT="${SCRIPT_DIR}/client/chain-client" PY="python3" SCEN() { ${PY} "${SCRIPT_DIR}/scenarios.py" >/dev/null 2>&1; } # ── PDF-only shortcut (Task 9 fills in the compile) ────────────────────────── if [ "${PDF_ONLY}" -eq 1 ]; then if command -v typst >/dev/null 2>&1 && [ -f "${SCRIPT_DIR}/E2E_Report.typ" ]; then cp "${SCRIPT_DIR}/E2E_Report.typ" "${OUT_DIR}/" 2>/dev/null || true (cd "${OUT_DIR}" && typst compile E2E_Report.typ E2E_Report.pdf) \ && echo "PDF: ${OUT_DIR}/E2E_Report.pdf" else echo "typst or E2E_Report.typ missing — skipping PDF" fi exit 0 fi # ── Build ──────────────────────────────────────────────────────────────────── if [ "${SKIP_BUILD}" -eq 0 ]; then echo "── Building components ──" make -C "${REPO_ROOT}/Source/Components/Interfaces/UDPStream" -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -1 make -C "${REPO_ROOT}/Source/Components/DataSources/UDPStreamer" -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -1 make -C "${REPO_ROOT}/Source/Applications/StreamHub" -f Makefile.gcc TARGET="${TARGET}" 2>&1 | tail -1 fi if [ ! -x "${CLIENT}" ]; then echo "── Building chain-client ──" (cd "${SCRIPT_DIR}/client" && go build -o chain-client .) || { echo "client build failed"; exit 1; } fi [ -x "${MARTE_APP}" ] || { echo "ERROR: MARTeApp.ex not found at ${MARTE_APP}" >&2; exit 1; } [ -x "${STREAMHUB_EX}" ] || { echo "ERROR: StreamHub.ex not found" >&2; exit 1; } # ── Scenario list (id|ws_port|udp_port0|network|oracle|trig|checks) ────────── LIST="$(${PY} - "${ONLY}" <<'PY' import sys, os sys.path.insert(0, os.path.dirname(os.path.abspath("Test/E2E/chain/scenarios.py"))) sys.path.insert(0, os.path.join(os.getcwd(), "Test/E2E/chain")) import scenarios as S only = sys.argv[1] if len(sys.argv) > 1 else "" for s in S.SCENARIOS: if only and s["id"] != only: continue trig = s.get("trig_signal") or "" checks = ",".join(s.get("client_checks", [])) if not trig: checks = ",".join(c for c in s.get("client_checks", []) if c != "trigger") print("|".join([s["id"], str(s["ws_port"]), str(s["sources"][0]["udp_port"]), s["network"], s["oracle"], trig, checks])) PY )" if [ -z "${LIST}" ]; then echo "no scenarios selected"; exit 1; fi SCEN_IDS="" HUB_PID=""; APP_PID="" cleanup() { [ -n "${APP_PID}" ] && kill "${APP_PID}" 2>/dev/null [ -n "${HUB_PID}" ] && kill "${HUB_PID}" 2>/dev/null wait "${APP_PID}" 2>/dev/null; wait "${HUB_PID}" 2>/dev/null APP_PID=""; HUB_PID="" } trap cleanup EXIT while IFS='|' read -r ID WSPORT UDPPORT NET ORACLE TRIG CHECKS; do [ -z "${ID}" ] && continue SCEN_IDS="${SCEN_IDS} ${ID}" echo "" echo "══ scenario ${ID} (net=${NET} oracle=${ORACLE} ws=${WSPORT}) ══" : > "${WORK}/status_${ID}.txt" # multicast route probe if [ "${NET}" = "multicast" ]; then GRP="$(${PY} -c "import sys;sys.path.insert(0,'${SCRIPT_DIR}');import scenarios as S;print(next(s for s in S.SCENARIOS if s['id']=='${ID}')['sources'][0]['multicast_group'])")" if ! ip route get "${GRP}" >/dev/null 2>&1; then echo " SKIP: no multicast route to ${GRP}" echo "SKIP" > "${WORK}/status_${ID}.txt" continue fi fi INPUT="${WORK}/input_${ID}.bin" MCFG="${WORK}/m_${ID}.cfg" HCFG="${WORK}/h_${ID}.cfg" TAP="" if [ "${ORACLE}" = "fed" ] || [ "${ORACLE}" = "both" ]; then TAP="${WORK}/tap_${ID}.bin" fi ${PY} "${SCRIPT_DIR}/gen_data.py" --scenario "${ID}" --out "${INPUT}" || { echo FAIL > "${WORK}/status_${ID}.txt"; continue; } if [ -n "${TAP}" ]; then ${PY} "${SCRIPT_DIR}/gen_cfg.py" --scenario "${ID}" --input "${INPUT}" --marte-out "${MCFG}" --hub-out "${HCFG}" --tap "${TAP}" else ${PY} "${SCRIPT_DIR}/gen_cfg.py" --scenario "${ID}" --input "${INPUT}" --marte-out "${MCFG}" --hub-out "${HCFG}" fi HUB_LOG="${OUT_DIR}/hub_${ID}.log" APP_LOG="${OUT_DIR}/marte_${ID}.log" rm -f "${WORK}/received_${ID}.bin" "${WORK}/checks_${ID}.json" "${WORK}/metrics_${ID}.json" "${STREAMHUB_EX}" -cfg "${HCFG}" > "${HUB_LOG}" 2>&1 & HUB_PID=$! sleep 1 timeout 120 "${MARTE_APP}" -l RealTimeLoader -f "${MCFG}" -s Running > "${APP_LOG}" 2>&1 & APP_PID=$! sleep 1 TRIGARG="" [ -n "${TRIG}" ] && TRIGARG="-trigsig ${TRIG}" if "${CLIENT}" -hub "127.0.0.1:${WSPORT}" -scenario "${ID}" ${TRIGARG} \ -checks "${CHECKS}" -out "${WORK}" -dur 4 > "${OUT_DIR}/client_${ID}.log" 2>&1; then echo " client OK" else echo " client FAILED (see client_${ID}.log)" tail -3 "${OUT_DIR}/client_${ID}.log" | sed 's/^/ /' fi cleanup # validate + plot VARGS="--scenario ${ID} --received ${WORK}/received_${ID}.bin --checks ${WORK}/checks_${ID}.json --out ${WORK}/metrics_${ID}.json" [ -n "${TAP}" ] && [ -f "${TAP}" ] && VARGS="${VARGS} --tap ${TAP}" if ${PY} "${SCRIPT_DIR}/validate_waveform.py" ${VARGS}; then echo "PASS" > "${WORK}/status_${ID}.txt" else echo "FAIL" > "${WORK}/status_${ID}.txt" fi ${PY} "${SCRIPT_DIR}/plots.py" --scenario "${ID}" --dir "${WORK}" >/dev/null 2>&1 || true done <<< "${LIST}" trap - EXIT cleanup # ── Aggregate results.json ─────────────────────────────────────────────────── WORK="${WORK}" OUT_DIR="${OUT_DIR}" SCEN_IDS="${SCEN_IDS}" ${PY} - <<'PY' import json, os work = os.environ["WORK"]; out = os.environ["OUT_DIR"] ids = os.environ["SCEN_IDS"].split() results = [] for sid in ids: rec = {"id": sid} st = os.path.join(work, f"status_{sid}.txt") rec["status"] = open(st).read().strip() if os.path.exists(st) else "UNKNOWN" mp = os.path.join(work, f"metrics_{sid}.json") if os.path.exists(mp): rec["metrics"] = json.load(open(mp)) results.append(rec) overall = all(r["status"] in ("PASS", "SKIP") for r in results) and bool(results) doc = {"overall": "PASS" if overall else "FAIL", "scenarios": results} with open(os.path.join(out, "results.json"), "w") as f: json.dump(doc, f, indent=2) print(f"\nresults.json: {sum(r['status']=='PASS' for r in results)} pass, " f"{sum(r['status']=='FAIL' for r in results)} fail, " f"{sum(r['status']=='SKIP' for r in results)} skip → {doc['overall']}") PY # ── Optional PDF ───────────────────────────────────────────────────────────── if command -v typst >/dev/null 2>&1 && [ -f "${SCRIPT_DIR}/E2E_Report.typ" ]; then cp "${SCRIPT_DIR}/E2E_Report.typ" "${OUT_DIR}/" 2>/dev/null || true (cd "${OUT_DIR}" && typst compile E2E_Report.typ E2E_Report.pdf 2>/dev/null) \ && echo "PDF: ${OUT_DIR}/E2E_Report.pdf" fi echo "Done — artifacts in ${OUT_DIR} and ${WORK}"