Sechs Review-Befunde an e2d836a/2aab1b0. C1: die Pixelschwellen in test_render.py waren gegen Pillow 12/Arial kalibriert und auf dem Pi wie auf Debian 13 (DejaVu) rot - "> 150 schwarze Pixel in Zeile 11" sind dort nur 143. Die Zusagen ueber den Kopfbalken sind jetzt Anteile und Symmetrien statt Pixelzahlen (mindestens 55 % des Balkens schwarz, weisse Schrift links UND rechts der Mitte, Balken ueber die volle Breite). Zwei weitere schriftabhaengige Schwellen ebenso: die grosse Zeile hebt sich ueber das VERHAELTNIS der Tintenhoehen vom Beiwerk ab, und die Randgrenze zaehlt Zeilenbaender statt Tinten-Cluster. I2: im Zustand "armed" stand "FREIGEGEBEN: <ziel>" in der Kopfzeile, wo 112 px Platz sind und der Text 182 px misst - gezeichnet wurde "FREIGEGEBE…", bei jedem Ziel gleich. Im Kopf steht jetzt nur noch das Label, der Zielname als eigene Beiwerkzeile ueber die volle Breite; die SSID-Zeile gibt dafuer in "armed" ihren Platz ab. Geprueft wird das gezeichnete Bild (Pixelvergleich gegen eine Referenzzeile), nicht lines(). I3: der Kommentar an display_handover() begruendete die Rueckgabewert-Durchreichung damit, dass der Handgriff bei stummem Panel rot werde. Das stimmt nicht - display.py beendet sich bei fehlendem Panel/SPI/Pillow mit 0. Der Kommentar sagt jetzt, dass man die Revision am Panel beurteilt und nicht am Exit-Code. I4: zwischen "systemctl stop" und "systemctl start" gab es keinen Signalhandler - ein Strg-C waehrend "display test" (der dokumentierte Weg, die Revision zu bestimmen, bei falscher Revision haengt das Panel) liess den Anzeigedienst gestoppt zurueck. trap auf INT/TERM/HUP, das Wiederanfahren in display_service_resume(), Handler beenden selbst mit 128 + Signalnummer. M2: HEADER_PHASE_MIN_WIDTH_PX bleibt als Sicherheitsnetz, ist jetzt aber getestet statt toter Code. M4: compose() baute je Aufruf fuenf Schriften neu. Prozess-Cache (mit ImageFont-Modul und FONT_SEARCH_PATHS im Schluessel, damit die Tests nicht verfaelscht werden) und faules Laden je Rolle: 25,0 ms -> 7,0 ms. Die Pixeltests laufen zusaetzlich unter Pillow 11.1 mit echtem DejaVu und unter fuenf weiteren Schriften. Sieben Mutationsproben angewandt, angesehen und zurueckgenommen. tools/test_host.sh gruen.
881 lines
44 KiB
Bash
Executable File
881 lines
44 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)
|
|
# Getrennt schaltbar je Unit: der Handbetrieb am Panel fragt nach ninlight-display
|
|
# (laeuft der Anzeigedienst?), alles andere nach ninlight-station. Die Unit ist
|
|
# immer das letzte Argument ("is-active --quiet <unit>" bzw. "is-active <unit>").
|
|
unit="\${!#}"
|
|
if [ "\$unit" = "ninlight-display" ]; then
|
|
code="\${FAKE_DISPLAY_SERVICE_ACTIVE_EXIT:-0}"
|
|
else
|
|
code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}"
|
|
fi
|
|
if [ "\$code" -eq 0 ]; then
|
|
echo active
|
|
exit 0
|
|
else
|
|
echo inactive
|
|
exit "\$code"
|
|
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.
|
|
#
|
|
# Die Regel gilt nur fuer ninlight-station - \$UNIT_FILE ist deren Unit-Datei.
|
|
# ninlight-display traegt gar kein RuntimeDirectory=, ein "stop ninlight-display"
|
|
# raeumt in Wirklichkeit nichts weg (frueher tat der Stub das trotzdem fuer JEDE
|
|
# Unit; harmlos, solange kein Testfall die Anzeige stoppt und danach noch Dateien
|
|
# in RUN_DIR erwartet - genau das tun 'display test'/'display clear' jetzt aber,
|
|
# die den Anzeigedienst waehrend des Handgriffs stoppen).
|
|
if [ "\$2" = "ninlight-station" ] \
|
|
&& ! 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" \
|
|
"$sandbox/lib/display"
|
|
echo "dummy" >"$sandbox/images/ninlight_foo_ota.img"
|
|
|
|
# NINLIGHT_STATION_LIB zeigt auf diese Sandbox statt auf tools/station selbst.
|
|
# products.py wird gebraucht (arm prueft das Ziel dagegen) und deshalb echt kopiert;
|
|
# display/display.py ist ein Rekorder, der seine Argumente in DIESELBE cmd.log
|
|
# schreibt wie die nmcli-/systemctl-Stubs. Nur so laesst sich die REIHENFOLGE
|
|
# "erst Dienst stoppen, dann Panel leeren" ueberhaupt pruefen - mit zwei getrennten
|
|
# Logs waere sie nicht rekonstruierbar.
|
|
#
|
|
# Der Rekorder ersetzt das echte display.py absichtlich: geprueft wird hier, WAS
|
|
# ninlight-station aufruft (Pfad, Schalter, Reihenfolge, Umgang mit dem Rueckgabe-
|
|
# wert), nicht was display.py daraus macht - das deckt tests/test_render.py ab.
|
|
cp "$STATION_DIR/products.py" "$sandbox/lib/products.py"
|
|
cat >"$sandbox/lib/display/display.py" <<EOF
|
|
import os
|
|
import signal
|
|
import sys
|
|
|
|
with open("$sandbox/cmd.log", "a") as fh:
|
|
fh.write("STUB display.py %s (EPD_REVISION=%s)\n"
|
|
% (" ".join(sys.argv[1:]), os.environ.get("EPD_REVISION", "<unset>")))
|
|
|
|
# Testschalter fuer Fall D2c: stellt das Strg-C des Abnehmers nach, waehrend display.py
|
|
# laeuft (bei falscher Panel-Revision kehrt ReadBusy() nie zurueck - das ist der
|
|
# dokumentierte Weg, die Revision zu bestimmen, und damit der Regelfall fuer ein Strg-C).
|
|
#
|
|
# Nachgestellt wird, was das Terminal wirklich tut: das Signal geht an die GANZE
|
|
# Vordergrund-Prozessgruppe, also an die ninlight-station-Shell UND an diesen Prozess.
|
|
# Der Elternprozess ist dabei die Shell selbst ("sudo" und "env" sind Stub bzw.
|
|
# exec-Aufruf, der Python-Prozess haengt also direkt unter ihr).
|
|
#
|
|
# Dass dieser Prozess AM SIGNAL STIRBT (Standardbehandlung) statt sich mit 128+n zu
|
|
# beenden, ist kein Detail: eine Shell unterscheidet beides. Beendet sich das Kind
|
|
# normal, verwirft /bin/sh (bash im sh-Modus, macOS) das inzwischen zugestellte SIGINT
|
|
# und laeuft weiter - der Testfall waere dann auch ohne Signalhandler in
|
|
# display_handover() gruen und wuerde gar nichts pruefen (beim Schreiben dieses Tests
|
|
# genau so passiert).
|
|
signal_name = os.environ.get("FAKE_DISPLAY_SIGNAL", "")
|
|
if signal_name:
|
|
number = getattr(signal, "SIG" + signal_name)
|
|
os.kill(os.getppid(), number)
|
|
signal.signal(number, signal.SIG_DFL)
|
|
os.kill(os.getpid(), number)
|
|
|
|
sys.exit(int(os.environ.get("FAKE_DISPLAY_EXIT", "0")))
|
|
EOF
|
|
|
|
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="$sandbox/lib" \
|
|
"$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 || true)
|
|
UP_LINE=$(grep -n "nmcli con up qconnex-firmware" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
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
|
|
echo "=== Fall D1 (Task 6b, Punkt 3): 'display test' ruft display.py mit --test und der konfigurierten Revision auf ==="
|
|
SBX="$TMPROOT/tD1"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "display.py wurde mit --test aufgerufen" || fail "kein '--test'-Aufruf im Log: $(cat "$SBX/cmd.log")"
|
|
# Der Testbild-Aufruf ist die einzige Moeglichkeit, die Panel-Revision zu bestimmen -
|
|
# er ist wertlos, wenn er nicht die Revision aus der Konfiguration durchreicht:
|
|
# EPD_REVISION wird per ". $CONF" nur als Shell-Variable gesetzt, nicht exportiert.
|
|
grep -Fq "STUB display.py --test (EPD_REVISION=V3)" "$SBX/cmd.log" && pass "EPD_REVISION aus der Konfiguration erreicht den Python-Prozess" || fail "EPD_REVISION kam nicht durch: $(cat "$SBX/cmd.log")"
|
|
# Der Anzeigedienst haelt die GPIO-Leitungen; ohne ihn kurz zu stoppen scheitert der
|
|
# Handgriff am Geraet mit "lgpio.error: 'GPIO busy'" (Hardware-Abnahme). Frueher stand
|
|
# hier die umgekehrte Zusage ("laesst den Anzeigedienst in Ruhe") - die war genau der
|
|
# Grund, warum der erste Handgriff der Abnahme nicht funktionierte.
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
TEST_LINE=$(grep -n "STUB display.py --test" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
START_LINE=$(grep -n "STUB systemctl start ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$TEST_LINE" ] && [ "$STOP_LINE" -lt "$TEST_LINE" ]; then
|
|
pass "der Anzeigedienst wird gestoppt, BEVOR display.py die GPIO-Leitungen anfasst"
|
|
else
|
|
fail "Anzeigedienst nicht (rechtzeitig) gestoppt (stop=$STOP_LINE, test=$TEST_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
# Und danach wieder an: ein Abnahme-Handgriff darf die Station nicht dauerhaft ohne
|
|
# Anzeige zuruecklassen.
|
|
if [ -n "$START_LINE" ] && [ -n "$TEST_LINE" ] && [ "$TEST_LINE" -lt "$START_LINE" ]; then
|
|
pass "der Anzeigedienst laeuft danach wieder (start=$START_LINE)"
|
|
else
|
|
fail "Anzeigedienst wurde nicht wieder gestartet (test=$TEST_LINE, start=$START_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D2: 'display clear' ruft display.py mit --clear auf ==="
|
|
SBX="$TMPROOT/tD2"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display clear 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "display.py wurde mit --clear aufgerufen" || fail "kein '--clear'-Aufruf im Log: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && fail "'display clear' hat --test aufgerufen" || pass "kein --test bei 'display clear'"
|
|
# Dieselbe GPIO-Kollision wie bei 'display test' - auch 'display clear' braucht die
|
|
# Leitungen fuer sich.
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'display clear' stoppt den Anzeigedienst fuer die Dauer" || fail "'display clear' laesst den Anzeigedienst laufen - das gibt 'GPIO busy': $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "'display clear' startet den Anzeigedienst danach wieder" || fail "'display clear' laesst den Anzeigedienst aus: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D2a: war der Anzeigedienst aus, bleibt er nach 'display test' aus ==="
|
|
# Sonst haette ein Abnahme-Handgriff die Station nebenbei umgeschaltet: nach
|
|
# 'ninlight-station off' ist der Anzeigedienst bewusst gestoppt, und 'display test'
|
|
# darf ihn nicht wieder hochziehen.
|
|
SBX="$TMPROOT/tD2a"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "Testbild wurde trotzdem gezeichnet" || fail "kein '--test'-Aufruf: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && fail "'display test' hat den Anzeigedienst eingeschaltet, obwohl er aus war" || pass "kein 'start' fuer einen Dienst, der gar nicht lief"
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'display test' stoppt einen Dienst, der gar nicht laeuft" || pass "kein ueberfluessiges 'stop'"
|
|
|
|
echo
|
|
echo "=== Fall D2b: scheitert das Panel, meldet 'display test' das - der Dienst laeuft trotzdem wieder ==="
|
|
# Der Rueckgabewert von display.py ist das Ergebnis des Handgriffs; ein erfolgreiches
|
|
# Wiederanfahren des Dienstes darf ihn nicht in eine Erfolgsmeldung verwandeln. Und
|
|
# umgekehrt darf ein gescheitertes Testbild den Dienst nicht dauerhaft aus lassen.
|
|
SBX="$TMPROOT/tD2b"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 1 ] && pass "Exit-Code 1 wird durchgereicht" || fail "Exit-Code $RC statt 1: $OUT"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "Anzeigedienst laeuft trotz Panel-Fehler wieder" || fail "Anzeigedienst blieb nach dem Fehler aus: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D2c: Strg-C waehrend 'display test' laesst den Anzeigedienst NICHT gestoppt zurueck ==="
|
|
# Der Ablauf aus der Abnahme: bei falscher Revision kehrt ReadBusy() nie zurueck
|
|
# (display.py-Docstring), der Abnehmer bricht mit Strg-C ab. Ohne Signalhandler zwischen
|
|
# "stop" und "start" stirbt display_handover mit - der Anzeigedienst bleibt aus, das
|
|
# Panel schwarz, und das sieht mitten in der Lampenabnahme aus wie "Panel kaputt".
|
|
# Geprueft wird fuer alle drei Signale, die das ausloesen koennen.
|
|
for SIG_NAME in INT TERM HUP; do
|
|
case "$SIG_NAME" in
|
|
INT) EXPECT_RC=130 ;;
|
|
TERM) EXPECT_RC=143 ;;
|
|
HUP) EXPECT_RC=129 ;;
|
|
esac
|
|
SBX="$TMPROOT/tD2c$SIG_NAME"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SIGNAL="$SIG_NAME" run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" \
|
|
&& pass "SIG$SIG_NAME: der Anzeigedienst war fuer den Handgriff gestoppt (Vorbedingung)" \
|
|
|| fail "SIG$SIG_NAME: Vorbedingung fehlt, der Dienst wurde gar nicht erst gestoppt: $(cat "$SBX/cmd.log")"
|
|
# GENAU einmal gestartet. Die Anzahl, nicht nur das Vorhandensein: der Handler muss
|
|
# selbst beenden (in diesem Projekt schon einmal ein Befund, setup_pi.sh). Ohne "exit"
|
|
# laeuft die Shell hinter dem unterbrochenen Kommando einfach weiter und faehrt den
|
|
# Dienst ein zweites Mal an - der Exit-Code sieht dabei zufaellig richtig aus (er ist
|
|
# dann der des am Signal gestorbenen Kindes), die doppelte Startzeile nicht. Genau
|
|
# diese Mutation ("trap ohne exit") war gegen eine blosse Vorhandensein-Pruefung
|
|
# gruen.
|
|
START_COUNT=$(grep -c "STUB systemctl start ninlight-display" "$SBX/cmd.log" || true)
|
|
[ "$START_COUNT" -eq 1 ] \
|
|
&& pass "SIG$SIG_NAME: der Anzeigedienst wird trotz Abbruch genau einmal wieder gestartet" \
|
|
|| fail "SIG$SIG_NAME: $START_COUNT Startversuche (1 erwartet) - Anzeigedienst bleibt aus oder der Handler beendet nicht selbst: $(cat "$SBX/cmd.log")"
|
|
[ "$RC" -eq "$EXPECT_RC" ] \
|
|
&& pass "SIG$SIG_NAME: Exit-Code $EXPECT_RC (128 + Signalnummer)" \
|
|
|| fail "SIG$SIG_NAME: Exit-Code $RC statt $EXPECT_RC: $OUT"
|
|
done
|
|
|
|
echo
|
|
echo "=== Fall D2d: war der Anzeigedienst aus, startet ihn auch ein Strg-C nicht ==="
|
|
# Spiegelbild zu D2a: der Signalhandler darf den Zustand nicht anders behandeln als der
|
|
# normale Ausgang - sonst schaltete ein abgebrochener Handgriff die Anzeige ein, die
|
|
# 'off' zuvor bewusst ausgeschaltet hat.
|
|
SBX="$TMPROOT/tD2d"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 FAKE_DISPLAY_SIGNAL=INT run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 130 ] && pass "Exit-Code 130 auch ohne laufenden Anzeigedienst" || fail "Exit-Code $RC statt 130: $OUT"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" \
|
|
&& fail "der Signalhandler hat den Anzeigedienst eingeschaltet, obwohl er aus war: $(cat "$SBX/cmd.log")" \
|
|
|| pass "kein 'start' fuer einen Dienst, der gar nicht lief"
|
|
|
|
echo
|
|
echo "=== Fall D3 (Task 6b, Punkt 4): 'display off' stoppt den Dienst VOR dem Leeren des Panels ==="
|
|
# Umgekehrte Reihenfolge waere wirkungslos: der Daemon fragt alle 5 s nach und
|
|
# schriebe sein altes Bild binnen dieser Zeit wieder aufs Panel.
|
|
SBX="$TMPROOT/tD3"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$CLEAR_LINE" ] && [ "$STOP_LINE" -lt "$CLEAR_LINE" ]; then
|
|
pass "Anzeigedienst wird gestoppt, BEVOR das Panel geleert wird (stop=$STOP_LINE, clear=$CLEAR_LINE)"
|
|
else
|
|
fail "Reihenfolge falsch oder Aufruf fehlt (stop=$STOP_LINE, clear=$CLEAR_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D4 (Task 6b, Punkt 4): 'off' nimmt die Anzeige mit - Dienst gestoppt, Panel geleert ==="
|
|
# Ohne das behauptet das stehengebliebene Bild einen Betriebszustand ("MIGRATION"),
|
|
# den es nicht mehr gibt - bei einer Station, die Lampen ungefragt Firmware
|
|
# unterschieben kann, die gefaehrliche Richtung.
|
|
SBX="$TMPROOT/tD4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
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 -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'off' stoppt den Anzeigedienst" || fail "'off' stoppt den Anzeigedienst nicht: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "'off' leert das Panel" || fail "'off' leert das Panel nicht: $(cat "$SBX/cmd.log")"
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$CLEAR_LINE" ] && [ "$STOP_LINE" -lt "$CLEAR_LINE" ]; then
|
|
pass "auch bei 'off': erst stoppen, dann leeren"
|
|
else
|
|
fail "Reihenfolge bei 'off' falsch (stop=$STOP_LINE, clear=$CLEAR_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
# Das Panel muss geleert sein, BEVOR ap_switch das Ergebnis von "off" verkuenden kann -
|
|
# und vor allem, bevor ap_switch bei haengendem NetworkManager abbricht (Fall D6).
|
|
UP_LINE=$(grep -n "nmcli con up home-wifi" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$UP_LINE" ] && [ "$CLEAR_LINE" -lt "$UP_LINE" ]; then
|
|
pass "Panel ist geleert, bevor wlan0 ins Heim-WLAN zurueckgeschaltet wird"
|
|
else
|
|
fail "Panel wurde erst nach dem Zurueckschalten geleert (clear=$CLEAR_LINE, up=$UP_LINE)"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D5: ein Fehler beim Leeren des Panels haelt 'off' nicht auf ==="
|
|
# Ein fehlendes/kaputtes Panel darf das Ausschalten der Station nie blockieren -
|
|
# sonst bliebe ausgerechnet der Zugangspunkt an, weil die Anzeige klemmt.
|
|
SBX="$TMPROOT/tD5"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem Leeren" || fail "Exit-Code $RC, stderr: $ERR"
|
|
echo "$ERR" | grep -q "Warnung" && pass "Warnung ueber das nicht geleerte Panel steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
|
grep -Fq "nmcli con up home-wifi" "$SBX/cmd.log" && pass "'off' hat trotzdem bis zum Zurueckschalten ins Heim-WLAN durchgezogen" || fail "'off' ist am Anzeigefehler haengengeblieben: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D6: haengender NetworkManager - das Panel wird trotzdem noch geleert ==="
|
|
# ap_switch "" bricht hier mit Fehler ab; unter "set -e" laeuft danach nichts mehr.
|
|
# Genau deshalb steht display_off VOR ap_switch: sonst bliebe ein "MIGRATION"-Bild
|
|
# stehen, waehrend der Zugangspunkt noch sendet und niemand mehr etwas abschaltet.
|
|
SBX="$TMPROOT/tD6"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
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 (der Zugangspunkt liess sich nicht abschalten)" || fail "Exit-Code 0, obwohl der Zugangspunkt noch aktiv ist: $OUT"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "Panel wurde trotz haengendem NetworkManager geleert" || fail "Panel blieb stehen, waehrend der Zugangspunkt noch sendet: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D7: 'display' ohne/mit unbekanntem Unterbefehl -> Hilfetext, Exit 2 ==="
|
|
SBX="$TMPROOT/tD7"
|
|
setup_sandbox "$SBX"
|
|
# Anlegen, bevor geprueft wird: eine fehlende Logdatei wuerde die "kein Aufruf"-Zusage
|
|
# unten sonst durch ein grep-Versagen erfuellen statt durch das Verhalten des Skripts.
|
|
: >"$SBX/cmd.log"
|
|
for ARG in "" "quatsch"; do
|
|
set +e
|
|
# shellcheck disable=SC2086
|
|
OUT=$(run_cli "$SBX" display $ARG 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 2 ] && pass "Exit-Code 2 fuer 'display ${ARG:-<leer>}'" || fail "Exit-Code $RC fuer 'display ${ARG:-<leer>}': $OUT"
|
|
echo "$OUT" | grep -q "display {test|clear|off}" && pass "Hilfetext nennt die Unterbefehle ('display ${ARG:-<leer>}')" || fail "Hilfetext fehlt: $OUT"
|
|
done
|
|
grep -Fq "STUB display.py" "$SBX/cmd.log" && fail "ein unbekannter Unterbefehl hat display.py trotzdem aufgerufen" || pass "kein display.py-Aufruf bei unbekanntem Unterbefehl"
|
|
|
|
echo
|
|
echo "=== Fall D8 (Task 6b, Punkt 3): der Hilfetext erklaert die Revisionsbestimmung ==="
|
|
# Die Panel-Revision steht am Geraet nirgends; 'display test' ist die einzige
|
|
# Moeglichkeit, sie zu bestimmen, und das muss dort stehen, wo man beim ersten
|
|
# Handgriff der Hardware-Abnahme hinsieht.
|
|
SBX="$TMPROOT/tD8"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
HELP=$(run_cli "$SBX" quatsch 2>&1)
|
|
set -e
|
|
echo "$HELP" | grep -q "EPD_REVISION" && pass "Hilfetext nennt EPD_REVISION" || fail "Hilfetext nennt EPD_REVISION nicht: $HELP"
|
|
echo "$HELP" | grep -q "ninlight-station.conf" && pass "Hilfetext nennt die Datei, in der die Revision steht" || fail "Hilfetext nennt /etc/ninlight-station.conf nicht: $HELP"
|
|
echo "$HELP" | grep -qE "V2|V3" && pass "Hilfetext nennt die beiden moeglichen Revisionen" || fail "Hilfetext nennt V2/V3 nicht: $HELP"
|
|
|
|
echo
|
|
if [ "$FAILED" -eq 0 ]; then
|
|
echo "Alle Tests bestanden."
|
|
else
|
|
echo "Mindestens ein Test ist fehlgeschlagen." >&2
|
|
fi
|
|
exit "$FAILED"
|