#!/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" <>"$log" NMSTATE="$nmstate" # "--escape no" abtrennen, BEVOR die Stellungen geprueft werden - sonst verschoebe der # Schalter alle Positionen. Und der Stub maskiert wie das echte nmcli: OHNE den Schalter # wird in den Werten von "-t" jeder Doppelpunkt zu "\\:" (Abschlussreview, M1). Nur so # prueft der Test das Skript und nicht die Attrappe - ein Stub, der nie maskiert, waere # mit und ohne den Fix gruen. ESCAPE=yes _args=() while [ \$# -gt 0 ]; do case "\$1" in --escape) ESCAPE=\$2; shift 2 ;; *) _args+=("\$1"); shift ;; esac done set -- "\${_args[@]}" emit() { # \$1 = Feldname, \$2 = Rohwert if [ "\$ESCAPE" = "no" ]; then echo "\$1:\$2" else echo "\$1:\${2//:/\\\\:}" fi } if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "NAME" ] && [ "\$4" = "con" ] && [ "\$5" = "show" ]; then while IFS= read -r _profile; do if [ "\$ESCAPE" = "no" ]; then echo "\$_profile" else echo "\${_profile//:/\\\\:}" fi done <"\$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 emit GENERAL.CONNECTION "\${FAKE_NMCLI_DEVSHOW}" exit 0 fi last=\$(tail -n1 "\$NMSTATE/active" 2>/dev/null) emit 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" <>"$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 " bzw. "is-active "). unit="\${!#}" if [ "\$unit" = "ninlight-display" ]; then code="\${FAKE_DISPLAY_SERVICE_ACTIVE_EXIT:-0}" # Testschalter fuer den Abschlussbildschirm: der Anzeigedienst gilt fuer die # ersten N Abfragen als AKTIV und danach als gestoppt - so, wie es beim # Herunterfahren wirklich ablaeuft (systemd stoppt ihn, das dauert einen Moment). # Gezaehlt wird in einer Datei, weil jeder Stub-Aufruf ein eigener Prozess ist. if [ -n "\${FAKE_DISPLAY_ACTIVE_FOR:-}" ]; then n=\$(cat "$rundir/.display-active-count" 2>/dev/null || echo 0) n=\$((n + 1)) echo "\$n" >"$rundir/.display-active-count" if [ "\$n" -le "\${FAKE_DISPLAY_ACTIVE_FOR}" ]; then echo active exit 0 fi echo inactive exit 3 fi else code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}" fi if [ "\$code" -eq 0 ]; then echo active exit 0 else echo inactive exit "\$code" fi ;; start) # Testschalter fuer die Resilienz-Faelle: bildet nach, dass "ninlight-display" # gar nicht installiert oder absichtlich maskiert ist (systemctl gibt dann einen # Fehler zurueck statt still zu tun, als waere alles gut). if [ "\$2" = "ninlight-display" ] && [ -n "\${FAKE_DISPLAY_SERVICE_MISSING:-}" ]; then echo "Failed to start ninlight-display.service: Unit not found." >&2 exit 5 fi exit 0 ;; restart|stop) # Bildet systemds RuntimeDirectory-Verhalten nach (Fixrunde 1, C1): ohne # RuntimeDirectoryPreserve=restart in der Unit leert systemd das # RuntimeDirectory bei JEDEM Stop/Restart, nicht nur beim allerersten Start, # und legt es leer wieder an. # # 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 [ "\$2" = "ninlight-display" ] && [ -n "\${FAKE_DISPLAY_SERVICE_MISSING:-}" ]; then echo "Failed to stop ninlight-display.service: Unit not found." >&2 exit 5 fi if [ "\$1" = "restart" ]; then exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" fi exit 0 ;; *) exit 0 ;; esac EOF cat >"$bindir/sudo" <>"$log" exec "\$@" EOF # Der Zeitschalter legt seinen Timer per "systemd-run --unit=..." an; der Stub # protokolliert nur, was angefordert wurde (der ECHTE Beleg dafuer, dass ein # Zeitschalter existiert bzw. nicht existiert, ist unten immer die Zustandsdatei # $RUN_DIR/autooff.json und das Verhalten des Skripts, nicht diese Zeile). cat >"$bindir/systemd-run" <>"$log" exit "\${FAKE_SYSTEMD_RUN_EXIT:-0}" EOF # "ss" beantwortet die Frage "laeuft gerade eine Auslieferung?" - mit # FAKE_SS_BUSY=1 gibt es eine offene Verbindung aus dem Stationsnetz auf Port 80. cat >"$bindir/ss" <>"$log" if [ -n "\${FAKE_SS_BUSY:-}" ]; then echo "ESTAB 0 0 192.168.8.1:80 192.168.8.42:52344" fi exit 0 EOF chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo \ "$bindir"/systemd-run "$bindir"/ss } # --- 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-nocreds" "$sandbox/images/migrate-wlan" \ "$sandbox/bin" "$sandbox/nmstate" "$sandbox/lib/display" "$sandbox/var" # Je ein Image je Bildsatz - "vorhanden" heisst fuer CLI, Server und Anzeige # gleichermassen "mindestens ein Image darin". echo "ohne" >"$sandbox/images/migrate-nocreds/foo_dev.img" echo "mit" >"$sandbox/images/migrate-wlan/foo_dev.img" 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" <"))) # 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" <"$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" \ NINLIGHT_IMAGESET_FILE="$sandbox/var/imageset" \ "$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-nocreds/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-nocreds/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" # der gewaehlte Bildsatz ("ohne-wlan") wird geleert - setup_sandbox bestueckt beide rm -f "$SBX/images/migrate-nocreds"/*.img 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-nocreds/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-nocreds/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-nocreds/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-nocreds/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-nocreds/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 M1 (Abschlussreview): ein Profilname MIT Doppelpunkt wird beim Nachpruefen richtig gelesen ===" # "nmcli -t" maskiert Doppelpunkte in Werten als "\:", und "cut -d: -f2" schneidet am # ersten Doppelpunkt ab. Ohne "--escape no" UND "-f2-" liest ap_switch() aus einem # Profil "ninlight:recovery" nur "ninlight\" bzw. "ninlight" - der Vergleich schluege # fehl, und zwar in die UNSICHERE Richtung: "Station aus" gemeldet, waehrend der # Zugangspunkt weitersendet. Aufbau wie Zusatzfall I2, nur mit Doppelpunkt im Namen. SBX="$TMPROOT/tM1" setup_sandbox "$SBX" sed -i.bak "s/^AP_CONNECTION=.*/AP_CONNECTION='ninlight:recovery'/" "$SBX/station.conf" rm -f "$SBX/station.conf.bak" printf 'ninlight:recovery\nqconnex-firmware\nhome-wifi\n' >"$SBX/nmstate/profiles" set +e OUT=$(run_cli "$SBX" on 2>"$SBX/stderr.log") RC=$? set -e [ "$RC" -eq 0 ] && pass "'on' kommt mit einem Doppelpunkt im Profilnamen zurecht" \ || fail "'on' scheiterte an einem Doppelpunkt im Profilnamen (rc=$RC): $OUT $(cat "$SBX/stderr.log")" [ "$(cat "$SBX/nmstate/active")" = "ninlight:recovery" ] \ && pass "das Profil mit Doppelpunkt ist aktiv" || fail "unerwarteter nmcli-Zustand: $(cat "$SBX/nmstate/active")" 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 haengende Zugangspunkt wird trotz Doppelpunkt im Namen erkannt" \ || 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 -q "ninlight:recovery" \ && pass "die Fehlermeldung nennt den vollstaendigen Profilnamen, nicht den abgeschnittenen" \ || fail "Fehlermeldung nennt den Profilnamen nicht vollstaendig: $ERR" 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-nocreds/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-nocreds/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-nocreds/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 (Befund Task 7f): 'off' fasst den Anzeigedienst nicht mehr an ===" # Frueher stoppte 'off' den Anzeigedienst und leerte das Panel - mit der Begruendung, # ein stehengebliebenes "MIGRATION"-Bild wuerde einen Betriebszustand behaupten, den es # nicht mehr gibt. Die Begruendung trug nicht: der Anzeigedienst erkennt den # ausgeschalteten Zustand selbst (systemctl is-active ninlight-station) und zeichnet dann # "Station aus". Das eigentliche Ergebnis des Stoppens war ein anderes: kein "on"-Pfad hat # den Dienst je wieder gestartet, und das Panel blieb ab dem zweiten Durchgang bis zum # naechsten Pi-Neustart dunkel. Jetzt laesst 'off' den Dienst laufen. SBX="$TMPROOT/tD4" setup_sandbox "$SBX" echo "dummy" >"$SBX/images/migrate-nocreds/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" && fail "'off' stoppt den Anzeigedienst weiterhin: $(cat "$SBX/cmd.log")" || pass "'off' stoppt den Anzeigedienst nicht mehr" grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && fail "'off' leert weiterhin das Panel: $(cat "$SBX/cmd.log")" || pass "'off' leert das Panel nicht mehr (der Dienst zeichnet 'Station aus' selbst)" echo echo "=== Fall D4b (die Zusage aus Task 7f): 'off' und ein erneutes 'on' lassen den Anzeigedienst durchgehend an ===" # Genau das ist die Zusage: nach 'off' und einem erneuten 'on' ist die Anzeige wieder in # Betrieb - und zwar OHNE dass irgendwer den Dienst zwischendurch neu gestartet haben # muesste, weil er nie gestoppt wurde. Ein/Aus/Ein durchspielen und ueber die gesamte # Sequenz pruefen, dass 'ninlight-display' kein einziges Mal gestoppt wird. SBX="$TMPROOT/tD4b" setup_sandbox "$SBX" : >"$SBX/cmd.log" set +e run_cli "$SBX" on >/dev/null 2>&1 RC1=$? run_cli "$SBX" off >/dev/null 2>&1 RC2=$? run_cli "$SBX" on >/dev/null 2>&1 RC3=$? set -e [ "$RC1" -eq 0 ] && [ "$RC2" -eq 0 ] && [ "$RC3" -eq 0 ] \ && pass "on/off/on liefern alle Exit-Code 0" \ || fail "on/off/on: Exit-Codes $RC1/$RC2/$RC3, Log: $(cat "$SBX/cmd.log")" grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" \ && fail "der Anzeigedienst wurde waehrend on/off/on gestoppt - nach 'off' bliebe das Panel dunkel: $(cat "$SBX/cmd.log")" \ || pass "der Anzeigedienst blieb ueber on/off/on hinweg durchgehend an" echo echo "=== Fall D9: ein fehlender/scheiternder Anzeigedienst haelt weder 'on' noch 'off' auf ===" # FAKE_DISPLAY_SERVICE_MISSING bildet nach, dass 'ninlight-display' gar nicht installiert # oder absichtlich deaktiviert ist (systemctl meldet dann einen Fehler statt still # mitzuspielen). Das darf 'on' und 'off' nicht scheitern lassen - eine tragende Zusage # dieses Projekts. SBX="$TMPROOT/tD9" setup_sandbox "$SBX" : >"$SBX/cmd.log" set +e OUT_ON=$(FAKE_DISPLAY_SERVICE_MISSING=1 FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=4 run_cli "$SBX" on 2>&1) RC_ON=$? OUT_OFF=$(FAKE_DISPLAY_SERVICE_MISSING=1 FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=4 run_cli "$SBX" off 2>&1) RC_OFF=$? set -e [ "$RC_ON" -eq 0 ] && pass "'on' gelingt trotz fehlendem Anzeigedienst" || fail "'on' scheitert am fehlenden Anzeigedienst, Exit $RC_ON: $OUT_ON" [ "$RC_OFF" -eq 0 ] && pass "'off' gelingt trotz fehlendem Anzeigedienst" || fail "'off' scheitert am fehlenden Anzeigedienst, Exit $RC_OFF: $OUT_OFF" 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:-}'" || fail "Exit-Code $RC fuer 'display ${ARG:-}': $OUT" echo "$OUT" | grep -q "display {test|clear|off}" && pass "Hilfetext nennt die Unterbefehle ('display ${ARG:-}')" || 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 echo "########## Zeitschalter (Auto-Aus) ##########" # Geprueft wird durchgehend das ERGEBNIS: die Zustandsdatei $RUN_DIR/autooff.json (sie # ist im Skript die einzige Quelle dafuer, ob ein Zeitschalter laeuft), der # Rueckgabewert, die Ausgabe und der nmcli-/systemd-Zustand - nicht die Interna der # Stubs. autooff_field() { # $1 = Pfad zu autooff.json, $2 = Feldname python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2]))" "$1" "$2" 2>/dev/null \ || echo "FEHLT" } write_autooff() { # $1 = sandbox, $2 = minutes, $3 = deadline-Offset in Sekunden ab jetzt, $4 = deferrals python3 - "$1/run/autooff.json" "$2" "$3" "$4" <<'PY' import json, sys, time path, minutes, offset, deferrals = sys.argv[1:5] with open(path, "w") as fh: json.dump({"minutes": int(minutes), "deadline": int(time.time()) + int(offset), "deferrals": int(deferrals)}, fh) PY } echo "=== Fall Z1 (die wichtigste Zusage): 'on' OHNE --for legt keinen Zeitschalter an ===" # Der Regelfall ist der Pi am Kabel - dem darf niemand den Zugangspunkt unter der # laufenden Arbeit wegschalten, nur weil jemand vorsichtig sein wollte. SBX="$TMPROOT/tZ1" 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" [ ! -e "$SBX/run/autooff.json" ] && pass "keine Zeitschalter-Zustandsdatei angelegt" || fail "ohne --for entstand trotzdem ein Zeitschalter: $(cat "$SBX/run/autooff.json")" grep -q "systemd-run" "$SBX/cmd.log" && fail "ohne --for wurde trotzdem ein Timer angelegt: $(grep systemd-run "$SBX/cmd.log")" || pass "kein systemd-run-Aufruf" echo "$OUT" | grep -qi "Auto-Aus" && fail "Ausgabe verspricht einen Zeitschalter, den es nicht gibt: $OUT" || pass "Ausgabe erwaehnt keinen Zeitschalter" OUT=$(run_cli "$SBX" status 2>&1) echo "$OUT" | grep -qi "Auto-Aus" && fail "'status' zeigt eine Zeitschalter-Zeile, obwohl keiner laeuft: $OUT" || pass "'status' schweigt ueber den Zeitschalter" echo echo "=== Fall Z2: 'on --for 30' legt einen Zeitschalter an ===" SBX="$TMPROOT/tZ2" setup_sandbox "$SBX" set +e OUT=$(run_cli "$SBX" on --for 30 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" [ -e "$SBX/run/autooff.json" ] && pass "Zustandsdatei angelegt" || fail "keine autooff.json angelegt: $(cat "$SBX/cmd.log")" [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "angeforderte Dauer 30 min gemerkt" || fail "minutes falsch: $(cat "$SBX/run/autooff.json")" [ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "0" ] && pass "noch keine Verschiebung" || fail "deferrals falsch: $(cat "$SBX/run/autooff.json")" [ "$(file_mode "$SBX/run/autooff.json")" = "644" ] && pass "autooff.json ist 0644 (display.py muss sie lesen)" || fail "falsche Rechte: $(file_mode "$SBX/run/autooff.json")" grep -q "systemd-run .*--unit=ninlight-station-autooff" "$SBX/cmd.log" && pass "Timer unter festem Unit-Namen angelegt" || fail "kein systemd-run mit festem Unit-Namen: $(cat "$SBX/cmd.log")" grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "Timer feuert nach 30 min" || fail "falsche Laufzeit: $(grep systemd-run "$SBX/cmd.log")" grep -q "__autooff" "$SBX/cmd.log" && pass "Timer ruft das CLI mit seiner Abschaltaktion" || fail "Timer ruft nichts Abschaltendes: $(grep systemd-run "$SBX/cmd.log")" [ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "die Station ist trotzdem ganz normal an" || fail "Modusdatei falsch" OUT=$(run_cli "$SBX" status 2>&1) echo "$OUT" | grep -qE "Auto-Aus: in (29|30) min" && pass "'status' zeigt die Restzeit" || fail "'status' zeigt keine brauchbare Restzeit: $OUT" echo echo "=== Fall Z3: ungueltige Minutenangaben schalten GAR NICHTS ein ===" # Geprueft wird nicht nur der Rueckgabewert: nach einer abgelehnten Angabe darf weder ein # Zugangspunkt hochgefahren noch der Dienst neu gestartet noch eine Modusdatei # geschrieben worden sein. for BAD in "0" "241" "abc" "-5" "1.5" ""; do SBX="$TMPROOT/tZ3-${BAD:-leer}" setup_sandbox "$SBX" # Logdatei anlegen, BEVOR geprueft wird (wie in Fall D7): eine fehlende Datei wuerde # die "hat nichts eingeschaltet"-Zusagen unten sonst durch ein grep-Versagen erfuellen # statt durch das Verhalten des Skripts. : >"$SBX/cmd.log" set +e if [ -z "$BAD" ]; then OUT=$(run_cli "$SBX" on --for 2>"$SBX/stderr.log") # --for ohne Wert else OUT=$(run_cli "$SBX" on --for "$BAD" 2>"$SBX/stderr.log") fi RC=$? set -e ERR=$(cat "$SBX/stderr.log") [ "$RC" -ne 0 ] && pass "'--for ${BAD:-}' wird abgelehnt (Exit $RC)" || fail "'--for ${BAD:-}' wurde angenommen: $OUT" echo "$ERR" | grep -qi "fehler" && pass "'--for ${BAD:-}': Fehlermeldung auf stderr" || fail "'--for ${BAD:-}': keine Fehlermeldung: $ERR" grep -q "nmcli con up" "$SBX/cmd.log" && fail "'--for ${BAD:-}' hat trotzdem einen Zugangspunkt hochgefahren: $(cat "$SBX/cmd.log")" || pass "'--for ${BAD:-}': kein Zugangspunkt hochgefahren" grep -q "systemctl restart" "$SBX/cmd.log" && fail "'--for ${BAD:-}' hat den Dienst gestartet" || pass "'--for ${BAD:-}': Dienst nicht gestartet" [ ! -e "$SBX/run/mode" ] && pass "'--for ${BAD:-}': keine Modusdatei geschrieben" || fail "'--for ${BAD:-}' hat eine Modusdatei geschrieben: $(cat "$SBX/run/mode")" [ ! -e "$SBX/run/autooff.json" ] && pass "'--for ${BAD:-}': kein Zeitschalter angelegt" || fail "'--for ${BAD:-}': Zeitschalter trotz ungueltiger Angabe" done echo echo "=== Fall Z4: 'migrate on --listen --for 5' - beides zusammen ===" SBX="$TMPROOT/tZ4" setup_sandbox "$SBX" echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img" set +e OUT=$(run_cli "$SBX" migrate on --listen --for 5 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 "phase=listen bleibt erhalten" || fail "phase falsch: $(cat "$SBX/run/control.json" 2>&1)" [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "5" ] && pass "Zeitschalter ueber 5 min angelegt" || fail "Zeitschalter falsch/fehlt: $(cat "$SBX/run/autooff.json" 2>&1)" # Umgekehrte Reihenfolge der Schalter muss genauso gehen. SBX="$TMPROOT/tZ4b" setup_sandbox "$SBX" echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img" set +e OUT=$(run_cli "$SBX" migrate on --for 5 --listen 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && [ "$(control_phase "$SBX/run/control.json")" = "listen" ] \ && pass "Schalterreihenfolge ist egal" || fail "'--for 5 --listen' scheiterte (Exit $RC): $OUT" echo echo "=== Fall Z5 (nicht verhandelbar): der Zeitschalter schaltet NICHT in eine laufende Auslieferung ===" SBX="$TMPROOT/tZ5" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 : >"$SBX/cmd.log" set +e OUT=$(FAKE_SS_BUSY=1 run_cli "$SBX" __autooff 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" && fail "die Station wurde mitten in der Auslieferung abgeschaltet: $(cat "$SBX/cmd.log")" || pass "die Station bleibt an" grep -q "nmcli con down" "$SBX/cmd.log" && fail "der Zugangspunkt wurde mitten in der Auslieferung heruntergefahren" || pass "der Zugangspunkt bleibt oben" [ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "1" ] && pass "Verschiebung gezaehlt (1)" || fail "deferrals nicht hochgezaehlt: $(cat "$SBX/run/autooff.json" 2>&1)" [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "die angeforderte Dauer bleibt fuer 'renew' erhalten" || fail "minutes wurde ueberschrieben: $(cat "$SBX/run/autooff.json")" echo "$OUT" | grep -qi "verschoben" && pass "die Verschiebung wird gemeldet" || fail "keine Meldung ueber die Verschiebung: $OUT" # Gegenprobe: dieselbe Lage ohne laufende Verbindung schaltet sehr wohl ab. SBX="$TMPROOT/tZ5b" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 : >"$SBX/cmd.log" set +e OUT=$(run_cli "$SBX" __autooff 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Gegenprobe: Exit-Code 0" || fail "Gegenprobe: Exit-Code $RC: $OUT" grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && pass "ohne laufende Auslieferung schaltet der Zeitschalter ab" || fail "der Zeitschalter schaltet gar nicht ab: $(cat "$SBX/cmd.log")" ACTIVE_AFTER=$(cat "$SBX/nmstate/active") echo "$ACTIVE_AFTER" | grep -qx "home-wifi" && pass "wlan0 ist zurueck im Heim-WLAN" || fail "nicht zurueck im Heim-WLAN: $ACTIVE_AFTER" [ ! -e "$SBX/run/autooff.json" ] && pass "der abgelaufene Zeitschalter ist abgeraeumt" || fail "Zustandsdatei blieb liegen" # Die Pruefung muss die richtige Frage stellen: offene Verbindungen auf Port 80 aus dem # Stationsnetz. grep -q "STUB ss .*:80" "$SBX/cmd.log" && pass "geprueft wird Port 80" || fail "die Verbindungspruefung fragt nicht nach Port 80: $(grep 'STUB ss' "$SBX/cmd.log")" grep -q "STUB ss .*192.168.8.0/24" "$SBX/cmd.log" && pass "geprueft wird das Stationsnetz 192.168.8.0/24" || fail "die Verbindungspruefung fragt nicht nach dem Stationsnetz: $(grep 'STUB ss' "$SBX/cmd.log")" echo echo "=== Fall Z6: die Obergrenze fuers Verschieben greift - eine haengende Verbindung haelt den Zeitschalter nicht ewig auf ===" SBX="$TMPROOT/tZ6" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 write_autooff "$SBX" 30 60 10 # 10 Verschiebungen sind die Obergrenze : >"$SBX/cmd.log" set +e OUT=$(FAKE_SS_BUSY=1 run_cli "$SBX" __autooff 2>"$SBX/stderr.log") RC=$? set -e ERR=$(cat "$SBX/stderr.log") [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT $ERR" grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && pass "an der Obergrenze wird trotz offener Verbindung abgeschaltet" || fail "die haengende Verbindung hebelt den Zeitschalter dauerhaft aus: $(cat "$SBX/cmd.log")" echo "$ERR" | grep -qi "warnung" && pass "das Abschalten trotz offener Verbindung wird gemeldet" || fail "keine Warnung: $ERR" [ ! -e "$SBX/run/autooff.json" ] && pass "Zustandsdatei abgeraeumt" || fail "Zustandsdatei blieb liegen" # Eine Verschiebung UNTER der Grenze verschiebt dagegen weiter (sonst koennte die # Grenzpruefung auch einfach immer abschalten). SBX="$TMPROOT/tZ6b" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 write_autooff "$SBX" 30 60 9 : >"$SBX/cmd.log" set +e FAKE_SS_BUSY=1 run_cli "$SBX" __autooff >/dev/null 2>&1 set -e grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "eine Verschiebung unter der Grenze hat schon abgeschaltet" || pass "unter der Grenze wird weiter verschoben" [ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "10" ] && pass "Zaehler steht jetzt auf der Grenze (10)" || fail "deferrals falsch: $(cat "$SBX/run/autooff.json" 2>&1)" echo echo "=== Fall Z7: 'off' raeumt einen laufenden Zeitschalter mit ab ===" # Sonst feuert er spaeter in eine Station hinein, die laengst aus ist - oder inzwischen # wieder an. SBX="$TMPROOT/tZ7" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 [ -e "$SBX/run/autooff.json" ] || fail "Vorbedingung: kein Zeitschalter angelegt" : >"$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: $OUT" [ ! -e "$SBX/run/autooff.json" ] && pass "Zeitschalter-Zustandsdatei ist weg" || fail "'off' liess den Zeitschalter stehen: $(cat "$SBX/run/autooff.json")" grep -q "systemctl stop ninlight-station-autooff.timer" "$SBX/cmd.log" && pass "der Timer selbst wurde angehalten" || fail "der systemd-Timer wurde nicht angehalten: $(cat "$SBX/cmd.log")" grep -q "systemctl stop ninlight-station-autooff$" "$SBX/cmd.log" && fail "es wurde die .service statt der .timer gestoppt - das brächte den Zeitschalter-Lauf um sich selbst" || pass "die gleichnamige .service wird nicht gestoppt" echo echo "=== Fall Z8: 'renew'/'hold' ohne laufenden Zeitschalter sind klare Fehler, kein stillschweigendes Neuanlegen ===" for CMD in renew hold; do SBX="$TMPROOT/tZ8-$CMD" setup_sandbox "$SBX" run_cli "$SBX" on >/dev/null 2>&1 # Station an, aber OHNE --for : >"$SBX/cmd.log" set +e OUT=$(run_cli "$SBX" "$CMD" 2>"$SBX/stderr.log") RC=$? set -e ERR=$(cat "$SBX/stderr.log") [ "$RC" -ne 0 ] && pass "'$CMD' ohne Zeitschalter: Exit-Code $RC" || fail "'$CMD' meldete Erfolg: $OUT" echo "$ERR" | grep -qi "kein Zeitschalter" && pass "'$CMD': klare Meldung auf stderr" || fail "'$CMD': unklare Meldung: $ERR" [ ! -e "$SBX/run/autooff.json" ] && pass "'$CMD' hat keinen Zeitschalter angelegt" || fail "'$CMD' hat stillschweigend einen angelegt: $(cat "$SBX/run/autooff.json")" grep -q "systemd-run" "$SBX/cmd.log" && fail "'$CMD' hat einen Timer angelegt" || pass "'$CMD': kein systemd-run" done echo echo "=== Fall Z9: 'renew' stellt die Uhr zurueck - ohne Argument auf die zuletzt angeforderte Dauer ===" SBX="$TMPROOT/tZ9" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 write_autooff "$SBX" 30 120 4 # Restzeit fast abgelaufen, schon viermal verschoben : >"$SBX/cmd.log" set +e OUT=$(run_cli "$SBX" renew 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "dieselbe Dauer wie zuletzt angefordert (30)" || fail "minutes falsch: $(cat "$SBX/run/autooff.json")" [ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "0" ] && pass "Verschiebungszaehler zurueckgesetzt" || fail "deferrals nicht zurueckgesetzt: $(cat "$SBX/run/autooff.json")" STATUS=$(run_cli "$SBX" status 2>&1) echo "$STATUS" | grep -qE "Auto-Aus: in (29|30) min" && pass "die Restzeit steht wieder bei 30 min" || fail "Restzeit nicht zurueckgestellt: $STATUS" grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "der Timer wurde neu gestellt" || fail "kein neuer Timer: $(cat "$SBX/cmd.log")" # Mit Argument gilt das Argument - und ungueltige Angaben werden auch hier abgelehnt. set +e OUT=$(run_cli "$SBX" renew 90 2>&1); RC=$? set -e [ "$RC" -eq 0 ] && [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "90" ] && pass "'renew 90' stellt auf 90 min" || fail "'renew 90' scheiterte (Exit $RC): $OUT" set +e OUT=$(run_cli "$SBX" renew 999 2>"$SBX/stderr.log"); RC=$? set -e [ "$RC" -ne 0 ] && [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "90" ] && pass "'renew 999' wird abgelehnt, der laufende Zeitschalter bleibt unveraendert" || fail "'renew 999' wurde angenommen (Exit $RC): $(cat "$SBX/run/autooff.json")" echo echo "=== Fall Z10: 'hold' bestellt ab - die Station bleibt an ===" SBX="$TMPROOT/tZ10" setup_sandbox "$SBX" run_cli "$SBX" on --for 30 >/dev/null 2>&1 : >"$SBX/cmd.log" set +e OUT=$(run_cli "$SBX" hold 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" [ ! -e "$SBX/run/autooff.json" ] && pass "Zeitschalter abbestellt" || fail "Zeitschalter laeuft weiter: $(cat "$SBX/run/autooff.json")" grep -q "systemctl stop ninlight-station-autooff.timer" "$SBX/cmd.log" && pass "der Timer wurde angehalten" || fail "Timer nicht angehalten: $(cat "$SBX/cmd.log")" grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "'hold' hat die Station abgeschaltet" || pass "die Station bleibt an" ACTIVE_AFTER=$(cat "$SBX/nmstate/active") [ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "der Zugangspunkt laeuft unveraendert weiter" || fail "Zugangspunkt-Zustand veraendert: $ACTIVE_AFTER" # Und ein danach doch noch feuernder Timer-Lauf schaltet nichts ab. : >"$SBX/cmd.log" set +e run_cli "$SBX" __autooff >/dev/null 2>&1 set -e grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "ein abbestellter Zeitschalter hat trotzdem abgeschaltet: $(cat "$SBX/cmd.log")" || pass "ein abbestellter Zeitschalter schaltet nichts mehr ab" echo echo "=== Fall Z11: scheitert systemd-run, bleibt das nicht unbemerkt ===" # Sonst glaubte der Nutzer an einen Zeitschalter, den es nie gab - und genau darauf # hat er sich verlassen, als er die SSH-Sitzung ueber WLAN aufgab. SBX="$TMPROOT/tZ11" setup_sandbox "$SBX" set +e OUT=$(FAKE_SYSTEMD_RUN_EXIT=1 run_cli "$SBX" on --for 30 2>"$SBX/stderr.log") RC=$? set -e ERR=$(cat "$SBX/stderr.log") [ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0, obwohl der Timer nicht angelegt werden konnte: $OUT" echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR" [ ! -e "$SBX/run/autooff.json" ] && pass "keine Zustandsdatei, die einen Zeitschalter vortaeuscht" || fail "autooff.json trotz gescheitertem Timer: $(cat "$SBX/run/autooff.json")" echo "$ERR" | grep -q "off" && pass "die Meldung sagt, dass die Station AN bleibt" || fail "die Meldung verschweigt den Zustand der Station: $ERR" echo echo "=== Bildsatz A: die Vorgabe ohne Umschalterdatei ist der Satz OHNE WLAN-Zugangsdaten ===" # Die sichere Richtung: ein Fehlgriff kostet einen Onboarding-Durchgang, kein # WLAN-Passwort in einer fremden Lampe. SBX="$TMPROOT/bs1" setup_sandbox "$SBX" set +e OUT=$(run_cli "$SBX" bildsatz 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" echo "$OUT" | grep -q "ohne-wlan" && pass "ohne Datei gilt 'ohne-wlan'" || fail "Vorgabe ist nicht 'ohne-wlan': $OUT" echo "$OUT" | grep -q "migrate-nocreds" && pass "und das zugehoerige Verzeichnis wird genannt" || fail "Verzeichnis fehlt in der Ausgabe: $OUT" [ ! -e "$SBX/var/imageset" ] && pass "das blosse Berichten legt keine Datei an" || fail "'bildsatz' ohne Argument hat geschrieben" echo echo "=== Bildsatz B: umschalten schreibt die Datei atomar mit 0644 und bleibt stehen ===" SBX="$TMPROOT/bs2" setup_sandbox "$SBX" set +e OUT=$(run_cli "$SBX" bildsatz mit-wlan 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" [ "$(cat "$SBX/var/imageset" 2>/dev/null | tr -d '\n')" = "mit-wlan" ] && pass "die Datei enthaelt 'mit-wlan'" || fail "Dateiinhalt: $(cat "$SBX/var/imageset" 2>&1)" MODE=$(file_mode "$SBX/var/imageset") [ "$MODE" = "644" ] && pass "Rechte 0644 (der Anzeigedienst muss lesen koennen)" || fail "Rechte $MODE statt 644" [ -z "$(find "$SBX/var" -name '.imageset-*' 2>/dev/null)" ] && pass "keine Temp-Datei liegengeblieben" || fail "Temp-Datei liegt noch da" echo "$OUT" | grep -qi "achtung" && pass "der Wechsel zu 'mit-wlan' warnt" || fail "keine Warnung beim Wechsel zu mit-wlan: $OUT" # ... und er BLEIBT stehen: ein zweiter, unabhaengiger Aufruf sieht ihn. OUT2=$(run_cli "$SBX" bildsatz 2>&1) echo "$OUT2" | grep -q "mit-wlan" && pass "der Zustand bleibt ueber Aufrufe hinweg" || fail "Zustand nicht gespeichert: $OUT2" # ... und zurueck. run_cli "$SBX" bildsatz ohne-wlan >/dev/null 2>&1 [ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "ohne-wlan" ] && pass "zurueckschalten geht auch" || fail "zurueckschalten hat nicht gewirkt" echo echo "=== Bildsatz C: ein unbekannter Name schaltet nichts um ===" SBX="$TMPROOT/bs3" setup_sandbox "$SBX" printf 'mit-wlan\n' >"$SBX/var/imageset" set +e ERR=$(run_cli "$SBX" bildsatz irgendwas 2>&1 >/dev/null) RC=$? set -e [ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 fuer einen unbekannten Bildsatz" [ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "mit-wlan" ] && pass "der bestehende Zustand blieb unveraendert" || fail "der Zustand wurde veraendert: $(cat "$SBX/var/imageset")" echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR" echo echo "=== Bildsatz D: ein kaputter Dateiinhalt faellt auf die SICHERE Vorgabe zurueck ===" # Nie auf den teureren Satz - das ist die eine Richtung, die kein Zweifelsfall nehmen darf. SBX="$TMPROOT/bs4" setup_sandbox "$SBX" printf 'voellig-anderes\n' >"$SBX/var/imageset" OUT=$(run_cli "$SBX" bildsatz 2>&1) echo "$OUT" | grep -q "ohne-wlan" && pass "kaputter Inhalt -> 'ohne-wlan'" || fail "kaputter Inhalt fuehrte nicht auf die Vorgabe: $OUT" echo "$OUT" | grep -q "migrate-nocreds" && pass "und das Verzeichnis des sicheren Satzes" || fail "falsches Verzeichnis: $OUT" # Der Bediener muss erfahren, dass sein Eintrag NICHT gewirkt hat - sonst haelt er die # Station fuer umgeschaltet. echo "$OUT" | grep -q "unbekannte Einstellung 'voellig-anderes'" \ && pass "die unbekannte Einstellung wird beim Namen genannt" \ || fail "die unbekannte Einstellung wird verschwiegen: $OUT" # Gegenprobe: bei einem GUELTIGEN Eintrag darf diese Zeile nicht erscheinen. run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1 OUT=$(run_cli "$SBX" bildsatz 2>&1) echo "$OUT" | grep -q "unbekannte Einstellung" \ && fail "meldet 'unbekannte Einstellung' fuer einen gueltigen Eintrag: $OUT" \ || pass "kein Fehlalarm bei gueltigem Eintrag" echo echo "=== Bildsatz E: ein gewaehlter, aber fehlender Satz wird beim Umschalten UND im Status gemeldet ===" SBX="$TMPROOT/bs5" setup_sandbox "$SBX" rm -rf "$SBX/images/migrate-wlan" set +e ERR=$(run_cli "$SBX" bildsatz mit-wlan 2>&1 >/dev/null) RC=$? set -e [ "$RC" -eq 0 ] && pass "Umschalten gelingt trotzdem (der Zustand ist gueltig, nur die Bestueckung fehlt)" || fail "Exit-Code $RC" echo "$ERR" | grep -qi "warnung" && pass "Warnung beim Umschalten" || fail "keine Warnung: $ERR" OUT=$(run_cli "$SBX" status 2>&1) echo "$OUT" | grep -q "FEHLT" && pass "'status' sagt, dass nichts ausgeliefert wird" || fail "'status' verschweigt den fehlenden Satz: $OUT" echo echo "=== Bildsatz F: ein LEERES Verzeichnis zaehlt genauso als fehlend ===" SBX="$TMPROOT/bs6" setup_sandbox "$SBX" rm -f "$SBX/images/migrate-nocreds"/*.img OUT=$(run_cli "$SBX" status 2>&1) echo "$OUT" | grep -q "FEHLT" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT" echo echo "=== Bildsatz G: 'migrate on' nennt den geltenden Bildsatz in der Einschaltmeldung ===" # Er kann von vorletzter Woche stammen - wer einschaltet, muss ihn sehen. SBX="$TMPROOT/bs7" setup_sandbox "$SBX" run_cli "$SBX" bildsatz mit-wlan >/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: $OUT" echo "$OUT" | grep -q "Bildsatz: mit-wlan" && pass "die Einschaltmeldung nennt den Bildsatz" || fail "Bildsatz fehlt in der Einschaltmeldung: $OUT" echo echo "=== Bildsatz H: die Warnung 'keine Migrationsimages' meint den GEWAEHLTEN Satz ===" SBX="$TMPROOT/bs8" setup_sandbox "$SBX" rm -f "$SBX/images/migrate-wlan"/*.img # nur der andere Satz ist leer set +e ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null) set -e echo "$ERR" | grep -q "keine Migrationsimages" && fail "warnt, obwohl der GEWAEHLTE Satz ('ohne-wlan') bestueckt ist: $ERR" || pass "kein Fehlalarm - gezaehlt wird im gewaehlten Satz" run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1 set +e ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null) set -e echo "$ERR" | grep -q "keine Migrationsimages" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR" echo echo "=== Abschluss A: 'display final' kennt zwei verschiedene Bilder ===" # E-Paper behaelt sein Bild dauerhaft. Beim Ausschalten darf man den Stecker ziehen, beim # Neustart gerade nicht - deshalb zwei Schalter, nicht einer. SBX="$TMPROOT/fin1" setup_sandbox "$SBX" export FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 # Anzeigedienst laeuft nicht set +e OUT=$(run_cli "$SBX" display final poweroff 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" grep -q "STUB display.py --final poweroff" "$SBX/cmd.log" && pass "display.py wird mit '--final poweroff' aufgerufen" || fail "falscher Aufruf: $(cat "$SBX/cmd.log")" : >"$SBX/cmd.log" run_cli "$SBX" display final reboot >/dev/null 2>&1 grep -q "STUB display.py --final reboot" "$SBX/cmd.log" && pass "und mit '--final reboot' fuer den Neustart" || fail "falscher Aufruf: $(cat "$SBX/cmd.log")" grep -q "poweroff" "$SBX/cmd.log" && fail "der Neustart ruft das Ausschaltbild auf" || pass "die beiden Wege werden nicht verwechselt" unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT echo echo "=== Abschluss B: ohne 'poweroff'/'reboot' passiert nichts ===" SBX="$TMPROOT/fin2" setup_sandbox "$SBX" set +e ERR=$(run_cli "$SBX" display final 2>&1 >/dev/null) RC=$? set -e [ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 ohne Spielart" grep -q "STUB display.py" "$SBX/cmd.log" 2>/dev/null && fail "display.py wurde trotzdem aufgerufen" || pass "das Panel wurde nicht angefasst" set +e ERR=$(run_cli "$SBX" display final quatsch 2>&1 >/dev/null) RC=$? set -e [ "$RC" -ne 0 ] && pass "Exit-Code != 0 fuer eine unbekannte Spielart" || fail "Exit-Code 0 fuer 'quatsch'" grep -q "STUB display.py" "$SBX/cmd.log" 2>/dev/null && fail "display.py wurde fuer 'quatsch' aufgerufen" || pass "und auch dafuer nicht" echo echo "=== Abschluss C: gezeichnet wird erst, NACHDEM der Anzeigedienst die Leitungen freigegeben hat ===" # Zwei Prozesse auf denselben GPIO-/SPI-Leitungen enden in "lgpio.error: 'GPIO busy'" - # in diesem Projekt schon zweimal passiert. SBX="$TMPROOT/fin3" setup_sandbox "$SBX" export FAKE_DISPLAY_ACTIVE_FOR=2 # erst beim dritten Blick ist er gestoppt set +e OUT=$(NINLIGHT_DISPLAY_RELEASE_WAIT_S=5 run_cli "$SBX" display final poweroff 2>&1) RC=$? set -e unset FAKE_DISPLAY_ACTIVE_FOR [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT" grep -q "STUB display.py --final poweroff" "$SBX/cmd.log" && pass "das Abschlussbild wurde gezeichnet" || fail "gar nicht gezeichnet: $(cat "$SBX/cmd.log")" # Die Reihenfolge in der GEMEINSAMEN Logdatei ist der Beleg: jede is-active-Abfrage steht # vor dem display.py-Aufruf, keine danach. # "|| true" ueberall: ohne Treffer liefert grep 1, und unter "set -e" wuerde die SUITE an # dieser Stelle stillschweigend abbrechen, statt einen Fehler zu melden - eine Mutation, # die das Warten ganz entfernt, waere dann scheinbar gruen (genau so passiert). DRAW_LINE=$(grep -n "STUB display.py" "$SBX/cmd.log" 2>/dev/null | head -n1 | cut -d: -f1 || true) LAST_CHECK=$(grep -n "systemctl is-active --quiet ninlight-display" "$SBX/cmd.log" 2>/dev/null | tail -n1 | cut -d: -f1 || true) CHECKS=$(grep -c "systemctl is-active --quiet ninlight-display" "$SBX/cmd.log" 2>/dev/null || true) CHECKS=${CHECKS:-0} if [ -n "$DRAW_LINE" ] && [ -n "$LAST_CHECK" ] && [ "$LAST_CHECK" -lt "$DRAW_LINE" ]; then pass "alle Abfragen des Anzeigedienstes stehen VOR dem Zeichnen" else fail "gezeichnet wurde nicht nach der Freigabe (Zeichnen Zeile '$DRAW_LINE', letzte Abfrage Zeile '$LAST_CHECK')" fi [ "$CHECKS" -ge 3 ] && pass "es wurde wirklich gewartet ($CHECKS Abfragen)" || fail "nur $CHECKS Abfrage(n) - es wurde gar nicht auf die Freigabe gewartet" echo echo "=== Abschluss D: ein Anzeigedienst, der nicht loslaesst, haelt das Herunterfahren nicht auf ===" # Lieber kein Abschlussbild als ein haengendes Herunterfahren - und auf keinen Fall ein # zweiter Prozess auf den belegten Leitungen. SBX="$TMPROOT/fin4" setup_sandbox "$SBX" export FAKE_DISPLAY_ACTIVE_FOR=9999 set +e ERR=$(NINLIGHT_DISPLAY_RELEASE_WAIT_S=2 run_cli "$SBX" display final poweroff 2>&1 >/dev/null) RC=$? set -e unset FAKE_DISPLAY_ACTIVE_FOR [ "$RC" -eq 0 ] && pass "Exit-Code 0 (ein Anzeigefehler haelt das Herunterfahren nie auf)" || fail "Exit-Code $RC: $ERR" grep -q "STUB display.py" "$SBX/cmd.log" && fail "es wurde auf belegte Leitungen gezeichnet - 'GPIO busy'" || pass "nicht gezeichnet, solange der Dienst haelt" echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR" # Und es wurde NICHT versucht, den Dienst von hier aus zu stoppen: das liefe mitten in # der Abschalt-Transaktion. grep -q "systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'systemctl stop' mitten im Herunterfahren" || pass "kein 'systemctl stop' aus dem Abschaltschritt heraus" echo echo "=== Abschluss E: ein scheiterndes display.py bleibt eine Warnung ===" SBX="$TMPROOT/fin5" setup_sandbox "$SBX" export FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 set +e ERR=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display final reboot 2>&1 >/dev/null) RC=$? set -e unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC - ein Anzeigefehler wuerde das Herunterfahren rot machen: $ERR" echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR" echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden." else echo "Mindestens ein Test ist fehlgeschlagen." >&2 fi exit "$FAILED"