"""Låser testfilene med et fingeravtrykk (SHA-256). python laas.py lag [mappe] skriver tester.laas, gjøres av et menneske python laas.py sjekk [mappe] feiler (exit 1) hvis en testfil er endret Mappa er der testene ligger. Uten mappe brukes mappa laas.py ligger i. Alle test_*.py under mappa tas med, også i undermapper, og tester.laas skrives i mappa. Hvorfor et fingeravtrykk og ikke bare en instruks: en agent som skal få testene grønne, har to veier. Den kan rette koden, eller den kan rette testen. En setning i en prompt ber den la være. Denne sjekken merker det hvis den ikke gjorde det. Hva den ikke beskytter mot: tester.laas ligger i samme repo som testene. Den som kan endre testene, kan også kjøre «laas.py lag» og committe en ny lås sammen med den endrede testen, og da går sjekken grønn. Låsen er bare så sterk som kravet om at et menneske godkjenner endringer i tester.laas, for eksempel med CODEOWNERS og grenbeskyttelse i GitHub. Uten det fanger den uhell og snarveier, ikke en som er bestemt på å gå rundt. Linjeskift normaliseres før hashing. Ellers ville git på Windows, som gjerne bytter LF til CRLF, se ut som en endring av testene. """ import hashlib import pathlib import sys MAPPE = pathlib.Path(__file__).parent def fingeravtrykk(fil: pathlib.Path) -> str: innhold = fil.read_bytes().replace(b"\r\n", b"\n") return hashlib.sha256(innhold).hexdigest() def testfiler(mappe: pathlib.Path) -> dict: # Relativ sti med skråstrek, så låsfila blir lik på Windows og Linux. return {f.relative_to(mappe).as_posix(): f for f in sorted(mappe.rglob("test_*.py"))} def lag(mappe: pathlib.Path = MAPPE) -> None: filer = testfiler(mappe) linjer = [f"{fingeravtrykk(f)} {navn}" for navn, f in filer.items()] (mappe / "tester.laas").write_text("\n".join(linjer) + "\n", encoding="utf-8", newline="\n") print(f"Låste {len(linjer)} testfil(er) i tester.laas") def sjekk(mappe: pathlib.Path = MAPPE, laasfil: pathlib.Path | None = None) -> int: laasfil = laasfil or mappe / "tester.laas" if not laasfil.exists(): print(f"FEIL: finner ikke {laasfil}. Lag låsen først: python laas.py lag") return 1 laast = {} for linje in laasfil.read_text(encoding="utf-8").splitlines(): if linje.strip(): hash_, navn = linje.split(maxsplit=1) laast[navn] = hash_ naa = {navn: fingeravtrykk(f) for navn, f in testfiler(mappe).items()} feil = [] for navn, hash_ in laast.items(): if navn not in naa: feil.append(f"{navn} er slettet") elif naa[navn] != hash_: feil.append(f"{navn} er endret siden testene ble låst") for navn in sorted(naa.keys() - laast.keys()): feil.append(f"{navn} er ny og ikke låst") if feil: for f in feil: print("FEIL:", f) print("Testene er låst. Endringer i dem må godkjennes av et menneske.") return 1 print(f"OK: {len(laast)} testfil(er) uendret") return 0 if __name__ == "__main__": sys.stdout.reconfigure(encoding="utf-8") kommando = sys.argv[1] if len(sys.argv) > 1 else "" mappe = pathlib.Path(sys.argv[2]) if len(sys.argv) > 2 else MAPPE if kommando == "lag": lag(mappe) elif kommando == "sjekk": sys.exit(sjekk(mappe)) else: print(__doc__) sys.exit(2)