feat(station): Umschalter fuer Migrationsimages mit/ohne WLAN-Daten
Die WLAN-Zugangsdaten stecken zur Bauzeit in der Firmware, es gibt also zwei Abzuege je Ziel. Die Station haelt jetzt beide vor - images/ migrate-nocreds (aus tools/build_nocreds.sh, Lampe startet im Onboarding-AP) und images/migrate-wlan (aus build/, Lampe haengt sofort im eigenen WLAN) - und der Bediener waehlt per Umschalter, welcher ausgeliefert wird: "ninlight-station bildsatz" oder der Menueeintrag am Taster. Der Zustand liegt in /var/lib/ninlight-station/imageset, ueberlebt Neustarts und steht dauerhaft in der Fusszeile des Panels - in jedem Modus, auch bei ausgeschalteter Station: man soll vor dem Einschalten sehen, was gilt. Vorgabe und Rueckfallwert in JEDEM Zweifelsfall ist "ohne-wlan". Im Automatikmodus wird jede Klyqa-Lampe in Reichweite migriert, auch fremde; ein Abzug ohne Zugangsdaten ist dann ein harmloser Fehlgriff, einer mit Zugangsdaten traegt das eigene WLAN-Passwort in ein fremdes Geraet. Fehlt der gewaehlte Satz (Verzeichnis weg oder leer), wird NICHTS ausgeliefert - kein stilles Ausweichen auf den anderen Satz. Der Server antwortet 404 mit Status "noimageset", CLI und Panel sagen "FEHLT". server.py und die Stations-Unit muessen mit: die Auswahl der ausgelieferten Datei ist genau die Aufgabe des Servers, und ein Symlink-Tausch haette "gewaehlt, aber nicht vorhanden" von "dieses Modell fehlt" nicht mehr unterscheidbar gemacht.
This commit is contained in:
@@ -26,10 +26,14 @@ import button # noqa: E402
|
||||
|
||||
# --- Zustaende, von denen die Menueeintraege abhaengen -------------------------------
|
||||
|
||||
STATE_OFF = {"running": False, "mode": "recovery", "target": None}
|
||||
STATE_RECOVERY = {"running": True, "mode": "recovery", "target": None}
|
||||
STATE_MIGRATION = {"running": True, "mode": "migration", "target": "gu10_rgb"}
|
||||
STATE_MIGRATION_BLIND = {"running": True, "mode": "migration", "target": None}
|
||||
STATE_OFF = {"running": False, "mode": "recovery", "target": None,
|
||||
"imageset": "ohne-wlan"}
|
||||
STATE_RECOVERY = {"running": True, "mode": "recovery", "target": None,
|
||||
"imageset": "ohne-wlan"}
|
||||
STATE_MIGRATION = {"running": True, "mode": "migration", "target": "gu10_rgb",
|
||||
"imageset": "ohne-wlan"}
|
||||
STATE_MIGRATION_BLIND = {"running": True, "mode": "migration", "target": None,
|
||||
"imageset": "ohne-wlan"}
|
||||
|
||||
|
||||
# --- Fake-Uhr, Taster-Doppel, Protokoll ---------------------------------------------
|
||||
@@ -164,7 +168,8 @@ def _labels(state):
|
||||
|
||||
def test_menu_off_offers_starting_and_shutdown():
|
||||
assert _labels(STATE_OFF) == ["Rettung starten", "Migration: horchen",
|
||||
"Migration: AUTOMATIK", "Herunterfahren"]
|
||||
"Migration: AUTOMATIK", "Umschalten: mit WLAN",
|
||||
"Herunterfahren"]
|
||||
|
||||
|
||||
def test_the_listening_migration_comes_before_the_automatic_one():
|
||||
@@ -175,21 +180,22 @@ def test_the_listening_migration_comes_before_the_automatic_one():
|
||||
|
||||
|
||||
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
|
||||
assert _labels(STATE_RECOVERY) == ["Station aus", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_RECOVERY) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
|
||||
|
||||
def test_menu_in_migration_offers_the_detected_target():
|
||||
assert _labels(STATE_MIGRATION) == ["Freigabe: gu10_rgb", "Station aus",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
"Umschalten: mit WLAN", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
|
||||
|
||||
def test_menu_in_migration_without_a_detected_target_offers_no_release():
|
||||
"""Ohne last_seen gibt es nichts freizugeben - der Eintrag faellt ganz weg, statt
|
||||
eine Freigabe ins Leere anzubieten (die das CLI ohnehin abweisen wuerde)."""
|
||||
assert "Freigabe: " not in " ".join(_labels(STATE_MIGRATION_BLIND))
|
||||
assert _labels(STATE_MIGRATION_BLIND) == ["Station aus", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_MIGRATION_BLIND) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
|
||||
|
||||
def test_only_the_refresh_entry_skips_the_confirmation():
|
||||
@@ -231,7 +237,8 @@ def test_a_short_press_steps_to_the_next_entry():
|
||||
|
||||
|
||||
def test_a_short_press_wraps_around_the_last_entry():
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0), _short(5.0)], 7.0)
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0), _short(5.0),
|
||||
_short(6.0)], 8.0)
|
||||
assert rec.views()[-1][1]["hero"] == "> Rettung starten"
|
||||
|
||||
|
||||
@@ -257,7 +264,8 @@ def test_a_short_press_in_the_confirmation_cancels_and_returns_to_the_menu():
|
||||
|
||||
|
||||
def test_the_refresh_entry_runs_without_a_confirmation():
|
||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 7.0, state=STATE_RECOVERY)
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 8.0,
|
||||
state=STATE_RECOVERY)
|
||||
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "display", "test"]]
|
||||
assert "confirm" not in rec.kinds()
|
||||
|
||||
@@ -342,8 +350,8 @@ def test_a_blocking_action_does_not_break_the_hold_gesture():
|
||||
|
||||
|
||||
def test_the_waiting_screen_is_written_before_the_slow_call():
|
||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 8.0, state=STATE_RECOVERY,
|
||||
action_duration_s=4.0)
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 9.0,
|
||||
state=STATE_RECOVERY, action_duration_s=4.0)
|
||||
kinds = [k for _t, k, _p in rec.events]
|
||||
run_at = kinds.index("run")
|
||||
assert rec.events[run_at - 1][2].get("view") == "busy"
|
||||
@@ -493,3 +501,53 @@ def test_an_unreadable_station_state_keeps_the_menu_closed(caplog):
|
||||
ui.update(0.0, True)
|
||||
assert ui.update(0.2, False) is None
|
||||
assert ui.view(0.3) == {"view": "idle", "active": False}
|
||||
|
||||
|
||||
# --- Bildsatz-Umschalter im Menue ----------------------------------------------------
|
||||
|
||||
|
||||
def test_the_image_set_entry_names_where_it_switches_to():
|
||||
"""Waehrend das Menue offen ist, ersetzt es das Statusbild samt Fusszeile - der
|
||||
Eintrag muss deshalb das ZIEL nennen, nicht den jetzigen Zustand."""
|
||||
assert "Umschalten: mit WLAN" in _labels(STATE_OFF)
|
||||
ohne = dict(STATE_OFF, imageset="mit-wlan")
|
||||
assert "Umschalten: ohne WLAN" in _labels(ohne)
|
||||
assert "Umschalten: mit WLAN" not in _labels(ohne)
|
||||
|
||||
|
||||
def test_the_image_set_entry_switches_through_the_cli():
|
||||
actions = button.CliActions(cli="/usr/local/bin/ninlight-station")
|
||||
assert actions.argv_for(button.action_imageset("ohne-wlan")) == [
|
||||
"/usr/local/bin/ninlight-station", "bildsatz", "mit-wlan"]
|
||||
assert actions.argv_for(button.action_imageset("mit-wlan")) == [
|
||||
"/usr/local/bin/ninlight-station", "bildsatz", "ohne-wlan"]
|
||||
|
||||
|
||||
def test_switching_to_wifi_credentials_asks_louder_than_switching_back():
|
||||
loud = button.action_imageset("ohne-wlan")
|
||||
back = button.action_imageset("mit-wlan")
|
||||
assert loud.confirm and back.confirm
|
||||
assert loud.confirm_title == "ACHTUNG"
|
||||
assert back.confirm_title == "RUECKFRAGE"
|
||||
assert "WLAN-Passwort" in " ".join(loud.confirm_details)
|
||||
assert back.confirm_details is None
|
||||
|
||||
|
||||
def test_the_image_set_entry_really_runs_the_switch(tmp_path):
|
||||
"""Durch die echte Hauptschleife, nicht nur ueber menu_actions(): vierter Eintrag im
|
||||
ausgeschalteten Zustand, Auswahl halten, Rueckfrage halten."""
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0),
|
||||
(5.0, 7.0), (8.0, 10.0)], 12.0)
|
||||
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "bildsatz", "mit-wlan"]]
|
||||
|
||||
|
||||
def test_station_state_reads_the_same_switch_file_as_the_cli(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(button, "_service_active", lambda service_name: False)
|
||||
switch = tmp_path / "imageset"
|
||||
state = lambda: button.station_state(run_dir=tmp_path, service_name="x",
|
||||
imageset_path=switch)
|
||||
assert state()["imageset"] == "ohne-wlan" # Datei fehlt -> sichere Vorgabe
|
||||
switch.write_text("mit-wlan\n")
|
||||
assert state()["imageset"] == "mit-wlan"
|
||||
switch.write_text("quatsch")
|
||||
assert state()["imageset"] == "ohne-wlan"
|
||||
|
||||
+142
-16
@@ -217,8 +217,12 @@ EOF
|
||||
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"
|
||||
mkdir -p "$sandbox/run" "$sandbox/images/migrate-nocreds" "$sandbox/images/migrate-wlan" \
|
||||
"$sandbox/bin" "$sandbox/nmstate" "$sandbox/lib/display" "$sandbox/var"
|
||||
# Je ein Image je Bildsatz - "vorhanden" heisst fuer CLI, Server und Anzeige
|
||||
# gleichermassen "mindestens ein Image darin".
|
||||
echo "ohne" >"$sandbox/images/migrate-nocreds/foo_dev.img"
|
||||
echo "mit" >"$sandbox/images/migrate-wlan/foo_dev.img"
|
||||
echo "dummy" >"$sandbox/images/ninlight_foo_ota.img"
|
||||
|
||||
# NINLIGHT_STATION_LIB zeigt auf diese Sandbox statt auf tools/station selbst.
|
||||
@@ -292,6 +296,7 @@ run_cli() {
|
||||
NINLIGHT_STATION_CONF="$sandbox/station.conf" \
|
||||
NINLIGHT_RUN_DIR="$sandbox/run" \
|
||||
NINLIGHT_STATION_LIB="$sandbox/lib" \
|
||||
NINLIGHT_IMAGESET_FILE="$sandbox/var/imageset" \
|
||||
"$CLI" "$@"
|
||||
}
|
||||
|
||||
@@ -334,7 +339,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" on >/dev/null 2>&1 # Recovery vorher an
|
||||
: >"$SBX/cmd.log"
|
||||
set +e
|
||||
@@ -356,7 +361,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" on >/dev/null 2>&1
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
||||
@@ -369,7 +374,8 @@ echo
|
||||
echo "=== Fall 5: 'migrate on' ohne Migrationsimages -> Warnung auf stderr, Rueckgabewert 0 ==="
|
||||
SBX="$TMPROOT/t5"
|
||||
setup_sandbox "$SBX"
|
||||
# images/migrate/ bleibt leer
|
||||
# der gewaehlte Bildsatz ("ohne-wlan") wird geleert - setup_sandbox bestueckt beide
|
||||
rm -f "$SBX/images/migrate-nocreds"/*.img
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
||||
RC=$?
|
||||
@@ -468,7 +474,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" migrate on >/dev/null 2>&1 # phase=auto in control.json, wie es der echte Ablauf waere
|
||||
# status.json so, wie server.py sie beim Start geschrieben haette (mode/last_seen).
|
||||
cat >"$SBX/run/status.json" <<'JSON'
|
||||
@@ -513,7 +519,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" on >/dev/null 2>&1
|
||||
ACTIVE_BEFORE=$(cat "$SBX/nmstate/active")
|
||||
echo "$ACTIVE_BEFORE" | grep -qx "ninlight-recovery" && pass "Vorbedingung: ninlight-recovery laeuft" || fail "Vorbedingung nicht erfuellt: $ACTIVE_BEFORE"
|
||||
@@ -538,7 +544,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" migrate on --listen 2>"$SBX/stderr.log")
|
||||
RC=$?
|
||||
@@ -555,7 +561,7 @@ ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
||||
# 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"
|
||||
echo "dummy" >"$SBX2/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
set +e
|
||||
OUT2=$(run_cli "$SBX2" migrate on --listen 2>&1)
|
||||
RC2=$?
|
||||
@@ -567,7 +573,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
||||
# status.json wird nur beim Dienststart/bei einer Auslieferung geschrieben - hier
|
||||
# simuliert, wie es nach dem Start aussehen wuerde (Phase "listen", wie beim Start).
|
||||
@@ -604,7 +610,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
set +e
|
||||
OUT=$(FAKE_NMCLI_DEVSHOW=fremdes-profil run_cli "$SBX" migrate on 2>"$SBX/stderr.log")
|
||||
RC=$?
|
||||
@@ -619,7 +625,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
||||
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1 # control.json bleibt mit phase=armed liegen
|
||||
run_cli "$SBX" on >/dev/null 2>&1 # zurueck in den Rettungsmodus - control.json wird bewusst NICHT aufgeraeumt
|
||||
@@ -638,7 +644,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" migrate on --listen >/dev/null 2>&1
|
||||
run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1
|
||||
run_cli "$SBX" on >/dev/null 2>&1
|
||||
@@ -819,7 +825,7 @@ echo "=== Fall D4 (Befund Task 7f): 'off' fasst den Anzeigedienst nicht mehr an
|
||||
# naechsten Pi-Neustart dunkel. Jetzt laesst 'off' den Dienst laufen.
|
||||
SBX="$TMPROOT/tD4"
|
||||
setup_sandbox "$SBX"
|
||||
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
run_cli "$SBX" migrate on >/dev/null 2>&1
|
||||
: >"$SBX/cmd.log"
|
||||
set +e
|
||||
@@ -997,7 +1003,7 @@ 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"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" migrate on --listen --for 5 2>&1)
|
||||
RC=$?
|
||||
@@ -1008,7 +1014,7 @@ set -e
|
||||
# Umgekehrte Reihenfolge der Schalter muss genauso gehen.
|
||||
SBX="$TMPROOT/tZ4b"
|
||||
setup_sandbox "$SBX"
|
||||
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
|
||||
echo "dummy" >"$SBX/images/migrate-nocreds/gu10_rgb_prod.img"
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" migrate on --for 5 --listen 2>&1)
|
||||
RC=$?
|
||||
@@ -1182,6 +1188,126 @@ echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "kein
|
||||
[ ! -e "$SBX/run/autooff.json" ] && pass "keine Zustandsdatei, die einen Zeitschalter vortaeuscht" || fail "autooff.json trotz gescheitertem Timer: $(cat "$SBX/run/autooff.json")"
|
||||
echo "$ERR" | grep -q "off" && pass "die Meldung sagt, dass die Station AN bleibt" || fail "die Meldung verschweigt den Zustand der Station: $ERR"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz A: die Vorgabe ohne Umschalterdatei ist der Satz OHNE WLAN-Zugangsdaten ==="
|
||||
# Die sichere Richtung: ein Fehlgriff kostet einen Onboarding-Durchgang, kein
|
||||
# WLAN-Passwort in einer fremden Lampe.
|
||||
SBX="$TMPROOT/bs1"
|
||||
setup_sandbox "$SBX"
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
||||
echo "$OUT" | grep -q "ohne-wlan" && pass "ohne Datei gilt 'ohne-wlan'" || fail "Vorgabe ist nicht 'ohne-wlan': $OUT"
|
||||
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das zugehoerige Verzeichnis wird genannt" || fail "Verzeichnis fehlt in der Ausgabe: $OUT"
|
||||
[ ! -e "$SBX/var/imageset" ] && pass "das blosse Berichten legt keine Datei an" || fail "'bildsatz' ohne Argument hat geschrieben"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz B: umschalten schreibt die Datei atomar mit 0644 und bleibt stehen ==="
|
||||
SBX="$TMPROOT/bs2"
|
||||
setup_sandbox "$SBX"
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" bildsatz mit-wlan 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
||||
[ "$(cat "$SBX/var/imageset" 2>/dev/null | tr -d '\n')" = "mit-wlan" ] && pass "die Datei enthaelt 'mit-wlan'" || fail "Dateiinhalt: $(cat "$SBX/var/imageset" 2>&1)"
|
||||
MODE=$(file_mode "$SBX/var/imageset")
|
||||
[ "$MODE" = "644" ] && pass "Rechte 0644 (der Anzeigedienst muss lesen koennen)" || fail "Rechte $MODE statt 644"
|
||||
[ -z "$(find "$SBX/var" -name '.imageset-*' 2>/dev/null)" ] && pass "keine Temp-Datei liegengeblieben" || fail "Temp-Datei liegt noch da"
|
||||
echo "$OUT" | grep -qi "achtung" && pass "der Wechsel zu 'mit-wlan' warnt" || fail "keine Warnung beim Wechsel zu mit-wlan: $OUT"
|
||||
# ... und er BLEIBT stehen: ein zweiter, unabhaengiger Aufruf sieht ihn.
|
||||
OUT2=$(run_cli "$SBX" bildsatz 2>&1)
|
||||
echo "$OUT2" | grep -q "mit-wlan" && pass "der Zustand bleibt ueber Aufrufe hinweg" || fail "Zustand nicht gespeichert: $OUT2"
|
||||
# ... und zurueck.
|
||||
run_cli "$SBX" bildsatz ohne-wlan >/dev/null 2>&1
|
||||
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "ohne-wlan" ] && pass "zurueckschalten geht auch" || fail "zurueckschalten hat nicht gewirkt"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz C: ein unbekannter Name schaltet nichts um ==="
|
||||
SBX="$TMPROOT/bs3"
|
||||
setup_sandbox "$SBX"
|
||||
printf 'mit-wlan\n' >"$SBX/var/imageset"
|
||||
set +e
|
||||
ERR=$(run_cli "$SBX" bildsatz irgendwas 2>&1 >/dev/null)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 fuer einen unbekannten Bildsatz"
|
||||
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "mit-wlan" ] && pass "der bestehende Zustand blieb unveraendert" || fail "der Zustand wurde veraendert: $(cat "$SBX/var/imageset")"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz D: ein kaputter Dateiinhalt faellt auf die SICHERE Vorgabe zurueck ==="
|
||||
# Nie auf den teureren Satz - das ist die eine Richtung, die kein Zweifelsfall nehmen darf.
|
||||
SBX="$TMPROOT/bs4"
|
||||
setup_sandbox "$SBX"
|
||||
printf 'voellig-anderes\n' >"$SBX/var/imageset"
|
||||
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
||||
echo "$OUT" | grep -q "ohne-wlan" && pass "kaputter Inhalt -> 'ohne-wlan'" || fail "kaputter Inhalt fuehrte nicht auf die Vorgabe: $OUT"
|
||||
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das Verzeichnis des sicheren Satzes" || fail "falsches Verzeichnis: $OUT"
|
||||
# Der Bediener muss erfahren, dass sein Eintrag NICHT gewirkt hat - sonst haelt er die
|
||||
# Station fuer umgeschaltet.
|
||||
echo "$OUT" | grep -q "unbekannte Einstellung 'voellig-anderes'" \
|
||||
&& pass "die unbekannte Einstellung wird beim Namen genannt" \
|
||||
|| fail "die unbekannte Einstellung wird verschwiegen: $OUT"
|
||||
# Gegenprobe: bei einem GUELTIGEN Eintrag darf diese Zeile nicht erscheinen.
|
||||
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
||||
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
||||
echo "$OUT" | grep -q "unbekannte Einstellung" \
|
||||
&& fail "meldet 'unbekannte Einstellung' fuer einen gueltigen Eintrag: $OUT" \
|
||||
|| pass "kein Fehlalarm bei gueltigem Eintrag"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz E: ein gewaehlter, aber fehlender Satz wird beim Umschalten UND im Status gemeldet ==="
|
||||
SBX="$TMPROOT/bs5"
|
||||
setup_sandbox "$SBX"
|
||||
rm -rf "$SBX/images/migrate-wlan"
|
||||
set +e
|
||||
ERR=$(run_cli "$SBX" bildsatz mit-wlan 2>&1 >/dev/null)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Umschalten gelingt trotzdem (der Zustand ist gueltig, nur die Bestueckung fehlt)" || fail "Exit-Code $RC"
|
||||
echo "$ERR" | grep -qi "warnung" && pass "Warnung beim Umschalten" || fail "keine Warnung: $ERR"
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -q "FEHLT" && pass "'status' sagt, dass nichts ausgeliefert wird" || fail "'status' verschweigt den fehlenden Satz: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz F: ein LEERES Verzeichnis zaehlt genauso als fehlend ==="
|
||||
SBX="$TMPROOT/bs6"
|
||||
setup_sandbox "$SBX"
|
||||
rm -f "$SBX/images/migrate-nocreds"/*.img
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -q "FEHLT" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz G: 'migrate on' nennt den geltenden Bildsatz in der Einschaltmeldung ==="
|
||||
# Er kann von vorletzter Woche stammen - wer einschaltet, muss ihn sehen.
|
||||
SBX="$TMPROOT/bs7"
|
||||
setup_sandbox "$SBX"
|
||||
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
||||
set +e
|
||||
OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
||||
echo "$OUT" | grep -q "Bildsatz: mit-wlan" && pass "die Einschaltmeldung nennt den Bildsatz" || fail "Bildsatz fehlt in der Einschaltmeldung: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz H: die Warnung 'keine Migrationsimages' meint den GEWAEHLTEN Satz ==="
|
||||
SBX="$TMPROOT/bs8"
|
||||
setup_sandbox "$SBX"
|
||||
rm -f "$SBX/images/migrate-wlan"/*.img # nur der andere Satz ist leer
|
||||
set +e
|
||||
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
|
||||
set -e
|
||||
echo "$ERR" | grep -q "keine Migrationsimages" && fail "warnt, obwohl der GEWAEHLTE Satz ('ohne-wlan') bestueckt ist: $ERR" || pass "kein Fehlalarm - gezaehlt wird im gewaehlten Satz"
|
||||
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
||||
set +e
|
||||
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
|
||||
set -e
|
||||
echo "$ERR" | grep -q "keine Migrationsimages" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR"
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "Alle Tests bestanden."
|
||||
|
||||
@@ -504,8 +504,8 @@ RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
|
||||
GZ_SIZE=$(wc -c <"$SANDBOX/build/foo/ninlight_foo_gz.img" | tr -d ' ')
|
||||
PROD_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img"
|
||||
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img"
|
||||
PROD_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
|
||||
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
|
||||
if [ -f "$PROD_IMG" ] && [ -f "$DEV_IMG" ]; then
|
||||
pass "beide Migrationsimages (prod, dev) wurden angelegt"
|
||||
else
|
||||
@@ -535,7 +535,7 @@ set +e
|
||||
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json"
|
||||
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
|
||||
if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then
|
||||
pass "manifest.json wurde angelegt"
|
||||
else
|
||||
@@ -592,8 +592,8 @@ if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ]; the
|
||||
else
|
||||
fail "Rettungsimage fehlt: $OUT"
|
||||
fi
|
||||
if [ ! -d "$SANDBOX_STATE/opt/ninlight-station/images/migrate" ] \
|
||||
|| [ -z "$(ls -A "$SANDBOX_STATE/opt/ninlight-station/images/migrate" 2>/dev/null)" ]; then
|
||||
if [ ! -d "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan" ] \
|
||||
|| [ -z "$(ls -A "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan" 2>/dev/null)" ]; then
|
||||
pass "keine Migrationsimages entstanden"
|
||||
else
|
||||
fail "Migrationsimages entstanden, obwohl kein Schluesselverzeichnis vorhanden war"
|
||||
@@ -618,12 +618,12 @@ OUT=$(TEST_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test foo 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" ]; then
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
|
||||
pass "dev-Migrationsimage wurde angelegt"
|
||||
else
|
||||
fail "dev-Migrationsimage fehlt: $OUT"
|
||||
fi
|
||||
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" ]; then
|
||||
if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" ]; then
|
||||
pass "prod-Migrationsimage entfiel wie erwartet"
|
||||
else
|
||||
fail "prod-Migrationsimage wurde trotz fehlendem Produktionsschluessel angelegt"
|
||||
@@ -644,15 +644,15 @@ make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" A
|
||||
make_gz_file "$SANDBOX/build/bar/ninlight_bar_gz.img" C
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
echo "bar-ota" >"$SANDBOX/build/bar/ninlight_bar_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
|
||||
echo "vorhandenes-bar-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_prod.img"
|
||||
echo "vorhandenes-bar-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_dev.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
|
||||
echo "vorhandenes-bar-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_prod.img"
|
||||
echo "vorhandenes-bar-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_dev.img"
|
||||
# Manifest eines frueheren Deploys, das bar beschrieben hat - genau das darf ein
|
||||
# gezielter Deploy von foo nicht einfach wegschreiben (Fixrunde 1, I1). bar traegt
|
||||
# absichtlich eine ANDERE Firmware-Version als das, was dieser Lauf fuer foo signiert
|
||||
# (nl_mkimage-Stub liefert immer "1.0.0") - das Manifest nach dem Merge muss beide
|
||||
# Versionen unveraendert nebeneinander stehen lassen (Fixrunde 2, N2).
|
||||
cat >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json" <<'JSON'
|
||||
cat >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json" <<'JSON'
|
||||
{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.9.0",
|
||||
"images": [
|
||||
{"target": "bar", "variant": "prod", "file": "bar_prod.img", "bytes": 20, "sha256": "x",
|
||||
@@ -667,14 +667,14 @@ OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" ] \
|
||||
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" ]; then
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" ] \
|
||||
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
|
||||
pass "Migrationsimages fuer das angegebene Ziel (foo) wurden angelegt"
|
||||
else
|
||||
fail "Migrationsimages fuer foo fehlen: $OUT"
|
||||
fi
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_prod.img" 2>/dev/null)" = "vorhandenes-bar-prod" ] \
|
||||
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_dev.img" 2>/dev/null)" = "vorhandenes-bar-dev" ]; then
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_prod.img" 2>/dev/null)" = "vorhandenes-bar-prod" ] \
|
||||
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/bar_dev.img" 2>/dev/null)" = "vorhandenes-bar-dev" ]; then
|
||||
pass "Migrationsimages des NICHT ausgewaehlten Geraetetyps (bar) blieben unangetastet"
|
||||
else
|
||||
fail "Migrationsimages von bar wurden veraendert oder geloescht - 4a-Fehler fuer migrate/"
|
||||
@@ -682,7 +682,7 @@ fi
|
||||
set +e
|
||||
MANIFEST_CHECK=$(python3 -c "
|
||||
import json
|
||||
with open('$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json') as fh:
|
||||
with open('$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json') as fh:
|
||||
data = json.load(fh)
|
||||
entries = data.get('images', [])
|
||||
keys = {(e.get('target'), e.get('variant')) for e in entries}
|
||||
@@ -768,7 +768,7 @@ OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
|
||||
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img"
|
||||
DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
|
||||
if [ -f "$DEV_IMG" ]; then
|
||||
set +e
|
||||
python3 -c "
|
||||
@@ -797,22 +797,22 @@ setup_sandbox "$SANDBOX"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo"
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
|
||||
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img"
|
||||
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
|
||||
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
|
||||
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
|
||||
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ]; then
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
|
||||
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ]; then
|
||||
pass "vorhandenes migrate/ blieb beim Voll-Deploy ohne Schluesselverzeichnis unangetastet"
|
||||
else
|
||||
fail "migrate/ wurde geleert, obwohl kein Schluesselverzeichnis vorhanden war - zerstoert die Bestueckung"
|
||||
fi
|
||||
if echo "$OUT" | grep -q "migrate/ auf dem Pi bleibt unangetastet"; then
|
||||
if echo "$OUT" | grep -q "migrate-wlan/ auf dem Pi bleibt unangetastet"; then
|
||||
pass "Ausgabe kuendigt an, dass migrate/ unangetastet bleibt"
|
||||
else
|
||||
fail "Ausgabe erwaehnt nicht, dass migrate/ unangetastet bleibt: $OUT"
|
||||
@@ -831,11 +831,11 @@ setup_sandbox "$SANDBOX"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo"
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
|
||||
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img"
|
||||
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
|
||||
echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
|
||||
echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
|
||||
echo '{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.0.1", "images": []}' \
|
||||
>"$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json"
|
||||
>"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(TEST_SIGN_DIR="$SIGN_EMPTY" run_deploy "$SANDBOX" pi-test 2>&1)
|
||||
@@ -847,9 +847,9 @@ if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ]; the
|
||||
else
|
||||
fail "Rettungsimage fehlt: $OUT"
|
||||
fi
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
|
||||
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ] \
|
||||
&& grep -q '"images": \[\]' "$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json"; then
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \
|
||||
&& [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ] \
|
||||
&& grep -q '"images": \[\]' "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"; then
|
||||
pass "vorhandenes migrate/ (Bilder UND manifest.json) blieb unangetastet - Verzeichnis war da, aber leer"
|
||||
else
|
||||
fail "migrate/ wurde trotz vorhandenem, aber leerem Schluesselverzeichnis veraendert/geleert - C2 nicht behoben"
|
||||
@@ -868,20 +868,20 @@ setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo"
|
||||
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
|
||||
echo "echtes-altes-prod-image" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
|
||||
echo "echtes-altes-prod-image" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(TEST_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test 2>&1) # Voll-Deploy, kein Ziel-Argument
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" ]; then
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
|
||||
pass "neues dev-Migrationsimage wurde signiert und ausgeliefert"
|
||||
else
|
||||
fail "dev-Migrationsimage fehlt: $OUT"
|
||||
fi
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" 2>/dev/null)" = "echtes-altes-prod-image" ]; then
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_prod.img" 2>/dev/null)" = "echtes-altes-prod-image" ]; then
|
||||
pass "das vorhandene _prod.img blieb unangetastet, obwohl dieser Voll-Deploy nur den dev-Schluessel hatte"
|
||||
else
|
||||
fail "das vorhandene _prod.img wurde beim Voll-Deploy mit nur dev-Schluessel entfernt - C2 (Teilfall) nicht behoben"
|
||||
@@ -907,12 +907,13 @@ set +e
|
||||
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json"
|
||||
MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
|
||||
if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then
|
||||
pass "manifest.json wurde angelegt"
|
||||
else
|
||||
fail "manifest.json fehlt oder Exit-Code $RC: $OUT"
|
||||
fi
|
||||
printf 'mit-wlan\n' >"$TMPROOT/s20-imageset"
|
||||
set +e
|
||||
VERSION=$(python3 -c "
|
||||
import importlib.util
|
||||
@@ -929,6 +930,9 @@ st = module.Station(
|
||||
stall_after=None,
|
||||
mode='migration',
|
||||
control_path=Path('$TMPROOT/s20-control.json'),
|
||||
# Das Manifest dieses Szenarios gehoert zum Satz MIT WLAN-Zugangsdaten - der
|
||||
# Umschalter muss also darauf stehen, sonst liest der Server im anderen Satz nach.
|
||||
imageset_path=Path('$TMPROOT/s20-imageset'),
|
||||
)
|
||||
print(st.image_version('foo', 'dev'))
|
||||
" 2>&1)
|
||||
@@ -965,8 +969,8 @@ for label in truncated empty array null string null-entry; do
|
||||
mkdir -p "$SANDBOX/build/foo"
|
||||
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
|
||||
cp "$TMPROOT/bad-manifests/$label.json" "$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan"
|
||||
cp "$TMPROOT/bad-manifests/$label.json" "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/manifest.json"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
# Nur EIN Schluessel (dev) -> MIGRATE_COMPLETE=0 -> der Merge-Zweig laeuft, nicht der
|
||||
@@ -980,7 +984,7 @@ for label in truncated empty array null string null-entry; do
|
||||
fail "Bestandsmanifest '$label': Exit-Code $RC - Deploy abgebrochen. Ausgabe: $OUT"
|
||||
fi
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] \
|
||||
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" ]; then
|
||||
&& [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ]; then
|
||||
pass "Bestandsmanifest '$label': Deploy lief vollstaendig durch (server.py installiert, foo_dev.img da)"
|
||||
else
|
||||
fail "Bestandsmanifest '$label': Deploy blieb auf halbem Weg stehen (server.py oder foo_dev.img fehlt)"
|
||||
@@ -1112,6 +1116,130 @@ ERR=$(cat "$TMPROOT/s27.err")
|
||||
echo "$ERR" | grep -q "Warnung" && pass "Warnung (keine Fehlermeldung) auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
||||
[ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] && pass "der Deploy selbst lief vollstaendig durch" || fail "server.py fehlt - der Deploy brach ab"
|
||||
|
||||
echo
|
||||
echo "=== Szenario 28: beide Bildsaetze werden ausgeliefert, und zwar aus verschiedenen Bauten ==="
|
||||
# Der Umschalter auf der Station ist nur so gut wie die Bestueckung: liegen beide Saetze
|
||||
# nicht getrennt auf dem Pi, kann er nichts umschalten. Geprueft wird am INHALT, nicht am
|
||||
# Verzeichnisnamen - die Fuellzeichen der beiden _gz.img sind verschieden, ein
|
||||
# vertauschter Quellbaum faellt also auf.
|
||||
SANDBOX="$TMPROOT/s28"; STUBBIN="$TMPROOT/bin28"; LOG="$TMPROOT/s28.log"; SANDBOX_STATE="$TMPROOT/s28-state"
|
||||
setup_sandbox "$SANDBOX"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo"
|
||||
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" W
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
||||
WLAN_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img"
|
||||
NOCR_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img"
|
||||
if [ -f "$WLAN_IMG" ] && [ -f "$NOCR_IMG" ]; then
|
||||
pass "beide Bildsaetze liegen getrennt auf dem Pi"
|
||||
else
|
||||
fail "mindestens ein Bildsatz fehlt: $OUT"
|
||||
fi
|
||||
if [ -f "$WLAN_IMG" ] && [ -f "$NOCR_IMG" ]; then
|
||||
# Die ersten Bytes sind das Fuellzeichen des jeweiligen _gz.img.
|
||||
W_HEAD=$(head -c 8 "$WLAN_IMG")
|
||||
N_HEAD=$(head -c 8 "$NOCR_IMG")
|
||||
[ "$W_HEAD" = "WWWWWWWW" ] && pass "migrate-wlan/ kommt wirklich aus build/" || fail "migrate-wlan/ enthaelt '$W_HEAD' statt der build/-Bytes"
|
||||
[ "$N_HEAD" = "NNNNNNNN" ] && pass "migrate-nocreds/ kommt wirklich aus build-nocreds/" || fail "migrate-nocreds/ enthaelt '$N_HEAD' statt der build-nocreds/-Bytes"
|
||||
fi
|
||||
for m in migrate-wlan migrate-nocreds; do
|
||||
if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/$m/manifest.json" ]; then
|
||||
pass "$m/manifest.json wurde angelegt"
|
||||
else
|
||||
fail "$m/manifest.json fehlt"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=== Szenario 29: ohne build-nocreds/ bleibt der Satz 'ohne-wlan' auf dem Pi unangetastet ==="
|
||||
# tools/build_nocreds.sh ist ein eigener, optionaler Bau. Ein Deploy darf nicht daran
|
||||
# scheitern, dass er nicht gelaufen ist - und er darf die Bestueckung frueherer Deploys
|
||||
# nicht wegraeumen (derselbe Fehler wie frueher bei migrate/, siehe Szenario 17).
|
||||
SANDBOX="$TMPROOT/s29"; STUBBIN="$TMPROOT/bin29"; LOG="$TMPROOT/s29.log"; SANDBOX_STATE="$TMPROOT/s29-state"
|
||||
setup_sandbox "$SANDBOX"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo"
|
||||
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds"
|
||||
echo "alter-nocreds-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlendem build-nocreds/" || fail "Exit-Code $RC: $OUT"
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" 2>/dev/null)" = "alter-nocreds-abzug" ]; then
|
||||
pass "vorhandener Satz 'ohne-wlan' blieb unangetastet"
|
||||
else
|
||||
fail "der Satz 'ohne-wlan' wurde geleert oder ueberschrieben, obwohl build-nocreds/ fehlte"
|
||||
fi
|
||||
[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ] \
|
||||
&& pass "der Satz 'mit-wlan' wurde trotzdem ausgeliefert" \
|
||||
|| fail "der Satz 'mit-wlan' fehlt: $OUT"
|
||||
echo "$OUT" | grep -q "build_nocreds.sh" \
|
||||
&& pass "der Hinweis nennt, womit der fehlende Satz entsteht" \
|
||||
|| fail "kein Hinweis auf tools/build_nocreds.sh: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Szenario 30: ein unvollstaendiger Satz 'ohne-wlan' raeumt nicht mit --delete auf ==="
|
||||
# build-nocreds/ hat nur eines von zwei Zielen - dieser Lauf beschreibt den Sollzustand
|
||||
# also NICHT vollstaendig, und --delete wuerde das zweite Ziel des vorigen Deploys
|
||||
# wegraeumen.
|
||||
SANDBOX="$TMPROOT/s30"; STUBBIN="$TMPROOT/bin30"; LOG="$TMPROOT/s30.log"; SANDBOX_STATE="$TMPROOT/s30-state"
|
||||
setup_sandbox "$SANDBOX"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar" "$SANDBOX/build-nocreds/foo"
|
||||
make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img"
|
||||
make_gz_file "$SANDBOX/build/bar/ninlight_bar_gz.img"
|
||||
echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
echo "bar-ota" >"$SANDBOX/build/bar/ninlight_bar_ota.img"
|
||||
make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds"
|
||||
echo "alter-bar-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/bar_dev.img"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
||||
if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/bar_dev.img" 2>/dev/null)" = "alter-bar-abzug" ]; then
|
||||
pass "das nicht neu gebaute Ziel blieb im Satz 'ohne-wlan' erhalten"
|
||||
else
|
||||
fail "das nicht neu gebaute Ziel wurde aus dem Satz 'ohne-wlan' entfernt - --delete lief trotz unvollstaendigem Satz"
|
||||
fi
|
||||
[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" ] \
|
||||
&& pass "das gebaute Ziel kam dazu" || fail "das gebaute Ziel fehlt im Satz 'ohne-wlan'"
|
||||
|
||||
echo
|
||||
echo "=== Szenario 31: das alte, namenlose migrate/ wird beim Deploy entfernt ==="
|
||||
# Bis zum Umschalter hiess das einzige Verzeichnis "migrate" und enthielt die Abzuege MIT
|
||||
# Zugangsdaten. Es wird von niemandem mehr gelesen - bliebe es liegen, haelt der naechste
|
||||
# Mensch am Pi es fuer den gueltigen Satz.
|
||||
SANDBOX="$TMPROOT/s31"; STUBBIN="$TMPROOT/bin31"; LOG="$TMPROOT/s31.log"; SANDBOX_STATE="$TMPROOT/s31-state"
|
||||
setup_sandbox "$SANDBOX"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
mkdir -p "$SANDBOX/build/foo"
|
||||
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
|
||||
mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate"
|
||||
echo "altbestand" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
||||
[ ! -e "$SANDBOX_STATE/opt/ninlight-station/images/migrate" ] \
|
||||
&& pass "der Altbestand images/migrate wurde entfernt" \
|
||||
|| fail "images/migrate liegt noch da"
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "Alle Tests bestanden."
|
||||
|
||||
@@ -861,9 +861,11 @@ def test_a_switched_off_station_shows_no_timer(tmp_path, monkeypatch):
|
||||
{"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")
|
||||
service_name="x",
|
||||
imageset_path=tmp_path / "imageset")
|
||||
|
||||
assert state == {"mode": "off"}
|
||||
assert state["mode"] == "off"
|
||||
assert "autooff_minutes" not in state
|
||||
assert not any("Auto-Aus" in line for line in render.lines(state))
|
||||
|
||||
|
||||
@@ -897,9 +899,13 @@ def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
||||
(tmp_path / "status.json").write_text(json.dumps({"mode": "migration", "served": 5}))
|
||||
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||
service_name="x")
|
||||
service_name="x",
|
||||
imageset_path=tmp_path / "imageset")
|
||||
|
||||
assert state == {"mode": "off"}
|
||||
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
||||
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll).
|
||||
assert state["mode"] == "off"
|
||||
assert set(state) == {"mode", "imageset", "imageset_ok"}
|
||||
|
||||
|
||||
def test_build_state_mode_comes_from_mode_file_not_status_json(tmp_path, monkeypatch):
|
||||
@@ -1707,3 +1713,155 @@ def test_the_expensive_station_state_is_still_only_read_every_five_seconds():
|
||||
|
||||
_drive_daemon(state_at, duration_s=10.0)
|
||||
assert calls == [0.0, 5.0, 10.0]
|
||||
|
||||
|
||||
# --- Bildsatz-Umschalter in der Fusszeile -------------------------------------------
|
||||
#
|
||||
# Zusage: welcher Migrations-Bildsatz gilt, steht DAUERHAFT auf dem Panel - nicht nur im
|
||||
# Menue. Der Zustand ueberlebt Neustarts und kann von vorletzter Woche stammen; wer ihn
|
||||
# erst beim Blaettern im Menue saehe, hat womoeglich schon zwanzig Lampen mit den eigenen
|
||||
# WLAN-Zugangsdaten geflasht.
|
||||
|
||||
def _flat(rows):
|
||||
"""_band_pixels() liefert Zeilen-Tupel - hier interessieren die einzelnen Pixel."""
|
||||
return [px for row in rows for px in row]
|
||||
|
||||
|
||||
def _imageset_line(state):
|
||||
texts = [text for text, role in render.blocks(state) if role == render.ROLE_FOOTER]
|
||||
assert len(texts) <= 1, texts
|
||||
return texts[0] if texts else None
|
||||
|
||||
|
||||
def test_the_image_set_is_shown_in_every_status_screen():
|
||||
for state in ({"mode": "off"},
|
||||
{"mode": "recovery", "ssid": "ninlight-recovery"},
|
||||
dict(_migration_state()),
|
||||
{"mode": None}):
|
||||
for name in ("ohne-wlan", "mit-wlan"):
|
||||
s = dict(state, imageset=name, imageset_ok=True)
|
||||
line = _imageset_line(s)
|
||||
assert line == render.IMAGESET_LABELS[name], (state.get("mode"), name)
|
||||
|
||||
|
||||
def test_the_two_image_sets_read_differently():
|
||||
"""Die beiden Zustaende muessen sich am Panel auf einen Blick unterscheiden - eine
|
||||
Fusszeile, die nur "Bildsatz gesetzt" sagt, waere wertlos."""
|
||||
assert render.IMAGESET_LABELS["ohne-wlan"] != render.IMAGESET_LABELS["mit-wlan"]
|
||||
assert "WLAN" in render.IMAGESET_LABELS["mit-wlan"]
|
||||
|
||||
|
||||
def test_a_missing_image_set_says_so_on_the_panel():
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=False)
|
||||
line = _imageset_line(state)
|
||||
assert line is not None and "FEHLT" in line
|
||||
|
||||
|
||||
def test_without_an_image_set_field_nothing_is_claimed():
|
||||
"""Ein Zustand aus einer aelteren display.py-Fassung bekommt keine erfundene
|
||||
Fusszeile."""
|
||||
assert _imageset_line({"mode": "off"}) is None
|
||||
|
||||
|
||||
def test_the_image_set_line_is_actually_drawn_at_the_bottom():
|
||||
"""Nicht nur in blocks(): auf dem Panel, unterhalb aller Beiwerkzeilen und ueber dem
|
||||
unteren Rand."""
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=True)
|
||||
footer_top = (render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX
|
||||
- render.FOOTER_BAND_HEIGHT_PX)
|
||||
|
||||
with_footer = render.compose(state)
|
||||
without = render.compose({k: v for k, v in state.items()
|
||||
if k not in ("imageset", "imageset_ok")})
|
||||
|
||||
bottom = render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX
|
||||
band = _band_pixels(with_footer, footer_top, bottom)
|
||||
assert 0 in _flat(band), "im Fussband ist gar keine Tinte"
|
||||
assert band != _band_pixels(without, footer_top, bottom), \
|
||||
"Fussband sieht mit und ohne Bildsatz gleich aus"
|
||||
|
||||
|
||||
def test_the_image_set_line_keeps_the_bottom_margin_free():
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=False)
|
||||
image = render.compose(state)
|
||||
margin = _flat(_band_pixels(image, render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX,
|
||||
render.PANEL_HEIGHT_PX))
|
||||
assert set(margin) <= {255}, "der untere Rand ist nicht mehr frei"
|
||||
|
||||
|
||||
def test_the_image_set_line_survives_the_fullest_state():
|
||||
"""Im vollsten Zustand faellt eine Beiwerkzeile heraus - aber nie die Fusszeile:
|
||||
sie steht an festem Platz und nicht im Zeilenfluss."""
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=True,
|
||||
autooff_minutes=17)
|
||||
assert _imageset_line(state) is not None
|
||||
# ... und der Zeilenfluss endet wirklich vor dem Fussband.
|
||||
image = render.compose(state)
|
||||
footer_top = (render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX
|
||||
- render.FOOTER_BAND_HEIGHT_PX)
|
||||
# Die Zeile direkt ueber dem Trennstrich darf ihn nicht beruehren.
|
||||
assert set(_flat(_band_pixels(image, footer_top - 1, footer_top))) <= {255}
|
||||
|
||||
|
||||
def test_build_state_reads_the_image_set_switch(tmp_path, monkeypatch):
|
||||
"""Dieselbe Datei, die das CLI schreibt und server.py liest - kein zweiter Weg."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
(tmp_path / "mode").write_text("migration")
|
||||
images = tmp_path / "images"
|
||||
(images / "migrate-nocreds").mkdir(parents=True)
|
||||
(images / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(images / "migrate-wlan").mkdir()
|
||||
(images / "migrate-wlan" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
conf = tmp_path / "station.conf"
|
||||
conf.write_text(f"MIGRATE_SSID='qconnex-firmware'\nIMAGE_DIR='{images}'\n")
|
||||
switch = tmp_path / "imageset"
|
||||
|
||||
def state():
|
||||
return display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)
|
||||
|
||||
# Ohne Datei: die sichere Vorgabe.
|
||||
assert state()["imageset"] == "ohne-wlan"
|
||||
switch.write_text("mit-wlan\n")
|
||||
assert state()["imageset"] == "mit-wlan"
|
||||
# Unbekannter Inhalt darf NIE auf den teureren Satz zeigen.
|
||||
switch.write_text("irgendwas")
|
||||
assert state()["imageset"] == "ohne-wlan"
|
||||
|
||||
|
||||
def test_build_state_reports_a_missing_image_set(tmp_path, monkeypatch):
|
||||
""""Vorhanden" heisst: Verzeichnis da UND mindestens ein Image darin - dieselbe Regel
|
||||
wie in server.py, sonst sagt das Panel "in Ordnung" und die Lampe bekommt nichts."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
images = tmp_path / "images"
|
||||
(images / "migrate-nocreds").mkdir(parents=True)
|
||||
(images / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
conf = tmp_path / "station.conf"
|
||||
conf.write_text(f"IMAGE_DIR='{images}'\n")
|
||||
switch = tmp_path / "imageset"
|
||||
|
||||
switch.write_text("ohne-wlan")
|
||||
assert display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)["imageset_ok"] is True
|
||||
switch.write_text("mit-wlan") # migrate-wlan/ gibt es nicht
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)
|
||||
assert state["imageset_ok"] is False
|
||||
assert "FEHLT" in _imageset_line(state)
|
||||
|
||||
# Und ein LEERES Verzeichnis zaehlt genauso als fehlend.
|
||||
(images / "migrate-wlan").mkdir()
|
||||
assert display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)["imageset_ok"] is False
|
||||
|
||||
|
||||
def test_the_display_and_the_server_agree_on_the_set_names():
|
||||
"""Zwei Prozesse, eine Wahrheit: waeren die Namen oder die Vorgabe verschieden,
|
||||
behauptete das Panel etwas anderes, als der Server tut."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"server_for_names", Path(__file__).resolve().parents[1] / "server.py")
|
||||
server = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(server)
|
||||
assert display.IMAGESET_DIRS == server.IMAGE_SET_DIRS
|
||||
assert display.IMAGESET_DEFAULT == server.DEFAULT_IMAGE_SET
|
||||
|
||||
@@ -494,12 +494,16 @@ class MigrationTestServer:
|
||||
control_dir = tmp_path / "control"
|
||||
control_dir.mkdir()
|
||||
self.control_path = control_dir / "control.json"
|
||||
# Der Umschalter des Bildsatzes. Die Datei existiert zunaechst NICHT - das ist
|
||||
# der Auslieferungszustand und muss "ohne-wlan" (migrate-nocreds/) bedeuten.
|
||||
self.imageset_path = tmp_path / "imageset"
|
||||
self.port = free_port()
|
||||
self.proc = subprocess.Popen(
|
||||
[sys.executable, str(SERVER), "--image-dir", str(self.image_dir),
|
||||
"--bind", "127.0.0.1", "--port", str(self.port),
|
||||
"--state", str(self.state_path), "--mode", mode,
|
||||
"--control", str(self.control_path)],
|
||||
"--control", str(self.control_path),
|
||||
"--imageset-file", str(self.imageset_path)],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
)
|
||||
deadline = time.time() + 10
|
||||
@@ -563,6 +567,17 @@ class MigrationTestServer:
|
||||
def write_raw_control(self, text):
|
||||
self.control_path.write_text(text)
|
||||
|
||||
def set_image_set(self, value):
|
||||
"""Stellt den Umschalter so um, wie es das CLI tut: eine Datei mit dem Namen des
|
||||
Bildsatzes. `None` loescht sie wieder (= Auslieferungszustand)."""
|
||||
if value is None:
|
||||
try:
|
||||
self.imageset_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return
|
||||
self.imageset_path.write_text(value)
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
self.proc.wait(timeout=5)
|
||||
@@ -584,8 +599,8 @@ def migration_server(tmp_path):
|
||||
|
||||
def test_migration_auto_serves_known_product(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG" * 1000)
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG" * 1000)
|
||||
r = srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1.1.1.1800-dev_ota_light.img")
|
||||
assert r.status == 200
|
||||
assert r.body == b"IMG" * 1000
|
||||
@@ -599,9 +614,9 @@ def test_migration_dev_only_in_version_serves_the_dev_image(migration_server):
|
||||
ging hier das produktionssignierte Image raus (200, und die Lampe lehnte es ab) -
|
||||
jetzt muss das dev-signierte gehen, und status.json muss die Quelle mitfuehren."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"DEV" * 1000)
|
||||
(imgdir / "migrate" / "g95_cw_prod.img").write_bytes(b"PROD" * 1000)
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"DEV" * 1000)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_prod.img").write_bytes(b"PROD" * 1000)
|
||||
r = srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.g95_V1.1.1.2357-dev_ota_light.img")
|
||||
assert r.status == 200
|
||||
assert r.body == b"DEV" * 1000
|
||||
@@ -617,9 +632,9 @@ def test_migration_production_lamp_still_gets_the_prod_image(migration_server):
|
||||
"""Die erfolgreich migrierte GU10: nirgends ein "-dev", also prod - und die Quelle
|
||||
"none", damit am Panel/in status.json steht, worauf die Einstufung beruht."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "gu10_rgb_dev.img").write_bytes(b"DEV" * 1000)
|
||||
(imgdir / "migrate" / "gu10_rgb_prod.img").write_bytes(b"PROD" * 1000)
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "gu10_rgb_dev.img").write_bytes(b"DEV" * 1000)
|
||||
(imgdir / "migrate-nocreds" / "gu10_rgb_prod.img").write_bytes(b"PROD" * 1000)
|
||||
r = srv.get("/qcx_firmware_-klyqa.lighting.rgb-cw-ww.gu10_V1.1.0.1138_ota_light.img")
|
||||
assert r.status == 200
|
||||
assert r.body == b"PROD" * 1000
|
||||
@@ -630,8 +645,8 @@ def test_migration_production_lamp_still_gets_the_prod_image(migration_server):
|
||||
|
||||
def test_migration_listen_records_but_serves_nothing(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.set_control(phase="listen")
|
||||
r = srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img")
|
||||
assert r.status == 404
|
||||
@@ -643,8 +658,8 @@ def test_migration_listen_records_but_serves_nothing(migration_server):
|
||||
|
||||
def test_arm_serves_exactly_once_then_falls_back_to_listen(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.set_control(phase="armed", armed_target="g95_cw")
|
||||
p = "/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img"
|
||||
assert srv.get(p).status == 200
|
||||
@@ -654,8 +669,8 @@ def test_arm_serves_exactly_once_then_falls_back_to_listen(migration_server):
|
||||
|
||||
def test_armed_for_other_target_does_not_serve(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.set_control(phase="armed", armed_target="gu10_rgb")
|
||||
r = srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img")
|
||||
assert r.status == 404
|
||||
@@ -671,7 +686,12 @@ def test_unknown_product_id_is_404_and_logged(migration_server):
|
||||
|
||||
|
||||
def test_missing_migrate_image_is_404(migration_server):
|
||||
srv, _ = migration_server
|
||||
srv, imgdir = migration_server
|
||||
# Der Bildsatz ist da (er enthaelt ein anderes Modell), nur dieses eine Modell fehlt
|
||||
# darin - ein anderer Zustand als ein fehlender Bildsatz (siehe
|
||||
# test_a_selected_but_missing_image_set_delivers_nothing).
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
r = srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.e14_V1_ota_light.img")
|
||||
assert r.status == 404
|
||||
st = srv.status(lambda s: len(s["recent"]) >= 1)
|
||||
@@ -692,8 +712,8 @@ def test_migration_path_is_404_in_recovery_mode(server):
|
||||
|
||||
def test_missing_control_file_means_auto(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.delete_control()
|
||||
assert srv.get(
|
||||
"/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img").status == 200
|
||||
@@ -701,8 +721,8 @@ def test_missing_control_file_means_auto(migration_server):
|
||||
|
||||
def test_corrupt_control_file_means_auto(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.write_raw_control("{das ist kein json")
|
||||
assert srv.get(
|
||||
"/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img").status == 200
|
||||
@@ -710,8 +730,8 @@ def test_corrupt_control_file_means_auto(migration_server):
|
||||
|
||||
def test_range_resume_on_migration_image(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(bytes(range(256)) * 40)
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(bytes(range(256)) * 40)
|
||||
p = "/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img"
|
||||
r = srv.get(p, headers={"Range": "bytes=1000-"})
|
||||
assert r.status == 206
|
||||
@@ -724,9 +744,9 @@ def test_version_comes_from_manifest(migration_server):
|
||||
Version traegt - Bestandskompatibilitaet mit einem Manifest aus der Zeit vor
|
||||
Fixrunde 2 (N2)."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate" / "manifest.json").write_text(json.dumps({
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds" / "manifest.json").write_text(json.dumps({
|
||||
"ninlight_version": "0.3.9",
|
||||
"images": [{"target": "g95_cw", "variant": "dev", "file": "g95_cw_dev.img"}]}))
|
||||
srv.get("/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img")
|
||||
@@ -742,10 +762,10 @@ def test_version_prefers_the_delivered_entry_over_the_manifest_wide_field(migrat
|
||||
darf dann nicht das manifestweite Feld fuer beide protokollieren, sondern muss die
|
||||
Version DES AUSGELIEFERTEN Eintrags nehmen."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG-DEV")
|
||||
(imgdir / "migrate" / "g95_cw_prod.img").write_bytes(b"IMG-PROD")
|
||||
(imgdir / "migrate" / "manifest.json").write_text(json.dumps({
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG-DEV")
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_prod.img").write_bytes(b"IMG-PROD")
|
||||
(imgdir / "migrate-nocreds" / "manifest.json").write_text(json.dumps({
|
||||
# Manifestweites Feld absichtlich falsch/veraltet, damit ein Zurueckfallen
|
||||
# darauf im Test auffallen wuerde.
|
||||
"ninlight_version": "1.2.0",
|
||||
@@ -770,8 +790,8 @@ def test_control_file_is_world_readable_even_under_a_restrictive_umask(tmp_path)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
try:
|
||||
(srv.image_dir / "migrate").mkdir(exist_ok=True)
|
||||
(srv.image_dir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(srv.image_dir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(srv.image_dir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.set_control(phase="armed", armed_target="g95_cw")
|
||||
|
||||
def world_readable():
|
||||
@@ -799,8 +819,8 @@ def test_armed_serves_exactly_once_under_concurrency(migration_server):
|
||||
Verbrauchen der Freigabe atomar unter einer Sperre machen, damit von n gleichzeitigen
|
||||
Anfragen gegen dieselbe Freigabe hoechstens eine durchkommt."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG" * 20000)
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG" * 20000)
|
||||
srv.set_control(phase="armed", armed_target="g95_cw")
|
||||
p = "/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img"
|
||||
|
||||
@@ -835,8 +855,8 @@ def test_control_write_failure_blocks_further_armed_deliveries(migration_server)
|
||||
ausserdem in status.json sichtbar sein, sonst wundert sich niemand, warum "arm" nicht
|
||||
mehr wirkt."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
srv.set_control(phase="armed", armed_target="g95_cw")
|
||||
p = "/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img"
|
||||
|
||||
@@ -899,3 +919,113 @@ def test_resolve_mode_garbage_file_means_recovery(tmp_path):
|
||||
def test_resolve_mode_default_without_mode_file_is_recovery():
|
||||
module = _load_server_module()
|
||||
assert module.resolve_mode(None, None) == "recovery"
|
||||
|
||||
|
||||
# --- Umschalter: mit oder ohne einkompilierte WLAN-Zugangsdaten ---------------------
|
||||
#
|
||||
# Zwei Bildsaetze liegen auf dem Pi (images/migrate-nocreds und images/migrate-wlan);
|
||||
# welcher ausgeliefert wird, entscheidet allein der Umschalter. Geprueft wird deshalb
|
||||
# immer an der TATSAECHLICH AUSGELIEFERTEN DATEI, nicht an einem Pfad oder einem
|
||||
# Statusfeld - der Fehlgriff, gegen den der Umschalter da ist, ist ein falscher Inhalt
|
||||
# in einer Lampe.
|
||||
|
||||
def _both_sets(imgdir):
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"OHNE" * 100)
|
||||
(imgdir / "migrate-wlan").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-wlan" / "g95_cw_dev.img").write_bytes(b"MITW" * 100)
|
||||
|
||||
|
||||
G95_DEV = "/qcx_firmware_-klyqa.lighting.cw-ww.g95-dev_V1_ota_light.img"
|
||||
|
||||
|
||||
def test_the_default_image_set_is_the_one_without_wifi_credentials(migration_server):
|
||||
"""Ohne Umschalterdatei (Auslieferungszustand) geht der Abzug OHNE Zugangsdaten
|
||||
raus. Die sichere Vorgabe ist die, bei der ein Fehlgriff niemandem das eigene
|
||||
WLAN-Passwort in ein fremdes Geraet schreibt."""
|
||||
srv, imgdir = migration_server
|
||||
_both_sets(imgdir)
|
||||
r = srv.get(G95_DEV)
|
||||
assert r.status == 200
|
||||
assert r.body == b"OHNE" * 100
|
||||
|
||||
|
||||
def test_the_switch_decides_which_file_is_actually_delivered(migration_server):
|
||||
srv, imgdir = migration_server
|
||||
_both_sets(imgdir)
|
||||
srv.set_image_set("mit-wlan")
|
||||
assert srv.get(G95_DEV).body == b"MITW" * 100
|
||||
# ... und wieder zurueck, ohne Dienstneustart: der Umschalter wirkt sofort.
|
||||
srv.set_image_set("ohne-wlan")
|
||||
assert srv.get(G95_DEV).body == b"OHNE" * 100
|
||||
|
||||
|
||||
def test_a_broken_switch_file_falls_back_to_the_safe_set(migration_server):
|
||||
"""Unbekannter Inhalt ist kein Grund, den teureren Satz zu nehmen."""
|
||||
srv, imgdir = migration_server
|
||||
_both_sets(imgdir)
|
||||
srv.set_image_set("voellig-anderes")
|
||||
assert srv.get(G95_DEV).body == b"OHNE" * 100
|
||||
|
||||
|
||||
def test_a_selected_but_missing_image_set_delivers_nothing(migration_server):
|
||||
"""Der gewaehlte Satz liegt nicht auf dem Pi: es darf NICHTS rausgehen - insbesondere
|
||||
nicht die Datei des anderen Satzes, die daneben liegt und genauso heisst."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
(imgdir / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"OHNE" * 100)
|
||||
srv.set_image_set("mit-wlan") # migrate-wlan/ existiert nicht
|
||||
|
||||
r = srv.get(G95_DEV)
|
||||
assert r.status == 404
|
||||
assert b"OHNE" not in r.body
|
||||
st = srv.status(lambda s: len(s["recent"]) >= 1)
|
||||
assert st["served"] == 0
|
||||
# Eigener Status, damit am Panel/in status.json "Satz fehlt" von "dieses Modell
|
||||
# fehlt" unterscheidbar bleibt.
|
||||
assert st["recent"][-1]["status"] == "noimageset"
|
||||
|
||||
|
||||
def test_a_missing_image_set_does_not_burn_the_release(migration_server):
|
||||
"""Wie beim fehlenden Einzelimage: eine Freigabe darf nicht daran verfallen, dass es
|
||||
gar nichts auszuliefern gab."""
|
||||
srv, imgdir = migration_server
|
||||
(imgdir / "migrate-nocreds").mkdir(exist_ok=True)
|
||||
srv.set_image_set("mit-wlan")
|
||||
srv.set_control(phase="armed", armed_target="g95_cw")
|
||||
assert srv.get(G95_DEV).status == 404
|
||||
assert srv.read_control()["phase"] == "armed"
|
||||
|
||||
|
||||
def test_the_version_comes_from_the_manifest_of_the_selected_set(migration_server):
|
||||
"""Nach dem Umschalten darf nicht die Version des vorigen Satzes stehenbleiben - die
|
||||
beiden manifest.json entstehen im selben Deploy und koennen dieselbe mtime tragen."""
|
||||
srv, imgdir = migration_server
|
||||
_both_sets(imgdir)
|
||||
for name, version in (("migrate-nocreds", "0.4.0-onb"), ("migrate-wlan", "0.4.0-wlan")):
|
||||
(imgdir / name / "manifest.json").write_text(json.dumps({
|
||||
"images": [{"target": "g95_cw", "variant": "dev", "file": "g95_cw_dev.img",
|
||||
"ninlight_version": version}]}))
|
||||
# Beiden Manifesten dieselbe mtime geben - genau der Fall, den ein reiner
|
||||
# mtime-Vergleich nicht bemerkt haette.
|
||||
stamp = 1600000000
|
||||
for name in ("migrate-nocreds", "migrate-wlan"):
|
||||
os.utime(imgdir / name / "manifest.json", (stamp, stamp))
|
||||
|
||||
assert srv.get(G95_DEV).status == 200
|
||||
assert srv.status(lambda s: s["recent"])["recent"][-1]["version"] == "0.4.0-onb"
|
||||
|
||||
srv.set_image_set("mit-wlan")
|
||||
assert srv.get(G95_DEV).status == 200
|
||||
assert srv.status(
|
||||
lambda s: len(s["recent"]) >= 2
|
||||
)["recent"][-1]["version"] == "0.4.0-wlan"
|
||||
|
||||
|
||||
def test_read_image_set_defaults_without_a_file(tmp_path):
|
||||
module = _load_server_module()
|
||||
assert module.read_image_set(None) == "ohne-wlan"
|
||||
assert module.read_image_set(tmp_path / "fehlt") == "ohne-wlan"
|
||||
p = tmp_path / "imageset"
|
||||
p.write_text("mit-wlan\n")
|
||||
assert module.read_image_set(p) == "mit-wlan"
|
||||
|
||||
@@ -212,6 +212,7 @@ run_setup() {
|
||||
NINLIGHT_STAGE_DIR="$state/tmp" \
|
||||
NINLIGHT_STATION_CONF="$state/etc/ninlight-station.conf" \
|
||||
NINLIGHT_STATION_ROOT="$state/opt/ninlight-station" \
|
||||
NINLIGHT_STATION_STATE_DIR="$state/var/lib/ninlight-station" \
|
||||
NINLIGHT_STATION_BIN="$state/usr/local/bin/ninlight-station" \
|
||||
NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \
|
||||
NINLIGHT_DISPLAY_UNIT="$state/etc/systemd/system/ninlight-display.service" \
|
||||
@@ -1047,6 +1048,38 @@ SUM_AFTER=$(find "$STATE/opt/ninlight-station/button" "$STATE/etc/systemd/system
|
||||
grep -Fq "systemctl enable ninlight-button.service" "$LOG" && pass "der zweite Lauf aktiviert den Tasterdienst erneut (idempotent, kein Fehler)" || fail "kein erneutes 'systemctl enable ninlight-button' im zweiten Lauf"
|
||||
echo "$OUT" | grep -q "veraltete Datei entfernt: button/" && fail "der zweite Lauf meldet eine veraltete Datei, obwohl nichts geloescht wurde" || pass "keine falsche 'veraltete Datei entfernt'-Meldung im zweiten Lauf"
|
||||
|
||||
echo
|
||||
echo "=== Szenario: das dauerhafte Zustandsverzeichnis entsteht, die Bildsatzverzeichnisse nicht ==="
|
||||
# /var/lib/ninlight-station traegt den Umschalter des Migrations-Bildsatzes - er muss
|
||||
# Neustarts ueberleben, darf also nicht in /run liegen. Die beiden Bildsatzverzeichnisse
|
||||
# dagegen duerfen hier NICHT entstehen: ein leeres Verzeichnis ist von einem bestueckten
|
||||
# nicht zu unterscheiden, und genau das soll das Panel melden koennen ("SATZ FEHLT").
|
||||
STATE="$TMPROOT/sstate-state"; STUBBIN="$TMPROOT/binstate"; LOG="$TMPROOT/sstate.log"
|
||||
setup_state "$STATE"
|
||||
setup_stubbin "$STUBBIN" "$LOG"
|
||||
: >"$LOG"
|
||||
set +e
|
||||
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
||||
[ -d "$STATE/var/lib/ninlight-station" ] \
|
||||
&& pass "/var/lib/ninlight-station wurde angelegt" \
|
||||
|| fail "das dauerhafte Zustandsverzeichnis fehlt - der Umschalter ueberlebt keinen Neustart"
|
||||
if [ -d "$STATE/var/lib/ninlight-station" ]; then
|
||||
MODE=$(file_mode "$STATE/var/lib/ninlight-station")
|
||||
[ "$MODE" = "755" ] && pass "Rechte 0755 (der Anzeigedienst muss den Umschalter lesen)" \
|
||||
|| fail "Rechte $MODE statt 755"
|
||||
fi
|
||||
[ ! -e "$STATE/var/lib/ninlight-station/imageset" ] \
|
||||
&& pass "die Umschalterdatei selbst wird nicht vorgeschrieben (Vorgabe gilt ohne sie)" \
|
||||
|| fail "setup_pi.sh legt eine Umschalterdatei an - zweite Quelle fuer denselben Wert"
|
||||
for d in migrate-wlan migrate-nocreds; do
|
||||
[ ! -e "$STATE/opt/ninlight-station/images/$d" ] \
|
||||
&& pass "images/$d bleibt ungeschaffen (leer waere nicht von bestueckt zu unterscheiden)" \
|
||||
|| fail "images/$d wurde leer angelegt"
|
||||
done
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "Alle Tests bestanden."
|
||||
|
||||
Reference in New Issue
Block a user