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
|
|
@ -3,12 +3,15 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import kontroll
|
||||
import pytest
|
||||
from conftest import kjor
|
||||
from conftest import FASIT, kjor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
|
@ -70,23 +73,53 @@ def test_dat_ulik_form(naa: str) -> None:
|
|||
|
||||
# ---------------- kontrollen som helhet ----------------
|
||||
|
||||
FALSK_SCENARIO = """\
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main(argv):
|
||||
fil, ut = Path(argv[0]), Path(argv[2])
|
||||
ut.mkdir(parents=True, exist_ok=True)
|
||||
fasit = Path(__file__).resolve().parent.parent / "fasit" / "data" / (fil.stem + ".dat")
|
||||
shutil.copyfile(fasit, ut / (fil.stem + ".dat"))
|
||||
return 0
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lett_kopi(kopi: Path) -> Path:
|
||||
"""En kopi der scenario.py er byttet mot en som kopierer fasit, så kontroll 6 går fort.
|
||||
|
||||
Brukes av testene som gjelder andre kontroller; scenariokjøreren selv prøves på pluginen og i egne tester.
|
||||
"""
|
||||
(kopi / "modell" / "scenario.py").write_text(FALSK_SCENARIO, encoding="utf-8")
|
||||
return kopi
|
||||
|
||||
|
||||
def test_rask_kontroll_av_pluginen(hjem: Path) -> None:
|
||||
prosess = kjor("kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
|
||||
assert "1. Kjernen: uendret fra notatet (3 filer," in prosess.stdout
|
||||
assert "1. Fasit: uendret fra notatet (27 filer)." in prosess.stdout
|
||||
assert "2. diagnose: lik notatet for eksempel_domstol og norge_2025." in prosess.stdout
|
||||
assert "3. forlop: lik notatet for eksempel_domstol og norge_2025." in prosess.stdout
|
||||
assert "4. Full kjøring: hoppet over (--rask)." in prosess.stdout
|
||||
assert ("6. Scenariokjøreren: 13 av 13 av notatets datafiler gjenskapt byte for byte fra "
|
||||
"eksempler/scenarioer/notatet/ (mpc11 bare i full kjøring).") in prosess.stdout
|
||||
assert "Nivå (uten full kjøring, --rask): bit for bit som notatet." in prosess.stdout
|
||||
|
||||
|
||||
def test_kontroll_uten_numpy_gjor_det_den_kan(hjem: Path) -> None:
|
||||
def test_kontroll_uten_numpy_skryter_ikke(hjem: Path) -> None:
|
||||
prosess = kjor("kontroll", python=[sys.executable, "-S"], hjem=hjem)
|
||||
assert prosess.returncode == 3
|
||||
assert "2. diagnose: lik notatet" in prosess.stdout
|
||||
assert "3. forlop: ikke kjørt, numpy og scipy mangler." in prosess.stdout
|
||||
assert "4. Full kjøring: ikke kjørt, numpy og scipy mangler." in prosess.stdout
|
||||
assert "Nivå (bare det som er kjørt): bit for bit som notatet." in prosess.stdout
|
||||
assert "6. Scenariokjøreren: ikke kjørt, numpy og scipy mangler." in prosess.stdout
|
||||
assert ("Nivå: ingen avvik i det som ble kjørt (Kjernen, Fasit, diagnose). Ikke kontrollert: forlop, full "
|
||||
"kjøring av sim.py, utvidelsenes grensetilfeller, scenariokjøreren.") in prosess.stdout
|
||||
assert "bit for bit som notatet" not in prosess.stdout
|
||||
|
||||
|
||||
def endre_diagnose(kopi: Path) -> None:
|
||||
|
|
@ -97,32 +130,91 @@ def endre_diagnose(kopi: Path) -> None:
|
|||
fil.write_text(tekst.replace("UPS_STAR = 0.95", "UPS_STAR = 0.90"), encoding="utf-8")
|
||||
|
||||
|
||||
def test_endret_kjerne_i_pluginen_er_en_feil(kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(kopi)
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
def test_endret_kjerne_utenfor_arbeidskopi_er_noytral(lett_kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(lett_kopi)
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "diagnose.py er endret" in prosess.stdout
|
||||
assert "2. diagnose: endret (se under)." in prosess.stdout
|
||||
assert "- Kapasitet: (0,80 x 134,3 + 273/24) / 0,95 = 125,1 saker/mnd, det vil si 19 % mer." in prosess.stdout
|
||||
assert "Nivå (uten full kjøring, --rask): endret: Kjernen, diagnose, forlop." in prosess.stdout
|
||||
assert "Pluginens modell skal regne som notatet, så dette er en feil." in prosess.stdout
|
||||
assert "Er dette pluginen slik den er installert, er det en feil" in prosess.stdout
|
||||
assert "Er modellen endret med vilje (for eksempel i en klone av repoet), er det en opplysning" in prosess.stdout
|
||||
assert "så dette er en feil." not in prosess.stdout
|
||||
|
||||
|
||||
def test_endret_kjerne_i_arbeidskopi_er_en_opplysning(kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(kopi)
|
||||
(kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
def test_endret_kjerne_i_arbeidskopi_er_en_opplysning(lett_kopi: Path, hjem: Path) -> None:
|
||||
endre_diagnose(lett_kopi)
|
||||
(lett_kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "(arbeidskopi)" in prosess.stdout
|
||||
assert "det er en opplysning, ikke en feil" in prosess.stdout
|
||||
assert "+ Kapasitet: (0,80 x 134,3 + 273/24) / 0,90" in prosess.stdout
|
||||
|
||||
|
||||
def test_manglende_fasit(kopi: Path, hjem: Path) -> None:
|
||||
(kopi / "fasit" / "diagnose_norge_2025.txt").unlink()
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
def test_endret_kjerne_med_ny_fasit_og_kilde_er_fortsatt_endret(lett_kopi: Path, hjem: Path) -> None:
|
||||
"""Funn 3: kontrollen ankres i notatet, ikke i KILDE.json og fasit/ i samme mappe."""
|
||||
endre_diagnose(lett_kopi)
|
||||
kjerne = lett_kopi / "modell" / "kjerne"
|
||||
for eksempel in ("eksempel_domstol", "norge_2025"):
|
||||
ny = subprocess.run([sys.executable, str(kjerne / "diagnose.py"), str(lett_kopi / "eksempler" /
|
||||
f"{eksempel}.json")], capture_output=True, text=True, encoding="utf-8", check=True)
|
||||
(lett_kopi / "fasit" / f"diagnose_{eksempel}.txt").write_text(ny.stdout, encoding="utf-8")
|
||||
kilde = json.loads((kjerne / "KILDE.json").read_text(encoding="utf-8"))
|
||||
kilde["filer"]["diagnose.py"]["sha256"] = hashlib.sha256((kjerne / "diagnose.py").read_bytes()).hexdigest()
|
||||
(kjerne / "KILDE.json").write_text(json.dumps(kilde), encoding="utf-8")
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "norge_2025: fant ikke" in prosess.stdout
|
||||
assert "1. Kjernen: endret fra notatet" in prosess.stdout
|
||||
assert "diagnose.py er endret" in prosess.stdout
|
||||
assert "1. Fasit: fasit er endret i forhold til notatet (2 av 27 filer)." in prosess.stdout
|
||||
assert "fasit/diagnose_eksempel_domstol.txt: har ikke notatets sha256" in prosess.stdout
|
||||
assert "2. diagnose: endret (se under)." in prosess.stdout
|
||||
assert "er endret i forhold til notatet, så sammenligningen sier ingenting om notatet" in prosess.stdout
|
||||
assert "bit for bit" not in prosess.stdout.split("Nivå")[-1]
|
||||
|
||||
|
||||
def test_manglende_fasit(lett_kopi: Path, hjem: Path) -> None:
|
||||
(lett_kopi / "fasit" / "diagnose_norge_2025.txt").unlink()
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1
|
||||
assert "fasit/diagnose_norge_2025.txt: mangler" in prosess.stdout
|
||||
assert "norge_2025: fant ikke fasit/diagnose_norge_2025.txt" in prosess.stdout
|
||||
|
||||
|
||||
def test_diagnose_krever_eksakt_likhet(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Funn 10: én enhet i siste siffer i diagnosen er endret, ikke innenfor toleranse."""
|
||||
fasit = tmp_path / "fasit"
|
||||
fasit.mkdir()
|
||||
for navn in ("eksempel_domstol", "norge_2025"):
|
||||
tekst = (FASIT / f"diagnose_{navn}.txt").read_text(encoding="utf-8")
|
||||
(fasit / f"diagnose_{navn}.txt").write_text(tekst.replace("1,28", "1,29", 1), encoding="utf-8")
|
||||
monkeypatch.setattr(kontroll, "FASIT", fasit)
|
||||
assert kontroll.kontroll_utskrift("diagnose", tmp_path, set(), toleranse=False).status == kontroll.ENDRET
|
||||
assert kontroll.kontroll_utskrift("diagnose", tmp_path, set(), toleranse=True).status == kontroll.TOLERANSE
|
||||
|
||||
|
||||
def test_scenariokjoreren_som_ikke_gjenskaper_notatet(kopi: Path, hjem: Path) -> None:
|
||||
"""Funn 4: en endring i det scenariokjøreren regner, blir oppdaget av kontroll --rask."""
|
||||
fil = kopi / "eksempler" / "scenarioer" / "notatet" / "p3.json"
|
||||
data = json.loads(fil.read_text(encoding="utf-8"))
|
||||
data["styring"]["reaktivitet"] = 4
|
||||
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
|
||||
assert "6. Scenariokjøreren: 12 av 13 av notatets datafiler gjenskapt byte for byte" in prosess.stdout
|
||||
assert "p3.dat (endret): største absolutte avvik per kolonne:" in prosess.stdout
|
||||
assert "endret: Scenariokjøreren." in prosess.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fjern", ["modell/scenario.py", "eksempler/scenarioer/notatet/endo_ko.json"])
|
||||
def test_scenariokjoreren_som_mangler_sies_fra(lett_kopi: Path, hjem: Path, fjern: str) -> None:
|
||||
(lett_kopi / fjern).unlink()
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
|
||||
assert "så den er ikke kontrollert." in prosess.stdout
|
||||
assert "endret: Scenariokjøreren." in prosess.stdout
|
||||
|
||||
|
||||
def lag_utvidelser(kopi: Path, filer: dict[str, str]) -> None:
|
||||
|
|
@ -130,36 +222,41 @@ def lag_utvidelser(kopi: Path, filer: dict[str, str]) -> None:
|
|||
mappe = kopi / "modell" / "utvidelser"
|
||||
mappe.mkdir(exist_ok=True)
|
||||
for gammel in mappe.glob("*.py"):
|
||||
gammel.unlink()
|
||||
if gammel.name != "__init__.py":
|
||||
gammel.unlink()
|
||||
for navn, innhold in filer.items():
|
||||
(mappe / f"{navn}.py").write_text(innhold, encoding="utf-8")
|
||||
|
||||
|
||||
def test_utvidelse_som_gjenskaper_kjernen(kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(kopi, {
|
||||
def test_utvidelse_som_gjenskaper_kjernen(lett_kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(lett_kopi, {
|
||||
"eksakt": "import sim\n\ndef grensetilfelle():\n return [('p3 med trappetrinn', 0.0)]\n",
|
||||
"_hjelper": "raise RuntimeError('filer som begynner med _ skal ikke kjøres')\n",
|
||||
})
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
|
||||
assert "5. Utvidelser: grensetilfellene gjenskaper kjernen eksakt (eksakt)." in prosess.stdout
|
||||
|
||||
|
||||
def test_utvidelser_som_ikke_gjenskaper_kjernen(kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(kopi, {
|
||||
"avvik": "def grensetilfelle():\n return [('p3 med trappetrinn', 1e-12), ('p6', 0.0)]\n",
|
||||
def test_utvidelser_som_ikke_gjenskaper_kjernen(lett_kopi: Path, hjem: Path) -> None:
|
||||
lag_utvidelser(lett_kopi, {
|
||||
"avvik": "def grensetilfelle():\n return [('avvik: trapp lik notatets sjokk', 1e-12), ('p6', 0.0)]\n",
|
||||
"tom": "def grensetilfelle():\n return []\n",
|
||||
"odelagt": "import sim\nraise RuntimeError('i stykker')\n",
|
||||
})
|
||||
prosess = kjor("--modell", str(kopi), "kontroll", "--rask", hjem=hjem)
|
||||
(lett_kopi / "LES-MEG.md").write_text("Arbeidskopi\n", encoding="utf-8")
|
||||
prosess = kjor("--modell", str(lett_kopi), "kontroll", "--rask", hjem=hjem)
|
||||
assert prosess.returncode == 1, prosess.stdout + prosess.stderr
|
||||
assert "5. Utvidelser: grensetilfellet gjenskaper ikke kjernen eksakt (avvik, odelagt, tom; se under)." in (
|
||||
prosess.stdout)
|
||||
assert "avvik: p3 med trappetrinn: avvik 1,0e-12, skal være eksakt 0" in prosess.stdout
|
||||
assert ("5. Utvidelser: avvik, odelagt, tom gjenskaper ikke kjernen i grensetilfellet. Det er en feil i "
|
||||
"utvidelsen, og resultatene den gir, kan ikke brukes (se under).") in prosess.stdout
|
||||
assert " avvik: trapp lik notatets sjokk: avvik 1,0e-12, skal være eksakt 0" in prosess.stdout
|
||||
assert "avvik: avvik:" not in prosess.stdout
|
||||
assert "tom: grensetilfelle() ga ingen tilfeller" in prosess.stdout
|
||||
assert "odelagt: grensetilfelle() kunne ikke kjøres" in prosess.stdout
|
||||
assert "RuntimeError: i stykker" in prosess.stdout
|
||||
assert "Nivå (uten full kjøring, --rask): endret: Utvidelser." in prosess.stdout
|
||||
assert "har en feil. Resultatene den gir, kan ikke brukes før den er rettet." in prosess.stdout
|
||||
assert "opplysning" not in prosess.stdout
|
||||
|
||||
|
||||
@pytest.mark.treg
|
||||
|
|
@ -167,4 +264,5 @@ def test_full_kontroll(hjem: Path) -> None:
|
|||
prosess = kjor("kontroll", hjem=hjem)
|
||||
assert prosess.returncode == 0, prosess.stdout + prosess.stderr
|
||||
assert "4. Full kjøring: 22 av 22 datafiler like byte for byte med notatet; utskriften lik." in prosess.stdout
|
||||
assert "6. Scenariokjøreren: 14 av 14 av notatets datafiler gjenskapt byte for byte" in prosess.stdout
|
||||
assert "\nNivå: bit for bit som notatet.\n" in prosess.stdout
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue