fix(e2e): headless TkAgg crash + readiness false-positive on forwards

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.
This commit is contained in:
2026-09-07 21:33:49 -07:00
parent 58b18d6260
commit ba31d79368
3 changed files with 44 additions and 4 deletions
+25 -3
View File
@@ -104,6 +104,26 @@ 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 =="
@@ -131,7 +151,9 @@ 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
READY="$("$VENV/bin/python" - "$HOST" "$NODES" <<'PY' || true
# 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 = []
@@ -154,10 +176,10 @@ PY
sleep 2
done
if [ -z "$READY" ]; then
echo "error: no Meshtasticator node ports became reachable on $HOST (see $SIM_LOG)" >&2
echo "error: no Meshtasticator node ports became reachable on $PROBE_HOST (see $SIM_LOG)" >&2
exit 1
fi
echo " node ports: $READY"
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.