Files
ninlight-w600/tools/station/tests/test_deploy.sh
T
Michael Sauer b41ed51018 feat(station): Migrationsroute mit Horch- und Freigabephase
server.py bedient jetzt zwei Modi ueber denselben Dienst: --mode/--mode-file/--control
legen fest, ob ninlight_<target>_ota.img (Rettung) oder qcx_firmware_<pid>_..._ota_light.img
(Migration alter Klyqa-Lampen, via products.parse_request) ausgeliefert wird. Der Modus
kommt wahlweise aus einer Datei statt einem Startparameter, damit Task 4 ihn spaeter ohne
Dienst-Neustart umschalten kann. Die Steuerdatei control.json (Phase auto/listen/armed)
wird bei jeder Anfrage frisch gelesen und atomar mit 0644 geschrieben - eine fehlende oder
kaputte Steuerdatei blockiert eine Migration nie, sondern faellt auf "auto" zurueck. Nach
einer erfolgreichen Auslieferung in Phase "armed" faellt die Freigabe erst nach dem
vollstaendigen Senden auf "listen" zurueck, nie davor, damit ein abgebrochener Transfer die
Freigabe fuer den naechsten Bootversuch stehen laesst. status.json bekommt die
Migrationsfelder aus Spec 5.2 (mode, phase, armed_target, last_seen).

deploy.sh liefert products.py mit aus, sonst startet der Dienst nach dem naechsten Deploy
mit ModuleNotFoundError; test_deploy.sh zieht die Sandbox-Dummy-Datei entsprechend nach.

Neue Tests in test_server.py verwenden eigene server/migration_server-Fixtures samt
Testklient (get/status/set_control/read_control/delete_control/write_raw_control), ohne die
bestehende station-Fixture und ihre Tests anzufassen.
2026-09-13 18:06:25 +02:00

367 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Stub-Tests fuer deploy.sh - ohne Pi ausfuehrbar.
#
# ssh/rsync werden im PATH durch Stubs ersetzt: ssh fuehrt das per Heredoc
# uebergebene Remote-Skript lokal aus (statt es an einen echten Pi zu
# schicken), rsync uebersetzt "host:pfad"-Ziele auf den blossen lokalen Pfad
# und ruft dann den echten rsync auf. So laeuft die komplette Logik von
# deploy.sh - inklusive des Installationsschritts, der normalerweise per SSH
# auf dem Pi passiert - tatsaechlich, aber vollstaendig gegen ein Sandbox-
# Verzeichnis (NINLIGHT_STATION_ROOT/…) statt gegen echte Systempfade.
#
# Aufruf: tools/station/tests/test_deploy.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
STATION_DIR=$(cd "$HERE/.." && pwd)
REAL_RSYNC=$(command -v rsync)
TMPROOT=$(mktemp -d)
trap 'rm -rf "$TMPROOT"' EXIT
FAILED=0
pass() { echo "ok - $1"; }
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
# --- Sandbox-Repo aufbauen: tools/station/deploy.sh + tools/mkimage/nl_mkimage.py + build/ ---
setup_sandbox() {
local sandbox=$1
rm -rf "$sandbox"
mkdir -p "$sandbox/tools/station" "$sandbox/tools/mkimage" "$sandbox/build"
cp "$STATION_DIR/deploy.sh" "$sandbox/tools/station/deploy.sh"
chmod +x "$sandbox/tools/station/deploy.sh"
# Inhalt irrelevant, die Dateien muessen nur existieren (werden "uebertragen").
echo "# dummy" >"$sandbox/tools/station/server.py"
echo "# dummy" >"$sandbox/tools/station/products.py"
echo "# dummy" >"$sandbox/tools/station/ninlight-station"
echo "# dummy" >"$sandbox/tools/station/ninlight-station.service"
# Fake nl_mkimage.py --verify: verhaelt sich wie das echte Tool (eine Zeile
# "<geraet> <version> <bytes>" auf stdout bei Erfolg, Exit 0; eine Zeile auf
# stderr bei Fehlschlag, Exit 1), aber steuerbar per Umgebungsvariable
# MKIMAGE_FAIL_SUBSTR: enthaelt der zu pruefende Dateiname diesen Teilstring,
# schlaegt die Pruefung fehl.
cat >"$sandbox/tools/mkimage/nl_mkimage.py" <<'PY'
import os
import sys
args = sys.argv[1:]
f = args[args.index("--verify") + 1]
target = args[args.index("--expect-device") + 1]
fail_substr = os.environ.get("MKIMAGE_FAIL_SUBSTR", "")
if fail_substr and fail_substr in f:
print(f"Fehler: {f} besteht die Trailer-Pruefung nicht (Test-Stub)", file=sys.stderr)
sys.exit(1)
size = os.path.getsize(f)
print(f"{target} 1.0.0 {size}")
PY
}
# --- Stub-Kommandos fuer ssh/rsync im PATH ---
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/rsync" <<EOF
#!/usr/bin/env bash
echo "STUB rsync \$*" >>"$log"
args=()
for a in "\$@"; do
case "\$a" in
-*) args+=("\$a") ;;
*:*) args+=("\${a#*:}") ;;
*) args+=("\$a") ;;
esac
done
exec "$REAL_RSYNC" "\${args[@]}"
EOF
cat >"$bindir/scp" <<EOF
#!/usr/bin/env bash
echo "STUB scp \$* - deploy.sh sollte scp nicht aufrufen" >>"$log"
exit 1
EOF
cat >"$bindir/sudo" <<EOF
#!/usr/bin/env bash
echo "STUB sudo \$*" >>"$log"
exec "\$@"
EOF
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
case "\$1" in
is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;;
*) exit 0 ;;
esac
EOF
chmod +x "$bindir"/ssh "$bindir"/rsync "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl
}
run_deploy() {
# Fuehrt deploy.sh mit stubbtem PATH aus; Ausgabe landet in $OUT, Exit-Code in $?.
local sandbox=$1; shift
# Auf dem echten Pi existieren /usr/local/bin und /etc/systemd/system schon;
# in der Sandbox muessen sie fuer "install" eigens angelegt werden.
mkdir -p "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system"
PATH="$STUBBIN:$PATH" \
NINLIGHT_SSH_BIN="$STUBBIN/ssh" \
NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \
"$sandbox/tools/station/deploy.sh" "$@"
}
echo "=== Szenario 1: Image besteht die Trailer-Pruefung nicht -> nichts uebertragen ==="
SANDBOX="$TMPROOT/s1"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"; SANDBOX_STATE="$TMPROOT/s1-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "dummy" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(MKIMAGE_FAIL_SUBSTR=foo run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi
if echo "$OUT" | grep -q "besteht die Trailer-Pruefung nicht"; then
pass "Fehlermeldung zur Trailer-Pruefung vorhanden"
else
fail "Fehlermeldung zur Trailer-Pruefung fehlt"
fi
if ! grep -q "STUB ssh\|STUB rsync\|STUB scp" "$LOG"; then
pass "kein ssh/rsync/scp aufgerufen - nichts wurde uebertragen"
else
fail "ssh/rsync/scp wurde trotz fehlgeschlagener Pruefung aufgerufen: $(cat "$LOG")"
fi
echo
echo "=== Szenario 2: keine Images unter build/ gefunden ==="
SANDBOX="$TMPROOT/s2"; STUBBIN="$TMPROOT/bin2"; LOG="$TMPROOT/s2.log"; SANDBOX_STATE="$TMPROOT/s2-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi
if echo "$OUT" | grep -q "keine Images unter"; then
pass "Fehlermeldung zu fehlenden Images vorhanden"
else
fail "Fehlermeldung zu fehlenden Images fehlt"
fi
if ! grep -q "STUB" "$LOG" 2>/dev/null; then
pass "kein Transfer-Kommando aufgerufen"
else
fail "ein Transfer-Kommando wurde trotz fehlender Images aufgerufen: $(cat "$LOG")"
fi
echo
echo "=== Szenario 3: gezielter Deploy eines Geraetetyps laesst andere Images auf dem Ziel unangetastet (Critical 2) ==="
SANDBOX="$TMPROOT/s3"; STUBBIN="$TMPROOT/bin3"; LOG="$TMPROOT/s3.log"; SANDBOX_STATE="$TMPROOT/s3-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar"
echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
echo "bar-image" >"$SANDBOX/build/bar/ninlight_bar_ota.img"
# Zielverzeichnis simuliert einen vorherigen Voll-Deploy: Images fuer foo UND bar liegen schon da.
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "foo-image-alt" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img"
echo "bar-image" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q "andere Geraetetypen im Zielverzeichnis bleiben erhalten"; then
pass "Ausgabe kuendigt gezielten Deploy ohne --delete an"
else
fail "Ausgabe erwaehnt den gezielten Deploy nicht"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img" ]; then
pass "Image des NICHT ausgewaehlten Geraetetyps (bar) blieb erhalten"
else
fail "Image des nicht ausgewaehlten Geraetetyps (bar) wurde geloescht - Critical 2 nicht behoben"
fi
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ] \
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img")" = "foo-image-neu" ]; then
pass "Image des ausgewaehlten Geraetetyps (foo) wurde aktualisiert"
else
fail "Image des ausgewaehlten Geraetetyps (foo) wurde nicht aktualisiert"
fi
echo
echo "=== Szenario 4: Voll-Deploy (kein target) entfernt veraltete Images - zur Abgrenzung von Szenario 3 ==="
SANDBOX="$TMPROOT/s4"; STUBBIN="$TMPROOT/bin4"; LOG="$TMPROOT/s4.log"; SANDBOX_STATE="$TMPROOT/s4-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" ]; then
pass "veraltetes Image wurde beim Voll-Deploy entfernt (--delete wirkt weiterhin, wenn gewollt)"
else
fail "veraltetes Image wurde beim Voll-Deploy NICHT entfernt"
fi
echo
echo "=== Szenario 5: Dienst laeuft, Neustart nach dem Deploy schlaegt fehl -> Exit != 0 (Minor) ==="
SANDBOX="$TMPROOT/s5"; STUBBIN="$TMPROOT/bin5"; LOG="$TMPROOT/s5.log"; SANDBOX_STATE="$TMPROOT/s5-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo" "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 \
PATH="$STUBBIN:$PATH" \
NINLIGHT_SSH_BIN="$STUBBIN/ssh" \
NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \
"$SANDBOX/tools/station/deploy.sh" pi-test foo 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0, wenn der Neustart fehlschlaegt ($RC)"; else fail "Exit-Code war 0, obwohl der Neustart fehlschlug"; fi
if echo "$OUT" | grep -q "Neustart nach dem Deploy ist aber fehlgeschlagen"; then
pass "Fehlermeldung unterscheidet Neustart-Fehlschlag von 'Dienst laeuft nicht'"
else
fail "Fehlermeldung fehlt oder ist mehrdeutig"
fi
if echo "$OUT" | grep -q "Dienst laeuft nicht"; then
fail "faelschlich 'Dienst laeuft nicht' gemeldet, obwohl der Dienst lief"
else
pass "'Dienst laeuft nicht' wird nicht faelschlich gemeldet"
fi
echo
echo "=== Szenario 6: deploy.sh/setup_pi.sh nutzen ohne Override /usr/bin/ssh (bzw. scp) ==="
# Die anderen Szenarien laufen mit NINLIGHT_SSH_BIN auf einen Stub umgebogen - das prueft die
# Logik, aber nicht mehr, WELCHEN ssh das Skript ohne Override tatsaechlich waehlt. Der Nutzer
# verlangt ausdruecklich /usr/bin/ssh und /usr/bin/scp statt einer Variante im PATH; das hier
# ist die Absicherung dagegen, dass der Default beim naechsten Umbau stillschweigend zurueck
# auf ein PATH-"ssh" rutscht.
if grep -q 'SSH_BIN=\${NINLIGHT_SSH_BIN:-/usr/bin/ssh}' "$STATION_DIR/deploy.sh"; then
pass "deploy.sh: Default fuer SSH_BIN ist /usr/bin/ssh"
else
fail "deploy.sh: Default fuer SSH_BIN ist nicht mehr /usr/bin/ssh"
fi
if grep -q 'SSH_BIN=\${NINLIGHT_SSH_BIN:-/usr/bin/ssh}' "$STATION_DIR/setup_pi.sh"; then
pass "setup_pi.sh: Default fuer SSH_BIN ist /usr/bin/ssh"
else
fail "setup_pi.sh: Default fuer SSH_BIN ist nicht mehr /usr/bin/ssh"
fi
if grep -q 'SCP_BIN=\${NINLIGHT_SCP_BIN:-/usr/bin/scp}' "$STATION_DIR/setup_pi.sh"; then
pass "setup_pi.sh: Default fuer SCP_BIN ist /usr/bin/scp"
else
fail "setup_pi.sh: Default fuer SCP_BIN ist nicht mehr /usr/bin/scp"
fi
echo
echo "=== Szenario 7: Voll-Deploy mit zwei Build-Verzeichnissen fuer denselben Dateinamen -> Abbruch (B1) ==="
SANDBOX="$TMPROOT/s7"; STUBBIN="$TMPROOT/bin7"; LOG="$TMPROOT/s7.log"; SANDBOX_STATE="$TMPROOT/s7-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/evk" "$SANDBOX/build/evk-onb"
echo "evk-mit-wlan" >"$SANDBOX/build/evk/ninlight_evk_ota.img"
echo "evk-ohne-wlan" >"$SANDBOX/build/evk-onb/ninlight_evk_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0, obwohl zwei Fundorte denselben Dateinamen liefern"; fi
if echo "$OUT" | grep -q "ninlight_evk_ota.img mehrfach gefunden"; then
pass "Fehlermeldung nennt den doppelten Dateinamen"
else
fail "Fehlermeldung zum doppelten Dateinamen fehlt: $OUT"
fi
if echo "$OUT" | grep -q "build/evk/ninlight_evk_ota.img" && echo "$OUT" | grep -q "build/evk-onb/ninlight_evk_ota.img"; then
pass "Fehlermeldung nennt beide Fundorte"
else
fail "Fehlermeldung nennt nicht beide Fundorte: $OUT"
fi
if ! grep -q "STUB" "$LOG" 2>/dev/null; then
pass "kein Transfer-Kommando aufgerufen - nichts wurde stillschweigend uebertragen"
else
fail "ein Transfer-Kommando wurde trotz doppeltem Dateinamen aufgerufen: $(cat "$LOG")"
fi
echo
echo "=== Szenario 8: Voll-Deploy zeigt vor dem Loeschen, welche Images verschwinden (B2) ==="
SANDBOX="$TMPROOT/s8"; STUBBIN="$TMPROOT/bin8"; LOG="$TMPROOT/s8.log"; SANDBOX_STATE="$TMPROOT/s8-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images"
echo "veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img"
echo "auch-veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_e14_cw_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q "folgende Images werden entfernt"; then
pass "Ausgabe kuendigt die zu entfernenden Images an, bevor geloescht wird"
else
fail "Ausgabe nennt die zu entfernenden Images nicht: $OUT"
fi
if echo "$OUT" | grep -q "ninlight_veraltet_ota.img" && echo "$OUT" | grep -q "ninlight_e14_cw_ota.img"; then
pass "beide zu entfernenden Dateien werden namentlich genannt"
else
fail "nicht alle zu entfernenden Dateien werden genannt: $OUT"
fi
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" ]; then
pass "veraltetes Image wurde nach der Ankuendigung tatsaechlich entfernt"
else
fail "veraltetes Image wurde trotz Ankuendigung nicht entfernt"
fi
echo
echo "=== Szenario 9: Voll-Deploy ohne verwaiste Images meldet das statt zu schweigen (B2) ==="
SANDBOX="$TMPROOT/s9"; STUBBIN="$TMPROOT/bin9"; LOG="$TMPROOT/s9.log"; SANDBOX_STATE="$TMPROOT/s9-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
if echo "$OUT" | grep -q "keine Images werden entfernt"; then
pass "Ausgabe meldet explizit, dass nichts entfernt wird"
else
fail "Ausgabe meldet nicht, dass nichts entfernt wird: $OUT"
fi
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
else
echo "Mindestens ein Test ist fehlgeschlagen." >&2
fi
exit "$FAILED"