Files
Projektwissen/scripts/test_audit.py
T
DanielandClaude Sonnet 5 ec400ae59e Test-Audit: Datei:Zeile und Quelltext-Ausgabe für bequemes Ansehen/Editieren
status zeigt je Treffer "<Datei>:<Zeile>" (in Editor-Terminals anklickbar)
und optional per --code den Testquelltext im Terminal. Der Prüfstand selbst
bleibt ohne Quelltext, um Diff-Lärm bei jeder Codeänderung zu vermeiden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 12:45:12 +02:00

205 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Test-Audit: zeigt und pflegt, wann ein Testfall zuletzt fachlich geprüft wurde.
Der Testkatalog (scripts/testkatalog.py) beantwortet "was prüft die Suite" und "läuft der Test
durch" (Baseline). Er beantwortet NICHT "hat das ein Mensch fachlich bestätigt" – ein Agent kann
einen überzeugenden, aber fachlich falschen Test schreiben, und weder Docstring noch Baseline
zeigen das. Dieses Skript schließt genau diese Lücke: es liest/schreibt
docs/status/testpruefungen.json (Datum + Kurzprüfsumme der Testfunktion je Testfall) und macht so
sichtbar, welche Tests nie geprüft wurden oder sich seit der letzten Prüfung geändert haben.
Reine Standardbibliothek, kein Import von sn_basis/QGIS nötig, da nur mit ast geparst wird
(genau wie testkatalog.py, dessen Funktionen hier wiederverwendet werden).
`status` zeigt je Treffer "<datei>:<zeile>" – in den meisten Terminals/Editoren (z. B. VS Code) anklickbar,
um den Test direkt zum Bearbeiten zu öffnen. `--code` gibt zusätzlich den Testquelltext im Terminal aus, zum
schnellen fachlichen Lesen ohne Editor. Bearbeitet wird die Testdatei ganz normal über die Konsole/den Editor –
dieses Skript schreibt nur `docs/status/testpruefungen.json`, nie Testdateien.
Aufrufe (aus dem Workspace-Root):
python scripts/test_audit.py status # alle Pakete
python scripts/test_audit.py status sn_verfahrensgebiet # nur ein Paket
python scripts/test_audit.py status sn_basis --nur ungeprueft # Filter auf Prüfstatus
python scripts/test_audit.py status --muster grenzpunkt # Filter auf Bezeichner
python scripts/test_audit.py status sn_basis --muster Test_x --code # mit Quelltext (zum Lesen, nicht editierbar)
python scripts/test_audit.py markieren sn_basis TestHallo.test_x # als heute geprüft markieren
python scripts/test_audit.py markieren sn_basis "TestX.*" --trocken # nur anzeigen, nichts schreiben
python scripts/test_audit.py markieren sn_basis TestX.test_x --datum 2026-08-01 # Nacherfassung
Exit-Code: 0 = in Ordnung, 1 = kein Treffer beim Markieren, 2 = falsche Angabe (unbekanntes Paket,
ungültiges Datum).
"""
from __future__ import annotations
import argparse
import datetime
import fnmatch
import json
import sys
from pathlib import Path
import testkatalog as tk
ROOT = tk.ROOT
def lade_pakete(manifest: dict) -> dict[str, dict]:
return {r["paket"]: r for r in manifest["repos"] if r.get("paket")}
def waehle_pakete(gewuenscht: list[str], repos: dict[str, dict]) -> list[str] | None:
"""Löst die gewünschten Paketnamen auf; leer bedeutet alle mit tests=true. None bei unbekanntem Namen."""
testbare = [p for p, r in repos.items() if r.get("tests")]
ziel = gewuenscht or testbare
unbekannt = [p for p in ziel if p not in repos]
if unbekannt:
print(f"Unbekannte Pakete: {unbekannt}. Bekannt: {sorted(repos)}")
return None
return ziel
def sammle(paket: str, repos: dict[str, dict]) -> tk.Paketkatalog | None:
repo = repos[paket]
if not repo.get("tests"):
print(f"[{paket}] laut workspace.toml ohne Tests – übersprungen")
return None
katalog = tk.sammle_paket(paket, repo)
if katalog is None:
print(f"[{paket}] kein Ordner {repo['name']}/tests – übersprungen")
return katalog
NUR_ZU_PRUEFSTATUS = {
"ungeprueft": "nie geprüft",
"veraltet": "veraltet",
"aktuell": "aktuell geprüft",
}
def befehl_status(args: argparse.Namespace) -> int:
manifest = tk.lade_manifest()
repos = lade_pakete(manifest)
pakete = waehle_pakete(args.pakete, repos)
if pakete is None:
return 2
rot_je_paket, _ = tk.lies_baseline(args.baseline_datei)
pruefungen, _ = tk.lies_pruefungen(args.pruefungsdatei)
gesucht_status = NUR_ZU_PRUEFSTATUS.get(args.nur) if args.nur else None
for paket in pakete:
katalog = sammle(paket, repos)
if katalog is None:
continue
tk.setze_status(katalog, rot_je_paket.get(paket, set()))
tk.setze_pruefstatus(katalog, pruefungen)
k = tk.zaehle(katalog.faelle)
print(
f"[{paket}] {k['gesamt']} Testfälle – {k['aktuell_geprueft']} aktuell geprüft, "
f"{k['veraltet_geprueft']} veraltet, {k['nie_geprueft']} nie geprüft"
)
for datei in katalog.dateien:
for fall in datei.faelle:
if args.muster and args.muster.lower() not in fall.bezeichnung.lower():
continue
if gesucht_status and fall.pruefstatus != gesucht_status:
continue
print(
f" {datei.anzeigepfad}:{fall.zeile} {fall.bezeichnung} -> {tk.pruef_text(fall)}"
)
if args.code:
for zeile in fall.quelltext.splitlines():
print(f" {zeile}")
return 0
def schreibe_pruefungen(pfad: Path, pruefungen: dict) -> None:
"""Schreibt die Prüf-Historie deterministisch (sortierte Schlüssel, feste Formatierung)."""
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_text(
json.dumps(pruefungen, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
encoding="utf-8",
newline="\n",
)
def befehl_markieren(args: argparse.Namespace) -> int:
try:
datum = datetime.date.fromisoformat(args.datum).isoformat()
except ValueError:
print(f"Ungültiges Datum: {args.datum} (erwartet JJJJ-MM-TT)")
return 2
manifest = tk.lade_manifest()
repos = lade_pakete(manifest)
if args.paket not in repos:
print(f"Unbekanntes Paket: {args.paket}. Bekannt: {sorted(repos)}")
return 2
katalog = sammle(args.paket, repos)
if katalog is None:
return 2
treffer: list[tuple[tk.Testdatei, tk.Testfall]] = []
gesehen: set[str] = set()
for datei in katalog.dateien:
for fall in datei.faelle:
voll = tk.voller_testpfad(datei, fall)
if voll in gesehen:
continue
if any(fnmatch.fnmatchcase(fall.bezeichnung, muster) for muster in args.bezeichner):
treffer.append((datei, fall))
gesehen.add(voll)
if not treffer:
print(f"Kein Testfall in {args.paket} passt zu {args.bezeichner}.")
return 1
pruefungen, _ = tk.lies_pruefungen(args.pruefungsdatei)
tk.setze_pruefstatus(katalog, pruefungen)
for datei, fall in treffer:
print(f"{datei.anzeigepfad}:{fall.zeile} {fall.bezeichnung} (vorher: {tk.pruef_text(fall)})")
if args.trocken:
print(f"Trockenlauf – nichts geschrieben. {len(treffer)} Treffer.")
return 0
for datei, fall in treffer:
pruefungen[tk.voller_testpfad(datei, fall)] = {"datum": datum, "hash": fall.quellhash}
schreibe_pruefungen(args.pruefungsdatei, pruefungen)
print(f"{len(treffer)} Testfälle als geprüft am {datum} markiert: {args.pruefungsdatei}")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
unter = ap.add_subparsers(dest="befehl", required=True)
p_status = unter.add_parser("status", help="Prüfstatus je Testfall anzeigen")
p_status.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle mit tests=true)")
p_status.add_argument("--muster", help="nur Testfälle, deren Bezeichner diesen Text enthält (Teilstring)")
p_status.add_argument("--nur", choices=sorted(NUR_ZU_PRUEFSTATUS), help="nur diesen Prüfstatus anzeigen")
p_status.add_argument("--code", action="store_true", help="Quelltext jeder passenden Testmethode mit ausgeben")
p_status.add_argument("--baseline-datei", type=Path, default=tk.BASELINE_DEFAULT)
p_status.add_argument("--pruefungsdatei", type=Path, default=tk.PRUEFUNGEN_DEFAULT)
p_status.set_defaults(func=befehl_status)
p_markieren = unter.add_parser("markieren", help="Testfälle als heute geprüft markieren")
p_markieren.add_argument("paket", help="Paketname, z. B. sn_basis")
p_markieren.add_argument(
"bezeichner", nargs="+", help="Klasse.methode, exakt oder als fnmatch-Muster (z. B. 'TestX.*')"
)
p_markieren.add_argument("--datum", default=datetime.date.today().isoformat(), help="Standard: heute")
p_markieren.add_argument("--trocken", action="store_true", help="nur anzeigen, nichts schreiben")
p_markieren.add_argument("--pruefungsdatei", type=Path, default=tk.PRUEFUNGEN_DEFAULT)
p_markieren.set_defaults(func=befehl_markieren)
args = ap.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())