2026-09-16 10:23:45 +02:00
|
|
|
|
"""Tests für scripts/test_audit.py und die Prüf-Audit-Funktionen in scripts/testkatalog.py.
|
|
|
|
|
|
|
|
|
|
|
|
Reines unittest, ohne QGIS/sn_basis-Abhängigkeit. Arbeitet auf einem eigenen, temporären
|
|
|
|
|
|
Mini-Workspace (nicht auf den echten Plugin-Repos), damit diese Tests unabhängig vom Inhalt der
|
|
|
|
|
|
sn_*-Testsuiten bleiben.
|
|
|
|
|
|
|
|
|
|
|
|
Ausführung (aus dem Workspace-Root): python -m unittest discover -s scripts/tests -p "test_*.py"
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import ast
|
|
|
|
|
|
import contextlib
|
|
|
|
|
|
import io
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import tempfile
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # scripts/
|
|
|
|
|
|
|
|
|
|
|
|
import test_audit # noqa: E402
|
|
|
|
|
|
import testkatalog as tk # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
WORKSPACE_TOML = """
|
|
|
|
|
|
[workspace]
|
|
|
|
|
|
name = "Test"
|
|
|
|
|
|
arbeitsbranch = "main"
|
|
|
|
|
|
|
|
|
|
|
|
[[repos]]
|
|
|
|
|
|
name = "Testpaket"
|
|
|
|
|
|
paket = "testpaket"
|
|
|
|
|
|
rolle = "infrastruktur"
|
|
|
|
|
|
branch = "main"
|
|
|
|
|
|
tests = true
|
|
|
|
|
|
abhaengigkeiten = []
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
TESTDATEI_INHALT = '''"""Beispieltestdatei für den Test-Audit-Selbsttest."""
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBeispiel(unittest.TestCase):
|
|
|
|
|
|
def test_eins(self):
|
|
|
|
|
|
"""Prüft, dass eins gleich eins ist."""
|
|
|
|
|
|
self.assertEqual(1, 1)
|
|
|
|
|
|
|
|
|
|
|
|
def test_zwei(self):
|
|
|
|
|
|
"""Prüft, dass zwei gleich zwei ist."""
|
|
|
|
|
|
self.assertEqual(2, 2)
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _knoten(quelltext: str) -> tuple[list[str], ast.FunctionDef]:
|
|
|
|
|
|
"""Parst eine einzelne Funktionsdefinition; liefert ihre Zeilen und den ast-Knoten."""
|
|
|
|
|
|
zeilen = quelltext.splitlines()
|
|
|
|
|
|
baum = ast.parse(quelltext)
|
|
|
|
|
|
return zeilen, baum.body[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HashTests(unittest.TestCase):
|
|
|
|
|
|
def test_gleicher_quelltext_ergibt_gleichen_hash(self):
|
|
|
|
|
|
"""Prüft, dass zwei identische Testfunktionen denselben Hash ergeben."""
|
|
|
|
|
|
quelltext = "def test_x(self):\n self.assertEqual(1, 1)\n"
|
|
|
|
|
|
h1 = tk._funktionshash(*_knoten(quelltext))
|
|
|
|
|
|
h2 = tk._funktionshash(*_knoten(quelltext))
|
|
|
|
|
|
self.assertEqual(h1, h2)
|
|
|
|
|
|
|
|
|
|
|
|
def test_geaenderter_testkoerper_aendert_hash(self):
|
|
|
|
|
|
"""Prüft, dass eine inhaltliche Änderung am Testkörper den Hash ändert."""
|
|
|
|
|
|
original = "def test_x(self):\n self.assertEqual(1, 1)\n"
|
|
|
|
|
|
geaendert = "def test_x(self):\n self.assertEqual(1, 2)\n"
|
|
|
|
|
|
self.assertNotEqual(tk._funktionshash(*_knoten(original)), tk._funktionshash(*_knoten(geaendert)))
|
|
|
|
|
|
|
|
|
|
|
|
def test_trailing_whitespace_aendert_hash_nicht(self):
|
|
|
|
|
|
"""Prüft, dass reines Trailing Whitespace am Zeilenende den Hash nicht ändert."""
|
|
|
|
|
|
ohne = "def test_x(self):\n self.assertEqual(1, 1)\n"
|
|
|
|
|
|
mit_leerzeichen = "def test_x(self): \n self.assertEqual(1, 1) \n"
|
|
|
|
|
|
self.assertEqual(tk._funktionshash(*_knoten(ohne)), tk._funktionshash(*_knoten(mit_leerzeichen)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LiesPruefungenTests(unittest.TestCase):
|
|
|
|
|
|
def test_lies_pruefungen_ohne_datei_gibt_leeres_ergebnis(self):
|
|
|
|
|
|
"""Prüft, dass eine fehlende Prüfungsdatei ein leeres Ergebnis ohne Prüfsumme liefert."""
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
|
daten, kurz = tk.lies_pruefungen(Path(tmp) / "nicht_vorhanden.json")
|
|
|
|
|
|
self.assertEqual(daten, {})
|
|
|
|
|
|
self.assertEqual(kurz, "")
|
|
|
|
|
|
|
|
|
|
|
|
def test_lies_pruefungen_liefert_kurzpruefsumme(self):
|
|
|
|
|
|
"""Prüft, dass eine vorhandene Prüfungsdatei eine 12-stellige Kurzprüfsumme liefert."""
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
|
pfad = Path(tmp) / "testpruefungen.json"
|
|
|
|
|
|
pfad.write_text('{"a": {"datum": "2026-01-01", "hash": "abc123"}}', encoding="utf-8")
|
|
|
|
|
|
daten, kurz = tk.lies_pruefungen(pfad)
|
|
|
|
|
|
self.assertEqual(daten["a"]["hash"], "abc123")
|
|
|
|
|
|
self.assertEqual(len(kurz), 12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SetzePruefstatusTests(unittest.TestCase):
|
|
|
|
|
|
def test_setze_pruefstatus_erkennt_nie_geprueft_aktuell_und_veraltet(self):
|
|
|
|
|
|
"""Prüft, dass setze_pruefstatus zwischen nie geprüft, aktuell geprüft und veraltet unterscheidet."""
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
|
pfad = Path(tmp) / "test_beispiel.py"
|
|
|
|
|
|
pfad.write_text(TESTDATEI_INHALT, encoding="utf-8")
|
|
|
|
|
|
with mock.patch.object(tk, "ROOT", Path(tmp)):
|
|
|
|
|
|
datei = tk.lies_testdatei(pfad, "testpaket")
|
|
|
|
|
|
|
|
|
|
|
|
faelle = {f.name: f for f in datei.faelle}
|
|
|
|
|
|
pruefungen = {
|
|
|
|
|
|
tk.voller_testpfad(datei, faelle["test_zwei"]): {
|
|
|
|
|
|
"datum": "2026-01-01",
|
|
|
|
|
|
"hash": faelle["test_zwei"].quellhash,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
katalog = tk.Paketkatalog(paket="testpaket", ordner="Testpaket", rolle="infrastruktur", dateien=[datei])
|
|
|
|
|
|
tk.setze_pruefstatus(katalog, pruefungen)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(faelle["test_eins"].pruefstatus, "nie geprüft")
|
|
|
|
|
|
self.assertEqual(faelle["test_zwei"].pruefstatus, "aktuell geprüft")
|
|
|
|
|
|
|
|
|
|
|
|
pruefungen[tk.voller_testpfad(datei, faelle["test_zwei"])]["hash"] = "veraltet-hash"
|
|
|
|
|
|
tk.setze_pruefstatus(katalog, pruefungen)
|
|
|
|
|
|
self.assertEqual(faelle["test_zwei"].pruefstatus, "veraltet")
|
|
|
|
|
|
|
|
|
|
|
|
def test_pruef_text_formatiert_alle_drei_zustaende(self):
|
|
|
|
|
|
"""Prüft, dass pruef_text für jeden Prüfstatus den erwarteten Anzeigetext liefert."""
|
|
|
|
|
|
nie = tk.Testfall(klasse="T", name="test_a", beschreibung="")
|
|
|
|
|
|
aktuell = tk.Testfall(klasse="T", name="test_b", beschreibung="", pruefstatus="aktuell geprüft", pruefdatum="2026-01-01")
|
|
|
|
|
|
veraltet = tk.Testfall(klasse="T", name="test_c", beschreibung="", pruefstatus="veraltet", pruefdatum="2026-01-01")
|
|
|
|
|
|
self.assertEqual(tk.pruef_text(nie), "nie geprüft")
|
|
|
|
|
|
self.assertEqual(tk.pruef_text(aktuell), "geprüft am 2026-01-01")
|
|
|
|
|
|
self.assertEqual(tk.pruef_text(veraltet), "veraltet seit Änderung – zuletzt geprüft am 2026-01-01")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SchreibePruefungenTests(unittest.TestCase):
|
|
|
|
|
|
def test_markieren_schreibt_deterministisch(self):
|
|
|
|
|
|
"""Prüft, dass schreibe_pruefungen unabhängig von der Einfügereihenfolge dieselbe Datei erzeugt."""
|
|
|
|
|
|
eintrag_a = {"datum": "2026-01-01", "hash": "aaa"}
|
|
|
|
|
|
eintrag_b = {"datum": "2026-01-02", "hash": "bbb"}
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
|
pfad1 = Path(tmp) / "eins.json"
|
|
|
|
|
|
pfad2 = Path(tmp) / "zwei.json"
|
|
|
|
|
|
test_audit.schreibe_pruefungen(pfad1, {"a": eintrag_a, "b": eintrag_b})
|
|
|
|
|
|
test_audit.schreibe_pruefungen(pfad2, {"b": eintrag_b, "a": eintrag_a})
|
|
|
|
|
|
self.assertEqual(pfad1.read_bytes(), pfad2.read_bytes())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WorkspaceGestuetzteTests(unittest.TestCase):
|
|
|
|
|
|
"""Basisklasse mit einem eigenen Mini-Workspace (ein Paket, eine Testdatei)."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
|
|
|
|
self.addCleanup(self._tmp.cleanup)
|
|
|
|
|
|
self.root = Path(self._tmp.name)
|
|
|
|
|
|
(self.root / "Testpaket" / "tests").mkdir(parents=True)
|
|
|
|
|
|
(self.root / "Testpaket" / "tests" / "test_beispiel.py").write_text(TESTDATEI_INHALT, encoding="utf-8")
|
|
|
|
|
|
(self.root / "workspace.toml").write_text(WORKSPACE_TOML, encoding="utf-8")
|
|
|
|
|
|
for patch in (
|
|
|
|
|
|
mock.patch.object(tk, "ROOT", self.root),
|
|
|
|
|
|
mock.patch.object(tk, "MANIFEST", self.root / "workspace.toml"),
|
|
|
|
|
|
):
|
|
|
|
|
|
patch.start()
|
|
|
|
|
|
self.addCleanup(patch.stop)
|
|
|
|
|
|
self.pruefungsdatei = self.root / "testpruefungen.json"
|
|
|
|
|
|
|
|
|
|
|
|
def _stdout(self, aufruf) -> tuple[int, str]:
|
|
|
|
|
|
puffer = io.StringIO()
|
|
|
|
|
|
with contextlib.redirect_stdout(puffer):
|
|
|
|
|
|
code = aufruf()
|
|
|
|
|
|
return code, puffer.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
def test_status_befehl_zeigt_ungeprueften_test_an(self):
|
|
|
|
|
|
"""Prüft, dass der status-Befehl einen noch nie geprüften Testfall als solchen anzeigt."""
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
args = argparse.Namespace(
|
2026-09-16 12:45:12 +02:00
|
|
|
|
pakete=[], muster=None, nur=None, code=False,
|
2026-09-16 10:23:45 +02:00
|
|
|
|
baseline_datei=self.root / "keine_baseline.json",
|
|
|
|
|
|
pruefungsdatei=self.pruefungsdatei,
|
|
|
|
|
|
)
|
|
|
|
|
|
code, ausgabe = self._stdout(lambda: test_audit.befehl_status(args))
|
|
|
|
|
|
self.assertEqual(code, 0)
|
|
|
|
|
|
self.assertIn("test_eins", ausgabe)
|
|
|
|
|
|
self.assertIn("nie geprüft", ausgabe)
|
2026-09-16 12:45:12 +02:00
|
|
|
|
self.assertRegex(ausgabe, r"test_beispiel\.py:\d+")
|
|
|
|
|
|
|
|
|
|
|
|
def test_status_befehl_mit_code_gibt_quelltext_aus(self):
|
|
|
|
|
|
"""Prüft, dass --code den Testquelltext der passenden Methode mit ausgibt."""
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
args = argparse.Namespace(
|
|
|
|
|
|
pakete=[], muster="test_eins", nur=None, code=True,
|
|
|
|
|
|
baseline_datei=self.root / "keine_baseline.json",
|
|
|
|
|
|
pruefungsdatei=self.pruefungsdatei,
|
|
|
|
|
|
)
|
|
|
|
|
|
_, ausgabe = self._stdout(lambda: test_audit.befehl_status(args))
|
|
|
|
|
|
self.assertIn("def test_eins(self):", ausgabe)
|
|
|
|
|
|
self.assertIn("assertEqual(1, 1)", ausgabe)
|
2026-09-16 10:23:45 +02:00
|
|
|
|
|
|
|
|
|
|
def test_markieren_ohne_treffer_gibt_exitcode_1(self):
|
|
|
|
|
|
"""Prüft, dass ein Muster ohne Treffer Exit-Code 1 liefert und keine Datei schreibt."""
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
args = argparse.Namespace(
|
|
|
|
|
|
paket="testpaket", bezeichner=["NichtVorhanden.*"], datum="2026-01-01",
|
|
|
|
|
|
trocken=False, pruefungsdatei=self.pruefungsdatei,
|
|
|
|
|
|
)
|
|
|
|
|
|
code, _ = self._stdout(lambda: test_audit.befehl_markieren(args))
|
|
|
|
|
|
self.assertEqual(code, 1)
|
|
|
|
|
|
self.assertFalse(self.pruefungsdatei.exists())
|
|
|
|
|
|
|
|
|
|
|
|
def test_markieren_schreibt_treffer_und_status_zeigt_es_danach_an(self):
|
|
|
|
|
|
"""Prüft, dass ein markierter Testfall im Status anschließend als aktuell geprüft erscheint."""
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
markieren_args = argparse.Namespace(
|
|
|
|
|
|
paket="testpaket", bezeichner=["TestBeispiel.test_eins"], datum="2026-01-01",
|
|
|
|
|
|
trocken=False, pruefungsdatei=self.pruefungsdatei,
|
|
|
|
|
|
)
|
|
|
|
|
|
code, _ = self._stdout(lambda: test_audit.befehl_markieren(markieren_args))
|
|
|
|
|
|
self.assertEqual(code, 0)
|
|
|
|
|
|
self.assertTrue(self.pruefungsdatei.exists())
|
|
|
|
|
|
|
|
|
|
|
|
status_args = argparse.Namespace(
|
2026-09-16 12:45:12 +02:00
|
|
|
|
pakete=[], muster="test_eins", nur=None, code=False,
|
2026-09-16 10:23:45 +02:00
|
|
|
|
baseline_datei=self.root / "keine_baseline.json",
|
|
|
|
|
|
pruefungsdatei=self.pruefungsdatei,
|
|
|
|
|
|
)
|
|
|
|
|
|
_, ausgabe = self._stdout(lambda: test_audit.befehl_status(status_args))
|
|
|
|
|
|
self.assertIn("geprüft am 2026-01-01", ausgabe)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
unittest.main()
|