Files
Projektwissen/scripts/tests/test_test_audit.py
T

237 lines
10 KiB
Python
Raw Normal View History

"""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(
pakete=[], muster=None, nur=None, code=False,
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)
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)
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(
pakete=[], muster="test_eins", nur=None, code=False,
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()