37 Commits
Author SHA1 Message Date
DanielandClaude Opus 5.5 e570d4524b Test-Audit sn_basis fortgesetzt: pruef_ergebnis.py fachlich geprüft (#76)
6 Testfälle nach Freigabe als geprüft markiert: test_pruef_ergebnis.py komplett
(Docstring-Anzahl korrigiert). Nebenbefund: die geprüften Properties
ist_verfahrens_db_aktion, erlaubte_persistierung und dateipfad werden derzeit nirgends
verwendet; PruefAktion schreibt "Datenbank" groß, Dateipruefer liefert "datenbank".

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:34:49 +02:00
DanielandClaude Opus 5.5 693e453fc6 Test-Audit sn_basis fortgesetzt: print_layout.py fachlich geprüft (#76)
8 Testfälle nach Freigabe als geprüft markiert: test_print_layout.py komplett
(5 bestehende, 3 neue für create_single_page_layout). Nebenbefunde (direkter
qgis.PyQt-Import per importlib, removeGrid-Mock nach Name statt UUID, ungenutztes
width_limit_mm) bewusst ohne Issue: das Druckmodul wird ohnehin grundlegend überarbeitet.

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:01:44 +02:00
DanielandClaude Opus 5.5 6966a67bd1 Test-Audit sn_basis fortgesetzt: os_wrapper.py fachlich geprüft (#76)
3 Testfälle nach Freigabe als geprüft markiert: test_os_wrapper.py komplett
(IsCaseSensitiveFsTests, alle drei Zweige abgedeckt, keine neuen Tests nötig).
Nebenbefund: Das Ziel „OS-Prüfung nur einmal, Plugin-Code ohne OS-Weichen“ ist nur
teilweise erfüllt (hasattr(os, "startfile") in Plan41, Wurzelpfad-Prüfung von Hand in
Dateipruefer/stilpruefer, ungenutzte Wrapper-Symbole). Dafür ist ein eigenes Issue
in Plugin_SN_Basis als Entwurf vorbereitet.

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:49:12 +02:00
DanielandClaude Opus 5.5 1c4da60845 Regel: Testfälle nur auf ausdrückliche Aufforderung als geprüft markieren (#16)
Die Markierung per test_audit.py markieren bestätigt eine fachliche Abnahme durch einen
Menschen. Agenten sichten und ergänzen Tests und schlagen Markierungen vor, markieren aber
erst nach ausdrücklicher Freigabe. Aufgenommen in AGENTS.md (harte Regeln, Tests),
docs/leitfaden/06_tests.md Abschnitt 7 und Skill sn-tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:39:44 +02:00
DanielandClaude Opus 5.5 1435b157b5 Test-Audit sn_basis fortgesetzt: navigation.py fachlich geprüft (#76)
15 Testfälle fachlich geprüft und markiert: test_navigation.py komplett (12 bestehende,
3 neue für Standardpriorität, Reihenfolge bei gleicher Priorität und fehlende Toolbar).
Nebenbefund: remove_action entfernt die Action aus Menü, Toolbar und Liste, aber nicht aus
plugin_group und ruft kein deleteLater() auf – mögliches Überbleibsel beim Plugin-Reload,
analog zu #72. Nicht behoben (kein Umbau nebenbei).

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:33:49 +02:00
DanielandClaude Opus 5.5 decd2a0002 Test-Audit sn_basis fortgesetzt: message_wrapper.py fachlich geprüft (#76)
5 Testfälle fachlich geprüft und markiert: test_message_wrapper.py komplett (inkl.
1 neuer Test für die Meldungsstufen von info/warning/error/success). Nebenbefund:
die Namens-Heuristik des Testkatalogs führte warning als referenziert, weil es in
test_pruefmanager/test_dockmanager nur per @patch ersetzt wird (bekannte, in
testkatalog.py dokumentierte Grenze).

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:33:26 +02:00
DanielandClaude Opus 5.5 e747b2227b Test-Audit sn_basis fortgesetzt: ly_style_wrapper.py fachlich geprüft (#76)
5 Testfälle fachlich geprüft und markiert: test_ly_style_wrapper.py komplett (inkl.
1 neuer Test für die Pfadübergabe). Nebenbefunde: Debug-print() in apply_style als
Kommentar zu #79 vorbereitet; ungültige Layer in apply_style/apply_style_from_path
im Code mit Verweis auf #84 markiert. Nachtrag zum vorigen Audit-Commit: die dort
vorbereiteten Issues sind #84 (layer_is_valid/layer_is_usable) und #85 (Layerpruefer).

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:23:55 +02:00
DanielandClaude Opus 5.5 3a872737a7 Test-Audit sn_basis fortgesetzt: ly_existence_wrapper.py fachlich geprüft (#76)
11 Testfälle fachlich geprüft und markiert: test_ly_existence_wrapper.py komplett
(inkl. 3 neuer Tests für layer_exists). Dabei gefunden und in Plugin_SN_Basis als
Issues vorbereitet (Absenden durch den Menschen): layer_is_valid/layer_is_usable
haben keinen Aufrufer, isValid() wird rund 20-mal direkt aufgerufen (nutzen oder
entfernen); Layerpruefer lässt ungültige Layer im Existenz-Schritt durch.

Testkatalog neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 09:08:48 +02:00
DanielandClaude Sonnet 5 94ab55923d Test-Audit sn_basis fortgesetzt: landkreise.py und LayerLoader.py fachlich geprüft (#76)
38 Testfälle fachlich geprüft und markiert: test_landkreise.py komplett (6, inkl.
4 neuer Tests) und test_layerloader.py komplett (32, inkl. 7 neuer Tests). Dabei
gefunden und in Plugin_SN_Basis als Issues vorbereitet (Absenden durch den
Menschen): #80 (REST-Link ohne Layer-Index lädt still Layer 0), #81 (unbekannter
Provider fällt still auf ogr zurück), #82 (Datenabruf und LayerLoader bauen aus
demselben REST-Link widersprüchliche URLs), #83 (filter_by_extent/filter_by_layer
reichen ungültige Layer still durch).

Testkatalog neu erzeugt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:26:25 +02:00
DanielandClaude Sonnet 5 a6dfbefbd4 Test-Audit sn_basis fortgesetzt: test_datenabruf, test_datenschreiber, test_dockmanager (#76)
48 weitere Testfälle in sn_basis fachlich geprüft und markiert (test_datenabruf.py: 24,
test_datenschreiber.py: 18, test_dockmanager.py: 6). Dabei zwei fragliche Codestellen
gefunden und als offene Punkte in STATUS.md festgehalten:
- Basis #77: Datenabruf._fetch_features() klassifiziert WMS-Zeilen immer als "ausserhalb",
  ohne dass je ein Layer geschrieben wird; der Kommentar "caller behandelt WMS separat"
  trifft nicht zu.
- Basis #78: Datenschreiber.schreibe_log() meldet Erfolg mit dem sachfremden Aktionscode
  "standarddatei_vorschlagen" (vermutlich Copy-Paste-Rest), aktuell folgenlos.

Testkatalog neu erzeugt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 14:49:28 +02:00
DanielandClaude Sonnet 5 159efc27a7 Testkatalog neu erzeugt für Basis #76
Docstring-Präzisierung von test_fehlender_layer_liefert_none übernommen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:49:25 +02:00
DanielandClaude Sonnet 5 8f54251eaf Test-Audit: 19 sn_basis-Tests fachlich geprüft (test_base_dockwidget, test_datagrabber, test_dateipruefer)
CloseEventTests (test_base_dockwidget.py, 5 Tests), TestDataGrabber._aggregate_results-Tests
(test_datagrabber.py, 2 Tests) und alle bisher ungeprüften TestDateipruefer-Tests
(test_dateipruefer.py, 12 Testfälle inkl. der mit #70 ergänzten) als am 2026-09-21 fachlich
geprüft markiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:00:05 +02:00
DanielandClaude Sonnet 5 b20503d7b6 Testkatalog neu erzeugt für Basis #72
sn_basis: 329 Testfälle (zuvor 328), Navigation.remove_all()-Test aus #72 mit erfasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 10:45:06 +02:00
DanielandClaude Sonnet 5 049a5ffe1d Testkatalog neu erzeugt, Wissensbasis für Basis #70 nachgezogen
sn_basis steht laut Prüfstand jetzt bei 0 ungeklärten Funktionen. Fallstricke.md um den
Windows-Fallstrick "sqlite3-Verbindung aus with-Block schließt nicht, TemporaryDirectory-
Cleanup schlägt fehl" ergänzt (Fund aus Basis #70).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 08:50:32 +02:00
DanielandClaude Sonnet 5 cd11e409a3 Prüfstand: Statistik-Kacheln je Paket, beabsichtigt/ungeklärt-Indikator
Jedes Paket zeigt jetzt dieselben Kennzahl-Kacheln wie die Gesamtübersicht
(Testdateien, Testfälle, grün/rot/übersprungen, mit Docstring, geprüft,
ohne Testbezug) statt eines Fließtexts.

Ungetestete Funktionen werden per Docstring-Auswertung als "beabsichtigt"
(Vermerk "von Test abgesehen, weil …" oder "Test ausgesetzt, zunächst
Refactor") oder "ungeklärt" markiert - in Markdown als Anmerkung je
Funktion, im Prüfstand als Pille mit Tooltip (Begründung).

Der Abschnitt "Ungetestete Funktionen" ist im Prüfstand jetzt eine
ausklappbare Karte wie eine Testdatei, mit eigener Kennzahl-Übersicht
(Anzahl, ungeklärt, beabsichtigt) im Kopf - bewusst von den Filter-Chips
ausgenommen (.gapcard), da diese sich auf Testfall-Status beziehen, nicht
auf Quellcode-Funktionen.

Neue Funktion absichtlicher_verzicht() plus 6 neue Tests in
scripts/tests/test_testkatalog_quellfunktionen.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:26:01 +02:00
DanielandClaude Sonnet 5 cb29620f6e Testkatalog neu erzeugt für Basis #67
sn_basis: 317 Testfälle (32 Testdateien), Kategorie "vielleicht testen"
abgeschlossen. modulkarte.md an aktuelle Testdateizahl angepasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:07:20 +02:00
DanielandClaude Sonnet 5 3f985ce209 Testkatalog neu erzeugt, Wissensbasis für Basis #65 nachgezogen
sn_basis: 244 -> 249 Testfälle. fallstricke.md: _MockQDockWidget ohne
closeEvent(), zwei bestätigte Testkatalog-Fehlalarme (toter Code in
Pruefmanager._ask_use_or_replace_pufferlayer, Issue #60) sowie das Muster
Klasse.__new__(Klasse) für UI-Klassen, die im Mock nicht konstruierbar
sind. modulkarte.md an aktuelle Testdateizahl angepasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 11:20:37 +02:00
DanielandClaude Sonnet 5 b47f740c10 STATUS.md: Merge von Basis #63 und Verfahrensgebiet #53 nachgetragen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 10:10:54 +02:00
DanielandClaude Sonnet 5 1359ba2761 Testkatalog neu erzeugt, Wissensbasis für Basis #63 nachgezogen
sn_basis: 96 -> 166 Testfälle (Datenabruf, Datenschreiber, LayerLoader,
excel_importer bekamen Tests). fallstricke.md: unvollständiger
_MockQgsProject (transformContext/addMapLayer fehlten). modulkarte.md und
STATUS.md an aktuellen Stand angepasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 09:50:45 +02:00
DanielandClaude Sonnet 5 906740eff6 Testkatalog: strukturelle Lückenanalyse (ungetestete Funktionen)
Ergänzt den Testkatalog um einen Abschnitt "Ungetestete Funktionen": Quellcode-Funktionen
und -Methoden, deren Name in keiner Testdatei des Pakets vorkommt (Namensabgleich per ast,
ohne Import). Keine Wiederaufnahme der in ADR 0009 verworfenen Zeilen-Coverage-in-Prozent,
sondern eine Lückenliste nach demselben Muster wie der bestehende Abschnitt für fehlende
Docstrings - entsprechend in ADR 0009 und docs/leitfaden/06_tests.md ergänzt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 07:37:40 +02:00
DanielandClaude Sonnet 5 e5e1b6e768 Testkatalog: neuer Test für Grenzpunkt-Stilanbringung (#53)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:29:26 +02:00
DanielandClaude Sonnet 5 4d036516a1 Testkatalog: neuer Test für get_plugin_root()-Fix (Verfahrensgebiet #50)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:12:17 +02:00
DanielandClaude Sonnet 5 4f87684224 Test-Audit: TestStilpruefer (sn_basis) fachlich geprüft
Alle 4 Tests gegen modules/stilpruefer.py geprüft - Docstrings passen
zur Implementierung, keine Diskrepanzen. Dabei entdeckt: der relative
Pfad-Zweig (sn_plan41/assets-Redirect) ist untestet und hängt von
get_plugin_root() ab, das laut #50 in Plugin_SN_Verfahrensgebiet einen
Junction-Auflösungsfehler hat. Test für diesen Zweig zurückgestellt,
bis der #50-Fix vorliegt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 15:51:40 +02:00
DanielandClaude Sonnet 5 a2467d3431 Test-Audit: TestSettingsLogic (sn_basis) fachlich geprüft
Alle 4 Tests gegen functions/settings_logic.py geprüft. Eine Docstring
war ungenau (Fallback-Verantwortung fälschlich load() zugeschrieben,
siehe vorheriger Commit) - jetzt korrigiert, alle vier bestätigt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 15:04:42 +02:00
DanielandClaude Sonnet 5 6b687c34d2 Testkatalog neu erzeugt: korrigierte Docstring test_settings_logic
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 15:01:29 +02:00
DanielandClaude Sonnet 5 fe55d5b914 Test-Audit: TestPruefmanager (sn_basis) fachlich geprüft
Alle 14 Tests gegen modules/Pruefmanager.py geprüft - Docstrings passen
zur Implementierung. Dabei entdeckt: _ask_use_or_replace_pufferlayer()
und _handle_verfahrensgebiet_layer_existiert() haben weder Tests noch
Aufrufer im gesamten Workspace; erstere referenziert ein nicht
existierendes self.pruefmanager (Absturz bei Aufruf) - dafür eigenes
Issue angelegt (Entwurf in entwuerfe/), analog zu #59.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:56:52 +02:00
DanielandClaude Sonnet 5 0f8f80a71d Test-Audit: TestLinkpruefer (sn_basis) fachlich geprüft
Alle 6 Tests (5 bestehende + neuer Testfall für leere Eingabe) gegen
modules/linkpruefer.py geprüft - Docstrings passen zur Implementierung,
keine Diskrepanzen gefunden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:46:22 +02:00
DanielandClaude Sonnet 5 18bb17ac57 Test-Audit: TestLayerpruefer (sn_basis) fachlich geprüft
Alle 10 Tests gegen modules/layerpruefer.py geprüft - Docstrings passen
zur Implementierung, keine Diskrepanzen gefunden. Monkeypatch-Isolation
in setUp() (kein tearDown) bewusst unangetastet gelassen, da aktuell
keine andere Testdatei Layerpruefer verwendet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:42:34 +02:00
DanielandClaude Sonnet 5 aa63f234e7 Test-Audit: TestGetLaenderkennung (sn_basis) fachlich geprüft
Alle 5 Tests gegen functions/laenderkennung.py geprüft - Docstrings
passen zur Implementierung, keine Diskrepanzen gefunden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:38:42 +02:00
DanielandClaude Sonnet 5 3a07785a66 Test-Audit: TestDateipruefer (5 von 6) fachlich geprüft
leereingabe_erlaubt, leereingabe_nicht_erlaubt, temporaer_erlaubt,
datei_nicht_gefunden, datei_ok geprüft und bestätigt - Docstrings passen
zur Implementierung. test_standarddatei_vorschlagen bewusst ausgenommen,
solange die Design-Entscheidung zu Basis #59 (Standarddatei-Modus) offen
ist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:35:44 +02:00
DanielandClaude Sonnet 5 78105fcd64 STATUS.md: offenen Punkt zur Standarddatei-Existenzprüfung ergänzt (Basis #59)
Fund aus dem Test-Audit (#16): Dateipruefer/Pruefmanager prüfen bei
standarddatei_vorschlagen nie file_exists(). Bearbeitung bewusst
zurückgestellt, bis geklärt ist, ob der Standarddatei-Modus überhaupt
gebraucht wird - separates Issue Basis #59, eigener Branch bei Bedarf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 14:27:23 +02:00
DanielandClaude Sonnet 5 f2eb883162 Testkatalog neu erzeugt: TestFlurstueckAlkis-Korrekturen (#16)
Nach der Testabgleichung in Plugin_SN_Verfahrensgebiet (bugfix/16-
flurstueck-alkis-testabgleich): Docstrings jetzt in "Prueft, dass ..."-
Form, ein Test entfernt, Gruen/Rot-Zaehlung aktualisiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:12:03 +02:00
DanielandClaude Sonnet 5 4fb90fba31 Testkatalog neu erzeugt: restliche sn_basis-Docstrings
sn_basis steht jetzt bei 94 von 94 Testfällen mit Docstring (100 %),
Gegenstück zu Plugin_SN_Basis-Commit 23aab76 (Branch
feature/testdatagrabber-docstrings).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:06:33 +02:00
DanielandClaude Sonnet 5 accf1bf7e8 Test-Audit: TestDataGrabber (sn_basis) fachlich geprüft
Erster echter Audit-Durchgang mit scripts/test_audit.py: alle sechs
TestDataGrabber-Testfälle durchgesehen, mit Docstring versehen
(Plugin_SN_Basis, separater Branch/PR) und als geprüft markiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 13:18:13 +02:00
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
DanielandClaude Sonnet 5 5dfec4f934 STATUS.md: Test-Audit-Infrastruktur als offene Baustelle ergänzt
Ticket #16 ist mit dieser PR technisch umgesetzt (Skript + Datenformat +
Prüfstand); der eigentliche Audit-Durchgang durch die 346 Testfälle ist
ein separater nächster Schritt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 10:25:09 +02:00
DanielandClaude Sonnet 5 d9d2542a20 Test-Audit einführen: Prüfstatus je Testfall im Prüfstand sichtbar machen
Ticket #16. docs/status/testpruefungen.json hält je Testfall Datum und
Kurzprüfsumme der letzten fachlichen Prüfung fest; scripts/testkatalog.py
liest das und zeigt es als eigene Spalte/Pille (nie geprüft / geprüft am …
/ veraltet seit Änderung …), unabhängig vom Baseline-Status. Neues Skript
scripts/test_audit.py (status/markieren) pflegt die Datei; erste Tests für
scripts/ unter scripts/tests/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 10:23:45 +02:00
14 changed files with 4130 additions and 652 deletions
+6
View File
@@ -57,6 +57,12 @@ listet Tests ohne Docstring – Stand 15.09.2026 haben nur 114 von 346 Tests ein
Testdatei per Mermaid, Filter, Suche) – lokal im Browser öffnen, kein Server nötig; Mermaid lädt von
`cdnjs.cloudflare.com`, ohne Internetverbindung bleiben nur die Diagramme leer.
## Test-Audit (Prüfvermerke)
`python scripts/test_audit.py status <paket> --nur ungeprueft` zeigt offene Testfälle. **`markieren` nur auf
ausdrückliche Aufforderung** – die Markierung bedeutet „ein Mensch hat fachlich bestätigt". Ohne Freigabe:
sichten, Lücken ergänzen, mit `markieren … --trocken` die vorgeschlagenen Treffer zeigen und nachfragen.
Details: `docs/leitfaden/06_tests.md` Abschnitt 7.
## Testkonzept-Seiten
Für fachlich wichtige Abläufe liegt in `docs/tests/<ablauf>.md` eine Seite nach `docs/vorlagen/testkonzept.md`:
Mermaid-Diagramm des fachlichen Ablaufs, Tabelle Schritt → deckender Test → bewusst nicht getestet mit
+3
View File
@@ -45,6 +45,9 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
Danach `python scripts/testkatalog.py` neu erzeugen und mitcommitten.
- Tests werden nicht gelöscht, nicht mit `skip` stillgelegt und nicht in ihrer Erwartung angepasst, um grün zu werden –
außer das Verhalten wurde bewusst geändert und der Pull Request begründet es. Details: `docs/leitfaden/06_tests.md`.
- **Testfälle nur auf ausdrückliche Aufforderung als „geprüft" markieren** (`scripts/test_audit.py markieren`).
Die Markierung bestätigt, dass ein Mensch den Test fachlich abgenommen hat; ein Auftrag wie „Audit fortsetzen"
reicht dafür nicht. Agenten sichten, ergänzen Tests und schlagen die Markierung vor – markieren erst nach Freigabe.
### Code
+7 -1
View File
@@ -63,7 +63,7 @@ Legende: ⚠ = über 600 Zeilen, nie komplett laden, per `grep -n "def "` navigi
| Pfad | Zweck |
|---|---|
| `tests/` | 13 Testdateien, `unittest`, Mock-Modus; `run_tests.py` (farbiger Runner), `test_qgis.bat` (Coverage in OSGeo4W) |
| `tests/` | 32 Testdateien, `unittest`, Mock-Modus; `run_tests.py` (farbiger Runner), `test_qgis.bat` (Coverage in OSGeo4W) |
| `styles/` | QML-Stile (Verfahrensgebiet, LSG, Denkmalschutz, Flurstücksbeschriftung) |
| `data/landkreise.json` | Landkreise mit Kennzahl |
| `assets/plan-qgis340ExitCrashFix.prompt.md` | KI-Planungsnotiz zum Exit-Crash (historisch) |
@@ -124,6 +124,12 @@ Dient als **minimales Muster** für ein neues Fachplugin (Skill `sn-neues-fachpl
| `Linkliste` | `linkliste.csv` → Gitea Action erzeugt XML/JSON/YAML |
| `Defaults` | Beispiel-Verfahrensdaten (JSON) und `styles/verfahrensgebiet.qml`; nicht vom Code referenziert |
## Workspace-Skripte (dieses Repository, `scripts/`)
| Pfad | Zweck |
|---|---|
| `scripts/test_audit.py` | Prüf-Audit: zeigt (`status`) und markiert (`markieren`) je Testfall, wann er zuletzt fachlich geprüft wurde; schreibt `docs/status/testpruefungen.json`. Nutzt die Katalog-Logik aus `scripts/testkatalog.py`, siehe `docs/leitfaden/06_tests.md` Abschnitt 7. |
## Größte Dateien (Kandidaten für das spätere Refactoring)
| Datei | Zeilen |
@@ -41,3 +41,11 @@ Nur den generierten Katalog (zeigt was getestet wird, aber nicht ob es genügt
geht, fiele weg); nur handgeschriebene Testkonzepte (laufen dem Code hinterher, dieselbe Drift, die die
Wissensbasis sonst vermeidet); Testabdeckung in Prozent als Ziel (misst durchlaufene Zeilen, nicht fachliche
Vollständigkeit, und verleitet zu Tests ohne Aussage).
## Ergänzung (18.09.2026)
Der Testkatalog weist seither zusätzlich einen Abschnitt „Ungetestete Funktionen (strukturelle Lücke)" aus:
Funktionen/Methoden im Quellcode, deren Name in keiner Testdatei des Pakets vorkommt (Namensabgleich per `ast`,
ohne Import und ohne Laufzeitmessung). Das ist **keine** Wiederaufnahme der oben verworfenen
Zeilen-Coverage-in-Prozent: Es gibt keine Prozentzahl als Ziel, sondern eine Lückenliste nach demselben Muster
wie der bestehende Abschnitt „Lücken" für fehlende Docstrings — ein Hinweis, wo ein Mensch hinschauen sollte,
kein Ausführungsnachweis und keine Aussage über fachliche Richtigkeit.
+57 -5
View File
@@ -1,7 +1,7 @@
# Tests: was geprüft wird, wie man es liest, was Pflicht ist
Stand 04.09.2026. Für alle, die an den LNO-Plugins mitarbeiten — auch für Fachleute ohne Programmiererfahrung.
Der Abschnitt 7 ist ausdrücklich für Sie geschrieben: dort geht es darum, zu **beurteilen**, ob eine Testsuite
Der Abschnitt 8 ist ausdrücklich für Sie geschrieben: dort geht es darum, zu **beurteilen**, ob eine Testsuite
fachlich das Richtige prüft. Dafür muss man keinen Code lesen können.
## 1. Warum das hier ernster genommen wird als sonst
@@ -113,10 +113,62 @@ Wozu das gut ist: Man sieht auf zwei Seiten, was die Suite abdeckt, ohne eine ei
Abschnitt „Lücken" am Ende listet alle Tests ohne Docstring — das ist die Arbeitsliste, um den Katalog
vollständig lesbar zu machen.
Ein zweiter Abschnitt, „Ungetestete Funktionen (strukturelle Lücke)", dreht die Blickrichtung um: Er listet
Funktionen und Methoden im **Quellcode**, deren Name in keiner Testdatei des Pakets vorkommt (Namensabgleich
per `ast`, ohne Import). Das ist bewusst **keine** Zeilen-Coverage in Prozent — die wurde in ADR 0009 als Ziel
verworfen, weil sie durchlaufene Zeilen statt fachliche Vollständigkeit misst. Es ist eine Lückenliste wie die
fehlenden Docstrings: ein Hinweis, wo ein Blick lohnt, kein Ausführungsnachweis und kein Ziel in Prozent.
Jede so gefundene Funktion gilt zusätzlich als **beabsichtigt** oder **ungeklärt**: Der Katalog liest dafür
den Docstring der Funktion und erkennt zwei Vermerke — `"""von Test abgesehen, weil …; Kürzel Datum"""` für
eine bewusst als vernachlässigbar eingestufte Funktion, `"""Test ausgesetzt, zunächst Refactor"""` für eine
Funktion, die erst nach einer geplanten Überarbeitung getestet wird. Fehlt der Vermerk, gilt die Funktion als
**ungeklärt** — das ist die eigentliche Arbeitsliste, nicht die beabsichtigten. Im Prüfstand
(`docs/tests/testkatalog.html`) erscheint das je Paket als eigene, aufklappbare Karte mit Kennzahlen
(„X ungeklärt, Y beabsichtigt"), genau wie bei den Testdateien.
Nach dem Hinzufügen von Tests den Katalog neu erzeugen und mit committen. `python scripts/testkatalog.py --pruefen`
meldet, wenn er veraltet ist.
## 7. Testkonzepte: prüfen, ob die Tests fachlich sinnvoll sind
## 7. Test-Audit: wann wurde ein Test zuletzt fachlich geprüft?
Der Testkatalog beantwortet „was prüft die Suite" und „läuft der Test durch" (Baseline, Abschnitt 8). Er
beantwortet **nicht** „hat das ein Mensch fachlich bestätigt". Ein Agent kann einen überzeugenden, aber
fachlich falschen Test schreiben (Abschnitt 1) — weder Docstring noch Baseline zeigen das. Das Test-Audit
schließt diese Lücke: `docs/status/testpruefungen.json` hält je Testfall Datum und Kurzprüfsumme der letzten
Prüfung fest. Ändert sich der Testcode danach, erkennt der Prüfstand das automatisch als **veraltet** — die
Prüfsumme stimmt nicht mehr überein.
Im Prüfstand (`docs/tests/testkatalog.html`) erscheint das als eigene Spalte/Pille je Testfall (unabhängig vom
Baseline-Status: ein grüner Test kann nie geprüft sein, ein roter Baseline-Test kann bereits geprüft sein) und
als eigene Filter-Chips „Nie geprüft" / „Veraltet geprüft".
```bash
python scripts/test_audit.py status sn_verfahrensgebiet # Prüfstatus je Testfall anzeigen
python scripts/test_audit.py status --nur ungeprueft # nur ungeprüfte Testfälle
python scripts/test_audit.py status sn_basis --muster TestX --code # mit Quelltext im Terminal
python scripts/test_audit.py markieren sn_basis TestX.test_y # als heute geprüft markieren
```
**Markiert wird nur auf ausdrückliche Aufforderung eines Menschen.** Die Markierung sagt „fachlich bestätigt",
nicht „ein Agent hat es gelesen". Ein Agent sichtet die Tests, ergänzt Lücken und nennt die Testfälle, die er zur
Markierung vorschlägt; `markieren` (ohne `--trocken`) ruft er erst auf, wenn die Person die Markierung ausdrücklich
freigibt. Ein allgemeiner Auftrag wie „Audit fortsetzen" ist keine solche Freigabe.
`markieren` nimmt exakte Bezeichner oder `fnmatch`-Muster (z. B. `"TestX.*"`) entgegen; `--trocken` zeigt die
Treffer, ohne zu schreiben — vor einem Muster mit vielen Treffern empfohlen. Das Skript importiert die
Katalog-Logik aus `scripts/testkatalog.py`, statt sie zu duplizieren.
`status` zeigt zu jedem Treffer „<Datei>:<Zeile>" — in den meisten Editor-Terminals anklickbar, um den Test
direkt zu öffnen und zu bearbeiten. Der Prüfstand selbst zeigt bewusst **keinen** Quelltext: er ist eine
versionierte, generierte Datei, und Quelltext darin würde bei jeder Codeänderung riesige, wenig aussagekräftige
Diffs erzeugen. Zum Lesen ohne Editor dient `--code`, zum Bearbeiten der übliche Weg über Konsole/Editor.
Abgrenzung zu Abschnitt 8 (Testkonzepte): Das Audit bestätigt, dass **vorhandene** Tests noch fachlich
zutreffen. Ob überhaupt genug getestet wird, zeigen die Testkonzept-Seiten — das ist der nächste, gesonderte
Schritt, kein Ersatz für das Audit.
## 8. Testkonzepte: prüfen, ob die Tests fachlich sinnvoll sind
Der Katalog beantwortet „was wird getestet". Er beantwortet **nicht** „ist das genug". Dafür gibt es je
wichtigem Fachablauf eine eigene Seite in [`docs/tests/`](../tests/), nach der Vorlage
@@ -143,7 +195,7 @@ Ein Beispiel liegt in [`docs/tests/verfahrensgebiet-shape-import.md`](../tests/v
Wenn Sie eine Lücke finden: Issue aufmachen (`python scripts/gitea.py entwurf issue Verfahrensgebiet` legt einen
Entwurf an). Eine gefundene Testlücke ist ein vollwertiges Ticket.
## 8. Die Baseline
## 9. Die Baseline
Nicht alle Tests sind grün. Der Ist-Stand steht in [`docs/status/testbaseline.json`](../status/testbaseline.json)
und wirkt als Sperrklinke: **bekannte rote Tests dürfen bleiben, neue nicht dazukommen.**
@@ -159,7 +211,7 @@ python scripts/run_tests.py sn_verfahrensgebiet --baseline
Die Baseline ändert **nur ein Maintainer** mit `--update-baseline`, nach Absprache. Ein Agent fasst sie nicht an.
Das Grünmachen der bekannten roten Tests läuft über eigene Issues, nicht nebenbei.
## 9. Wenn ein Test rot ist
## 10. Wenn ein Test rot ist
Erst die Frage beantworten: **Ist der Test veraltet oder ist der Code falsch?**
@@ -177,7 +229,7 @@ das ist das Verstecken des Problems vor dem nächsten Leser.
Details zum Deuten einzelner Fehler: Skill `sn-tests`.
## 10. Vor dem Pull Request
## 11. Vor dem Pull Request
- [ ] `python scripts/run_tests.py <paket> --baseline` ohne `NEU ROT`
- [ ] Neue Funktionalität hat mindestens einen Test je neuem Verhalten
+44 -8
View File
@@ -1,7 +1,7 @@
# Status des Projekts
Kurz und aktuell. Wird bei jedem Merge und jeder Übergabe gepflegt (Skill `sn-uebergabe`). Älteres wandert nach unten
in „Erledigt" oder wird gelöscht. **Stand: 04.09.2026.**
in „Erledigt" oder wird gelöscht. **Stand: 18.09.2026.**
## Aktuelle Phase
@@ -13,7 +13,7 @@ spätere Phase und beginnt erst, wenn die Test-Baseline grün ist und das Projek
| Person / Behörde | Schwerpunkt | Stand |
|---|---|---|
| Daniel Helbig (LK Meißen) | Fachfunktionen Verfahrensgebiet, Plan41, VLN-API | aktiv |
| Daniel Helbig (LK Meißen) | Fachfunktionen Verfahrensgebiet, Plan41, VLN-API; aktuell Test-Audit (Basis #63 gemergt, weitere Lücken laut Testkatalog offen) | aktiv |
| Michael Otto (LK Mittelsachsen) | Release-Infrastruktur, Linkliste, zuletzt Verfahrensgebiet (Shape-Laden, Beteiligung) | aktiv |
| 22munzke | Druck/Layout-Anforderungen (Issues Basis #37–#44) | Issues |
| weitere Behörden | Einstieg über dieses Repository | ab 09/2026 |
@@ -30,16 +30,18 @@ spätere Phase und beginnt erst, wenn die Test-Baseline grün ist und das Projek
Stable 4.2 wird am 25.09.2026 LTR. Produktentscheidung, blockiert die CI-Matrix
(`docs/konzept/07_qgis_tests_plan.md` Abschnitt 2).
4. **Test-Baseline grün machen.** Ist-Stand Mock-Modus laut `docs/status/testbaseline.json`:
sn_basis 87/87 grün, sn_plan41 43/44 (1 Fehler in `test_tab_a_ui`), sn_verfahrensgebiet 157/201
sn_basis 166/166 grün, sn_plan41 43/44 (1 Fehler in `test_tab_a_ui`), sn_verfahrensgebiet 157/201
(36 rot, 8 übersprungen; meist veraltete Tests nach dem Mehrfachauswahl-Umbau).
Probelauf am 03.09.2026 mit frischen Klonen: `sn_verfahrensgebiet` 154/201, **drei neue rote Tests** in
`test_verfahrensgebiet_alkis_komplett` durch PR #43 („Nur-laden-Option durch Stop-Abfrage ersetzt").
Entscheidung des Maintainers: Tests anpassen oder Baseline mit `--update-baseline` nachziehen.
5. **Testbeschreibungen nachziehen.** Von 332 Testfällen haben nur 100 (30 %) einen Docstring — `sn_basis`
keinen einzigen von 87, `sn_plan41` einen von 44. Ohne Beschreibung ist von außen nicht erkennbar, was ein
Test prüft. Arbeitsliste: Abschnitt „Lücken" in `docs/tests/testkatalog.md`. Pflicht für neue und geänderte
Tests gilt ab sofort (ADR 0009). Seit 15.09.2026 zusätzlich als Dashboard `docs/tests/testkatalog.html`
(lokal im Browser öffnen) – zeigt Testabdeckung und -status auf einen Blick, ohne in den Code zu schauen.
5. **Testbeschreibungen nachziehen.** `sn_basis` und `sn_verfahrensgebiet` (Docstring-Pflicht) sind in Arbeit,
`sn_plan41` weiterhin fast vollständig ohne Docstring (1 von 44). Ohne Beschreibung ist von außen nicht
erkennbar, was ein Test prüft. Arbeitsliste: Abschnitt „Lücken" in `docs/tests/testkatalog.md`. Pflicht für
neue und geänderte Tests gilt ab sofort (ADR 0009). Seit 15.09.2026 zusätzlich als Dashboard
`docs/tests/testkatalog.html` (lokal im Browser öffnen) – zeigt Testabdeckung und -status auf einen Blick,
seit 18.09.2026 auch strukturelle Lücken (Quellcode-Funktionen ohne Testbezug, Abschnitt „Ungetestete
Funktionen"), ohne in den Code zu schauen.
6. **Testlücken beim Shape-Import schließen.** Beim Erstellen von `docs/tests/verfahrensgebiet-shape-import.md`
gefunden und gegen `origin/unstable` geprüft: `_ermittle_gebietstyp_aus_fbschl` hat **keinen** Test — obwohl
PR #44/#45 genau diese Funktion tolerant gemacht hat. Ebenso ungetestet: mehrere Shapefile-Sätze in einer
@@ -63,12 +65,46 @@ spätere Phase und beginnt erst, wenn die Test-Baseline grün ist und das Projek
(fest verdrahteter NAS-Namespace `gid/6.0` in `grenzpunkt_alkis.py`, abgeflachte WFS-Feldnamen in
`sql=`-Filtern). Vergleich 6↔7 und Anpassungsliste: `docs/konzept/09_alkis7_umstellung.md`.
Nächster Schritt: Gitea-Issue in `Plugin_SN_Verfahrensgebiet`.
15. **Test-Audit durchführen (Ticket #16).** Infrastruktur ist fertig, PR steht aus: `scripts/test_audit.py`
(`status`/`markieren`) und `docs/status/testpruefungen.json` machen im Prüfstand
(`docs/tests/testkatalog.html`) je Testfall sichtbar, ob und wann er zuletzt fachlich geprüft wurde
(unabhängig vom Baseline-Status). Aktuell gelten alle 346 Testfälle als „nie geprüft" — nächster Schritt
ist der eigentliche Audit-Durchgang (Team geht die Pakete durch und markiert per `test_audit.py markieren`),
danach getrennt: Testlücken/Fachtests aus den Testkonzept-Seiten ergänzen (Ticket-Text nennt das
ausdrücklich als separaten Folgeschritt).
16. **Standarddatei-Existenzprüfung fehlt (Basis #59).** `Dateipruefer._handle_leere_eingabe()` und
`Pruefmanager._handle_basic_decision()` prüfen bei `standarddatei_vorschlagen` nie `file_exists()` —
bestätigt der Nutzer eine nicht vorhandene Standarddatei, kommt trotzdem `ok=True`/`"ok"` zurück statt
`datei_nicht_gefunden`. Bearbeitung bewusst zurückgestellt: **hängt von der Design-Entscheidung ab, ob
die Funktion „Standarddatei" überhaupt gebraucht wird** — aktuell übergibt kein Aufrufer
(`DataGrabber.py`, `excel_importer.py`) den Parameter `standarddatei`. `TestDateipruefer.test_
standarddatei_vorschlagen` bleibt bis zur Entscheidung bewusst ohne Gegenstück für den Fehlerfall
(kein Testfall-„nie geprüft", sondern Verhalten ungeklärt) — siehe Kommentar im Testfall.
17. **WMS-Zeilen im Datenabruf ungeklärt (Basis #77).** `Datenabruf._fetch_features()` liefert für
`provider == "WMS"` immer `([], None)`, der Kommentar dort verspricht „caller behandelt WMS separat" —
das tut `datenabruf()` aber nicht: jede WMS-Zeile durchläuft dieselbe relevant/ausserhalb-Klassifizierung
wie WFS/REST/OGR und landet deshalb zwangsläufig in `ausserhalb`, ohne dass je ein Layer geschrieben
wird. Offen: ist das gewollt (WMS-Kartendienste werden in diesem Pfad bewusst nicht geladen) oder fehlt
die im Kommentar versprochene Sonderbehandlung? `DatenabrufOrchestrierungTests.
test_wms_zeile_ohne_features_landet_in_ausserhalb` dokumentiert bewusst den Ist-Zustand, nicht den
bestätigt gewünschten — siehe Kommentar im Testfall.
18. **Falscher Aktionscode in `Datenschreiber.schreibe_log()` (Basis #78).** Der Erfolg wird mit
`aktion="standarddatei_vorschlagen"` gemeldet — demselben Code, den `Dateipruefer`/`Pruefmanager` für
die Standarddatei-Rückfrage nutzen (Punkt 16). Aktuell folgenlos, weil `Pruefmanager.verarbeite()` bei
`ok=True` sofort zurückkehrt, bevor die Aktion ausgewertet wird — aber missverständlich und zerbrechlich
bei künftigem Code, der auf diese Aktion filtert. `SchreibeLogTests.
test_schreibt_json_datei_mit_ergebnissen_und_gibt_pfad_zurueck` dokumentiert bewusst den Ist-Zustand —
siehe Kommentar im Testfall.
**Hinweis zum lokalen Stand (04.09.2026):** Die Klone `Plugin_SN_Verfahrensgebiet` (4 Commits: PR #43, PR #45)
und `Repository` (Release 26.8.4-unstable) liegen hinter `origin`. Aktualisieren mit `python scripts/hallo.py`.
## Zuletzt gemergt (unstable)
- 18.09.2026 Basis #63 (PR #64): Tests für die vier bis dahin testlosen Kernmodule der Datenpipeline
(`Datenabruf.py`, `Datenschreiber.py`, `LayerLoader.py`, `excel_importer.py`), 70 neue Testfälle, sn_basis
jetzt 166/166 grün. Nebenbei `_MockQgsProject` um `transformContext()`/`addMapLayer()` ergänzt.
- 18.09.2026 Verfahrensgebiet #53: Grenzpunkt-Stilanbringung, neuer Test.
- 02.09.2026 Verfahrensgebiet #45: FBSchl-Feldsuche tolerant gegenüber abweichenden Feldnamen; #43: Nur-laden-Option
bei ALKIS-Laden durch Stop-Abfrage ersetzt (Release 26.8.4-unstable).
- 02.09.2026 Verfahrensgebiet #42: Shape-Ladevorgang mit Mehrfachauswahl, ZIP-Entpacken, `FBSchl`-Prüfung.
+854
View File
@@ -0,0 +1,854 @@
{
"sn_basis.tests.test_base_dockwidget.CloseEventTests.test_fehler_in_cleanup_wird_abgefangen_und_andere_tabs_laufen_weiter": {
"datum": "2026-09-21",
"hash": "f4a963ed9caa"
},
"sn_basis.tests.test_base_dockwidget.CloseEventTests.test_ignoriert_tabs_ohne_cleanup": {
"datum": "2026-09-21",
"hash": "58feb87af614"
},
"sn_basis.tests.test_base_dockwidget.CloseEventTests.test_ohne_action_wird_nichts_zurueckgesetzt": {
"datum": "2026-09-21",
"hash": "a84f8ea5d6cb"
},
"sn_basis.tests.test_base_dockwidget.CloseEventTests.test_ruft_cleanup_aller_tabs_auf": {
"datum": "2026-09-21",
"hash": "9f131bed4c7e"
},
"sn_basis.tests.test_base_dockwidget.CloseEventTests.test_setzt_toolbar_action_zurueck": {
"datum": "2026-09-21",
"hash": "0b84bc4f86f5"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_aggregate_results_mit_zeilen_liefert_erfolgsergebnis": {
"datum": "2026-09-21",
"hash": "87e624c158da"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_aggregate_results_ohne_zeilen_liefert_keine_validen_eintraege": {
"datum": "2026-09-21",
"hash": "5cbff16ee7da"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_analyze_source_type_detects_excel": {
"datum": "2026-09-16",
"hash": "9559f9923f6f"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_analyze_source_type_detects_service_when_file_check_fails": {
"datum": "2026-09-16",
"hash": "90c4384eac7f"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_analyze_source_type_returns_unbekannte_quelle_on_full_failure": {
"datum": "2026-09-16",
"hash": "3b35bfdb3f04"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_analyze_source_type_returns_unknown_filetype": {
"datum": "2026-09-16",
"hash": "78cd94c858de"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_run_returns_failure_summary_without_rows": {
"datum": "2026-09-16",
"hash": "1369d06aff3d"
},
"sn_basis.tests.test_datagrabber.TestDataGrabber.test_run_returns_success_summary_with_valid_rows": {
"datum": "2026-09-16",
"hash": "55fcd3a0fc84"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_datei_nicht_gefunden": {
"datum": "2026-09-17",
"hash": "9e83de8df1b9"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_datei_ok": {
"datum": "2026-09-17",
"hash": "c9633046fcdd"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_erkenne_dateityp_unterscheidet_excel_datenbank_und_unbekannt": {
"datum": "2026-09-21",
"hash": "ea829ba032a0"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_ist_gueltiger_gpkg_pfad_akzeptiert_vorhandenes_verzeichnis": {
"datum": "2026-09-21",
"hash": "44ffe876b36f"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_ist_gueltiger_gpkg_pfad_lehnt_falsche_endung_ab": {
"datum": "2026-09-21",
"hash": "1adbbbcdbdd3"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_ist_gueltiger_gpkg_pfad_lehnt_fehlendes_verzeichnis_ab": {
"datum": "2026-09-21",
"hash": "b64b61e72220"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_ist_leer_erkennt_none_und_nicht_string_als_leer": {
"datum": "2026-09-21",
"hash": "93f594a311f3"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_ist_leer_erkennt_reinen_whitespace_als_leer": {
"datum": "2026-09-21",
"hash": "0141a1d2a3a8"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_ist_leer_meldet_pfad_mit_sichtbaren_zeichen_nicht_als_leer": {
"datum": "2026-09-21",
"hash": "8bffda4a5f74"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_leere_eingabe_bevorzugt_leereingabe_erlaubt_vor_standarddatei": {
"datum": "2026-09-21",
"hash": "73b3b4878b72"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_leere_eingabe_bevorzugt_standarddatei_vor_temporaer_erlaubt": {
"datum": "2026-09-21",
"hash": "a411d346c6a8"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_leereingabe_erlaubt": {
"datum": "2026-09-17",
"hash": "54c35530e1b3"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_leereingabe_nicht_erlaubt": {
"datum": "2026-09-17",
"hash": "bbb8d936f5f9"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_pfad_laesst_absoluten_pfad_unveraendert": {
"datum": "2026-09-21",
"hash": "fbe7bd8caaf0"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_pfad_verbindet_relativen_pfad_mit_basispfad": {
"datum": "2026-09-21",
"hash": "1686b4174b71"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_standarddatei_vorschlagen": {
"datum": "2026-09-21",
"hash": "2395b97dbfa4"
},
"sn_basis.tests.test_dateipruefer.TestDateipruefer.test_temporaer_erlaubt": {
"datum": "2026-09-17",
"hash": "00cfdb7d23d5"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_default_provider_haengt_generischen_bbox_parameter_an": {
"datum": "2026-09-21",
"hash": "db91265822d6"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_default_provider_ohne_raumfilter_laesst_query_unveraendert": {
"datum": "2026-09-21",
"hash": "ae305bc95c08"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_rest_provider_haengt_query_endpunkt_an": {
"datum": "2026-09-21",
"hash": "006e39a2e481"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_rest_provider_mit_bbox_baut_geometry_envelope_und_srid": {
"datum": "2026-09-21",
"hash": "419db8104fe9"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_url_praefix_wird_entfernt": {
"datum": "2026-09-21",
"hash": "0fd137b2ee4b"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_wfs_baut_standardparameter_und_bbox_mit_crs": {
"datum": "2026-09-21",
"hash": "4d8a5a034b66"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_wfs_ohne_raumfilter_setzt_keine_bbox": {
"datum": "2026-09-21",
"hash": "89bc47b753aa"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_wfs_repariert_doppeltes_fragezeichen": {
"datum": "2026-09-21",
"hash": "637f4ef9773a"
},
"sn_basis.tests.test_datenabruf.BuildProviderUrlTests.test_wms_liefert_link_unveraendert": {
"datum": "2026-09-21",
"hash": "50bcb7d3148a"
},
"sn_basis.tests.test_datenabruf.DatenabrufOrchestrierungTests.test_abbruch_ueber_progress_stoppt_weitere_zeilen": {
"datum": "2026-09-21",
"hash": "06730e19b7b9"
},
"sn_basis.tests.test_datenabruf.DatenabrufOrchestrierungTests.test_fehlende_pflichtfelder_werden_als_ungueltig_gemeldet": {
"datum": "2026-09-21",
"hash": "6a32846a1eb1"
},
"sn_basis.tests.test_datenabruf.DatenabrufOrchestrierungTests.test_wms_zeile_ohne_features_landet_in_ausserhalb": {
"datum": "2026-09-21",
"hash": "b5415522be70"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_fehlender_layer_liefert_none": {
"datum": "2026-09-21",
"hash": "2d593c0b6e97"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_pufferlayer_liefert_extent_des_pufferlayers": {
"datum": "2026-09-21",
"hash": "1daa2cd15c6a"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_pufferlayer_ohne_puffer_liefert_none": {
"datum": "2026-09-21",
"hash": "f3648846089e"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_raumfilter_ohne_liefert_none": {
"datum": "2026-09-21",
"hash": "6a388d156309"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_unbekannter_raumfilter_liefert_none": {
"datum": "2026-09-21",
"hash": "7e647695312a"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_verfahrensgebiet_liefert_extent_und_crs": {
"datum": "2026-09-21",
"hash": "cebbb9f28131"
},
"sn_basis.tests.test_datenabruf.DetermineSpatialFilterTests.test_verfahrensgebiet_ohne_extent_liefert_none": {
"datum": "2026-09-21",
"hash": "3665e39cebde"
},
"sn_basis.tests.test_datenabruf.FetchFeaturesTests.test_abbruch_vor_abruf_liefert_fehlermeldung": {
"datum": "2026-09-21",
"hash": "12b1d6288c42"
},
"sn_basis.tests.test_datenabruf.FetchFeaturesTests.test_json_featurecollection_wird_geparst": {
"datum": "2026-09-21",
"hash": "53b6b78bab8b"
},
"sn_basis.tests.test_datenabruf.FetchFeaturesTests.test_netzwerkfehler_wird_als_fehlermeldung_gemeldet": {
"datum": "2026-09-21",
"hash": "c2a85986efe8"
},
"sn_basis.tests.test_datenabruf.FetchFeaturesTests.test_wfs_xml_liefert_anzahl_aus_numberofmatched": {
"datum": "2026-09-21",
"hash": "c77af9bd078b"
},
"sn_basis.tests.test_datenabruf.FetchFeaturesTests.test_wms_liefert_keine_features_ohne_fehler": {
"datum": "2026-09-21",
"hash": "878771057e04"
},
"sn_basis.tests.test_datenschreiber.LadeLayerTests.test_gueltiger_layer_wird_dem_projekt_hinzugefuegt": {
"datum": "2026-09-21",
"hash": "9d7193dfe584"
},
"sn_basis.tests.test_datenschreiber.LadeLayerTests.test_ohne_layer_path_wird_uebersprungen": {
"datum": "2026-09-21",
"hash": "792dabdcd97c"
},
"sn_basis.tests.test_datenschreiber.LadeLayerTests.test_ungueltiger_layer_meldet_layer_nicht_gefunden": {
"datum": "2026-09-21",
"hash": "8e1f436c1225"
},
"sn_basis.tests.test_datenschreiber.ResolveStylePathTests.test_absoluter_vorhandener_stilpfad_wird_aufgeloest": {
"datum": "2026-09-21",
"hash": "6f5e91c64fff"
},
"sn_basis.tests.test_datenschreiber.ResolveStylePathTests.test_leerer_stilpfad_liefert_none": {
"datum": "2026-09-21",
"hash": "2209002c5cdd"
},
"sn_basis.tests.test_datenschreiber.ResolveStylePathTests.test_nicht_vorhandener_stilpfad_liefert_none": {
"datum": "2026-09-21",
"hash": "63fbf010d51a"
},
"sn_basis.tests.test_datenschreiber.SchreibeDatenTests.test_eintrag_ohne_gueltigen_layer_wird_uebersprungen": {
"datum": "2026-09-21",
"hash": "7209c778971b"
},
"sn_basis.tests.test_datenschreiber.SchreibeDatenTests.test_erfolgreicher_eintrag_liefert_layer_path_und_thema": {
"datum": "2026-09-21",
"hash": "5afabcf4df68"
},
"sn_basis.tests.test_datenschreiber.SchreibeDatenTests.test_ohne_speicherort_wirft_valueerror": {
"datum": "2026-09-21",
"hash": "beb0ef913056"
},
"sn_basis.tests.test_datenschreiber.SchreibeDatenTests.test_ohne_thema_wird_ident_als_layername_verwendet": {
"datum": "2026-09-21",
"hash": "f8d4cdff4193"
},
"sn_basis.tests.test_datenschreiber.SchreibeLogTests.test_ohne_speicherort_wirft_valueerror": {
"datum": "2026-09-21",
"hash": "10b44f5e3001"
},
"sn_basis.tests.test_datenschreiber.SchreibeLogTests.test_schreibt_json_datei_mit_ergebnissen_und_gibt_pfad_zurueck": {
"datum": "2026-09-21",
"hash": "0d66a75e3e47"
},
"sn_basis.tests.test_datenschreiber.StoreStyleInGpkgTests.test_fehlender_stilpfad_meldet_fehler_ueber_pruefmanager_statt_absturz": {
"datum": "2026-09-21",
"hash": "dd56f7b08b30"
},
"sn_basis.tests.test_datenschreiber.StoreStyleInGpkgTests.test_legt_tabelle_an_und_speichert_stil_als_default": {
"datum": "2026-09-21",
"hash": "aabc43a7e54d"
},
"sn_basis.tests.test_datenschreiber.StoreStyleInGpkgTests.test_liest_geometriespalte_ueber_geometrycolumn_aus": {
"datum": "2026-09-21",
"hash": "89e54e04844f"
},
"sn_basis.tests.test_datenschreiber.StoreStyleInGpkgTests.test_setzt_bisherigen_default_stil_desselben_layers_zurueck": {
"datum": "2026-09-21",
"hash": "81ce39c5698e"
},
"sn_basis.tests.test_datenschreiber.WriteLayerToGpkgTests.test_gueltiger_layer_wird_ohne_fehler_geschrieben": {
"datum": "2026-09-21",
"hash": "f71a4fc1c402"
},
"sn_basis.tests.test_datenschreiber.WriteLayerToGpkgTests.test_ungueltiger_layer_liefert_fehlermeldung": {
"datum": "2026-09-21",
"hash": "c28e1d77d4b8"
},
"sn_basis.tests.test_dockmanager.DockManagerShowTests.test_entfernt_andere_sn_docks_aber_nicht_sich_selbst": {
"datum": "2026-09-21",
"hash": "24382480ecf7"
},
"sn_basis.tests.test_dockmanager.DockManagerShowTests.test_fehler_beim_anzeigen_wird_gemeldet": {
"datum": "2026-09-21",
"hash": "d8e7624c3732"
},
"sn_basis.tests.test_dockmanager.DockManagerShowTests.test_fehler_beim_entfernen_wird_als_warnung_gemeldet": {
"datum": "2026-09-21",
"hash": "a2936c8db946"
},
"sn_basis.tests.test_dockmanager.DockManagerShowTests.test_none_dock_meldet_fehler_und_zeigt_nichts_an": {
"datum": "2026-09-21",
"hash": "c4fdeb71426d"
},
"sn_basis.tests.test_dockmanager.DockManagerShowTests.test_setzt_objectname_wenn_leer": {
"datum": "2026-09-21",
"hash": "c72a31202f92"
},
"sn_basis.tests.test_dockmanager.DockManagerShowTests.test_zeigt_neues_dock_mit_default_area_an": {
"datum": "2026-09-21",
"hash": "bd2cdb4f27a1"
},
"sn_basis.tests.test_laenderkennung.TestGetLaenderkennung.test_bekannter_praefix_gibt_kennung_zurueck": {
"datum": "2026-09-17",
"hash": "c1fe3fb105a9"
},
"sn_basis.tests.test_laenderkennung.TestGetLaenderkennung.test_mapping_enthaelt_sachsen": {
"datum": "2026-09-17",
"hash": "b4bed27578b6"
},
"sn_basis.tests.test_laenderkennung.TestGetLaenderkennung.test_ohne_argument_wird_aus_aufrufendem_modul_abgeleitet": {
"datum": "2026-09-17",
"hash": "50ea04ad4c1c"
},
"sn_basis.tests.test_laenderkennung.TestGetLaenderkennung.test_praefix_gross_klein_unabhaengig": {
"datum": "2026-09-17",
"hash": "f7a39c2d9eb0"
},
"sn_basis.tests.test_laenderkennung.TestGetLaenderkennung.test_unbekannter_praefix_wirft_keyerror": {
"datum": "2026-09-17",
"hash": "14f73fb0a974"
},
"sn_basis.tests.test_landkreise.AlleLandkreiseSentinelTests.test_alle_landkreise_sentinel_kollidiert_nicht_mit_echter_kennzahl": {
"datum": "2026-09-22",
"hash": "3904d6ac7626"
},
"sn_basis.tests.test_landkreise.LadeLandkreiseTests.test_alle_kennzahlen_kommen_genau_einmal_vor": {
"datum": "2026-09-22",
"hash": "fca83f397af2"
},
"sn_basis.tests.test_landkreise.LadeLandkreiseTests.test_fehlende_datei_liefert_leere_liste": {
"datum": "2026-09-22",
"hash": "247c9b9bc02e"
},
"sn_basis.tests.test_landkreise.LadeLandkreiseTests.test_laedt_die_echte_landkreisliste_nicht_leer": {
"datum": "2026-09-22",
"hash": "5daef82a286e"
},
"sn_basis.tests.test_landkreise.LadeLandkreiseTests.test_ungueltiges_json_liefert_leere_liste": {
"datum": "2026-09-22",
"hash": "29c14c5d2401"
},
"sn_basis.tests.test_landkreise.LandkreislisteVollstaendigkeitTests.test_landkreisliste_enthaelt_alle_13_saechsischen_gebietskoerperschaften": {
"datum": "2026-09-22",
"hash": "6081fa0fbbd1"
},
"sn_basis.tests.test_layerloader.AddToProjectTests.test_gueltiger_layer_wird_dem_projekt_hinzugefuegt": {
"datum": "2026-09-22",
"hash": "7c8ed62e7051"
},
"sn_basis.tests.test_layerloader.AddToProjectTests.test_none_layer_wird_nicht_hinzugefuegt": {
"datum": "2026-09-22",
"hash": "db489f5f485f"
},
"sn_basis.tests.test_layerloader.AddToProjectTests.test_ungueltiger_layer_wird_nicht_hinzugefuegt": {
"datum": "2026-09-22",
"hash": "1261f9711940"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_ausnahme_beim_erstellen_wird_abgefangen": {
"datum": "2026-09-22",
"hash": "12cd21f33e57"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_ogr_provider_nutzt_link_direkt": {
"datum": "2026-09-22",
"hash": "1910e68956dd"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_rest_featureserver_ergaenzt_layer_index": {
"datum": "2026-09-22",
"hash": "776f68881c59"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_rest_mit_url_praefix_wird_nicht_doppelt_gesetzt": {
"datum": "2026-09-22",
"hash": "25d8a8ad0d5a"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_rest_ohne_featureserver_endung_bleibt_unveraendert": {
"datum": "2026-09-22",
"hash": "fc7415b43710"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_unbekannter_provider_faellt_auf_ogr_zurueck": {
"datum": "2026-09-22",
"hash": "6bb5f7ab89e3"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_ungueltiger_layer_meldet_layer_nicht_verfuegbar": {
"datum": "2026-09-22",
"hash": "feabb435d115"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_wfs_baut_uri_ohne_getfeature_parameter": {
"datum": "2026-09-22",
"hash": "92bb8d53e711"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_wfs_ergaenzt_service_und_version_defaults": {
"datum": "2026-09-22",
"hash": "81ecbf43a140"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_wfs_erkennt_typename_in_grossschreibung": {
"datum": "2026-09-22",
"hash": "2898b56c4e1c"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_wfs_mit_url_praefix_wird_nicht_doppelt_gesetzt": {
"datum": "2026-09-22",
"hash": "5385bc25e511"
},
"sn_basis.tests.test_layerloader.CreateLayerTests.test_wms_baut_url_praefix_und_nutzt_rasterlayer": {
"datum": "2026-09-22",
"hash": "407c1a9ba825"
},
"sn_basis.tests.test_layerloader.FilterByExtentTests.test_abbruch_liefert_none_und_meldet_needs_user_action": {
"datum": "2026-09-22",
"hash": "839e5055e5eb"
},
"sn_basis.tests.test_layerloader.FilterByExtentTests.test_erfolgreicher_filter_liefert_neuen_layer_mit_features": {
"datum": "2026-09-22",
"hash": "d42dbc209f8d"
},
"sn_basis.tests.test_layerloader.FilterByExtentTests.test_falscher_layertyp_wird_unveraendert_zurueckgegeben": {
"datum": "2026-09-22",
"hash": "571bcc79cd34"
},
"sn_basis.tests.test_layerloader.FilterByExtentTests.test_keine_treffer_liefert_none": {
"datum": "2026-09-22",
"hash": "ead34605a071"
},
"sn_basis.tests.test_layerloader.FilterByExtentTests.test_ohne_extent_wird_layer_unveraendert_zurueckgegeben": {
"datum": "2026-09-22",
"hash": "6df5b38eda97"
},
"sn_basis.tests.test_layerloader.FilterByExtentTests.test_ungueltiger_layer_wird_unveraendert_zurueckgegeben": {
"datum": "2026-09-22",
"hash": "f98f92c748d3"
},
"sn_basis.tests.test_layerloader.FilterByLayerTests.test_erfolgreicher_geometriefilter_liefert_neuen_layer": {
"datum": "2026-09-22",
"hash": "8bc46d7cc336"
},
"sn_basis.tests.test_layerloader.FilterByLayerTests.test_filterlayer_ohne_features_liefert_none": {
"datum": "2026-09-22",
"hash": "a8963f651828"
},
"sn_basis.tests.test_layerloader.FilterByLayerTests.test_keine_ueberschneidung_liefert_none": {
"datum": "2026-09-22",
"hash": "4d033b3df276"
},
"sn_basis.tests.test_layerloader.FilterByLayerTests.test_ungueltiger_layer_oder_filterlayer_wird_unveraendert_zurueckgegeben": {
"datum": "2026-09-22",
"hash": "29d5ae987f69"
},
"sn_basis.tests.test_layerloader.TransformExtentTests.test_ausnahme_bei_der_extent_transformation_liefert_extent_unveraendert": {
"datum": "2026-09-22",
"hash": "f11e2c936436"
},
"sn_basis.tests.test_layerloader.TransformExtentTests.test_gleiche_crs_liefert_extent_unveraendert": {
"datum": "2026-09-22",
"hash": "7f5d3da32cfc"
},
"sn_basis.tests.test_layerloader.TransformExtentTests.test_unterschiedliche_crs_nutzt_coordinate_transform": {
"datum": "2026-09-22",
"hash": "1f99cdecd25a"
},
"sn_basis.tests.test_layerloader.TransformGeometryTests.test_ausnahme_bei_der_transformation_liefert_geometrie_unveraendert": {
"datum": "2026-09-22",
"hash": "6cfb8b4dbd06"
},
"sn_basis.tests.test_layerloader.TransformGeometryTests.test_fehlender_layer_liefert_geometrie_unveraendert": {
"datum": "2026-09-22",
"hash": "9577d78fb696"
},
"sn_basis.tests.test_layerloader.TransformGeometryTests.test_gleiche_crs_liefert_geometrie_unveraendert": {
"datum": "2026-09-22",
"hash": "31d753a90fb5"
},
"sn_basis.tests.test_layerloader.TransformGeometryTests.test_unterschiedliche_crs_klont_und_transformiert_geometrie": {
"datum": "2026-09-22",
"hash": "ec11affdda72"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_datenquelle_unerwartet": {
"datum": "2026-09-17",
"hash": "4b838528f751"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_falscher_geotyp": {
"datum": "2026-09-17",
"hash": "2f0c982126cc"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_falscher_layertyp": {
"datum": "2026-09-17",
"hash": "8dc755c3f3fd"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_falsches_crs": {
"datum": "2026-09-17",
"hash": "c04ff66b1456"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_felder_fehlen": {
"datum": "2026-09-17",
"hash": "eda63c51426f"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_layer_exists": {
"datum": "2026-09-17",
"hash": "22e7012bf002"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_layer_leer": {
"datum": "2026-09-17",
"hash": "5482bd99468b"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_layer_nicht_editierbar": {
"datum": "2026-09-17",
"hash": "c9b5c18995a0"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_layer_ok": {
"datum": "2026-09-17",
"hash": "190f2397704f"
},
"sn_basis.tests.test_layerpruefer.TestLayerpruefer.test_layer_unsichtbar": {
"datum": "2026-09-17",
"hash": "37ebf360bee0"
},
"sn_basis.tests.test_linkpruefer.TestLinkpruefer.test_leere_eingabe": {
"datum": "2026-09-17",
"hash": "0eb3f4c67feb"
},
"sn_basis.tests.test_linkpruefer.TestLinkpruefer.test_local_link_not_found": {
"datum": "2026-09-17",
"hash": "16d912fcf3bc"
},
"sn_basis.tests.test_linkpruefer.TestLinkpruefer.test_local_link_ok": {
"datum": "2026-09-17",
"hash": "971bd5136933"
},
"sn_basis.tests.test_linkpruefer.TestLinkpruefer.test_remote_link_error": {
"datum": "2026-09-17",
"hash": "dd1c05ef7b8c"
},
"sn_basis.tests.test_linkpruefer.TestLinkpruefer.test_remote_link_network_error": {
"datum": "2026-09-17",
"hash": "74910c1b9cd9"
},
"sn_basis.tests.test_linkpruefer.TestLinkpruefer.test_remote_link_ok": {
"datum": "2026-09-17",
"hash": "67e86095b7ad"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerExistsTests.test_none_existiert_nicht": {
"datum": "2026-09-23",
"hash": "46610a354f52"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerExistsTests.test_ungueltiger_layer_existiert_trotzdem": {
"datum": "2026-09-23",
"hash": "2723abe4b70a"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerExistsTests.test_vorhandenes_objekt_existiert": {
"datum": "2026-09-23",
"hash": "36946f5654b9"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsUsableTests.test_gueltiger_layer_ist_nutzbar": {
"datum": "2026-09-23",
"hash": "d023884b88ed"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsUsableTests.test_none_ist_nicht_nutzbar": {
"datum": "2026-09-23",
"hash": "3c3dfa47e10c"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsUsableTests.test_ungueltiger_layer_ist_nicht_nutzbar": {
"datum": "2026-09-23",
"hash": "371afd48088a"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsValidTests.test_ausnahme_bei_isvalid_liefert_false": {
"datum": "2026-09-23",
"hash": "cdea9c039230"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsValidTests.test_gueltiger_layer_liefert_true": {
"datum": "2026-09-23",
"hash": "b59492eacf41"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsValidTests.test_none_ist_nicht_gueltig": {
"datum": "2026-09-23",
"hash": "fe9e75b0dd64"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsValidTests.test_objekt_ohne_isvalid_ist_nicht_gueltig": {
"datum": "2026-09-23",
"hash": "a9ac192cbaea"
},
"sn_basis.tests.test_ly_existence_wrapper.LayerIsValidTests.test_ungueltiger_layer_liefert_false": {
"datum": "2026-09-23",
"hash": "8c70103cdd59"
},
"sn_basis.tests.test_ly_style_wrapper.ApplyStyleFromPathTests.test_ausnahme_beim_laden_wird_abgefangen": {
"datum": "2026-09-23",
"hash": "9f7b05ef45e3"
},
"sn_basis.tests.test_ly_style_wrapper.ApplyStyleFromPathTests.test_erfolgreiches_laden_liefert_true_und_triggert_repaint": {
"datum": "2026-09-23",
"hash": "bd86c7e8e769"
},
"sn_basis.tests.test_ly_style_wrapper.ApplyStyleFromPathTests.test_fehlender_layer_liefert_false": {
"datum": "2026-09-23",
"hash": "3ff023bcd0f7"
},
"sn_basis.tests.test_ly_style_wrapper.ApplyStyleFromPathTests.test_fehlgeschlagenes_laden_liefert_false_ohne_repaint": {
"datum": "2026-09-23",
"hash": "c1f4a7972411"
},
"sn_basis.tests.test_ly_style_wrapper.ApplyStyleFromPathTests.test_pfad_wird_als_text_an_loadnamedstyle_uebergeben": {
"datum": "2026-09-23",
"hash": "83fd0b51b9df"
},
"sn_basis.tests.test_message_wrapper.GetMessageBarTests.test_faellt_bei_fehlerhaftem_iface_auf_mock_bar_zurueck": {
"datum": "2026-09-23",
"hash": "dc6c3cdbfae1"
},
"sn_basis.tests.test_message_wrapper.GetMessageBarTests.test_liefert_objekt_mit_pushmessage_methode": {
"datum": "2026-09-23",
"hash": "e9d99a0b1f02"
},
"sn_basis.tests.test_message_wrapper.MeldungsstufenTests.test_info_warning_error_success_setzen_die_passende_meldungsstufe": {
"datum": "2026-09-23",
"hash": "04f2f5ea94e9"
},
"sn_basis.tests.test_message_wrapper.PushMessageTests.test_fehler_in_der_message_bar_wird_abgefangen": {
"datum": "2026-09-23",
"hash": "a8fdcf8ba3b8"
},
"sn_basis.tests.test_message_wrapper.PushMessageTests.test_liefert_dict_mit_titel_text_level_und_dauer": {
"datum": "2026-09-23",
"hash": "525b2fdab218"
},
"sn_basis.tests.test_navigation.AddActionTests.test_erzeugt_action_und_fuegt_sie_gruppe_und_liste_hinzu": {
"datum": "2026-09-29",
"hash": "7aca9de43703"
},
"sn_basis.tests.test_navigation.AddActionTests.test_ohne_plugin_group_liefert_none": {
"datum": "2026-09-29",
"hash": "518094424549"
},
"sn_basis.tests.test_navigation.AddActionTests.test_ohne_priority_wird_standardprioritaet_100_verwendet": {
"datum": "2026-09-29",
"hash": "49ed3079911d"
},
"sn_basis.tests.test_navigation.FinalizeMenuAndToolbarTests.test_gleiche_prioritaet_behaelt_registrierungsreihenfolge": {
"datum": "2026-09-29",
"hash": "7fe5c4b93d7b"
},
"sn_basis.tests.test_navigation.FinalizeMenuAndToolbarTests.test_nur_menu_ohne_toolbar_laesst_menu_unveraendert": {
"datum": "2026-09-29",
"hash": "93cc53d73004"
},
"sn_basis.tests.test_navigation.FinalizeMenuAndToolbarTests.test_ohne_menu_oder_toolbar_tut_nichts": {
"datum": "2026-09-29",
"hash": "f8a271e5aedf"
},
"sn_basis.tests.test_navigation.FinalizeMenuAndToolbarTests.test_sortiert_und_fuegt_aktionen_in_prioritaetsreihenfolge_hinzu": {
"datum": "2026-09-29",
"hash": "452ef0a0228b"
},
"sn_basis.tests.test_navigation.RemoveActionTests.test_entfernt_action_aus_menu_toolbar_und_liste": {
"datum": "2026-09-29",
"hash": "3c2aea3516cf"
},
"sn_basis.tests.test_navigation.RemoveActionTests.test_none_action_tut_nichts": {
"datum": "2026-09-29",
"hash": "5f69f2236ec0"
},
"sn_basis.tests.test_navigation.RemoveActionTests.test_ohne_menu_oder_toolbar_entfernt_trotzdem_aus_liste": {
"datum": "2026-09-29",
"hash": "57387489da73"
},
"sn_basis.tests.test_navigation.RemoveAllTests.test_entfernt_menu_toolbar_und_setzt_zustand_zurueck": {
"datum": "2026-09-29",
"hash": "3d4ea907aabe"
},
"sn_basis.tests.test_navigation.RemoveAllTests.test_loescht_menu_und_toolbar_objekt_damit_kein_duplikat_uebrig_bleibt": {
"datum": "2026-09-29",
"hash": "f6b94d873e0a"
},
"sn_basis.tests.test_navigation.RemoveAllTests.test_ohne_menu_und_toolbar_ruft_wrapper_nicht_auf": {
"datum": "2026-09-29",
"hash": "7219c12b1516"
},
"sn_basis.tests.test_navigation.SetActivePluginTests.test_ohne_aktive_action_setzt_alle_auf_false": {
"datum": "2026-09-29",
"hash": "e7234529c346"
},
"sn_basis.tests.test_navigation.SetActivePluginTests.test_setzt_nur_aktive_action_auf_checked": {
"datum": "2026-09-29",
"hash": "3e9505367374"
},
"sn_basis.tests.test_os_wrapper.IsCaseSensitiveFsTests.test_linux_ist_case_sensitiv": {
"datum": "2026-09-29",
"hash": "6dd37c5bcfb1"
},
"sn_basis.tests.test_os_wrapper.IsCaseSensitiveFsTests.test_macos_ist_nicht_case_sensitiv": {
"datum": "2026-09-29",
"hash": "953b361870c7"
},
"sn_basis.tests.test_os_wrapper.IsCaseSensitiveFsTests.test_windows_ist_nicht_case_sensitiv": {
"datum": "2026-09-29",
"hash": "a6ff8a4c0b1e"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_configure_hauptkarte_grids_uses_wrapper_constants": {
"datum": "2026-09-29",
"hash": "785b2482b97b"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_configure_legend_size_uses_content_adaptive_size": {
"datum": "2026-09-29",
"hash": "630779aecd82"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_create_legend_item_does_not_copy_project_layers_into_legend_model": {
"datum": "2026-09-29",
"hash": "c2dcdadb3111"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_create_legend_item_links_map_and_enables_map_filtering": {
"datum": "2026-09-29",
"hash": "50f14d31bbb0"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_create_single_page_layout_bricht_bei_vorhandenem_layoutnamen_ab": {
"datum": "2026-09-29",
"hash": "ff67ab4dce47"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_create_single_page_layout_keeps_legend_linked_to_hauptkarte_and_theme": {
"datum": "2026-09-29",
"hash": "cc7f60d92735"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_create_single_page_layout_ohne_thema_oder_aktuell_folgt_keinem_kartenthema": {
"datum": "2026-09-29",
"hash": "da3c5570378b"
},
"sn_basis.tests.test_print_layout.TestPrintLayout.test_create_single_page_layout_setzt_nur_gueltigen_plotmassstab": {
"datum": "2026-09-29",
"hash": "1fdaf9400c3d"
},
"sn_basis.tests.test_pruef_ergebnis.DateipfadTests.test_liefert_none_wenn_kontext_kein_path_ist": {
"datum": "2026-09-29",
"hash": "ce0674d82678"
},
"sn_basis.tests.test_pruef_ergebnis.DateipfadTests.test_liefert_path_wenn_kontext_ein_path_ist": {
"datum": "2026-09-29",
"hash": "1325b88bdf57"
},
"sn_basis.tests.test_pruef_ergebnis.ErlaubtePersistierungTests.test_liefert_false_fuer_ueberspringen_und_reines_existieren": {
"datum": "2026-09-29",
"hash": "6ed3d1c5fb87"
},
"sn_basis.tests.test_pruef_ergebnis.ErlaubtePersistierungTests.test_liefert_true_fuer_erzeugen_ueberschreiben_anhaengen": {
"datum": "2026-09-29",
"hash": "876608170849"
},
"sn_basis.tests.test_pruef_ergebnis.IstVerfahrensDbAktionTests.test_liefert_false_fuer_andere_aktionen": {
"datum": "2026-09-29",
"hash": "99b113c8c52d"
},
"sn_basis.tests.test_pruef_ergebnis.IstVerfahrensDbAktionTests.test_liefert_true_fuer_alle_verfahrens_db_aktionen": {
"datum": "2026-09-29",
"hash": "e247283a0cdf"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_datei_existiert_append": {
"datum": "2026-09-17",
"hash": "25e169c45423"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_datei_existiert_cancel": {
"datum": "2026-09-17",
"hash": "0b7718a5f16f"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_datei_existiert_in_non_qgis_mode_returns_input": {
"datum": "2026-09-17",
"hash": "ccff88a11189"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_datei_existiert_overwrite": {
"datum": "2026-09-17",
"hash": "b1a1017d7970"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_layer_unsichtbar_ja": {
"datum": "2026-09-17",
"hash": "c6090b772bf0"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_layer_unsichtbar_nein": {
"datum": "2026-09-17",
"hash": "c865caa3e4c6"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_leereingabe_erlaubt_ja": {
"datum": "2026-09-17",
"hash": "b7097a4b3c4c"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_leereingabe_erlaubt_nein": {
"datum": "2026-09-17",
"hash": "1b792a3d7a51"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_ok": {
"datum": "2026-09-17",
"hash": "7183f163899a"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_standarddatei_vorschlagen_ja": {
"datum": "2026-09-17",
"hash": "7fe9491d4249"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_standarddatei_vorschlagen_nein": {
"datum": "2026-09-17",
"hash": "f2196cc4a990"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_temporaer_erlaubt_ja": {
"datum": "2026-09-17",
"hash": "9665a3c380d0"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_temporaer_erlaubt_nein": {
"datum": "2026-09-17",
"hash": "3a24d85db9f5"
},
"sn_basis.tests.test_pruefmanager.TestPruefmanager.test_unbekannte_aktion": {
"datum": "2026-09-17",
"hash": "e369c5874b64"
},
"sn_basis.tests.test_settings_logic.TestSettingsLogic.test_load": {
"datum": "2026-09-17",
"hash": "f14c558bc597"
},
"sn_basis.tests.test_settings_logic.TestSettingsLogic.test_load_handles_partial_values_with_empty_fallback": {
"datum": "2026-09-17",
"hash": "7dced8a9ea7f"
},
"sn_basis.tests.test_settings_logic.TestSettingsLogic.test_save": {
"datum": "2026-09-17",
"hash": "f214c8244ae7"
},
"sn_basis.tests.test_settings_logic.TestSettingsLogic.test_save_empty_string_value_is_persisted": {
"datum": "2026-09-17",
"hash": "96f217d347c2"
},
"sn_basis.tests.test_stilpruefer.TestStilpruefer.test_datei_existiert_falsche_endung": {
"datum": "2026-09-17",
"hash": "37de9cd7e14c"
},
"sn_basis.tests.test_stilpruefer.TestStilpruefer.test_datei_existiert_mit_qml": {
"datum": "2026-09-17",
"hash": "1faf026ed344"
},
"sn_basis.tests.test_stilpruefer.TestStilpruefer.test_datei_existiert_nicht": {
"datum": "2026-09-17",
"hash": "2919c732c211"
},
"sn_basis.tests.test_stilpruefer.TestStilpruefer.test_keine_datei_angegeben": {
"datum": "2026-09-17",
"hash": "7a3d7bd800c3"
},
"sn_basis.tests.test_sys_wrapper.TestGetPluginRoot.test_get_plugin_root_folgt_keiner_verzeichnisverknuepfung": {
"datum": "2026-09-17",
"hash": "c9b434520d62"
}
}
File diff suppressed because one or more lines are too long
+1185 -547
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -8,6 +8,32 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
- **QGIS 3.40 ist seit Januar 2026 End of Life.** Aktueller LTR ist 3.44, Stable 4.2 (wird 25.09.2026 LTR). `workspace.toml` und alle `metadata.template` stehen noch auf `3.40` – offene Produktentscheidung, siehe `docs/konzept/07_qgis_tests_plan.md` Abschnitt 2. (04.09.2026, QGIS-Release-Fahrplan)
- **Zwei Qt-Welten.** QGIS 3.40/3.44 laufen mit Qt5/PyQt5, QGIS 4 mit Qt6/PyQt6. `qt_wrapper` lädt PyQt6 zuerst, dann PyQt5, sonst Mock. Enum-Zugriffe unterscheiden sich (`Qt.AlignmentFlag.AlignTop` statt `Qt.AlignTop`), deshalb nur Wrapper-Aliase verwenden. (09/2026, Code)
- **Mock-Modus ist kein QGIS.** `python scripts/run_tests.py` läuft in Sekunden, prüft aber nur Logik gegen Mock-Klassen. Geometrieoperationen, WFS, Layouts und Signale müssen in QGIS geprüft werden (`tests/test_qgis.bat` unter Windows). Manuelle QGIS-Tests im PR dokumentieren. (09/2026)
- **`sqlite3.connect()` in einem `with`-Block schließt die Verbindung nicht.** Das `with`-Statement committet
bzw. rollt nur zurück, `conn` bleibt offen (siehe `Datenschreiber._store_style_in_gpkg`). Unter Windows
sperrt das die Datei, bis die Verbindung per Garbage Collection oder explizitem `close()` freigegeben wird –
ein Test, der danach dasselbe `tempfile.TemporaryDirectory()` verlassen will, bricht mit
`PermissionError: [WinError 32]` ab. Betroffene Produktionsfunktion nicht nebenbei umschreiben (kein
Refactoring im Testticket); stattdessen im Test `tempfile.mkdtemp()` + `try/finally` mit `gc.collect()`
vor `shutil.rmtree(..., ignore_errors=True)` verwenden. (21.09.2026, Issue Basis #70)
- **`_MockQgsProject` war unvollständig:** `transformContext()` und `addMapLayer()` fehlten, wodurch
`Datenschreiber._write_layer_to_gpkg` und `LayerLoader.add_to_project` im Mock-Modus für **jeden** gültigen
Layer eine Ausnahme geworfen bzw. eine Fehlermeldung erzeugt hätten – nicht durch einen Fehler in diesen
Funktionen, sondern durch den unvollständigen Mock. Beim Schreiben neuer Tests für einen dieser Pfade prüfen,
ob `functions/qgiscore_wrapper.py::_MockQgsProject` die benötigte Methode schon hat, statt lange nach dem
„Bug" in der eigentlichen Funktion zu suchen. Ergänzt in Issue Basis #63 (18.09.2026).
- **`_MockQDockWidget` hatte kein `closeEvent()`:** `BaseDockWidget.closeEvent()` ruft am Ende
`super().closeEvent(event)` auf – im Mock-Modus stürzte das für **jede** reale `BaseDockWidget`-Instanz mit
`AttributeError` ab. Ergänzt in Issue Basis #65 (18.09.2026); gleiche Kategorie Fund wie oben – erst den
Mock in `qt_wrapper.py`/`qgiscore_wrapper.py` auf Vollständigkeit prüfen, bevor die eigentliche Funktion in
Verdacht gerät. Bei UI-Klassen mit `__init__`, die selbst nicht QGIS-frei konstruierbar sind (fehlende
`setLayout()`/`QGroupBox` im Mock, z. B. `SettingsTab`), Instanz stattdessen über `Klasse.__new__(Klasse)`
ohne `__init__` erzeugen und nur die für den Test nötigen Attribute von Hand setzen (siehe
`tests/test_base_dockwidget.py`, `tests/test_settings_tab.py`).
- **Zwei Testkatalog-Fehlalarme durch die „Ungetestete Funktionen"-Heuristik entdeckt und real bestätigt (Issue
Basis #60):** `Pruefmanager._ask_use_or_replace_pufferlayer()` referenziert `self.pruefmanager`, das auf
`Pruefmanager`-Instanzen nie existiert (`AttributeError` bei jedem Aufruf) – toter Code, im ganzen Workspace
ohne Aufrufer. Vor einem Test für eine als „testen" markierte Funktion kurz prüfen, ob sie überhaupt
syntaktisch fehlerfrei aufrufbar ist.
- **`openpyxl` ist eine harte Abhängigkeit** von `sn_basis` (Excel-Import) und unter Linux-QGIS oft nicht installiert (Issue Verfahrensgebiet #21). `requests` ist nur optionaler Fallback. Es gibt keine `requirements.txt`. (09/2026)
- **QGIS lädt Plugins in nicht garantierter Reihenfolge.** Fachplugins dürfen in `initGui()` nicht davon ausgehen, dass `sn_basis` fertig ist; `sn_basis` ruft `initGui()` aller `sn_*` nach. (Code `main.py`)
- **Exit-Crash unter QGIS 3.40** entstand durch nicht getrennte Projektsignale beim Unload. Regel: jeder Tab/Manager, der Signale verbindet, bietet `cleanup()`/`teardown_...()` und wird beim Schließen aufgerufen. (Basis, `assets/plan-qgis340ExitCrashFix.prompt.md`)
@@ -15,6 +41,21 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
Internetverbindung beim Öffnen bleiben die Baumdiagramme leer, Text/Tabellen/Filter funktionieren trotzdem
(kein Server nötig, einfach lokal im Browser öffnen). Erzeugt zusammen mit `testkatalog.md` durch
`python scripts/testkatalog.py`; beide Dateien müssen zueinander passen (`--pruefen` prüft beide). (09/2026)
- **Test-Audit (`scripts/test_audit.py`, `docs/status/testpruefungen.json`) reagiert auf jede inhaltliche
Änderung am Testkörper, aber nicht auf reines Trailing Whitespace.** Der Hash je Testfall wird über den
`rstrip()`-normalisierten Quelltext gebildet. Eine größere Reformatierung (Docstring-Text, Einrückung,
Assertions) macht bestehende Prüfungen also bewusst „veraltet" – das ist gewollt, sollte aber vor einer
größeren Reformatierungs-PR angekündigt werden, damit niemand über plötzlich viele „veraltet"-Einträge
stolpert. (09/2026, Ticket #16)
- **`scripts/test_audit.py` importiert `scripts/testkatalog.py` als Modul** (`import testkatalog`). Das
funktioniert nur, wenn es als `python scripts/test_audit.py …` aufgerufen wird – Python setzt dabei
automatisch das Verzeichnis des Skripts als ersten Sucheintrag. Ein Aufruf aus einem anderen Arbeitsverzeichnis
über einen vollen Pfad funktioniert genauso; ein Import von `test_audit` als Paket ohne diesen Mechanismus
(z. B. `python -c "import test_audit"` von anderswo) schlägt fehl. (09/2026)
- **`docs/status/testpruefungen.json` wird nur von `scripts/test_audit.py markieren` geschrieben.**
`scripts/testkatalog.py` liest sie ausschließlich (wie bei `testbaseline.json`); die Datei fehlt zunächst und
entsteht erst mit dem ersten `markieren`-Aufruf – der Prüfstand zeigt bis dahin alle Testfälle als „nie
geprüft". (09/2026)
- **`.vscode/` gehört nicht ins Repository.** Im Projektwissen wurde er am 04.09.2026 entfernt; brauchbare Einstellungen stehen jetzt in `docs/leitfaden/04_werkzeuge_einrichten.md` Abschnitt 4.2 zum Kopieren in die Benutzereinstellungen. In `Plugin_SN_Basis`, `Plugin_SN_Plan41`, `Plugin_SN_Verfahrensgebiet` und `Plugin_SN_Widmung` liegt er noch und enthält Pfade eines einzelnen Arbeitsplatzes (OSGeo-Installationspfad, persönliches Benutzerprofil) – nicht als Vorgabe verstehen. (09/2026)
## QGIS-Tests (geplant, siehe `docs/konzept/07_qgis_tests_plan.md`)
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Test-Audit: zeigt und pflegt, wann ein Testfall zuletzt fachlich geprüft wurde.
Der Testkatalog (scripts/testkatalog.py) beantwortet "was prüft die Suite" und "läuft der Test
durch" (Baseline). Er beantwortet NICHT "hat das ein Mensch fachlich bestätigt" – ein Agent kann
einen überzeugenden, aber fachlich falschen Test schreiben, und weder Docstring noch Baseline
zeigen das. Dieses Skript schließt genau diese Lücke: es liest/schreibt
docs/status/testpruefungen.json (Datum + Kurzprüfsumme der Testfunktion je Testfall) und macht so
sichtbar, welche Tests nie geprüft wurden oder sich seit der letzten Prüfung geändert haben.
Reine Standardbibliothek, kein Import von sn_basis/QGIS nötig, da nur mit ast geparst wird
(genau wie testkatalog.py, dessen Funktionen hier wiederverwendet werden).
`status` zeigt je Treffer "<datei>:<zeile>" – in den meisten Terminals/Editoren (z. B. VS Code) anklickbar,
um den Test direkt zum Bearbeiten zu öffnen. `--code` gibt zusätzlich den Testquelltext im Terminal aus, zum
schnellen fachlichen Lesen ohne Editor. Bearbeitet wird die Testdatei ganz normal über die Konsole/den Editor –
dieses Skript schreibt nur `docs/status/testpruefungen.json`, nie Testdateien.
Aufrufe (aus dem Workspace-Root):
python scripts/test_audit.py status # alle Pakete
python scripts/test_audit.py status sn_verfahrensgebiet # nur ein Paket
python scripts/test_audit.py status sn_basis --nur ungeprueft # Filter auf Prüfstatus
python scripts/test_audit.py status --muster grenzpunkt # Filter auf Bezeichner
python scripts/test_audit.py status sn_basis --muster Test_x --code # mit Quelltext (zum Lesen, nicht editierbar)
python scripts/test_audit.py markieren sn_basis TestHallo.test_x # als heute geprüft markieren
python scripts/test_audit.py markieren sn_basis "TestX.*" --trocken # nur anzeigen, nichts schreiben
python scripts/test_audit.py markieren sn_basis TestX.test_x --datum 2026-08-01 # Nacherfassung
Exit-Code: 0 = in Ordnung, 1 = kein Treffer beim Markieren, 2 = falsche Angabe (unbekanntes Paket,
ungültiges Datum).
"""
from __future__ import annotations
import argparse
import datetime
import fnmatch
import json
import sys
from pathlib import Path
import testkatalog as tk
ROOT = tk.ROOT
def lade_pakete(manifest: dict) -> dict[str, dict]:
return {r["paket"]: r for r in manifest["repos"] if r.get("paket")}
def waehle_pakete(gewuenscht: list[str], repos: dict[str, dict]) -> list[str] | None:
"""Löst die gewünschten Paketnamen auf; leer bedeutet alle mit tests=true. None bei unbekanntem Namen."""
testbare = [p for p, r in repos.items() if r.get("tests")]
ziel = gewuenscht or testbare
unbekannt = [p for p in ziel if p not in repos]
if unbekannt:
print(f"Unbekannte Pakete: {unbekannt}. Bekannt: {sorted(repos)}")
return None
return ziel
def sammle(paket: str, repos: dict[str, dict]) -> tk.Paketkatalog | None:
repo = repos[paket]
if not repo.get("tests"):
print(f"[{paket}] laut workspace.toml ohne Tests – übersprungen")
return None
katalog = tk.sammle_paket(paket, repo)
if katalog is None:
print(f"[{paket}] kein Ordner {repo['name']}/tests – übersprungen")
return katalog
NUR_ZU_PRUEFSTATUS = {
"ungeprueft": "nie geprüft",
"veraltet": "veraltet",
"aktuell": "aktuell geprüft",
}
def befehl_status(args: argparse.Namespace) -> int:
manifest = tk.lade_manifest()
repos = lade_pakete(manifest)
pakete = waehle_pakete(args.pakete, repos)
if pakete is None:
return 2
rot_je_paket, _ = tk.lies_baseline(args.baseline_datei)
pruefungen, _ = tk.lies_pruefungen(args.pruefungsdatei)
gesucht_status = NUR_ZU_PRUEFSTATUS.get(args.nur) if args.nur else None
for paket in pakete:
katalog = sammle(paket, repos)
if katalog is None:
continue
tk.setze_status(katalog, rot_je_paket.get(paket, set()))
tk.setze_pruefstatus(katalog, pruefungen)
k = tk.zaehle(katalog.faelle)
print(
f"[{paket}] {k['gesamt']} Testfälle – {k['aktuell_geprueft']} aktuell geprüft, "
f"{k['veraltet_geprueft']} veraltet, {k['nie_geprueft']} nie geprüft"
)
for datei in katalog.dateien:
for fall in datei.faelle:
if args.muster and args.muster.lower() not in fall.bezeichnung.lower():
continue
if gesucht_status and fall.pruefstatus != gesucht_status:
continue
print(
f" {datei.anzeigepfad}:{fall.zeile} {fall.bezeichnung} -> {tk.pruef_text(fall)}"
)
if args.code:
for zeile in fall.quelltext.splitlines():
print(f" {zeile}")
return 0
def schreibe_pruefungen(pfad: Path, pruefungen: dict) -> None:
"""Schreibt die Prüf-Historie deterministisch (sortierte Schlüssel, feste Formatierung)."""
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_text(
json.dumps(pruefungen, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
encoding="utf-8",
newline="\n",
)
def befehl_markieren(args: argparse.Namespace) -> int:
try:
datum = datetime.date.fromisoformat(args.datum).isoformat()
except ValueError:
print(f"Ungültiges Datum: {args.datum} (erwartet JJJJ-MM-TT)")
return 2
manifest = tk.lade_manifest()
repos = lade_pakete(manifest)
if args.paket not in repos:
print(f"Unbekanntes Paket: {args.paket}. Bekannt: {sorted(repos)}")
return 2
katalog = sammle(args.paket, repos)
if katalog is None:
return 2
treffer: list[tuple[tk.Testdatei, tk.Testfall]] = []
gesehen: set[str] = set()
for datei in katalog.dateien:
for fall in datei.faelle:
voll = tk.voller_testpfad(datei, fall)
if voll in gesehen:
continue
if any(fnmatch.fnmatchcase(fall.bezeichnung, muster) for muster in args.bezeichner):
treffer.append((datei, fall))
gesehen.add(voll)
if not treffer:
print(f"Kein Testfall in {args.paket} passt zu {args.bezeichner}.")
return 1
pruefungen, _ = tk.lies_pruefungen(args.pruefungsdatei)
tk.setze_pruefstatus(katalog, pruefungen)
for datei, fall in treffer:
print(f"{datei.anzeigepfad}:{fall.zeile} {fall.bezeichnung} (vorher: {tk.pruef_text(fall)})")
if args.trocken:
print(f"Trockenlauf – nichts geschrieben. {len(treffer)} Treffer.")
return 0
for datei, fall in treffer:
pruefungen[tk.voller_testpfad(datei, fall)] = {"datum": datum, "hash": fall.quellhash}
schreibe_pruefungen(args.pruefungsdatei, pruefungen)
print(f"{len(treffer)} Testfälle als geprüft am {datum} markiert: {args.pruefungsdatei}")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
unter = ap.add_subparsers(dest="befehl", required=True)
p_status = unter.add_parser("status", help="Prüfstatus je Testfall anzeigen")
p_status.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle mit tests=true)")
p_status.add_argument("--muster", help="nur Testfälle, deren Bezeichner diesen Text enthält (Teilstring)")
p_status.add_argument("--nur", choices=sorted(NUR_ZU_PRUEFSTATUS), help="nur diesen Prüfstatus anzeigen")
p_status.add_argument("--code", action="store_true", help="Quelltext jeder passenden Testmethode mit ausgeben")
p_status.add_argument("--baseline-datei", type=Path, default=tk.BASELINE_DEFAULT)
p_status.add_argument("--pruefungsdatei", type=Path, default=tk.PRUEFUNGEN_DEFAULT)
p_status.set_defaults(func=befehl_status)
p_markieren = unter.add_parser("markieren", help="Testfälle als heute geprüft markieren")
p_markieren.add_argument("paket", help="Paketname, z. B. sn_basis")
p_markieren.add_argument(
"bezeichner", nargs="+", help="Klasse.methode, exakt oder als fnmatch-Muster (z. B. 'TestX.*')"
)
p_markieren.add_argument("--datum", default=datetime.date.today().isoformat(), help="Standard: heute")
p_markieren.add_argument("--trocken", action="store_true", help="nur anzeigen, nichts schreiben")
p_markieren.add_argument("--pruefungsdatei", type=Path, default=tk.PRUEFUNGEN_DEFAULT)
p_markieren.set_defaults(func=befehl_markieren)
args = ap.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
+430 -20
View File
@@ -16,6 +16,11 @@ dieses Skript mit jedem Python >= 3.11 ohne jede Abhängigkeit, auch ohne QGIS.
Der Status je Testfall stammt aus docs/status/testbaseline.json; dort stehen
unter <paket>.rot Einträge der Form "<methode> (<modulpfad>.<Klasse>.<methode>)".
Eine dritte Datenquelle ist docs/status/testpruefungen.json: sie hält je Testfall fest, wann er zuletzt von
einem Menschen fachlich geprüft wurde (Datum plus Kurzprüfsumme der Testfunktion, damit eine Änderung am
Testcode die Prüfung automatisch als veraltet kennzeichnet). Diese Datei wird hier nur gelesen; geschrieben
wird sie von scripts/test_audit.py.
Determinismus: Die erzeugte Datei wird versioniert, darf also bei unverändertem
Eingang keine Diffs erzeugen. Deshalb steht im Kopf weder Uhrzeit noch
Erzeugungsdatum – auch ein reines Tagesdatum würde die Datei bei jedem Lauf an
@@ -60,6 +65,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
BASELINE_DEFAULT = ROOT / "docs" / "status" / "testbaseline.json"
PRUEFUNGEN_DEFAULT = ROOT / "docs" / "status" / "testpruefungen.json"
AUSGABE_DEFAULT = ROOT / "docs" / "tests" / "testkatalog.md"
HTML_AUSGABE_DEFAULT = ROOT / "docs" / "tests" / "testkatalog.html"
@@ -87,6 +93,11 @@ class Testfall:
grund: str = ""
erwarteter_fehlschlag: bool = False
status: str = "grün"
quellhash: str = ""
pruefstatus: str = "nie geprüft"
pruefdatum: str = ""
zeile: int = 0
quelltext: str = ""
@property
def bezeichnung(self) -> str:
@@ -117,6 +128,17 @@ class Testdatei:
return [f for g in self.gruppen for f in g.faelle]
@dataclass
class Quellfunktion:
"""Eine Top-Level-Funktion oder Klassenmethode im Quellcode (kein Test)."""
datei: str
name: str
zeile: int
referenziert: bool = False
absichtlich_grund: str = ""
@dataclass
class Paketkatalog:
"""Alle Testdateien eines Pakets samt Kennzahlen."""
@@ -125,6 +147,7 @@ class Paketkatalog:
ordner: str
rolle: str
dateien: list[Testdatei] = field(default_factory=list)
quellfunktionen: list[Quellfunktion] = field(default_factory=list)
@property
def faelle(self) -> list[Testfall]:
@@ -188,6 +211,22 @@ def lies_skip(dekoratoren: list[ast.expr]) -> tuple[bool, str, bool]:
return uebersprungen, grund, erwartet
def _funktionsquelle(zeilen: list[str], knoten: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
"""Quelltext einer Testmethode inkl. eigener Dekoratoren, Trailing Whitespace entfernt.
Trailing Whitespace wird bewusst ignoriert, damit reine Whitespace-Nacharbeit keine Prüfung
veraltet macht. Einrückung und sonstiger Inhalt bleiben signifikant.
"""
start = min([knoten.lineno] + [d.lineno for d in knoten.decorator_list])
ende = knoten.end_lineno or knoten.lineno
return "\n".join(zeile.rstrip() for zeile in zeilen[start - 1:ende])
def _funktionshash(zeilen: list[str], knoten: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
"""SHA-256-Kurzprüfsumme (12 Zeichen) einer Testmethode, für den Abgleich in testpruefungen.json."""
return hashlib.sha256(_funktionsquelle(zeilen, knoten).encode("utf-8")).hexdigest()[:12]
def ist_testcase(klasse: ast.ClassDef) -> bool:
"""True für Klassen, die als unittest-TestCase gelten.
@@ -209,6 +248,7 @@ def ist_testcase(klasse: ast.ClassDef) -> bool:
def lies_testdatei(pfad: Path, paket: str) -> Testdatei:
"""Parst eine Testdatei per ast (ohne sie zu importieren)."""
quelle = pfad.read_text(encoding="utf-8")
zeilen = quelle.splitlines()
baum = ast.parse(quelle, filename=str(pfad))
datei = Testdatei(
pfad=pfad,
@@ -235,6 +275,9 @@ def lies_testdatei(pfad: Path, paket: str) -> Testdatei:
uebersprungen=skip or klassen_skip,
grund=grund or (klassen_grund if klassen_skip else ""),
erwarteter_fehlschlag=erwartet or klassen_erwartet,
quellhash=_funktionshash(zeilen, glied),
zeile=glied.lineno,
quelltext=_funktionsquelle(zeilen, glied),
)
)
if gruppe.faelle:
@@ -253,6 +296,122 @@ def sammle_paket(paket: str, repo: dict) -> Paketkatalog | None:
return katalog
AUSSCHLUSS_ORDNER = {"tests", "__pycache__"}
AUSSCHLUSS_DATEIEN = {"main.py", "__init__.py"}
WORTZEICHEN = re.compile(r"[A-Za-zÄÖÜäöüß_][A-Za-zÄÖÜäöüß0-9_]*")
def _ist_dunder(name: str) -> bool:
return name.startswith("__") and name.endswith("__")
ABSICHTLICH_MARKER = ("von Test abgesehen", "Test ausgesetzt")
def absichtlicher_verzicht(docstring: str | None) -> str:
"""Docstring-Zeile, die einen bewusst dokumentierten Testverzicht enthält, sonst "".
Erkennt die beiden im Projekt verwendeten Vermerke: "von Test abgesehen, weil …" (bewusst
vernachlässigbar) und "Test ausgesetzt, zunächst Refactor" (zurückgestellt bis zu einer geplanten
Überarbeitung). Ein Namensabgleich wie bei `sammle_referenzierte_namen` – reicht der Vermerk nicht
wörtlich, wird die Funktion weiterhin als offen (ungeklärt) geführt.
"""
if not docstring:
return ""
for zeile in docstring.splitlines():
text = " ".join(zeile.split())
if text and any(marker in text for marker in ABSICHTLICH_MARKER):
return text
return ""
def sammle_quellcode(ordner: Path) -> list[Quellfunktion]:
"""Listet Top-Level-Funktionen und Methoden im Quellcode eines Pakets (ohne tests/).
`main.py`/`__init__.py` bleiben außen vor: reine QGIS-Einstiegspunkte (`classFactory`,
`initGui`, `unload`) lassen sich ohne laufendes QGIS nicht sinnvoll unit-testen. Dunder-
Methoden (`__init__`, `__str__`, …) bleiben ebenfalls außen vor, sonst überwiegt Rauschen
aus Standardmethoden die eigentlich interessanten Lücken.
"""
ergebnisse: list[Quellfunktion] = []
for pfad in sorted(ordner.rglob("*.py")):
rel = pfad.relative_to(ordner)
if AUSSCHLUSS_ORDNER & set(rel.parts[:-1]):
continue
if rel.name in AUSSCHLUSS_DATEIEN:
continue
anzeigepfad = rel.as_posix()
baum = ast.parse(pfad.read_text(encoding="utf-8"), filename=str(pfad))
for knoten in baum.body:
if isinstance(knoten, (ast.FunctionDef, ast.AsyncFunctionDef)):
ergebnisse.append(
Quellfunktion(
datei=anzeigepfad,
name=knoten.name,
zeile=knoten.lineno,
absichtlich_grund=absichtlicher_verzicht(ast.get_docstring(knoten)),
)
)
elif isinstance(knoten, ast.ClassDef):
for glied in knoten.body:
if isinstance(glied, (ast.FunctionDef, ast.AsyncFunctionDef)) and not _ist_dunder(glied.name):
ergebnisse.append(
Quellfunktion(
datei=anzeigepfad,
name=f"{knoten.name}.{glied.name}",
zeile=glied.lineno,
absichtlich_grund=absichtlicher_verzicht(ast.get_docstring(glied)),
)
)
return ergebnisse
def sammle_referenzierte_namen(testdateien: list[Path]) -> set[str]:
"""Alle Bezeichner, die irgendwo in den Testdateien eines Pakets vorkommen.
Erfasst werden Namen (Aufrufe, Importe), Attributzugriffe (`self.x.methode()`) und
Wortbestandteile in Zeichenketten (deckt `mock.patch("modul.Klasse.methode")` ab). Bewusst
großzügig: ein Treffer gilt schon als Bezug, auch wenn er nur zufällig denselben Namen trifft –
lieber eine echte Lücke übersehen als eine vortäuschen, die keine ist.
"""
namen: set[str] = set()
for pfad in testdateien:
baum = ast.parse(pfad.read_text(encoding="utf-8"), filename=str(pfad))
for knoten in ast.walk(baum):
if isinstance(knoten, ast.Name):
namen.add(knoten.id)
elif isinstance(knoten, ast.Attribute):
namen.add(knoten.attr)
elif isinstance(knoten, ast.Constant) and isinstance(knoten.value, str):
namen.update(WORTZEICHEN.findall(knoten.value))
return namen
def setze_referenzstatus(katalog: Paketkatalog) -> None:
"""Füllt katalog.quellfunktionen und markiert, welche in keiner Testdatei referenziert werden."""
katalog.quellfunktionen = sammle_quellcode(ROOT / katalog.ordner)
referenziert = sammle_referenzierte_namen([d.pfad for d in katalog.dateien])
for funktion in katalog.quellfunktionen:
bezeichner = funktion.name.rsplit(".", 1)[-1]
funktion.referenziert = bezeichner in referenziert
def zaehle_quellfunktionen(funktionen: list[Quellfunktion]) -> dict[str, int]:
ohne_bezug = [f for f in funktionen if not f.referenziert]
return {
"gesamt": len(funktionen),
"referenziert": sum(1 for f in funktionen if f.referenziert),
"ohne_bezug": len(ohne_bezug),
"absichtlich": sum(1 for f in ohne_bezug if f.absichtlich_grund),
"ungeklaert": sum(1 for f in ohne_bezug if not f.absichtlich_grund),
}
def voller_testpfad(datei: Testdatei, fall: Testfall) -> str:
"""Voller Schlüssel eines Testfalls – identisch zum Format in testbaseline.json/testpruefungen.json."""
return f"{datei.modulpfad}.{fall.bezeichnung}"
def lies_baseline(pfad: Path) -> tuple[dict[str, set[str]], str]:
"""Rote Tests je Paket als Menge '<modulpfad>.<Klasse>.<methode>' plus Kurzprüfsumme."""
if not pfad.exists():
@@ -274,7 +433,7 @@ def lies_baseline(pfad: Path) -> tuple[dict[str, set[str]], str]:
def setze_status(katalog: Paketkatalog, rot: set[str]) -> None:
for datei in katalog.dateien:
for fall in datei.faelle:
voll = f"{datei.modulpfad}.{fall.bezeichnung}"
voll = voller_testpfad(datei, fall)
if fall.uebersprungen:
fall.status = "übersprungen"
elif fall.erwarteter_fehlschlag:
@@ -285,6 +444,27 @@ def setze_status(katalog: Paketkatalog, rot: set[str]) -> None:
fall.status = "grün"
def lies_pruefungen(pfad: Path) -> tuple[dict[str, dict], str]:
"""Prüf-Historie je Testfall plus Kurzprüfsumme der Datei; ({}, "") wenn sie fehlt."""
if not pfad.exists():
return {}, ""
rohdaten = pfad.read_bytes()
kurz = hashlib.sha256(rohdaten).hexdigest()[:12]
return json.loads(rohdaten.decode("utf-8")), kurz
def setze_pruefstatus(katalog: Paketkatalog, pruefungen: dict[str, dict]) -> None:
for datei in katalog.dateien:
for fall in datei.faelle:
eintrag = pruefungen.get(voller_testpfad(datei, fall))
if eintrag is None:
fall.pruefstatus, fall.pruefdatum = "nie geprüft", ""
elif eintrag.get("hash") == fall.quellhash:
fall.pruefstatus, fall.pruefdatum = "aktuell geprüft", eintrag.get("datum", "")
else:
fall.pruefstatus, fall.pruefdatum = "veraltet", eintrag.get("datum", "")
def zaehle(faelle: list[Testfall]) -> dict[str, int]:
return {
"gesamt": len(faelle),
@@ -293,6 +473,9 @@ def zaehle(faelle: list[Testfall]) -> dict[str, int]:
"uebersprungen": sum(1 for f in faelle if f.status in ("übersprungen", "erwarteter Fehlschlag")),
"erwartet": sum(1 for f in faelle if f.status == "erwarteter Fehlschlag"),
"mit_docstring": sum(1 for f in faelle if f.beschreibung),
"nie_geprueft": sum(1 for f in faelle if f.pruefstatus == "nie geprüft"),
"aktuell_geprueft": sum(1 for f in faelle if f.pruefstatus == "aktuell geprüft"),
"veraltet_geprueft": sum(1 for f in faelle if f.pruefstatus == "veraltet"),
}
@@ -313,10 +496,29 @@ def status_text(fall: Testfall) -> str:
return fall.status
def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kurz: str) -> str:
def pruef_text(fall: Testfall) -> str:
"""Anzeigetext für den Prüfstatus (Prüfstand-Spalte 'geprüft')."""
if fall.pruefstatus == "aktuell geprüft":
return f"geprüft am {fall.pruefdatum}"
if fall.pruefstatus == "veraltet":
return f"veraltet seit Änderung – zuletzt geprüft am {fall.pruefdatum}"
return "nie geprüft"
def erzeuge_markdown(
kataloge: list[Paketkatalog],
baseline_pfad: Path,
baseline_kurz: str,
pruefungen_pfad: Path,
pruefungen_kurz: str,
) -> str:
alle = [f for k in kataloge for f in k.faelle]
summe = zaehle(alle)
summe_qf = zaehle_quellfunktionen([f for k in kataloge for f in k.quellfunktionen])
baseline_anzeige = baseline_pfad.relative_to(ROOT).as_posix() if baseline_pfad.is_relative_to(ROOT) else str(baseline_pfad)
pruefungen_anzeige = (
pruefungen_pfad.relative_to(ROOT).as_posix() if pruefungen_pfad.is_relative_to(ROOT) else str(pruefungen_pfad)
)
z: list[str] = []
z.append("# Testkatalog der sn_*-Plugins")
@@ -332,6 +534,12 @@ def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline
z.append(f"Stand der Baseline: `{baseline_anzeige}` (SHA-256 `{baseline_kurz}`)")
else:
z.append(f"Stand der Baseline: `{baseline_anzeige}` **nicht gefunden** – alle Testfälle gelten als grün.")
if pruefungen_kurz:
z.append(f"Stand des Prüf-Audits: `{pruefungen_anzeige}` (SHA-256 `{pruefungen_kurz}`)")
else:
z.append(
f"Stand des Prüf-Audits: `{pruefungen_anzeige}` **nicht gefunden** – alle Testfälle gelten als nie geprüft."
)
z.append("")
z.append(
f"Gesamt: {len(kataloge)} Pakete, {sum(len(k.dateien) for k in kataloge)} Testdateien, "
@@ -339,22 +547,37 @@ def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline
f"{summe['uebersprungen']} übersprungen."
)
z.append(f"Mit Beschreibung (Docstring): {anteil(summe['mit_docstring'], summe['gesamt'])}.")
z.append(
f"Fachlich geprüft: {anteil(summe['aktuell_geprueft'], summe['gesamt'])} aktuell, "
f"{summe['veraltet_geprueft']} veraltet seit Codeänderung, {summe['nie_geprueft']} nie geprüft."
)
z.append(
f"Strukturelle Lücken (heuristisch, Namensabgleich): {anteil(summe_qf['ohne_bezug'], summe_qf['gesamt'])} "
"Funktionen/Methoden ohne erkennbaren Testbezug – Abschnitt „Ungetestete Funktionen“."
)
z.append("")
z.append("## Zusammenfassung")
z.append("")
z.append("| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung |")
z.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |")
z.append(
"| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung | "
"fachlich geprüft | ohne Testbezug |"
)
z.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
for katalog in kataloge:
k = zaehle(katalog.faelle)
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
z.append(
f"| `{katalog.paket}` | {len(katalog.dateien)} | {k['gesamt']} | {k['gruen']} | "
f"{k['rot']} | {k['uebersprungen']} | {anteil(k['mit_docstring'], k['gesamt'])} |"
f"{k['rot']} | {k['uebersprungen']} | {anteil(k['mit_docstring'], k['gesamt'])} | "
f"{anteil(k['aktuell_geprueft'], k['gesamt'])} | {anteil(qf['ohne_bezug'], qf['gesamt'])} |"
)
z.append(
f"| **Summe** | **{sum(len(k.dateien) for k in kataloge)}** | **{summe['gesamt']}** | "
f"**{summe['gruen']}** | **{summe['rot']}** | **{summe['uebersprungen']}** | "
f"**{anteil(summe['mit_docstring'], summe['gesamt'])}** |"
f"**{anteil(summe['mit_docstring'], summe['gesamt'])}** | "
f"**{anteil(summe['aktuell_geprueft'], summe['gesamt'])}** | "
f"**{anteil(summe_qf['ohne_bezug'], summe_qf['gesamt'])}** |"
)
z.append("")
if summe["erwartet"]:
@@ -368,6 +591,19 @@ def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline
z.append(f"- **rot (Baseline)** – der Testfall schlägt bereits fehl und ist in `{baseline_anzeige}` vermerkt; er darf nicht ignoriert, aber auch nicht als neuer Fehler gewertet werden.")
z.append("- **übersprungen** – per `@unittest.skip`/`skipIf`/`skipUnless` abgeschaltet; der Grund steht dabei.")
z.append("- Ein Testfall heißt `Testgruppe.test_name`; die Testgruppe fasst zusammengehörige Testfälle einer Datei zusammen.")
z.append(
"- **geprüft** – Prüf-Audit (`scripts/test_audit.py`): **nie geprüft** kein Eintrag in "
f"`{pruefungen_anzeige}`; **geprüft am …** ein Mensch hat den Testfall bestätigt und der Testcode hat sich "
"seither nicht geändert; **veraltet seit Änderung …** der Testcode wurde nach der letzten Prüfung geändert."
)
z.append(
"- **ohne Testbezug** – Funktionen/Methoden im Quellcode, deren Name in keiner Testdatei des Pakets "
"vorkommt (weder als Aufruf noch als `mock.patch(\"…\")`-Ziel). Ein Namensabgleich, kein Ausführungsnachweis "
"– siehe Abschnitt „Ungetestete Funktionen“ für die Grenzen dieser Heuristik. Das ist bewusst **keine** "
"Zeilen-Coverage in Prozent (siehe ADR 0009, „Verworfene Alternativen“), sondern eine Lückenliste wie bei "
"fehlenden Beschreibungen. Davon **beabsichtigt** – der Docstring dokumentiert den Testverzicht; "
"**ungeklärt** – weder Test noch Vermerk, die eigentliche Arbeitsliste."
)
z.append("")
for katalog in kataloge:
@@ -399,10 +635,12 @@ def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline
for gruppe in datei.gruppen:
z.append(f"- `{gruppe.name}` ({len(gruppe.faelle)} Testfälle): {zelle(gruppe.beschreibung)}")
z.append("")
z.append("| Testfall | prüft | Status |")
z.append("| --- | --- | --- |")
z.append("| Testfall | prüft | Status | geprüft |")
z.append("| --- | --- | --- | --- |")
for fall in datei.faelle:
z.append(f"| `{fall.bezeichnung}` | {zelle(fall.beschreibung)} | {status_text(fall)} |")
z.append(
f"| `{fall.bezeichnung}` | {zelle(fall.beschreibung)} | {status_text(fall)} | {pruef_text(fall)} |"
)
z.append("")
z.append("## Lücken")
@@ -430,6 +668,48 @@ def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline
for fall in luecken:
z.append(f"- `{fall.bezeichnung}`")
z.append("")
z.append("## Ungetestete Funktionen (strukturelle Lücke)")
z.append("")
z.append(
"Heuristik, kein Beweis: Diese Funktionen und Methoden kommen unter ihrem Namen in keiner Testdatei"
" des Pakets vor. Das schließt nicht aus, dass sie indirekt über eine andere, getestete Funktion"
" mitlaufen – umgekehrt gilt: Taucht der Name irgendwo in einer Testdatei auf, gilt die Funktion schon"
" als referenziert, auch bei zufälliger Namensgleichheit mit etwas anderem. `main.py`/`__init__.py`"
" (QGIS-Einstiegspunkte wie `classFactory`, `initGui`, `unload`) und Dunder-Methoden sind ausgenommen."
" Diese Liste ersetzt keine fachliche Prüfung, ob ein Test sinnvoll wäre – dafür braucht es einen"
" Menschen, der die Funktion kennt."
)
z.append(
"„beabsichtigt“ liest den Docstring der Funktion: Enthält er einen Vermerk wie „von Test abgesehen,"
" weil …“ oder „Test ausgesetzt, zunächst Refactor“, gilt der fehlende Test als bewusste Entscheidung."
" Alles andere ist „ungeklärt“ – die eigentliche Arbeitsliste."
)
z.append("")
for katalog in kataloge:
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
z.append(
f"### `{katalog.paket}` – {anteil(qf['ohne_bezug'], qf['gesamt'])} ohne Testbezug "
f"({qf['ungeklaert']} ungeklärt, {qf['absichtlich']} beabsichtigt)"
)
z.append("")
luecken_qf = [f for f in katalog.quellfunktionen if not f.referenziert]
if not luecken_qf:
z.append("Keine Lücken.")
z.append("")
continue
je_datei: dict[str, list[Quellfunktion]] = {}
for funktion in luecken_qf:
je_datei.setdefault(funktion.datei, []).append(funktion)
for datei, funktionen in je_datei.items():
z.append(f"#### `{datei}` ({len(funktionen)})")
z.append("")
for funktion in funktionen:
if funktion.absichtlich_grund:
z.append(f"- `{funktion.name}` (Zeile {funktion.zeile}) — **beabsichtigt:** {zelle(funktion.absichtlich_grund)}")
else:
z.append(f"- `{funktion.name}` (Zeile {funktion.zeile}) — ungeklärt")
z.append("")
return "\n".join(z) + "\n"
@@ -440,6 +720,12 @@ STATUS_KLASSE = {
"erwarteter Fehlschlag": "skip",
}
PRUEF_KLASSE = {
"aktuell geprüft": "ok",
"veraltet": "warn",
"nie geprüft": "skip",
}
def _slug(text: str) -> str:
return re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower()
@@ -480,6 +766,10 @@ def _html_karte_fuer_datei(datei: Testdatei) -> str:
zaehler[STATUS_KLASSE.get(fall.status, "ok")] += 1
mit_doc = sum(1 for f in faelle if f.beschreibung)
doc_pct = round(100 * mit_doc / gesamt) if gesamt else 0
aktuell_geprueft = sum(1 for f in faelle if f.pruefstatus == "aktuell geprüft")
audit_pct = round(100 * aktuell_geprueft / gesamt) if gesamt else 0
hat_ungeprueft = any(f.pruefstatus == "nie geprüft" for f in faelle)
hat_veraltet = any(f.pruefstatus == "veraltet" for f in faelle)
if not datei.gruppen:
koerper = (
@@ -501,6 +791,7 @@ def _html_karte_fuer_datei(datei: Testdatei) -> str:
kurz = {"grün": "grün", "rot (Baseline)": "rot", "übersprungen": "übersp.", "erwarteter Fehlschlag": "übersp."}
for fall in faelle:
klasse = STATUS_KLASSE.get(fall.status, "ok")
pruef_klasse = PRUEF_KLASSE.get(fall.pruefstatus, "skip")
pruft = (
f'<span class="nodoc">— (Docstring fehlt)</span>'
if not fall.beschreibung
@@ -511,10 +802,12 @@ def _html_karte_fuer_datei(datei: Testdatei) -> str:
f'<tr><td class="mono">{html.escape(fall.klasse)}<br>'
f'<span class="testname">{html.escape(fall.name)}</span></td>'
f"<td>{pruft}</td>"
f'<td><span class="pill pill-{klasse}"{titel}>{kurz.get(fall.status, fall.status)}</span></td></tr>'
f'<td><span class="pill pill-{klasse}"{titel}>{kurz.get(fall.status, fall.status)}</span></td>'
f'<td><span class="pill pill-pr-{pruef_klasse}">{html.escape(pruef_text(fall))}</span></td></tr>'
)
tabelle = (
'<table class="test-table"><thead><tr><th>Testfall</th><th>prüft</th><th>Status</th></tr></thead>'
'<table class="test-table"><thead><tr><th>Testfall</th><th>prüft</th><th>Status</th>'
"<th>geprüft</th></tr></thead>"
f"<tbody>{''.join(zeilen)}</tbody></table>"
)
diagramm = (
@@ -536,38 +829,111 @@ def _html_karte_fuer_datei(datei: Testdatei) -> str:
'<span class="card-meta">'
f'<span class="count-total">{gesamt} Testfälle</span>{pills}'
f'<span class="docbadge" title="Anteil mit Docstring">{doc_pct}% beschrieben</span>'
f'<span class="auditbadge" title="Anteil fachlich aktuell geprüft">{audit_pct}% geprüft</span>'
'<span class="chev" aria-hidden="true">▾</span></span></button>'
)
return (
f'<article class="filecard" data-search="{suchtext}" '
f'data-hasred="{1 if zaehler["warn"] else 0}" data-hasskip="{1 if zaehler["skip"] else 0}" '
f'data-nodoc="{1 if doc_pct < 50 else 0}">'
f'data-nodoc="{1 if doc_pct < 50 else 0}" '
f'data-ungeprueft="{1 if hat_ungeprueft else 0}" data-veraltet="{1 if hat_veraltet else 0}">'
f"{header}"
f'<div class="card-body" id="body-{fid}">{koerper}</div>'
"</article>"
)
def _html_luecken_fuer_paket(katalog: Paketkatalog) -> str:
"""Ausklappbare Karte je Paket (wie eine Testdatei-Karte): Quellfunktionen ohne erkennbaren Testbezug."""
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
if not qf["gesamt"]:
return ""
fid = _slug(f"{katalog.paket}-luecken")
luecken = sorted([f for f in katalog.quellfunktionen if not f.referenziert], key=lambda f: (f.datei, f.zeile))
if not luecken:
tabelle = '<p class="empty-note">Keine Lücken.</p>'
else:
zeilen = []
for f in luecken:
if f.absichtlich_grund:
status = f'<span class="pill pill-skip" title="{html.escape(f.absichtlich_grund)}">beabsichtigt</span>'
else:
status = '<span class="pill pill-warn">ungeklärt</span>'
zeilen.append(
f'<tr><td class="mono">{html.escape(f.datei)}</td><td class="mono">{html.escape(f.name)}</td>'
f"<td>{f.zeile}</td><td>{status}</td></tr>"
)
tabelle = (
'<table class="test-table"><thead><tr><th>Datei</th><th>Funktion/Methode</th><th>Zeile</th>'
"<th>Status</th></tr></thead>"
f"<tbody>{''.join(zeilen)}</tbody></table>"
)
beschreibung = (
'<p class="filedesc">Heuristik: Name kommt in keiner Testdatei des Pakets vor (Details siehe '
'<span class="mono">docs/tests/testkatalog.md</span>). <b>beabsichtigt</b> liest den Docstring der '
"Funktion – ein Vermerk wie „von Test abgesehen, weil …“ oder „Test ausgesetzt, zunächst Refactor“ "
"gilt als bewusste Entscheidung. <b>ungeklärt</b> hat weder Test noch Vermerk.</p>"
)
suchtext = html.escape(f"ungetestete funktionen {katalog.paket}".lower())
header = (
f'<button class="card-toggle" data-target="{fid}" aria-expanded="false">'
'<span class="card-path mono">Ungetestete Funktionen</span>'
'<span class="card-meta">'
f'<span class="count-total">{qf["gesamt"]} Funktionen</span>'
f'<span class="cpill cpill-warn">{qf["ungeklaert"]} ungeklärt</span>'
f'<span class="cpill cpill-skip">{qf["absichtlich"]} beabsichtigt</span>'
'<span class="chev" aria-hidden="true">▾</span></span></button>'
)
return (
f'<article class="filecard gapcard" data-search="{suchtext}">'
f"{header}"
f'<div class="card-body" id="body-{fid}">{beschreibung}{tabelle}</div>'
"</article>"
)
def _html_abschnitt_fuer_paket(katalog: Paketkatalog) -> str:
k = zaehle(katalog.faelle)
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
rolle = ROLLENTEXT.get(katalog.rolle, katalog.rolle or "Plugin")
karten = "".join(_html_karte_fuer_datei(d) for d in katalog.dateien) or "<p>Keine Testdateien gefunden.</p>"
gesamt = k["gesamt"] or 1
ok_pct = round(100 * k["gruen"] / gesamt)
warn_pct = round(100 * k["rot"] / gesamt)
skip_pct = max(0, 100 - ok_pct - warn_pct)
doc_pct = round(100 * k["mit_docstring"] / k["gesamt"]) if k["gesamt"] else 0
audit_pct = round(100 * k["aktuell_geprueft"] / k["gesamt"]) if k["gesamt"] else 0
kacheln = [
("", len(katalog.dateien), "Testdateien"),
("", k["gesamt"], "Testfälle"),
("ok", k["gruen"], "grün"),
("warn", k["rot"], "rot (Baseline)"),
("skip", k["uebersprungen"], "übersprungen"),
("", f"{doc_pct}%", "mit Docstring"),
("", f"{audit_pct}%", "geprüft"),
("warn" if qf["ungeklaert"] else "", qf["ohne_bezug"], "ohne Testbezug"),
]
tiles = "".join(
f'<div class="stat stat-sm{" " + klasse if klasse else ""}"><span class="n">{wert}</span>'
f'<span class="l">{label}</span></div>'
for klasse, wert, label in kacheln
)
return f"""
<section class="pkg-section" id="pkg-{html.escape(katalog.paket)}">
<div class="pkg-head">
<div>
<h2>{html.escape(katalog.paket)}</h2>
<p class="pkg-sub">{html.escape(rolle)} · Ordner <span class="mono">{html.escape(katalog.ordner)}</span> · {len(katalog.dateien)} Testdateien · {k['gesamt']} Testfälle</p>
<p class="pkg-sub">{html.escape(rolle)} · Ordner <span class="mono">{html.escape(katalog.ordner)}</span></p>
</div>
<div class="pkg-bar" role="img" aria-label="{k['gruen']} grün, {k['rot']} rot, {k['uebersprungen']} übersprungen">
<span class="seg seg-ok" style="width:{ok_pct}%"></span><span class="seg seg-warn" style="width:{warn_pct}%"></span><span class="seg seg-skip" style="width:{skip_pct}%"></span>
</div>
</div>
<div class="pkg-summary">{tiles}</div>
<div class="card-grid">{karten}</div>
{_html_luecken_fuer_paket(katalog)}
</section>
"""
@@ -649,6 +1015,10 @@ _HTML_KOPF = """<title>Prüfstand</title>
.pkg-bar{ width:220px; height:9px; border-radius:6px; overflow:hidden; display:flex; background:var(--surface-2); border:1px solid var(--line); }
.pkg-bar .seg{ height:100%; display:block; }
.seg-ok{ background:var(--ok); } .seg-warn{ background:var(--warn); } .seg-skip{ background:var(--skip); }
.pkg-summary{ max-width:100%; margin:0 0 16px; display:grid; gap:8px; grid-template-columns:repeat(auto-fit,minmax(108px,1fr)); }
.stat-sm{ padding:8px 10px; }
.stat-sm .n{ font-size:17px; }
.stat-sm .l{ font-size:10px; }
.card-grid{ display:grid; gap:12px; }
.filecard{ background:var(--surface); border:1px solid var(--line); border-radius:12px; box-shadow:var(--shadow); overflow:hidden; }
.card-toggle{ width:100%; display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; justify-content:space-between;
@@ -661,6 +1031,7 @@ _HTML_KOPF = """<title>Prüfstand</title>
.cpill{ font-family:"IBM Plex Mono",monospace; font-variant-numeric:tabular-nums; font-size:11.5px; padding:2px 7px; border-radius:999px; font-weight:600; }
.cpill-ok{ background:var(--ok-bg); color:var(--ok); } .cpill-warn{ background:var(--warn-bg); color:var(--warn); } .cpill-skip{ background:var(--skip-bg); color:var(--skip); }
.docbadge{ color:var(--muted); border:1px solid var(--line-strong); border-radius:999px; padding:2px 8px; }
.auditbadge{ color:var(--muted); border:1px solid var(--line-strong); border-radius:999px; padding:2px 8px; }
.chev{ color:var(--muted); transition:transform .15s ease; }
.filecard.open .chev{ transform:rotate(180deg); }
.card-body{ max-height:0; overflow:hidden; transition:max-height .22s ease; border-top:0 solid var(--line); }
@@ -684,6 +1055,7 @@ _HTML_KOPF = """<title>Prüfstand</title>
.nodoc{ color:var(--muted); font-style:italic; }
.pill{ font-size:11.5px; padding:2px 8px; border-radius:999px; font-weight:600; white-space:nowrap; }
.pill-ok{ background:var(--ok-bg); color:var(--ok); } .pill-warn{ background:var(--warn-bg); color:var(--warn); } .pill-skip{ background:var(--skip-bg); color:var(--skip); }
.pill-pr-ok{ background:var(--ok-bg); color:var(--ok); } .pill-pr-warn{ background:var(--warn-bg); color:var(--warn); } .pill-pr-skip{ background:var(--skip-bg); color:var(--skip); }
footer{ max-width:1180px; margin:40px auto 30px; color:var(--muted); font-size:12px; border-top:1px solid var(--line); padding-top:14px; }
[hidden]{ display:none !important; }
@media (max-width:640px){ .pkg-bar{ width:130px; } .controls{ position:static; } }
@@ -723,13 +1095,13 @@ _HTML_SKRIPT = """
function applyFilters(){
var active = document.querySelector('.chip[aria-pressed="true"]').dataset.filter;
var q = document.getElementById('searchInput').value.trim().toLowerCase();
document.querySelectorAll('.filecard').forEach(function(card){
document.querySelectorAll('.filecard:not(.gapcard)').forEach(function(card){
var passFilter = active === 'all' || card.dataset[active] === '1';
var passSearch = !q || card.dataset.search.indexOf(q) !== -1;
card.hidden = !(passFilter && passSearch);
});
document.querySelectorAll('.pkg-section').forEach(function(sec){
sec.hidden = !sec.querySelector('.filecard:not([hidden])');
sec.hidden = !sec.querySelector('.filecard:not(.gapcard):not([hidden])');
});
}
document.querySelectorAll('.chip').forEach(function(chip){
@@ -747,7 +1119,13 @@ _HTML_SKRIPT = """
"""
def erzeuge_html(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kurz: str) -> str:
def erzeuge_html(
kataloge: list[Paketkatalog],
baseline_pfad: Path,
baseline_kurz: str,
pruefungen_pfad: Path,
pruefungen_kurz: str,
) -> str:
"""Erzeugt das HTML-Dashboard 'Prüfstand' – dieselben Daten wie erzeuge_markdown, als
interaktive Seite mit Baumdiagramm (Mermaid) je Testdatei. Läuft lokal ohne Server."""
alle = [f for k in kataloge for f in k.faelle]
@@ -755,11 +1133,19 @@ def erzeuge_html(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kur
baseline_anzeige = (
baseline_pfad.relative_to(ROOT).as_posix() if baseline_pfad.is_relative_to(ROOT) else str(baseline_pfad)
)
pruefungen_anzeige = (
pruefungen_pfad.relative_to(ROOT).as_posix() if pruefungen_pfad.is_relative_to(ROOT) else str(pruefungen_pfad)
)
meta_zeile = (
f'Baseline: <span class="mono">{html.escape(baseline_anzeige)}</span> (SHA-256 {baseline_kurz})'
if baseline_kurz
else f'Baseline <span class="mono">{html.escape(baseline_anzeige)}</span> nicht gefunden'
)
meta_zeile_audit = (
f'Prüf-Audit: <span class="mono">{html.escape(pruefungen_anzeige)}</span> (SHA-256 {pruefungen_kurz})'
if pruefungen_kurz
else f'Prüf-Audit <span class="mono">{html.escape(pruefungen_anzeige)}</span> nicht gefunden'
)
nav_links = "\n ".join(
f'<a href="#pkg-{html.escape(k.paket)}">{html.escape(k.paket)} <span class="mono">({len(k.faelle)})</span></a>'
@@ -789,7 +1175,9 @@ def erzeuge_html(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kur
teile.append(
'<span class="tagline">Testkatalog der sn_*-Plugins — was die Suite heute prüft, auf einen Blick.</span></div>'
)
teile.append(f'<div class="meta-line">Quelle: <b>docs/tests/testkatalog.md</b><br>{meta_zeile}</div></div>')
teile.append(
f'<div class="meta-line">Quelle: <b>docs/tests/testkatalog.md</b><br>{meta_zeile}<br>{meta_zeile_audit}</div></div>'
)
teile.append('<div class="summary">')
teile.append(f'<div class="stat"><span class="n">{summe["gesamt"]}</span><span class="l">Testfälle</span></div>')
@@ -797,8 +1185,13 @@ def erzeuge_html(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kur
teile.append(f'<div class="stat warn"><span class="n">{summe["rot"]}</span><span class="l">rot (Baseline)</span></div>')
teile.append(f'<div class="stat skip"><span class="n">{summe["uebersprungen"]}</span><span class="l">übersprungen</span></div>')
doc_pct = round(100 * summe["mit_docstring"] / summe["gesamt"]) if summe["gesamt"] else 0
audit_pct = round(100 * summe["aktuell_geprueft"] / summe["gesamt"]) if summe["gesamt"] else 0
teile.append(f'<div class="stat"><span class="n">{doc_pct}%</span><span class="l">mit Docstring</span></div>')
teile.append(f'<div class="stat"><span class="n">{audit_pct}%</span><span class="l">aktuell geprüft</span></div>')
teile.append(f'<div class="stat"><span class="n">{sum(len(k.dateien) for k in kataloge)}</span><span class="l">Testdateien</span></div>')
summe_qf = zaehle_quellfunktionen([f for k in kataloge for f in k.quellfunktionen])
qf_pct = round(100 * summe_qf["ohne_bezug"] / summe_qf["gesamt"]) if summe_qf["gesamt"] else 0
teile.append(f'<div class="stat warn"><span class="n">{qf_pct}%</span><span class="l">ohne Testbezug</span></div>')
teile.append("</div>")
teile.append('<div class="pkg-bars"><div class="pkg-bars-title">Verteilung je Paket</div>')
@@ -812,6 +1205,8 @@ def erzeuge_html(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kur
teile.append('<button class="chip" data-filter="hasred" aria-pressed="false">Mit rot</button>')
teile.append('<button class="chip" data-filter="hasskip" aria-pressed="false">Mit übersprungen</button>')
teile.append('<button class="chip" data-filter="nodoc" aria-pressed="false">Schwach beschrieben (&lt;50%)</button>')
teile.append('<button class="chip" data-filter="ungeprueft" aria-pressed="false">Nie geprüft</button>')
teile.append('<button class="chip" data-filter="veraltet" aria-pressed="false">Veraltet geprüft</button>')
teile.append("</div>")
teile.append(
'<div class="searchbox"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
@@ -863,6 +1258,8 @@ def main() -> int:
help="Zieldatei für das HTML-Dashboard (Standard: docs/tests/testkatalog.html)")
ap.add_argument("--ohne-html", action="store_true", help="kein HTML-Dashboard erzeugen/prüfen")
ap.add_argument("--baseline-datei", type=Path, default=BASELINE_DEFAULT)
ap.add_argument("--pruefungsdatei", type=Path, default=PRUEFUNGEN_DEFAULT,
help="Zieldatei für den Prüfstatus (Standard: docs/status/testpruefungen.json)")
args = ap.parse_args()
manifest = lade_manifest()
@@ -879,6 +1276,9 @@ def main() -> int:
rot_je_paket, baseline_kurz = lies_baseline(args.baseline_datei)
if not baseline_kurz:
print(f"Keine Baseline unter {args.baseline_datei} – alle Testfälle werden als grün geführt.")
pruefungen, pruefungen_kurz = lies_pruefungen(args.pruefungsdatei)
if not pruefungen_kurz:
print(f"Kein Prüf-Audit unter {args.pruefungsdatei} – alle Testfälle gelten als nie geprüft.")
kataloge: list[Paketkatalog] = []
for paket in reihenfolge:
@@ -890,10 +1290,14 @@ def main() -> int:
print(f"[{paket}] kein Ordner {repos[paket]['name']}/tests – übersprungen")
continue
setze_status(katalog, rot_je_paket.get(paket, set()))
setze_pruefstatus(katalog, pruefungen)
setze_referenzstatus(katalog)
k = zaehle(katalog.faelle)
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
print(f"[{paket}] {len(katalog.dateien)} Testdateien, {k['gesamt']} Testfälle "
f"({k['gruen']} grün, {k['rot']} rot, {k['uebersprungen']} übersprungen, "
f"{k['mit_docstring']} mit Docstring)")
f"{k['mit_docstring']} mit Docstring); {qf['ohne_bezug']} von {qf['gesamt']} "
f"Quellfunktionen ohne Testbezug")
if not katalog.dateien:
print(f" Keine Datei tests/test_*.py gefunden.")
kataloge.append(katalog)
@@ -902,9 +1306,15 @@ def main() -> int:
print("Keine Pakete mit Testdateien gefunden – nichts zu tun.")
return 2
ausgaben = [(args.ausgabe, erzeuge_markdown(kataloge, args.baseline_datei, baseline_kurz))]
ausgaben = [(
args.ausgabe,
erzeuge_markdown(kataloge, args.baseline_datei, baseline_kurz, args.pruefungsdatei, pruefungen_kurz),
)]
if not args.ohne_html:
ausgaben.append((args.html_ausgabe, erzeuge_html(kataloge, args.baseline_datei, baseline_kurz)))
ausgaben.append((
args.html_ausgabe,
erzeuge_html(kataloge, args.baseline_datei, baseline_kurz, args.pruefungsdatei, pruefungen_kurz),
))
ergebnis = 0
for ziel, text in ausgaben:
+236
View File
@@ -0,0 +1,236 @@
"""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()
@@ -0,0 +1,182 @@
"""Tests für die strukturelle Lückenanalyse in scripts/testkatalog.py (Abschnitt „Ungetestete Funktionen").
Reines unittest, ohne QGIS/sn_basis-Abhängigkeit. Arbeitet auf temporären Mini-Paketen, nicht auf den
echten Plugin-Repos.
Ausführung (aus dem Workspace-Root): python -m unittest discover -s scripts/tests -p "test_*.py"
"""
from __future__ import annotations
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 testkatalog as tk # noqa: E402
QUELLDATEI_INHALT = '''"""Beispielmodul für den Selbsttest der Lückenanalyse."""
def oeffentliche_funktion(x):
return x
def _private_funktion(x):
return x
class Beispielklasse:
def __init__(self):
self.wert = 0
def getestete_methode(self):
return self.wert
def ungetestete_methode(self):
return self.wert
def bewusst_ausgelassene_methode(self):
"""von Test abgesehen, weil vernachlässigbar; XY 01.01.2026"""
return self.wert
'''
TESTDATEI_MIT_AUFRUF = '''"""Testet Beispielklasse."""
import unittest
from unittest import mock
class TestBeispielklasse(unittest.TestCase):
def test_oeffentliche_funktion(self):
"""Prüft, dass oeffentliche_funktion den übergebenen Wert zurückgibt."""
self.assertEqual(1, 1)
def test_getestete_methode(self):
"""Prüft, dass getestete_methode über mock.patch angesprochen wird."""
with mock.patch("paket.modul.Beispielklasse.getestete_methode"):
pass
'''
class SammleQuellcodeTests(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.ordner = Path(self._tmp.name)
(self.ordner / "modul.py").write_text(QUELLDATEI_INHALT, encoding="utf-8")
(self.ordner / "main.py").write_text("def classFactory(iface):\n return None\n", encoding="utf-8")
(self.ordner / "__init__.py").write_text("VERSION = 1\n", encoding="utf-8")
(self.ordner / "tests").mkdir()
(self.ordner / "tests" / "test_modul.py").write_text(
"def test_hilfsfunktion():\n pass\n", encoding="utf-8"
)
def test_main_und_init_werden_ausgeschlossen(self):
"""Prüft, dass main.py und __init__.py nicht in die Quellcode-Analyse aufgenommen werden."""
funktionen = tk.sammle_quellcode(self.ordner)
dateien = {f.datei for f in funktionen}
self.assertNotIn("main.py", dateien)
self.assertNotIn("__init__.py", dateien)
def test_tests_ordner_wird_ausgeschlossen(self):
"""Prüft, dass Dateien unter tests/ nicht in die Quellcode-Analyse aufgenommen werden."""
funktionen = tk.sammle_quellcode(self.ordner)
self.assertFalse(any(f.datei.startswith("tests/") for f in funktionen))
def test_modulfunktion_und_methode_werden_mit_zeile_erfasst(self):
"""Prüft, dass Top-Level-Funktionen und Methoden mit qualifiziertem Namen und Zeile erfasst werden."""
funktionen = {f.name: f for f in tk.sammle_quellcode(self.ordner)}
self.assertIn("oeffentliche_funktion", funktionen)
self.assertIn("_private_funktion", funktionen)
self.assertIn("Beispielklasse.getestete_methode", funktionen)
self.assertEqual(funktionen["oeffentliche_funktion"].zeile, 4)
def test_dunder_methoden_werden_ausgeschlossen(self):
"""Prüft, dass Dunder-Methoden wie __init__ nicht in die Quellcode-Analyse aufgenommen werden."""
namen = {f.name for f in tk.sammle_quellcode(self.ordner)}
self.assertNotIn("Beispielklasse.__init__", namen)
def test_docstring_mit_vermerk_setzt_absichtlich_grund(self):
"""Prüft, dass eine Methode mit „von Test abgesehen“-Vermerk im Docstring absichtlich_grund gesetzt bekommt."""
funktionen = {f.name: f for f in tk.sammle_quellcode(self.ordner)}
self.assertIn("von Test abgesehen", funktionen["Beispielklasse.bewusst_ausgelassene_methode"].absichtlich_grund)
def test_docstring_ohne_vermerk_laesst_absichtlich_grund_leer(self):
"""Prüft, dass eine Methode ohne Vermerk im Docstring ein leeres absichtlich_grund behält."""
funktionen = {f.name: f for f in tk.sammle_quellcode(self.ordner)}
self.assertEqual(funktionen["Beispielklasse.ungetestete_methode"].absichtlich_grund, "")
class AbsichtlicherVerzichtTests(unittest.TestCase):
def test_erkennt_von_test_abgesehen_marker(self):
"""Prüft, dass absichtlicher_verzicht den 'von Test abgesehen'-Vermerk erkennt."""
text = tk.absichtlicher_verzicht("Kurzbeschreibung.\n\nvon Test abgesehen, weil vernachlässigbar; XY 01.01.2026")
self.assertIn("von Test abgesehen", text)
def test_erkennt_test_ausgesetzt_marker(self):
"""Prüft, dass absichtlicher_verzicht den 'Test ausgesetzt'-Vermerk erkennt."""
self.assertEqual(tk.absichtlicher_verzicht("Test ausgesetzt, zunächst Refactor"), "Test ausgesetzt, zunächst Refactor")
def test_liefert_leeren_string_ohne_marker(self):
"""Prüft, dass absichtlicher_verzicht ohne bekannten Vermerk einen leeren String liefert."""
self.assertEqual(tk.absichtlicher_verzicht("Ganz normale Beschreibung ohne Vermerk."), "")
def test_liefert_leeren_string_fuer_fehlenden_docstring(self):
"""Prüft, dass absichtlicher_verzicht für None einen leeren String liefert."""
self.assertEqual(tk.absichtlicher_verzicht(None), "")
class SammleReferenzierteNamenTests(unittest.TestCase):
def test_erkennt_aufruf_attribut_und_string_ziel(self):
"""Prüft, dass Aufrufe, Attributzugriffe und mock.patch-Zeichenketten als Referenz zählen."""
with tempfile.TemporaryDirectory() as tmp:
pfad = Path(tmp) / "test_modul.py"
pfad.write_text(TESTDATEI_MIT_AUFRUF, encoding="utf-8")
namen = tk.sammle_referenzierte_namen([pfad])
self.assertIn("oeffentliche_funktion", namen)
self.assertIn("getestete_methode", namen)
self.assertNotIn("ungetestete_methode", namen)
class SetzeReferenzstatusTests(unittest.TestCase):
def test_markiert_referenzierte_und_ungetestete_funktionen(self):
"""Prüft, dass setze_referenzstatus referenzierte von ungetesteten Funktionen unterscheidet."""
with tempfile.TemporaryDirectory() as tmp:
ordner = Path(tmp)
(ordner / "modul.py").write_text(QUELLDATEI_INHALT, encoding="utf-8")
(ordner / "tests").mkdir()
testpfad = ordner / "tests" / "test_modul.py"
testpfad.write_text(TESTDATEI_MIT_AUFRUF, encoding="utf-8")
with mock.patch.object(tk, "ROOT", ordner):
datei = tk.lies_testdatei(testpfad, "testpaket")
katalog = tk.Paketkatalog(paket="testpaket", ordner=str(ordner), rolle="infrastruktur", dateien=[datei])
tk.setze_referenzstatus(katalog)
status = {f.name: f.referenziert for f in katalog.quellfunktionen}
self.assertTrue(status["oeffentliche_funktion"])
self.assertTrue(status["Beispielklasse.getestete_methode"])
self.assertFalse(status["Beispielklasse.ungetestete_methode"])
self.assertFalse(status["_private_funktion"])
class ZaehleQuellfunktionenTests(unittest.TestCase):
def test_zaehlt_referenzierte_beabsichtigte_und_ungeklaerte_funktionen_korrekt(self):
"""Prüft, dass zaehle_quellfunktionen zwischen referenziert, beabsichtigt und ungeklärt unterscheidet."""
funktionen = [
tk.Quellfunktion(datei="modul.py", name="a", zeile=1, referenziert=True),
tk.Quellfunktion(datei="modul.py", name="b", zeile=2, referenziert=False, absichtlich_grund="von Test abgesehen, weil x"),
tk.Quellfunktion(datei="modul.py", name="c", zeile=3, referenziert=False),
]
ergebnis = tk.zaehle_quellfunktionen(funktionen)
self.assertEqual(
ergebnis,
{"gesamt": 3, "referenziert": 1, "ohne_bezug": 2, "absichtlich": 1, "ungeklaert": 1},
)
if __name__ == "__main__":
unittest.main()