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
@@ -0,0 +1,86 @@
"""Wache dagegen, dass eine Testdatei aus allen Suiten herausfaellt.
Genau das war schon einmal passiert: tools/test_host.sh rief pytest auf
tools/mkimage/tests und tools/station/tests auf - tools/mkimage/test_nl_mkimage.py
liegt aber DIREKT in tools/mkimage/ und lief damit in keiner Suite mehr mit. 19 Tests
waren still verschwunden, darunter test_stays_python38_compatible, die Wache fuer die
bindende 3.8-Randbedingung des Docker-Build-Containers (`str | None` in nl_mkimage.py
hatte den Build schon einmal gebrochen).
Eine stumm ausgefallene Testdatei ist schlimmer als eine fehlende: die Suite bleibt
gruen und behauptet damit eine Deckung, die es nicht gibt. Deshalb wird hier nicht ein
einzelner Pfad festgenagelt, sondern die Regel selbst - JEDE test_*.py unter tools/
muss von einem der Pfade erfasst sein, die tools/test_host.sh an pytest uebergibt.
Eine neue Testdatei an einer neuen Stelle faellt damit sofort auf, statt erst dann,
wenn jemand sie vermisst.
"""
import re
from pathlib import Path
TOOLS_DIR = Path(__file__).resolve().parents[2]
REPO_ROOT = TOOLS_DIR.parent
TEST_HOST = TOOLS_DIR / "test_host.sh"
# Verzeichnisse, die keine Tests DIESES Projekts enthalten: virtuelle Umgebungen und
# installierte Fremdpakete bringen eigene test_*.py mit, die hier nichts zu suchen haben.
IGNORED_PARTS = (".venv", "__pycache__", "site-packages", "node_modules", ".git")
def pytest_paths():
"""Die Pfade, die tools/test_host.sh an pytest uebergibt, als aufgeloeste Pfade."""
text = TEST_HOST.read_text()
match = re.search(r"^\s*python3 -m pytest (.+?)(?:\s+-\S+)*\s*$", text, re.M)
assert match, "in tools/test_host.sh ist kein 'python3 -m pytest'-Aufruf zu finden"
paths = []
for raw in match.group(1).split():
arg = raw.strip('"')
if arg.startswith("-"):
continue
paths.append(Path(arg.replace("$ROOT", str(REPO_ROOT))).resolve())
assert paths, "der pytest-Aufruf in tools/test_host.sh uebergibt keine Pfade"
return paths
def own_test_files():
return sorted(
p.resolve()
for p in TOOLS_DIR.rglob("test_*.py")
if not any(part in IGNORED_PARTS for part in p.parts)
)
def test_every_test_file_is_reached_by_test_host():
roots = pytest_paths()
orphans = []
for test_file in own_test_files():
if not any(test_file == r or r in test_file.parents for r in roots):
orphans.append(test_file.relative_to(REPO_ROOT))
assert not orphans, (
"diese Testdateien laufen in keiner Suite mit - tools/test_host.sh sammelt sie "
"nicht ein: {}. Entweder den Pfad in test_host.sh erweitern oder die Datei "
"dorthin verschieben, wo sie eingesammelt wird.".format(
", ".join(str(o) for o in orphans)
)
)
def test_python38_guard_is_among_them():
"""Die konkrete Datei, deren Ausfall den Build-Bruch damals durchgelassen haette."""
guard = (TOOLS_DIR / "mkimage" / "test_nl_mkimage.py").resolve()
assert guard.exists(), "tools/mkimage/test_nl_mkimage.py fehlt"
roots = pytest_paths()
assert any(guard == r or r in guard.parents for r in roots), (
"tools/mkimage/test_nl_mkimage.py wird von tools/test_host.sh nicht eingesammelt "
"- darin steht test_stays_python38_compatible, die einzige Wache gegen erneute "
"3.10-Syntax in nl_mkimage.py (der Docker-Build-Container hat Python 3.8.10)"
)
def test_vermin_is_installed():
"""vermin fehlt -> harter Fehlschlag, kein Skip.
Eine Wache, die sich beim Fehlen ihres Werkzeugs stillschweigend abschaltet, haette
den Build-Bruch von damals genauso durchgelassen wie gar kein Test. Nachinstallieren:
pip install vermin (oder pip3 install --user vermin).
"""
import vermin # noqa: F401
+31 -2
View File
@@ -202,7 +202,14 @@ echo "== uebertragen =="
# ausgelieferte Verzeichnis. # ausgelieferte Verzeichnis.
# -e: rsync soll fuer den Transport zum Pi denselben ssh wie oben benutzen. # -e: rsync soll fuer den Transport zum Pi denselben ssh wie oben benutzen.
rsync -a -c --delete -e "$SSH_BIN" "$STAGE/" "$HOST:/tmp/ninlight-images/" rsync -a -c --delete -e "$SSH_BIN" "$STAGE/" "$HOST:/tmp/ninlight-images/"
rsync -a -e "$SSH_BIN" "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" "$HERE/ninlight-station.service" "$HOST:/tmp/" rsync -a -e "$SSH_BIN" "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \
"$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HOST:/tmp/"
# Das Anzeigeverzeichnis als Ganzes (render.py, display.py, epdconfig.py, die beiden
# uebernommenen Panel-Treiber und HERKUNFT.md - der Herkunftsnachweis gehoert mit auf
# den Pi, nicht nur ins Repo). --delete gilt hier nur fuer die fluechtige Ablage unter
# /tmp; __pycache__ bleibt draussen, sonst landen die .pyc dieses Rechners (andere
# Python-Version als auf dem Pi) im Zielverzeichnis und bleiben dort liegen.
rsync -a -c --delete --exclude=__pycache__ -e "$SSH_BIN" "$HERE/display/" "$HOST:/tmp/ninlight-display/"
# Pfade als Variablen mit Default: so kann ein Test die Installation gegen # Pfade als Variablen mit Default: so kann ein Test die Installation gegen
# ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen lassen. # ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen lassen.
"$SSH_BIN" "$HOST" bash -s -- "$FULL_SYNC" "$MIGRATE_COMPLETE" <<'REMOTE' "$SSH_BIN" "$HOST" bash -s -- "$FULL_SYNC" "$MIGRATE_COMPLETE" <<'REMOTE'
@@ -212,6 +219,7 @@ MIGRATE_COMPLETE=$2
ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station} ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station}
BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station} BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station}
UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service} UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service}
DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service}
sudo install -d -m 0755 "$ROOT_DIR/images" sudo install -d -m 0755 "$ROOT_DIR/images"
if [ "$FULL_SYNC" -eq 1 ]; then if [ "$FULL_SYNC" -eq 1 ]; then
@@ -333,7 +341,14 @@ sudo install -m 0755 /tmp/server.py "$ROOT_DIR/server.py"
sudo install -m 0644 /tmp/products.py "$ROOT_DIR/products.py" sudo install -m 0644 /tmp/products.py "$ROOT_DIR/products.py"
sudo install -m 0755 /tmp/ninlight-station "$BIN_PATH" sudo install -m 0755 /tmp/ninlight-station "$BIN_PATH"
sudo install -m 0644 /tmp/ninlight-station.service "$UNIT_PATH" sudo install -m 0644 /tmp/ninlight-station.service "$UNIT_PATH"
rm -rf /tmp/ninlight-images /tmp/server.py /tmp/products.py /tmp/ninlight-station /tmp/ninlight-station.service # Anzeige: Verzeichnis und Unit. --delete ist hier richtig (anders als bei den Images):
# display/ ist reiner Code aus dem Repo, es gibt keinen Bestand, der einen Deploy
# ueberleben muesste - eine im Repo geloeschte Datei muss auch hier verschwinden.
sudo install -d -m 0755 "$ROOT_DIR/display"
sudo rsync -a -c --delete /tmp/ninlight-display/ "$ROOT_DIR/display/"
sudo install -m 0644 /tmp/ninlight-display.service "$DISPLAY_UNIT_PATH"
rm -rf /tmp/ninlight-images /tmp/server.py /tmp/products.py /tmp/ninlight-station /tmp/ninlight-station.service \
/tmp/ninlight-display /tmp/ninlight-display.service
sudo systemctl daemon-reload sudo systemctl daemon-reload
if systemctl is-active --quiet ninlight-station; then if systemctl is-active --quiet ninlight-station; then
if sudo systemctl restart ninlight-station; then if sudo systemctl restart ninlight-station; then
@@ -345,6 +360,20 @@ if systemctl is-active --quiet ninlight-station; then
else else
echo " Dienst laeuft nicht (Station ist aus)" echo " Dienst laeuft nicht (Station ist aus)"
fi fi
# Die Anzeige laeuft mit dem gerade ersetzten Code weiter, bis sie neu gestartet wird.
# Ein Fehlschlag ist hier aber ausdruecklich KEIN Deploy-Fehler (anders als oben beim
# Hauptdienst): die Anzeige ist nirgends Voraussetzung fuer eine Migration, und ein
# Deploy, der wegen des Panels rot wird, verleitet nur dazu, den echten Fehler oben zu
# uebersehen.
if systemctl is-active --quiet ninlight-display; then
if sudo systemctl restart ninlight-display; then
echo " Anzeigedienst neu gestartet"
else
echo " Warnung: Anzeigedienst lief, Neustart ist fehlgeschlagen - journalctl -u ninlight-display" >&2
fi
else
echo " Anzeigedienst laeuft nicht"
fi
REMOTE REMOTE
echo "fertig." echo "fertig."
+56 -1
View File
@@ -257,8 +257,41 @@ migrate_on() {
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist." echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
} }
# Ruft display.py mit einem Schalter auf. EPD_REVISION steht in $CONF und wird von
# ". $CONF" nur als Shell-Variable gesetzt, nicht exportiert - "env VAR=..." reicht sie
# an den Python-Prozess durch. "sudo", weil /dev/spidev0.0 und die GPIO-Leitungen root
# gehoeren (der Anzeigedienst laeuft aus demselben Grund als root).
display_run() {
sudo env EPD_REVISION="$EPD_REVISION" python3 "$STATION_LIB/display/display.py" "$1"
}
# Nimmt die Anzeige mit aus. Reihenfolge: erst den Dienst stoppen, dann das Panel leeren -
# umgekehrt zeichnet der noch laufende Daemon binnen 5 s sein altes Bild wieder darauf.
#
# Warum leeren und nicht "Station aus" stehen lassen: der Anzeigedienst erkennt den
# ausgeschalteten Zustand zwar selbst (systemctl is-active), aber ein laufender
# Anzeigedienst nach "off" bedeutet, dass "off" eben NICHT alles abgeschaltet hat. Ein
# leeres Panel behauptet dagegen gar nichts - und das ist bei einer Station, die Lampen
# ungefragt Firmware unterschieben kann, die einzig ungefaehrliche Richtung: ein
# stehengebliebenes "MIGRATION"-Bild wuerde einen Betriebszustand behaupten, den es nicht
# mehr gibt, waehrend ein leeres Panel hoechstens nach "aus" aussieht - was es ist.
display_off() {
sudo systemctl stop ninlight-display 2>/dev/null || true
# Kein Abbruch, wenn das Panel fehlt: display.py beendet sich dann selbst mit 0, und
# ein Fehler an der Anzeige darf das Ausschalten der Station nie aufhalten.
display_run --clear || echo "Warnung: Panel konnte nicht geleert werden - Anzeige pruefen" >&2
}
full_off() { full_off() {
sudo systemctl stop ninlight-station 2>/dev/null || true sudo systemctl stop ninlight-station 2>/dev/null || true
# VOR ap_switch, nicht danach: ap_switch "" bricht bei einem haengenden
# NetworkManager mit Fehler ab, und unter "set -e" kaeme danach nichts mehr. Genau
# dann - Zugangspunkt laesst sich nicht abschalten - waere die Anzeige sonst der
# letzte Rest von "off", der nie ausgefuehrt wird: Daemon laeuft weiter, Panel zeigt
# unveraendert "MIGRATION". Ein leeres Panel behauptet dagegen nichts; die Wahrheit
# ueber den noch offenen Zugangspunkt steht in ap_switch's Fehlermeldung, wo man sie
# liest, statt auf einem Panel, das man auch fuer aktuell halten koennte.
display_off
ap_switch "" ap_switch ""
if nmcli -t -f NAME con show | grep -Fqx "$HOME_CONNECTION"; then if nmcli -t -f NAME con show | grep -Fqx "$HOME_CONNECTION"; then
if sudo nmcli con up "$HOME_CONNECTION" >/dev/null; then if sudo nmcli con up "$HOME_CONNECTION" >/dev/null; then
@@ -439,8 +472,27 @@ PY
exit 0 exit 0
} }
display_cmd() {
case "${1:-}" in
test) display_run --test ;;
clear) display_run --clear ;;
off) display_off ;;
*) usage; exit 2 ;;
esac
}
usage() { usage() {
echo "Aufruf: ninlight-station on|recovery {on|off}|migrate {on [--listen]|off}|arm [<target>]|listen|off|status" >&2 echo "Aufruf: ninlight-station on|recovery {on|off}|migrate {on [--listen]|off}|arm [<target>]|listen|display {test|clear|off}|off|status" >&2
echo "" >&2
echo " display test Testbild aufs Panel zeichnen. Das ist der erste Handgriff bei der" >&2
echo " Hardware-Abnahme und die EINZIGE Moeglichkeit, die Panel-Revision" >&2
echo " zu bestimmen: die steht am Geraet nirgends. Bleibt das Bild leer," >&2
echo " verzerrt oder verschoben, ist EPD_REVISION in" >&2
echo " /etc/ninlight-station.conf falsch - auf die andere Revision" >&2
echo " umstellen (V2 statt V3 oder umgekehrt), das Panel kurz" >&2
echo " stromlos machen und erneut 'display test'." >&2
echo " display clear Panel leeren." >&2
echo " display off Anzeigedienst stoppen und das Panel leeren (macht 'off' mit)." >&2
} }
case "${1:-}" in case "${1:-}" in
@@ -467,6 +519,9 @@ case "${1:-}" in
listen) listen)
listen_cmd listen_cmd
;; ;;
display)
display_cmd "${2:-}"
;;
off) off)
full_off full_off
;; ;;
+118 -23
View File
@@ -77,8 +77,31 @@ fi
rm -f "$TEMPLATE" rm -f "$TEMPLATE"
REMOTE REMOTE
echo "== Server, CLI und Unit installieren ==" echo "== Pakete =="
"$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" "$HERE/ninlight-station.service" "$HOST:$STAGE_DIR/" # Pillow kommt auf dem Pi bewusst aus dem Distributionspaket (python3-pil), nicht per
# pip: tools/station/ laeuft dort unter dem System-Python 3.13, und Debian 13 laesst ein
# "pip install" ins System-Python gar nicht mehr zu (PEP 668). Ohne Pillow zeichnet die
# Anzeige nicht - display.py faengt den ImportError ab und beendet sich still mit 0
# (Absicht, damit ein fehlendes Panel nie eine Migration verhindert). Genau deshalb muss
# das Paket HIER kommen: sonst faellt es nirgends auf.
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
set -e
if python3 -c 'import PIL' 2>/dev/null; then
echo " python3-pil ist bereits vorhanden"
else
sudo apt-get install -y python3-pil
echo " python3-pil installiert"
fi
REMOTE
echo "== Server, CLI und Units installieren =="
"$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \
"$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HOST:$STAGE_DIR/"
# Das Anzeigeverzeichnis als Ganzes: render.py, display.py, epdconfig.py, die beiden
# uebernommenen Panel-Treiber und HERKUNFT.md (der Herkunftsnachweis gehoert mit auf den
# Pi). "scp -r" statt rsync, weil setup_pi.sh - anders als deploy.sh - ausdruecklich nur
# ssh/scp voraussetzt; __pycache__ wird auf der Gegenseite weggeraeumt (unten).
"$SCP_BIN" -q -r "$HERE/display" "$HOST:$STAGE_DIR/ninlight-display"
# Pfade als Variablen mit Default (wie in deploy.sh): so kann ein Test die # Pfade als Variablen mit Default (wie in deploy.sh): so kann ein Test die
# Installation gegen ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen # Installation gegen ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen
# lassen. # lassen.
@@ -88,17 +111,40 @@ STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp}
ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station} ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station}
BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station} BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station}
UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service} UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service}
sudo install -d -m 0755 "$ROOT_DIR" "$ROOT_DIR/images" DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service}
sudo install -d -m 0755 "$ROOT_DIR" "$ROOT_DIR/images" "$ROOT_DIR/display"
sudo install -m 0755 "$STAGE_DIR/server.py" "$ROOT_DIR/server.py" sudo install -m 0755 "$STAGE_DIR/server.py" "$ROOT_DIR/server.py"
sudo install -m 0644 "$STAGE_DIR/products.py" "$ROOT_DIR/products.py" sudo install -m 0644 "$STAGE_DIR/products.py" "$ROOT_DIR/products.py"
sudo install -m 0755 "$STAGE_DIR/ninlight-station" "$BIN_PATH" sudo install -m 0755 "$STAGE_DIR/ninlight-station" "$BIN_PATH"
sudo install -m 0644 "$STAGE_DIR/ninlight-station.service" "$UNIT_PATH" sudo install -m 0644 "$STAGE_DIR/ninlight-station.service" "$UNIT_PATH"
rm -f "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" "$STAGE_DIR/ninlight-station.service" # Anzeige: display.py ist ausfuehrbar (die Unit ruft es zwar ueber /usr/bin/python3 auf,
# "ninlight-station display test" aber auch von Hand), der Rest ist importierter Code.
rm -rf "$STAGE_DIR/ninlight-display/__pycache__"
for f in "$STAGE_DIR"/ninlight-display/*; do
[ -f "$f" ] || continue
case "$(basename "$f")" in
display.py) sudo install -m 0755 "$f" "$ROOT_DIR/display/$(basename "$f")" ;;
*) sudo install -m 0644 "$f" "$ROOT_DIR/display/$(basename "$f")" ;;
esac
done
sudo install -m 0644 "$STAGE_DIR/ninlight-display.service" "$DISPLAY_UNIT_PATH"
rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \
"$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \
"$STAGE_DIR/ninlight-display"
sudo systemctl daemon-reload sudo systemctl daemon-reload
echo " installiert" # Die Anzeige wird aktiviert (startet also beim Booten mit) UND jetzt gestartet - anders
# als die Station selbst, die absichtlich aus bleibt: die Anzeige sendet nichts, sie
# zeigt nur an, und "Station aus" ist genau das, was nach dem Einrichten auf dem Panel
# stehen soll. Fehlt Panel oder Pillow, beendet sich der Dienst still mit 0 (siehe
# display.py) - deshalb "|| true": ein fehlendes Panel darf setup_pi.sh nicht abbrechen.
sudo systemctl enable ninlight-display.service >/dev/null 2>&1 || \
echo " Warnung: 'systemctl enable ninlight-display' fehlgeschlagen" >&2
sudo systemctl restart ninlight-display.service >/dev/null 2>&1 || \
echo " Warnung: 'systemctl restart ninlight-display' fehlgeschlagen" >&2
echo " installiert (Anzeigedienst aktiviert)"
REMOTE REMOTE
echo "== NetworkManager-Profil ==" echo "== NetworkManager-Profile =="
# Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf: # Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf:
# - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht # - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht
# jeder lesen koennen). Der SSH-Benutzer ist aber nicht root, direktes # jeder lesen koennen). Der SSH-Benutzer ist aber nicht root, direktes
@@ -116,29 +162,67 @@ set -e
CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf} CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf}
NM_DIR=${NINLIGHT_NM_DIR:-/etc/NetworkManager/system-connections} NM_DIR=${NINLIGHT_NM_DIR:-/etc/NetworkManager/system-connections}
eval "$(sudo cat "$CONF")" eval "$(sudo cat "$CONF")"
CONN_FILE="$NM_DIR/${AP_CONNECTION}.nmconnection"
if sudo test -f "$CONN_FILE"; then # Die Keyfiles werden erst in einer Temp-Datei aufgebaut und dann per "install"
UUID=$(sudo sed -n 's/^uuid=//p' "$CONN_FILE" | head -n1) # an ihren Platz gelegt - dazwischen liegt das WLAN-Passwort in einer Datei, die
ACTION="aktualisiert" # noch nicht 0600 root:root ist. Ein Abbruch (set -e) genau dort darf sie nicht
# liegen lassen. Ein EXIT-Trap in write_profile() selbst waere dafuer untauglich:
# die Funktion laeuft zweimal, der zweite trap ueberschriebe den ersten, und
# "$tmp" ist "local" - beim Auswerten des Traps am Skriptende gaebe es die
# Variable gar nicht mehr. Deshalb ein gemeinsames Verzeichnis mit genau einem
# Trap darum herum (0700, mktemp -d legt es so an).
PROFILE_TMPDIR=$(mktemp -d)
trap 'rm -rf "$PROFILE_TMPDIR"' EXIT
# $1 = Profilname, $2 = SSID, $3 = PSK, $4 = "update" (bestehendes Profil neu schreiben,
# UUID behalten) oder "keep" (bestehendes Profil unangetastet lassen).
#
# Beide Profile bekommen dieselbe Adresse 192.168.8.1/24 auf wlan0 - genau deshalb
# schliessen sie sich gegenseitig aus, und genau deshalb prueft ninlight-station's
# ap_switch() nach jedem Umschalten nach, dass wirklich nur eines aktiv ist.
write_profile() {
local id=$1 ssid=$2 psk=$3 mode=$4
# Ein leerer Profilname wuerde eine Datei ".nmconnection" anlegen, eine leere SSID
# oder ein leeres PSK ein Profil, das NetworkManager beim Hochfahren stumm ablehnt -
# in beiden Faellen faellt es erst auf, wenn jemand mit einer toten Lampe vor der
# Station steht. Der Konfigurationsschritt oben ergaenzt fehlende Schluessel aus der
# Vorlage; leer sein koennen sie trotzdem (von Hand geleert).
if [ -z "$id" ] || [ -z "$ssid" ] || [ -z "$psk" ]; then
echo "Fehler: Profilname, SSID oder PSK ist leer - $CONF pruefen (Profil '$id')" >&2
return 1
fi
local conn_file="$NM_DIR/${id}.nmconnection"
local uuid action tmp
if sudo test -f "$conn_file"; then
if [ "$mode" = "keep" ]; then
# Der Migrations-AP ist auf manchen Pis von Hand angelegt worden (SSID/PSK sind
# von der alten Klyqa-Firmware fest vorgegeben, es gibt also nichts zu variieren).
# Ein bestehendes Profil wird deshalb nicht ueberschrieben - es koennte
# handjustierte Felder (Kanal, Band, Regulierungsdomaene) tragen, die dieses
# Skript gar nicht kennt.
echo " Profil $id existiert bereits - unveraendert gelassen"
return 0
fi
uuid=$(sudo sed -n 's/^uuid=//p' "$conn_file" | head -n1)
action="aktualisiert"
else else
# /proc/sys/kernel/random/uuid gibt es nur unter Linux (dem Pi) - der Fallback # /proc/sys/kernel/random/uuid gibt es nur unter Linux (dem Pi) - der Fallback
# auf uuidgen ist es, was einen Testlauf gegen den lokalen Stub (macOS) ueberhaupt # auf uuidgen ist es, was einen Testlauf gegen den lokalen Stub (macOS) ueberhaupt
# ermoeglicht, aendert aber nichts am Verhalten auf dem Pi selbst. # ermoeglicht, aendert aber nichts am Verhalten auf dem Pi selbst.
UUID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen | tr '[:upper:]' '[:lower:]') uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen | tr '[:upper:]' '[:lower:]')
ACTION="angelegt" action="angelegt"
fi fi
TMP=$(mktemp) tmp=$(mktemp "$PROFILE_TMPDIR/profile.XXXXXX")
trap 'rm -f "$TMP"' EXIT
{ {
printf '%s\n' '[connection]' printf '%s\n' '[connection]'
printf 'id=%s\n' "$AP_CONNECTION" printf 'id=%s\n' "$id"
printf 'uuid=%s\n' "$UUID" printf 'uuid=%s\n' "$uuid"
printf '%s\n' 'type=wifi' printf '%s\n' 'type=wifi'
printf '%s\n' 'interface-name=wlan0' printf '%s\n' 'interface-name=wlan0'
printf '%s\n' 'autoconnect=false' printf '%s\n' 'autoconnect=false'
printf '\n%s\n' '[wifi]' printf '\n%s\n' '[wifi]'
printf '%s\n' 'mode=ap' printf '%s\n' 'mode=ap'
printf 'ssid=%s\n' "$SSID" printf 'ssid=%s\n' "$ssid"
printf '%s\n' 'band=bg' printf '%s\n' 'band=bg'
printf '%s\n' 'channel=6' printf '%s\n' 'channel=6'
printf '\n%s\n' '[wifi-security]' printf '\n%s\n' '[wifi-security]'
@@ -146,17 +230,23 @@ trap 'rm -f "$TMP"' EXIT
printf '%s\n' 'proto=rsn' printf '%s\n' 'proto=rsn'
printf '%s\n' 'pairwise=ccmp;' printf '%s\n' 'pairwise=ccmp;'
printf '%s\n' 'group=ccmp;' printf '%s\n' 'group=ccmp;'
printf 'psk=%s\n' "$PSK" printf 'psk=%s\n' "$psk"
printf '\n%s\n' '[ipv4]' printf '\n%s\n' '[ipv4]'
printf '%s\n' 'method=shared' printf '%s\n' 'method=shared'
printf '%s\n' 'address1=192.168.8.1/24' printf '%s\n' 'address1=192.168.8.1/24'
printf '\n%s\n' '[ipv6]' printf '\n%s\n' '[ipv6]'
printf '%s\n' 'method=disabled' printf '%s\n' 'method=disabled'
} >"$TMP" } >"$tmp"
sudo install -o root -g root -m 0600 "$TMP" "$CONN_FILE" sudo install -o root -g root -m 0600 "$tmp" "$conn_file"
rm -f "$TMP" rm -f "$tmp"
sudo nmcli connection load "$CONN_FILE" >/dev/null sudo nmcli connection load "$conn_file" >/dev/null
echo " Profil $AP_CONNECTION $ACTION" echo " Profil $id $action"
}
write_profile "$AP_CONNECTION" "$SSID" "$PSK" update
# Ohne dieses zweite Profil scheitert "ninlight-station migrate on" auf einem frisch
# eingerichteten Pi an need_profile "$MIGRATE_AP_CONNECTION".
write_profile "$MIGRATE_AP_CONNECTION" "$MIGRATE_SSID" "$MIGRATE_PSK" keep
REMOTE REMOTE
cat <<'EOF' cat <<'EOF'
@@ -164,6 +254,11 @@ cat <<'EOF'
Fertig. Die Station ist absichtlich noch AUS. Fertig. Die Station ist absichtlich noch AUS.
Naechste Schritte: Naechste Schritte:
ssh <pi-host> ninlight-station display test
Testbild aufs E-Paper - bleibt es leer oder
verzerrt, steht EPD_REVISION in
/etc/ninlight-station.conf auf der falschen
Panel-Revision (V2 statt V3 oder umgekehrt)
tools/station/deploy.sh <pi-host> Images ausliefern tools/station/deploy.sh <pi-host> Images ausliefern
ssh <pi-host> ninlight-station on Station einschalten ssh <pi-host> ninlight-station on Station einschalten
ssh <pi-host> ninlight-station off wieder ausschalten (nicht vergessen) ssh <pi-host> ninlight-station off wieder ausschalten (nicht vergessen)
+175 -2
View File
@@ -164,9 +164,31 @@ EOF
setup_sandbox() { setup_sandbox() {
local sandbox=$1 unitfile=${2:-$REAL_UNIT} local sandbox=$1 unitfile=${2:-$REAL_UNIT}
rm -rf "$sandbox" 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" 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 cat >"$sandbox/station.conf" <<EOF
SSID='test-recovery' SSID='test-recovery'
PSK='test-psk' PSK='test-psk'
@@ -192,7 +214,7 @@ run_cli() {
PATH="$sandbox/bin:$PATH" \ PATH="$sandbox/bin:$PATH" \
NINLIGHT_STATION_CONF="$sandbox/station.conf" \ NINLIGHT_STATION_CONF="$sandbox/station.conf" \
NINLIGHT_RUN_DIR="$sandbox/run" \ NINLIGHT_RUN_DIR="$sandbox/run" \
NINLIGHT_STATION_LIB="$STATION_DIR" \ NINLIGHT_STATION_LIB="$sandbox/lib" \
"$CLI" "$@" "$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 -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 "$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 echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden." 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/products.py"
echo "# dummy" >"$sandbox/tools/station/ninlight-station" echo "# dummy" >"$sandbox/tools/station/ninlight-station"
echo "# dummy" >"$sandbox/tools/station/ninlight-station.service" 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 # 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 # "<geraet> <version> <bytes>" auf stdout bei Erfolg, Exit 0; eine Zeile auf
@@ -111,9 +122,22 @@ echo "STUB sudo \$*" >>"$log"
exec "\$@" exec "\$@"
EOF 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 cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash #!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log" 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 case "\$1" in
is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-1}" ;; is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-1}" ;;
restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;; restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;;
@@ -146,6 +170,7 @@ run_deploy() {
NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \ NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \ NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \ 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" \ NINLIGHT_SIGN_DIR="$sign_dir" \
"$sandbox/tools/station/deploy.sh" "$@" "$sandbox/tools/station/deploy.sh" "$@"
} }
@@ -948,6 +973,89 @@ for label in truncated empty array null string null-entry; do
fi fi
done 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 echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden." echo "Alle Tests bestanden."
+184 -1
View File
@@ -55,12 +55,41 @@ echo "STUB scp \$*" >>"$log"
args=() args=()
for a in "\$@"; do for a in "\$@"; do
case "\$a" in 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 ;; -*) continue ;;
*:*) args+=("\${a#*:}") ;; *:*) args+=("\${a#*:}") ;;
*) args+=("\$a") ;; *) args+=("\$a") ;;
esac esac
done done
exec cp "\${args[@]}" 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 EOF
# "sudo install -o root -g root ..." (im NetworkManager-Keyfile-Schritt) braucht # "sudo install -o root -g root ..." (im NetworkManager-Keyfile-Schritt) braucht
@@ -104,7 +133,8 @@ fi
exit 0 exit 0
EOF 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" # $1 = Sandbox-Wurzel fuer die "Systempfade"
@@ -133,6 +163,7 @@ run_setup() {
NINLIGHT_STATION_ROOT="$state/opt/ninlight-station" \ NINLIGHT_STATION_ROOT="$state/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$state/usr/local/bin/ninlight-station" \ NINLIGHT_STATION_BIN="$state/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \ 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" \ NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \
"$STATION_DIR/setup_pi.sh" pi-test "$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" grep -Eq "^${KEY}=" "$CONF_OUT4" && pass "fehlender Schluessel $KEY wurde trotzdem ergaenzt" || fail "Schluessel $KEY fehlt weiterhin"
done 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 echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden." echo "Alle Tests bestanden."
+13 -1
View File
@@ -7,9 +7,21 @@ cmake --build "$ROOT/build/host"
ctest --test-dir "$ROOT/build/host" --output-on-failure "$@" ctest --test-dir "$ROOT/build/host" --output-on-failure "$@"
if python3 -c 'import pytest' 2>/dev/null; then if python3 -c 'import pytest' 2>/dev/null; then
python3 -m pytest "$ROOT/tools/mkimage/tests" "$ROOT/tools/station/tests" -q # tools/mkimage als GANZES Verzeichnis, nicht nur tools/mkimage/tests: die 19 Tests in
# tools/mkimage/test_nl_mkimage.py liegen direkt daneben und liefen sonst in keiner
# Suite mit - darunter test_stays_python38_compatible, die Wache fuer die bindende
# 3.8-Randbedingung des Docker-Build-Containers.
#
# Diese Wache braucht das Paket "vermin" und schlaegt ohne es HART fehl (kein Skip):
# eine Wache, die sich beim Fehlen ihres Werkzeugs stillschweigend abschaltet, haette
# den Build-Bruch von damals genauso durchgelassen wie gar kein Test. Fehlt vermin,
# nachinstallieren:
# pip install vermin (oder: pip3 install --user vermin)
python3 -m pytest "$ROOT/tools/mkimage" "$ROOT/tools/station/tests" -q
else else
echo "Hinweis: pytest fehlt - Python-Tests uebersprungen" >&2 echo "Hinweis: pytest fehlt - Python-Tests uebersprungen" >&2
echo " Installation: pip install pytest vermin (vermin braucht" >&2
echo " tools/mkimage/test_nl_mkimage.py::test_stays_python38_compatible)" >&2
fi fi
"$ROOT/tools/station/tests/test_cli.sh" "$ROOT/tools/station/tests/test_cli.sh"