The operator tested v0.1.0 in Cowork on 2026-11-11. README now says what was measured there and what was not. - miljo: no longer claims that kontroll compares "with tolerance, not bit for bit" when numpy or scipy differ from the note's versions. kontroll always compares byte for byte first, and numpy 2.5.3 gave bit for bit. - kontroll --alt: show every differing line. The cut-off line now names the option instead of only counting the lines left out. - skill simuler: the question before a run must be answerable with yes or no. In the test it became a choice between variants and the run stalled. - skill forbedre: offer the working copy as a zip when the user cannot open the folder it was made in, as in Cowork. 263 tests pass; full kontroll: bit for bit with the note. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
520 lines
22 KiB
Python
520 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"]]
|
|
ut = capsys.readouterr().out
|
|
assert "lot seg ikke installere, så nyere versjoner er installert" in ut
|
|
assert "bit for bit eller innenfor toleranse" in ut
|
|
# Measured in Cowork 2026-11-11: numpy 2.5.3 (not the pinned 2.5.4) still gave bit for bit,
|
|
# so the message must not promise a tolerance comparison.
|
|
assert "ikke bit for bit" not in ut
|
|
|
|
|
|
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
|