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:
commit
464ad38b49
51 changed files with 11207 additions and 0 deletions
79
tests/conftest.py
Normal file
79
tests/conftest.py
Normal 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
|
||||
67
tests/test_kjerne_og_fasit.py
Normal file
67
tests/test_kjerne_og_fasit.py
Normal 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
406
tests/test_kjor.py
Normal 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
170
tests/test_kontroll.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue