fix(station): RuntimeDirectory-Race, Statusphase, off/ap_switch-Nachpruefung, Pi-Bestandskonfiguration

Fixrunde 1 zu Task 4:

- C1 (kritisch): die Unit leerte /run/ninlight-station bei jedem
  systemctl restart, weil RuntimeDirectoryPreserve fehlte - genau der
  Neustart, mit dem migrate/recovery on den Dienst hochfahren, wischte
  die eben geschriebene Modus-/Steuerdatei wieder weg, und der Server
  fiel still auf den Recovery-Default zurueck. Unit bekommt
  RuntimeDirectoryPreserve=restart, und start_and_verify() prueft nach
  dem Neustart zusaetzlich ausdruecklich nach, dass Modus und Phase noch
  stimmen.
- I1: status las die Phase aus status.json, die arm/listen (die den
  Dienst bewusst nicht neu starten) nie aktualisieren - jetzt kommt sie
  aus control.json.
- I2: off pruefte nach dem Herunterfahren nichts nach und konnte
  "Station aus" melden, waehrend ein Profil wegen haengendem
  NetworkManager weiterhin aktiv war - ap_switch() verifiziert das jetzt
  auch im Aus-Fall.
- I3: der Fehlerzweig von ap_switch (Mismatch nach dem Umschalten) war
  im bisherigen Test nie erreichbar; der nmcli-Stub kann das jetzt
  erzwingen.
- I4: neue Pflichtschluessel blockierten jeden Unterbefehl auf einem
  bereits eingerichteten Pi. setup_pi.sh ergaenzt fehlende Schluessel in
  einer bestehenden Konfiguration jetzt einzeln, statt sie unangetastet
  zu lassen; dafuer musste das Skript per bash-s-Heredocs und
  ueberschreibbaren Pfadvariablen erst testbar gemacht werden (neuer
  test_setup_pi.sh).

Dazu vier kleinere Punkte aus derselben Review: grep -Fqx statt grep -qx
fuer Profilnamen, install-Stub durch tee+chmod ersetzt (POSIX, kein
Sonderfall mehr fuer macOS), Rechtepruefung auf control.json in den
arm/listen-Testfaellen, stderr sauber von stdout getrennt in den
Warnungs-/Fehlerfaellen.
This commit is contained in:
Michael Sauer
2026-09-13 19:04:23 +02:00
parent 1d3627bacd
commit 1c077a15d0
6 changed files with 550 additions and 90 deletions
+152 -42
View File
@@ -7,7 +7,11 @@
# Der nmcli-Stub haelt zusaetzlich einen kleinen Zustand (welche Profile
# "bekannt" sind, welche gerade per "con up" aktiv sind) in Dateien, damit
# "con up"/"con down"/"device show wlan0" konsistent zueinander antworten -
# das ist die Grundlage fuer Fall 15, den wichtigsten Test hier.
# das ist die Grundlage fuer Fall 15, den wichtigsten Test hier. Der
# systemctl-Stub bildet zusaetzlich nach, dass systemd das RuntimeDirectory bei
# jedem Stop/Restart leert, sofern die Unit kein RuntimeDirectoryPreserve=restart
# traegt - ohne das haette Fixrunde 1's C1 (siehe unten) unsichtbar bleiben
# koennen, weil ein Stub, der nichts loescht, diese Klasse von Bug nicht zeigt.
#
# Aufruf: tools/station/tests/test_cli.sh
set -euo pipefail
@@ -15,6 +19,7 @@ set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
STATION_DIR=$(cd "$HERE/.." && pwd)
CLI="$STATION_DIR/ninlight-station"
REAL_UNIT="$STATION_DIR/ninlight-station.service"
TMPROOT=$(mktemp -d)
trap 'rm -rf "$TMPROOT"' EXIT
@@ -23,10 +28,18 @@ FAILED=0
pass() { echo "ok - $1"; }
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
file_mode() {
# $1 = Pfad. BSD-stat (macOS) und GNU-stat (Linux/Pi) haben unterschiedliche
# Flags - hier reicht "irgendeine funktioniert".
stat -f '%Lp' "$1" 2>/dev/null || stat -c '%a' "$1" 2>/dev/null
}
# --- Stub-Kommandos fuer nmcli/systemctl/sudo im PATH ---
# $1 = bindir, $2 = logdatei, $3 = nmcli-Zustandsverzeichnis
# $1 = bindir, $2 = logdatei, $3 = nmcli-Zustandsverzeichnis, $4 = RUN_DIR,
# $5 = Pfad zur systemd-Unit, deren RuntimeDirectoryPreserve-Zeile ueber das
# Leeren von $4 bei restart/stop entscheidet (wie beim echten systemd).
setup_stubbin() {
local bindir=$1 log=$2 nmstate=$3
local bindir=$1 log=$2 nmstate=$3 rundir=$4 unitfile=$5
mkdir -p "$bindir" "$nmstate"
: >"$nmstate/active" # Reihenfolge der per "con up" aktiven Profile, eine Zeile je Profil
@@ -41,6 +54,12 @@ if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "NAME" ] && [ "\$4" = "con"
fi
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "GENERAL.CONNECTION" ] && [ "\$4" = "device" ] && [ "\$5" = "show" ]; then
# Testschalter fuer Fall I3 (Fixrunde 1): erzwingt eine Antwort, die nicht zum
# zuletzt hochgefahrenen Profil passt, um ap_switch()'s Fehlerzweig zu pruefen.
if [ -n "\${FAKE_NMCLI_DEVSHOW:-}" ]; then
echo "GENERAL.CONNECTION:\${FAKE_NMCLI_DEVSHOW}"
exit 0
fi
last=\$(tail -n1 "\$NMSTATE/active" 2>/dev/null)
echo "GENERAL.CONNECTION:\${last:---}"
exit 0
@@ -54,6 +73,13 @@ if [ "\$1" = "con" ] && [ "\$2" = "up" ]; then
fi
if [ "\$1" = "con" ] && [ "\$2" = "down" ]; then
# Testschalter fuer Fall I2 (Fixrunde 1): simuliert einen haengenden
# NetworkManager - der Aufruf "gelingt" (Exit 0, wie ein echtes "nmcli con
# down" es bei einem haengenden Backend oft tut), aendert den Zustand aber
# nicht. ap_switch() darf das nicht als "aus" durchgehen lassen.
if [ -n "\${FAKE_NMCLI_STUCK:-}" ]; then
exit 0
fi
profile=\$3
grep -vxF "\$profile" "\$NMSTATE/active" 2>/dev/null >"\$NMSTATE/active.tmp" || true
mv "\$NMSTATE/active.tmp" "\$NMSTATE/active"
@@ -66,6 +92,8 @@ EOF
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
RUN_DIR="$rundir"
UNIT_FILE="$unitfile"
case "\$1" in
is-active)
if [ "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}" -eq 0 ]; then
@@ -76,7 +104,20 @@ case "\$1" in
exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT}"
fi
;;
restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;;
restart|stop)
# Bildet systemds RuntimeDirectory-Verhalten nach (Fixrunde 1, C1): ohne
# RuntimeDirectoryPreserve=restart in der Unit leert systemd das
# RuntimeDirectory bei JEDEM Stop/Restart, nicht nur beim allerersten Start,
# und legt es leer wieder an.
if ! grep -q '^RuntimeDirectoryPreserve=restart' "\$UNIT_FILE" 2>/dev/null; then
rm -rf "\$RUN_DIR"
mkdir -p "\$RUN_DIR"
fi
if [ "\$1" = "restart" ]; then
exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}"
fi
exit 0
;;
*) exit 0 ;;
esac
EOF
@@ -87,37 +128,14 @@ echo "STUB sudo \$*" >>"$log"
exec "\$@"
EOF
# BSD-"install" (macOS) kennt kein /dev/stdin als Quelle ("Inappropriate file
# type or format") - GNU coreutils auf dem Pi schon. Das Skript nutzt genau
# dieses Muster (sudo install -m 0644 /dev/stdin <ziel>), also braucht der
# Test einen kleinen Ersatz, der nur dieses eine Aufrufmuster kennt.
cat >"$bindir/install" <<'EOF'
#!/usr/bin/env bash
mode=""
args=()
while [ $# -gt 0 ]; do
case "$1" in
-m) mode=$2; shift 2 ;;
*) args+=("$1"); shift ;;
esac
done
src=${args[0]}
dest=${args[1]}
if [ "$src" = "/dev/stdin" ]; then
cat >"$dest"
else
cp "$src" "$dest"
fi
[ -n "$mode" ] && chmod "$mode" "$dest"
EOF
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo "$bindir"/install
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo
}
# --- Sandbox fuer einen Testfall: eigene conf/RUN_DIR/IMAGE_DIR/nmcli-Zustand ---
# $1 = sandbox-Verzeichnis
# $1 = sandbox-Verzeichnis, $2 = Unit-Datei fuer den systemctl-Stub (Default: die
# echte, gerade zu testende tools/station/ninlight-station.service).
setup_sandbox() {
local sandbox=$1
local sandbox=$1 unitfile=${2:-$REAL_UNIT}
rm -rf "$sandbox"
mkdir -p "$sandbox/run" "$sandbox/images/migrate" "$sandbox/bin" "$sandbox/nmstate"
echo "dummy" >"$sandbox/images/ninlight_foo_ota.img"
@@ -137,7 +155,7 @@ EOF
printf 'ninlight-recovery\nqconnex-firmware\nhome-wifi\n' >"$sandbox/nmstate/profiles"
: >"$sandbox/nmstate/active"
setup_stubbin "$sandbox/bin" "$sandbox/cmd.log" "$sandbox/nmstate"
setup_stubbin "$sandbox/bin" "$sandbox/cmd.log" "$sandbox/nmstate" "$sandbox/run" "$unitfile"
}
# Fuehrt ninlight-station mit gestubbtem PATH und ueberschriebenen Pfaden aus.
@@ -227,11 +245,12 @@ SBX="$TMPROOT/t5"
setup_sandbox "$SBX"
# images/migrate/ bleibt leer
set +e
OUT=$(run_cli "$SBX" migrate on 2>&1)
OUT=$(run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlender Migrationsimages" || fail "Exit-Code $RC, Ausgabe: $OUT"
echo "$OUT" | grep -q "Warnung" && pass "Warnung vorhanden" || fail "keine Warnung in der Ausgabe: $OUT"
ERR=$(cat "$SBX/stderr.log")
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlender Migrationsimages" || fail "Exit-Code $RC, stderr: $ERR"
echo "$ERR" | grep -q "Warnung" && pass "Warnung steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
echo
echo "=== Fall 6: 'arm' ohne Argument liest last_seen.target aus status.json ==="
@@ -247,6 +266,7 @@ set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "control.json: phase=armed" || fail "phase ist nicht 'armed'"
[ "$(control_target "$SBX/run/control.json")" = "gu10_rgb" ] && pass "armed_target=gu10_rgb (aus last_seen)" || fail "armed_target falsch: $(cat "$SBX/run/control.json")"
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
echo
echo "=== Fall 7: 'arm gu10_rgb' mit explizitem Ziel ==="
@@ -259,30 +279,33 @@ set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "control.json: phase=armed" || fail "phase ist nicht 'armed'"
[ "$(control_target "$SBX/run/control.json")" = "gu10_rgb" ] && pass "armed_target=gu10_rgb" || fail "armed_target falsch: $(cat "$SBX/run/control.json")"
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
echo
echo "=== Fall 8: 'arm' ohne last_seen -> Fehler, Rueckgabewert != 0, control.json unveraendert ==="
echo "=== Fall 8: 'arm' ohne last_seen -> Fehler auf stderr, Rueckgabewert != 0, control.json unveraendert ==="
SBX="$TMPROOT/t8"
setup_sandbox "$SBX"
# keine status.json vorhanden
set +e
OUT=$(run_cli "$SBX" arm 2>&1)
OUT=$(run_cli "$SBX" arm 2>"$SBX/stderr.log")
RC=$?
set -e
ERR=$(cat "$SBX/stderr.log")
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
echo "$OUT" | grep -qi "fehler" && pass "Fehlermeldung vorhanden" || fail "keine Fehlermeldung: $OUT"
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt: $(cat "$SBX/run/control.json")"
echo
echo "=== Fall 9: 'arm unbekannt' -> Fehler, Rueckgabewert != 0 ==="
echo "=== Fall 9: 'arm unbekannt' -> Fehler auf stderr, Rueckgabewert != 0 ==="
SBX="$TMPROOT/t9"
setup_sandbox "$SBX"
set +e
OUT=$(run_cli "$SBX" arm unbekannt 2>&1)
OUT=$(run_cli "$SBX" arm unbekannt 2>"$SBX/stderr.log")
RC=$?
set -e
ERR=$(cat "$SBX/stderr.log")
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
echo "$OUT" | grep -qi "fehler" && pass "Fehlermeldung vorhanden" || fail "keine Fehlermeldung: $OUT"
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt"
echo
@@ -295,6 +318,7 @@ RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "control.json: phase=listen" || fail "phase ist nicht 'listen': $(cat "$SBX/run/control.json" 2>&1)"
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
echo
echo "=== Fall 11: 'off' faehrt beide Profile herunter, stoppt den Dienst, zurueck ins Heim-WLAN ==="
@@ -318,8 +342,11 @@ echo
echo "=== Fall 12: 'status' im Migrationsmodus zeigt Modus, Phase und zuletzt erkannte Lampe ==="
SBX="$TMPROOT/t12"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
run_cli "$SBX" migrate on >/dev/null 2>&1 # phase=auto in control.json, wie es der echte Ablauf waere
# status.json so, wie server.py sie beim Start geschrieben haette (mode/last_seen).
cat >"$SBX/run/status.json" <<'JSON'
{"mode": "migration", "phase": "auto",
{"mode": "migration",
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
JSON
set +e
@@ -379,6 +406,89 @@ fi
DEVSHOW=$(PATH="$SBX/bin:$PATH" nmcli -t -f GENERAL.CONNECTION device show wlan0)
[ "$DEVSHOW" = "GENERAL.CONNECTION:qconnex-firmware" ] && pass "nmcli device show wlan0 bestaetigt qconnex-firmware" || fail "device show liefert: $DEVSHOW"
echo
echo "=== Zusatzfall C1: ohne RuntimeDirectoryPreserve=restart wuerde der Neustart mode/control.json wegwischen - das CLI muss das erkennen und abbrechen ==="
SBX="$TMPROOT/tC1"
BROKEN_UNIT="$TMPROOT/broken-ninlight-station.service"
grep -v '^RuntimeDirectoryPreserve=restart' "$REAL_UNIT" >"$BROKEN_UNIT"
setup_sandbox "$SBX" "$BROKEN_UNIT"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
set +e
OUT=$(run_cli "$SBX" migrate on --listen 2>"$SBX/stderr.log")
RC=$?
set -e
ERR=$(cat "$SBX/stderr.log")
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - eine geleerte RuntimeDirectory wird nicht als Erfolg gemeldet" || fail "Exit-Code war 0, obwohl RUN_DIR beim Neustart geleert wurde: $OUT"
echo "$ERR" | grep -qi "RuntimeDirectoryPreserve" && pass "Fehlermeldung nennt RuntimeDirectoryPreserve als Ursache" || fail "Fehlermeldung nennt die Ursache nicht: $ERR"
echo "$OUT" | grep -q "Migration an" && fail "CLI meldete faelschlich 'Migration an', obwohl die Phase nach dem Neustart nicht mehr stimmte" || pass "kein falscher Erfolgsmeldung 'Migration an'"
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
[ -z "$ACTIVE_AFTER" ] && pass "Zugangspunkt wurde nach dem erkannten Fehler wieder heruntergefahren" || fail "Zugangspunkt blieb aktiv: $ACTIVE_AFTER"
# Gegenprobe: dieselbe Sandbox, aber mit der ECHTEN (reparierten) Unit-Datei -
# jetzt darf RUN_DIR beim Neustart nicht geleert werden und der Aufruf muss
# durchlaufen. Das ist die eigentliche Bestaetigung, dass RuntimeDirectoryPreserve
# in tools/station/ninlight-station.service tatsaechlich steht und wirkt.
SBX2="$TMPROOT/tC1-fixed"
setup_sandbox "$SBX2"
echo "dummy" >"$SBX2/images/migrate/gu10_rgb_prod.img"
set +e
OUT2=$(run_cli "$SBX2" migrate on --listen 2>&1)
RC2=$?
set -e
[ "$RC2" -eq 0 ] && pass "mit der echten (reparierten) Unit-Datei gelingt derselbe Aufruf" || fail "Gegenprobe schlug fehl - Exit-Code $RC2: $OUT2"
[ "$(control_phase "$SBX2/run/control.json")" = "listen" ] && pass "Gegenprobe: control.json ueberlebt den Neustart mit phase=listen" || fail "Gegenprobe: Phase nach Neustart falsch: $(cat "$SBX2/run/control.json" 2>&1)"
echo
echo "=== Zusatzfall I1: 'status' zeigt die Phase aus control.json, nicht die veraltete Phase aus status.json ==="
SBX="$TMPROOT/tI1"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
# status.json wird nur beim Dienststart/bei einer Auslieferung geschrieben - hier
# simuliert, wie es nach dem Start aussehen wuerde (Phase "listen", wie beim Start).
cat >"$SBX/run/status.json" <<'JSON'
{"mode": "migration", "phase": "listen",
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
JSON
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # startet den Dienst NICHT neu - status.json bleibt bei "listen"
OUT=$(run_cli "$SBX" status 2>&1)
echo "$OUT" | grep -q "Phase.*armed" && pass "status zeigt 'armed' (aus control.json), nicht das veraltete 'listen' aus status.json" || fail "status zeigt die falsche/veraltete Phase: $OUT"
echo "$OUT" | grep -q "gu10_rgb" && pass "status nennt das freigegebene Ziel" || fail "status nennt das armed_target nicht: $OUT"
echo
echo "=== Zusatzfall I2: 'off' bricht mit Fehler ab, wenn ein Stationsprofil trotz 'con down' aktiv bleibt (haengender NetworkManager) ==="
SBX="$TMPROOT/tI2"
setup_sandbox "$SBX"
run_cli "$SBX" on >/dev/null 2>&1
# HOME_CONNECTION aus den bekannten Profilen entfernen, um zugleich den in I2
# genannten zweiten Umstand abzudecken (Zielprofil fuers Heim-WLAN fehlt).
grep -v '^home-wifi$' "$SBX/nmstate/profiles" >"$SBX/nmstate/profiles.tmp"
mv "$SBX/nmstate/profiles.tmp" "$SBX/nmstate/profiles"
set +e
OUT=$(FAKE_NMCLI_STUCK=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
RC=$?
set -e
ERR=$(cat "$SBX/stderr.log")
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - ein haengender Zugangspunkt wird nicht als 'Station aus' gemeldet" || fail "Exit-Code war 0, obwohl ninlight-recovery weiterhin aktiv war: $OUT"
echo "$OUT" | grep -q "Station aus" && fail "CLI behauptete faelschlich 'Station aus'" || pass "keine falsche 'Station aus'-Meldung"
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr vorhanden" || fail "keine Fehlermeldung auf stderr: $ERR"
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
[ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "ninlight-recovery ist (wie vom Stub simuliert) tatsaechlich noch aktiv" || fail "unerwarteter nmcli-Zustand: $ACTIVE_AFTER"
echo
echo "=== Zusatzfall I3: ap_switch bricht ab und faehrt beide Profile herunter, wenn 'device show wlan0' nach dem Umschalten nicht passt ==="
SBX="$TMPROOT/tI3"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
set +e
OUT=$(FAKE_NMCLI_DEVSHOW=fremdes-profil run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
RC=$?
set -e
ERR=$(cat "$SBX/stderr.log")
[ "$RC" -ne 0 ] && pass "Exit-Code != 0, wenn wlan0 nach dem Umschalten ein fremdes Profil zeigt" || fail "Exit-Code war 0 trotz Profil-Mismatch: $OUT"
echo "$ERR" | grep -q "unerwartet" && pass "Fehlermeldung nennt den Mismatch" || fail "Fehlermeldung fehlt: $ERR"
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
[ -z "$ACTIVE_AFTER" ] && pass "beide Stationsprofile wurden nach dem erkannten Mismatch heruntergefahren" || fail "mindestens ein Profil blieb aktiv: $ACTIVE_AFTER"
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# Stub-Tests fuer setup_pi.sh - ohne Pi ausfuehrbar.
#
# ssh/scp werden im PATH durch Stubs ersetzt (Muster wie tests/test_deploy.sh):
# ssh fuehrt das per Heredoc uebergebene Remote-Skript lokal aus, scp uebersetzt
# "host:pfad"-Ziele auf den blossen lokalen Pfad und ruft dann "cp" auf. Die
# System-Zielpfade (/etc/ninlight-station.conf, /opt/ninlight-station, ...) sind
# per Umgebungsvariable auf ein Sandbox-Verzeichnis umgebogen (dasselbe Prinzip
# wie NINLIGHT_STATION_ROOT/_BIN/_UNIT in deploy.sh) - setup_pi.sh fasst damit
# nichts auf dem Testrechner selbst an ausser dem transienten /tmp, das schon
# deploy.sh fuer denselben Zweck benutzt.
#
# Schwerpunkt: Fixrunde 1, I4 - eine bestehende Konfiguration muss um fehlende
# Schluessel ergaenzt werden, ohne vorhandene Werte anzutasten.
#
# Aufruf: tools/station/tests/test_setup_pi.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
STATION_DIR=$(cd "$HERE/.." && pwd)
TMPROOT=$(mktemp -d)
trap 'rm -rf "$TMPROOT"' EXIT
FAILED=0
pass() { echo "ok - $1"; }
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
setup_stubbin() {
local bindir=$1 log=$2
mkdir -p "$bindir"
cat >"$bindir/ssh" <<EOF
#!/usr/bin/env bash
echo "STUB ssh \$*" >>"$log"
host=\$1; shift
"\$@"
EOF
cat >"$bindir/scp" <<EOF
#!/usr/bin/env bash
echo "STUB scp \$*" >>"$log"
args=()
for a in "\$@"; do
case "\$a" in
-*) continue ;;
*:*) args+=("\${a#*:}") ;;
*) args+=("\$a") ;;
esac
done
exec cp "\${args[@]}"
EOF
# "sudo install -o root -g root ..." (im NetworkManager-Keyfile-Schritt) braucht
# echtes Root, um den Eigentuemer zu setzen - hat der Testrechner (anders als der
# Pi) nicht. Der Stub streicht -o/-g nur fuer "install", der Rest laeuft wie auf
# dem Pi ueber ein exec des ungestubbten Kommandos.
cat >"$bindir/sudo" <<EOF
#!/usr/bin/env bash
echo "STUB sudo \$*" >>"$log"
if [ "\$1" = "install" ]; then
args=()
skip=0
for a in "\$@"; do
if [ "\$skip" -eq 1 ]; then skip=0; continue; fi
case "\$a" in
-o|-g) skip=1 ;;
*) args+=("\$a") ;;
esac
done
exec "\${args[@]}"
fi
exec "\$@"
EOF
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
case "\$1" in
is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}" ;;
*) exit 0 ;;
esac
EOF
cat >"$bindir/nmcli" <<EOF
#!/usr/bin/env bash
echo "STUB nmcli \$*" >>"$log"
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "DEVICE,STATE" ]; then
echo "wlan0:connected"
exit 0
fi
exit 0
EOF
chmod +x "$bindir"/ssh "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl "$bindir"/nmcli
}
# $1 = Sandbox-Wurzel fuer die "Systempfade"
setup_state() {
local state=$1
rm -rf "$state"
mkdir -p "$state/etc" "$state/opt" "$state/usr/local/bin" "$state/etc/systemd/system" \
"$state/etc/NetworkManager/system-connections"
}
# Fuehrt setup_pi.sh mit gestubbtem PATH und auf die Sandbox umgebogenen
# Systempfaden aus. $1 = state-Verzeichnis, $2 = stub-bin-Verzeichnis.
run_setup() {
local state=$1 stubbin=$2
PATH="$stubbin:$PATH" \
NINLIGHT_SSH_BIN="$stubbin/ssh" \
NINLIGHT_SCP_BIN="$stubbin/scp" \
NINLIGHT_STATION_CONF="$state/etc/ninlight-station.conf" \
NINLIGHT_STATION_ROOT="$state/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$state/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \
NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \
"$STATION_DIR/setup_pi.sh" pi-test
}
echo "=== Szenario 1: frischer Pi ohne Konfiguration -> Vorlage wird komplett angelegt ==="
STATE="$TMPROOT/s1-state"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
if diff -q "$STATION_DIR/station.conf.example" "$STATE/etc/ninlight-station.conf" >/dev/null 2>&1; then
pass "Konfiguration wurde 1:1 aus der Vorlage angelegt"
else
fail "Konfiguration weicht von der Vorlage ab oder fehlt"
fi
[ -x "$STATE/opt/ninlight-station/server.py" ] && pass "server.py installiert" || fail "server.py fehlt"
[ -f "$STATE/opt/ninlight-station/products.py" ] && pass "products.py installiert" || fail "products.py fehlt (arm braucht es)"
[ -x "$STATE/usr/local/bin/ninlight-station" ] && pass "CLI installiert" || fail "ninlight-station CLI fehlt"
[ -f "$STATE/etc/systemd/system/ninlight-station.service" ] && pass "Unit installiert" || fail "Unit fehlt"
[ -d "$STATE/opt/ninlight-station/images" ] && pass "images/-Verzeichnis angelegt" || fail "images/-Verzeichnis fehlt"
echo
echo "=== Szenario 2 (Fixrunde 1, I4): bestehende Konfiguration ohne die neuen Schluessel wird ergaenzt, nicht ersetzt ==="
STATE="$TMPROOT/s2-state"; STUBBIN="$TMPROOT/bin2"; LOG="$TMPROOT/s2.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
cat >"$STATE/etc/ninlight-station.conf" <<'CONF'
SSID='eigene-ssid'
PSK='eigenes-passwort'
IMAGE_DIR='/opt/ninlight-station/images'
HOME_CONNECTION='preconfigured'
AP_CONNECTION='ninlight-recovery'
CONF
chmod 0600 "$STATE/etc/ninlight-station.conf"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
echo "$OUT" | grep -q "ergaenzt" && pass "Ausgabe kuendigt das Ergaenzen fehlender Schluessel an" || fail "Ausgabe erwaehnt 'ergaenzt' nicht: $OUT"
CONF_OUT="$STATE/etc/ninlight-station.conf"
if grep -Fqx "SSID='eigene-ssid'" "$CONF_OUT" && grep -Fqx "PSK='eigenes-passwort'" "$CONF_OUT"; then
pass "vorhandene Werte (SSID/PSK) blieben unveraendert"
else
fail "vorhandene Werte wurden ueberschrieben: $(cat "$CONF_OUT")"
fi
for KEY in MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
if grep -Eq "^${KEY}=" "$CONF_OUT"; then
pass "fehlender Schluessel $KEY wurde ergaenzt"
else
fail "Schluessel $KEY fehlt weiterhin in der Konfiguration"
fi
done
MODE=$(stat -f '%Lp' "$CONF_OUT" 2>/dev/null || stat -c '%a' "$CONF_OUT" 2>/dev/null)
[ "$MODE" = "600" ] && pass "Konfigurationsdatei bleibt 0600" || fail "Konfigurationsdatei hat falsche Rechte: $MODE"
echo
echo "=== Szenario 3: erneuter Lauf auf einer bereits vollstaendigen Konfiguration ist ein echtes No-Op ==="
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
echo "$OUT" | grep -q "alle Schluessel vorhanden" && pass "meldet 'alle Schluessel vorhanden' statt erneut zu ergaenzen" || fail "Ausgabe: $OUT"
DUP=$(grep -c '^EPD_REVISION=' "$CONF_OUT")
[ "$DUP" -eq 1 ] && pass "EPD_REVISION steht nur einmal in der Datei (kein doppeltes Anhaengen)" || fail "EPD_REVISION steht $DUP mal in der Datei"
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
else
echo "Mindestens ein Test ist fehlgeschlagen." >&2
fi
exit "$FAILED"