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>
406 lines
17 KiB
Python
406 lines
17 KiB
Python
# 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
|