feat(station): Anzeige ausliefern, Migrationsprofil, display-Befehle

Schritt 8 des Plans: die fertige E-Paper-Anzeige wird endlich ausgeliefert
und verdrahtet, dazu zwei liegengebliebene Dinge.

- deploy.sh/setup_pi.sh uebertragen display/ (mit Panel-Treibern und
  HERKUNFT.md) und ninlight-display.service; setup_pi.sh installiert und
  aktiviert die Unit und zieht python3-pil nach - ohne Pillow beendet sich
  der Anzeigedienst still mit 0, das faellt sonst nirgends auf.
- setup_pi.sh legt auch das Profil fuer den Migrations-Zugangspunkt an
  (qconnex-firmware, von der alten Klyqa-Firmware fest vorgegeben, gleiche
  Adresse wie das Rettungsprofil). Ohne das scheiterte "migrate on" auf
  einem frisch eingerichteten Pi an need_profile. Bestehende Profile
  bleiben unangetastet.
- "ninlight-station display test|clear|off"; der Hilfetext erklaert die
  Revisionsbestimmung, denn EPD_REVISION laesst sich nur am Testbild
  bestimmen.
- "off" nimmt die Anzeige mit: erst Dienst stoppen, dann Panel leeren
  (sonst schreibt der Daemon binnen 5 s sein altes Bild zurueck) und beides
  vor ap_switch, das bei haengendem NetworkManager abbricht. Ein
  stehengebliebenes "MIGRATION" wuerde einen Betriebszustand behaupten, den
  es nicht mehr gibt.
- test_host.sh sammelt tools/mkimage als Ganzes ein: die 19 Tests in
  test_nl_mkimage.py liefen in keiner Suite mehr mit, darunter die Wache
  fuer die Python-3.8-Randbedingung des Build-Containers. Ein neuer Test
  haelt die Regel fest, dass keine Testdatei mehr herausfallen kann.
- Nebenbei: die Temp-Datei mit dem WLAN-Passwort wird wieder zuverlaessig
  aufgeraeumt, und leere Konfigurationswerte legen kein kaputtes Profil an.
This commit is contained in:
Michael Sauer
2026-09-13 23:19:14 +02:00
parent 22f92988dd
commit cc398ef57a
8 changed files with 795 additions and 54 deletions
+175 -2
View File
@@ -164,9 +164,31 @@ EOF
setup_sandbox() {
local sandbox=$1 unitfile=${2:-$REAL_UNIT}
rm -rf "$sandbox"
mkdir -p "$sandbox/run" "$sandbox/images/migrate" "$sandbox/bin" "$sandbox/nmstate"
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'
@@ -192,7 +214,7 @@ run_cli() {
PATH="$sandbox/bin:$PATH" \
NINLIGHT_STATION_CONF="$sandbox/station.conf" \
NINLIGHT_RUN_DIR="$sandbox/run" \
NINLIGHT_STATION_LIB="$STATION_DIR" \
NINLIGHT_STATION_LIB="$sandbox/lib" \
"$CLI" "$@"
}
@@ -549,6 +571,157 @@ echo "$OUT" | grep -qE "Modus[[:space:]]*: -" && pass "Modus zeigt '-' (keine st
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")"
# Nicht gleichzeitig den Anzeigedienst umbringen: 'display test' ist ein Handgriff bei
# der Abnahme, kein Ausschalten.
grep -Fq "systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'display test' stoppt den Anzeigedienst - das soll nur 'display off'/'off'" || pass "'display test' laesst den Anzeigedienst in Ruhe"
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'"
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)
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
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)
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
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)
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."
+108
View File
@@ -51,6 +51,17 @@ setup_sandbox() {
echo "# dummy" >"$sandbox/tools/station/products.py"
echo "# dummy" >"$sandbox/tools/station/ninlight-station"
echo "# dummy" >"$sandbox/tools/station/ninlight-station.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-display.service"
# display/ wird ECHT kopiert, nicht durch Attrappen ersetzt: die Zusage aus Task 6b
# ist, dass die beiden uebernommenen Panel-Treiber UND der Herkunftsnachweis
# HERKUNFT.md mit auf den Pi gehen. Mit selbst erfundenen Dateinamen im Sandbox-Repo
# wuerde der Test genau das nicht mehr zeigen - er pruefte nur noch sich selbst.
cp -R "$STATION_DIR/display" "$sandbox/tools/station/display"
rm -rf "$sandbox/tools/station/display/__pycache__"
# Ein __pycache__ mit .pyc dieses Rechners (andere Python-Version als der Pi) - es
# darf nicht mit uebertragen werden.
mkdir -p "$sandbox/tools/station/display/__pycache__"
echo "pyc-muell" >"$sandbox/tools/station/display/__pycache__/render.cpython-999.pyc"
# Fake nl_mkimage.py --verify: verhaelt sich wie das echte Tool (eine Zeile
# "<geraet> <version> <bytes>" auf stdout bei Erfolg, Exit 0; eine Zeile auf
@@ -111,9 +122,22 @@ echo "STUB sudo \$*" >>"$log"
exec "\$@"
EOF
# Der Stub unterscheidet nach Unit: deploy.sh behandelt den Anzeigedienst
# ausdruecklich anders als den Hauptdienst (ein fehlgeschlagener Neustart der
# Anzeige ist KEIN Deploy-Fehler). Mit einem Schalter fuer beide Units zusammen
# liesse sich genau dieser Unterschied nicht pruefen.
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
case "\$*" in
*ninlight-display*)
case "\$1" in
is-active) exit "\${FAKE_DISPLAY_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_DISPLAY_RESTART_EXIT:-0}" ;;
*) exit 0 ;;
esac
;;
esac
case "\$1" in
is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;;
@@ -146,6 +170,7 @@ run_deploy() {
NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \
NINLIGHT_DISPLAY_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" \
NINLIGHT_SIGN_DIR="$sign_dir" \
"$sandbox/tools/station/deploy.sh" "$@"
}
@@ -948,6 +973,89 @@ for label in truncated empty array null string null-entry; do
fi
done
echo
echo "=== Szenario 22 (Task 6b, Punkt 1): deploy.sh liefert Anzeigeverzeichnis und Anzeige-Unit mit aus ==="
SANDBOX="$TMPROOT/s22"; STUBBIN="$TMPROOT/bin22"; LOG="$TMPROOT/s22.log"; SANDBOX_STATE="$TMPROOT/s22-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
# Bestand auf dem "Pi": eine Datei, die es im Repo nicht mehr gibt. display/ ist reiner
# Code aus dem Repo - was dort verschwindet, muss auch hier verschwinden, sonst laedt
# ein zurueckgebliebenes altes Modul weiter mit.
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/display"
echo "alt" >"$SANDBOX_STATE/opt/ninlight-station/display/veraltet.py"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
DISP="$SANDBOX_STATE/opt/ninlight-station/display"
for F in display.py render.py epdconfig.py epd2in13_V2.py epd2in13_V3.py HERKUNFT.md; do
[ -f "$DISP/$F" ] && pass "display/$F ausgeliefert" || fail "display/$F fehlt unter $DISP"
done
[ -f "$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" ] && pass "ninlight-display.service ausgeliefert" || fail "Anzeige-Unit fehlt - sie waere weiter toter Text im Repo"
[ ! -f "$DISP/veraltet.py" ] && pass "im Repo geloeschte Datei verschwindet auch auf dem Pi (--delete)" || fail "veraltet.py blieb im Zielverzeichnis liegen"
[ ! -d "$DISP/__pycache__" ] && pass "kein __pycache__ dieses Rechners auf dem Pi gelandet" || fail "__pycache__ wurde mit uebertragen: $(ls "$DISP/__pycache__")"
# Die Anzeige lief nicht (Stub-Default) - dann wird sie auch nicht neu gestartet, aber
# der Deploy sagt es.
echo "$OUT" | grep -q "Anzeigedienst laeuft nicht" && pass "Deploy meldet, dass der Anzeigedienst nicht laeuft" || fail "Ausgabe erwaehnt den Anzeigedienst nicht: $OUT"
# Die fluechtige Ablage wird wieder aufgeraeumt (sie liegt, wie bei server.py & Co.,
# im echten /tmp - deploy.sh's REMOTE-Block kennt dafuer keine Sandbox-Variable).
[ ! -d /tmp/ninlight-display ] && pass "die fluechtige Ablage /tmp/ninlight-display wurde aufgeraeumt" || fail "/tmp/ninlight-display blieb liegen"
echo
echo "=== Szenario 23: laeuft die Anzeige, wird sie nach dem Deploy neu gestartet ==="
# Ohne Neustart liefe sie mit dem gerade ersetzten Code weiter.
SANDBOX="$TMPROOT/s23"; STUBBIN="$TMPROOT/bin23"; LOG="$TMPROOT/s23.log"; SANDBOX_STATE="$TMPROOT/s23-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_DISPLAY_ACTIVE_EXIT=0 run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "systemctl restart ninlight-display" "$LOG" && pass "Anzeigedienst wurde neu gestartet" || fail "kein 'systemctl restart ninlight-display' im Log: $(cat "$LOG")"
echo "$OUT" | grep -q "Anzeigedienst neu gestartet" && pass "Ausgabe meldet den Neustart" || fail "Ausgabe meldet den Neustart nicht: $OUT"
echo
echo "=== Szenario 24: ein fehlgeschlagener Neustart der ANZEIGE ist kein Deploy-Fehler ==="
# Die Anzeige ist nirgends Voraussetzung fuer eine Migration. Ein Deploy, der wegen des
# Panels rot wird, verleitet nur dazu, einen echten Fehler zu uebersehen. Beim
# HAUPTdienst ist es umgekehrt - das prueft das Szenario darunter zur Abgrenzung.
SANDBOX="$TMPROOT/s24"; STUBBIN="$TMPROOT/bin24"; LOG="$TMPROOT/s24.log"; SANDBOX_STATE="$TMPROOT/s24-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_DISPLAY_ACTIVE_EXIT=0 FAKE_DISPLAY_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>"$TMPROOT/s24.err")
RC=$?
set -e
ERR=$(cat "$TMPROOT/s24.err")
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem Anzeige-Neustart" || fail "Exit-Code $RC - der Deploy wurde wegen der Anzeige rot: $ERR"
echo "$ERR" | grep -q "Warnung" && pass "Warnung (keine Fehlermeldung) auf stderr" || fail "keine Warnung auf stderr: $ERR"
[ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] && pass "der Deploy selbst lief vollstaendig durch" || fail "server.py fehlt - der Deploy brach ab"
echo
echo "=== Szenario 25 (Abgrenzung): ein fehlgeschlagener Neustart des HAUPTdienstes ist sehr wohl ein Deploy-Fehler ==="
SANDBOX="$TMPROOT/s25"; STUBBIN="$TMPROOT/bin25"; LOG="$TMPROOT/s25.log"; SANDBOX_STATE="$TMPROOT/s25-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 beim Hauptdienst" || fail "Exit-Code 0 - ein toter Hauptdienst ginge unbemerkt durch: $OUT"
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
+184 -1
View File
@@ -55,12 +55,41 @@ echo "STUB scp \$*" >>"$log"
args=()
for a in "\$@"; do
case "\$a" in
# "-r" muss durchgereicht werden (als cp -R): setup_pi.sh uebertraegt das
# Anzeigeverzeichnis als Ganzes, und ein Stub, der das Flag verschluckt, laesst
# "cp" an einem Verzeichnis scheitern - der Test wuerde dann den Stub anzeigen,
# nicht das Skript. Alle anderen Optionen (-q) bleiben weiterhin draussen.
-r|-R) args+=("-R") ;;
-*) continue ;;
*:*) args+=("\${a#*:}") ;;
*) args+=("\$a") ;;
esac
done
exec cp "\${args[@]}"
EOF
# apt-get gibt es auf dem Entwicklungsrechner nicht - und selbst auf einem Debian
# duerfte ein Test es nie wirklich aufrufen. Der Stub protokolliert nur.
cat >"$bindir/apt-get" <<EOF
#!/usr/bin/env bash
echo "STUB apt-get \$*" >>"$log"
exit "\${FAKE_APT_EXIT:-0}"
EOF
# Steht fuer das python3 DES PI, nicht fuer das des Testrechners: nur die
# Pillow-Probe wird beantwortet (steuerbar per FAKE_PIL_MISSING - auf diesem Rechner
# ist Pillow sonst je nach Installation zufaellig vorhanden oder nicht, und der
# Paketschritt waere damit gar nicht pruefbar). Jeder andere Aufruf laeuft durch das
# echte python3.
cat >"$bindir/python3" <<EOF
#!/usr/bin/env bash
echo "STUB python3 \$*" >>"$log"
for a in "\$@"; do
case "\$a" in
*"import PIL"*) exit "\${FAKE_PIL_MISSING:-0}" ;;
esac
done
exec "$(command -v python3)" "\$@"
EOF
# "sudo install -o root -g root ..." (im NetworkManager-Keyfile-Schritt) braucht
@@ -104,7 +133,8 @@ fi
exit 0
EOF
chmod +x "$bindir"/ssh "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl "$bindir"/nmcli
chmod +x "$bindir"/ssh "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl "$bindir"/nmcli \
"$bindir"/apt-get "$bindir"/python3
}
# $1 = Sandbox-Wurzel fuer die "Systempfade"
@@ -133,6 +163,7 @@ run_setup() {
NINLIGHT_STATION_ROOT="$state/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$state/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \
NINLIGHT_DISPLAY_UNIT="$state/etc/systemd/system/ninlight-display.service" \
NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \
"$STATION_DIR/setup_pi.sh" pi-test
}
@@ -247,6 +278,158 @@ for KEY in MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
grep -Eq "^${KEY}=" "$CONF_OUT4" && pass "fehlender Schluessel $KEY wurde trotzdem ergaenzt" || fail "Schluessel $KEY fehlt weiterhin"
done
echo
echo "=== Szenario 5 (Task 6b, Punkt 1): Anzeige wird ausgeliefert, installiert und aktiviert ==="
STATE="$TMPROOT/s5-state"; STUBBIN="$TMPROOT/bin5"; LOG="$TMPROOT/s5.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
DISP="$STATE/opt/ninlight-station/display"
# Die Panel-Treiber und der Herkunftsnachweis gehoeren mit auf den Pi, nicht nur ins
# Repo: HERKUNFT.md ist der Nachweis, woher der uebernommene Waveshare-Code stammt.
for F in display.py render.py epdconfig.py epd2in13_V2.py epd2in13_V3.py HERKUNFT.md; do
[ -f "$DISP/$F" ] && pass "display/$F ausgeliefert" || fail "display/$F fehlt unter $DISP"
done
[ -x "$DISP/display.py" ] && pass "display.py ist ausfuehrbar (Handbetrieb 'display test')" || fail "display.py ist nicht ausfuehrbar"
MODE=$(file_mode "$DISP/render.py")
[ "$MODE" = "644" ] && pass "importierter Code ist 0644" || fail "render.py hat Rechte $MODE statt 644"
[ ! -d "$DISP/__pycache__" ] && pass "kein __pycache__ auf dem Pi gelandet" || fail "__pycache__ wurde mitkopiert"
[ -f "$STATE/etc/systemd/system/ninlight-display.service" ] && pass "ninlight-display.service installiert" || fail "Anzeige-Unit fehlt - die Unit waere weiter toter Text im Repo"
# Ohne "enable" startet die Anzeige nach einem Stromausfall nicht wieder mit.
grep -Fq "systemctl enable ninlight-display.service" "$LOG" && pass "Anzeigedienst wurde aktiviert" || fail "kein 'systemctl enable ninlight-display' im Log"
grep -Fq "systemctl restart ninlight-display.service" "$LOG" && pass "Anzeigedienst wurde gestartet" || fail "kein 'systemctl restart ninlight-display' im Log"
# Die Station selbst bleibt nach dem Einrichten absichtlich AUS - nur die Anzeige
# laeuft. Ein versehentliches "enable ninlight-station" waere ein Sicherheitsfehler:
# der Zugangspunkt kaeme nach jedem Boot von allein hoch.
grep -Fq "systemctl enable ninlight-station" "$LOG" && fail "setup_pi.sh aktiviert den Stationsdienst - der Zugangspunkt kaeme nach jedem Boot von allein hoch" || pass "Stationsdienst bleibt deaktiviert (Station ist nach dem Einrichten aus)"
echo
echo "=== Szenario 6 (Task 6b, Punkt 1): python3-pil wird nachgezogen, wenn Pillow fehlt ==="
# Ohne Pillow beendet sich der Anzeigedienst still mit 0 (Absicht) - das faellt
# nirgends auf, also muss das Paket hier kommen.
STATE="$TMPROOT/s6-state"; STUBBIN="$TMPROOT/bin6"; LOG="$TMPROOT/s6.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(FAKE_PIL_MISSING=1 run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "apt-get install -y python3-pil" "$LOG" && pass "python3-pil wird per apt nachinstalliert" || fail "kein 'apt-get install -y python3-pil' im Log"
echo
echo "=== Szenario 7: ist Pillow schon da, wird apt gar nicht erst bemueht ==="
STATE="$TMPROOT/s7-state"; STUBBIN="$TMPROOT/bin7"; LOG="$TMPROOT/s7.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "apt-get" "$LOG" && fail "apt-get wurde trotz vorhandenem Pillow aufgerufen" || pass "kein apt-Aufruf, wenn Pillow schon vorhanden ist"
echo
echo "=== Szenario 8 (Task 6b, Punkt 2): das Profil fuer den Migrations-Zugangspunkt wird angelegt ==="
# Ohne dieses zweite Profil scheitert "ninlight-station migrate on" auf einem frisch
# eingerichteten Pi an need_profile "$MIGRATE_AP_CONNECTION".
#
# Geprueft wird der Zustand aus Szenario 7 (ein vollstaendiger Lauf auf einem frischen
# Pi, $STATE/$LOG zeigen noch dorthin) - deshalb hier kein eigener Lauf.
NMDIR="$STATE/etc/NetworkManager/system-connections"
MIGRATE_FILE="$NMDIR/qconnex-firmware.nmconnection"
[ -f "$MIGRATE_FILE" ] && pass "qconnex-firmware.nmconnection angelegt" || fail "Migrationsprofil fehlt in $NMDIR: $(ls "$NMDIR")"
[ -f "$NMDIR/ninlight-recovery.nmconnection" ] && pass "Rettungsprofil weiterhin angelegt" || fail "Rettungsprofil fehlt"
# SSID und PSK sind von der alten Klyqa-Firmware fest vorgegeben - weicht eines ab,
# findet keine alte Lampe die Migrationsstation.
grep -Fqx "ssid=qconnex-firmware" "$MIGRATE_FILE" && pass "SSID ist die von der Klyqa-Firmware vorgegebene" || fail "falsche SSID: $(grep '^ssid=' "$MIGRATE_FILE")"
grep -Fqx "psk=qconnex-firmware" "$MIGRATE_FILE" && pass "PSK ist der von der Klyqa-Firmware vorgegebene" || fail "falscher PSK"
grep -Fqx "mode=ap" "$MIGRATE_FILE" && pass "Profil ist ein Zugangspunkt" || fail "mode=ap fehlt"
grep -Fqx "method=shared" "$MIGRATE_FILE" && pass "ipv4.method ist 'shared'" || fail "method=shared fehlt"
# Dieselbe Adresse wie das Rettungsprofil - genau deshalb schliessen sich beide aus,
# und genau darauf verlaesst sich ap_switch() in ninlight-station.
grep -Fqx "address1=192.168.8.1/24" "$MIGRATE_FILE" && pass "Adresse 192.168.8.1/24 (wie das Rettungsprofil)" || fail "falsche Adresse: $(grep '^address1=' "$MIGRATE_FILE")"
grep -Fqx "autoconnect=false" "$MIGRATE_FILE" && pass "kein Selbststart beim Booten" || fail "autoconnect ist nicht false - der Migrations-AP kaeme nach jedem Boot von allein hoch"
MODE=$(file_mode "$MIGRATE_FILE")
[ "$MODE" = "600" ] && pass "Keyfile ist 0600 (das Passwort steht darin)" || fail "Keyfile hat Rechte $MODE statt 600"
# Das Passwort darf nie als Kommandozeilenargument an nmcli gehen (in "ps" sichtbar).
grep -Fq "nmcli connection load" "$LOG" && pass "Profil wird per 'connection load' eingelesen" || fail "kein 'nmcli connection load' im Log"
grep -Eq "STUB nmcli .*(psk|wifi-sec)" "$LOG" && fail "das Passwort wurde als nmcli-Argument uebergeben - in 'ps' fuer jeden lokalen Benutzer sichtbar" || pass "kein Passwort in einer nmcli-Kommandozeile"
echo
echo "=== Szenario 9 (Task 6b, Punkt 2): ein bestehendes Migrationsprofil bleibt unangetastet ==="
STATE="$TMPROOT/s9-state"; STUBBIN="$TMPROOT/bin9"; LOG="$TMPROOT/s9.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
NMDIR9="$STATE/etc/NetworkManager/system-connections"
# Von Hand angelegt, mit handjustierten Feldern (anderer Kanal), die dieses Skript
# gar nicht kennt.
cat >"$NMDIR9/qconnex-firmware.nmconnection" <<'NM'
[connection]
id=qconnex-firmware
uuid=11111111-2222-3333-4444-555555555555
type=wifi
[wifi]
mode=ap
ssid=qconnex-firmware
channel=11
NM
chmod 0600 "$NMDIR9/qconnex-firmware.nmconnection"
BEFORE=$(cat "$NMDIR9/qconnex-firmware.nmconnection")
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
if [ "$BEFORE" = "$(cat "$NMDIR9/qconnex-firmware.nmconnection")" ]; then
pass "bestehendes Migrationsprofil wurde Byte fuer Byte unveraendert gelassen"
else
fail "bestehendes Migrationsprofil wurde ueberschrieben: $(cat "$NMDIR9/qconnex-firmware.nmconnection")"
fi
echo "$OUT" | grep -q "unveraendert gelassen" && pass "Ausgabe sagt, dass das Profil unveraendert blieb" || fail "Ausgabe erwaehnt das nicht: $OUT"
# Das Rettungsprofil dagegen wird bewusst neu geschrieben (SSID/PSK koennen sich
# geaendert haben) - aber unter Beibehaltung der UUID.
[ -f "$NMDIR9/ninlight-recovery.nmconnection" ] && pass "Rettungsprofil trotzdem angelegt" || fail "Rettungsprofil fehlt"
echo
echo "=== Szenario 10: ein leerer Konfigurationswert legt kein kaputtes Profil an ==="
STATE="$TMPROOT/s10-state"; STUBBIN="$TMPROOT/bin10"; LOG="$TMPROOT/s10.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
cat >"$STATE/etc/ninlight-station.conf" <<'CONF'
SSID='eigene-ssid'
PSK='eigenes-passwort'
IMAGE_DIR='/opt/ninlight-station/images'
HOME_CONNECTION='preconfigured'
AP_CONNECTION='ninlight-recovery'
MIGRATE_SSID='qconnex-firmware'
MIGRATE_PSK=''
MIGRATE_AP_CONNECTION='qconnex-firmware'
EPD_REVISION='V3'
CONF
chmod 0600 "$STATE/etc/ninlight-station.conf"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 bei leerem PSK" || fail "Exit-Code 0 trotz leerem Migrations-PSK: $OUT"
echo "$OUT" | grep -qi "leer" && pass "Fehlermeldung nennt den leeren Wert" || fail "Fehlermeldung fehlt: $OUT"
NMDIR10="$STATE/etc/NetworkManager/system-connections"
if [ -f "$NMDIR10/qconnex-firmware.nmconnection" ]; then
fail "trotz leerem PSK wurde ein Migrationsprofil geschrieben"
else
pass "kein Migrationsprofil mit leerem PSK angelegt"
fi
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."