Fixrunde 2 zu Task 4:
- N1 (wichtig): file_mode() in test_cli.sh/test_setup_pi.sh probierte
"stat -f '%Lp'" vor "stat -c '%a'" - auf GNU-coreutils (dem Pi) deutet
"-f" dort "Dateisystemstatus" und liefert Muell statt der Rechte-Zahl,
faellt aber trotzdem in den "||"-Zweig durch, der zusaetzlich "644"
anhaengt. Reihenfolge umgedreht und mit einem per gstat erzwungenen
GNU-Testlauf zusaetzlich abgesichert.
- N2 (wichtig): status zeigte nach einem Wechsel zurueck in den
Rettungsmodus weiterhin die letzte Migrationsphase aus einer liegen
gebliebenen control.json (moeglich seit RuntimeDirectoryPreserve=
restart aus Fixrunde 1). Phase wird jetzt nur noch angezeigt, wenn
Modus tatsaechlich "migration" ist.
- N3 (klein): die Schluesselerkennung in setup_pi.sh war rein
zeilenverankert ("^KEY=") und haette einen eingerueckten Bestandswert
als fehlend gewertet - beim naechsten Sourcen der Konfiguration haette
die zusaetzlich angehaengte Vorlagenzeile das eigene Passwort
ueberschrieben. Muster auf fuehrenden Leerraum/export gehaertet.
- N4 (klein): test_setup_pi.sh liess setup_pi.sh ueber den scp-Stub echte
Dateien im /tmp des Entwicklungsrechners anlegen und wieder loeschen.
Neue Variable NINLIGHT_STAGE_DIR biegt das auf eine Sandbox um.
Alle vier Punkte mit Regressionstests, N2 und N3 zusaetzlich per
Mutationsprobe (Fix zurueckgenommen, Test wird rot) verifiziert.
559 lines
27 KiB
Bash
Executable File
559 lines
27 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Stub-Tests fuer ninlight-station (das CLI, nicht server.py) - ohne Pi und ohne
|
|
# echte NetworkManager-/systemd-Rechte ausfuehrbar.
|
|
#
|
|
# nmcli, systemctl und sudo werden im PATH durch Stubs ersetzt, die ihre Aufrufe
|
|
# in eine Logdatei schreiben (Muster wie tests/test_deploy.sh fuer ssh/rsync).
|
|
# 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. 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
|
|
|
|
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
|
|
|
|
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". Reihenfolge ist wichtig
|
|
# (Fixrunde 2, N1): "stat -f '%Lp' datei" auf GNU-coreutils deutet "-f" als
|
|
# "Dateisystemstatus" und '%Lp' als ZWEITES Dateiargument - der Aufruf schreibt
|
|
# mehrere Zeilen Dateisystem-Infos auf stdout und scheitert (kein passendes
|
|
# zweites File), aber der "||"-Zweig haengt DANACH trotzdem noch "644" an, und
|
|
# der Vergleich "= 644" schlaegt fehl, obwohl die Rechte stimmen - auf dem Pi
|
|
# (echtes GNU-coreutils) also immer rot. BSD-"stat -c" dagegen bricht sofort
|
|
# mit einer Fehlermeldung auf stderr ab, ganz ohne Ausgabe auf stdout - der
|
|
# GNU-Aufruf muss deshalb ZUERST stehen.
|
|
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null
|
|
}
|
|
|
|
# Erzwingt den GNU-Zweig von file_mode() (der auf dem Pi tatsaechlich laeuft),
|
|
# indem "stat" im PATH auf GNU-coreutils' "gstat" umgebogen wird, falls
|
|
# vorhanden (macOS mit "brew install coreutils"). Ohne diese Probe wuerde ein
|
|
# falsch herum sortiertes file_mode() auf dieser Entwicklungsmaschine (BSD-stat
|
|
# zuerst probiert und erfolgreich) unbemerkt bleiben, obwohl es auf dem Pi
|
|
# (nur GNU-stat) rot waere - genau der Fehler aus Fixrunde 2, N1.
|
|
if command -v gstat >/dev/null 2>&1; then
|
|
GSTAT_CHECK_DIR="$TMPROOT/gstat-check"
|
|
mkdir -p "$GSTAT_CHECK_DIR/bin"
|
|
ln -sf "$(command -v gstat)" "$GSTAT_CHECK_DIR/bin/stat"
|
|
GSTAT_CHECK_FILE="$TMPROOT/gstat-check/probe"
|
|
: >"$GSTAT_CHECK_FILE"
|
|
chmod 0644 "$GSTAT_CHECK_FILE"
|
|
GSTAT_MODE=$(PATH="$GSTAT_CHECK_DIR/bin:$PATH" file_mode "$GSTAT_CHECK_FILE")
|
|
[ "$GSTAT_MODE" = "644" ] && pass "file_mode() liefert unter erzwungenem GNU-stat korrekt '644'" || fail "file_mode() liefert unter GNU-stat '$GSTAT_MODE' statt '644' - siehe Fixrunde 2, N1"
|
|
else
|
|
echo "Hinweis: gstat nicht gefunden - GNU-Zweig von file_mode() wird nur auf dem Pi selbst geprueft" >&2
|
|
fi
|
|
|
|
# --- Stub-Kommandos fuer nmcli/systemctl/sudo im PATH ---
|
|
# $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 rundir=$4 unitfile=$5
|
|
mkdir -p "$bindir" "$nmstate"
|
|
: >"$nmstate/active" # Reihenfolge der per "con up" aktiven Profile, eine Zeile je Profil
|
|
|
|
cat >"$bindir/nmcli" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB nmcli \$*" >>"$log"
|
|
NMSTATE="$nmstate"
|
|
|
|
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "NAME" ] && [ "\$4" = "con" ] && [ "\$5" = "show" ]; then
|
|
cat "\$NMSTATE/profiles" 2>/dev/null
|
|
exit 0
|
|
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
|
|
fi
|
|
|
|
if [ "\$1" = "con" ] && [ "\$2" = "up" ]; then
|
|
profile=\$3
|
|
grep -qxF "\$profile" "\$NMSTATE/profiles" 2>/dev/null || { echo "Error: unknown connection '\$profile'." >&2; exit 1; }
|
|
grep -qxF "\$profile" "\$NMSTATE/active" 2>/dev/null || echo "\$profile" >>"\$NMSTATE/active"
|
|
exit 0
|
|
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"
|
|
exit 0
|
|
fi
|
|
|
|
exit 0
|
|
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
|
|
echo active
|
|
exit 0
|
|
else
|
|
echo inactive
|
|
exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT}"
|
|
fi
|
|
;;
|
|
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
|
|
|
|
cat >"$bindir/sudo" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB sudo \$*" >>"$log"
|
|
exec "\$@"
|
|
EOF
|
|
|
|
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo
|
|
}
|
|
|
|
# --- Sandbox fuer einen Testfall: eigene conf/RUN_DIR/IMAGE_DIR/nmcli-Zustand ---
|
|
# $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 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"
|
|
|
|
cat >"$sandbox/station.conf" <<EOF
|
|
SSID='test-recovery'
|
|
PSK='test-psk'
|
|
IMAGE_DIR='$sandbox/images'
|
|
HOME_CONNECTION='home-wifi'
|
|
AP_CONNECTION='ninlight-recovery'
|
|
MIGRATE_SSID='qconnex-firmware'
|
|
MIGRATE_PSK='qconnex-firmware'
|
|
MIGRATE_AP_CONNECTION='qconnex-firmware'
|
|
EPD_REVISION='V3'
|
|
EOF
|
|
|
|
printf 'ninlight-recovery\nqconnex-firmware\nhome-wifi\n' >"$sandbox/nmstate/profiles"
|
|
: >"$sandbox/nmstate/active"
|
|
|
|
setup_stubbin "$sandbox/bin" "$sandbox/cmd.log" "$sandbox/nmstate" "$sandbox/run" "$unitfile"
|
|
}
|
|
|
|
# Fuehrt ninlight-station mit gestubbtem PATH und ueberschriebenen Pfaden aus.
|
|
# $1 = sandbox-Verzeichnis, Rest = Argumente fuer ninlight-station.
|
|
run_cli() {
|
|
local sandbox=$1; shift
|
|
PATH="$sandbox/bin:$PATH" \
|
|
NINLIGHT_STATION_CONF="$sandbox/station.conf" \
|
|
NINLIGHT_RUN_DIR="$sandbox/run" \
|
|
NINLIGHT_STATION_LIB="$STATION_DIR" \
|
|
"$CLI" "$@"
|
|
}
|
|
|
|
control_phase() {
|
|
# $1 = Pfad zu control.json
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('phase','?'))" "$1" 2>/dev/null || echo "FEHLT"
|
|
}
|
|
control_target() {
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('armed_target'))" "$1" 2>/dev/null || echo "FEHLT"
|
|
}
|
|
|
|
echo "=== Fall 1: 'on' faehrt ninlight-recovery hoch, startet den Dienst neu, meldet 'Station an' ==="
|
|
SBX="$TMPROOT/t1"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
|
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch: $(cat "$SBX/run/mode" 2>&1)"
|
|
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 2: 'recovery on' identisch zu 'on' (Rueckwaertskompatibilitaet) ==="
|
|
SBX="$TMPROOT/t2"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" recovery on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
|
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch"
|
|
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 3: 'migrate on' faehrt ninlight-recovery herunter, BEVOR qconnex-firmware hochkommt; control.json phase=auto ==="
|
|
SBX="$TMPROOT/t3"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1 # Recovery vorher an
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
DOWN_LINE=$(grep -n "nmcli con down ninlight-recovery" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
|
|
UP_LINE=$(grep -n "nmcli con up qconnex-firmware" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
|
|
if [ -n "$DOWN_LINE" ] && [ -n "$UP_LINE" ] && [ "$DOWN_LINE" -lt "$UP_LINE" ]; then
|
|
pass "ninlight-recovery wird VOR qconnex-firmware heruntergefahren"
|
|
else
|
|
fail "Reihenfolge falsch oder Aufrufe fehlen (down=$DOWN_LINE, up=$UP_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
[ "$(cat "$SBX/run/mode")" = "migration" ] && pass "Modusdatei enthaelt 'migration'" || fail "Modusdatei falsch"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "auto" ] && pass "control.json: phase=auto" || fail "control.json phase ist nicht 'auto': $(cat "$SBX/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall 4: 'migrate on --listen' wie Fall 3, aber phase=listen ==="
|
|
SBX="$TMPROOT/t4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
|
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 "control.json phase ist nicht 'listen': $(cat "$SBX/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall 5: 'migrate on' ohne Migrationsimages -> Warnung auf stderr, Rueckgabewert 0 ==="
|
|
SBX="$TMPROOT/t5"
|
|
setup_sandbox "$SBX"
|
|
# images/migrate/ bleibt leer
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
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 ==="
|
|
SBX="$TMPROOT/t6"
|
|
setup_sandbox "$SBX"
|
|
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
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm 2>&1)
|
|
RC=$?
|
|
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 ==="
|
|
SBX="$TMPROOT/t7"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm gu10_rgb 2>&1)
|
|
RC=$?
|
|
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 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>"$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 "$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 auf stderr, Rueckgabewert != 0 ==="
|
|
SBX="$TMPROOT/t9"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
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 "$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
|
|
echo "=== Fall 10: 'listen' setzt phase=listen ==="
|
|
SBX="$TMPROOT/t10"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" listen 2>&1)
|
|
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 ==="
|
|
SBX="$TMPROOT/t11"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "systemctl stop ninlight-station" "$SBX/cmd.log" && pass "Dienst gestoppt" || fail "kein systemctl stop im Log"
|
|
ACTIVE=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE" | grep -qx "ninlight-recovery" && fail "ninlight-recovery ist noch aktiv" || pass "ninlight-recovery ist nicht mehr aktiv"
|
|
echo "$ACTIVE" | grep -qx "qconnex-firmware" && fail "qconnex-firmware ist noch aktiv" || pass "qconnex-firmware ist nicht mehr aktiv"
|
|
echo "$ACTIVE" | grep -qx "home-wifi" && pass "zurueck im Heim-WLAN (home-wifi aktiv)" || fail "home-wifi ist nicht aktiv: $ACTIVE"
|
|
echo "$OUT" | grep -q "Heim-WLAN" && pass "Ausgabe erwaehnt Heim-WLAN" || fail "Ausgabe erwaehnt Heim-WLAN nicht: $OUT"
|
|
|
|
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",
|
|
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
set +e
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
echo "$OUT" | grep -q "Modus.*migration" && pass "Modus wird angezeigt" || fail "Modus fehlt: $OUT"
|
|
echo "$OUT" | grep -q "Phase.*auto" && pass "Phase wird angezeigt" || fail "Phase fehlt: $OUT"
|
|
echo "$OUT" | grep -q "Erkannt.*Klyqa GU10 RGB CW/WW" && pass "zuletzt erkannte Lampe wird angezeigt" || fail "erkannte Lampe fehlt: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 13: 'status' mit unlesbarer status.json meldet das ausdruecklich (4a-Fehler) ==="
|
|
SBX="$TMPROOT/t13"
|
|
setup_sandbox "$SBX"
|
|
echo '{"mode": "recovery"}' >"$SBX/run/status.json"
|
|
chmod 000 "$SBX/run/status.json"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
RC=$?
|
|
set -e
|
|
chmod 644 "$SBX/run/status.json" # damit trap-Aufraeumen am Ende sicher klappt
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (status liefert trotzdem eine Auskunft)" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
echo "$OUT" | grep -q "existiert, ist aber nicht lesbar" && pass "unlesbare status.json wird ausdruecklich gemeldet" || fail "Meldung zu unlesbarer status.json fehlt: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 14: unbekannter Unterbefehl -> Aufrufhinweis, Rueckgabewert 2 ==="
|
|
SBX="$TMPROOT/t14"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" frobnicate 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 2 ] && pass "Exit-Code 2" || fail "Exit-Code war $RC statt 2"
|
|
echo "$OUT" | grep -qi "Aufruf" && pass "Aufrufhinweis vorhanden" || fail "kein Aufrufhinweis: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 15 (wichtigster Test): 'migrate on' bei laufendem Recovery-Modus laesst GENAU EIN Profil aktiv ==="
|
|
SBX="$TMPROOT/t15"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
ACTIVE_BEFORE=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE_BEFORE" | grep -qx "ninlight-recovery" && pass "Vorbedingung: ninlight-recovery laeuft" || fail "Vorbedingung nicht erfuellt: $ACTIVE_BEFORE"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
COUNT=$(echo "$ACTIVE_AFTER" | grep -c . || true)
|
|
if [ "$COUNT" -eq 1 ] && [ "$ACTIVE_AFTER" = "qconnex-firmware" ]; then
|
|
pass "genau ein Profil aktiv: qconnex-firmware"
|
|
else
|
|
fail "erwartet genau 'qconnex-firmware', tatsaechlich aktiv: [$ACTIVE_AFTER]"
|
|
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
|
|
echo "=== Zusatzfall N2 (Fixrunde 2): 'status' zeigt keine veraltete Migrationsphase mehr, nachdem in den Rettungsmodus zurueckgeschaltet wurde ==="
|
|
SBX="$TMPROOT/tN2"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # control.json bleibt mit phase=armed liegen
|
|
run_cli "$SBX" on >/dev/null 2>&1 # zurueck in den Rettungsmodus - control.json wird bewusst NICHT aufgeraeumt
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "Vorbedingung: control.json ist tatsaechlich noch mit phase=armed liegen geblieben" || fail "Vorbedingung nicht erfuellt (control.json wurde schon vorher veraendert)"
|
|
# status.json so, wie sie der Server nach dem Neustart im Rettungsmodus schreiben
|
|
# wuerde (mode=recovery, keine last_seen/phase-Felder - siehe server.py write_state()).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "recovery"}
|
|
JSON
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Modus[[:space:]]*: recovery" && pass "Modus zeigt korrekt 'recovery'" || fail "Modus falsch: $OUT"
|
|
echo "$OUT" | grep -qi "armed" && fail "status zeigt weiterhin die veraltete Migrationsphase 'armed' im Rettungsmodus: $OUT" || pass "keine veraltete Migrationsphase 'armed' im Rettungsmodus"
|
|
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt den neutralen Platzhalter '-' ausserhalb des Migrationsmodus" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
|
|
|
echo
|
|
echo "=== Zusatzfall N2b (Fixrunde 2): dieselbe veraltete Freigabe, aber ohne aktualisierte status.json (Modus '-') - genau das woertliche Repro aus dem Review ==="
|
|
SBX="$TMPROOT/tN2b"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
rm -f "$SBX/run/status.json" # kein Serverprozess in diesem Test aktualisiert sie
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Modus[[:space:]]*: -" && pass "Modus zeigt '-' (keine status.json vorhanden)" || fail "Modus unerwartet: $OUT"
|
|
echo "$OUT" | grep -qi "armed" && fail "status zeigt 'armed', obwohl der Modus gar nicht bekannt ist: $OUT" || pass "keine 'armed'-Phase bei unbekanntem Modus"
|
|
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt '-' statt einer Vermutung" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
|
|
|
echo
|
|
if [ "$FAILED" -eq 0 ]; then
|
|
echo "Alle Tests bestanden."
|
|
else
|
|
echo "Mindestens ein Test ist fehlgeschlagen." >&2
|
|
fi
|
|
exit "$FAILED"
|