dynamisk-rettsstat/tests/test_kjor.py
Kjell Tore Guttormsen 776325c680
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>
2026-11-11 15:27:14 +01:00

515 lines
22 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 json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any
import kjor as kjor_modul
import pytest
from conftest import DOMSTOL, EKSEMPLER, FASIT, MODELL, NORGE, ROT, fasit, kjor, lag_venv_omvei
if TYPE_CHECKING:
from collections.abc import Callable
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()
# ---------------- ukjente feltnavn (funn 5) ----------------
def skriv_variant(tmp_path: Path, endre: Callable[[dict[str, Any]], None], navn: str = "variant.json") -> Path:
"""En kopi av eksempel_domstol.json med en endring."""
data = json.loads(DOMSTOL.read_text(encoding="utf-8"))
endre(data)
fil = tmp_path / navn
fil.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return fil
def skrivefeil_i_ledd(data: dict[str, Any]) -> None:
domstol = data["ledd"][2]
domstol["prognose_innstrømning"] = domstol.pop("prognose_innstromning")
@pytest.mark.parametrize("kommando", ["diagnose", "forlop"])
def test_skrivefeil_i_ledd_avvises(kommando: str, tmp_path: Path) -> None:
fil = skriv_variant(tmp_path, skrivefeil_i_ledd)
prosess = kjor(kommando, str(fil))
assert prosess.returncode == 2
assert prosess.stdout == ""
assert prosess.stderr.startswith(
'Feil i inndata, ledd 3 (Domstol): feltet "prognose_innstrømning" finnes ikke. '
'Mente du "prognose_innstromning"? Gyldige felt: navn, innstromning, kapasitet, restanse, maal_tid,')
def test_skrivefeil_i_styring_avvises(tmp_path: Path) -> None:
def endre(data: dict[str, Any]) -> None:
data["styring"]["reaktivitett"] = data["styring"].pop("reaktivitet")
prosess = kjor("forlop", str(skriv_variant(tmp_path, endre)))
assert prosess.returncode == 2
assert prosess.stderr == ('Feil i inndata, styring: feltet "reaktivitett" finnes ikke. Mente du "reaktivitet"? '
"Gyldige felt: forsinkelse, reaktivitet, merknad, kilde, kilder.\n")
def test_merknadsfelt_og_ukjente_toppfelt_er_tillatt(tmp_path: Path) -> None:
def endre(data: dict[str, Any]) -> None:
data["ledd"][2].update(merknad="fri tekst", kilde="SSB", kilder=["a", "b"])
data["styring"]["merknad"] = "regime A"
data["noe_annet"] = {"fritt": True}
prosess = kjor("diagnose", str(skriv_variant(tmp_path, endre)))
assert prosess.returncode == 0, prosess.stderr
assert prosess.stdout == fasit("diagnose_eksempel_domstol.txt")
def test_feltene_leses_fra_kjernen_i_modellen(kopi: Path, tmp_path: Path) -> None:
diagnose = kopi / "modell" / "kjerne" / "diagnose.py"
tekst = diagnose.read_text(encoding="utf-8")
assert '"innstromning_for_okning")' in tekst
diagnose.write_text(tekst.replace('"innstromning_for_okning")', '"innstromning_for_okning", "nytt_felt")', 1),
encoding="utf-8")
fil = skriv_variant(tmp_path, lambda data: data["ledd"][0].update(nytt_felt=1))
assert kjor("diagnose", str(fil)).returncode == 2
prosess = kjor("--modell", str(kopi), "diagnose", str(fil))
assert prosess.returncode == 0, prosess.stderr
def test_feil_form_overlates_til_kjernen(tmp_path: Path) -> None:
fil = tmp_path / "feil.json"
fil.write_text('{"ledd": [1, {"navn": "A"}]}', encoding="utf-8")
prosess = kjor("diagnose", str(fil))
assert prosess.returncode == 2
assert prosess.stderr == "Feil i inndata, ledd 1: hvert ledd må være et objekt med felt og verdier.\n"
# ---------------- --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"
@pytest.mark.parametrize(("system", "start", "sitert"), [
("nt", "python ", '"/data med mellomrom"'), ("posix", "python3 ", "'/data med mellomrom'"),
])
def test_foreslatt_kommando_per_system(system: str, start: str, sitert: str) -> None:
valg = kjor_modul.Valg(argv=[], data=Path("/data med mellomrom"), data_oppgitt=True, modell=MODELL,
kommando="forlop", args=[])
tekst = kjor_modul.kommando_tekst(valg, "miljo", "--opprett", system=system)
assert tekst.startswith(start)
assert f"--data {sitert} miljo --opprett" in tekst
assert kjor_modul.python_navn(system) == start.strip()
# ---------------- 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:
(kopi / "modell" / f"{kommando}.py").unlink()
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.skipif(os.name == "nt" or os.geteuid() == 0, reason="skrivebeskyttelse virker ikke for root")
def test_notatet_i_mappe_som_ikke_kan_skrives(tmp_path: Path) -> None:
sperret = tmp_path / "sperret"
sperret.mkdir()
sperret.chmod(0o555)
try:
prosess = kjor("notatet", "--ut", str(sperret / "ut"))
finally:
sperret.chmod(0o755)
assert prosess.returncode == 2
assert prosess.stderr.startswith(f"Kan ikke skrive i {(sperret / 'ut').resolve()}: ")
assert prosess.stderr.endswith(". Velg en annen mappe med --ut.\n")
assert "Traceback" not in prosess.stderr
def test_notatet_naar_sim_stopper(kopi: Path, tmp_path: Path) -> None:
(kopi / "modell" / "kjerne" / "sim.py").write_text("print('start')\nraise SystemExit(5)\n", encoding="utf-8")
prosess = kjor("--modell", str(kopi), "notatet", "--ut", str(tmp_path / "ut"))
assert prosess.returncode == 2
assert "Kjøringen av sim.py stoppet underveis (exit-kode 5)" in prosess.stderr
assert "start\n" in prosess.stdout
@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