"""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()