feat: model core, launcher, reproduction check and the first two skills

The core in modell/kjerne/ is an unmodified copy of sim.py, diagnose.py and
forlop.py from den-dynamiske-rettsstaten @2badc25, pinned by sha256.
kjor.py is the single entry point; kontroll.py reproduces the note's 22 data
files and printed results. Skills: diagnose and forlop (Norwegian).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-11 13:53:32 +01:00
commit 464ad38b49
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
51 changed files with 11207 additions and 0 deletions

79
tests/conftest.py Normal file
View file

@ -0,0 +1,79 @@
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""Felles hjelpere for testene: stier, kjøring av kjor.py og en kopi av pluginens modell."""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROT = Path(__file__).resolve().parent.parent
MODELL = ROT / "modell"
KJOR = MODELL / "kjor.py"
FASIT = ROT / "fasit"
EKSEMPLER = ROT / "eksempler"
DOMSTOL = EKSEMPLER / "eksempel_domstol.json"
NORGE = EKSEMPLER / "norge_2025.json"
def kjor(
*args: str,
python: list[str] | None = None,
hjem: Path | None = None,
cwd: Path | None = None,
) -> subprocess.CompletedProcess[str]:
"""Kjør kjor.py som brukeren gjør, og fang utskrift og exit-kode.
python: tolken (med valg), standard er den som kjører testene (har numpy og scipy).
hjem: HOME for prosessen, slik at standardmappa ~/.dynamisk-rettsstat aldri er brukerens egen.
"""
miljo = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1")
miljo.pop("DYNAMISK_RETTSSTAT_VENV", None)
if hjem is not None:
miljo["HOME"] = str(hjem)
return subprocess.run(
[*(python or [sys.executable]), str(KJOR), *args],
capture_output=True, text=True, encoding="utf-8", env=miljo, cwd=str(cwd or ROT), check=False,
)
def fasit(navn: str) -> str:
"""Innholdet i en fasitfil."""
return (FASIT / navn).read_text(encoding="utf-8")
def lag_venv_omvei(data: Path, *python: str) -> Path:
"""Lag en «egen mappe» i data/venv der bin/python er et lite skript som starter en gitt Python.
Testene installerer ingenting: omveien peker på en Python som allerede finnes.
"""
bin_mappe = data / "venv" / "bin"
bin_mappe.mkdir(parents=True)
tolk = bin_mappe / "python"
kommando = " ".join(f'"{p}"' for p in python)
tolk.write_text(f'#!/bin/sh\nexec {kommando} "$@"\n', encoding="utf-8")
tolk.chmod(0o755)
return tolk
@pytest.fixture
def hjem(tmp_path: Path) -> Path:
"""En tom hjemmemappe for prosessen."""
mappe = tmp_path / "hjem"
mappe.mkdir()
return mappe
@pytest.fixture
def kopi(tmp_path: Path) -> Path:
"""En kopi av modell/, fasit/ og eksempler/ som testen kan endre. Returnerer kopiens rot."""
rot = tmp_path / "kopi"
for navn in ("modell", "fasit", "eksempler"):
shutil.copytree(ROT / navn, rot / navn, ignore=shutil.ignore_patterns("__pycache__"))
for navn in ("scenario.py", "arbeidskopi.py"):
(rot / "modell" / navn).unlink(missing_ok=True)
return rot

View file

@ -0,0 +1,67 @@
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""Kjernen er uendret fra notatet, og kjernens utskrifter er lik fasit."""
from __future__ import annotations
import hashlib
import json
import os
import subprocess
import sys
from typing import TYPE_CHECKING
import pytest
from conftest import EKSEMPLER, FASIT, MODELL, fasit
if TYPE_CHECKING:
from pathlib import Path
KILDE = json.loads((MODELL / "kjerne" / "KILDE.json").read_text(encoding="utf-8"))
def test_kilde_peker_paa_notatets_commit() -> None:
assert KILDE["repo"] == "https://git.fromaitochitta.com/open/den-dynamiske-rettsstaten"
assert KILDE["commit"] == "2badc25e5134ee26309bd8c3bd008ac64afe55a4"
assert set(KILDE["filer"]) == {"sim.py", "diagnose.py", "forlop.py"}
assert {f.name for f in (MODELL / "kjerne").glob("*.py")} == set(KILDE["filer"])
@pytest.mark.parametrize("navn", ["sim.py", "diagnose.py", "forlop.py"])
def test_kjernen_har_sin_sha256(navn: str) -> None:
innhold = (MODELL / "kjerne" / navn).read_bytes()
assert hashlib.sha256(innhold).hexdigest() == KILDE["filer"][navn]["sha256"]
assert KILDE["filer"][navn]["kilde"] == f"paper/{navn}"
def test_fasit_har_22_datafiler() -> None:
assert len(list((FASIT / "data").glob("*.dat"))) == 22
@pytest.mark.parametrize(
("skript", "eksempel"),
[("diagnose", "eksempel_domstol"), ("diagnose", "norge_2025"),
("forlop", "eksempel_domstol"), ("forlop", "norge_2025")],
)
def test_kjernens_utskrift_er_lik_fasit(skript: str, eksempel: str, tmp_path: Path) -> None:
miljo = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1")
prosess = subprocess.run(
[sys.executable, str(MODELL / "kjerne" / f"{skript}.py"), str(EKSEMPLER / f"{eksempel}.json")],
capture_output=True, text=True, encoding="utf-8", env=miljo, cwd=str(tmp_path), check=False,
)
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == fasit(f"{skript}_{eksempel}.txt")
def test_fasit_har_ankrene_fra_notatet() -> None:
domstol = fasit("diagnose_eksempel_domstol.txt")
assert "Skranken som binder: Domstol (utnyttelse 1,28" in domstol
assert "det vil si 19 % mer" in domstol
norge = fasit("diagnose_norge_2025.txt")
for ledd, utnyttelse in (("Politi og påtale", "1,09"), ("Tingrett, meddomsrett", "1,04"),
("Lagmannsrett, bevisanke", "0,88"), ("Fengsel", "1,09")):
linje = next(linje for linje in norge.splitlines() if linje.startswith(ledd))
assert linje.split()[-4] == utnyttelse
forlop = fasit("forlop_eksempel_domstol.txt")
assert "Kritisk forsinkelse: 7,2 mnd." in forlop
assert "- nå: bestill 19 % mer kapasitet" in forlop
assert "- om 12 mnd: bestill 12 % mer kapasitet" in forlop

406
tests/test_kjor.py Normal file
View file

@ -0,0 +1,406 @@
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""kjor.py: kommandoer, exit-koder, --data, --modell og valg av Python (uten å installere noe)."""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from pathlib import Path
import kjor as kjor_modul
import pytest
from conftest import DOMSTOL, EKSEMPLER, FASIT, MODELL, NORGE, ROT, fasit, kjor, lag_venv_omvei
UTEN_SITE = [sys.executable, "-S"] # samme Python uten site-packages: numpy og scipy mangler
# ---------------- bruk og exit-koder ----------------
def test_uten_kommando_gir_bruk_og_kode_2() -> None:
prosess = kjor()
assert prosess.returncode == 2
assert prosess.stderr.startswith("Bruk: python3 kjor.py")
@pytest.mark.parametrize("valg", ["--hjelp", "-h", "--help"])
def test_hjelp_gir_kode_0(valg: str) -> None:
prosess = kjor(valg)
assert prosess.returncode == 0
for kommando in kjor_modul.KOMMANDOER:
assert f" {kommando} " in prosess.stdout
def test_ukjent_kommando() -> None:
prosess = kjor("simuler", "x.json")
assert prosess.returncode == 2
assert "Ukjent kommando «simuler»" in prosess.stderr
assert "diagnose, forlop, scenario, notatet, kontroll, miljo, arbeidskopi" in prosess.stderr
@pytest.mark.parametrize("args", [["--tull", "diagnose", "x"], ["--data"], ["--data", "mappe"], ["--modell"]])
def test_feil_i_globale_valg(args: list[str]) -> None:
prosess = kjor(*args)
assert prosess.returncode == 2
assert "Skriv «python3 kjor.py --hjelp» for bruk." in prosess.stderr
@pytest.mark.parametrize("args", [["diagnose"], ["diagnose", "a", "b"], ["forlop"], ["forlop", "a", "b", "c"],
["miljo", "--tull"], ["notatet", "--ut"], ["notatet", "x"],
["kontroll", "--tull"]])
def test_feil_antall_argumenter(args: list[str]) -> None:
prosess = kjor(*args)
assert prosess.returncode == 2, prosess.stdout + prosess.stderr
# ---------------- diagnose og forlop ----------------
@pytest.mark.parametrize(("fil", "navn"), [(DOMSTOL, "eksempel_domstol"), (NORGE, "norge_2025")])
def test_diagnose_er_lik_fasit(fil: Path, navn: str) -> None:
prosess = kjor("diagnose", str(fil))
assert prosess.returncode == 0
assert prosess.stdout == fasit(f"diagnose_{navn}.txt")
assert prosess.stderr == ""
@pytest.mark.parametrize(("fil", "navn"), [(DOMSTOL, "eksempel_domstol"), (NORGE, "norge_2025")])
def test_forlop_er_lik_fasit(fil: Path, navn: str) -> None:
prosess = kjor("forlop", str(fil))
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == fasit(f"forlop_{navn}.txt")
def test_diagnose_virker_uten_numpy(hjem: Path) -> None:
prosess = kjor("diagnose", str(DOMSTOL), python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 0
assert prosess.stdout == fasit("diagnose_eksempel_domstol.txt")
@pytest.mark.parametrize("kommando", ["diagnose", "forlop"])
def test_manglende_fil_gir_kode_2(kommando: str, tmp_path: Path) -> None:
fil = tmp_path / "finnes_ikke.json"
prosess = kjor(kommando, str(fil))
assert prosess.returncode == 2
assert prosess.stderr == f"Fant ikke fila {fil}.\n"
def test_kjernens_sys_exit_gir_kode_2_og_meldingen_uendret(tmp_path: Path) -> None:
fil = tmp_path / "tom.json"
fil.write_text('{"ledd": []}', encoding="utf-8")
for kommando in ("diagnose", "forlop"):
prosess = kjor(kommando, str(fil))
assert prosess.returncode == 2
assert prosess.stderr == 'Feil i inndata: fila må ha en liste "ledd" med minst ett ledd.\n'
assert prosess.stdout == ""
def test_forlop_med_ukjent_ledd() -> None:
prosess = kjor("forlop", str(DOMSTOL), "Tingrett")
assert prosess.returncode == 2
assert prosess.stderr == (
'Fant ikke leddet "Tingrett". Leddene i fila: Politi, Påtale, Domstol, Kriminalomsorg.\n'
)
def test_forlop_med_valgt_ledd() -> None:
prosess = kjor("forlop", str(NORGE), "Tingrett, meddomsrett")
assert prosess.returncode == 0, prosess.stderr
assert "Forløp over tid for leddet du valgte: Tingrett, meddomsrett" in prosess.stdout
def test_ingen_pycache_i_kjernen(kopi: Path) -> None:
miljo = {k: v for k, v in os.environ.items() if k != "PYTHONDONTWRITEBYTECODE"}
prosess = subprocess.run(
[sys.executable, str(kopi / "modell" / "kjor.py"), "forlop", str(DOMSTOL)],
capture_output=True, text=True, encoding="utf-8", env=miljo, check=False,
)
assert prosess.returncode == 0, prosess.stderr
assert not (kopi / "modell" / "kjerne" / "__pycache__").exists()
# ---------------- --data og valg av Python ----------------
@pytest.mark.parametrize("verdi", [None, "", " ", "${CLAUDE_PLUGIN_DATA}", "/a/${CLAUDE_PLUGIN_DATA}/b"])
def test_datamappe_ikke_oppgitt(verdi: str | None) -> None:
assert kjor_modul.datamappe(verdi) == (Path.home() / ".dynamisk-rettsstat", False)
def test_datamappe_oppgitt(tmp_path: Path) -> None:
assert kjor_modul.datamappe(str(tmp_path / "d")) == ((tmp_path / "d").resolve(), True)
assert kjor_modul.datamappe("~/x") == ((Path.home() / "x").resolve(), True)
@pytest.mark.parametrize("verdi", ["", "${CLAUDE_PLUGIN_DATA}"])
def test_data_uten_verdi_bruker_standardmappa(verdi: str, hjem: Path) -> None:
prosess = kjor("--data", verdi, "miljo", hjem=hjem)
assert prosess.returncode == 0
assert f"Egen mappe for numpy og scipy: {hjem / '.dynamisk-rettsstat' / 'venv'}" in prosess.stdout
assert not (hjem / ".dynamisk-rettsstat").exists()
def test_data_med_likhetstegn(tmp_path: Path, hjem: Path) -> None:
prosess = kjor(f"--data={tmp_path / 'd'}", "miljo", hjem=hjem)
assert prosess.returncode == 0
assert f"Egen mappe for numpy og scipy: {(tmp_path / 'd').resolve() / 'venv'}" in prosess.stdout
def test_miljo_viser_versjonene(hjem: Path) -> None:
prosess = kjor("miljo", hjem=hjem)
assert prosess.returncode == 0
assert "numpy: 2.5.4 (notatet: 2.5.4)" in prosess.stdout
assert "scipy: 1.18.1 (notatet: 1.18.1)" in prosess.stdout
assert "regner med Python som kjører nå" in prosess.stdout
assert "kontroll kan gi bit for bit" in prosess.stdout
assert "finnes ikke" in prosess.stdout
def test_miljo_uten_numpy(hjem: Path) -> None:
prosess = kjor("miljo", python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 0
assert "numpy: mangler" in prosess.stdout
assert "kan ikke regne nå: numpy, scipy mangler" in prosess.stdout
def test_uten_numpy_og_uten_venv_gir_kode_3(tmp_path: Path, hjem: Path) -> None:
data = tmp_path / "data"
prosess = kjor("--data", str(data), "forlop", str(DOMSTOL), python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 3
assert "«forlop» trenger numpy (2.0 eller nyere) og scipy. Mangler: numpy, scipy." in prosess.stderr
assert f"--data {data.resolve()} miljo --opprett" in prosess.stderr
assert "Ingenting installeres uten at du ber om det." in prosess.stderr
assert not data.exists()
@pytest.mark.parametrize("kommando", [["notatet"], ["forlop", str(DOMSTOL)]])
def test_uten_numpy_i_standardmappa_gir_kode_3(kommando: list[str], hjem: Path) -> None:
prosess = kjor(*kommando, python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 3
assert f"Fant ingen egen mappe med numpy og scipy i {hjem / '.dynamisk-rettsstat' / 'venv'}" in prosess.stderr
assert not (hjem / ".dynamisk-rettsstat").exists()
def test_venv_i_datamappa_brukes(tmp_path: Path, hjem: Path) -> None:
data = tmp_path / "data"
lag_venv_omvei(data, sys.executable)
prosess = kjor("--data", str(data), "forlop", str(DOMSTOL), python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == fasit("forlop_eksempel_domstol.txt")
def test_venv_i_datamappa_gir_videre_exit_koden(tmp_path: Path, hjem: Path) -> None:
data = tmp_path / "data"
lag_venv_omvei(data, sys.executable)
prosess = kjor("--data", str(data), "forlop", str(DOMSTOL), "Ukjent", python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 2
assert prosess.stderr.startswith('Fant ikke leddet "Ukjent".')
def test_venv_uten_numpy_gir_kode_3_uten_lokke(tmp_path: Path, hjem: Path) -> None:
data = tmp_path / "data"
lag_venv_omvei(data, sys.executable, "-S")
prosess = kjor("--data", str(data), "forlop", str(DOMSTOL), python=UTEN_SITE, hjem=hjem)
assert prosess.returncode == 3
assert "mangler dem også" in prosess.stderr
def beskrivelse(*, numpy: str | None, scipy: str | None, trapezoid: bool = True) -> kjor_modul.Beskrivelse:
"""En oppdiktet beskrivelse av en Python-tolk."""
return kjor_modul.Beskrivelse(python="3.9.6", sti="/x/python", numpy=numpy, trapezoid=trapezoid, scipy=scipy)
def test_mangler_i_ord() -> None:
assert kjor_modul.mangler(beskrivelse(numpy="2.5.4", scipy="1.18.1")) == []
assert kjor_modul.mangler(beskrivelse(numpy=None, scipy=None)) == ["numpy", "scipy"]
assert kjor_modul.mangler(beskrivelse(numpy="1.26.4", scipy="1.13.0", trapezoid=False)) == [
"numpy 2.0 eller nyere (har 1.26.4)"]
def test_velg_tolk(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(kjor_modul.VENV_MERKE, raising=False)
monkeypatch.setattr(kjor_modul, "beskriv_denne", lambda: beskrivelse(numpy="2.5.4", scipy="1.18.1"))
assert kjor_modul.velg_tolk(tmp_path).hvor == "denne"
monkeypatch.setattr(kjor_modul, "beskriv_denne", lambda: beskrivelse(numpy="2.4.2", scipy=None))
valg = kjor_modul.velg_tolk(tmp_path)
assert (valg.hvor, valg.mangler) == ("mangler", ["scipy"])
tolk = lag_venv_omvei(tmp_path, sys.executable)
assert kjor_modul.velg_tolk(tmp_path) == kjor_modul.Tolk("venv", tolk, ["scipy"])
monkeypatch.setenv(kjor_modul.VENV_MERKE, "1")
assert kjor_modul.velg_tolk(tmp_path).hvor == "mangler"
# ---------------- miljo --opprett (uten å installere) ----------------
def test_opprett_proever_laaste_versjoner_forst(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
lag_venv_omvei(tmp_path, sys.executable)
kommandoer: list[list[str]] = []
def falsk(kommando: list[str]) -> int:
kommandoer.append(kommando)
return 0
monkeypatch.setattr(kjor_modul, "kjor_prosess", falsk)
monkeypatch.setattr(kjor_modul, "rapport_miljo", lambda _valg: None)
assert kjor_modul.main(["--data", str(tmp_path), "miljo", "--opprett"]) == 0
assert len(kommandoer) == 1
assert kommandoer[0][-2:] == ["-r", str(MODELL / "requirements.txt")]
def test_opprett_faller_tilbake_og_sier_toleranse(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
) -> None:
lag_venv_omvei(tmp_path, sys.executable)
kommandoer: list[list[str]] = []
def falsk(kommando: list[str]) -> int:
kommandoer.append(kommando)
return 1 if "-r" in kommando else 0
monkeypatch.setattr(kjor_modul, "kjor_prosess", falsk)
monkeypatch.setattr(kjor_modul, "rapport_miljo", lambda _valg: None)
assert kjor_modul.main(["--data", str(tmp_path), "miljo", "--opprett"]) == 0
assert [k[-2:] for k in kommandoer] == [["-r", str(MODELL / "requirements.txt")], ["numpy>=2.0", "scipy"]]
assert "kontrolleres da med toleranse, ikke bit for bit" in capsys.readouterr().out
def test_opprett_som_feiler_gir_kode_3(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
lag_venv_omvei(tmp_path, sys.executable)
monkeypatch.setattr(kjor_modul, "kjor_prosess", lambda _kommando: 1)
assert kjor_modul.main(["--data", str(tmp_path), "miljo", "--opprett"]) == 3
def test_opprett_lager_venv_med_denne_python(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
kommandoer: list[list[str]] = []
def falsk(kommando: list[str]) -> int:
kommandoer.append(kommando)
return 0 # later som om venv ble laget, men lager ingenting
monkeypatch.setattr(kjor_modul, "kjor_prosess", falsk)
data = tmp_path / "ny"
assert kjor_modul.main(["--data", str(data), "miljo", "--opprett"]) == 3
assert kommandoer == [[sys.executable, "-m", "venv", str(data.resolve() / "venv")]]
# ---------------- --modell, scenario og arbeidskopi ----------------
def test_modell_som_ikke_finnes(tmp_path: Path) -> None:
prosess = kjor("--modell", str(tmp_path), "diagnose", str(DOMSTOL))
assert prosess.returncode == 2
assert f"Fant ingen modell i {tmp_path.resolve()}" in prosess.stderr
@pytest.mark.parametrize("undermappe", ["", "modell"])
def test_modell_i_arbeidskopi(kopi: Path, undermappe: str) -> None:
diagnose = kopi / "modell" / "kjerne" / "diagnose.py"
diagnose.write_text(diagnose.read_text(encoding="utf-8").replace(
'print("Trinn (c): utnyttelse', 'print("ARBEIDSKOPI")\n print("Trinn (c): utnyttelse'), encoding="utf-8")
prosess = kjor("--modell", str(kopi / undermappe), "diagnose", str(DOMSTOL))
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == "ARBEIDSKOPI\n" + fasit("diagnose_eksempel_domstol.txt")
@pytest.mark.parametrize("kommando", ["scenario", "arbeidskopi"])
def test_modul_som_ikke_finnes_ennaa(kopi: Path, kommando: str) -> None:
prosess = kjor("--modell", str(kopi), kommando, "x")
assert prosess.returncode == 2
assert f"Kommandoen «{kommando}» er ikke på plass i denne modellen ennå" in prosess.stderr
FALSK_MODUL = """\
import sys
import sim
def main(argv):
if argv and argv[0] == "stopp":
sys.exit("Feil i scenariofila: feltet «tull» finnes ikke.")
if argv and argv[0] == "kode":
return 1
print(argv, sim.DT)
return 0
"""
@pytest.mark.parametrize("kommando", ["scenario", "arbeidskopi"])
def test_modul_faar_argumentene_og_kjernen(kopi: Path, kommando: str) -> None:
(kopi / "modell" / f"{kommando}.py").write_text(FALSK_MODUL, encoding="utf-8")
prosess = kjor("--modell", str(kopi), kommando, "fil.json", "--ut", "mappe")
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == "['fil.json', '--ut', 'mappe'] 0.05\n"
prosess = kjor("--modell", str(kopi), kommando, "stopp")
assert prosess.returncode == 2
assert prosess.stderr == "Feil i scenariofila: feltet «tull» finnes ikke.\n"
assert kjor("--modell", str(kopi), kommando, "kode").returncode == 1
def test_modul_uten_main(kopi: Path) -> None:
(kopi / "modell" / "scenario.py").write_text("X = 1\n", encoding="utf-8")
prosess = kjor("--modell", str(kopi), "scenario", "x")
assert prosess.returncode == 2
assert "mangler funksjonen main(argv)" in prosess.stderr
def test_uventet_feil_i_modul_gir_kode_2(kopi: Path) -> None:
(kopi / "modell" / "scenario.py").write_text("def main(argv):\n return 1 / 0\n", encoding="utf-8")
prosess = kjor("--modell", str(kopi), "scenario", "x")
assert prosess.returncode == 2
assert "ZeroDivisionError" in prosess.stderr
assert "Uventet feil i scenario.py (se over)." in prosess.stderr
# ---------------- notatet ----------------
def test_notatet_nekter_ut_mappe_i_modellen() -> None:
prosess = kjor("notatet", "--ut", str(MODELL / "kjerne"))
assert prosess.returncode == 2
assert "ligger inne i modell-mappa" in prosess.stderr
assert not (MODELL / "kjerne" / "data").exists()
@pytest.mark.treg
def test_notatet_gjenskaper_datafilene(tmp_path: Path) -> None:
ut = tmp_path / "ut"
prosess = kjor("notatet", "--ut", str(ut))
assert prosess.returncode == 0, prosess.stderr
navn = sorted(f.name for f in (FASIT / "data").glob("*.dat"))
assert sorted(f.name for f in (ut / "data").glob("*.dat")) == navn
for fil in navn:
assert (ut / "data" / fil).read_bytes() == (FASIT / "data" / fil).read_bytes(), fil
assert (ut / "sim_utskrift.txt").read_text(encoding="utf-8") == fasit("sim_utskrift.txt")
# ---------------- Python 3.9 ----------------
def python39() -> str | None:
"""En Python 3.9 på maskinen, hvis det finnes en."""
for kandidat in ("/usr/bin/python3", shutil.which("python3.9")):
if not kandidat or not Path(kandidat).exists():
continue
try:
versjon = subprocess.run([kandidat, "-c", "import sys; print(sys.version_info[:2])"],
capture_output=True, text=True, check=False, timeout=60).stdout.strip()
except (OSError, subprocess.TimeoutExpired):
continue
if versjon == "(3, 9)":
return kandidat
return None
PY39 = python39()
@pytest.mark.skipif(PY39 is None, reason="fant ingen Python 3.9 på maskinen")
def test_kjor_og_kontroll_virker_paa_python_39(hjem: Path) -> None:
assert PY39 is not None
prosess = kjor("diagnose", str(EKSEMPLER / "norge_2025.json"), python=[PY39], hjem=hjem)
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == fasit("diagnose_norge_2025.txt")
assert kjor("miljo", python=[PY39], hjem=hjem).returncode == 0
har_regnemiljo = subprocess.run([PY39, "-c", "import numpy, scipy.optimize; numpy.trapezoid"],
capture_output=True, check=False).returncode == 0
prosess = kjor("kontroll", "--rask", python=[PY39], hjem=hjem, cwd=ROT)
assert "1. Kjernen: uendret fra notatet" in prosess.stdout
assert "2. diagnose: lik notatet" in prosess.stdout
assert prosess.returncode == (0 if har_regnemiljo else 3), prosess.stdout + prosess.stderr

170
tests/test_kontroll.py Normal file
View file

@ -0,0 +1,170 @@
# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""kontroll.py: sammenligningene, dommene og nivået."""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
import kontroll
import pytest
from conftest import kjor
if TYPE_CHECKING:
from pathlib import Path
# ---------------- sammenligningene ----------------
@pytest.mark.parametrize(("tall", "forventet"), [("19", 1.0), ("1,28", 0.01), ("7.21", 0.01),
("0.00000", 1e-5), ("1.11e-15", 1e-17), ("-3.0E+02", 10.0)])
def test_enhet_i_siste_siffer(tall: str, forventet: float) -> None:
assert kontroll.enhet(tall) == pytest.approx(forventet)
def test_tekst_lik() -> None:
assert kontroll.sammenlign_tekst("a 1,23\n", "a 1,23\n").status == kontroll.LIK
def test_tekst_innenfor_toleranse() -> None:
resultat = kontroll.sammenlign_tekst("utnyttelse 1,23 og 7.21\n", "utnyttelse 1,24 og 7.20\n")
assert resultat.status == kontroll.TOLERANSE
assert resultat.avvik == pytest.approx(0.01)
assert "-utnyttelse 1,23 og 7.21" in resultat.detaljer
assert "+utnyttelse 1,24 og 7.20" in resultat.detaljer
@pytest.mark.parametrize(("fasit", "naa"), [("a 1,23\n", "a 1,25\n"), ("a 1,23\n", "b 1,23\n"),
("a 1,23\n", "a 1,23\nb\n"), ("19 %\n", "21 %\n")])
def test_tekst_endret(fasit: str, naa: str) -> None:
resultat = kontroll.sammenlign_tekst(fasit, naa)
assert resultat.status == kontroll.ENDRET
assert resultat.detaljer
DAT = "t K Q\n0.00000 1.00000 0.29100\n0.50000 1.10000 0.30000\n"
def test_dat_lik() -> None:
assert kontroll.sammenlign_dat(DAT, DAT).status == kontroll.LIK
def test_dat_innenfor_toleranse() -> None:
resultat = kontroll.sammenlign_dat(DAT, DAT.replace("1.10000", "1.10001"))
assert resultat.status == kontroll.TOLERANSE
assert resultat.detaljer == ["største absolutte avvik per kolonne: K 1,0e-05"]
def test_dat_endret_viser_avvik_per_kolonne() -> None:
resultat = kontroll.sammenlign_dat(DAT, DAT.replace("1.10000", "1.10030").replace("0.30000", "0.30200"))
assert resultat.status == kontroll.ENDRET
assert resultat.detaljer == ["største absolutte avvik per kolonne: K 3,0e-04, Q 2,0e-03"]
assert resultat.avvik == pytest.approx(0.002)
@pytest.mark.parametrize("naa", ["t K X\n0.00000 1.00000 0.29100\n0.50000 1.10000 0.30000\n",
"t K Q\n0.00000 1.00000 0.29100\n"])
def test_dat_ulik_form(naa: str) -> None:
assert kontroll.sammenlign_dat(DAT, naa).status == kontroll.ENDRET
# ---------------- kontrollen som helhet ----------------
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 "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 "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:
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
def endre_diagnose(kopi: Path) -> None:
"""Endre målutnyttelsen i kopiens diagnose.py fra 0,95 til 0,90."""
fil = kopi / "modell" / "kjerne" / "diagnose.py"
tekst = fil.read_text(encoding="utf-8")
assert "UPS_STAR = 0.95" in tekst
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)
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
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)
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)
assert prosess.returncode == 1
assert "norge_2025: fant ikke" in prosess.stdout
def lag_utvidelser(kopi: Path, filer: dict[str, str]) -> None:
"""Legg utvidelser i kopiens modell/utvidelser/ (og fjern dem som lå der)."""
mappe = kopi / "modell" / "utvidelser"
mappe.mkdir(exist_ok=True)
for gammel in mappe.glob("*.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, {
"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)
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",
"tom": "def grensetilfelle():\n return []\n",
"odelagt": "import sim\nraise RuntimeError('i stykker')\n",
})
prosess = kjor("--modell", str(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 "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
@pytest.mark.treg
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 "\nNivå: bit for bit som notatet.\n" in prosess.stdout