feat(e2e): Meshtasticator radio-level validation harness for the adapter #2

Closed
eric wants to merge 9 commits from feat/meshtasticator-e2e into main
2 changed files with 45 additions and 13 deletions
Showing only changes of commit eff352b2ea - Show all commits
+9 -4
View File
@@ -85,11 +85,16 @@ simulator. Nothing is forwarded when the daemon is local (unset/unix
on the forwarders themselves. on the forwarders themselves.
Other headless adaptations the script makes automatically: Other headless adaptations the script makes automatically:
* patches `lib/gui.py` `matplotlib.use("TkAgg")` → `"Agg"` in the pinned * patches `lib/gui.py` in the pinned Meshtasticator checkout: forces the Agg
Meshtasticator checkout — the TkAgg call crashes headless runs even under backend (the code calls `matplotlib.use("TkAgg")` at import) and makes
`MPLBACKEND=Agg` (only the interactive GUI mode is lost), `move_figure()` a no-op when no Tk window manager exists (its unconditional
`canvas.manager.window` access crashes under Agg at Graph construction).
Only the interactive GUI modes, never used by this harness, are lost,
* warms the `meshtastic/meshtasticd` image (`docker pull`) before booting the * warms the `meshtastic/meshtasticd` image (`docker pull`) before booting the
simulator so the pull is not on the simulator's node-boot critical path. simulator so the pull is not on the simulator's node-boot critical path,
* feeds the simulator's interactive `cmdloop` from a FIFO held open until
teardown — an EOF stdin (e.g. CI) would otherwise make it exit right after
booting the nodes.
Without `MESHTASTICATOR_E2E=1` the e2e module skips, so ordinary unit/CI runs Without `MESHTASTICATOR_E2E=1` the e2e module skips, so ordinary unit/CI runs
stay fast and gateway-free. stay fast and gateway-free.
+36 -9
View File
@@ -109,14 +109,33 @@ else
PROBE_HOST="$HOST" PROBE_HOST="$HOST"
fi fi
# The simulator forces TkAgg at import (lib/gui.py: matplotlib.use("TkAgg")), # The simulator assumes an interactive Tk desktop: lib/gui.py forces
# which crashes headless runs ("Tkinter is needed") even under MPLBACKEND=Agg — # matplotlib.use("TkAgg") at import and move_figure() touches the Tk window
# an explicit use() overrides the env var. Neutralize it in the pinned # manager at Graph construction — both crash headless runs even under
# checkout; only the interactive GUI mode (never used by this harness) is lost. # MPLBACKEND=Agg. Neutralize them in the pinned checkout (only the interactive
if grep -q 'matplotlib.use("TkAgg")' "$SIM_DIR/lib/gui.py"; then # GUI modes, never used by this harness, are lost).
echo "== patching meshtasticator lib/gui.py: TkAgg -> Agg (headless) ==" echo "== patching meshtasticator lib/gui.py for headless runs =="
sed -i.bak 's/matplotlib.use("TkAgg")/matplotlib.use("Agg")/' "$SIM_DIR/lib/gui.py" "$VENV/bin/python" - "$SIM_DIR" <<'PY'
fi import sys
from pathlib import Path
gui = Path(sys.argv[1]) / "lib/gui.py"
src = gui.read_text()
src = src.replace('matplotlib.use("TkAgg")', 'matplotlib.use("Agg")')
old = 'def move_figure(fig, x, y):\n fig.canvas.manager.window.wm_geometry("+%d+%d" % (x, y))'
new = ('def move_figure(fig, x, y):\n'
' # Headless (Agg) backends have no Tk window manager.\n'
' try:\n'
' fig.canvas.manager.window.wm_geometry("+%d+%d" % (x, y))\n'
' except AttributeError:\n'
' pass')
assert old in src, "move_figure source no longer matches the pinned meshtasticator"
src = src.replace(old, new)
gui.write_text(src)
print("patched lib/gui.py")
PY
if [ "$MODE" = docker ]; then if [ "$MODE" = docker ]; then
# Warm the node image on the (possibly remote) daemon first: the simulator # Warm the node image on the (possibly remote) daemon first: the simulator
@@ -127,14 +146,22 @@ if [ "$MODE" = docker ]; then
fi fi
echo "== booting Meshtasticator ($MODE) with $NODES node(s) from $SIM_DIR ==" echo "== booting Meshtasticator ($MODE) with $NODES node(s) from $SIM_DIR =="
# The simulator ends in an interactive cmdloop reading stdin; an EOF there
# (e.g. CI) would make it exit right after booting the nodes. Feed it from a
# FIFO whose write end this shell keeps open until cleanup.
SIM_FIFO="$(mktemp -u /tmp/meshtasticator-XXXXXX.fifo)"
mkfifo "$SIM_FIFO"
( (
cd "$SIM_DIR" cd "$SIM_DIR"
"$SIM_PYTHON" interactiveSim.py "${SIM_ARGS[@]}" >"$SIM_LOG" 2>&1 "$SIM_PYTHON" interactiveSim.py "${SIM_ARGS[@]}" <"$SIM_FIFO" >"$SIM_LOG" 2>&1
) & ) &
SIM_PID=$! SIM_PID=$!
exec 9>"$SIM_FIFO" # hold the write end open -> sim stdin never hits EOF
rm -f "$SIM_FIFO"
cleanup() { cleanup() {
echo "== tearing down simulator (pid $SIM_PID) ==" echo "== tearing down simulator (pid $SIM_PID) =="
exec 9>&- 2>/dev/null || true # release the FIFO write end (sim stdin EOF)
kill "$SIM_PID" 2>/dev/null || true kill "$SIM_PID" 2>/dev/null || true
wait "$SIM_PID" 2>/dev/null || true wait "$SIM_PID" 2>/dev/null || true
for pid in "${FWD_PIDS[@]:-}"; do for pid in "${FWD_PIDS[@]:-}"; do