Files
ninlight-w600/tools/mkimage/tests/test_suite_coverage.py
Michael Sauer 47532a6820 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.
2026-09-14 00:45:04 +02:00

143 lines
6.4 KiB
Python

"""Wache dagegen, dass eine Testdatei aus allen Suiten herausfaellt.
Genau das war schon einmal passiert: tools/test_host.sh rief pytest auf
tools/mkimage/tests und tools/station/tests auf - tools/mkimage/test_nl_mkimage.py
liegt aber DIREKT in tools/mkimage/ und lief damit in keiner Suite mehr mit. 19 Tests
waren still verschwunden, darunter test_stays_python38_compatible, die Wache fuer die
bindende 3.8-Randbedingung des Docker-Build-Containers (`str | None` in nl_mkimage.py
hatte den Build schon einmal gebrochen).
Eine stumm ausgefallene Testdatei ist schlimmer als eine fehlende: die Suite bleibt
gruen und behauptet damit eine Deckung, die es nicht gibt. Deshalb wird hier nicht ein
einzelner Pfad festgenagelt, sondern die Regel selbst - JEDE test_*.py unter tools/
muss von einem der Pfade erfasst sein, die tools/test_host.sh an pytest uebergibt.
Eine neue Testdatei an einer neuen Stelle faellt damit sofort auf, statt erst dann,
wenn jemand sie vermisst.
"""
import re
from pathlib import Path
TOOLS_DIR = Path(__file__).resolve().parents[2]
REPO_ROOT = TOOLS_DIR.parent
TEST_HOST = TOOLS_DIR / "test_host.sh"
# Verzeichnisse, die keine Tests DIESES Projekts enthalten: virtuelle Umgebungen und
# installierte Fremdpakete bringen eigene test_*.py mit, die hier nichts zu suchen haben.
IGNORED_PARTS = (".venv", "__pycache__", "site-packages", "node_modules", ".git")
def pytest_paths():
"""Die Pfade, die tools/test_host.sh an pytest uebergibt, als aufgeloeste Pfade."""
text = TEST_HOST.read_text()
match = re.search(r"^\s*python3 -m pytest (.+?)(?:\s+-\S+)*\s*$", text, re.M)
assert match, "in tools/test_host.sh ist kein 'python3 -m pytest'-Aufruf zu finden"
paths = []
for raw in match.group(1).split():
arg = raw.strip('"')
if arg.startswith("-"):
continue
paths.append(Path(arg.replace("$ROOT", str(REPO_ROOT))).resolve())
assert paths, "der pytest-Aufruf in tools/test_host.sh uebergibt keine Pfade"
return paths
def own_test_files(suffix=".py"):
return sorted(
p.resolve()
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 = []
for test_file in own_test_files():
if not any(test_file == r or r in test_file.parents for r in roots):
orphans.append(test_file.relative_to(REPO_ROOT))
assert not orphans, (
"diese Testdateien laufen in keiner Suite mit - tools/test_host.sh sammelt sie "
"nicht ein: {}. Entweder den Pfad in test_host.sh erweitern oder die Datei "
"dorthin verschieben, wo sie eingesammelt wird.".format(
", ".join(str(o) for o in orphans)
)
)
def test_every_shell_suite_is_called_by_test_host():
"""Dieselbe Regel fuer die Shell-Suiten (Task 6b F1, I3).
test_deploy.sh war in tools/test_host.sh nicht aufgefuehrt und lief damit nur bei
Handaufruf - 95 Zusagen, die niemand mehr sah. Die .py-Regel oben deckt genau diese
Haelfte nicht ab, weil die Shell-Suiten nicht ueber pytest laufen, sondern einzeln
aufgerufen werden.
"""
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
]
assert not 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)
)
)
def test_python38_guard_is_among_them():
"""Die konkrete Datei, deren Ausfall den Build-Bruch damals durchgelassen haette."""
guard = (TOOLS_DIR / "mkimage" / "test_nl_mkimage.py").resolve()
assert guard.exists(), "tools/mkimage/test_nl_mkimage.py fehlt"
roots = pytest_paths()
assert any(guard == r or r in guard.parents for r in roots), (
"tools/mkimage/test_nl_mkimage.py wird von tools/test_host.sh nicht eingesammelt "
"- darin steht test_stays_python38_compatible, die einzige Wache gegen erneute "
"3.10-Syntax in nl_mkimage.py (der Docker-Build-Container hat Python 3.8.10)"
)
# Kein eigener "ist vermin installiert"-Test hier: test_nl_mkimage.py importiert vermin
# ohnehin hart (kein Skip), und ein fehlendes Paket faellt dort mit derselben Deutlichkeit
# auf. Zwei Tests fuer dieselbe Zusage sind nur zwei Stellen zum Pflegen.