fix: findings from the independent review
Scenario runner: stops when overridden base parameters leave the start out of balance (new field startlikevekt: krev, kalibrer, ubalanse); stops on non-finite results; says that models with a court queue drift slowly even without a shock and prints the drift before the shock; queue in months of inflow; never deletes files it did not write; stricter limits. Check: anchored on the note's own checksums for core and reference files, runs the scenario files too, exact comparison for the diagnosis, honest wording when parts were skipped or an extension is broken. Launcher: unknown field names in input are rejected with a suggestion; unwritable folders give a message, not a traceback. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
parent
2669759f5a
commit
776325c680
14 changed files with 1524 additions and 199 deletions
|
|
@ -5,30 +5,48 @@ Bruk:
|
|||
python3 kjor.py kontroll [--rask]
|
||||
python3 kontroll.py [--rask] (med en Python som har numpy og scipy)
|
||||
|
||||
Fire kontroller, hver med én linje dom og detaljer bare ved avvik:
|
||||
1. Kjernen: sha256 for filene i kjerne/ mot kjerne/KILDE.json.
|
||||
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst).
|
||||
Kontrollene, hver med én linje dom og detaljer bare ved avvik:
|
||||
1. Kjernen og fasit: sha256 for filene i kjerne/ og fasit/ mot notatets verdier, som står som
|
||||
konstanter i denne fila (NOTATETS_KJERNE og NOTATETS_FASIT). Ankeret er notatet, ikke
|
||||
filene i samme mappe: kjerne/KILDE.json er bare en opplysning.
|
||||
2. diagnose.py på begge eksempelfilene mot fasit/ (tekst, eksakt: diagnose.py bruker bare
|
||||
standardbiblioteket, så ingen toleranse).
|
||||
3. forlop.py på begge eksempelfilene mot fasit/ (tekst; trenger numpy og scipy).
|
||||
4. Full kjøring av sim.py i en midlertidig mappe: datafilene mot fasit/data/ byte for byte,
|
||||
med største absolutte avvik per kolonne når en fil er ulik, og utskriften mot
|
||||
fasit/sim_utskrift.txt. Om lag 1-2 min; --rask hopper over den.
|
||||
I tillegg kjøres grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
||||
5. grensetilfelle() i hver utvidelse i utvidelser/; hvert avvik skal være eksakt 0.
|
||||
6. Scenariokjøreren: scenariofilene i eksempler/scenarioer/notatet/ som gjenskaper notatets
|
||||
datafiler byte for byte, kjørt gjennom scenario.py i denne modell-mappa, mot fasit/data/.
|
||||
De 13 raske er med i --rask; mpc11 (MPC, treg) bare i full kjøring.
|
||||
Er en fasit-fil endret i forhold til notatet, sier en sammenligning mot den ingenting om
|
||||
notatet; den regnes da som endret.
|
||||
|
||||
Nivået til slutt:
|
||||
- «bit for bit som notatet»: alt er likt byte for byte.
|
||||
- «bit for bit som notatet»: alt er kjørt og likt byte for byte (med --rask: alt unntatt den
|
||||
fulle kjøringen, og det står i linja).
|
||||
- «innenfor toleranse»: teksten er lik, og hvert tall avviker med høyst én enhet i siste
|
||||
viste siffer (det som kan skje med andre versjoner av numpy og scipy, eller en annen maskin).
|
||||
- «endret»: alt annet. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som
|
||||
arbeidskopi.py lager) er det en opplysning, ikke en feil: detaljene sier hvilke av
|
||||
notatets resultater som har flyttet seg, og hvor mye.
|
||||
- Mangler numpy eller scipy, sies bare at det som ble kjørt, var uten avvik, og hva som ikke
|
||||
er kontrollert.
|
||||
- «endret»: alt annet. Detaljene sier hvilke av notatets resultater som har flyttet seg, og
|
||||
hvor mye. I en arbeidskopi (mappa over modell/ har LES-MEG.md, som arbeidskopi.py lager) er
|
||||
det en opplysning, ikke en feil; ellers avhenger det av om modellen er endret med vilje.
|
||||
En utvidelse som ikke gjenskaper kjernen, er alltid en feil i utvidelsen.
|
||||
|
||||
Exit-kode: 0 for bit for bit og innenfor toleranse, 1 for endret, 3 når numpy eller scipy
|
||||
mangler for noe av kontrollen (det som kunne kjøres, er kjørt), 2 for feil bruk.
|
||||
|
||||
Når kjernen synkroniseres fra notatets repo (ny commit), må disse oppdateres sammen:
|
||||
kjerne/*.py, kjerne/KILDE.json, fasit/ (lages på nytt med notatets eget miljø), og
|
||||
konstantene NOTATETS_COMMIT, NOTATETS_KJERNE og NOTATETS_FASIT her. Sha256 for fasit/data/
|
||||
skal være lik notatets paper/data/ ved samme commit.
|
||||
|
||||
Grensesnitt: main(argv: list[str]) -> int, som kjor.py kaller.
|
||||
Utvidelser: utvidelser/<navn>.py (ikke filer som begynner med _) med
|
||||
grensetilfelle() -> list av (beskrivelse, største absolutte avvik mot kjernen). Hver
|
||||
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path.
|
||||
kjøres i en egen prosess med modell/kjerne og modell/ først i sys.path. Scenariokjøreren
|
||||
kjøres på samme måte: scenario.main([fil, "--ut", mappe]) skal skrive <navn>.dat i mappa.
|
||||
Bare standardbiblioteket her; kjernen kjøres som egne prosesser med samme Python.
|
||||
"""
|
||||
|
||||
|
|
@ -52,13 +70,57 @@ KJERNE = MODELL / "kjerne"
|
|||
ROT = MODELL.parent
|
||||
FASIT = ROT / "fasit"
|
||||
EKSEMPLER = ROT / "eksempler"
|
||||
SCENARIOER = EKSEMPLER / "scenarioer" / "notatet"
|
||||
EKSEMPELFILER = ("eksempel_domstol", "norge_2025")
|
||||
ARBEIDSKOPI_MERKE = "LES-MEG.md"
|
||||
|
||||
# Notatet: https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten @ 2badc25 (se synkroniseringen over).
|
||||
NOTATETS_REPO = "https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten"
|
||||
NOTATETS_COMMIT = "2badc25e5134ee26309bd8c3bd008ac64afe55a4"
|
||||
NOTATETS_KJERNE = {
|
||||
"sim.py": "96857a92492f5768bc5d1d98bec30f4292b603c1273913e69901b6a1b220a01e",
|
||||
"diagnose.py": "e5a20c68615836dd854c1414346c7932037eefaf1adb29a18458dbd590a3367a",
|
||||
"forlop.py": "5b369e30f98783b712f76e5dc90bcea4f711603944690c4f19da01efd29c6e21",
|
||||
}
|
||||
NOTATETS_FASIT = {
|
||||
"data/endo_des.dat": "cc200766c80dd486611b4f0a0e20e229c6f1842763b205e86760bf17458f594f",
|
||||
"data/endo_desp.dat": "66794618783bd098a16ddbb427c20d46795f6805e5a14f14122f86e3f8825847",
|
||||
"data/endo_e5.dat": "de137561fe952336f94008378f1bed4b72bbec91ca9613a054c47860086c5671",
|
||||
"data/endo_ko.dat": "ebbf9a096d04a5a52977303b221ee6d8144fc771354c86b7d0ba85262e543378",
|
||||
"data/kjede_s1.dat": "5f7597aea8a39909cfa3784f841a4e4f11cff8822721fa42a5ee90e3ffcd9ce2",
|
||||
"data/kjede_s2.dat": "9dedcc6821a286eca9f05d1a908e377f11c1e542baf7a30c3658b1128f7aacf9",
|
||||
"data/kjede_s3.dat": "a88c964f3a98c2eb962fa2066b3c1d1f4d092b0abfadc2b93c8cd6d747b9a68d",
|
||||
"data/kjede_s4.dat": "6b7dccdcade9eead1e483d046e745a1291b2238c2027f05ee38ee248787cd407",
|
||||
"data/kjede_s5.dat": "cdf936372eed8f3e1117508172f63e8c72f89294cb06caf994262652d044fe59",
|
||||
"data/mpc11.dat": "2609b5bb73efb12e31adf3d4b40c6798e93d40e6bd68afb2583a565d60cb3be0",
|
||||
"data/p11.dat": "526a839a393c2020fff0280a39c263e0ea73d7131ff86d3d9b734be919e884da",
|
||||
"data/p11d.dat": "dfa920ebeafef640735ce27eb98e50f6977e20b643a9c140a1672df86d8e8bf3",
|
||||
"data/p3.dat": "448def8fa662a3b68152d184a33888f7d5a0cfcdfe619064a51a47ff07d6037c",
|
||||
"data/p6.dat": "fcb78c473b4927aaf1a8360a6e2652d0363eaa6b2ac2a80ef551eb577ca4390d",
|
||||
"data/prio_s2.dat": "38389ac7a3d4cc2b5ad4cf54248f0a98d89bb21eb5c8f4f1eba799c6b4b5c7fc",
|
||||
"data/prio_s3.dat": "b675f87eefbcd4097732a258b6e81d06b5b5d96d4bcfbc8599270cc2079baf7e",
|
||||
"data/prio_s5.dat": "dd8744bd75ee27eceb9f312c4740058d13973cdcabf499fef895d2fe32c41813",
|
||||
"data/sat_B.dat": "6a24ce8f69964a055deaaa9260375b8e4cb1c6c09f1a113fd20f6f96a4251847",
|
||||
"data/sat_C.dat": "851d218a3a7772da65e3b19301cdf2006582358df873bab9fcaf7c3da0236fa7",
|
||||
"data/stab.dat": "9a09e14290634beafa1560a31097f76d26c592e7be46b7590e1480a01b60974e",
|
||||
"data/thumb.dat": "49c07f7ae6c23c9f728be27c422e76e8afa895b9bdd46c43ea2f5d5ebe7c767d",
|
||||
"data/vippe.dat": "7185a3edff28dede529a230504d63d85e4821096730acd293855cd74bd005ed1",
|
||||
"diagnose_eksempel_domstol.txt": "226212b0177b54a5402c2fd83d2df334184c7533b295bb3a72a277a4b99f82f0",
|
||||
"diagnose_norge_2025.txt": "3764c9236ec5afde9a47ebbeacec3e0d42cde98123a7d5a6e11c3fbec56a696e",
|
||||
"forlop_eksempel_domstol.txt": "b56cf2a7408d9611088eebaba787ab5d5a1dcdd3ee6116ce73dc226e1cea88c4",
|
||||
"forlop_norge_2025.txt": "ec75dae235031743816ad4b2d63830fcee6050e9e1477acb68269e7e9ded456a",
|
||||
"sim_utskrift.txt": "6225f9427845d915914a0bf78acf4359f6935d437f48cb94e1da48220a99eafd",
|
||||
}
|
||||
# Scenariofilene som gjenskaper notatets datafil med samme navn byte for byte.
|
||||
SCENARIO_RASKE = ("p3", "p6", "p11", "p11d", "kjede_s1", "kjede_s2", "kjede_s3", "kjede_s4", "kjede_s5",
|
||||
"endo_e5", "endo_des", "endo_desp", "endo_ko")
|
||||
SCENARIO_TREGE = ("mpc11",)
|
||||
|
||||
LIK, TOLERANSE, ENDRET, IKKE_KJORT = "lik", "toleranse", "endret", "ikke kjørt"
|
||||
RANG = {LIK: 0, TOLERANSE: 1, IKKE_KJORT: 2, ENDRET: 3}
|
||||
MAKS_LINJER = 40 # linjer med forskjeller som vises per sammenligning
|
||||
SLINGRING = 1e-9 # relativ slingring når avviket sammenlignes med én enhet i siste siffer
|
||||
UTVIDELSER = "Utvidelser" # merkelappen for kontroll 5
|
||||
|
||||
TALL = re.compile(r"[-+]?\d+(?:[.,]\d+)?(?:[eE][-+]?\d+)?")
|
||||
|
||||
|
|
@ -75,6 +137,32 @@ print()
|
|||
print(json.dumps([[str(b), float(a)] for b, a in modul.grensetilfelle()]))
|
||||
"""
|
||||
|
||||
SCENARIO_KJORER = """\
|
||||
import contextlib, importlib.util, io, json, os, sys, traceback
|
||||
modell, ut, filer = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
sys.path[:0] = [os.path.join(modell, "kjerne"), modell]
|
||||
spec = importlib.util.spec_from_file_location("scenario", os.path.join(modell, "scenario.py"))
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
sys.modules["scenario"] = modul
|
||||
spec.loader.exec_module(modul)
|
||||
svar = []
|
||||
for fil in filer:
|
||||
navn = os.path.splitext(os.path.basename(fil))[0]
|
||||
feil = io.StringIO()
|
||||
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(feil):
|
||||
try:
|
||||
kode = modul.main([fil, "--ut", os.path.join(ut, navn)])
|
||||
except SystemExit as stopp:
|
||||
kode = stopp.code if isinstance(stopp.code, int) else 2
|
||||
feil.write(str(stopp.code or ""))
|
||||
except Exception:
|
||||
kode = 2
|
||||
feil.write(traceback.format_exc())
|
||||
svar.append([navn, kode, feil.getvalue()[-3000:]])
|
||||
print()
|
||||
print(json.dumps(svar))
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dom:
|
||||
|
|
@ -84,6 +172,7 @@ class Dom:
|
|||
linje: str
|
||||
detaljer: list[str] = field(default_factory=list)
|
||||
avvik: float = 0.0
|
||||
ikke_kjort: str = "" # hva som ikke ble kontrollert, når status er IKKE_KJORT
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -226,44 +315,77 @@ def sha256(fil: Path) -> str:
|
|||
return hashlib.sha256(fil.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
|
||||
def lest_fasit(rel: str, endret_fasit: set[str]) -> str | None:
|
||||
"""Innholdet i fasit/<rel>, eller None når fila mangler eller er endret i forhold til notatet."""
|
||||
fil = FASIT / rel
|
||||
if rel in endret_fasit or not fil.is_file():
|
||||
return None
|
||||
return fil.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def ugyldig_fasit(rel: str) -> str:
|
||||
"""Detaljen når en fasit-fil ikke kan brukes som notatets verdi."""
|
||||
if not (FASIT / rel).is_file():
|
||||
return f"fant ikke fasit/{rel}"
|
||||
return f"fasit/{rel} er endret i forhold til notatet, så sammenligningen sier ingenting om notatet"
|
||||
|
||||
|
||||
# ---------------- kontrollene ----------------
|
||||
|
||||
def kontroll_kjerne() -> Dom:
|
||||
"""1. Kjernen mot sha256 i KILDE.json."""
|
||||
kildefil = KJERNE / "KILDE.json"
|
||||
if not kildefil.is_file():
|
||||
return Dom(ENDRET, f"Kjernen: fant ikke {kildefil}, så kjernen kan ikke sammenlignes med notatet.")
|
||||
kilde = json.loads(kildefil.read_text(encoding="utf-8"))
|
||||
"""1. Kjernen mot notatets sha256 (konstantene over, ikke KILDE.json)."""
|
||||
endret = []
|
||||
for navn, info in kilde["filer"].items():
|
||||
for navn, verdi in NOTATETS_KJERNE.items():
|
||||
fil = KJERNE / navn
|
||||
if not fil.is_file():
|
||||
endret.append(f"{navn} mangler")
|
||||
elif sha256(fil) != info["sha256"]:
|
||||
elif sha256(fil) != verdi:
|
||||
endret.append(f"{navn} er endret")
|
||||
versjon = f"{kilde['repo']} @ {kilde['commit'][:7]}"
|
||||
versjon = f"{NOTATETS_REPO} @ {NOTATETS_COMMIT[:7]}"
|
||||
if not endret:
|
||||
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(kilde['filer'])} filer, {versjon}).")
|
||||
return Dom(LIK, f"Kjernen: uendret fra notatet ({len(NOTATETS_KJERNE)} filer, {versjon}).")
|
||||
return Dom(ENDRET, f"Kjernen: endret fra notatet ({versjon}): {', '.join(endret)}.")
|
||||
|
||||
|
||||
def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
||||
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit."""
|
||||
def endrede_fasitfiler() -> set[str]:
|
||||
"""Filene i fasit/ som mangler eller ikke har notatets sha256."""
|
||||
return {rel for rel, verdi in NOTATETS_FASIT.items()
|
||||
if not (FASIT / rel).is_file() or sha256(FASIT / rel) != verdi}
|
||||
|
||||
|
||||
def kontroll_fasit(endret_fasit: set[str]) -> Dom:
|
||||
"""1. Fasit mot notatets sha256."""
|
||||
if not endret_fasit:
|
||||
return Dom(LIK, f"Fasit: uendret fra notatet ({len(NOTATETS_FASIT)} filer).")
|
||||
detaljer = [f"fasit/{rel}: " + ("mangler" if not (FASIT / rel).is_file() else "har ikke notatets sha256")
|
||||
for rel in sorted(endret_fasit)]
|
||||
return Dom(ENDRET, f"Fasit: fasit er endret i forhold til notatet ({len(endret_fasit)} av "
|
||||
f"{len(NOTATETS_FASIT)} filer).", detaljer, math.inf)
|
||||
|
||||
|
||||
def kontroll_utskrift(skript: str, mappe: Path, endret_fasit: set[str], *, toleranse: bool) -> Dom:
|
||||
"""2 og 3. Utskriften fra diagnose.py eller forlop.py på begge eksempelfilene mot fasit.
|
||||
|
||||
toleranse=False: bare byte-lik tekst godtas (diagnose.py avhenger ikke av numpy og scipy).
|
||||
"""
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
for eksempel in EKSEMPELFILER:
|
||||
fasitfil = FASIT / f"{skript}_{eksempel}.txt"
|
||||
rel = f"{skript}_{eksempel}.txt"
|
||||
fasit = lest_fasit(rel, endret_fasit)
|
||||
inndata = EKSEMPLER / f"{eksempel}.json"
|
||||
mangler = [str(f) for f in (fasitfil, inndata) if not f.is_file()]
|
||||
if mangler:
|
||||
if fasit is None or not inndata.is_file():
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{eksempel}: fant ikke {', '.join(mangler)}")
|
||||
detaljer.append(f"{eksempel}: " + (ugyldig_fasit(rel) if fasit is None else f"fant ikke {inndata}"))
|
||||
continue
|
||||
prosess = kjor_python([str(KJERNE / f"{skript}.py"), str(inndata)], mappe)
|
||||
if prosess.returncode != 0:
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{eksempel}:", *(" " + linje for linje in feilmelding(prosess))]
|
||||
continue
|
||||
resultat = sammenlign_tekst(fasitfil.read_text(encoding="utf-8"), prosess.stdout)
|
||||
resultat = sammenlign_tekst(fasit, prosess.stdout)
|
||||
if resultat.status == TOLERANSE and not toleranse:
|
||||
resultat.status = ENDRET
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status != LIK:
|
||||
|
|
@ -278,23 +400,27 @@ def kontroll_utskrift(skript: str, mappe: Path) -> Dom:
|
|||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
def kontroll_full(mappe: Path) -> Dom:
|
||||
def kontroll_full(mappe: Path, endret_fasit: set[str]) -> Dom:
|
||||
"""4. Full kjøring av sim.py i en midlertidig mappe mot fasit/data og fasit/sim_utskrift.txt."""
|
||||
print(" (kjører sim.py, om lag 1-2 minutter ...)", flush=True)
|
||||
prosess = kjor_python([str(KJERNE / "sim.py")], mappe)
|
||||
if prosess.returncode != 0:
|
||||
return Dom(ENDRET, "Full kjøring: sim.py stoppet med en feil.", feilmelding(prosess), math.inf)
|
||||
fasit_data = FASIT / "data"
|
||||
navn_fasit = sorted(f.name for f in fasit_data.glob("*.dat"))
|
||||
navn_fasit = sorted(rel[len("data/"):] for rel in NOTATETS_FASIT if rel.startswith("data/"))
|
||||
navn_naa = sorted(f.name for f in (mappe / "data").glob("*.dat"))
|
||||
statuser, detaljer, storst, like = [], [], 0.0, 0
|
||||
for navn in sorted(set(navn_fasit) | set(navn_naa)):
|
||||
if navn not in navn_naa or navn not in navn_fasit:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa else "finnes ikke i fasit"))
|
||||
detaljer.append(f"{navn}: " + ("mangler i kjøringen" if navn not in navn_naa
|
||||
else "er ikke en av notatets datafiler"))
|
||||
continue
|
||||
resultat = sammenlign_dat((fasit_data / navn).read_text(encoding="utf-8"),
|
||||
(mappe / "data" / navn).read_text(encoding="utf-8"))
|
||||
fasit = lest_fasit(f"data/{navn}", endret_fasit)
|
||||
if fasit is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{navn}: {ugyldig_fasit(f'data/{navn}')}")
|
||||
continue
|
||||
resultat = sammenlign_dat(fasit, (mappe / "data" / navn).read_text(encoding="utf-8"))
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status == LIK:
|
||||
|
|
@ -302,11 +428,11 @@ def kontroll_full(mappe: Path) -> Dom:
|
|||
else:
|
||||
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
||||
detaljer.append(f"{navn} ({dom}): {'; '.join(resultat.detaljer)}")
|
||||
utskriftfil = FASIT / "sim_utskrift.txt"
|
||||
if utskriftfil.is_file():
|
||||
utskrift = sammenlign_tekst(utskriftfil.read_text(encoding="utf-8"), prosess.stdout)
|
||||
fasit_utskrift = lest_fasit("sim_utskrift.txt", endret_fasit)
|
||||
if fasit_utskrift is None:
|
||||
utskrift = Sammenligning(ENDRET, math.inf, [ugyldig_fasit("sim_utskrift.txt")])
|
||||
else:
|
||||
utskrift = Sammenligning(ENDRET, math.inf, [f"fant ikke {utskriftfil}"])
|
||||
utskrift = sammenlign_tekst(fasit_utskrift, prosess.stdout)
|
||||
statuser.append(utskrift.status)
|
||||
storst = max(storst, utskrift.avvik)
|
||||
if utskrift.status != LIK:
|
||||
|
|
@ -318,40 +444,112 @@ def kontroll_full(mappe: Path) -> Dom:
|
|||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
def grensetilfeller(fil: Path, mappe: Path) -> tuple[list[str], list[str], float]:
|
||||
"""Kjør grensetilfelle() i én utvidelse. Returnerer statuser, detaljer og største avvik."""
|
||||
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
||||
try:
|
||||
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
tilfeller = None
|
||||
if tilfeller is None:
|
||||
return [ENDRET], [f"{fil.stem}: grensetilfelle() kunne ikke kjøres",
|
||||
*(" " + linje for linje in feilmelding(prosess))], math.inf
|
||||
if not tilfeller:
|
||||
return [ENDRET], [f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett."], 0.0
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
storst = max(storst, abs(avvik))
|
||||
if avvik == 0.0:
|
||||
statuser.append(LIK)
|
||||
continue
|
||||
statuser.append(ENDRET)
|
||||
hvem = beskrivelse if str(beskrivelse).startswith(fil.stem) else f"{fil.stem}: {beskrivelse}"
|
||||
detaljer.append(f"{hvem}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
||||
return statuser, detaljer, storst
|
||||
|
||||
|
||||
def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
||||
"""Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
||||
"""5. Grensetilfellet i hver utvidelse skal gjenskape kjernen eksakt (avvik 0)."""
|
||||
utvidelser = sorted(f for f in (MODELL / "utvidelser").glob("*.py") if not f.name.startswith("_"))
|
||||
if not utvidelser:
|
||||
return Dom(LIK, "Utvidelser: ingen å kontrollere.")
|
||||
return Dom(LIK, f"{UTVIDELSER}: ingen å kontrollere.")
|
||||
if not regnemiljo:
|
||||
return Dom(IKKE_KJORT, f"Utvidelser: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.")
|
||||
statuser, detaljer, storst = [], [], 0.0
|
||||
return Dom(IKKE_KJORT, f"{UTVIDELSER}: ikke kjørt ({len(utvidelser)} stk.), numpy og scipy mangler.",
|
||||
ikke_kjort="utvidelsenes grensetilfeller")
|
||||
statuser, detaljer, storst, feilet = [], [], 0.0, []
|
||||
for fil in utvidelser:
|
||||
prosess = kjor_python(["-c", UTVIDELSE_KJORER, str(MODELL), str(fil)], mappe)
|
||||
try:
|
||||
tilfeller = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
tilfeller = None
|
||||
if tilfeller is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{fil.stem}: grensetilfelle() kunne ikke kjøres", *(" " + linje for linje in
|
||||
feilmelding(prosess))]
|
||||
continue
|
||||
if not tilfeller:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{fil.stem}: grensetilfelle() ga ingen tilfeller; en utvidelse skal ha minst ett.")
|
||||
continue
|
||||
for beskrivelse, avvik in tilfeller:
|
||||
storst = max(storst, abs(avvik))
|
||||
if avvik != 0.0:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{fil.stem}: {beskrivelse}: avvik {vis_tall(abs(avvik))}, skal være eksakt 0")
|
||||
else:
|
||||
statuser.append(LIK)
|
||||
status = verst(statuser)
|
||||
egne, egne_detaljer, avvik = grensetilfeller(fil, mappe)
|
||||
statuser += egne
|
||||
detaljer += egne_detaljer
|
||||
storst = max(storst, avvik)
|
||||
if ENDRET in egne:
|
||||
feilet.append(fil.stem)
|
||||
navn = ", ".join(f.stem for f in utvidelser)
|
||||
linje = (f"Utvidelser: grensetilfellene gjenskaper kjernen eksakt ({navn})." if status == LIK
|
||||
else f"Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt ({navn}; se under).")
|
||||
if not feilet:
|
||||
return Dom(LIK, f"{UTVIDELSER}: grensetilfellene gjenskaper kjernen eksakt ({navn}).", [], storst)
|
||||
linje = (f"{UTVIDELSER}: {', '.join(feilet)} gjenskaper ikke kjernen i grensetilfellet. Det er en feil i "
|
||||
"utvidelsen, og resultatene den gir, kan ikke brukes (se under).")
|
||||
return Dom(ENDRET, linje, detaljer, storst)
|
||||
|
||||
|
||||
def kjor_scenarioer(navn: tuple[str, ...], mappe: Path) -> tuple[dict[str, tuple[int, str]], list[str]]:
|
||||
"""Kjør scenariofilene gjennom scenario.py i en egen prosess.
|
||||
|
||||
Returnerer {navn: (exit-kode, stderr)} og feillinjer når prosessen selv stoppet.
|
||||
"""
|
||||
ut = mappe / "scenarioer"
|
||||
filer = [str(SCENARIOER / f"{n}.json") for n in navn]
|
||||
prosess = kjor_python(["-c", SCENARIO_KJORER, str(MODELL), str(ut), *filer], mappe)
|
||||
try:
|
||||
svar = json.loads(prosess.stdout.strip().splitlines()[-1]) if prosess.returncode == 0 else None
|
||||
except (ValueError, IndexError):
|
||||
svar = None
|
||||
if svar is None:
|
||||
return {}, ["scenario.py kunne ikke lastes:", *(" " + linje for linje in feilmelding(prosess))]
|
||||
return {str(n): (int(kode), str(feil)) for n, kode, feil in svar}, []
|
||||
|
||||
|
||||
def kontroll_scenario(mappe: Path, endret_fasit: set[str], *, rask: bool, regnemiljo: bool) -> Dom:
|
||||
"""6. Scenariokjøreren gjenskaper notatets datafiler fra scenariofilene i eksempler/scenarioer/notatet/."""
|
||||
navn = SCENARIO_RASKE if rask else SCENARIO_RASKE + SCENARIO_TREGE
|
||||
if not (MODELL / "scenario.py").is_file():
|
||||
return Dom(ENDRET, f"Scenariokjøreren: fant ikke {MODELL / 'scenario.py'}, så den er ikke kontrollert.",
|
||||
[], math.inf)
|
||||
mangler = [n for n in navn if not (SCENARIOER / f"{n}.json").is_file()]
|
||||
if mangler:
|
||||
return Dom(ENDRET, f"Scenariokjøreren: fant ikke {len(mangler)} av {len(navn)} scenariofiler i "
|
||||
f"{SCENARIOER}, så den er ikke kontrollert.", [f"mangler: {', '.join(mangler)}"], math.inf)
|
||||
if not regnemiljo:
|
||||
return Dom(IKKE_KJORT, "Scenariokjøreren: ikke kjørt, numpy og scipy mangler.",
|
||||
ikke_kjort="scenariokjøreren")
|
||||
resultater, feil = kjor_scenarioer(navn, mappe)
|
||||
if feil:
|
||||
return Dom(ENDRET, "Scenariokjøreren: scenario.py stoppet før noen fil var kjørt.", feil, math.inf)
|
||||
statuser, detaljer, storst, like = [], [], 0.0, 0
|
||||
for n in navn:
|
||||
kode, melding = resultater.get(n, (2, "ble ikke kjørt"))
|
||||
fasit = lest_fasit(f"data/{n}.dat", endret_fasit)
|
||||
laget = mappe / "scenarioer" / n / f"{n}.dat"
|
||||
if kode != 0 or not laget.is_file():
|
||||
statuser.append(ENDRET)
|
||||
detaljer += [f"{n}.json: stoppet med exit-kode {kode}" if kode != 0 else f"{n}.json: laget ikke {n}.dat",
|
||||
*(" " + linje for linje in melding.strip().splitlines()[-MAKS_LINJER // 2:])]
|
||||
continue
|
||||
if fasit is None:
|
||||
statuser.append(ENDRET)
|
||||
detaljer.append(f"{n}.dat: {ugyldig_fasit(f'data/{n}.dat')}")
|
||||
continue
|
||||
resultat = sammenlign_dat(fasit, laget.read_text(encoding="utf-8"))
|
||||
statuser.append(resultat.status)
|
||||
storst = max(storst, resultat.avvik)
|
||||
if resultat.status == LIK:
|
||||
like += 1
|
||||
else:
|
||||
dom = "innenfor toleranse" if resultat.status == TOLERANSE else "endret"
|
||||
detaljer.append(f"{n}.dat ({dom}): {'; '.join(resultat.detaljer)}")
|
||||
status = verst(statuser)
|
||||
linje = (f"Scenariokjøreren: {like} av {len(navn)} av notatets datafiler gjenskapt byte for byte fra "
|
||||
"eksempler/scenarioer/notatet/" + (" (mpc11 bare i full kjøring)." if rask else "."))
|
||||
return Dom(status, linje, detaljer, storst)
|
||||
|
||||
|
||||
|
|
@ -360,25 +558,35 @@ def kontroll_utvidelser(mappe: Path, *, regnemiljo: bool) -> Dom:
|
|||
def niva(dommer: list[Dom], *, rask: bool, arbeidskopi: bool) -> list[str]:
|
||||
"""Linjene om nivået som er oppnådd."""
|
||||
kjort = [d for d in dommer if d.status != IKKE_KJORT]
|
||||
hoppet_over = [d.ikke_kjort for d in dommer if d.status == IKKE_KJORT]
|
||||
status = verst([d.status for d in kjort])
|
||||
deler = (["uten full kjøring, --rask"] if rask else []) + (
|
||||
["bare det som er kjørt"] if any(d.status == IKKE_KJORT for d in dommer) else [])
|
||||
hva = f" ({'; '.join(deler)})" if deler else ""
|
||||
if status == LIK:
|
||||
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
||||
hva = " (uten full kjøring, --rask)" if rask else ""
|
||||
if status == ENDRET:
|
||||
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
||||
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
||||
if UTVIDELSER in endret:
|
||||
linjer.append("En utvidelse som ikke gjenskaper kjernen i grensetilfellet, har en feil. Resultatene den "
|
||||
"gir, kan ikke brukes før den er rettet.")
|
||||
if [e for e in endret if e != UTVIDELSER]:
|
||||
if arbeidskopi:
|
||||
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
||||
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
||||
else:
|
||||
linjer.append("Er dette pluginen slik den er installert, er det en feil: pluginens modell skal regne "
|
||||
"som notatet. Er modellen endret med vilje (for eksempel i en klone av repoet), er det "
|
||||
"en opplysning: detaljene over viser hvilke av notatets resultater som har flyttet "
|
||||
"seg, og hvor mye.")
|
||||
elif hoppet_over:
|
||||
kjorte = [d.linje.split(":")[0] for d in kjort]
|
||||
linjer = [(f"Nivå{hva}: ingen avvik i det som ble kjørt ({', '.join(kjorte)}). Ikke kontrollert: "
|
||||
f"{', '.join(hoppet_over)}.")]
|
||||
elif status == TOLERANSE:
|
||||
storst = max(d.avvik for d in kjort)
|
||||
linjer = [(f"Nivå{hva}: innenfor toleranse (største avvik {vis_tall(storst)}): hvert tall avviker med "
|
||||
"høyst én enhet i siste viste siffer.")]
|
||||
else:
|
||||
endret = [d.linje.split(":")[0] for d in kjort if d.status == ENDRET]
|
||||
linjer = [f"Nivå{hva}: endret: {', '.join(endret)}."]
|
||||
if arbeidskopi:
|
||||
linjer.append("Dette er en arbeidskopi, så det er en opplysning, ikke en feil: detaljene over viser "
|
||||
"hvilke av notatets resultater som har flyttet seg, og hvor mye.")
|
||||
else:
|
||||
linjer.append("Pluginens modell skal regne som notatet, så dette er en feil.")
|
||||
if any(d.status == IKKE_KJORT for d in dommer):
|
||||
linjer = [f"Nivå{hva}: bit for bit som notatet."]
|
||||
if hoppet_over:
|
||||
linjer.append(f"Ikke kjørt fordi numpy eller scipy mangler i {sys.executable}. "
|
||||
"«kjor.py miljo --opprett» installerer dem i en egen mappe.")
|
||||
return linjer
|
||||
|
|
@ -402,21 +610,25 @@ def main(argv: list[str]) -> int:
|
|||
print(f" {linje}")
|
||||
sys.stdout.flush()
|
||||
|
||||
endret_fasit = endrede_fasitfiler()
|
||||
with tempfile.TemporaryDirectory(prefix="dynamisk-rettsstat-") as tmp:
|
||||
mappe = Path(tmp)
|
||||
vis("1.", kontroll_kjerne())
|
||||
vis("2.", kontroll_utskrift("diagnose", mappe))
|
||||
vis("1.", kontroll_fasit(endret_fasit))
|
||||
vis("2.", kontroll_utskrift("diagnose", mappe, endret_fasit, toleranse=False))
|
||||
if regnemiljo:
|
||||
vis("3.", kontroll_utskrift("forlop", mappe))
|
||||
vis("3.", kontroll_utskrift("forlop", mappe, endret_fasit, toleranse=True))
|
||||
else:
|
||||
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler."))
|
||||
vis("3.", Dom(IKKE_KJORT, "forlop: ikke kjørt, numpy og scipy mangler.", ikke_kjort="forlop"))
|
||||
if rask:
|
||||
print("4. Full kjøring: hoppet over (--rask).")
|
||||
elif regnemiljo:
|
||||
vis("4.", kontroll_full(mappe))
|
||||
vis("4.", kontroll_full(mappe, endret_fasit))
|
||||
else:
|
||||
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler."))
|
||||
vis("4.", Dom(IKKE_KJORT, "Full kjøring: ikke kjørt, numpy og scipy mangler.",
|
||||
ikke_kjort="full kjøring av sim.py"))
|
||||
vis("5.", kontroll_utvidelser(mappe, regnemiljo=regnemiljo))
|
||||
vis("6.", kontroll_scenario(mappe, endret_fasit, rask=rask, regnemiljo=regnemiljo))
|
||||
print()
|
||||
print("\n".join(niva(dommer, rask=rask, arbeidskopi=arbeidskopi)))
|
||||
statuser = [d.status for d in dommer]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue