ba31d79368
Run 10536 reached pytest but every connect was reset: the simulator died at
import with 'Tkinter is needed' — lib/gui.py calls matplotlib.use("TkAgg")
unconditionally (an explicit use() overrides MPLBACKEND=Agg), and the port
probe passed instantly because it was accepting the localhost forwarders, not
the nodes.
- run_e2e.sh: patch meshtasticator lib/gui.py TkAgg->Agg before boot;
probe readiness on the upstream host (docker daemon) when forwards are
active; docker pull meshtastic/meshtasticd up front so the pull is off the
simulator's node-boot path.
- e2e tests: cancel leftover loop tasks (watchdog) at loop teardown.
198 lines
7.4 KiB
Bash
Executable File
198 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Boot N Meshtasticator nodes and run the plugin's radio-level e2e tests
|
|
# (plugin/tests/e2e) against node 0.
|
|
#
|
|
# Meshtasticator's interactive simulator (https://meshtastic.org/docs/software/meshtasticator/)
|
|
# runs the real MeshtasticD device software per node and simulates the LoRa
|
|
# PHY, so these tests exercise the adapter's real TCP/protobuf session,
|
|
# inbound/outbound text flow across the simulated mesh, and chunked sends —
|
|
# without physical radios. It cannot validate silicon-level RF.
|
|
#
|
|
# Prerequisites
|
|
# * Linux host, OR Docker (macOS/Windows): the simulator auto-falls back to
|
|
# Docker on non-Linux and can build MeshtasticD itself (`-d`).
|
|
# * Native Linux mode: build MeshtasticD's 'native' target (PlatformIO,
|
|
# select 'native') and pass --program <dir containing binary 'program'>.
|
|
# * uv + python 3.13 for the contract venv (plugin/.venv-contract).
|
|
#
|
|
# Docker on a remote daemon: if DOCKER_HOST points at a tcp:// daemon, the
|
|
# simulator's own control connections assume nodes listen on localhost, so
|
|
# this script starts lightweight localhost->daemon TCP forwarders for the node
|
|
# ports first (scripts/meshtasticator_e2e/tcp_forward.py). Nothing is
|
|
# forwarded when the daemon is local (unset/unix DOCKER_HOST).
|
|
#
|
|
# Usage
|
|
# MESHTASTICATOR_DIR=/path/to/meshtasticator scripts/meshtasticator_e2e/run_e2e.sh
|
|
# # docker mode (default): ... run_e2e.sh --mode docker
|
|
# # native mode: ... run_e2e.sh --mode native --program /path/to/firmware/.pio/build/native
|
|
# # node count / channel: ... run_e2e.sh --nodes 3 --channel-index 0
|
|
#
|
|
# Environment
|
|
# MESHTASTICATOR_DIR path to a clone of github.com/meshtastic/meshtasticator
|
|
# E2E_VENV python env with hermes-agent + plugin deps installed
|
|
# (default: plugin/.venv-contract — see scripts/contract-test.sh,
|
|
# then `uv pip install --python plugin/.venv-contract/bin/python -e plugin`)
|
|
# SIM_VENV optional python env holding the simulator's deps
|
|
# (meshtasticator/requirements.txt + `pip install docker`);
|
|
# defaults to plain `python3`.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
SIM_DIR="${MESHTASTICATOR_DIR:?set MESHTASTICATOR_DIR to a meshtasticator clone}"
|
|
NODES=3
|
|
MODE=docker
|
|
PROGRAM=""
|
|
CHANNEL_INDEX=0
|
|
HOST=127.0.0.1
|
|
VENV="${E2E_VENV:-$REPO_ROOT/plugin/.venv-contract}"
|
|
if [ -n "${SIM_VENV:-}" ]; then
|
|
SIM_PYTHON="$SIM_VENV/bin/python"
|
|
else
|
|
SIM_PYTHON="${SIM_PYTHON:-python3}"
|
|
fi
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--nodes) NODES="$2"; shift 2 ;;
|
|
--mode) MODE="$2"; shift 2 ;;
|
|
--program) PROGRAM="$2"; shift 2 ;;
|
|
--channel-index) CHANNEL_INDEX="$2"; shift 2 ;;
|
|
--host) HOST="$2"; shift 2 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
command -v uv >/dev/null 2>&1 || { echo "uv required" >&2; exit 1; }
|
|
[ -d "$SIM_DIR" ] || { echo "meshtasticator not found at $SIM_DIR" >&2; exit 1; }
|
|
[ -x "$VENV/bin/python" ] || { echo "e2e venv missing at $VENV (see scripts/contract-test.sh)" >&2; exit 1; }
|
|
|
|
export MPLBACKEND=Agg # headless matplotlib (no display needed)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Remote-daemon port forwarding: Meshtasticator talks to nodes on localhost.
|
|
# ---------------------------------------------------------------------------
|
|
DOCKER_TARGET=""
|
|
if [[ "${DOCKER_HOST:-}" == tcp://* ]]; then
|
|
DOCKER_TARGET="${DOCKER_HOST#tcp://}"
|
|
DOCKER_TARGET="${DOCKER_TARGET%%:*}"
|
|
if [ "$DOCKER_TARGET" = "127.0.0.1" ] || [ "$DOCKER_TARGET" = "localhost" ]; then
|
|
DOCKER_TARGET=""
|
|
fi
|
|
fi
|
|
FWD_PIDS=()
|
|
PORT_BASE=4404 # matches meshtasticator lib/interactive.py TCP_PORT_OFFSET
|
|
|
|
start_forwards() {
|
|
for ((i = 0; i < NODES; i++)); do
|
|
port=$((PORT_BASE + i))
|
|
"$VENV/bin/python" "$REPO_ROOT/scripts/meshtasticator_e2e/tcp_forward.py" \
|
|
--listen "127.0.0.1:$port" --target "$DOCKER_TARGET:$port" >/dev/null 2>&1 &
|
|
FWD_PIDS+=("$!")
|
|
done
|
|
sleep 1
|
|
}
|
|
|
|
SIM_LOG="$REPO_ROOT/meshtasticator-sim.log"
|
|
SIM_ARGS=("$NODES")
|
|
if [ "$MODE" = native ]; then
|
|
[ -n "$PROGRAM" ] && [ -d "$PROGRAM" ] || { echo "--mode native needs --program <dir>" >&2; exit 2; }
|
|
SIM_ARGS+=(-p "$PROGRAM")
|
|
else
|
|
SIM_ARGS+=(-d)
|
|
fi
|
|
|
|
if [ -n "$DOCKER_TARGET" ]; then
|
|
echo "== remote docker daemon detected ($DOCKER_TARGET): starting localhost port forwards =="
|
|
start_forwards
|
|
PROBE_HOST="$DOCKER_TARGET"
|
|
else
|
|
PROBE_HOST="$HOST"
|
|
fi
|
|
|
|
# The simulator forces TkAgg at import (lib/gui.py: matplotlib.use("TkAgg")),
|
|
# which crashes headless runs ("Tkinter is needed") even under MPLBACKEND=Agg —
|
|
# an explicit use() overrides the env var. Neutralize it in the pinned
|
|
# checkout; only the interactive GUI mode (never used by this harness) is lost.
|
|
if grep -q 'matplotlib.use("TkAgg")' "$SIM_DIR/lib/gui.py"; then
|
|
echo "== patching meshtasticator lib/gui.py: TkAgg -> Agg (headless) =="
|
|
sed -i.bak 's/matplotlib.use("TkAgg")/matplotlib.use("Agg")/' "$SIM_DIR/lib/gui.py"
|
|
fi
|
|
|
|
if [ "$MODE" = docker ]; then
|
|
# Warm the node image on the (possibly remote) daemon first: the simulator
|
|
# starts node processes ~4s after the container is created and control
|
|
# connections must succeed quickly, so the pull cannot sit in that path.
|
|
echo "== warming meshtastic/meshtasticd image =="
|
|
docker pull meshtastic/meshtasticd
|
|
fi
|
|
|
|
echo "== booting Meshtasticator ($MODE) with $NODES node(s) from $SIM_DIR =="
|
|
(
|
|
cd "$SIM_DIR"
|
|
"$SIM_PYTHON" interactiveSim.py "${SIM_ARGS[@]}" >"$SIM_LOG" 2>&1
|
|
) &
|
|
SIM_PID=$!
|
|
|
|
cleanup() {
|
|
echo "== tearing down simulator (pid $SIM_PID) =="
|
|
kill "$SIM_PID" 2>/dev/null || true
|
|
wait "$SIM_PID" 2>/dev/null || true
|
|
for pid in "${FWD_PIDS[@]:-}"; do
|
|
kill "$pid" 2>/dev/null || true
|
|
done
|
|
if [ "$MODE" = docker ]; then
|
|
# The sim names its container/volume (auto_remove only fires on stop).
|
|
docker rm -f Meshtastic >/dev/null 2>&1 || true
|
|
docker volume rm Meshtasticator >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "== waiting for $NODES node TCP API port(s) to accept connections =="
|
|
READY=""
|
|
for _ in $(seq 1 180); do # up to ~5 min: docker pulls meshtastic/meshtasticd on first run
|
|
# Probe the upstream node host (the docker daemon when forwards are active,
|
|
# localhost otherwise) — probing the forwarders themselves would always pass.
|
|
READY="$("$VENV/bin/python" - "$PROBE_HOST" "$NODES" <<'PY' || true
|
|
import socket, sys
|
|
host, count = sys.argv[1], int(sys.argv[2])
|
|
found = []
|
|
# Meshtasticator assigns node n -> TCP port starting at 4404 (see
|
|
# lib/interactive.py TCP_PORT_OFFSET); probe a small window to be safe.
|
|
for port in range(4403, 4403 + 16):
|
|
try:
|
|
with socket.create_connection((host, port), timeout=0.5):
|
|
found.append(port)
|
|
except OSError:
|
|
pass
|
|
if len(found) >= count:
|
|
break
|
|
print(",".join(map(str, found[:count])))
|
|
PY
|
|
)"
|
|
if [ -n "$READY" ]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
if [ -z "$READY" ]; then
|
|
echo "error: no Meshtasticator node ports became reachable on $PROBE_HOST (see $SIM_LOG)" >&2
|
|
exit 1
|
|
fi
|
|
echo " node ports: $READY (probed on $PROBE_HOST)"
|
|
|
|
# Ports accept before the node firmware finished booting inside the container;
|
|
# give meshtasticd a moment to open its TCP API for real.
|
|
echo "== settling (node firmware boot) =="
|
|
sleep 15
|
|
|
|
echo "== running radio-level e2e tests =="
|
|
(
|
|
cd "$REPO_ROOT"
|
|
MESHTASTICATOR_E2E=1 \
|
|
E2E_HOST="$HOST" \
|
|
E2E_PORTS="$READY" \
|
|
E2E_CHANNEL_INDEX="$CHANNEL_INDEX" \
|
|
"$VENV/bin/python" -m pytest plugin/tests/e2e -q "$@"
|
|
)
|