Files
Projektwissen/scripts/tests/test_test_audit.py
T
DanielandClaude Sonnet 5 ec400ae59e Test-Audit: Datei:Zeile und Quelltext-Ausgabe für bequemes Ansehen/Editieren
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>
2026-09-16 12:45:12 +02:00

237 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()