Zwei Befunde der Hardware-Abnahme. 1) Die Schrift war zu klein: compose() zeichnete alles in ImageFont.load_default() (rund 10 px) - auf 250x122 aus zwei Metern nicht lesbar. blocks() vergibt jetzt Rollen (header/hero/detail/big), compose() setzt jede Rolle eigen: Modus fett und weiss auf schwarzem Balken, die wichtigste Zeile gross (Migration: erkannte Lampe, Rettung: SSID), Beiwerk klein, "Station aus" gross und mittig. Die Schrift kommt per ImageFont.truetype() aus FONT_SEARCH_PATHS mit DejaVu (/usr/share/fonts/truetype/dejavu/) als Hauptweg - auf beiden Pis vorhanden. load_default(size=...) scheidet aus: das gibt es erst ab Pillow 10.1, Pi 2 hat 9.4.0. Letzter Rueckfall bleibt load_default() ohne Argument, damit eine fehlende Schrift die Anzeige (und damit nie eine Migration) aufhaelt. lines() bleibt ohne Pillow importierbar, die Breitenkuerzung misst weiter in Pixeln. 2) "display test" scheiterte bei laufendem Anzeigedienst mit einem rohen "lgpio.error: 'GPIO busy'": lgpio.error ist keine gpiozero.GPIOZeroError und fiel durch die Ausnahmeliste in epdconfig.module_init(). Sie ist jetzt aufgefuehrt - weiterhin gezielt, TypeError/AttributeError/NameError bleiben Programmierfehler - und die Meldung nennt bei belegten Leitungen den Anzeigedienst. "display test|clear" stoppt den Dienst fuer die Dauer des Handgriffs und startet ihn danach wieder, aber nur, wenn er vorher lief.
801 lines
40 KiB
Bash
Executable File
801 lines
40 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 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>")))
|
|
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 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"
|