"on"/"recovery on"/"migrate on" nehmen jetzt "--for <minuten>" (1-240): die Station schaltet sich danach selbsttaetig ab und kehrt ins Heim-WLAN zurueck. Das hilft, wenn man ueber WLAN angemeldet ist - "on" macht wlan0 zum Zugangspunkt und reisst jede SSH-Sitzung ab. Ausdruecklich optional: ohne "--for" entsteht kein Zeitschalter und alles verhaelt sich exakt wie bisher. Dazu "renew [<minuten>]" (Uhr zurueckstellen), "hold" (abbestellen, Station bleibt an) und eine Restzeit in "status"; "off" raeumt einen laufenden Zeitschalter mit ab. Umgesetzt als transienter systemd-Timer unter festem Unit-Namen (systemd-run), Zustand in RUN_DIR/autooff.json. Vor dem Abschalten wird per "ss" geprueft, ob noch eine Verbindung auf Port 80 aus dem Stationsnetz offen ist - dann wird um rund eine Minute verschoben, hoechstens zehnmal (Begruendung im Kommentar). Das Panel zeigt den Zeitschalter als eigene Zeile, aber nur wenn einer laeuft.
1181 lines
61 KiB
Bash
Executable File
1181 lines
61 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Stub-Tests fuer ninlight-station (das CLI, nicht server.py) - ohne Pi und ohne
|
|
# echte NetworkManager-/systemd-Rechte ausfuehrbar.
|
|
#
|
|
# nmcli, systemctl und sudo werden im PATH durch Stubs ersetzt, die ihre Aufrufe
|
|
# in eine Logdatei schreiben (Muster wie tests/test_deploy.sh fuer ssh/rsync).
|
|
# Der nmcli-Stub haelt zusaetzlich einen kleinen Zustand (welche Profile
|
|
# "bekannt" sind, welche gerade per "con up" aktiv sind) in Dateien, damit
|
|
# "con up"/"con down"/"device show wlan0" konsistent zueinander antworten -
|
|
# das ist die Grundlage fuer Fall 15, den wichtigsten Test hier. Der
|
|
# systemctl-Stub bildet zusaetzlich nach, dass systemd das RuntimeDirectory bei
|
|
# jedem Stop/Restart leert, sofern die Unit kein RuntimeDirectoryPreserve=restart
|
|
# traegt - ohne das haette Fixrunde 1's C1 (siehe unten) unsichtbar bleiben
|
|
# koennen, weil ein Stub, der nichts loescht, diese Klasse von Bug nicht zeigt.
|
|
#
|
|
# Aufruf: tools/station/tests/test_cli.sh
|
|
set -euo pipefail
|
|
|
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
STATION_DIR=$(cd "$HERE/.." && pwd)
|
|
CLI="$STATION_DIR/ninlight-station"
|
|
REAL_UNIT="$STATION_DIR/ninlight-station.service"
|
|
|
|
TMPROOT=$(mktemp -d)
|
|
trap 'rm -rf "$TMPROOT"' EXIT
|
|
|
|
FAILED=0
|
|
pass() { echo "ok - $1"; }
|
|
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
|
|
|
|
file_mode() {
|
|
# $1 = Pfad. BSD-stat (macOS) und GNU-stat (Linux/Pi) haben unterschiedliche
|
|
# Flags - hier reicht "irgendeine funktioniert". Reihenfolge ist wichtig
|
|
# (Fixrunde 2, N1): "stat -f '%Lp' datei" auf GNU-coreutils deutet "-f" als
|
|
# "Dateisystemstatus" und '%Lp' als ZWEITES Dateiargument - der Aufruf schreibt
|
|
# mehrere Zeilen Dateisystem-Infos auf stdout und scheitert (kein passendes
|
|
# zweites File), aber der "||"-Zweig haengt DANACH trotzdem noch "644" an, und
|
|
# der Vergleich "= 644" schlaegt fehl, obwohl die Rechte stimmen - auf dem Pi
|
|
# (echtes GNU-coreutils) also immer rot. BSD-"stat -c" dagegen bricht sofort
|
|
# mit einer Fehlermeldung auf stderr ab, ganz ohne Ausgabe auf stdout - der
|
|
# GNU-Aufruf muss deshalb ZUERST stehen.
|
|
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null
|
|
}
|
|
|
|
# Erzwingt den GNU-Zweig von file_mode() (der auf dem Pi tatsaechlich laeuft),
|
|
# indem "stat" im PATH auf GNU-coreutils' "gstat" umgebogen wird, falls
|
|
# vorhanden (macOS mit "brew install coreutils"). Ohne diese Probe wuerde ein
|
|
# falsch herum sortiertes file_mode() auf dieser Entwicklungsmaschine (BSD-stat
|
|
# zuerst probiert und erfolgreich) unbemerkt bleiben, obwohl es auf dem Pi
|
|
# (nur GNU-stat) rot waere - genau der Fehler aus Fixrunde 2, N1.
|
|
if command -v gstat >/dev/null 2>&1; then
|
|
GSTAT_CHECK_DIR="$TMPROOT/gstat-check"
|
|
mkdir -p "$GSTAT_CHECK_DIR/bin"
|
|
ln -sf "$(command -v gstat)" "$GSTAT_CHECK_DIR/bin/stat"
|
|
GSTAT_CHECK_FILE="$TMPROOT/gstat-check/probe"
|
|
: >"$GSTAT_CHECK_FILE"
|
|
chmod 0644 "$GSTAT_CHECK_FILE"
|
|
GSTAT_MODE=$(PATH="$GSTAT_CHECK_DIR/bin:$PATH" file_mode "$GSTAT_CHECK_FILE")
|
|
[ "$GSTAT_MODE" = "644" ] && pass "file_mode() liefert unter erzwungenem GNU-stat korrekt '644'" || fail "file_mode() liefert unter GNU-stat '$GSTAT_MODE' statt '644' - siehe Fixrunde 2, N1"
|
|
else
|
|
echo "Hinweis: gstat nicht gefunden - GNU-Zweig von file_mode() wird nur auf dem Pi selbst geprueft" >&2
|
|
fi
|
|
|
|
# --- Stub-Kommandos fuer nmcli/systemctl/sudo im PATH ---
|
|
# $1 = bindir, $2 = logdatei, $3 = nmcli-Zustandsverzeichnis, $4 = RUN_DIR,
|
|
# $5 = Pfad zur systemd-Unit, deren RuntimeDirectoryPreserve-Zeile ueber das
|
|
# Leeren von $4 bei restart/stop entscheidet (wie beim echten systemd).
|
|
setup_stubbin() {
|
|
local bindir=$1 log=$2 nmstate=$3 rundir=$4 unitfile=$5
|
|
mkdir -p "$bindir" "$nmstate"
|
|
: >"$nmstate/active" # Reihenfolge der per "con up" aktiven Profile, eine Zeile je Profil
|
|
|
|
cat >"$bindir/nmcli" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB nmcli \$*" >>"$log"
|
|
NMSTATE="$nmstate"
|
|
|
|
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "NAME" ] && [ "\$4" = "con" ] && [ "\$5" = "show" ]; then
|
|
cat "\$NMSTATE/profiles" 2>/dev/null
|
|
exit 0
|
|
fi
|
|
|
|
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "GENERAL.CONNECTION" ] && [ "\$4" = "device" ] && [ "\$5" = "show" ]; then
|
|
# Testschalter fuer Fall I3 (Fixrunde 1): erzwingt eine Antwort, die nicht zum
|
|
# zuletzt hochgefahrenen Profil passt, um ap_switch()'s Fehlerzweig zu pruefen.
|
|
if [ -n "\${FAKE_NMCLI_DEVSHOW:-}" ]; then
|
|
echo "GENERAL.CONNECTION:\${FAKE_NMCLI_DEVSHOW}"
|
|
exit 0
|
|
fi
|
|
last=\$(tail -n1 "\$NMSTATE/active" 2>/dev/null)
|
|
echo "GENERAL.CONNECTION:\${last:---}"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "\$1" = "con" ] && [ "\$2" = "up" ]; then
|
|
profile=\$3
|
|
grep -qxF "\$profile" "\$NMSTATE/profiles" 2>/dev/null || { echo "Error: unknown connection '\$profile'." >&2; exit 1; }
|
|
grep -qxF "\$profile" "\$NMSTATE/active" 2>/dev/null || echo "\$profile" >>"\$NMSTATE/active"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "\$1" = "con" ] && [ "\$2" = "down" ]; then
|
|
# Testschalter fuer Fall I2 (Fixrunde 1): simuliert einen haengenden
|
|
# NetworkManager - der Aufruf "gelingt" (Exit 0, wie ein echtes "nmcli con
|
|
# down" es bei einem haengenden Backend oft tut), aendert den Zustand aber
|
|
# nicht. ap_switch() darf das nicht als "aus" durchgehen lassen.
|
|
if [ -n "\${FAKE_NMCLI_STUCK:-}" ]; then
|
|
exit 0
|
|
fi
|
|
profile=\$3
|
|
grep -vxF "\$profile" "\$NMSTATE/active" 2>/dev/null >"\$NMSTATE/active.tmp" || true
|
|
mv "\$NMSTATE/active.tmp" "\$NMSTATE/active"
|
|
exit 0
|
|
fi
|
|
|
|
exit 0
|
|
EOF
|
|
|
|
cat >"$bindir/systemctl" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB systemctl \$*" >>"$log"
|
|
RUN_DIR="$rundir"
|
|
UNIT_FILE="$unitfile"
|
|
case "\$1" in
|
|
is-active)
|
|
# Getrennt schaltbar je Unit: der Handbetrieb am Panel fragt nach ninlight-display
|
|
# (laeuft der Anzeigedienst?), alles andere nach ninlight-station. Die Unit ist
|
|
# immer das letzte Argument ("is-active --quiet <unit>" bzw. "is-active <unit>").
|
|
unit="\${!#}"
|
|
if [ "\$unit" = "ninlight-display" ]; then
|
|
code="\${FAKE_DISPLAY_SERVICE_ACTIVE_EXIT:-0}"
|
|
else
|
|
code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}"
|
|
fi
|
|
if [ "\$code" -eq 0 ]; then
|
|
echo active
|
|
exit 0
|
|
else
|
|
echo inactive
|
|
exit "\$code"
|
|
fi
|
|
;;
|
|
restart|stop)
|
|
# Bildet systemds RuntimeDirectory-Verhalten nach (Fixrunde 1, C1): ohne
|
|
# RuntimeDirectoryPreserve=restart in der Unit leert systemd das
|
|
# RuntimeDirectory bei JEDEM Stop/Restart, nicht nur beim allerersten Start,
|
|
# und legt es leer wieder an.
|
|
#
|
|
# Die Regel gilt nur fuer ninlight-station - \$UNIT_FILE ist deren Unit-Datei.
|
|
# ninlight-display traegt gar kein RuntimeDirectory=, ein "stop ninlight-display"
|
|
# raeumt in Wirklichkeit nichts weg (frueher tat der Stub das trotzdem fuer JEDE
|
|
# Unit; harmlos, solange kein Testfall die Anzeige stoppt und danach noch Dateien
|
|
# in RUN_DIR erwartet - genau das tun 'display test'/'display clear' jetzt aber,
|
|
# die den Anzeigedienst waehrend des Handgriffs stoppen).
|
|
if [ "\$2" = "ninlight-station" ] \
|
|
&& ! grep -q '^RuntimeDirectoryPreserve=restart' "\$UNIT_FILE" 2>/dev/null; then
|
|
rm -rf "\$RUN_DIR"
|
|
mkdir -p "\$RUN_DIR"
|
|
fi
|
|
if [ "\$1" = "restart" ]; then
|
|
exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}"
|
|
fi
|
|
exit 0
|
|
;;
|
|
*) exit 0 ;;
|
|
esac
|
|
EOF
|
|
|
|
cat >"$bindir/sudo" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo "STUB sudo \$*" >>"$log"
|
|
exec "\$@"
|
|
EOF
|
|
|
|
# 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" "$sandbox/bin" "$sandbox/nmstate" \
|
|
"$sandbox/lib/display"
|
|
echo "dummy" >"$sandbox/images/ninlight_foo_ota.img"
|
|
|
|
# NINLIGHT_STATION_LIB zeigt auf diese Sandbox statt auf tools/station selbst.
|
|
# products.py wird gebraucht (arm prueft das Ziel dagegen) und deshalb echt kopiert;
|
|
# display/display.py ist ein Rekorder, der seine Argumente in DIESELBE cmd.log
|
|
# schreibt wie die nmcli-/systemctl-Stubs. Nur so laesst sich die REIHENFOLGE
|
|
# "erst Dienst stoppen, dann Panel leeren" ueberhaupt pruefen - mit zwei getrennten
|
|
# Logs waere sie nicht rekonstruierbar.
|
|
#
|
|
# Der Rekorder ersetzt das echte display.py absichtlich: geprueft wird hier, WAS
|
|
# ninlight-station aufruft (Pfad, Schalter, Reihenfolge, Umgang mit dem Rueckgabe-
|
|
# wert), nicht was display.py daraus macht - das deckt tests/test_render.py ab.
|
|
cp "$STATION_DIR/products.py" "$sandbox/lib/products.py"
|
|
cat >"$sandbox/lib/display/display.py" <<EOF
|
|
import os
|
|
import 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" \
|
|
"$CLI" "$@"
|
|
}
|
|
|
|
control_phase() {
|
|
# $1 = Pfad zu control.json
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('phase','?'))" "$1" 2>/dev/null || echo "FEHLT"
|
|
}
|
|
control_target() {
|
|
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('armed_target'))" "$1" 2>/dev/null || echo "FEHLT"
|
|
}
|
|
|
|
echo "=== Fall 1: 'on' faehrt ninlight-recovery hoch, startet den Dienst neu, meldet 'Station an' ==="
|
|
SBX="$TMPROOT/t1"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
|
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch: $(cat "$SBX/run/mode" 2>&1)"
|
|
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 2: 'recovery on' identisch zu 'on' (Rueckwaertskompatibilitaet) ==="
|
|
SBX="$TMPROOT/t2"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" recovery on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
|
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
|
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch"
|
|
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 3: 'migrate on' faehrt ninlight-recovery herunter, BEVOR qconnex-firmware hochkommt; control.json phase=auto ==="
|
|
SBX="$TMPROOT/t3"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1 # Recovery vorher an
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
DOWN_LINE=$(grep -n "nmcli con down ninlight-recovery" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
UP_LINE=$(grep -n "nmcli con up qconnex-firmware" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$DOWN_LINE" ] && [ -n "$UP_LINE" ] && [ "$DOWN_LINE" -lt "$UP_LINE" ]; then
|
|
pass "ninlight-recovery wird VOR qconnex-firmware heruntergefahren"
|
|
else
|
|
fail "Reihenfolge falsch oder Aufrufe fehlen (down=$DOWN_LINE, up=$UP_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
[ "$(cat "$SBX/run/mode")" = "migration" ] && pass "Modusdatei enthaelt 'migration'" || fail "Modusdatei falsch"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "auto" ] && pass "control.json: phase=auto" || fail "control.json phase ist nicht 'auto': $(cat "$SBX/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall 4: 'migrate on --listen' wie Fall 3, aber phase=listen ==="
|
|
SBX="$TMPROOT/t4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "control.json: phase=listen" || fail "control.json phase ist nicht 'listen': $(cat "$SBX/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Fall 5: 'migrate on' ohne Migrationsimages -> Warnung auf stderr, Rueckgabewert 0 ==="
|
|
SBX="$TMPROOT/t5"
|
|
setup_sandbox "$SBX"
|
|
# images/migrate/ bleibt leer
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlender Migrationsimages" || fail "Exit-Code $RC, stderr: $ERR"
|
|
echo "$ERR" | grep -q "Warnung" && pass "Warnung steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
|
|
|
echo
|
|
echo "=== Fall 6: 'arm' ohne Argument liest last_seen.target aus status.json ==="
|
|
SBX="$TMPROOT/t6"
|
|
setup_sandbox "$SBX"
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "migration", "phase": "listen", "last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "control.json: phase=armed" || fail "phase ist nicht 'armed'"
|
|
[ "$(control_target "$SBX/run/control.json")" = "gu10_rgb" ] && pass "armed_target=gu10_rgb (aus last_seen)" || fail "armed_target falsch: $(cat "$SBX/run/control.json")"
|
|
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 7: 'arm gu10_rgb' mit explizitem Ziel ==="
|
|
SBX="$TMPROOT/t7"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm gu10_rgb 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "control.json: phase=armed" || fail "phase ist nicht 'armed'"
|
|
[ "$(control_target "$SBX/run/control.json")" = "gu10_rgb" ] && pass "armed_target=gu10_rgb" || fail "armed_target falsch: $(cat "$SBX/run/control.json")"
|
|
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 8: 'arm' ohne last_seen -> Fehler auf stderr, Rueckgabewert != 0, control.json unveraendert ==="
|
|
SBX="$TMPROOT/t8"
|
|
setup_sandbox "$SBX"
|
|
# keine status.json vorhanden
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
|
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt: $(cat "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 9: 'arm unbekannt' -> Fehler auf stderr, Rueckgabewert != 0 ==="
|
|
SBX="$TMPROOT/t9"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" arm unbekannt 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
|
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt"
|
|
|
|
echo
|
|
echo "=== Fall 10: 'listen' setzt phase=listen ==="
|
|
SBX="$TMPROOT/t10"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" listen 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "control.json: phase=listen" || fail "phase ist nicht 'listen': $(cat "$SBX/run/control.json" 2>&1)"
|
|
[ "$(file_mode "$SBX/run/control.json")" = "644" ] && pass "control.json ist 0644" || fail "control.json hat falsche Rechte: $(file_mode "$SBX/run/control.json")"
|
|
|
|
echo
|
|
echo "=== Fall 11: 'off' faehrt beide Profile herunter, stoppt den Dienst, zurueck ins Heim-WLAN ==="
|
|
SBX="$TMPROOT/t11"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -q "systemctl stop ninlight-station" "$SBX/cmd.log" && pass "Dienst gestoppt" || fail "kein systemctl stop im Log"
|
|
ACTIVE=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE" | grep -qx "ninlight-recovery" && fail "ninlight-recovery ist noch aktiv" || pass "ninlight-recovery ist nicht mehr aktiv"
|
|
echo "$ACTIVE" | grep -qx "qconnex-firmware" && fail "qconnex-firmware ist noch aktiv" || pass "qconnex-firmware ist nicht mehr aktiv"
|
|
echo "$ACTIVE" | grep -qx "home-wifi" && pass "zurueck im Heim-WLAN (home-wifi aktiv)" || fail "home-wifi ist nicht aktiv: $ACTIVE"
|
|
echo "$OUT" | grep -q "Heim-WLAN" && pass "Ausgabe erwaehnt Heim-WLAN" || fail "Ausgabe erwaehnt Heim-WLAN nicht: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 12: 'status' im Migrationsmodus zeigt Modus, Phase und zuletzt erkannte Lampe ==="
|
|
SBX="$TMPROOT/t12"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1 # phase=auto in control.json, wie es der echte Ablauf waere
|
|
# status.json so, wie server.py sie beim Start geschrieben haette (mode/last_seen).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "migration",
|
|
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
set +e
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
echo "$OUT" | grep -q "Modus.*migration" && pass "Modus wird angezeigt" || fail "Modus fehlt: $OUT"
|
|
echo "$OUT" | grep -q "Phase.*auto" && pass "Phase wird angezeigt" || fail "Phase fehlt: $OUT"
|
|
echo "$OUT" | grep -q "Erkannt.*Klyqa GU10 RGB CW/WW" && pass "zuletzt erkannte Lampe wird angezeigt" || fail "erkannte Lampe fehlt: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 13: 'status' mit unlesbarer status.json meldet das ausdruecklich (4a-Fehler) ==="
|
|
SBX="$TMPROOT/t13"
|
|
setup_sandbox "$SBX"
|
|
echo '{"mode": "recovery"}' >"$SBX/run/status.json"
|
|
chmod 000 "$SBX/run/status.json"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
RC=$?
|
|
set -e
|
|
chmod 644 "$SBX/run/status.json" # damit trap-Aufraeumen am Ende sicher klappt
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (status liefert trotzdem eine Auskunft)" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
echo "$OUT" | grep -q "existiert, ist aber nicht lesbar" && pass "unlesbare status.json wird ausdruecklich gemeldet" || fail "Meldung zu unlesbarer status.json fehlt: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 14: unbekannter Unterbefehl -> Aufrufhinweis, Rueckgabewert 2 ==="
|
|
SBX="$TMPROOT/t14"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" frobnicate 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 2 ] && pass "Exit-Code 2" || fail "Exit-Code war $RC statt 2"
|
|
echo "$OUT" | grep -qi "Aufruf" && pass "Aufrufhinweis vorhanden" || fail "kein Aufrufhinweis: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall 15 (wichtigster Test): 'migrate on' bei laufendem Recovery-Modus laesst GENAU EIN Profil aktiv ==="
|
|
SBX="$TMPROOT/t15"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
ACTIVE_BEFORE=$(cat "$SBX/nmstate/active")
|
|
echo "$ACTIVE_BEFORE" | grep -qx "ninlight-recovery" && pass "Vorbedingung: ninlight-recovery laeuft" || fail "Vorbedingung nicht erfuellt: $ACTIVE_BEFORE"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
COUNT=$(echo "$ACTIVE_AFTER" | grep -c . || true)
|
|
if [ "$COUNT" -eq 1 ] && [ "$ACTIVE_AFTER" = "qconnex-firmware" ]; then
|
|
pass "genau ein Profil aktiv: qconnex-firmware"
|
|
else
|
|
fail "erwartet genau 'qconnex-firmware', tatsaechlich aktiv: [$ACTIVE_AFTER]"
|
|
fi
|
|
DEVSHOW=$(PATH="$SBX/bin:$PATH" nmcli -t -f GENERAL.CONNECTION device show wlan0)
|
|
[ "$DEVSHOW" = "GENERAL.CONNECTION:qconnex-firmware" ] && pass "nmcli device show wlan0 bestaetigt qconnex-firmware" || fail "device show liefert: $DEVSHOW"
|
|
|
|
echo
|
|
echo "=== Zusatzfall C1: ohne RuntimeDirectoryPreserve=restart wuerde der Neustart mode/control.json wegwischen - das CLI muss das erkennen und abbrechen ==="
|
|
SBX="$TMPROOT/tC1"
|
|
BROKEN_UNIT="$TMPROOT/broken-ninlight-station.service"
|
|
grep -v '^RuntimeDirectoryPreserve=restart' "$REAL_UNIT" >"$BROKEN_UNIT"
|
|
setup_sandbox "$SBX" "$BROKEN_UNIT"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" migrate on --listen 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - eine geleerte RuntimeDirectory wird nicht als Erfolg gemeldet" || fail "Exit-Code war 0, obwohl RUN_DIR beim Neustart geleert wurde: $OUT"
|
|
echo "$ERR" | grep -qi "RuntimeDirectoryPreserve" && pass "Fehlermeldung nennt RuntimeDirectoryPreserve als Ursache" || fail "Fehlermeldung nennt die Ursache nicht: $ERR"
|
|
echo "$OUT" | grep -q "Migration an" && fail "CLI meldete faelschlich 'Migration an', obwohl die Phase nach dem Neustart nicht mehr stimmte" || pass "kein falscher Erfolgsmeldung 'Migration an'"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ -z "$ACTIVE_AFTER" ] && pass "Zugangspunkt wurde nach dem erkannten Fehler wieder heruntergefahren" || fail "Zugangspunkt blieb aktiv: $ACTIVE_AFTER"
|
|
# Gegenprobe: dieselbe Sandbox, aber mit der ECHTEN (reparierten) Unit-Datei -
|
|
# jetzt darf RUN_DIR beim Neustart nicht geleert werden und der Aufruf muss
|
|
# durchlaufen. Das ist die eigentliche Bestaetigung, dass RuntimeDirectoryPreserve
|
|
# in tools/station/ninlight-station.service tatsaechlich steht und wirkt.
|
|
SBX2="$TMPROOT/tC1-fixed"
|
|
setup_sandbox "$SBX2"
|
|
echo "dummy" >"$SBX2/images/migrate/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT2=$(run_cli "$SBX2" migrate on --listen 2>&1)
|
|
RC2=$?
|
|
set -e
|
|
[ "$RC2" -eq 0 ] && pass "mit der echten (reparierten) Unit-Datei gelingt derselbe Aufruf" || fail "Gegenprobe schlug fehl - Exit-Code $RC2: $OUT2"
|
|
[ "$(control_phase "$SBX2/run/control.json")" = "listen" ] && pass "Gegenprobe: control.json ueberlebt den Neustart mit phase=listen" || fail "Gegenprobe: Phase nach Neustart falsch: $(cat "$SBX2/run/control.json" 2>&1)"
|
|
|
|
echo
|
|
echo "=== Zusatzfall I1: 'status' zeigt die Phase aus control.json, nicht die veraltete Phase aus status.json ==="
|
|
SBX="$TMPROOT/tI1"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
# status.json wird nur beim Dienststart/bei einer Auslieferung geschrieben - hier
|
|
# simuliert, wie es nach dem Start aussehen wuerde (Phase "listen", wie beim Start).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "migration", "phase": "listen",
|
|
"last_seen": {"target": "gu10_rgb", "product_name": "Klyqa GU10 RGB CW/WW", "lamp_version": "V1.1.0.1138"}}
|
|
JSON
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # startet den Dienst NICHT neu - status.json bleibt bei "listen"
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -q "Phase.*armed" && pass "status zeigt 'armed' (aus control.json), nicht das veraltete 'listen' aus status.json" || fail "status zeigt die falsche/veraltete Phase: $OUT"
|
|
echo "$OUT" | grep -q "gu10_rgb" && pass "status nennt das freigegebene Ziel" || fail "status nennt das armed_target nicht: $OUT"
|
|
|
|
echo
|
|
echo "=== Zusatzfall I2: 'off' bricht mit Fehler ab, wenn ein Stationsprofil trotz 'con down' aktiv bleibt (haengender NetworkManager) ==="
|
|
SBX="$TMPROOT/tI2"
|
|
setup_sandbox "$SBX"
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
# HOME_CONNECTION aus den bekannten Profilen entfernen, um zugleich den in I2
|
|
# genannten zweiten Umstand abzudecken (Zielprofil fuers Heim-WLAN fehlt).
|
|
grep -v '^home-wifi$' "$SBX/nmstate/profiles" >"$SBX/nmstate/profiles.tmp"
|
|
mv "$SBX/nmstate/profiles.tmp" "$SBX/nmstate/profiles"
|
|
set +e
|
|
OUT=$(FAKE_NMCLI_STUCK=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - ein haengender Zugangspunkt wird nicht als 'Station aus' gemeldet" || fail "Exit-Code war 0, obwohl ninlight-recovery weiterhin aktiv war: $OUT"
|
|
echo "$OUT" | grep -q "Station aus" && fail "CLI behauptete faelschlich 'Station aus'" || pass "keine falsche 'Station aus'-Meldung"
|
|
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr vorhanden" || fail "keine Fehlermeldung auf stderr: $ERR"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "ninlight-recovery ist (wie vom Stub simuliert) tatsaechlich noch aktiv" || fail "unerwarteter nmcli-Zustand: $ACTIVE_AFTER"
|
|
|
|
echo
|
|
echo "=== Zusatzfall I3: ap_switch bricht ab und faehrt beide Profile herunter, wenn 'device show wlan0' nach dem Umschalten nicht passt ==="
|
|
SBX="$TMPROOT/tI3"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
set +e
|
|
OUT=$(FAKE_NMCLI_DEVSHOW=fremdes-profil run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0, wenn wlan0 nach dem Umschalten ein fremdes Profil zeigt" || fail "Exit-Code war 0 trotz Profil-Mismatch: $OUT"
|
|
echo "$ERR" | grep -q "unerwartet" && pass "Fehlermeldung nennt den Mismatch" || fail "Fehlermeldung fehlt: $ERR"
|
|
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
|
[ -z "$ACTIVE_AFTER" ] && pass "beide Stationsprofile wurden nach dem erkannten Mismatch heruntergefahren" || fail "mindestens ein Profil blieb aktiv: $ACTIVE_AFTER"
|
|
|
|
echo
|
|
echo "=== Zusatzfall N2 (Fixrunde 2): 'status' zeigt keine veraltete Migrationsphase mehr, nachdem in den Rettungsmodus zurueckgeschaltet wurde ==="
|
|
SBX="$TMPROOT/tN2"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # control.json bleibt mit phase=armed liegen
|
|
run_cli "$SBX" on >/dev/null 2>&1 # zurueck in den Rettungsmodus - control.json wird bewusst NICHT aufgeraeumt
|
|
[ "$(control_phase "$SBX/run/control.json")" = "armed" ] && pass "Vorbedingung: control.json ist tatsaechlich noch mit phase=armed liegen geblieben" || fail "Vorbedingung nicht erfuellt (control.json wurde schon vorher veraendert)"
|
|
# status.json so, wie sie der Server nach dem Neustart im Rettungsmodus schreiben
|
|
# wuerde (mode=recovery, keine last_seen/phase-Felder - siehe server.py write_state()).
|
|
cat >"$SBX/run/status.json" <<'JSON'
|
|
{"mode": "recovery"}
|
|
JSON
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Modus[[:space:]]*: recovery" && pass "Modus zeigt korrekt 'recovery'" || fail "Modus falsch: $OUT"
|
|
echo "$OUT" | grep -qi "armed" && fail "status zeigt weiterhin die veraltete Migrationsphase 'armed' im Rettungsmodus: $OUT" || pass "keine veraltete Migrationsphase 'armed' im Rettungsmodus"
|
|
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt den neutralen Platzhalter '-' ausserhalb des Migrationsmodus" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
|
|
|
echo
|
|
echo "=== Zusatzfall N2b (Fixrunde 2): dieselbe veraltete Freigabe, aber ohne aktualisierte status.json (Modus '-') - genau das woertliche Repro aus dem Review ==="
|
|
SBX="$TMPROOT/tN2b"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
|
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1
|
|
run_cli "$SBX" on >/dev/null 2>&1
|
|
rm -f "$SBX/run/status.json" # kein Serverprozess in diesem Test aktualisiert sie
|
|
OUT=$(run_cli "$SBX" status 2>&1)
|
|
echo "$OUT" | grep -qE "Modus[[:space:]]*: -" && pass "Modus zeigt '-' (keine status.json vorhanden)" || fail "Modus unerwartet: $OUT"
|
|
echo "$OUT" | grep -qi "armed" && fail "status zeigt 'armed', obwohl der Modus gar nicht bekannt ist: $OUT" || pass "keine 'armed'-Phase bei unbekanntem Modus"
|
|
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt '-' statt einer Vermutung" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
|
|
|
echo
|
|
echo "=== Fall D1 (Task 6b, Punkt 3): 'display test' ruft display.py mit --test und der konfigurierten Revision auf ==="
|
|
SBX="$TMPROOT/tD1"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "display.py wurde mit --test aufgerufen" || fail "kein '--test'-Aufruf im Log: $(cat "$SBX/cmd.log")"
|
|
# Der Testbild-Aufruf ist die einzige Moeglichkeit, die Panel-Revision zu bestimmen -
|
|
# er ist wertlos, wenn er nicht die Revision aus der Konfiguration durchreicht:
|
|
# EPD_REVISION wird per ". $CONF" nur als Shell-Variable gesetzt, nicht exportiert.
|
|
grep -Fq "STUB display.py --test (EPD_REVISION=V3)" "$SBX/cmd.log" && pass "EPD_REVISION aus der Konfiguration erreicht den Python-Prozess" || fail "EPD_REVISION kam nicht durch: $(cat "$SBX/cmd.log")"
|
|
# Der Anzeigedienst haelt die GPIO-Leitungen; ohne ihn kurz zu stoppen scheitert der
|
|
# Handgriff am Geraet mit "lgpio.error: 'GPIO busy'" (Hardware-Abnahme). Frueher stand
|
|
# hier die umgekehrte Zusage ("laesst den Anzeigedienst in Ruhe") - die war genau der
|
|
# Grund, warum der erste Handgriff der Abnahme nicht funktionierte.
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
TEST_LINE=$(grep -n "STUB display.py --test" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
START_LINE=$(grep -n "STUB systemctl start ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$TEST_LINE" ] && [ "$STOP_LINE" -lt "$TEST_LINE" ]; then
|
|
pass "der Anzeigedienst wird gestoppt, BEVOR display.py die GPIO-Leitungen anfasst"
|
|
else
|
|
fail "Anzeigedienst nicht (rechtzeitig) gestoppt (stop=$STOP_LINE, test=$TEST_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
# Und danach wieder an: ein Abnahme-Handgriff darf die Station nicht dauerhaft ohne
|
|
# Anzeige zuruecklassen.
|
|
if [ -n "$START_LINE" ] && [ -n "$TEST_LINE" ] && [ "$TEST_LINE" -lt "$START_LINE" ]; then
|
|
pass "der Anzeigedienst laeuft danach wieder (start=$START_LINE)"
|
|
else
|
|
fail "Anzeigedienst wurde nicht wieder gestartet (test=$TEST_LINE, start=$START_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D2: 'display clear' ruft display.py mit --clear auf ==="
|
|
SBX="$TMPROOT/tD2"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" display clear 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "display.py wurde mit --clear aufgerufen" || fail "kein '--clear'-Aufruf im Log: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && fail "'display clear' hat --test aufgerufen" || pass "kein --test bei 'display clear'"
|
|
# Dieselbe GPIO-Kollision wie bei 'display test' - auch 'display clear' braucht die
|
|
# Leitungen fuer sich.
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'display clear' stoppt den Anzeigedienst fuer die Dauer" || fail "'display clear' laesst den Anzeigedienst laufen - das gibt 'GPIO busy': $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "'display clear' startet den Anzeigedienst danach wieder" || fail "'display clear' laesst den Anzeigedienst aus: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D2a: war der Anzeigedienst aus, bleibt er nach 'display test' aus ==="
|
|
# Sonst haette ein Abnahme-Handgriff die Station nebenbei umgeschaltet: nach
|
|
# 'ninlight-station off' ist der Anzeigedienst bewusst gestoppt, und 'display test'
|
|
# darf ihn nicht wieder hochziehen.
|
|
SBX="$TMPROOT/tD2a"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "Testbild wurde trotzdem gezeichnet" || fail "kein '--test'-Aufruf: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && fail "'display test' hat den Anzeigedienst eingeschaltet, obwohl er aus war" || pass "kein 'start' fuer einen Dienst, der gar nicht lief"
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'display test' stoppt einen Dienst, der gar nicht laeuft" || pass "kein ueberfluessiges 'stop'"
|
|
|
|
echo
|
|
echo "=== Fall D2b: scheitert das Panel, meldet 'display test' das - der Dienst laeuft trotzdem wieder ==="
|
|
# Der Rueckgabewert von display.py ist das Ergebnis des Handgriffs; ein erfolgreiches
|
|
# Wiederanfahren des Dienstes darf ihn nicht in eine Erfolgsmeldung verwandeln. Und
|
|
# umgekehrt darf ein gescheitertes Testbild den Dienst nicht dauerhaft aus lassen.
|
|
SBX="$TMPROOT/tD2b"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display test 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 1 ] && pass "Exit-Code 1 wird durchgereicht" || fail "Exit-Code $RC statt 1: $OUT"
|
|
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "Anzeigedienst laeuft trotz Panel-Fehler wieder" || fail "Anzeigedienst blieb nach dem Fehler aus: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall 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 (Task 6b, Punkt 4): 'off' nimmt die Anzeige mit - Dienst gestoppt, Panel geleert ==="
|
|
# Ohne das behauptet das stehengebliebene Bild einen Betriebszustand ("MIGRATION"),
|
|
# den es nicht mehr gibt - bei einer Station, die Lampen ungefragt Firmware
|
|
# unterschieben kann, die gefaehrliche Richtung.
|
|
SBX="$TMPROOT/tD4"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(run_cli "$SBX" off 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
|
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'off' stoppt den Anzeigedienst" || fail "'off' stoppt den Anzeigedienst nicht: $(cat "$SBX/cmd.log")"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "'off' leert das Panel" || fail "'off' leert das Panel nicht: $(cat "$SBX/cmd.log")"
|
|
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$STOP_LINE" ] && [ -n "$CLEAR_LINE" ] && [ "$STOP_LINE" -lt "$CLEAR_LINE" ]; then
|
|
pass "auch bei 'off': erst stoppen, dann leeren"
|
|
else
|
|
fail "Reihenfolge bei 'off' falsch (stop=$STOP_LINE, clear=$CLEAR_LINE): $(cat "$SBX/cmd.log")"
|
|
fi
|
|
# Das Panel muss geleert sein, BEVOR ap_switch das Ergebnis von "off" verkuenden kann -
|
|
# und vor allem, bevor ap_switch bei haengendem NetworkManager abbricht (Fall D6).
|
|
UP_LINE=$(grep -n "nmcli con up home-wifi" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
|
|
if [ -n "$UP_LINE" ] && [ "$CLEAR_LINE" -lt "$UP_LINE" ]; then
|
|
pass "Panel ist geleert, bevor wlan0 ins Heim-WLAN zurueckgeschaltet wird"
|
|
else
|
|
fail "Panel wurde erst nach dem Zurueckschalten geleert (clear=$CLEAR_LINE, up=$UP_LINE)"
|
|
fi
|
|
|
|
echo
|
|
echo "=== Fall D5: ein Fehler beim Leeren des Panels haelt 'off' nicht auf ==="
|
|
# Ein fehlendes/kaputtes Panel darf das Ausschalten der Station nie blockieren -
|
|
# sonst bliebe ausgerechnet der Zugangspunkt an, weil die Anzeige klemmt.
|
|
SBX="$TMPROOT/tD5"
|
|
setup_sandbox "$SBX"
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem Leeren" || fail "Exit-Code $RC, stderr: $ERR"
|
|
echo "$ERR" | grep -q "Warnung" && pass "Warnung ueber das nicht geleerte Panel steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
|
grep -Fq "nmcli con up home-wifi" "$SBX/cmd.log" && pass "'off' hat trotzdem bis zum Zurueckschalten ins Heim-WLAN durchgezogen" || fail "'off' ist am Anzeigefehler haengengeblieben: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D6: haengender NetworkManager - das Panel wird trotzdem noch geleert ==="
|
|
# ap_switch "" bricht hier mit Fehler ab; unter "set -e" laeuft danach nichts mehr.
|
|
# Genau deshalb steht display_off VOR ap_switch: sonst bliebe ein "MIGRATION"-Bild
|
|
# stehen, waehrend der Zugangspunkt noch sendet und niemand mehr etwas abschaltet.
|
|
SBX="$TMPROOT/tD6"
|
|
setup_sandbox "$SBX"
|
|
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
|
run_cli "$SBX" migrate on >/dev/null 2>&1
|
|
: >"$SBX/cmd.log"
|
|
set +e
|
|
OUT=$(FAKE_NMCLI_STUCK=1 run_cli "$SBX" off 2>"$SBX/stderr.log")
|
|
RC=$?
|
|
set -e
|
|
ERR=$(cat "$SBX/stderr.log")
|
|
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 (der Zugangspunkt liess sich nicht abschalten)" || fail "Exit-Code 0, obwohl der Zugangspunkt noch aktiv ist: $OUT"
|
|
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "Panel wurde trotz haengendem NetworkManager geleert" || fail "Panel blieb stehen, waehrend der Zugangspunkt noch sendet: $(cat "$SBX/cmd.log")"
|
|
|
|
echo
|
|
echo "=== Fall D7: 'display' ohne/mit unbekanntem Unterbefehl -> Hilfetext, Exit 2 ==="
|
|
SBX="$TMPROOT/tD7"
|
|
setup_sandbox "$SBX"
|
|
# Anlegen, bevor geprueft wird: eine fehlende Logdatei wuerde die "kein Aufruf"-Zusage
|
|
# unten sonst durch ein grep-Versagen erfuellen statt durch das Verhalten des Skripts.
|
|
: >"$SBX/cmd.log"
|
|
for ARG in "" "quatsch"; do
|
|
set +e
|
|
# shellcheck disable=SC2086
|
|
OUT=$(run_cli "$SBX" display $ARG 2>&1)
|
|
RC=$?
|
|
set -e
|
|
[ "$RC" -eq 2 ] && pass "Exit-Code 2 fuer 'display ${ARG:-<leer>}'" || fail "Exit-Code $RC fuer 'display ${ARG:-<leer>}': $OUT"
|
|
echo "$OUT" | grep -q "display {test|clear|off}" && pass "Hilfetext nennt die Unterbefehle ('display ${ARG:-<leer>}')" || fail "Hilfetext fehlt: $OUT"
|
|
done
|
|
grep -Fq "STUB display.py" "$SBX/cmd.log" && fail "ein unbekannter Unterbefehl hat display.py trotzdem aufgerufen" || pass "kein display.py-Aufruf bei unbekanntem Unterbefehl"
|
|
|
|
echo
|
|
echo "=== Fall D8 (Task 6b, Punkt 3): der Hilfetext erklaert die Revisionsbestimmung ==="
|
|
# Die Panel-Revision steht am Geraet nirgends; 'display test' ist die einzige
|
|
# Moeglichkeit, sie zu bestimmen, und das muss dort stehen, wo man beim ersten
|
|
# Handgriff der Hardware-Abnahme hinsieht.
|
|
SBX="$TMPROOT/tD8"
|
|
setup_sandbox "$SBX"
|
|
set +e
|
|
HELP=$(run_cli "$SBX" quatsch 2>&1)
|
|
set -e
|
|
echo "$HELP" | grep -q "EPD_REVISION" && pass "Hilfetext nennt EPD_REVISION" || fail "Hilfetext nennt EPD_REVISION nicht: $HELP"
|
|
echo "$HELP" | grep -q "ninlight-station.conf" && pass "Hilfetext nennt die Datei, in der die Revision steht" || fail "Hilfetext nennt /etc/ninlight-station.conf nicht: $HELP"
|
|
echo "$HELP" | grep -qE "V2|V3" && pass "Hilfetext nennt die beiden moeglichen Revisionen" || fail "Hilfetext nennt V2/V3 nicht: $HELP"
|
|
|
|
echo
|
|
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/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/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
|
|
if [ "$FAILED" -eq 0 ]; then
|
|
echo "Alle Tests bestanden."
|
|
else
|
|
echo "Mindestens ein Test ist fehlgeschlagen." >&2
|
|
fi
|
|
exit "$FAILED"
|