feat(station): optionaler Zeitschalter schaltet die Station selbst ab
"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.
This commit is contained in:
@@ -172,7 +172,29 @@ echo "STUB sudo \$*" >>"$log"
|
||||
exec "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo
|
||||
# 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 ---
|
||||
@@ -871,6 +893,284 @@ echo "$HELP" | grep -q "EPD_REVISION" && pass "Hilfetext nennt EPD_REVISION" ||
|
||||
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."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -731,6 +732,121 @@ def test_armed_phase_label_is_visible():
|
||||
assert "g95_cw" in text
|
||||
|
||||
|
||||
# --- Zeitschalter am Panel (Task 7e) ------------------------------------------------
|
||||
#
|
||||
# Ein Zeitschalter, den man am Panel nicht sieht, ist schlimmer als keiner: man glaubt,
|
||||
# die Station laeuft, und sie ist laengst aus. Umgekehrt darf ohne Zeitschalter (der
|
||||
# Regelfall - er entsteht nur mit "--for") keine Zeile dafuer erscheinen.
|
||||
|
||||
def test_autooff_line_appears_only_with_a_running_timer():
|
||||
without = render.lines(_migration_state())
|
||||
assert not any("Auto-Aus" in line for line in without), without
|
||||
|
||||
state = dict(_migration_state(), autooff_minutes=7)
|
||||
with_timer = render.lines(state)
|
||||
assert "Auto-Aus in 7 min" in with_timer, with_timer
|
||||
|
||||
|
||||
def test_autooff_line_also_appears_in_recovery_mode():
|
||||
"""Der Zeitschalter haengt nicht am Migrationsmodus - im Rettungsmodus reisst "on"
|
||||
die SSH-Sitzung genauso ab."""
|
||||
lines = render.lines({"mode": "recovery", "ssid": "ninlight-recovery", "served": 0,
|
||||
"recent": [], "autooff_minutes": 42})
|
||||
assert "Auto-Aus in 42 min" in lines, lines
|
||||
|
||||
|
||||
def test_autooff_line_is_actually_drawn_on_the_panel():
|
||||
"""Nicht nur in lines(): die Zeile muss im vollsten Zustand auch tatsaechlich aufs
|
||||
Panel kommen und nicht unten herausfallen (compose() bricht Beiwerkzeilen vor dem
|
||||
unteren Rand ab). Geprueft per Pixelvergleich gegen eine unabhaengig gezeichnete
|
||||
Referenzzeile - schriftunabhaengig und ohne behauptete Pixelbreiten."""
|
||||
state = dict(_armed_state(), autooff_minutes=7)
|
||||
expected = "Auto-Aus in 7 min"
|
||||
# Die wievielte BEIWERKZEILE ist es? (Kopfbalken, Phase im Kopf und grosse Zeile
|
||||
# sind eigene Rollen und belegen keine Beiwerkzeile.)
|
||||
details = [text for text, role in render.blocks(state) if role == render.ROLE_DETAIL]
|
||||
assert expected in details, details
|
||||
index = details.index(expected)
|
||||
|
||||
top, bottom = _detail_band(index)
|
||||
drawn = _band_pixels(render.compose(state), top, bottom)
|
||||
reference = _band_pixels(_reference_detail_line(expected, top), top, bottom)
|
||||
|
||||
assert drawn == reference, (
|
||||
"die Zeitschalter-Zeile steht nicht (ungekuerzt) auf dem Panel - der Bediener "
|
||||
"sieht nicht, dass sich die Station gleich abschaltet")
|
||||
|
||||
|
||||
def test_autooff_line_says_so_when_the_deadline_has_arrived():
|
||||
lines = render.lines({"mode": "recovery", "ssid": "x", "served": 0, "recent": [],
|
||||
"autooff_minutes": 0})
|
||||
assert "Auto-Aus steht an" in lines, lines
|
||||
|
||||
|
||||
def test_build_state_reads_the_autooff_file(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
(tmp_path / "mode").write_text("recovery")
|
||||
conf_path = tmp_path / "station.conf"
|
||||
conf_path.write_text("SSID='ninlight-recovery'\n")
|
||||
# Noch 6 min 1 s - aufgerundet 7, wie es "ninlight-station status" auch rechnet.
|
||||
(tmp_path / "autooff.json").write_text(json.dumps(
|
||||
{"minutes": 30, "deadline": time.time() + 361, "deferrals": 0}))
|
||||
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf_path, service_name="x")
|
||||
|
||||
assert state["autooff_minutes"] == 7
|
||||
assert "Auto-Aus in 7 min" in render.lines(state)
|
||||
|
||||
|
||||
def test_build_state_without_autooff_file_has_no_timer_field(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
(tmp_path / "mode").write_text("recovery")
|
||||
conf_path = tmp_path / "station.conf"
|
||||
conf_path.write_text("SSID='ninlight-recovery'\n")
|
||||
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf_path, service_name="x")
|
||||
|
||||
assert "autooff_minutes" not in state
|
||||
assert not any("Auto-Aus" in line for line in render.lines(state))
|
||||
|
||||
|
||||
def test_broken_autooff_file_is_treated_as_no_timer(tmp_path):
|
||||
"""Dieselbe Zusage wie fuer status.json/control.json: halb geschrieben, kaputt oder
|
||||
aus einer aelteren Fassung darf keine Ausnahme werfen - und vor allem keine
|
||||
erfundene Restzeit zeigen."""
|
||||
(tmp_path / "autooff.json").write_text("{kein json")
|
||||
assert display._read_autooff_minutes(tmp_path) is None
|
||||
|
||||
(tmp_path / "autooff.json").write_text(json.dumps({"minutes": 30}))
|
||||
assert display._read_autooff_minutes(tmp_path) is None
|
||||
|
||||
(tmp_path / "autooff.json").write_text(json.dumps({"deadline": "gleich"}))
|
||||
assert display._read_autooff_minutes(tmp_path) is None
|
||||
|
||||
|
||||
def test_autooff_remaining_time_rounds_up(tmp_path):
|
||||
(tmp_path / "autooff.json").write_text(json.dumps({"deadline": 1000.0}))
|
||||
assert display._read_autooff_minutes(tmp_path, now=1000.0 - 600) == 10
|
||||
assert display._read_autooff_minutes(tmp_path, now=1000.0 - 601) == 11
|
||||
assert display._read_autooff_minutes(tmp_path, now=1000.0 - 1) == 1
|
||||
assert display._read_autooff_minutes(tmp_path, now=1000.0) == 0
|
||||
assert display._read_autooff_minutes(tmp_path, now=1000.0 + 90) == 0
|
||||
|
||||
|
||||
def test_a_switched_off_station_shows_no_timer(tmp_path, monkeypatch):
|
||||
""""off" raeumt die Zustandsdatei ab - aber selbst wenn sie durch einen Absturz
|
||||
liegen bliebe, darf neben "Station aus" keine Restzeit stehen."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: False)
|
||||
(tmp_path / "autooff.json").write_text(json.dumps(
|
||||
{"minutes": 30, "deadline": time.time() + 600, "deferrals": 0}))
|
||||
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||
service_name="x")
|
||||
|
||||
assert state == {"mode": "off"}
|
||||
assert not any("Auto-Aus" in line for line in render.lines(state))
|
||||
|
||||
|
||||
# --- display.build_state() (Fixrunde 1, I3) ---------------------------------------
|
||||
#
|
||||
# Dieselbe Unterscheidung, die im CLI (tools/station/ninlight-station, status_cmd) zweimal
|
||||
|
||||
Reference in New Issue
Block a user