"""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(): return sorted( p.resolve() for p in TOOLS_DIR.rglob("test_*.py") if not any(part in IGNORED_PARTS for part in p.parts) ) 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. """ # 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("#") ] 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)) ) 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.