"""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.