status zeigt je Treffer "<Datei>:<Zeile>" (in Editor-Terminals anklickbar) und optional per --code den Testquelltext im Terminal. Der Prüfstand selbst bleibt ohne Quelltext, um Diff-Lärm bei jeder Codeänderung zu vermeiden. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
237 lines
10 KiB
Python
237 lines
10 KiB
Python
"""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()
|