Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e570d4524b | ||
|
|
693e453fc6 | ||
|
|
6966a67bd1 | ||
|
|
1c4da60845 | ||
|
|
1435b157b5 | ||
|
|
decd2a0002 | ||
|
|
e747b2227b | ||
|
|
3a872737a7 | ||
|
|
94ab55923d | ||
|
|
a6dfbefbd4 | ||
|
|
159efc27a7 | ||
|
|
8f54251eaf | ||
|
|
b20503d7b6 | ||
|
|
049a5ffe1d | ||
|
|
cd11e409a3 | ||
|
|
cb29620f6e | ||
|
|
3f985ce209 | ||
|
|
b47f740c10 | ||
|
|
1359ba2761 | ||
|
|
906740eff6 | ||
|
|
e5e1b6e768 | ||
|
|
4d036516a1 | ||
|
|
4f87684224 | ||
|
|
a2467d3431 | ||
|
|
6b687c34d2 | ||
|
|
fe55d5b914 | ||
|
|
0f8f80a71d | ||
|
|
18bb17ac57 | ||
|
|
aa63f234e7 | ||
|
|
3a07785a66 | ||
|
|
78105fcd64 | ||
|
|
f2eb883162 | ||
|
|
4fb90fba31 | ||
|
|
accf1bf7e8 | ||
|
|
ec400ae59e | ||
|
|
5dfec4f934 | ||
|
|
d9d2542a20 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+873
-71
File diff suppressed because one or more lines are too long
+1185
-547
File diff suppressed because it is too large
Load Diff
@@ -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`)
|
||||
|
||||
@@ -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
@@ -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 (<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:
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user