From 47532a6820bee600a316d14fba295b3bcdfa60b8 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Mon, 14 Sep 2026 00:45:04 +0200 Subject: [PATCH] fix(station): ehrliche Abbruchmeldung, apt-update darf scheitern Fixrunde 2 zu Task 6b. - Die Vorbedingung "display.py liegt in der Ablage" steht jetzt VOR dem ersten sudo install. Vorher stand sie dahinter, und die Meldung "Nichts installiert" war schlicht unwahr: server.py, products.py, das CLI und die Stations-Unit lagen dann schon auf dem Pi. Ausgerechnet an dem Werkbank-Moment, fuer den die Pruefung gedacht ist, log das Skript den Bediener an. Die Ablage raeumt ein EXIT-Trap jetzt auf beiden Wegen aus dem Block ab, nicht nur im Erfolgsfall. - "apt-get update" ist keine Voraussetzung mehr fuer "apt-get install": update meldet schon dann einen Fehler, wenn eine einzige Quelle nicht erreichbar ist, und ein Pi, auf dem das Paket aus dem Cache gekommen waere, bekam so still kein Pillow. - Die Signalhandler beenden jetzt selbst (130/143/129). Ein Handler ohne exit laesst bash weiterlaufen - ein Strg-C konnte in "fertig, Exit 0" muenden. Die Begruendung im Kommentar war falsch und ist korrigiert: zum Aufraeumen haette der EXIT-Trap allein genuegt. - test_host.sh ruft die Shell-Suiten aus einer Liste auf, und der Coverage-Test prueft diese Liste gegen das Dateisystem statt im Skripttext nach Dateinamen zu suchen - ein nachgestellter Kommentar hebelte die alte Regel aus. Er deckt jetzt ganz tools/ ab, nicht nur station/tests. - Neue Szenarien: Wahrheitsgehalt der Abbruchmeldung, apt-update-Fall, und ein Strg-C, das aus den Trap-Zeilen des Skripts selbst geprueft wird. --- tools/mkimage/tests/test_suite_coverage.py | 67 ++++++++++++++------ tools/station/setup_pi.sh | 71 ++++++++++++++++------ tools/station/tests/test_setup_pi.sh | 70 +++++++++++++++++++++ tools/test_host.sh | 26 +++++--- 4 files changed, 189 insertions(+), 45 deletions(-) diff --git a/tools/mkimage/tests/test_suite_coverage.py b/tools/mkimage/tests/test_suite_coverage.py index dfd53a8..3908551 100644 --- a/tools/mkimage/tests/test_suite_coverage.py +++ b/tools/mkimage/tests/test_suite_coverage.py @@ -41,14 +41,42 @@ def pytest_paths(): return paths -def own_test_files(): +def own_test_files(suffix=".py"): return sorted( p.resolve() - for p in TOOLS_DIR.rglob("test_*.py") + for p in TOOLS_DIR.rglob("test_*" + suffix) if not any(part in IGNORED_PARTS for part in p.parts) ) +def listed_shell_suites(): + """Die Eintraege des SHELL_SUITES-Feldes in tools/test_host.sh, aufgeloest. + + Ausdruecklich gegen das FELD geprueft und nicht per Textsuche nach Dateinamen: eine + Textsuche ist schon durch einen nachgestellten Kommentar ("echo x # test_deploy.sh"), + ein ": \"test_deploy.sh\"" oder einen Heredoc-Inhalt erfuellt - sie belegt nur, dass + der Name irgendwo vorkommt, nicht dass die Suite laeuft (Task 6b F2). + """ + text = TEST_HOST.read_text() + block = re.search(r"^SHELL_SUITES=\((.*?)^\)", text, re.M | re.S) + assert block, ( + "in tools/test_host.sh gibt es kein 'SHELL_SUITES=(...)'-Feld mehr - die " + "Vollstaendigkeitspruefung der Shell-Suiten haengt daran" + ) + # Und das Feld muss auch wirklich abgearbeitet werden, sonst ist es nur Deko. + assert re.search(r'for\s+\w+\s+in\s+"\$\{SHELL_SUITES\[@\]\}"', text), ( + "tools/test_host.sh arbeitet SHELL_SUITES nicht in einer Schleife ab - die " + "aufgefuehrten Suiten wuerden gar nicht laufen" + ) + paths = [] + for raw in block.group(1).split(): + arg = raw.strip('"') + if not arg or arg.startswith("#"): + continue + paths.append(Path(arg.replace("$ROOT", str(REPO_ROOT))).resolve()) + return paths + + def test_every_test_file_is_reached_by_test_host(): roots = pytest_paths() orphans = [] @@ -72,23 +100,28 @@ def test_every_shell_suite_is_called_by_test_host(): Haelfte nicht ab, weil die Shell-Suiten nicht ueber pytest laufen, sondern einzeln aufgerufen werden. """ - # Nur AUSFUEHRBARE Zeilen zaehlen, keine Kommentare: der Kommentarblock ueber den - # Aufrufen nennt test_deploy.sh beim Namen, und eine blosse Textsuche waere damit - # auch dann erfuellt, wenn die Aufrufzeile fehlt - der Test waere gruen, ohne etwas - # zu pruefen (genau die Falle, die er verhindern soll). - lines = [ - line.strip() - for line in TEST_HOST.read_text().splitlines() - if line.strip() and not line.strip().startswith("#") + listed = listed_shell_suites() + # Symmetrisch zur .py-Regel: das GANZE tools/ wird durchsucht, nicht nur + # tools/station/tests - eine kuenftige Shell-Suite an einer neuen Stelle faellt sonst + # weiterhin durch (Task 6b F2). + # tools/test_host.sh heisst selbst "test_*.sh", ist aber der Aufrufer und keine + # Suite - es kann sich schlecht selbst auffuehren. + missing = [ + s.relative_to(REPO_ROOT) + for s in own_test_files(".sh") + if s != TEST_HOST.resolve() and s not in listed ] - missing = [] - for suite in sorted((TOOLS_DIR / "station" / "tests").glob("test_*.sh")): - # So, wie test_host.sh sie aufruft: "$ROOT/tools/station/tests/" - if not any(suite.name in line for line in lines): - missing.append(suite.relative_to(REPO_ROOT)) assert not missing, ( - "diese Shell-Suiten ruft tools/test_host.sh nicht auf, sie laufen also nur bei " - "Handaufruf: {}".format(", ".join(str(m) for m in missing)) + "diese Shell-Suiten stehen nicht in SHELL_SUITES von tools/test_host.sh, sie " + "laufen also nur bei Handaufruf: {}".format(", ".join(str(m) for m in missing)) + ) + # Und andersherum: ein Eintrag, den es nicht mehr gibt, laesst test_host.sh beim + # naechsten Lauf hart scheitern - das faellt hier schon vorher auf. + verschwunden = [str(p) for p in listed if not p.exists()] + assert not verschwunden, ( + "SHELL_SUITES in tools/test_host.sh nennt Dateien, die es nicht gibt: {}".format( + ", ".join(verschwunden) + ) ) diff --git a/tools/station/setup_pi.sh b/tools/station/setup_pi.sh index a6a0f3f..a71a600 100755 --- a/tools/station/setup_pi.sh +++ b/tools/station/setup_pi.sh @@ -109,6 +109,35 @@ ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station} BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station} UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service} DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service} + +# Die Ablage wird in JEDEM Fall abgeraeumt, auch wenn dieser Block vorzeitig aussteigt +# (Task 6b F2, N1). Vorher stand das "rm -rf" nur am Blockende und wurde beim Abbruch +# uebersprungen - die uebertragenen Dateien blieben unter /tmp liegen und waren beim +# naechsten Lauf genau der Reststand, gegen den der Block eigentlich absichert. +cleanup_stage() { + rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \ + "$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \ + "$STAGE_DIR/ninlight-display" +} +trap cleanup_stage EXIT + +rm -rf "$STAGE_DIR/ninlight-display/__pycache__" +# Harte Vorbedingung, und zwar VOR dem ersten "sudo install" (Task 6b F2, N1): ohne +# display.py in der Ablage hat das scp nicht geliefert, was es sollte - abgebrochen, +# falsch verschachtelt oder gar nicht gelaufen. Die Installationsschleife weiter unten +# wuerde das stillschweigend als "nichts zu tun" verbuchen, der Lauf meldete Erfolg und +# der aktivierte Anzeigedienst liefe gegen eine nicht existierende Datei. +# +# Die Pruefung stand zuerst NACH den vier "sudo install" darunter - die Meldung "Nichts +# installiert" war damit schlicht unwahr: server.py, products.py, das CLI und die +# Stations-Unit lagen dann bereits auf dem Pi. Ausgerechnet an dem Werkbank-Moment, fuer +# den diese Pruefung gedacht ist, log das Skript den Bediener an. Jetzt stimmt der Satz. +if [ ! -f "$STAGE_DIR/ninlight-display/display.py" ]; then + echo "Fehler: $STAGE_DIR/ninlight-display/display.py fehlt - die Uebertragung des Anzeigeverzeichnisses ist fehlgeschlagen." >&2 + echo " Nichts installiert: $ROOT_DIR, $BIN_PATH und beide Units sind unveraendert. Ablage aufgeraeumt, Lauf einfach wiederholen." >&2 + exit 1 +fi + sudo install -d -m 0755 "$ROOT_DIR" "$ROOT_DIR/images" "$ROOT_DIR/display" sudo install -m 0755 "$STAGE_DIR/server.py" "$ROOT_DIR/server.py" sudo install -m 0644 "$STAGE_DIR/products.py" "$ROOT_DIR/products.py" @@ -116,16 +145,6 @@ sudo install -m 0755 "$STAGE_DIR/ninlight-station" "$BIN_PATH" sudo install -m 0644 "$STAGE_DIR/ninlight-station.service" "$UNIT_PATH" # Anzeige: display.py ist ausfuehrbar (die Unit ruft es zwar ueber /usr/bin/python3 auf, # "ninlight-station display test" aber auch von Hand), der Rest ist importierter Code. -rm -rf "$STAGE_DIR/ninlight-display/__pycache__" -# Harte Vorbedingung (Task 6b F1, I1): ohne display.py in der Ablage hat das scp nicht -# geliefert, was es sollte - abgebrochen, falsch verschachtelt oder gar nicht gelaufen. -# Die Schleife darunter wuerde das stillschweigend als "nichts zu tun" verbuchen, der -# Lauf meldete Erfolg und der aktivierte Anzeigedienst liefe gegen eine nicht -# existierende Datei. Lieber hier laut abbrechen. -if [ ! -f "$STAGE_DIR/ninlight-display/display.py" ]; then - echo "Fehler: $STAGE_DIR/ninlight-display/display.py fehlt - die Uebertragung des Anzeigeverzeichnisses ist fehlgeschlagen. Nichts installiert." >&2 - exit 1 -fi for f in "$STAGE_DIR"/ninlight-display/*; do [ -f "$f" ] || continue case "$(basename "$f")" in @@ -146,9 +165,7 @@ for f in "$ROOT_DIR"/display/*; do fi done sudo install -m 0644 "$STAGE_DIR/ninlight-display.service" "$DISPLAY_UNIT_PATH" -rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \ - "$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \ - "$STAGE_DIR/ninlight-display" +# Das Aufraeumen der Ablage macht der EXIT-Trap oben - auf beiden Wegen aus diesem Block. sudo systemctl daemon-reload echo " installiert" REMOTE @@ -175,8 +192,15 @@ if python3 -c 'import PIL' 2>/dev/null; then else # "update" zuerst: auf einem laenger nicht gepflegten Pi zeigen die Paketlisten auf # Versionen, die es im Archiv nicht mehr gibt, und "install" scheitert mit 404. - # Beide Aufrufe duerfen nur warnen - siehe oben. - if sudo apt-get update >/dev/null 2>&1 && sudo apt-get install -y python3-pil; then + # + # Aber "|| true", und "install" laeuft UNABHAENGIG davon (Task 6b F2, N2): "apt-get + # update" liefert schon dann einen Fehler, wenn eine einzige Quelle nicht erreichbar + # ist - eine veraltete Drittanbieter-Liste, teilweises Netz. Mit "update && install" + # bekaeme ein Pi, auf dem "install" aus dem Paketcache oder den erreichbaren Quellen + # muehelos durchgelaufen waere, still kein Pillow. Das Auffrischen ist eine + # Verbesserung der Chancen, keine Voraussetzung. + sudo apt-get update >/dev/null 2>&1 || true + if sudo apt-get install -y python3-pil; then echo " python3-pil installiert" else echo " Warnung: python3-pil liess sich nicht installieren (kein Netz? veraltete Paketlisten?)." >&2 @@ -233,10 +257,14 @@ eval "$(sudo cat "$CONF")" # Variable gar nicht mehr. Deshalb ein gemeinsames Verzeichnis mit genau einem # Trap darum herum (0700, mktemp -d legt es so an). # -# Auch auf INT/TERM/HUP, nicht nur EXIT: bash fuehrt den EXIT-Trap zwar nach einem -# abgefangenen Signal aus, aber nur, wenn das Signal ueberhaupt abgefangen wird - ohne -# eigenen Handler beendet ein Strg-C (INT) oder ein abbrechendes ssh (HUP) die Shell -# sofort, und der EXIT-Trap laeuft nie. Genau dann bliebe die Datei liegen. +# Zur Reichweite des Traps (Task 6b F2, N3 - die Begruendung hier war vorher falsch): +# der EXIT-Trap allein WUERDE bei einem Strg-C schon reichen, bash fuehrt ihn auch beim +# Abbruch durch ein nicht abgefangenes SIGINT aus (Exit 130). Der Grund fuer die +# Signalhandler ist ein anderer: sie sollen den Exit-Code ehrlich halten. Ein Handler +# fuer INT/TERM, der nicht selbst beendet, laesst bash naemlich NACH dem Handler +# weiterlaufen - ein Strg-C mitten im Schreiben der Profile koennte dann in "fertig, +# Exit 0" muenden, obwohl der Lauf abgebrochen wurde. Deshalb raeumt jeder Handler auf +# und beendet danach ausdruecklich mit dem ueblichen Signal-Code (128+n). # # Mit ausdruecklicher Vorlage unter ${TMPDIR:-/tmp}, nicht als blankes "mktemp -d": # BSD-mktemp (macOS, wo diese Suite auch laeuft) beachtet TMPDIR ohne Vorlage gar nicht @@ -245,7 +273,10 @@ eval "$(sudo cat "$CONF")" # der nachsieht, ob wirklich nichts liegengeblieben ist, sucht dann auf macOS an der # falschen Stelle und ist gruen, ohne etwas geprueft zu haben. PROFILE_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/ninlight-profile.XXXXXX") -trap 'rm -rf "$PROFILE_TMPDIR"' EXIT INT TERM HUP +trap 'rm -rf "$PROFILE_TMPDIR"' EXIT +trap 'rm -rf "$PROFILE_TMPDIR"; exit 130' INT +trap 'rm -rf "$PROFILE_TMPDIR"; exit 143' TERM +trap 'rm -rf "$PROFILE_TMPDIR"; exit 129' HUP # $1 = Profilname, $2 = SSID, $3 = PSK, $4 = "update" (bestehendes Profil neu schreiben, # UUID behalten) oder "keep" (bestehendes Profil unangetastet lassen). diff --git a/tools/station/tests/test_setup_pi.sh b/tools/station/tests/test_setup_pi.sh index 7e3f410..f8c0624 100755 --- a/tools/station/tests/test_setup_pi.sh +++ b/tools/station/tests/test_setup_pi.sh @@ -501,6 +501,21 @@ set -e echo "$OUT" | grep -q "display.py fehlt" && pass "Fehlermeldung nennt die fehlende Datei" || fail "Fehlermeldung fehlt: $OUT" # Das Schlimmste waere ein aktivierter Dienst ohne ExecStart-Datei. grep -Fq "systemctl enable ninlight-display" "$LOG" && fail "der Anzeigedienst wurde trotz fehlender Dateien aktiviert - 203/EXEC im Neustart-Takt" || pass "kein aktivierter Dienst ohne Dateien" +# WAHRHEITSGEHALT der Meldung, nicht ihr Wortlaut (Task 6b F2, N1): "Nichts installiert" +# muss auch stimmen. Vorher stand die Pruefung hinter vier "sudo install", und auf dem Pi +# lagen server.py, products.py, das CLI und die Stations-Unit bereits - ausgerechnet an +# dem Werkbank-Moment, fuer den die Pruefung gedacht ist, log das Skript den Bediener an. +echo "$OUT" | grep -q "Nichts installiert" && pass "die Meldung behauptet, nichts installiert zu haben" || fail "Meldung ohne diese Zusage: $OUT" +for F in "$STATE/opt/ninlight-station/server.py" "$STATE/opt/ninlight-station/products.py" \ + "$STATE/usr/local/bin/ninlight-station" \ + "$STATE/etc/systemd/system/ninlight-station.service" \ + "$STATE/etc/systemd/system/ninlight-display.service"; do + [ ! -e "$F" ] && pass "wirklich nicht installiert: $(basename "$F")" || fail "die Meldung luegt - $F liegt auf dem Pi" +done +# Und die Ablage darf nicht liegenbleiben: sonst ist sie beim naechsten Lauf genau der +# Reststand, gegen den Szenario 11 absichert. +REST=$(find "$STATE/tmp" -mindepth 1 2>/dev/null | head -n5) +[ -z "$REST" ] && pass "die Ablage wurde auch im Fehlerfall aufgeraeumt" || fail "Reststand in der Ablage: $REST" echo echo "=== Szenario 13 (Task 6b F1, I1): eine im Repo geloeschte Datei verschwindet auch auf dem Pi ===" @@ -613,6 +628,61 @@ echo "$ERR" | grep -q "systemctl enable ninlight-display" && pass "Warnung zu 'e echo "$ERR" | grep -q "systemctl restart ninlight-display" && pass "Warnung zu 'restart' auf stderr" || fail "keine restart-Warnung: $ERR" [ -f "$STATE/etc/NetworkManager/system-connections/qconnex-firmware.nmconnection" ] && pass "der Lauf ging danach normal weiter (Profile angelegt)" || fail "der Lauf brach nach der Warnung ab" +echo +echo "=== Szenario 17 (Task 6b F2, N2): ein gescheitertes 'apt-get update' verhindert die Installation von Pillow nicht ===" +# "apt-get update" liefert schon dann einen Fehler, wenn eine einzige Quelle nicht +# erreichbar ist (veraltete Drittanbieter-Liste, teilweises Netz). Mit "update && +# install" bekaeme ein Pi, auf dem "install" aus dem Cache muehelos durchgelaufen waere, +# still kein Pillow - und genau das Stillsein war der Grund, das Paket ueberhaupt hier +# nachzuziehen. +STATE="$TMPROOT/s17-state"; STUBBIN="$TMPROOT/bin17"; LOG="$TMPROOT/s17.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +: >"$LOG" +set +e +OUT=$(FAKE_PIL_MISSING=1 FAKE_APT_UPDATE_EXIT=100 run_setup "$STATE" "$STUBBIN" 2>"$TMPROOT/s17.err") +RC=$? +set -e +ERR=$(cat "$TMPROOT/s17.err") +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $ERR" +grep -Fq "apt-get install -y python3-pil" "$LOG" && pass "'install' wird trotz gescheitertem 'update' versucht" || fail "'install' wurde nach dem gescheiterten 'update' gar nicht mehr versucht: $(grep apt-get "$LOG")" +echo "$OUT" | grep -q "python3-pil installiert" && pass "Pillow wurde installiert" || fail "Ausgabe meldet die Installation nicht: $OUT" +echo "$ERR" | grep -q "Warnung" && fail "es wurde gewarnt, obwohl das Paket installiert werden konnte: $ERR" || pass "keine unnoetige Warnung" + +echo +echo "=== Szenario 18 (Task 6b F2, N3): ein Strg-C waehrend des Profilschreibens endet nicht als 'erfolgreich' ===" +# Ein INT/TERM-Handler, der nicht selbst beendet, laesst bash NACH dem Handler +# weiterlaufen - der Lauf liefe bis "fertig" durch und endete mit 0, obwohl er +# abgebrochen wurde. (Der EXIT-Trap allein wuerde zum Aufraeumen genuegen; hier geht es +# um den ehrlichen Exit-Code.) +# +# Die Trap-Zeilen werden aus setup_pi.sh HERAUSGELESEN, nicht hier nachgebaut: sonst +# prueft der Test seine eigene Kopie und bleibt gruen, wenn das Skript sie aendert. +TRAPS=$(grep -E "^trap '.*PROFILE_TMPDIR.*' (EXIT|INT|TERM|HUP)$" "$STATION_DIR/setup_pi.sh") +TRAP_COUNT=$(printf '%s\n' "$TRAPS" | grep -c . || true) +[ "$TRAP_COUNT" -ge 2 ] && pass "die Trap-Zeilen liessen sich aus setup_pi.sh auslesen ($TRAP_COUNT Stueck)" || fail "keine Trap-Zeilen in setup_pi.sh gefunden - Szenario 18 prueft sonst nichts" +SIGPROBE="$TMPROOT/sigprobe.sh" +{ + echo '#!/usr/bin/env bash' + echo 'set -eu' + echo 'PROFILE_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/ninlight-sigprobe.XXXXXX")' + printf '%s\n' "$TRAPS" + echo 'printf "%s" "$PROFILE_TMPDIR" >"$1"' + echo 'touch "$PROFILE_TMPDIR/profile.geheim"' + # Das Signal an den eigenen Prozess: deterministisch, ohne Wartezeiten. + echo 'kill -INT $$' + echo 'echo WEITERGELAUFEN' +} >"$SIGPROBE" +chmod +x "$SIGPROBE" +set +e +PROBE_OUT=$(TMPDIR="$TMPROOT" "$SIGPROBE" "$TMPROOT/sigprobe.dir" 2>&1) +PROBE_RC=$? +set -e +PROBE_DIR=$(cat "$TMPROOT/sigprobe.dir" 2>/dev/null || true) +echo "$PROBE_OUT" | grep -q WEITERGELAUFEN && fail "nach dem Strg-C lief das Skript weiter - der Lauf koennte mit 'fertig' und Exit 0 enden" || pass "das Skript laeuft nach dem Strg-C nicht weiter" +[ "$PROBE_RC" -eq 130 ] && pass "Exit-Code 130 (128+SIGINT), nicht 0" || fail "Exit-Code $PROBE_RC statt 130" +[ -n "$PROBE_DIR" ] && [ ! -d "$PROBE_DIR" ] && pass "das Temp-Verzeichnis mit dem Passwort wurde dabei aufgeraeumt" || fail "Temp-Verzeichnis blieb liegen: $PROBE_DIR" + echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden." diff --git a/tools/test_host.sh b/tools/test_host.sh index d6b9696..eb38429 100755 --- a/tools/test_host.sh +++ b/tools/test_host.sh @@ -24,11 +24,21 @@ else echo " tools/mkimage/test_nl_mkimage.py::test_stays_python38_compatible)" >&2 fi -# Alle Shell-Suiten unter tools/station/tests/ - test_deploy.sh fehlte hier und lief -# damit nur bei Handaufruf (Task 6b F1, I3): dieselbe Klasse von Luecke wie die aus -# keiner Suite gefallene test_nl_mkimage.py, nur eine Ebene hoeher. Mit ihr liefen 95 -# Zusagen nirgends mit, darunter die komplette Deploy-Haelfte der Anzeige-Auslieferung. -# tools/mkimage/tests/test_suite_coverage.py haelt fest, dass hier keine Suite fehlt. -"$ROOT/tools/station/tests/test_cli.sh" -"$ROOT/tools/station/tests/test_deploy.sh" -"$ROOT/tools/station/tests/test_setup_pi.sh" +# Alle Shell-Suiten - test_deploy.sh fehlte hier und lief damit nur bei Handaufruf +# (Task 6b F1, I3): dieselbe Klasse von Luecke wie die aus keiner Suite gefallene +# test_nl_mkimage.py, nur eine Ebene hoeher. Mit ihr liefen 95 Zusagen nirgends mit, +# darunter die komplette Deploy-Haelfte der Anzeige-Auslieferung. +# +# Als ausdrueckliche Liste, nicht als lose Folge von Aufrufen (Task 6b F2): so gibt es +# EINE Stelle, die vollstaendig sein muss, und +# tools/mkimage/tests/test_suite_coverage.py kann sie gegen das Dateisystem pruefen, +# statt im Skripttext nach Dateinamen zu suchen - eine Textsuche haelt schon ein +# nachgestellter Kommentar zum Narren. +SHELL_SUITES=( + "$ROOT/tools/station/tests/test_cli.sh" + "$ROOT/tools/station/tests/test_deploy.sh" + "$ROOT/tools/station/tests/test_setup_pi.sh" +) +for suite in "${SHELL_SUITES[@]}"; do + "$suite" +done