#!/usr/bin/env bash # Stub-Tests fuer deploy.sh - ohne Pi ausfuehrbar. # # ssh/rsync werden im PATH durch Stubs ersetzt: ssh fuehrt das per Heredoc # uebergebene Remote-Skript lokal aus (statt es an einen echten Pi zu # schicken), rsync uebersetzt "host:pfad"-Ziele auf den blossen lokalen Pfad # und ruft dann den echten rsync auf. So laeuft die komplette Logik von # deploy.sh - inklusive des Installationsschritts, der normalerweise per SSH # auf dem Pi passiert - tatsaechlich, aber vollstaendig gegen ein Sandbox- # Verzeichnis (NINLIGHT_STATION_ROOT/…) statt gegen echte Systempfade. # # Aufruf: tools/station/tests/test_deploy.sh set -euo pipefail HERE=$(cd "$(dirname "$0")" && pwd) STATION_DIR=$(cd "$HERE/.." && pwd) MKIMAGE_DIR=$(cd "$STATION_DIR/../mkimage" && pwd) REAL_RSYNC=$(command -v rsync) # Sicherheitsnetz (Fixrunde 1, C4): NINLIGHT_SIGN_DIR ist genau die Variable, mit der # man auf ~/workspace/fw-sign zeigt - und auf diesem Entwicklungsrechner liegen dort # ECHTE Signierschluessel. Ist sie im aufrufenden Profil oder fuer einen frueheren # manuellen Deploy exportiert, darf sie unter keinen Umstaenden in diese Suite # durchsickern. Deshalb: hier explizit loeschen, und run_deploy() unten liest sie NIE # wieder - jedes Szenario, das echte Testschluessel braucht, uebergibt sie ueber die # separate Variable TEST_SIGN_DIR (die deploy.sh selbst gar nicht kennt und die daher # auch nicht zufaellig aus der Umgebung vorbelegt sein kann). unset NINLIGHT_SIGN_DIR unset TEST_SIGN_DIR TMPROOT=$(mktemp -d) trap 'rm -rf "$TMPROOT"' EXIT FAILED=0 pass() { echo "ok - $1"; } fail() { echo "FEHLER - $1" >&2; FAILED=1; } # --- Sandbox-Repo aufbauen: tools/station/deploy.sh + tools/mkimage/nl_mkimage.py + build/ --- setup_sandbox() { local sandbox=$1 rm -rf "$sandbox" mkdir -p "$sandbox/tools/station" "$sandbox/tools/mkimage" "$sandbox/build" cp "$STATION_DIR/deploy.sh" "$sandbox/tools/station/deploy.sh" chmod +x "$sandbox/tools/station/deploy.sh" # Das ECHTE Signierwerkzeug wird gebraucht (nicht gestubbt): die Migrations-Szenarien # pruefen echtes Signieren/die echten Formatchecks (Mindestlaenge, kein Trailer, 4096 # Bit). Nur die Schluessel selbst sind Wegwerf-Testschluessel, nie echte. cp "$MKIMAGE_DIR/nl_sign.py" "$sandbox/tools/mkimage/nl_sign.py" # Inhalt irrelevant, die Dateien muessen nur existieren (werden "uebertragen"). echo "# dummy" >"$sandbox/tools/station/server.py" echo "# dummy" >"$sandbox/tools/station/products.py" echo "# dummy" >"$sandbox/tools/station/ninlight-station" echo "# dummy" >"$sandbox/tools/station/ninlight-station.service" echo "# dummy" >"$sandbox/tools/station/ninlight-display.service" # display/ wird ECHT kopiert, nicht durch Attrappen ersetzt: die Zusage aus Task 6b # ist, dass die beiden uebernommenen Panel-Treiber UND der Herkunftsnachweis # HERKUNFT.md mit auf den Pi gehen. Mit selbst erfundenen Dateinamen im Sandbox-Repo # wuerde der Test genau das nicht mehr zeigen - er pruefte nur noch sich selbst. cp -R "$STATION_DIR/display" "$sandbox/tools/station/display" rm -rf "$sandbox/tools/station/display/__pycache__" # Ein __pycache__ mit .pyc dieses Rechners (andere Python-Version als der Pi) - es # darf nicht mit uebertragen werden. mkdir -p "$sandbox/tools/station/display/__pycache__" echo "pyc-muell" >"$sandbox/tools/station/display/__pycache__/render.cpython-999.pyc" # Fake nl_mkimage.py --verify: verhaelt sich wie das echte Tool (eine Zeile # " " auf stdout bei Erfolg, Exit 0; eine Zeile auf # stderr bei Fehlschlag, Exit 1), aber steuerbar per Umgebungsvariable # MKIMAGE_FAIL_SUBSTR: enthaelt der zu pruefende Dateiname diesen Teilstring, # schlaegt die Pruefung fehl. cat >"$sandbox/tools/mkimage/nl_mkimage.py" <<'PY' import os import sys args = sys.argv[1:] f = args[args.index("--verify") + 1] target = args[args.index("--expect-device") + 1] fail_substr = os.environ.get("MKIMAGE_FAIL_SUBSTR", "") if fail_substr and fail_substr in f: print(f"Fehler: {f} besteht die Trailer-Pruefung nicht (Test-Stub)", file=sys.stderr) sys.exit(1) size = os.path.getsize(f) print(f"{target} 1.0.0 {size}") PY } # --- Stub-Kommandos fuer ssh/rsync im PATH --- setup_stubbin() { local bindir=$1 log=$2 mkdir -p "$bindir" cat >"$bindir/ssh" <>"$log" host=\$1; shift "\$@" EOF cat >"$bindir/rsync" <>"$log" args=() for a in "\$@"; do case "\$a" in -*) args+=("\$a") ;; *:*) args+=("\${a#*:}") ;; *) args+=("\$a") ;; esac done exec "$REAL_RSYNC" "\${args[@]}" EOF cat >"$bindir/scp" <>"$log" exit 1 EOF cat >"$bindir/sudo" <>"$log" exec "\$@" EOF # Der Stub unterscheidet nach Unit: deploy.sh behandelt den Anzeigedienst # ausdruecklich anders als den Hauptdienst (ein fehlgeschlagener Neustart der # Anzeige ist KEIN Deploy-Fehler). Mit einem Schalter fuer beide Units zusammen # liesse sich genau dieser Unterschied nicht pruefen. cat >"$bindir/systemctl" <>"$log" case "\$*" in *ninlight-display*) case "\$1" in is-active) exit "\${FAKE_DISPLAY_ACTIVE_EXIT:-1}" ;; restart) exit "\${FAKE_DISPLAY_RESTART_EXIT:-0}" ;; *) exit 0 ;; esac ;; esac case "\$1" in is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-1}" ;; restart) exit "\${FAKE_SYSTEMCTL_RESTART_EXIT:-0}" ;; *) exit 0 ;; esac EOF chmod +x "$bindir"/ssh "$bindir"/rsync "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl } run_deploy() { # Fuehrt deploy.sh mit stubbtem PATH aus; Ausgabe landet in $OUT, Exit-Code in $?. local sandbox=$1; shift # Auf dem echten Pi existieren /usr/local/bin und /etc/systemd/system schon; # in der Sandbox muessen sie fuer "install" eigens angelegt werden. mkdir -p "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system" # NINLIGHT_SIGN_DIR wird hier HART auf einen lokalen Wert gesetzt, niemals ueber # "${NINLIGHT_SIGN_DIR:-default}" aus der geerbten Umgebung uebernommen (das war der # C4-Fehler aus Fixrunde 1: ein Fallback greift nur, wenn die Variable NICHT gesetzt # ist - war sie es, z.B. weil ein Profil sie dauerhaft auf ~/workspace/fw-sign zeigen # laesst, haette der Fallback sie unveraendert durchgereicht). Die einzige Variable, # die ein Szenario mit echten Testschluesseln setzen darf, ist TEST_SIGN_DIR - eine # Variable, die deploy.sh selbst gar nicht kennt und die deshalb nie zufaellig von # aussen vorbelegt sein kann wie NINLIGHT_SIGN_DIR es auf einem Entwicklungsrechner # ist. Ohne TEST_SIGN_DIR zeigt sie auf einen Pfad, der in der Sandbox garantiert # nicht existiert. local sign_dir="${TEST_SIGN_DIR:-$sandbox/no-such-sign-dir}" PATH="$STUBBIN:$PATH" \ NINLIGHT_SSH_BIN="$STUBBIN/ssh" \ NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \ NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \ NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \ NINLIGHT_DISPLAY_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" \ NINLIGHT_SIGN_DIR="$sign_dir" \ "$sandbox/tools/station/deploy.sh" "$@" } # --- Hilfsmittel fuer die Migrations-Szenarien --- # Erzeugt Wegwerf-Testschluessel (nl_sign.generate_test_key(), nie echte Schluessel, # nie geschrieben ausser in dieses Sandbox-Verzeichnis) unter $keydir. with_prod/ # with_dev steuern, welche der beiden Dateien entsteht. make_test_keys() { local keydir=$1 with_prod=$2 with_dev=$3 mkdir -p "$keydir/QCX_Device_CA" PYTHONPATH="$MKIMAGE_DIR" python3 - "$keydir" "$with_prod" "$with_dev" <<'PY' import sys import nl_sign keydir, with_prod, with_dev = sys.argv[1], sys.argv[2] == "1", sys.argv[3] == "1" if with_prod: k = nl_sign.generate_test_key() with open(keydir + "/QCX_Device_CA/qcx_firmware_signing_key.pem", "wb") as fh: fh.write(k.private_pem) if with_dev: k = nl_sign.generate_test_key() with open(keydir + "/fw_sign.pem", "wb") as fh: fh.write(k.private_pem) PY } # Erzeugt eine gueltige rohe _gz.img-Attrappe: >= 1024 Byte, durch 4 teilbar, ohne # NINL-Trailer - genau das, was nl_sign.py als Eingabe akzeptiert. make_gz_file() { local path=$1 fill=${2:-A} python3 -c " import sys path, fill = sys.argv[1], sys.argv[2].encode() data = bytearray((fill * 2048)[:2048]) data[-512:-508] = b'\\x00\\x00\\x00\\x00' # nie zufaellig wie 'NINL' aussehen with open(path, 'wb') as fh: fh.write(bytes(data)) " "$path" "$fill" } # Erzeugt eine _ota.img-Attrappe mit ANDEREM Inhalt als das zugehoerige _gz.img und # einem echten NINL-Trailer - damit ein versehentliches Signieren des _ota.img statt # des _gz.img (Fall 7) zweifelsfrei auffliegt: weder Inhalt noch Groesse passen dann. make_ota_file() { local path=$1 python3 -c " import sys path = sys.argv[1] body = b'B' * 2048 trailer = bytearray(b'\\xff' * 512) trailer[0:4] = b'NINL' with open(path, 'wb') as fh: fh.write(body + bytes(trailer)) " "$path" } echo "=== Szenario 1: Image besteht die Trailer-Pruefung nicht -> nichts uebertragen ===" SANDBOX="$TMPROOT/s1"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"; SANDBOX_STATE="$TMPROOT/s1-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "dummy" >"$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e OUT=$(MKIMAGE_FAIL_SUBSTR=foo run_deploy "$SANDBOX" pi-test foo 2>&1) RC=$? set -e if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi if echo "$OUT" | grep -q "besteht die Trailer-Pruefung nicht"; then pass "Fehlermeldung zur Trailer-Pruefung vorhanden" else fail "Fehlermeldung zur Trailer-Pruefung fehlt" fi if ! grep -q "STUB ssh\|STUB rsync\|STUB scp" "$LOG"; then pass "kein ssh/rsync/scp aufgerufen - nichts wurde uebertragen" else fail "ssh/rsync/scp wurde trotz fehlgeschlagener Pruefung aufgerufen: $(cat "$LOG")" fi echo echo "=== Szenario 2: keine Images unter build/ gefunden ===" SANDBOX="$TMPROOT/s2"; STUBBIN="$TMPROOT/bin2"; LOG="$TMPROOT/s2.log"; SANDBOX_STATE="$TMPROOT/s2-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" : >"$LOG" set +e OUT=$(run_deploy "$SANDBOX" pi-test 2>&1) RC=$? set -e if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi if echo "$OUT" | grep -q "keine Images unter"; then pass "Fehlermeldung zu fehlenden Images vorhanden" else fail "Fehlermeldung zu fehlenden Images fehlt" fi if ! grep -q "STUB" "$LOG" 2>/dev/null; then pass "kein Transfer-Kommando aufgerufen" else fail "ein Transfer-Kommando wurde trotz fehlender Images aufgerufen: $(cat "$LOG")" fi echo echo "=== Szenario 3: gezielter Deploy eines Geraetetyps laesst andere Images auf dem Ziel unangetastet (Critical 2) ===" SANDBOX="$TMPROOT/s3"; STUBBIN="$TMPROOT/bin3"; LOG="$TMPROOT/s3.log"; SANDBOX_STATE="$TMPROOT/s3-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar" echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img" echo "bar-image" >"$SANDBOX/build/bar/ninlight_bar_ota.img" # Zielverzeichnis simuliert einen vorherigen Voll-Deploy: Images fuer foo UND bar liegen schon da. mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images" echo "foo-image-alt" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" echo "bar-image" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img" : >"$LOG" set +e OUT=$(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 echo "$OUT" | grep -q "andere Geraetetypen im Zielverzeichnis bleiben erhalten"; then pass "Ausgabe kuendigt gezielten Deploy ohne --delete an" else fail "Ausgabe erwaehnt den gezielten Deploy nicht" fi if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_bar_ota.img" ]; then pass "Image des NICHT ausgewaehlten Geraetetyps (bar) blieb erhalten" else fail "Image des nicht ausgewaehlten Geraetetyps (bar) wurde geloescht - Critical 2 nicht behoben" fi if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ] \ && [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img")" = "foo-image-neu" ]; then pass "Image des ausgewaehlten Geraetetyps (foo) wurde aktualisiert" else fail "Image des ausgewaehlten Geraetetyps (foo) wurde nicht aktualisiert" fi echo echo "=== Szenario 4: Voll-Deploy (kein target) entfernt veraltete Images - zur Abgrenzung von Szenario 3 ===" SANDBOX="$TMPROOT/s4"; STUBBIN="$TMPROOT/bin4"; LOG="$TMPROOT/s4.log"; SANDBOX_STATE="$TMPROOT/s4-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img" mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images" echo "veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.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 [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" ]; then pass "veraltetes Image wurde beim Voll-Deploy entfernt (--delete wirkt weiterhin, wenn gewollt)" else fail "veraltetes Image wurde beim Voll-Deploy NICHT entfernt" fi echo echo "=== Szenario 5: Dienst laeuft, Neustart nach dem Deploy schlaegt fehl -> Exit != 0 (Minor) ===" SANDBOX="$TMPROOT/s5"; STUBBIN="$TMPROOT/bin5"; LOG="$TMPROOT/s5.log"; SANDBOX_STATE="$TMPROOT/s5-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system" echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e # Ueber run_deploy() statt eines eigenen Aufrufs (Fixrunde 1: der vorherige direkte # Aufruf hier setzte NINLIGHT_SIGN_DIR zwar auch hart, ging aber am gemeinsamen Muster # vorbei - ein Praezedenzfall, den ein spaeterer Umbau leicht wieder falsch abschreiben # koennte). FAKE_SYSTEMCTL_* werden als gewoehnliche vorangestellte Umgebungsvariablen # durchgereicht, run_deploy() ist eine Funktion und sieht sie genauso wie jeder andere # Befehl. OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test foo 2>&1) RC=$? set -e if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0, wenn der Neustart fehlschlaegt ($RC)"; else fail "Exit-Code war 0, obwohl der Neustart fehlschlug"; fi if echo "$OUT" | grep -q "Neustart nach dem Deploy ist aber fehlgeschlagen"; then pass "Fehlermeldung unterscheidet Neustart-Fehlschlag von 'Dienst laeuft nicht'" else fail "Fehlermeldung fehlt oder ist mehrdeutig" fi if echo "$OUT" | grep -q "Dienst laeuft nicht"; then fail "faelschlich 'Dienst laeuft nicht' gemeldet, obwohl der Dienst lief" else pass "'Dienst laeuft nicht' wird nicht faelschlich gemeldet" fi echo echo "=== Szenario 6: deploy.sh/setup_pi.sh nutzen ohne Override /usr/bin/ssh (bzw. scp) ===" # Die anderen Szenarien laufen mit NINLIGHT_SSH_BIN auf einen Stub umgebogen - das prueft die # Logik, aber nicht mehr, WELCHEN ssh das Skript ohne Override tatsaechlich waehlt. Der Nutzer # verlangt ausdruecklich /usr/bin/ssh und /usr/bin/scp statt einer Variante im PATH; das hier # ist die Absicherung dagegen, dass der Default beim naechsten Umbau stillschweigend zurueck # auf ein PATH-"ssh" rutscht. if grep -q 'SSH_BIN=\${NINLIGHT_SSH_BIN:-/usr/bin/ssh}' "$STATION_DIR/deploy.sh"; then pass "deploy.sh: Default fuer SSH_BIN ist /usr/bin/ssh" else fail "deploy.sh: Default fuer SSH_BIN ist nicht mehr /usr/bin/ssh" fi if grep -q 'SSH_BIN=\${NINLIGHT_SSH_BIN:-/usr/bin/ssh}' "$STATION_DIR/setup_pi.sh"; then pass "setup_pi.sh: Default fuer SSH_BIN ist /usr/bin/ssh" else fail "setup_pi.sh: Default fuer SSH_BIN ist nicht mehr /usr/bin/ssh" fi if grep -q 'SCP_BIN=\${NINLIGHT_SCP_BIN:-/usr/bin/scp}' "$STATION_DIR/setup_pi.sh"; then pass "setup_pi.sh: Default fuer SCP_BIN ist /usr/bin/scp" else fail "setup_pi.sh: Default fuer SCP_BIN ist nicht mehr /usr/bin/scp" fi echo echo "=== Szenario 7: Voll-Deploy mit zwei Build-Verzeichnissen fuer denselben Dateinamen -> Abbruch (B1) ===" SANDBOX="$TMPROOT/s7"; STUBBIN="$TMPROOT/bin7"; LOG="$TMPROOT/s7.log"; SANDBOX_STATE="$TMPROOT/s7-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/evk" "$SANDBOX/build/evk-onb" echo "evk-mit-wlan" >"$SANDBOX/build/evk/ninlight_evk_ota.img" echo "evk-ohne-wlan" >"$SANDBOX/build/evk-onb/ninlight_evk_ota.img" : >"$LOG" set +e OUT=$(run_deploy "$SANDBOX" pi-test 2>&1) RC=$? set -e if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0, obwohl zwei Fundorte denselben Dateinamen liefern"; fi if echo "$OUT" | grep -q "ninlight_evk_ota.img mehrfach gefunden"; then pass "Fehlermeldung nennt den doppelten Dateinamen" else fail "Fehlermeldung zum doppelten Dateinamen fehlt: $OUT" fi if echo "$OUT" | grep -q "build/evk/ninlight_evk_ota.img" && echo "$OUT" | grep -q "build/evk-onb/ninlight_evk_ota.img"; then pass "Fehlermeldung nennt beide Fundorte" else fail "Fehlermeldung nennt nicht beide Fundorte: $OUT" fi if ! grep -q "STUB" "$LOG" 2>/dev/null; then pass "kein Transfer-Kommando aufgerufen - nichts wurde stillschweigend uebertragen" else fail "ein Transfer-Kommando wurde trotz doppeltem Dateinamen aufgerufen: $(cat "$LOG")" fi echo echo "=== Szenario 8: Voll-Deploy zeigt vor dem Loeschen, welche Images verschwinden (B2) ===" SANDBOX="$TMPROOT/s8"; STUBBIN="$TMPROOT/bin8"; LOG="$TMPROOT/s8.log"; SANDBOX_STATE="$TMPROOT/s8-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image-neu" >"$SANDBOX/build/foo/ninlight_foo_ota.img" mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images" echo "veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" echo "auch-veraltet" >"$SANDBOX_STATE/opt/ninlight-station/images/ninlight_e14_cw_ota.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 echo "$OUT" | grep -q "folgende Images werden entfernt"; then pass "Ausgabe kuendigt die zu entfernenden Images an, bevor geloescht wird" else fail "Ausgabe nennt die zu entfernenden Images nicht: $OUT" fi if echo "$OUT" | grep -q "ninlight_veraltet_ota.img" && echo "$OUT" | grep -q "ninlight_e14_cw_ota.img"; then pass "beide zu entfernenden Dateien werden namentlich genannt" else fail "nicht alle zu entfernenden Dateien werden genannt: $OUT" fi if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_veraltet_ota.img" ]; then pass "veraltetes Image wurde nach der Ankuendigung tatsaechlich entfernt" else fail "veraltetes Image wurde trotz Ankuendigung nicht entfernt" fi echo echo "=== Szenario 9: Voll-Deploy ohne verwaiste Images meldet das statt zu schweigen (B2) ===" SANDBOX="$TMPROOT/s9"; STUBBIN="$TMPROOT/bin9"; LOG="$TMPROOT/s9.log"; SANDBOX_STATE="$TMPROOT/s9-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e OUT=$(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 echo "$OUT" | grep -q "keine Images werden entfernt"; then pass "Ausgabe meldet explizit, dass nichts entfernt wird" else fail "Ausgabe meldet nicht, dass nichts entfernt wird: $OUT" fi echo echo "=== Vorbereitung: Wegwerf-Testschluessel fuer die Migrations-Szenarien ===" # Nur ueber nl_sign.generate_test_key() erzeugt, nie ausserhalb dieses Testlaufs # verwendet und nie in ein Repository- oder Log-File jenseits dieser TMPROOT-Sandbox # geschrieben. KEYDIR_FULL="$TMPROOT/keys-full" make_test_keys "$KEYDIR_FULL" 1 1 KEYDIR_DEV_ONLY="$TMPROOT/keys-dev-only" make_test_keys "$KEYDIR_DEV_ONLY" 0 1 pass "Testschluessel erzeugt" echo echo "=== Szenario 10: Mit vorhandenem Schluesselverzeichnis entstehen zwei Migrationsimages je Ziel (Fall 1) ===" SANDBOX="$TMPROOT/s10"; STUBBIN="$TMPROOT/bin10"; LOG="$TMPROOT/s10.log"; SANDBOX_STATE="$TMPROOT/s10-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" : >"$LOG" set +e 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 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" if [ -f "$PROD_IMG" ] && [ -f "$DEV_IMG" ]; then pass "beide Migrationsimages (prod, dev) wurden angelegt" else fail "mindestens ein Migrationsimage fehlt: $OUT" fi for img in "$PROD_IMG" "$DEV_IMG"; do [ -f "$img" ] || continue size=$(wc -c <"$img" | tr -d ' ') expected=$((GZ_SIZE + 512)) if [ "$size" -eq "$expected" ]; then pass "$(basename "$img") ist genau 512 Byte groesser als das _gz.img ($size Byte)" else fail "$(basename "$img") hat $size Byte, erwartet $expected" fi done echo echo "=== Szenario 11: manifest.json listet jedes ausgelieferte Migrationsimage (Fall 2) ===" SANDBOX="$TMPROOT/s11"; STUBBIN="$TMPROOT/bin11"; LOG="$TMPROOT/s11.log"; SANDBOX_STATE="$TMPROOT/s11-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" : >"$LOG" 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" if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then pass "manifest.json wurde angelegt" else fail "manifest.json fehlt oder Exit-Code $RC: $OUT" fi set +e # Form haelt sich an die Spec (docs/superpowers/spec Abschnitt 4) und an das, was # server.py tatsaechlich liest. Seit Fixrunde 2 (N2) traegt "ninlight_version" PRO # EINTRAG die Version - hier stimmen beide Eintraege ueberein, deshalb steht das # manifestweite Feld zusaetzlich als Bequemlichkeit oben. Ein reines json.load haette # eine falsche Verschachtelung nicht gemerkt (Fixrunde 1, C1); die naechste Pruefung # (Szenario 20) schickt dieselbe Datei zusaetzlich durch den echten Server-Leser. CHECK=$(python3 -c " import json with open('$MANIFEST') as fh: data = json.load(fh) entries = data.get('images', []) by_variant = {e.get('variant'): e for e in entries if e.get('target') == 'foo'} ok = ( isinstance(data.get('built'), str) and len(data['built']) > 0 and data.get('ninlight_version') == '1.0.0' and len(entries) == 2 and set(by_variant) == {'prod', 'dev'} and all(e.get('file') in ('foo_prod.img', 'foo_dev.img') for e in entries) and all(isinstance(e.get('bytes'), int) and e['bytes'] > 0 for e in entries) and all(isinstance(e.get('sha256'), str) and len(e['sha256']) == 64 for e in entries) and all(e.get('ninlight_version') == '1.0.0' for e in entries) ) print('OK' if ok else 'FAIL: ' + json.dumps(data)) " 2>&1) set -e if [ "$CHECK" = "OK" ]; then pass "manifest.json traegt built/ninlight_version oben (Bequemlichkeit) und target/variant/file/bytes/sha256/ninlight_version je Eintrag" else fail "manifest.json-Inhalt unerwartet: $CHECK" fi echo echo "=== Szenario 12: Ohne NINLIGHT_SIGN_DIR entfallen die Migrationsimages, das Rettungsimage kommt trotzdem an (Fall 3) ===" SANDBOX="$TMPROOT/s12"; STUBBIN="$TMPROOT/bin12"; LOG="$TMPROOT/s12.log"; SANDBOX_STATE="$TMPROOT/s12-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" : >"$LOG" set +e OUT=$(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/ninlight_foo_ota.img" ]; then pass "Rettungsimage (_ota.img) wurde trotzdem ausgeliefert" 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 pass "keine Migrationsimages entstanden" else fail "Migrationsimages entstanden, obwohl kein Schluesselverzeichnis vorhanden war" fi if echo "$OUT" | grep -q "Schluesselverzeichnis"; then pass "Meldung ueber fehlendes Schluesselverzeichnis vorhanden" else fail "keine Meldung ueber das fehlende Schluesselverzeichnis: $OUT" fi echo echo "=== Szenario 13: Schluesselverzeichnis ohne Produktionsschluessel - nur dev-Images entstehen (Fall 4) ===" SANDBOX="$TMPROOT/s13"; STUBBIN="$TMPROOT/bin13"; LOG="$TMPROOT/s13.log"; SANDBOX_STATE="$TMPROOT/s13-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" : >"$LOG" set +e 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 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 pass "prod-Migrationsimage entfiel wie erwartet" else fail "prod-Migrationsimage wurde trotz fehlendem Produktionsschluessel angelegt" fi if echo "$OUT" | grep -q "Produktionsschluessel fehlt"; then pass "Meldung ueber fehlenden Produktionsschluessel vorhanden" else fail "keine Meldung ueber den fehlenden Produktionsschluessel: $OUT" fi echo echo "=== Szenario 14: gezielter Deploy erzeugt nur die Migrationsimages des angegebenen Ziels (Fall 5, 4a-Fehler fuer migrate/) ===" SANDBOX="$TMPROOT/s14"; STUBBIN="$TMPROOT/bin14"; LOG="$TMPROOT/s14.log"; SANDBOX_STATE="$TMPROOT/s14-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar" 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" # 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' {"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.9.0", "images": [ {"target": "bar", "variant": "prod", "file": "bar_prod.img", "bytes": 20, "sha256": "x", "ninlight_version": "0.9.0"}, {"target": "bar", "variant": "dev", "file": "bar_dev.img", "bytes": 19, "sha256": "y", "ninlight_version": "0.9.0"} ]} JSON : >"$LOG" set +e 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 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 pass "Migrationsimages des NICHT ausgewaehlten Geraetetyps (bar) blieben unangetastet" else fail "Migrationsimages von bar wurden veraendert oder geloescht - 4a-Fehler fuer migrate/" fi set +e MANIFEST_CHECK=$(python3 -c " import json with open('$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json') as fh: data = json.load(fh) entries = data.get('images', []) keys = {(e.get('target'), e.get('variant')) for e in entries} expected = {('foo', 'prod'), ('foo', 'dev'), ('bar', 'prod'), ('bar', 'dev')} by_key = {(e.get('target'), e.get('variant')): e for e in entries} ok = ( keys == expected # foo wurde in DIESEM Lauf signiert -> Stub-Version 1.0.0 (Fixrunde 2, N2: Version # steht pro Eintrag). and by_key[('foo', 'prod')].get('ninlight_version') == '1.0.0' and by_key[('foo', 'dev')].get('ninlight_version') == '1.0.0' # bar wurde NICHT neu signiert -> seine alte Version (0.9.0) bleibt unangetastet, # auch wenn sie von foos Version abweicht. and by_key[('bar', 'prod')].get('ninlight_version') == '0.9.0' and by_key[('bar', 'dev')].get('ninlight_version') == '0.9.0' # Da foo (1.0.0) und bar (0.9.0) nach dem Merge NICHT uebereinstimmen, darf das # manifestweite Feld nicht mehr die Version dieses Laufs behaupten - es muss ganz # fehlen (Fixrunde 2, N2: das war der eigentliche Fehler - vorher stand hier # weiterhin 1.0.0, obwohl bars Bilder unveraendert auf 0.9.0 blieben). and 'ninlight_version' not in data ) print('OK' if ok else 'FAIL: ' + json.dumps(data)) " 2>&1) set -e if [ "$MANIFEST_CHECK" = "OK" ]; then pass "manifest.json beschreibt nach dem gezielten Deploy weiterhin BEIDE Geraetetypen mit je eigener Version, ohne falsches manifestweites Feld" else fail "manifest.json-Inhalt nach dem gezielten Deploy unerwartet: $MANIFEST_CHECK" fi echo echo "=== Szenario 15: Kein privater Schluessel und kein Schluesselpfad-Inhalt in der Ausgabe (Fall 6) ===" SANDBOX="$TMPROOT/s15"; STUBBIN="$TMPROOT/bin15"; LOG="$TMPROOT/s15.log"; SANDBOX_STATE="$TMPROOT/s15-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" : >"$LOG" set +e 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 echo "$OUT" | grep -q -- "-----BEGIN"; then fail "PEM-Header (Schluesselinhalt) taucht in der Ausgabe auf" else pass "kein PEM-Header in der Ausgabe" fi # ALLE Zeilen BEIDER privater Schluessel pruefen (nicht nur eine einzelne Zeile des # Produktionsschluessels) und dabei auch das ssh/rsync/sudo-Log durchsuchen, nicht nur # die Kommandozeilenausgabe von deploy.sh selbst (Fixrunde 1: die vorherige Version # haette einen Leak ueber eine andere Zeile oder ueber $LOG glatt uebersehen). LEAK="" while IFS= read -r line; do [ -n "$line" ] || continue if echo "$OUT" | grep -qF -- "$line"; then LEAK="Ausgabe enthaelt: $line" break fi if grep -qF -- "$line" "$LOG"; then LEAK="Log enthaelt: $line" break fi done < <(cat "$KEYDIR_FULL/QCX_Device_CA/qcx_firmware_signing_key.pem" "$KEYDIR_FULL/fw_sign.pem") if [ -z "$LEAK" ]; then pass "keine Zeile eines privaten Schluessels taucht in Ausgabe oder Log auf" else fail "Schluesselinhalt gefunden - $LEAK" fi echo echo "=== Szenario 16: Signiert wird das _gz.img, niemals das _ota.img (Fall 7, gefaehrlichster Fehler) ===" SANDBOX="$TMPROOT/s16"; STUBBIN="$TMPROOT/bin16"; LOG="$TMPROOT/s16.log"; SANDBOX_STATE="$TMPROOT/s16-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" A make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e 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" if [ -f "$DEV_IMG" ]; then set +e python3 -c " import sys with open(sys.argv[1], 'rb') as fh: signed = fh.read() with open(sys.argv[2], 'rb') as fh: gz = fh.read() sys.exit(0 if signed[:-512] == gz else 1) " "$DEV_IMG" "$SANDBOX/build/foo/ninlight_foo_gz.img" PAYLOAD_MATCH=$? set -e if [ "$PAYLOAD_MATCH" -eq 0 ]; then pass "die signierte Nutzlast ist byteidentisch mit dem _gz.img" else fail "die signierte Nutzlast entspricht NICHT dem _gz.img - das _ota.img wurde signiert!" fi else fail "dev-Migrationsimage wurde nicht angelegt: $OUT" fi echo echo "=== Szenario 17: Ohne Schluesselverzeichnis bleibt ein vorhandenes migrate/ beim Voll-Deploy unangetastet ===" SANDBOX="$TMPROOT/s17"; STUBBIN="$TMPROOT/bin17"; LOG="$TMPROOT/s17.log"; SANDBOX_STATE="$TMPROOT/s17-state" 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" : >"$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 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 pass "Ausgabe kuendigt an, dass migrate/ unangetastet bleibt" else fail "Ausgabe erwaehnt nicht, dass migrate/ unangetastet bleibt: $OUT" fi echo echo "=== Szenario 18 (Fixrunde 1, C2): Schluesselverzeichnis vorhanden, aber leer - migrate/ bleibt unangetastet, Rettungsimage kommt trotzdem ===" # Der eigentliche C2-Repro: anders als Szenario 12 (Verzeichnis fehlt komplett) EXISTIERT # $SIGN_DIR hier, enthaelt aber keinen der beiden Schluessel. Vorher fuehrte das trotzdem # zu einem (leeren) $STAGE/migrate mit Manifest, das dann per --delete ein vorhandenes # migrate/ auf dem Pi geleert hat. SIGN_EMPTY="$TMPROOT/keys-empty" mkdir -p "$SIGN_EMPTY" SANDBOX="$TMPROOT/s18"; STUBBIN="$TMPROOT/bin18"; LOG="$TMPROOT/s18.log"; SANDBOX_STATE="$TMPROOT/s18-state" 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" echo '{"built": "2026-01-01T00:00:00Z", "ninlight_version": "0.0.1", "images": []}' \ >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json" : >"$LOG" set +e OUT=$(TEST_SIGN_DIR="$SIGN_EMPTY" 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 [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ]; then pass "Rettungsimage (_ota.img) wurde trotzdem ausgeliefert" 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 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" fi if echo "$OUT" | grep -q "enthaelt weder Produktions- noch Entwicklungsschluessel"; then pass "Meldung nennt das leere Schluesselverzeichnis" else fail "keine Meldung ueber das leere Schluesselverzeichnis: $OUT" fi echo echo "=== Szenario 19 (Fixrunde 1, C2): Voll-Deploy mit nur dev-Schluessel raeumt keine vorhandenen _prod.img weg ===" SANDBOX="$TMPROOT/s19"; STUBBIN="$TMPROOT/bin19"; LOG="$TMPROOT/s19.log"; SANDBOX_STATE="$TMPROOT/s19-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" echo "echtes-altes-prod-image" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/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 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 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" fi echo echo "=== Szenario 20 (Fixrunde 1, C1): manifest.json wird vom ECHTEN Server-Leser korrekt interpretiert ===" # json.load allein (wie in Szenario 11) haette die falsche Verschachtelung von # ninlight_version nicht gemerkt, weil es nur die Feldnamen innerhalb der Eintraege # prueft. Hier laeuft dieselbe Datei stattdessen durch server.py's eigene # Station.image_version(target, variant) - genau die Funktion, die im echten Betrieb # entscheidet, welche Version protokolliert und angezeigt wird (seit Fixrunde 2, N2: # pro ausgeliefertem Eintrag, nicht mehr nur manifestweit - deshalb jetzt mit # target/variant statt ohne Argumente). SANDBOX="$TMPROOT/s20"; STUBBIN="$TMPROOT/bin20"; LOG="$TMPROOT/s20.log"; SANDBOX_STATE="$TMPROOT/s20-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" : >"$LOG" 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" if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then pass "manifest.json wurde angelegt" else fail "manifest.json fehlt oder Exit-Code $RC: $OUT" fi set +e VERSION=$(python3 -c " import importlib.util from pathlib import Path station_dir = Path('$STATION_DIR') spec = importlib.util.spec_from_file_location('ninlight_station_server_c1', station_dir / 'server.py') module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) st = module.Station( image_dir=Path('$MANIFEST').resolve().parent.parent, state_path=Path('$TMPROOT/s20-status.json'), stall_after=None, mode='migration', control_path=Path('$TMPROOT/s20-control.json'), ) print(st.image_version('foo', 'dev')) " 2>&1) RC2=$? set -e if [ "$RC2" -eq 0 ] && [ "$VERSION" = "1.0.0" ]; then pass "Station.image_version() (der echte Server-Code) liest die Version korrekt, nicht 'unbekannt'" else fail "Station.image_version() liefert '$VERSION' statt '1.0.0' (RC=$RC2) - Manifest-Form stimmt nicht mit dem Server ueberein" fi echo echo "=== Szenario 21 (Fixrunde 2, N1): kaputtes/falsch geformtes Bestandsmanifest darf den Deploy nicht mitten drin abbrechen ===" # Der Merge steht in deploy.sh VOR der Installation von server.py/products.py/CLI/Unit # und vor dem Neustart - ein Abbruch hier liesse den Pi mit neuen Images, aber altem # Stationscode zurueck. "truncated" und "empty" liefen schon vorher durch (json.load # wirft ValueError, das war immer abgefangen); "array"/"null"/"string"/"null-entry" # sind gueltiges JSON mit falscher FORM und haben vorher mit AttributeError abgebrochen # ("... has no attribute 'get'"), weil nur auf Lese-, nicht auf Parse-Fehler geprueft # wurde. Alle vier Zeilen aus der Review-Tabelle. mkdir -p "$TMPROOT/bad-manifests" printf '%s' '{ kaputt' >"$TMPROOT/bad-manifests/truncated.json" : >"$TMPROOT/bad-manifests/empty.json" printf '%s' '[]' >"$TMPROOT/bad-manifests/array.json" printf '%s' 'null' >"$TMPROOT/bad-manifests/null.json" printf '%s' '"text"' >"$TMPROOT/bad-manifests/string.json" printf '%s' '{"images": [null]}' >"$TMPROOT/bad-manifests/null-entry.json" for label in truncated empty array null string null-entry; do SANDBOX="$TMPROOT/s21-$label"; STUBBIN="$TMPROOT/bin21-$label" LOG="$TMPROOT/s21-$label.log"; SANDBOX_STATE="$TMPROOT/s21-$label-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" cp "$TMPROOT/bad-manifests/$label.json" "$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json" : >"$LOG" set +e # Nur EIN Schluessel (dev) -> MIGRATE_COMPLETE=0 -> der Merge-Zweig laeuft, nicht der # --delete-Zweig, der das kaputte Bestandsmanifest ohnehin ueberschreiben wuerde. OUT=$(TEST_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test foo 2>&1) RC=$? set -e if [ "$RC" -eq 0 ]; then pass "Bestandsmanifest '$label': Exit-Code 0, Deploy bricht nicht mitten drin ab" else 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 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)" fi done echo echo "=== Szenario 22 (Task 6b, Punkt 1): deploy.sh liefert Anzeigeverzeichnis und Anzeige-Unit mit aus ===" SANDBOX="$TMPROOT/s22"; STUBBIN="$TMPROOT/bin22"; LOG="$TMPROOT/s22.log"; SANDBOX_STATE="$TMPROOT/s22-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img" # Bestand auf dem "Pi": eine Datei, die es im Repo nicht mehr gibt. display/ ist reiner # Code aus dem Repo - was dort verschwindet, muss auch hier verschwinden, sonst laedt # ein zurueckgebliebenes altes Modul weiter mit. mkdir -p "$SANDBOX_STATE/opt/ninlight-station/display" echo "alt" >"$SANDBOX_STATE/opt/ninlight-station/display/veraltet.py" : >"$LOG" set +e OUT=$(run_deploy "$SANDBOX" pi-test 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" DISP="$SANDBOX_STATE/opt/ninlight-station/display" for F in display.py render.py epdconfig.py epd2in13_V2.py epd2in13_V3.py HERKUNFT.md; do [ -f "$DISP/$F" ] && pass "display/$F ausgeliefert" || fail "display/$F fehlt unter $DISP" done [ -f "$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" ] && pass "ninlight-display.service ausgeliefert" || fail "Anzeige-Unit fehlt - sie waere weiter toter Text im Repo" [ ! -f "$DISP/veraltet.py" ] && pass "im Repo geloeschte Datei verschwindet auch auf dem Pi (--delete)" || fail "veraltet.py blieb im Zielverzeichnis liegen" [ ! -d "$DISP/__pycache__" ] && pass "kein __pycache__ dieses Rechners auf dem Pi gelandet" || fail "__pycache__ wurde mit uebertragen: $(ls "$DISP/__pycache__")" # Die Anzeige lief nicht (Stub-Default) - dann wird sie auch nicht neu gestartet, aber # der Deploy sagt es. echo "$OUT" | grep -q "Anzeigedienst laeuft nicht" && pass "Deploy meldet, dass der Anzeigedienst nicht laeuft" || fail "Ausgabe erwaehnt den Anzeigedienst nicht: $OUT" # Die fluechtige Ablage wird wieder aufgeraeumt (sie liegt, wie bei server.py & Co., # im echten /tmp - deploy.sh's REMOTE-Block kennt dafuer keine Sandbox-Variable). [ ! -d /tmp/ninlight-display ] && pass "die fluechtige Ablage /tmp/ninlight-display wurde aufgeraeumt" || fail "/tmp/ninlight-display blieb liegen" echo echo "=== Szenario 23: laeuft die Anzeige, wird sie nach dem Deploy neu gestartet ===" # Ohne Neustart liefe sie mit dem gerade ersetzten Code weiter. SANDBOX="$TMPROOT/s23"; STUBBIN="$TMPROOT/bin23"; LOG="$TMPROOT/s23.log"; SANDBOX_STATE="$TMPROOT/s23-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e OUT=$(FAKE_DISPLAY_ACTIVE_EXIT=0 run_deploy "$SANDBOX" pi-test 2>&1) RC=$? set -e [ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" grep -Fq "systemctl restart ninlight-display" "$LOG" && pass "Anzeigedienst wurde neu gestartet" || fail "kein 'systemctl restart ninlight-display' im Log: $(cat "$LOG")" echo "$OUT" | grep -q "Anzeigedienst neu gestartet" && pass "Ausgabe meldet den Neustart" || fail "Ausgabe meldet den Neustart nicht: $OUT" echo echo "=== Szenario 24: ein fehlgeschlagener Neustart der ANZEIGE ist kein Deploy-Fehler ===" # Die Anzeige ist nirgends Voraussetzung fuer eine Migration. Ein Deploy, der wegen des # Panels rot wird, verleitet nur dazu, einen echten Fehler zu uebersehen. Beim # HAUPTdienst ist es umgekehrt - das prueft das Szenario darunter zur Abgrenzung. SANDBOX="$TMPROOT/s24"; STUBBIN="$TMPROOT/bin24"; LOG="$TMPROOT/s24.log"; SANDBOX_STATE="$TMPROOT/s24-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e OUT=$(FAKE_DISPLAY_ACTIVE_EXIT=0 FAKE_DISPLAY_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>"$TMPROOT/s24.err") RC=$? set -e ERR=$(cat "$TMPROOT/s24.err") [ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem Anzeige-Neustart" || fail "Exit-Code $RC - der Deploy wurde wegen der Anzeige rot: $ERR" echo "$ERR" | grep -q "Warnung" && pass "Warnung (keine Fehlermeldung) auf stderr" || fail "keine Warnung auf stderr: $ERR" [ -f "$SANDBOX_STATE/opt/ninlight-station/server.py" ] && pass "der Deploy selbst lief vollstaendig durch" || fail "server.py fehlt - der Deploy brach ab" echo echo "=== Szenario 25 (Abgrenzung): ein fehlgeschlagener Neustart des HAUPTdienstes ist sehr wohl ein Deploy-Fehler ===" SANDBOX="$TMPROOT/s25"; STUBBIN="$TMPROOT/bin25"; LOG="$TMPROOT/s25.log"; SANDBOX_STATE="$TMPROOT/s25-state" setup_sandbox "$SANDBOX" setup_stubbin "$STUBBIN" "$LOG" mkdir -p "$SANDBOX/build/foo" echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img" : >"$LOG" set +e OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 run_deploy "$SANDBOX" pi-test 2>&1) RC=$? set -e [ "$RC" -ne 0 ] && pass "Exit-Code != 0 beim Hauptdienst" || fail "Exit-Code 0 - ein toter Hauptdienst ginge unbemerkt durch: $OUT" echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden." else echo "Mindestens ein Test ist fehlgeschlagen." >&2 fi exit "$FAILED"