nmcli -t maskiert Doppelpunkte in Werten als \: und cut -d: -f2 schneidet ohnehin am ersten Doppelpunkt ab. Ein Profilname mit Doppelpunkt lieferte der Nachpruefung in ap_switch() damit einen abgeschnittenen Wert - und der Fehler zeigte in die unsichere Richtung: "Station aus" gemeldet, waehrend der Zugangspunkt weitersendet. Jetzt --escape no plus cut -f2-, auch bei den beiden Profilabfragen und in der Statusausgabe. Der nmcli-Stub in test_cli.sh maskiert jetzt wie das echte nmcli, sonst waere der neue Zusatzfall M1 mit und ohne Fix gruen.
1494 lines
77 KiB
Bash
Executable File
1494 lines
77 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"
|
|
|
|
# "--escape no" abtrennen, BEVOR die Stellungen geprueft werden - sonst verschoebe der
|
|
# Schalter alle Positionen. Und der Stub maskiert wie das echte nmcli: OHNE den Schalter
|
|
# wird in den Werten von "-t" jeder Doppelpunkt zu "\\:" (Abschlussreview, M1). Nur so
|
|
# prueft der Test das Skript und nicht die Attrappe - ein Stub, der nie maskiert, waere
|
|
# mit und ohne den Fix gruen.
|
|
ESCAPE=yes
|
|
_args=()
|
|
while [ \$# -gt 0 ]; do
|
|
case "\$1" in
|
|
--escape) ESCAPE=\$2; shift 2 ;;
|
|
*) _args+=("\$1"); shift ;;
|
|
esac
|
|
done
|
|
set -- "\${_args[@]}"
|
|
|
|
emit() {
|
|
# \$1 = Feldname, \$2 = Rohwert
|
|
if [ "\$ESCAPE" = "no" ]; then
|
|
echo "\$1:\$2"
|
|
else
|
|
echo "\$1:\${2//:/\\\\:}"
|
|
fi
|
|
}
|
|
|
|
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "NAME" ] && [ "\$4" = "con" ] && [ "\$5" = "show" ]; then
|
|
while IFS= read -r _profile; do
|
|
if [ "\$ESCAPE" = "no" ]; then
|
|
echo "\$_profile"
|
|
else
|
|
echo "\${_profile//:/\\\\:}"
|
|
fi
|
|
done <"\$NMSTATE/profiles" 2>/dev/null
|
|
exit 0
|
|
fi
|
|
|
|
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "GENERAL.CONNECTION" ] && [ "\$4" = "device" ] && [ "\$5" = "show" ]; then
|
|
# Testschalter fuer Fall I3 (Fixrunde 1): erzwingt eine Antwort, die nicht zum
|
|
# zuletzt hochgefahrenen Profil passt, um ap_switch()'s Fehlerzweig zu pruefen.
|
|
if [ -n "\${FAKE_NMCLI_DEVSHOW:-}" ]; then
|
|
emit GENERAL.CONNECTION "\${FAKE_NMCLI_DEVSHOW}"
|
|
exit 0
|
|
fi
|
|
last=\$(tail -n1 "\$NMSTATE/active" 2>/dev/null)
|
|
emit GENERAL.CONNECTION "\${last:---}"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "\$1" = "con" ] && [ "\$2" = "up" ]; then
|
|
profile=\$3
|
|
grep -qxF "\$profile" "\$NMSTATE/profiles" 2>/dev/null || { echo "Error: unknown connection '\$profile'." >&2; exit 1; }
|
|
grep -qxF "\$profile" "\$NMSTATE/active" 2>/dev/null || echo "\$profile" >>"\$NMSTATE/active"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "\$1" = "con" ] && [ "\$2" = "down" ]; then
|
|
# Testschalter fuer Fall I2 (Fixrunde 1): simuliert einen haengenden
|
|
# NetworkManager - der Aufruf "gelingt" (Exit 0, wie ein echtes "nmcli con
|
|
# down" es bei einem haengenden Backend oft tut), aendert den Zustand aber
|
|
# nicht. ap_switch() darf das nicht als "aus" durchgehen lassen.
|
|
if [ -n "\${FAKE_NMCLI_STUCK:-}" ]; then
|
|
exit 0
|
|
fi
|
|
profile=\$3
|
|
grep -vxF "\$profile" "\$NMSTATE/active" 2>/dev/null >"\$NMSTATE/active.tmp" || true
|
|
mv "\$NMSTATE/active.tmp" "\$NMSTATE/active"
|
|
exit 0
|
|
fi
|
|
|
|
exit 0
|
|
EOF
|
|
|
|
cat >"$bindir/systemctl" <<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}"
|
|
# Testschalter fuer den Abschlussbildschirm: der Anzeigedienst gilt fuer die
|
|
# ersten N Abfragen als AKTIV und danach als gestoppt - so, wie es beim
|
|
# Herunterfahren wirklich ablaeuft (systemd stoppt ihn, das dauert einen Moment).
|
|
# Gezaehlt wird in einer Datei, weil jeder Stub-Aufruf ein eigener Prozess ist.
|
|
if [ -n "\${FAKE_DISPLAY_ACTIVE_FOR:-}" ]; then
|
|
n=\$(cat "$rundir/.display-active-count" 2>/dev/null || echo 0)
|
|
n=\$((n + 1))
|
|
echo "\$n" >"$rundir/.display-active-count"
|
|
if [ "\$n" -le "\${FAKE_DISPLAY_ACTIVE_FOR}" ]; then
|
|
echo active
|
|
exit 0
|
|
fi
|
|
echo inactive
|
|
exit 3
|
|
fi
|
|
else
|
|
code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}"
|
|
fi
|
|
if [ "\$code" -eq 0 ]; then
|
|
echo active
|
|
exit 0
|
|
else
|
|
echo inactive
|
|
exit "\$code"
|
|
fi
|
|
;;
|
|
start)
|
|
# Testschalter fuer die Resilienz-Faelle: bildet nach, dass "ninlight-display"
|
|
# gar nicht installiert oder absichtlich maskiert ist (systemctl gibt dann einen
|
|
# Fehler zurueck statt still zu tun, als waere alles gut).
|
|
if [ "\$2" = "ninlight-display" ] && [ -n "\${FAKE_DISPLAY_SERVICE_MISSING:-}" ]; then
|
|
echo "Failed to start ninlight-display.service: Unit not found." >&2
|
|
exit 5
|
|
fi
|
|
exit 0
|
|
;;
|
|
restart|stop)
|
|
# Bildet systemds RuntimeDirectory-Verhalten nach (Fixrunde 1, C1): ohne
|
|
# RuntimeDirectoryPreserve=restart in der Unit leert systemd das
|
|
# RuntimeDirectory bei JEDEM Stop/Restart, nicht nur beim allerersten Start,
|
|
# und legt es leer wieder an.
|
|
#
|
|
# Die Regel gilt nur fuer ninlight-station - \$UNIT_FILE ist deren Unit-Datei.
|
|
# ninlight-display traegt gar kein RuntimeDirectory=, ein "stop ninlight-display"
|
|
# raeumt in Wirklichkeit nichts weg (frueher tat der Stub das trotzdem fuer JEDE
|
|
# Unit; harmlos, solange kein Testfall die Anzeige stoppt und danach noch Dateien
|
|
# in RUN_DIR erwartet - genau das tun 'display test'/'display clear' jetzt aber,
|
|
# die den Anzeigedienst waehrend des Handgriffs stoppen).
|
|
if [ "\$2" = "ninlight-station" ] \
|
|
&& ! grep -q '^RuntimeDirectoryPreserve=restart' "\$UNIT_FILE" 2>/dev/null; then
|
|
rm -rf "\$RUN_DIR"
|
|
mkdir -p "\$RUN_DIR"
|
|
fi
|
|
if [ "\$2" = "ninlight-display" ] && [ -n "\${FAKE_DISPLAY_SERVICE_MISSING:-}" ]; then
|
|
echo "Failed to stop ninlight-display.service: Unit not found." >&2
|
|
exit 5
|
|
fi
|
|
if [ "\$1" = "restart" ]; then
|
|
exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}"
|
|
fi
|
|
exit 0
|
|
;;
|
|
*) exit 0 ;;
|
|
esac
|
|
EOF
|
|
|
|
cat >"$bindir/sudo" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB sudo \$*" >>"$log"
|
|
exec "\$@"
|
|
EOF
|
|
|
|
# Der Zeitschalter legt seinen Timer per "systemd-run --unit=..." an; der Stub
|
|
# protokolliert nur, was angefordert wurde (der ECHTE Beleg dafuer, dass ein
|
|
# Zeitschalter existiert bzw. nicht existiert, ist unten immer die Zustandsdatei
|
|
# $RUN_DIR/autooff.json und das Verhalten des Skripts, nicht diese Zeile).
|
|
cat >"$bindir/systemd-run" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB systemd-run \$*" >>"$log"
|
|
exit "\${FAKE_SYSTEMD_RUN_EXIT:-0}"
|
|
EOF
|
|
|
|
# "ss" beantwortet die Frage "laeuft gerade eine Auslieferung?" - mit
|
|
# FAKE_SS_BUSY=1 gibt es eine offene Verbindung aus dem Stationsnetz auf Port 80.
|
|
cat >"$bindir/ss" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB ss \$*" >>"$log"
|
|
if [ -n "\${FAKE_SS_BUSY:-}" ]; then
|
|
echo "ESTAB 0 0 192.168.8.1:80 192.168.8.42:52344"
|
|
fi
|
|
exit 0
|
|
EOF
|
|
|
|
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo \
|
|
"$bindir"/systemd-run "$bindir"/ss
|
|
}
|
|
|
|
# --- Sandbox fuer einen Testfall: eigene conf/RUN_DIR/IMAGE_DIR/nmcli-Zustand ---
|
|
# $1 = sandbox-Verzeichnis, $2 = Unit-Datei fuer den systemctl-Stub (Default: die
|
|
# echte, gerade zu testende tools/station/ninlight-station.service).
|
|
setup_sandbox() {
|
|
local sandbox=$1 unitfile=${2:-$REAL_UNIT}
|
|
rm -rf "$sandbox"
|
|
mkdir -p "$sandbox/run" "$sandbox/images/migrate-nocreds" "$sandbox/images/migrate-wlan" \
|
|
"$sandbox/bin" "$sandbox/nmstate" "$sandbox/lib/display" "$sandbox/var"
|
|
# Je ein Image je Bildsatz - "vorhanden" heisst fuer CLI, Server und Anzeige
|
|
# gleichermassen "mindestens ein Image darin".
|
|
echo "ohne" >"$sandbox/images/migrate-nocreds/foo_dev.img"
|
|
echo "mit" >"$sandbox/images/migrate-wlan/foo_dev.img"
|
|
echo "dummy" >"$sandbox/images/ninlight_foo_ota.img"
|
|
|
|
# NINLIGHT_STATION_LIB zeigt auf diese Sandbox statt auf tools/station selbst.
|
|
# products.py wird gebraucht (arm prueft das Ziel dagegen) und deshalb echt kopiert;
|
|
# display/display.py ist ein Rekorder, der seine Argumente in DIESELBE cmd.log
|
|
# schreibt wie die nmcli-/systemctl-Stubs. Nur so laesst sich die REIHENFOLGE
|
|
# "erst Dienst stoppen, dann Panel leeren" ueberhaupt pruefen - mit zwei getrennten
|
|
# Logs waere sie nicht rekonstruierbar.
|
|
#
|
|
# Der Rekorder ersetzt das echte display.py absichtlich: geprueft wird hier, WAS
|
|
# ninlight-station aufruft (Pfad, Schalter, Reihenfolge, Umgang mit dem Rueckgabe-
|
|
# wert), nicht was display.py daraus macht - das deckt tests/test_render.py ab.
|
|
cp "$STATION_DIR/products.py" "$sandbox/lib/products.py"
|
|
cat >"$sandbox/lib/display/display.py" <<EOF
|
|
import os
|
|
import signal
|
|
import sys
|
|
|
|
with open("$sandbox/cmd.log", "a") as fh:
|
|
fh.write("STUB display.py %s (EPD_REVISION=%s)\n"
|
|
% (" ".join(sys.argv[1:]), os.environ.get("EPD_REVISION", "<unset>")))
|
|
|
|
# Testschalter fuer Fall D2c: stellt das Strg-C des Abnehmers nach, waehrend display.py
|
|
# laeuft (bei falscher Panel-Revision kehrt ReadBusy() nie zurueck - das ist der
|
|
# dokumentierte Weg, die Revision zu bestimmen, und damit der Regelfall fuer ein Strg-C).
|
|
#
|
|
# Nachgestellt wird, was das Terminal wirklich tut: das Signal geht an die GANZE
|
|
# Vordergrund-Prozessgruppe, also an die ninlight-station-Shell UND an diesen Prozess.
|
|
# Der Elternprozess ist dabei die Shell selbst ("sudo" und "env" sind Stub bzw.
|
|
# exec-Aufruf, der Python-Prozess haengt also direkt unter ihr).
|
|
#
|
|
# Dass dieser Prozess AM SIGNAL STIRBT (Standardbehandlung) statt sich mit 128+n zu
|
|
# beenden, ist kein Detail: eine Shell unterscheidet beides. Beendet sich das Kind
|
|
# normal, verwirft /bin/sh (bash im sh-Modus, macOS) das inzwischen zugestellte SIGINT
|
|
# und laeuft weiter - der Testfall waere dann auch ohne Signalhandler in
|
|
# display_handover() gruen und wuerde gar nichts pruefen (beim Schreiben dieses Tests
|
|
# genau so passiert).
|
|
signal_name = os.environ.get("FAKE_DISPLAY_SIGNAL", "")
|
|
if signal_name:
|
|
number = getattr(signal, "SIG" + signal_name)
|
|
os.kill(os.getppid(), number)
|
|
signal.signal(number, signal.SIG_DFL)
|
|
os.kill(os.getpid(), number)
|
|
|
|
sys.exit(int(os.environ.get("FAKE_DISPLAY_EXIT", "0")))
|
|
EOF
|
|
|
|
cat >"$sandbox/station.conf" <<EOF
|
|
SSID='test-recovery'
|
|
PSK='test-psk'
|
|
IMAGE_DIR='$sandbox/images'
|
|
HOME_CONNECTION='home-wifi'
|
|
AP_CONNECTION='ninlight-recovery'
|
|
MIGRATE_SSID='qconnex-firmware'
|
|
MIGRATE_PSK='qconnex-firmware'
|
|
MIGRATE_AP_CONNECTION='qconnex-firmware'
|
|
EPD_REVISION='V3'
|
|
EOF
|
|
|
|
printf 'ninlight-recovery\nqconnex-firmware\nhome-wifi\n' >"$sandbox/nmstate/profiles"
|
|
: >"$sandbox/nmstate/active"
|
|
|
|
setup_stubbin "$sandbox/bin" "$sandbox/cmd.log" "$sandbox/nmstate" "$sandbox/run" "$unitfile"
|
|
}
|
|
|
|
# Fuehrt ninlight-station mit gestubbtem PATH und ueberschriebenen Pfaden aus.
|
|
# $1 = sandbox-Verzeichnis, Rest = Argumente fuer ninlight-station.
|
|
run_cli() {
|
|
local sandbox=$1; shift
|
|
PATH="$sandbox/bin:$PATH" \
|
|
NINLIGHT_STATION_CONF="$sandbox/station.conf" \
|
|
NINLIGHT_RUN_DIR="$sandbox/run" \
|
|
NINLIGHT_STATION_LIB="$sandbox/lib" \
|
|
NINLIGHT_IMAGESET_FILE="$sandbox/var/imageset" \
|
|
"$CLI" "$@"
|
|
}
|
|
|
|
control_phase() {
|
|
# $1 = Pfad zu control.json
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('phase','?'))" "$1" 2>/dev/null || echo "FEHLT"
|
|
}
|
|
control_target() {
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('armed_target'))" "$1" 2>/dev/null || echo "FEHLT"
|
|
}
|
|
|
|
echo "=== Fall 1: 'on' faehrt ninlight-recovery hoch, startet den Dienst neu, meldet 'Station an' ==="
|
|
SBX="$TMPROOT/t1"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
|
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch: $(cat "$SBX/run/mode" 2>&1)"
|
|
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 2: 'recovery on' identisch zu 'on' (Rueckwaertskompatibilitaet) ==="
|
|
SBX="$TMPROOT/t2"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" recovery on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
|
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch"
|
|
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 3: 'migrate on' faehrt ninlight-recovery herunter, BEVOR qconnex-firmware hochkommt; control.json phase=auto ==="
|
|
SBX="$TMPROOT/t3"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1 # Recovery vorher an
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
DOWN_LINE=$(grep -n "nmcli con down ninlight-recovery" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
UP_LINE=$(grep -n "nmcli con up qconnex-firmware" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$DOWN_LINE" ] && [ -n "$UP_LINE" ] && [ "$DOWN_LINE" -lt "$UP_LINE" ]; then
|
|
pass "ninlight-recovery wird VOR qconnex-firmware heruntergefahren"
|
|
else
|
|
fail "Reihenfolge falsch oder Aufrufe fehlen (down=$DOWN_LINE, up=$UP_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
[ "$(cat "$SBX/run/mode")" = "migration" ] && pass "Modusdatei enthaelt 'migration'" || fail "Modusdatei falsch"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "auto" ] && pass "control.json: phase=auto" || fail "control.json phase ist nicht 'auto': $(cat "$SBX/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall 4: 'migrate on --listen' wie Fall 3, aber phase=listen ==="
|
|
SBX="$TMPROOT/t4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "control.json: phase=listen" || fail "control.json phase ist nicht 'listen': $(cat "$SBX/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall 5: 'migrate on' ohne Migrationsimages -> Warnung auf stderr, Rueckgabewert 0 ==="
|
|
SBX="$TMPROOT/t5"
|
|
setup_sandbox "$SBX"
|
|
# der gewaehlte Bildsatz ("ohne-wlan") wird geleert - setup_sandbox bestueckt beide
|
|
rm -f "$SBX/images/migrate-nocreds"/*.img
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlender Migrationsimages" || fail "Exit-Code $RC, stderr: $ERR"
|
|
echo "$ERR" | grep -q "Warnung" && pass "Warnung steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
|
|
|
echo
|
|
echo "=== Fall 6: 'arm' ohne Argument liest last_seen.target aus status.json ==="
|
|
SBX="$TMPROOT/t6"
|
|
setup_sandbox "$SBX"
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "migration", "phase": "listen", "last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "control.json: phase=armed" || fail "phase ist nicht 'armed'"
|
|
[ "$(control_target "$SBX/run/control.json")" = "gu10_rgb" ] && pass "armed_target=gu10_rgb (aus last_seen)" || fail "armed_target falsch: $(cat "$SBX/run/control.json")"
|
|
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 7: 'arm gu10_rgb' mit explizitem Ziel ==="
|
|
SBX="$TMPROOT/t7"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm gu10_rgb 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "control.json: phase=armed" || fail "phase ist nicht 'armed'"
|
|
[ "$(control_target "$SBX/run/control.json")" = "gu10_rgb" ] && pass "armed_target=gu10_rgb" || fail "armed_target falsch: $(cat "$SBX/run/control.json")"
|
|
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 8: 'arm' ohne last_seen -> Fehler auf stderr, Rueckgabewert != 0, control.json unveraendert ==="
|
|
SBX="$TMPROOT/t8"
|
|
setup_sandbox "$SBX"
|
|
# keine status.json vorhanden
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
|
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt: $(cat "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 9: 'arm unbekannt' -> Fehler auf stderr, Rueckgabewert != 0 ==="
|
|
SBX="$TMPROOT/t9"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm unbekannt 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
|
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt"
|
|
|
|
echo
|
|
echo "=== Fall 10: 'listen' setzt phase=listen ==="
|
|
SBX="$TMPROOT/t10"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" listen 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "control.json: phase=listen" || fail "phase ist nicht 'listen': $(cat "$SBX/run/control.json" 2>&1)"
|
|
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 11: 'off' faehrt beide Profile herunter, stoppt den Dienst, zurueck ins Heim-WLAN ==="
|
|
SBX="$TMPROOT/t11"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "systemctl stop ninlight-station" "$SBX/cmd.log" && pass "Dienst gestoppt" || fail "kein systemctl stop im Log"
|
|
ACTIVE=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE" | grep -qx "ninlight-recovery" && fail "ninlight-recovery ist noch aktiv" || pass "ninlight-recovery ist nicht mehr aktiv"
|
|
echo "$ACTIVE" | grep -qx "qconnex-firmware" && fail "qconnex-firmware ist noch aktiv" || pass "qconnex-firmware ist nicht mehr aktiv"
|
|
echo "$ACTIVE" | grep -qx "home-wifi" && pass "zurueck im Heim-WLAN (home-wifi aktiv)" || fail "home-wifi ist nicht aktiv: $ACTIVE"
|
|
echo "$OUT" | grep -q "Heim-WLAN" && pass "Ausgabe erwaehnt Heim-WLAN" || fail "Ausgabe erwaehnt Heim-WLAN nicht: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 12: 'status' im Migrationsmodus zeigt Modus, Phase und zuletzt erkannte Lampe ==="
|
|
SBX="$TMPROOT/t12"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1 # phase=auto in control.json, wie es der echte Ablauf waere
|
|
# status.json so, wie server.py sie beim Start geschrieben haette (mode/last_seen).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "migration",
|
|
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
set +e
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
echo "$OUT" | grep -q "Modus.*migration" && pass "Modus wird angezeigt" || fail "Modus fehlt: $OUT"
|
|
echo "$OUT" | grep -q "Phase.*auto" && pass "Phase wird angezeigt" || fail "Phase fehlt: $OUT"
|
|
echo "$OUT" | grep -q "Erkannt.*Klyqa GU10 RGB CW/WW" && pass "zuletzt erkannte Lampe wird angezeigt" || fail "erkannte Lampe fehlt: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 13: 'status' mit unlesbarer status.json meldet das ausdruecklich (4a-Fehler) ==="
|
|
SBX="$TMPROOT/t13"
|
|
setup_sandbox "$SBX"
|
|
echo '{"mode": "recovery"}' >"$SBX/run/status.json"
|
|
chmod 000 "$SBX/run/status.json"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
RC=$?
|
|
set -e
|
|
chmod 644 "$SBX/run/status.json" # damit trap-Aufraeumen am Ende sicher klappt
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (status liefert trotzdem eine Auskunft)" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
echo "$OUT" | grep -q "existiert, ist aber nicht lesbar" && pass "unlesbare status.json wird ausdruecklich gemeldet" || fail "Meldung zu unlesbarer status.json fehlt: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 14: unbekannter Unterbefehl -> Aufrufhinweis, Rueckgabewert 2 ==="
|
|
SBX="$TMPROOT/t14"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" frobnicate 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 2 ] && pass "Exit-Code 2" || fail "Exit-Code war $RC statt 2"
|
|
echo "$OUT" | grep -qi "Aufruf" && pass "Aufrufhinweis vorhanden" || fail "kein Aufrufhinweis: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 15 (wichtigster Test): 'migrate on' bei laufendem Recovery-Modus laesst GENAU EIN Profil aktiv ==="
|
|
SBX="$TMPROOT/t15"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
ACTIVE_BEFORE=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE_BEFORE" | grep -qx "ninlight-recovery" && pass "Vorbedingung: ninlight-recovery laeuft" || fail "Vorbedingung nicht erfuellt: $ACTIVE_BEFORE"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
COUNT=$(echo "$ACTIVE_AFTER" | grep -c . || true)
|
|
if [ "$COUNT" -eq 1 ] && [ "$ACTIVE_AFTER" = "qconnex-firmware" ]; then
|
|
pass "genau ein Profil aktiv: qconnex-firmware"
|
|
else
|
|
fail "erwartet genau 'qconnex-firmware', tatsaechlich aktiv: [$ACTIVE_AFTER]"
|
|
fi
|
|
DEVSHOW=$(PATH="$SBX/bin:$PATH" nmcli -t -f GENERAL.CONNECTION device show wlan0)
|
|
[ "$DEVSHOW" = "GENERAL.CONNECTION:qconnex-firmware" ] && pass "nmcli device show wlan0 bestaetigt qconnex-firmware" || fail "device show liefert: $DEVSHOW"
|
|
|
|
echo
|
|
echo "=== Zusatzfall C1: ohne RuntimeDirectoryPreserve=restart wuerde der Neustart mode/control.json wegwischen - das CLI muss das erkennen und abbrechen ==="
|
|
SBX="$TMPROOT/tC1"
|
|
BROKEN_UNIT="$TMPROOT/broken-ninlight-station.service"
|
|
grep -v '^RuntimeDirectoryPreserve=restart' "$REAL_UNIT" >"$BROKEN_UNIT"
|
|
setup_sandbox "$SBX" "$BROKEN_UNIT"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - eine geleerte RuntimeDirectory wird nicht als Erfolg gemeldet" || fail "Exit-Code war 0, obwohl RUN_DIR beim Neustart geleert wurde: $OUT"
|
|
echo "$ERR" | grep -qi "RuntimeDirectoryPreserve" && pass "Fehlermeldung nennt RuntimeDirectoryPreserve als Ursache" || fail "Fehlermeldung nennt die Ursache nicht: $ERR"
|
|
echo "$OUT" | grep -q "Migration an" && fail "CLI meldete faelschlich 'Migration an', obwohl die Phase nach dem Neustart nicht mehr stimmte" || pass "kein falscher Erfolgsmeldung 'Migration an'"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ -z "$ACTIVE_AFTER" ] && pass "Zugangspunkt wurde nach dem erkannten Fehler wieder heruntergefahren" || fail "Zugangspunkt blieb aktiv: $ACTIVE_AFTER"
|
|
# Gegenprobe: dieselbe Sandbox, aber mit der ECHTEN (reparierten) Unit-Datei -
|
|
# jetzt darf RUN_DIR beim Neustart nicht geleert werden und der Aufruf muss
|
|
# durchlaufen. Das ist die eigentliche Bestaetigung, dass RuntimeDirectoryPreserve
|
|
# in tools/station/ninlight-station.service tatsaechlich steht und wirkt.
|
|
SBX2="$TMPROOT/tC1-fixed"
|
|
setup_sandbox "$SBX2"
|
|
echo "dummy" >"$SBX2/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT2=$(run_cli "$SBX2" migrate on --listen 2>&1)
|
|
RC2=$?
|
|
set -e
|
|
[ "$RC2" -eq 0 ] && pass "mit der echten (reparierten) Unit-Datei gelingt derselbe Aufruf" || fail "Gegenprobe schlug fehl - Exit-Code $RC2: $OUT2"
|
|
[ "$(control_phase "$SBX2/run/control.json")" = "listen" ] && pass "Gegenprobe: control.json ueberlebt den Neustart mit phase=listen" || fail "Gegenprobe: Phase nach Neustart falsch: $(cat "$SBX2/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Zusatzfall I1: 'status' zeigt die Phase aus control.json, nicht die veraltete Phase aus status.json ==="
|
|
SBX="$TMPROOT/tI1"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
# status.json wird nur beim Dienststart/bei einer Auslieferung geschrieben - hier
|
|
# simuliert, wie es nach dem Start aussehen wuerde (Phase "listen", wie beim Start).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "migration", "phase": "listen",
|
|
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # startet den Dienst NICHT neu - status.json bleibt bei "listen"
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -q "Phase.*armed" && pass "status zeigt 'armed' (aus control.json), nicht das veraltete 'listen' aus status.json" || fail "status zeigt die falsche/veraltete Phase: $OUT"
|
|
echo "$OUT" | grep -q "gu10_rgb" && pass "status nennt das freigegebene Ziel" || fail "status nennt das armed_target nicht: $OUT"
|
|
|
|
echo
|
|
echo "=== Zusatzfall I2: 'off' bricht mit Fehler ab, wenn ein Stationsprofil trotz 'con down' aktiv bleibt (haengender NetworkManager) ==="
|
|
SBX="$TMPROOT/tI2"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
# HOME_CONNECTION aus den bekannten Profilen entfernen, um zugleich den in I2
|
|
# genannten zweiten Umstand abzudecken (Zielprofil fuers Heim-WLAN fehlt).
|
|
grep -v '^home-wifi$' "$SBX/nmstate/profiles" >"$SBX/nmstate/profiles.tmp"
|
|
mv "$SBX/nmstate/profiles.tmp" "$SBX/nmstate/profiles"
|
|
set +e
|
|
OUT=$(FAKE_NMCLI_STUCK=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - ein haengender Zugangspunkt wird nicht als 'Station aus' gemeldet" || fail "Exit-Code war 0, obwohl ninlight-recovery weiterhin aktiv war: $OUT"
|
|
echo "$OUT" | grep -q "Station aus" && fail "CLI behauptete faelschlich 'Station aus'" || pass "keine falsche 'Station aus'-Meldung"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr vorhanden" || fail "keine Fehlermeldung auf stderr: $ERR"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "ninlight-recovery ist (wie vom Stub simuliert) tatsaechlich noch aktiv" || fail "unerwarteter nmcli-Zustand: $ACTIVE_AFTER"
|
|
|
|
echo
|
|
echo "=== Zusatzfall M1 (Abschlussreview): ein Profilname MIT Doppelpunkt wird beim Nachpruefen richtig gelesen ==="
|
|
# "nmcli -t" maskiert Doppelpunkte in Werten als "\:", und "cut -d: -f2" schneidet am
|
|
# ersten Doppelpunkt ab. Ohne "--escape no" UND "-f2-" liest ap_switch() aus einem
|
|
# Profil "ninlight:recovery" nur "ninlight\" bzw. "ninlight" - der Vergleich schluege
|
|
# fehl, und zwar in die UNSICHERE Richtung: "Station aus" gemeldet, waehrend der
|
|
# Zugangspunkt weitersendet. Aufbau wie Zusatzfall I2, nur mit Doppelpunkt im Namen.
|
|
SBX="$TMPROOT/tM1"
|
|
setup_sandbox "$SBX"
|
|
sed -i.bak "s/^AP_CONNECTION=.*/AP_CONNECTION='ninlight:recovery'/" "$SBX/station.conf"
|
|
rm -f "$SBX/station.conf.bak"
|
|
printf 'ninlight:recovery\nqconnex-firmware\nhome-wifi\n' >"$SBX/nmstate/profiles"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" on 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "'on' kommt mit einem Doppelpunkt im Profilnamen zurecht" \
|
|
|| fail "'on' scheiterte an einem Doppelpunkt im Profilnamen (rc=$RC): $OUT $(cat "$SBX/stderr.log")"
|
|
[ "$(cat "$SBX/nmstate/active")" = "ninlight:recovery" ] \
|
|
&& pass "das Profil mit Doppelpunkt ist aktiv" || fail "unerwarteter nmcli-Zustand: $(cat "$SBX/nmstate/active")"
|
|
set +e
|
|
OUT=$(FAKE_NMCLI_STUCK=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] \
|
|
&& pass "Exit-Code != 0 - der haengende Zugangspunkt wird trotz Doppelpunkt im Namen erkannt" \
|
|
|| fail "Exit-Code war 0, obwohl 'ninlight:recovery' weiterhin aktiv war: $OUT"
|
|
echo "$OUT" | grep -q "Station aus" && fail "CLI behauptete faelschlich 'Station aus'" \
|
|
|| pass "keine falsche 'Station aus'-Meldung"
|
|
echo "$ERR" | grep -q "ninlight:recovery" \
|
|
&& pass "die Fehlermeldung nennt den vollstaendigen Profilnamen, nicht den abgeschnittenen" \
|
|
|| fail "Fehlermeldung nennt den Profilnamen nicht vollstaendig: $ERR"
|
|
echo
|
|
echo "=== Zusatzfall I3: ap_switch bricht ab und faehrt beide Profile herunter, wenn 'device show wlan0' nach dem Umschalten nicht passt ==="
|
|
SBX="$TMPROOT/tI3"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT=$(FAKE_NMCLI_DEVSHOW=fremdes-profil run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0, wenn wlan0 nach dem Umschalten ein fremdes Profil zeigt" || fail "Exit-Code war 0 trotz Profil-Mismatch: $OUT"
|
|
echo "$ERR" | grep -q "unerwartet" && pass "Fehlermeldung nennt den Mismatch" || fail "Fehlermeldung fehlt: $ERR"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ -z "$ACTIVE_AFTER" ] && pass "beide Stationsprofile wurden nach dem erkannten Mismatch heruntergefahren" || fail "mindestens ein Profil blieb aktiv: $ACTIVE_AFTER"
|
|
|
|
echo
|
|
echo "=== Zusatzfall N2 (Fixrunde 2): 'status' zeigt keine veraltete Migrationsphase mehr, nachdem in den Rettungsmodus zurueckgeschaltet wurde ==="
|
|
SBX="$TMPROOT/tN2"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # control.json bleibt mit phase=armed liegen
|
|
run_cli "$SBX" on >/dev/null 2>&1 # zurueck in den Rettungsmodus - control.json wird bewusst NICHT aufgeraeumt
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "Vorbedingung: control.json ist tatsaechlich noch mit phase=armed liegen geblieben" || fail "Vorbedingung nicht erfuellt (control.json wurde schon vorher veraendert)"
|
|
# status.json so, wie sie der Server nach dem Neustart im Rettungsmodus schreiben
|
|
# wuerde (mode=recovery, keine last_seen/phase-Felder - siehe server.py write_state()).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "recovery"}
|
|
JSON
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Modus[[:space:]]*: recovery" && pass "Modus zeigt korrekt 'recovery'" || fail "Modus falsch: $OUT"
|
|
echo "$OUT" | grep -qi "armed" && fail "status zeigt weiterhin die veraltete Migrationsphase 'armed' im Rettungsmodus: $OUT" || pass "keine veraltete Migrationsphase 'armed' im Rettungsmodus"
|
|
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt den neutralen Platzhalter '-' ausserhalb des Migrationsmodus" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
|
|
|
echo
|
|
echo "=== Zusatzfall N2b (Fixrunde 2): dieselbe veraltete Freigabe, aber ohne aktualisierte status.json (Modus '-') - genau das woertliche Repro aus dem Review ==="
|
|
SBX="$TMPROOT/tN2b"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
rm -f "$SBX/run/status.json" # kein Serverprozess in diesem Test aktualisiert sie
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Modus[[:space:]]*: -" && pass "Modus zeigt '-' (keine status.json vorhanden)" || fail "Modus unerwartet: $OUT"
|
|
echo "$OUT" | grep -qi "armed" && fail "status zeigt 'armed', obwohl der Modus gar nicht bekannt ist: $OUT" || pass "keine 'armed'-Phase bei unbekanntem Modus"
|
|
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt '-' statt einer Vermutung" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall D1 (Task 6b, Punkt 3): 'display test' ruft display.py mit --test und der konfigurierten Revision auf ==="
|
|
SBX="$TMPROOT/tD1"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "display.py wurde mit --test aufgerufen" || fail "kein '--test'-Aufruf im Log: $(cat "$SBX/cmd.log")"
|
|
# Der Testbild-Aufruf ist die einzige Moeglichkeit, die Panel-Revision zu bestimmen -
|
|
# er ist wertlos, wenn er nicht die Revision aus der Konfiguration durchreicht:
|
|
# EPD_REVISION wird per ". $CONF" nur als Shell-Variable gesetzt, nicht exportiert.
|
|
grep -Fq "STUB display.py --test (EPD_REVISION=V3)" "$SBX/cmd.log" && pass "EPD_REVISION aus der Konfiguration erreicht den Python-Prozess" || fail "EPD_REVISION kam nicht durch: $(cat "$SBX/cmd.log")"
|
|
# Der Anzeigedienst haelt die GPIO-Leitungen; ohne ihn kurz zu stoppen scheitert der
|
|
# Handgriff am Geraet mit "lgpio.error: 'GPIO busy'" (Hardware-Abnahme). Frueher stand
|
|
# hier die umgekehrte Zusage ("laesst den Anzeigedienst in Ruhe") - die war genau der
|
|
# Grund, warum der erste Handgriff der Abnahme nicht funktionierte.
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
TEST_LINE=$(grep -n "STUB display.py --test" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
START_LINE=$(grep -n "STUB systemctl start ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$TEST_LINE" ] && [ "$STOP_LINE" -lt "$TEST_LINE" ]; then
|
|
pass "der Anzeigedienst wird gestoppt, BEVOR display.py die GPIO-Leitungen anfasst"
|
|
else
|
|
fail "Anzeigedienst nicht (rechtzeitig) gestoppt (stop=$STOP_LINE, test=$TEST_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
# Und danach wieder an: ein Abnahme-Handgriff darf die Station nicht dauerhaft ohne
|
|
# Anzeige zuruecklassen.
|
|
if [ -n "$START_LINE" ] && [ -n "$TEST_LINE" ] && [ "$TEST_LINE" -lt "$START_LINE" ]; then
|
|
pass "der Anzeigedienst laeuft danach wieder (start=$START_LINE)"
|
|
else
|
|
fail "Anzeigedienst wurde nicht wieder gestartet (test=$TEST_LINE, start=$START_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D2: 'display clear' ruft display.py mit --clear auf ==="
|
|
SBX="$TMPROOT/tD2"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display clear 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "display.py wurde mit --clear aufgerufen" || fail "kein '--clear'-Aufruf im Log: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && fail "'display clear' hat --test aufgerufen" || pass "kein --test bei 'display clear'"
|
|
# Dieselbe GPIO-Kollision wie bei 'display test' - auch 'display clear' braucht die
|
|
# Leitungen fuer sich.
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'display clear' stoppt den Anzeigedienst fuer die Dauer" || fail "'display clear' laesst den Anzeigedienst laufen - das gibt 'GPIO busy': $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "'display clear' startet den Anzeigedienst danach wieder" || fail "'display clear' laesst den Anzeigedienst aus: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D2a: war der Anzeigedienst aus, bleibt er nach 'display test' aus ==="
|
|
# Sonst haette ein Abnahme-Handgriff die Station nebenbei umgeschaltet: nach
|
|
# 'ninlight-station off' ist der Anzeigedienst bewusst gestoppt, und 'display test'
|
|
# darf ihn nicht wieder hochziehen.
|
|
SBX="$TMPROOT/tD2a"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "Testbild wurde trotzdem gezeichnet" || fail "kein '--test'-Aufruf: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && fail "'display test' hat den Anzeigedienst eingeschaltet, obwohl er aus war" || pass "kein 'start' fuer einen Dienst, der gar nicht lief"
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'display test' stoppt einen Dienst, der gar nicht laeuft" || pass "kein ueberfluessiges 'stop'"
|
|
|
|
echo
|
|
echo "=== Fall D2b: scheitert das Panel, meldet 'display test' das - der Dienst laeuft trotzdem wieder ==="
|
|
# Der Rueckgabewert von display.py ist das Ergebnis des Handgriffs; ein erfolgreiches
|
|
# Wiederanfahren des Dienstes darf ihn nicht in eine Erfolgsmeldung verwandeln. Und
|
|
# umgekehrt darf ein gescheitertes Testbild den Dienst nicht dauerhaft aus lassen.
|
|
SBX="$TMPROOT/tD2b"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 1 ] && pass "Exit-Code 1 wird durchgereicht" || fail "Exit-Code $RC statt 1: $OUT"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "Anzeigedienst laeuft trotz Panel-Fehler wieder" || fail "Anzeigedienst blieb nach dem Fehler aus: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D2c: Strg-C waehrend 'display test' laesst den Anzeigedienst NICHT gestoppt zurueck ==="
|
|
# Der Ablauf aus der Abnahme: bei falscher Revision kehrt ReadBusy() nie zurueck
|
|
# (display.py-Docstring), der Abnehmer bricht mit Strg-C ab. Ohne Signalhandler zwischen
|
|
# "stop" und "start" stirbt display_handover mit - der Anzeigedienst bleibt aus, das
|
|
# Panel schwarz, und das sieht mitten in der Lampenabnahme aus wie "Panel kaputt".
|
|
# Geprueft wird fuer alle drei Signale, die das ausloesen koennen.
|
|
for SIG_NAME in INT TERM HUP; do
|
|
case "$SIG_NAME" in
|
|
INT) EXPECT_RC=130 ;;
|
|
TERM) EXPECT_RC=143 ;;
|
|
HUP) EXPECT_RC=129 ;;
|
|
esac
|
|
SBX="$TMPROOT/tD2c$SIG_NAME"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SIGNAL="$SIG_NAME" run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" \
|
|
&& pass "SIG$SIG_NAME: der Anzeigedienst war fuer den Handgriff gestoppt (Vorbedingung)" \
|
|
|| fail "SIG$SIG_NAME: Vorbedingung fehlt, der Dienst wurde gar nicht erst gestoppt: $(cat "$SBX/cmd.log")"
|
|
# GENAU einmal gestartet. Die Anzahl, nicht nur das Vorhandensein: der Handler muss
|
|
# selbst beenden (in diesem Projekt schon einmal ein Befund, setup_pi.sh). Ohne "exit"
|
|
# laeuft die Shell hinter dem unterbrochenen Kommando einfach weiter und faehrt den
|
|
# Dienst ein zweites Mal an - der Exit-Code sieht dabei zufaellig richtig aus (er ist
|
|
# dann der des am Signal gestorbenen Kindes), die doppelte Startzeile nicht. Genau
|
|
# diese Mutation ("trap ohne exit") war gegen eine blosse Vorhandensein-Pruefung
|
|
# gruen.
|
|
START_COUNT=$(grep -c "STUB systemctl start ninlight-display" "$SBX/cmd.log" || true)
|
|
[ "$START_COUNT" -eq 1 ] \
|
|
&& pass "SIG$SIG_NAME: der Anzeigedienst wird trotz Abbruch genau einmal wieder gestartet" \
|
|
|| fail "SIG$SIG_NAME: $START_COUNT Startversuche (1 erwartet) - Anzeigedienst bleibt aus oder der Handler beendet nicht selbst: $(cat "$SBX/cmd.log")"
|
|
[ "$RC" -eq "$EXPECT_RC" ] \
|
|
&& pass "SIG$SIG_NAME: Exit-Code $EXPECT_RC (128 + Signalnummer)" \
|
|
|| fail "SIG$SIG_NAME: Exit-Code $RC statt $EXPECT_RC: $OUT"
|
|
done
|
|
|
|
echo
|
|
echo "=== Fall D2d: war der Anzeigedienst aus, startet ihn auch ein Strg-C nicht ==="
|
|
# Spiegelbild zu D2a: der Signalhandler darf den Zustand nicht anders behandeln als der
|
|
# normale Ausgang - sonst schaltete ein abgebrochener Handgriff die Anzeige ein, die
|
|
# 'off' zuvor bewusst ausgeschaltet hat.
|
|
SBX="$TMPROOT/tD2d"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 FAKE_DISPLAY_SIGNAL=INT run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 130 ] && pass "Exit-Code 130 auch ohne laufenden Anzeigedienst" || fail "Exit-Code $RC statt 130: $OUT"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" \
|
|
&& fail "der Signalhandler hat den Anzeigedienst eingeschaltet, obwohl er aus war: $(cat "$SBX/cmd.log")" \
|
|
|| pass "kein 'start' fuer einen Dienst, der gar nicht lief"
|
|
|
|
echo
|
|
echo "=== Fall D3 (Task 6b, Punkt 4): 'display off' stoppt den Dienst VOR dem Leeren des Panels ==="
|
|
# Umgekehrte Reihenfolge waere wirkungslos: der Daemon fragt alle 5 s nach und
|
|
# schriebe sein altes Bild binnen dieser Zeit wieder aufs Panel.
|
|
SBX="$TMPROOT/tD3"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$CLEAR_LINE" ] && [ "$STOP_LINE" -lt "$CLEAR_LINE" ]; then
|
|
pass "Anzeigedienst wird gestoppt, BEVOR das Panel geleert wird (stop=$STOP_LINE, clear=$CLEAR_LINE)"
|
|
else
|
|
fail "Reihenfolge falsch oder Aufruf fehlt (stop=$STOP_LINE, clear=$CLEAR_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D4 (Befund Task 7f): 'off' fasst den Anzeigedienst nicht mehr an ==="
|
|
# Frueher stoppte 'off' den Anzeigedienst und leerte das Panel - mit der Begruendung,
|
|
# ein stehengebliebenes "MIGRATION"-Bild wuerde einen Betriebszustand behaupten, den es
|
|
# nicht mehr gibt. Die Begruendung trug nicht: der Anzeigedienst erkennt den
|
|
# ausgeschalteten Zustand selbst (systemctl is-active ninlight-station) und zeichnet dann
|
|
# "Station aus". Das eigentliche Ergebnis des Stoppens war ein anderes: kein "on"-Pfad hat
|
|
# den Dienst je wieder gestartet, und das Panel blieb ab dem zweiten Durchgang bis zum
|
|
# naechsten Pi-Neustart dunkel. Jetzt laesst 'off' den Dienst laufen.
|
|
SBX="$TMPROOT/tD4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'off' stoppt den Anzeigedienst weiterhin: $(cat "$SBX/cmd.log")" || pass "'off' stoppt den Anzeigedienst nicht mehr"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && fail "'off' leert weiterhin das Panel: $(cat "$SBX/cmd.log")" || pass "'off' leert das Panel nicht mehr (der Dienst zeichnet 'Station aus' selbst)"
|
|
|
|
echo
|
|
echo "=== Fall D4b (die Zusage aus Task 7f): 'off' und ein erneutes 'on' lassen den Anzeigedienst durchgehend an ==="
|
|
# Genau das ist die Zusage: nach 'off' und einem erneuten 'on' ist die Anzeige wieder in
|
|
# Betrieb - und zwar OHNE dass irgendwer den Dienst zwischendurch neu gestartet haben
|
|
# muesste, weil er nie gestoppt wurde. Ein/Aus/Ein durchspielen und ueber die gesamte
|
|
# Sequenz pruefen, dass 'ninlight-display' kein einziges Mal gestoppt wird.
|
|
SBX="$TMPROOT/tD4b"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
RC1=$?
|
|
run_cli "$SBX" off >/dev/null 2>&1
|
|
RC2=$?
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
RC3=$?
|
|
set -e
|
|
[ "$RC1" -eq 0 ] && [ "$RC2" -eq 0 ] && [ "$RC3" -eq 0 ] \
|
|
&& pass "on/off/on liefern alle Exit-Code 0" \
|
|
|| fail "on/off/on: Exit-Codes $RC1/$RC2/$RC3, Log: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" \
|
|
&& fail "der Anzeigedienst wurde waehrend on/off/on gestoppt - nach 'off' bliebe das Panel dunkel: $(cat "$SBX/cmd.log")" \
|
|
|| pass "der Anzeigedienst blieb ueber on/off/on hinweg durchgehend an"
|
|
|
|
echo
|
|
echo "=== Fall D9: ein fehlender/scheiternder Anzeigedienst haelt weder 'on' noch 'off' auf ==="
|
|
# FAKE_DISPLAY_SERVICE_MISSING bildet nach, dass 'ninlight-display' gar nicht installiert
|
|
# oder absichtlich deaktiviert ist (systemctl meldet dann einen Fehler statt still
|
|
# mitzuspielen). Das darf 'on' und 'off' nicht scheitern lassen - eine tragende Zusage
|
|
# dieses Projekts.
|
|
SBX="$TMPROOT/tD9"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT_ON=$(FAKE_DISPLAY_SERVICE_MISSING=1 FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=4 run_cli "$SBX" on 2>&1)
|
|
RC_ON=$?
|
|
OUT_OFF=$(FAKE_DISPLAY_SERVICE_MISSING=1 FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=4 run_cli "$SBX" off 2>&1)
|
|
RC_OFF=$?
|
|
set -e
|
|
[ "$RC_ON" -eq 0 ] && pass "'on' gelingt trotz fehlendem Anzeigedienst" || fail "'on' scheitert am fehlenden Anzeigedienst, Exit $RC_ON: $OUT_ON"
|
|
[ "$RC_OFF" -eq 0 ] && pass "'off' gelingt trotz fehlendem Anzeigedienst" || fail "'off' scheitert am fehlenden Anzeigedienst, Exit $RC_OFF: $OUT_OFF"
|
|
|
|
echo
|
|
echo "=== Fall D7: 'display' ohne/mit unbekanntem Unterbefehl -> Hilfetext, Exit 2 ==="
|
|
SBX="$TMPROOT/tD7"
|
|
setup_sandbox "$SBX"
|
|
# Anlegen, bevor geprueft wird: eine fehlende Logdatei wuerde die "kein Aufruf"-Zusage
|
|
# unten sonst durch ein grep-Versagen erfuellen statt durch das Verhalten des Skripts.
|
|
: >"$SBX/cmd.log"
|
|
for ARG in "" "quatsch"; do
|
|
set +e
|
|
# shellcheck disable=SC2086
|
|
OUT=$(run_cli "$SBX" display $ARG 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 2 ] && pass "Exit-Code 2 fuer 'display ${ARG:-<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 "########## Zeitschalter (Auto-Aus) ##########"
|
|
# Geprueft wird durchgehend das ERGEBNIS: die Zustandsdatei $RUN_DIR/autooff.json (sie
|
|
# ist im Skript die einzige Quelle dafuer, ob ein Zeitschalter laeuft), der
|
|
# Rueckgabewert, die Ausgabe und der nmcli-/systemd-Zustand - nicht die Interna der
|
|
# Stubs.
|
|
|
|
autooff_field() {
|
|
# $1 = Pfad zu autooff.json, $2 = Feldname
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2]))" "$1" "$2" 2>/dev/null \
|
|
|| echo "FEHLT"
|
|
}
|
|
write_autooff() {
|
|
# $1 = sandbox, $2 = minutes, $3 = deadline-Offset in Sekunden ab jetzt, $4 = deferrals
|
|
python3 - "$1/run/autooff.json" "$2" "$3" "$4" <<'PY'
|
|
import json, sys, time
|
|
path, minutes, offset, deferrals = sys.argv[1:5]
|
|
with open(path, "w") as fh:
|
|
json.dump({"minutes": int(minutes),
|
|
"deadline": int(time.time()) + int(offset),
|
|
"deferrals": int(deferrals)}, fh)
|
|
PY
|
|
}
|
|
|
|
echo "=== Fall Z1 (die wichtigste Zusage): 'on' OHNE --for legt keinen Zeitschalter an ==="
|
|
# Der Regelfall ist der Pi am Kabel - dem darf niemand den Zugangspunkt unter der
|
|
# laufenden Arbeit wegschalten, nur weil jemand vorsichtig sein wollte.
|
|
SBX="$TMPROOT/tZ1"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "keine Zeitschalter-Zustandsdatei angelegt" || fail "ohne --for entstand trotzdem ein Zeitschalter: $(cat "$SBX/run/autooff.json")"
|
|
grep -q "systemd-run" "$SBX/cmd.log" && fail "ohne --for wurde trotzdem ein Timer angelegt: $(grep systemd-run "$SBX/cmd.log")" || pass "kein systemd-run-Aufruf"
|
|
echo "$OUT" | grep -qi "Auto-Aus" && fail "Ausgabe verspricht einen Zeitschalter, den es nicht gibt: $OUT" || pass "Ausgabe erwaehnt keinen Zeitschalter"
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qi "Auto-Aus" && fail "'status' zeigt eine Zeitschalter-Zeile, obwohl keiner laeuft: $OUT" || pass "'status' schweigt ueber den Zeitschalter"
|
|
|
|
echo
|
|
echo "=== Fall Z2: 'on --for 30' legt einen Zeitschalter an ==="
|
|
SBX="$TMPROOT/tZ2"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" on --for 30 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ -e "$SBX/run/autooff.json" ] && pass "Zustandsdatei angelegt" || fail "keine autooff.json angelegt: $(cat "$SBX/cmd.log")"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "angeforderte Dauer 30 min gemerkt" || fail "minutes falsch: $(cat "$SBX/run/autooff.json")"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "0" ] && pass "noch keine Verschiebung" || fail "deferrals falsch: $(cat "$SBX/run/autooff.json")"
|
|
[ "$(file_mode "$SBX/run/autooff.json")" = "644" ] && pass "autooff.json ist 0644 (display.py muss sie lesen)" || fail "falsche Rechte: $(file_mode "$SBX/run/autooff.json")"
|
|
grep -q "systemd-run .*--unit=ninlight-station-autooff" "$SBX/cmd.log" && pass "Timer unter festem Unit-Namen angelegt" || fail "kein systemd-run mit festem Unit-Namen: $(cat "$SBX/cmd.log")"
|
|
grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "Timer feuert nach 30 min" || fail "falsche Laufzeit: $(grep systemd-run "$SBX/cmd.log")"
|
|
grep -q "__autooff" "$SBX/cmd.log" && pass "Timer ruft das CLI mit seiner Abschaltaktion" || fail "Timer ruft nichts Abschaltendes: $(grep systemd-run "$SBX/cmd.log")"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "die Station ist trotzdem ganz normal an" || fail "Modusdatei falsch"
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Auto-Aus: in (29|30) min" && pass "'status' zeigt die Restzeit" || fail "'status' zeigt keine brauchbare Restzeit: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall Z3: ungueltige Minutenangaben schalten GAR NICHTS ein ==="
|
|
# Geprueft wird nicht nur der Rueckgabewert: nach einer abgelehnten Angabe darf weder ein
|
|
# Zugangspunkt hochgefahren noch der Dienst neu gestartet noch eine Modusdatei
|
|
# geschrieben worden sein.
|
|
for BAD in "0" "241" "abc" "-5" "1.5" ""; do
|
|
SBX="$TMPROOT/tZ3-${BAD:-leer}"
|
|
setup_sandbox "$SBX"
|
|
# Logdatei anlegen, BEVOR geprueft wird (wie in Fall D7): eine fehlende Datei wuerde
|
|
# die "hat nichts eingeschaltet"-Zusagen unten sonst durch ein grep-Versagen erfuellen
|
|
# statt durch das Verhalten des Skripts.
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
if [ -z "$BAD" ]; then
|
|
OUT=$(run_cli "$SBX" on --for 2>"$SBX/stderr.log") # --for ohne Wert
|
|
else
|
|
OUT=$(run_cli "$SBX" on --for "$BAD" 2>"$SBX/stderr.log")
|
|
fi
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "'--for ${BAD:-<leer>}' wird abgelehnt (Exit $RC)" || fail "'--for ${BAD:-<leer>}' wurde angenommen: $OUT"
|
|
echo "$ERR" | grep -qi "fehler" && pass "'--for ${BAD:-<leer>}': Fehlermeldung auf stderr" || fail "'--for ${BAD:-<leer>}': keine Fehlermeldung: $ERR"
|
|
grep -q "nmcli con up" "$SBX/cmd.log" && fail "'--for ${BAD:-<leer>}' hat trotzdem einen Zugangspunkt hochgefahren: $(cat "$SBX/cmd.log")" || pass "'--for ${BAD:-<leer>}': kein Zugangspunkt hochgefahren"
|
|
grep -q "systemctl restart" "$SBX/cmd.log" && fail "'--for ${BAD:-<leer>}' hat den Dienst gestartet" || pass "'--for ${BAD:-<leer>}': Dienst nicht gestartet"
|
|
[ ! -e "$SBX/run/mode" ] && pass "'--for ${BAD:-<leer>}': keine Modusdatei geschrieben" || fail "'--for ${BAD:-<leer>}' hat eine Modusdatei geschrieben: $(cat "$SBX/run/mode")"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "'--for ${BAD:-<leer>}': kein Zeitschalter angelegt" || fail "'--for ${BAD:-<leer>}': Zeitschalter trotz ungueltiger Angabe"
|
|
done
|
|
|
|
echo
|
|
echo "=== Fall Z4: 'migrate on --listen --for 5' - beides zusammen ==="
|
|
SBX="$TMPROOT/tZ4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen --for 5 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "phase=listen bleibt erhalten" || fail "phase falsch: $(cat "$SBX/run/control.json" 2>&1)"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "5" ] && pass "Zeitschalter ueber 5 min angelegt" || fail "Zeitschalter falsch/fehlt: $(cat "$SBX/run/autooff.json" 2>&1)"
|
|
# Umgekehrte Reihenfolge der Schalter muss genauso gehen.
|
|
SBX="$TMPROOT/tZ4b"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --for 5 --listen 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && [ "$(control_phase "$SBX/run/control.json")" = "listen" ] \
|
|
&& pass "Schalterreihenfolge ist egal" || fail "'--for 5 --listen' scheiterte (Exit $RC): $OUT"
|
|
|
|
echo
|
|
echo "=== Fall Z5 (nicht verhandelbar): der Zeitschalter schaltet NICHT in eine laufende Auslieferung ==="
|
|
SBX="$TMPROOT/tZ5"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_SS_BUSY=1 run_cli "$SBX" __autooff 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "die Station wurde mitten in der Auslieferung abgeschaltet: $(cat "$SBX/cmd.log")" || pass "die Station bleibt an"
|
|
grep -q "nmcli con down" "$SBX/cmd.log" && fail "der Zugangspunkt wurde mitten in der Auslieferung heruntergefahren" || pass "der Zugangspunkt bleibt oben"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "1" ] && pass "Verschiebung gezaehlt (1)" || fail "deferrals nicht hochgezaehlt: $(cat "$SBX/run/autooff.json" 2>&1)"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "die angeforderte Dauer bleibt fuer 'renew' erhalten" || fail "minutes wurde ueberschrieben: $(cat "$SBX/run/autooff.json")"
|
|
echo "$OUT" | grep -qi "verschoben" && pass "die Verschiebung wird gemeldet" || fail "keine Meldung ueber die Verschiebung: $OUT"
|
|
# Gegenprobe: dieselbe Lage ohne laufende Verbindung schaltet sehr wohl ab.
|
|
SBX="$TMPROOT/tZ5b"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" __autooff 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Gegenprobe: Exit-Code 0" || fail "Gegenprobe: Exit-Code $RC: $OUT"
|
|
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && pass "ohne laufende Auslieferung schaltet der Zeitschalter ab" || fail "der Zeitschalter schaltet gar nicht ab: $(cat "$SBX/cmd.log")"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE_AFTER" | grep -qx "home-wifi" && pass "wlan0 ist zurueck im Heim-WLAN" || fail "nicht zurueck im Heim-WLAN: $ACTIVE_AFTER"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "der abgelaufene Zeitschalter ist abgeraeumt" || fail "Zustandsdatei blieb liegen"
|
|
# Die Pruefung muss die richtige Frage stellen: offene Verbindungen auf Port 80 aus dem
|
|
# Stationsnetz.
|
|
grep -q "STUB ss .*:80" "$SBX/cmd.log" && pass "geprueft wird Port 80" || fail "die Verbindungspruefung fragt nicht nach Port 80: $(grep 'STUB ss' "$SBX/cmd.log")"
|
|
grep -q "STUB ss .*192.168.8.0/24" "$SBX/cmd.log" && pass "geprueft wird das Stationsnetz 192.168.8.0/24" || fail "die Verbindungspruefung fragt nicht nach dem Stationsnetz: $(grep 'STUB ss' "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall Z6: die Obergrenze fuers Verschieben greift - eine haengende Verbindung haelt den Zeitschalter nicht ewig auf ==="
|
|
SBX="$TMPROOT/tZ6"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
write_autooff "$SBX" 30 60 10 # 10 Verschiebungen sind die Obergrenze
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_SS_BUSY=1 run_cli "$SBX" __autooff 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT $ERR"
|
|
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && pass "an der Obergrenze wird trotz offener Verbindung abgeschaltet" || fail "die haengende Verbindung hebelt den Zeitschalter dauerhaft aus: $(cat "$SBX/cmd.log")"
|
|
echo "$ERR" | grep -qi "warnung" && pass "das Abschalten trotz offener Verbindung wird gemeldet" || fail "keine Warnung: $ERR"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "Zustandsdatei abgeraeumt" || fail "Zustandsdatei blieb liegen"
|
|
# Eine Verschiebung UNTER der Grenze verschiebt dagegen weiter (sonst koennte die
|
|
# Grenzpruefung auch einfach immer abschalten).
|
|
SBX="$TMPROOT/tZ6b"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
write_autooff "$SBX" 30 60 9
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
FAKE_SS_BUSY=1 run_cli "$SBX" __autooff >/dev/null 2>&1
|
|
set -e
|
|
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "eine Verschiebung unter der Grenze hat schon abgeschaltet" || pass "unter der Grenze wird weiter verschoben"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "10" ] && pass "Zaehler steht jetzt auf der Grenze (10)" || fail "deferrals falsch: $(cat "$SBX/run/autooff.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall Z7: 'off' raeumt einen laufenden Zeitschalter mit ab ==="
|
|
# Sonst feuert er spaeter in eine Station hinein, die laengst aus ist - oder inzwischen
|
|
# wieder an.
|
|
SBX="$TMPROOT/tZ7"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
[ -e "$SBX/run/autooff.json" ] || fail "Vorbedingung: kein Zeitschalter angelegt"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "Zeitschalter-Zustandsdatei ist weg" || fail "'off' liess den Zeitschalter stehen: $(cat "$SBX/run/autooff.json")"
|
|
grep -q "systemctl stop ninlight-station-autooff.timer" "$SBX/cmd.log" && pass "der Timer selbst wurde angehalten" || fail "der systemd-Timer wurde nicht angehalten: $(cat "$SBX/cmd.log")"
|
|
grep -q "systemctl stop ninlight-station-autooff$" "$SBX/cmd.log" && fail "es wurde die .service statt der .timer gestoppt - das brächte den Zeitschalter-Lauf um sich selbst" || pass "die gleichnamige .service wird nicht gestoppt"
|
|
|
|
echo
|
|
echo "=== Fall Z8: 'renew'/'hold' ohne laufenden Zeitschalter sind klare Fehler, kein stillschweigendes Neuanlegen ==="
|
|
for CMD in renew hold; do
|
|
SBX="$TMPROOT/tZ8-$CMD"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on >/dev/null 2>&1 # Station an, aber OHNE --for
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" "$CMD" 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "'$CMD' ohne Zeitschalter: Exit-Code $RC" || fail "'$CMD' meldete Erfolg: $OUT"
|
|
echo "$ERR" | grep -qi "kein Zeitschalter" && pass "'$CMD': klare Meldung auf stderr" || fail "'$CMD': unklare Meldung: $ERR"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "'$CMD' hat keinen Zeitschalter angelegt" || fail "'$CMD' hat stillschweigend einen angelegt: $(cat "$SBX/run/autooff.json")"
|
|
grep -q "systemd-run" "$SBX/cmd.log" && fail "'$CMD' hat einen Timer angelegt" || pass "'$CMD': kein systemd-run"
|
|
done
|
|
|
|
echo
|
|
echo "=== Fall Z9: 'renew' stellt die Uhr zurueck - ohne Argument auf die zuletzt angeforderte Dauer ==="
|
|
SBX="$TMPROOT/tZ9"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
write_autooff "$SBX" 30 120 4 # Restzeit fast abgelaufen, schon viermal verschoben
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" renew 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "dieselbe Dauer wie zuletzt angefordert (30)" || fail "minutes falsch: $(cat "$SBX/run/autooff.json")"
|
|
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "0" ] && pass "Verschiebungszaehler zurueckgesetzt" || fail "deferrals nicht zurueckgesetzt: $(cat "$SBX/run/autooff.json")"
|
|
STATUS=$(run_cli "$SBX" status 2>&1)
|
|
echo "$STATUS" | grep -qE "Auto-Aus: in (29|30) min" && pass "die Restzeit steht wieder bei 30 min" || fail "Restzeit nicht zurueckgestellt: $STATUS"
|
|
grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "der Timer wurde neu gestellt" || fail "kein neuer Timer: $(cat "$SBX/cmd.log")"
|
|
# Mit Argument gilt das Argument - und ungueltige Angaben werden auch hier abgelehnt.
|
|
set +e
|
|
OUT=$(run_cli "$SBX" renew 90 2>&1); RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "90" ] && pass "'renew 90' stellt auf 90 min" || fail "'renew 90' scheiterte (Exit $RC): $OUT"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" renew 999 2>"$SBX/stderr.log"); RC=$?
|
|
set -e
|
|
[ "$RC" -ne 0 ] && [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "90" ] && pass "'renew 999' wird abgelehnt, der laufende Zeitschalter bleibt unveraendert" || fail "'renew 999' wurde angenommen (Exit $RC): $(cat "$SBX/run/autooff.json")"
|
|
|
|
echo
|
|
echo "=== Fall Z10: 'hold' bestellt ab - die Station bleibt an ==="
|
|
SBX="$TMPROOT/tZ10"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on --for 30 >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" hold 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "Zeitschalter abbestellt" || fail "Zeitschalter laeuft weiter: $(cat "$SBX/run/autooff.json")"
|
|
grep -q "systemctl stop ninlight-station-autooff.timer" "$SBX/cmd.log" && pass "der Timer wurde angehalten" || fail "Timer nicht angehalten: $(cat "$SBX/cmd.log")"
|
|
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "'hold' hat die Station abgeschaltet" || pass "die Station bleibt an"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "der Zugangspunkt laeuft unveraendert weiter" || fail "Zugangspunkt-Zustand veraendert: $ACTIVE_AFTER"
|
|
# Und ein danach doch noch feuernder Timer-Lauf schaltet nichts ab.
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
run_cli "$SBX" __autooff >/dev/null 2>&1
|
|
set -e
|
|
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "ein abbestellter Zeitschalter hat trotzdem abgeschaltet: $(cat "$SBX/cmd.log")" || pass "ein abbestellter Zeitschalter schaltet nichts mehr ab"
|
|
|
|
echo
|
|
echo "=== Fall Z11: scheitert systemd-run, bleibt das nicht unbemerkt ==="
|
|
# Sonst glaubte der Nutzer an einen Zeitschalter, den es nie gab - und genau darauf
|
|
# hat er sich verlassen, als er die SSH-Sitzung ueber WLAN aufgab.
|
|
SBX="$TMPROOT/tZ11"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(FAKE_SYSTEMD_RUN_EXIT=1 run_cli "$SBX" on --for 30 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0, obwohl der Timer nicht angelegt werden konnte: $OUT"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
|
[ ! -e "$SBX/run/autooff.json" ] && pass "keine Zustandsdatei, die einen Zeitschalter vortaeuscht" || fail "autooff.json trotz gescheitertem Timer: $(cat "$SBX/run/autooff.json")"
|
|
echo "$ERR" | grep -q "off" && pass "die Meldung sagt, dass die Station AN bleibt" || fail "die Meldung verschweigt den Zustand der Station: $ERR"
|
|
|
|
echo
|
|
echo "=== Bildsatz A: die Vorgabe ohne Umschalterdatei ist der Satz OHNE WLAN-Zugangsdaten ==="
|
|
# Die sichere Richtung: ein Fehlgriff kostet einen Onboarding-Durchgang, kein
|
|
# WLAN-Passwort in einer fremden Lampe.
|
|
SBX="$TMPROOT/bs1"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
echo "$OUT" | grep -q "ohne-wlan" && pass "ohne Datei gilt 'ohne-wlan'" || fail "Vorgabe ist nicht 'ohne-wlan': $OUT"
|
|
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das zugehoerige Verzeichnis wird genannt" || fail "Verzeichnis fehlt in der Ausgabe: $OUT"
|
|
[ ! -e "$SBX/var/imageset" ] && pass "das blosse Berichten legt keine Datei an" || fail "'bildsatz' ohne Argument hat geschrieben"
|
|
|
|
echo
|
|
echo "=== Bildsatz B: umschalten schreibt die Datei atomar mit 0644 und bleibt stehen ==="
|
|
SBX="$TMPROOT/bs2"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" bildsatz mit-wlan 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
[ "$(cat "$SBX/var/imageset" 2>/dev/null | tr -d '\n')" = "mit-wlan" ] && pass "die Datei enthaelt 'mit-wlan'" || fail "Dateiinhalt: $(cat "$SBX/var/imageset" 2>&1)"
|
|
MODE=$(file_mode "$SBX/var/imageset")
|
|
[ "$MODE" = "644" ] && pass "Rechte 0644 (der Anzeigedienst muss lesen koennen)" || fail "Rechte $MODE statt 644"
|
|
[ -z "$(find "$SBX/var" -name '.imageset-*' 2>/dev/null)" ] && pass "keine Temp-Datei liegengeblieben" || fail "Temp-Datei liegt noch da"
|
|
echo "$OUT" | grep -qi "achtung" && pass "der Wechsel zu 'mit-wlan' warnt" || fail "keine Warnung beim Wechsel zu mit-wlan: $OUT"
|
|
# ... und er BLEIBT stehen: ein zweiter, unabhaengiger Aufruf sieht ihn.
|
|
OUT2=$(run_cli "$SBX" bildsatz 2>&1)
|
|
echo "$OUT2" | grep -q "mit-wlan" && pass "der Zustand bleibt ueber Aufrufe hinweg" || fail "Zustand nicht gespeichert: $OUT2"
|
|
# ... und zurueck.
|
|
run_cli "$SBX" bildsatz ohne-wlan >/dev/null 2>&1
|
|
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "ohne-wlan" ] && pass "zurueckschalten geht auch" || fail "zurueckschalten hat nicht gewirkt"
|
|
|
|
echo
|
|
echo "=== Bildsatz C: ein unbekannter Name schaltet nichts um ==="
|
|
SBX="$TMPROOT/bs3"
|
|
setup_sandbox "$SBX"
|
|
printf 'mit-wlan\n' >"$SBX/var/imageset"
|
|
set +e
|
|
ERR=$(run_cli "$SBX" bildsatz irgendwas 2>&1 >/dev/null)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 fuer einen unbekannten Bildsatz"
|
|
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "mit-wlan" ] && pass "der bestehende Zustand blieb unveraendert" || fail "der Zustand wurde veraendert: $(cat "$SBX/var/imageset")"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
|
|
|
echo
|
|
echo "=== Bildsatz D: ein kaputter Dateiinhalt faellt auf die SICHERE Vorgabe zurueck ==="
|
|
# Nie auf den teureren Satz - das ist die eine Richtung, die kein Zweifelsfall nehmen darf.
|
|
SBX="$TMPROOT/bs4"
|
|
setup_sandbox "$SBX"
|
|
printf 'voellig-anderes\n' >"$SBX/var/imageset"
|
|
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
|
echo "$OUT" | grep -q "ohne-wlan" && pass "kaputter Inhalt -> 'ohne-wlan'" || fail "kaputter Inhalt fuehrte nicht auf die Vorgabe: $OUT"
|
|
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das Verzeichnis des sicheren Satzes" || fail "falsches Verzeichnis: $OUT"
|
|
# Der Bediener muss erfahren, dass sein Eintrag NICHT gewirkt hat - sonst haelt er die
|
|
# Station fuer umgeschaltet.
|
|
echo "$OUT" | grep -q "unbekannte Einstellung 'voellig-anderes'" \
|
|
&& pass "die unbekannte Einstellung wird beim Namen genannt" \
|
|
|| fail "die unbekannte Einstellung wird verschwiegen: $OUT"
|
|
# Gegenprobe: bei einem GUELTIGEN Eintrag darf diese Zeile nicht erscheinen.
|
|
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
|
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
|
echo "$OUT" | grep -q "unbekannte Einstellung" \
|
|
&& fail "meldet 'unbekannte Einstellung' fuer einen gueltigen Eintrag: $OUT" \
|
|
|| pass "kein Fehlalarm bei gueltigem Eintrag"
|
|
|
|
echo
|
|
echo "=== Bildsatz E: ein gewaehlter, aber fehlender Satz wird beim Umschalten UND im Status gemeldet ==="
|
|
SBX="$TMPROOT/bs5"
|
|
setup_sandbox "$SBX"
|
|
rm -rf "$SBX/images/migrate-wlan"
|
|
set +e
|
|
ERR=$(run_cli "$SBX" bildsatz mit-wlan 2>&1 >/dev/null)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Umschalten gelingt trotzdem (der Zustand ist gueltig, nur die Bestueckung fehlt)" || fail "Exit-Code $RC"
|
|
echo "$ERR" | grep -qi "warnung" && pass "Warnung beim Umschalten" || fail "keine Warnung: $ERR"
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -q "FEHLT" && pass "'status' sagt, dass nichts ausgeliefert wird" || fail "'status' verschweigt den fehlenden Satz: $OUT"
|
|
|
|
echo
|
|
echo "=== Bildsatz F: ein LEERES Verzeichnis zaehlt genauso als fehlend ==="
|
|
SBX="$TMPROOT/bs6"
|
|
setup_sandbox "$SBX"
|
|
rm -f "$SBX/images/migrate-nocreds"/*.img
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -q "FEHLT" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT"
|
|
|
|
echo
|
|
echo "=== Bildsatz G: 'migrate on' nennt den geltenden Bildsatz in der Einschaltmeldung ==="
|
|
# Er kann von vorletzter Woche stammen - wer einschaltet, muss ihn sehen.
|
|
SBX="$TMPROOT/bs7"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
echo "$OUT" | grep -q "Bildsatz: mit-wlan" && pass "die Einschaltmeldung nennt den Bildsatz" || fail "Bildsatz fehlt in der Einschaltmeldung: $OUT"
|
|
|
|
echo
|
|
echo "=== Bildsatz H: die Warnung 'keine Migrationsimages' meint den GEWAEHLTEN Satz ==="
|
|
SBX="$TMPROOT/bs8"
|
|
setup_sandbox "$SBX"
|
|
rm -f "$SBX/images/migrate-wlan"/*.img # nur der andere Satz ist leer
|
|
set +e
|
|
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
|
|
set -e
|
|
echo "$ERR" | grep -q "keine Migrationsimages" && fail "warnt, obwohl der GEWAEHLTE Satz ('ohne-wlan') bestueckt ist: $ERR" || pass "kein Fehlalarm - gezaehlt wird im gewaehlten Satz"
|
|
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
|
set +e
|
|
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
|
|
set -e
|
|
echo "$ERR" | grep -q "keine Migrationsimages" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR"
|
|
|
|
echo
|
|
echo "=== Abschluss A: 'display final' kennt zwei verschiedene Bilder ==="
|
|
# E-Paper behaelt sein Bild dauerhaft. Beim Ausschalten darf man den Stecker ziehen, beim
|
|
# Neustart gerade nicht - deshalb zwei Schalter, nicht einer.
|
|
SBX="$TMPROOT/fin1"
|
|
setup_sandbox "$SBX"
|
|
export FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 # Anzeigedienst laeuft nicht
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display final poweroff 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
grep -q "STUB display.py --final poweroff" "$SBX/cmd.log" && pass "display.py wird mit '--final poweroff' aufgerufen" || fail "falscher Aufruf: $(cat "$SBX/cmd.log")"
|
|
: >"$SBX/cmd.log"
|
|
run_cli "$SBX" display final reboot >/dev/null 2>&1
|
|
grep -q "STUB display.py --final reboot" "$SBX/cmd.log" && pass "und mit '--final reboot' fuer den Neustart" || fail "falscher Aufruf: $(cat "$SBX/cmd.log")"
|
|
grep -q "poweroff" "$SBX/cmd.log" && fail "der Neustart ruft das Ausschaltbild auf" || pass "die beiden Wege werden nicht verwechselt"
|
|
unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT
|
|
|
|
echo
|
|
echo "=== Abschluss B: ohne 'poweroff'/'reboot' passiert nichts ==="
|
|
SBX="$TMPROOT/fin2"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
ERR=$(run_cli "$SBX" display final 2>&1 >/dev/null)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 ohne Spielart"
|
|
grep -q "STUB display.py" "$SBX/cmd.log" 2>/dev/null && fail "display.py wurde trotzdem aufgerufen" || pass "das Panel wurde nicht angefasst"
|
|
set +e
|
|
ERR=$(run_cli "$SBX" display final quatsch 2>&1 >/dev/null)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 fuer eine unbekannte Spielart" || fail "Exit-Code 0 fuer 'quatsch'"
|
|
grep -q "STUB display.py" "$SBX/cmd.log" 2>/dev/null && fail "display.py wurde fuer 'quatsch' aufgerufen" || pass "und auch dafuer nicht"
|
|
|
|
echo
|
|
echo "=== Abschluss C: gezeichnet wird erst, NACHDEM der Anzeigedienst die Leitungen freigegeben hat ==="
|
|
# Zwei Prozesse auf denselben GPIO-/SPI-Leitungen enden in "lgpio.error: 'GPIO busy'" -
|
|
# in diesem Projekt schon zweimal passiert.
|
|
SBX="$TMPROOT/fin3"
|
|
setup_sandbox "$SBX"
|
|
export FAKE_DISPLAY_ACTIVE_FOR=2 # erst beim dritten Blick ist er gestoppt
|
|
set +e
|
|
OUT=$(NINLIGHT_DISPLAY_RELEASE_WAIT_S=5 run_cli "$SBX" display final poweroff 2>&1)
|
|
RC=$?
|
|
set -e
|
|
unset FAKE_DISPLAY_ACTIVE_FOR
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
|
grep -q "STUB display.py --final poweroff" "$SBX/cmd.log" && pass "das Abschlussbild wurde gezeichnet" || fail "gar nicht gezeichnet: $(cat "$SBX/cmd.log")"
|
|
# Die Reihenfolge in der GEMEINSAMEN Logdatei ist der Beleg: jede is-active-Abfrage steht
|
|
# vor dem display.py-Aufruf, keine danach.
|
|
# "|| true" ueberall: ohne Treffer liefert grep 1, und unter "set -e" wuerde die SUITE an
|
|
# dieser Stelle stillschweigend abbrechen, statt einen Fehler zu melden - eine Mutation,
|
|
# die das Warten ganz entfernt, waere dann scheinbar gruen (genau so passiert).
|
|
DRAW_LINE=$(grep -n "STUB display.py" "$SBX/cmd.log" 2>/dev/null | head -n1 | cut -d: -f1 || true)
|
|
LAST_CHECK=$(grep -n "systemctl is-active --quiet ninlight-display" "$SBX/cmd.log" 2>/dev/null | tail -n1 | cut -d: -f1 || true)
|
|
CHECKS=$(grep -c "systemctl is-active --quiet ninlight-display" "$SBX/cmd.log" 2>/dev/null || true)
|
|
CHECKS=${CHECKS:-0}
|
|
if [ -n "$DRAW_LINE" ] && [ -n "$LAST_CHECK" ] && [ "$LAST_CHECK" -lt "$DRAW_LINE" ]; then
|
|
pass "alle Abfragen des Anzeigedienstes stehen VOR dem Zeichnen"
|
|
else
|
|
fail "gezeichnet wurde nicht nach der Freigabe (Zeichnen Zeile '$DRAW_LINE', letzte Abfrage Zeile '$LAST_CHECK')"
|
|
fi
|
|
[ "$CHECKS" -ge 3 ] && pass "es wurde wirklich gewartet ($CHECKS Abfragen)" || fail "nur $CHECKS Abfrage(n) - es wurde gar nicht auf die Freigabe gewartet"
|
|
|
|
echo
|
|
echo "=== Abschluss D: ein Anzeigedienst, der nicht loslaesst, haelt das Herunterfahren nicht auf ==="
|
|
# Lieber kein Abschlussbild als ein haengendes Herunterfahren - und auf keinen Fall ein
|
|
# zweiter Prozess auf den belegten Leitungen.
|
|
SBX="$TMPROOT/fin4"
|
|
setup_sandbox "$SBX"
|
|
export FAKE_DISPLAY_ACTIVE_FOR=9999
|
|
set +e
|
|
ERR=$(NINLIGHT_DISPLAY_RELEASE_WAIT_S=2 run_cli "$SBX" display final poweroff 2>&1 >/dev/null)
|
|
RC=$?
|
|
set -e
|
|
unset FAKE_DISPLAY_ACTIVE_FOR
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (ein Anzeigefehler haelt das Herunterfahren nie auf)" || fail "Exit-Code $RC: $ERR"
|
|
grep -q "STUB display.py" "$SBX/cmd.log" && fail "es wurde auf belegte Leitungen gezeichnet - 'GPIO busy'" || pass "nicht gezeichnet, solange der Dienst haelt"
|
|
echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
|
|
# Und es wurde NICHT versucht, den Dienst von hier aus zu stoppen: das liefe mitten in
|
|
# der Abschalt-Transaktion.
|
|
grep -q "systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'systemctl stop' mitten im Herunterfahren" || pass "kein 'systemctl stop' aus dem Abschaltschritt heraus"
|
|
|
|
echo
|
|
echo "=== Abschluss E: ein scheiterndes display.py bleibt eine Warnung ==="
|
|
SBX="$TMPROOT/fin5"
|
|
setup_sandbox "$SBX"
|
|
export FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3
|
|
set +e
|
|
ERR=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display final reboot 2>&1 >/dev/null)
|
|
RC=$?
|
|
set -e
|
|
unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC - ein Anzeigefehler wuerde das Herunterfahren rot machen: $ERR"
|
|
echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
|
|
|
|
echo
|
|
if [ "$FAILED" -eq 0 ]; then
|
|
echo "Alle Tests bestanden."
|
|
else
|
|
echo "Mindestens ein Test ist fehlgeschlagen." >&2
|
|
fi
|
|
exit "$FAILED"
|