Testkatalog: strukturelle Lückenanalyse (ungetestete Funktionen)
Ergänzt den Testkatalog um einen Abschnitt "Ungetestete Funktionen": Quellcode-Funktionen und -Methoden, deren Name in keiner Testdatei des Pakets vorkommt (Namensabgleich per ast, ohne Import). Keine Wiederaufnahme der in ADR 0009 verworfenen Zeilen-Coverage-in-Prozent, sondern eine Lückenliste nach demselben Muster wie der bestehende Abschnitt für fehlende Docstrings - entsprechend in ADR 0009 und docs/leitfaden/06_tests.md ergänzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -113,6 +113,12 @@ 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.
|
||||
|
||||
Nach dem Hinzufügen von Tests den Katalog neu erzeugen und mit committen. `python scripts/testkatalog.py --pruefen`
|
||||
meldet, wenn er veraltet ist.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+405
-6
@@ -13,15 +13,16 @@ Stand des Prüf-Audits: `docs/status/testpruefungen.json` (SHA-256 `68b590c8feef
|
||||
Gesamt: 3 Pakete, 32 Testdateien, 348 Testfälle – 305 grün, 35 rot (Baseline), 8 übersprungen.
|
||||
Mit Beschreibung (Docstring): 203 von 348 (58 %).
|
||||
Fachlich geprüft: 55 von 348 (16 %) aktuell, 0 veraltet seit Codeänderung, 293 nie geprüft.
|
||||
Strukturelle Lücken (heuristisch, Namensabgleich): 249 von 413 (60 %) Funktionen/Methoden ohne erkennbaren Testbezug – Abschnitt „Ungetestete Funktionen“.
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung | fachlich geprüft |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `sn_basis` | 15 | 96 | 96 | 0 | 0 | 96 von 96 (100 %) | 55 von 96 (57 %) |
|
||||
| `sn_verfahrensgebiet` | 12 | 208 | 166 | 34 | 8 | 106 von 208 (51 %) | 0 von 208 (0 %) |
|
||||
| `sn_plan41` | 5 | 44 | 43 | 1 | 0 | 1 von 44 (2 %) | 0 von 44 (0 %) |
|
||||
| **Summe** | **32** | **348** | **305** | **35** | **8** | **203 von 348 (58 %)** | **55 von 348 (16 %)** |
|
||||
| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung | fachlich geprüft | ohne Testbezug |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `sn_basis` | 15 | 96 | 96 | 0 | 0 | 96 von 96 (100 %) | 55 von 96 (57 %) | 176 von 229 (77 %) |
|
||||
| `sn_verfahrensgebiet` | 12 | 208 | 166 | 34 | 8 | 106 von 208 (51 %) | 0 von 208 (0 %) | 34 von 104 (33 %) |
|
||||
| `sn_plan41` | 5 | 44 | 43 | 1 | 0 | 1 von 44 (2 %) | 0 von 44 (0 %) | 39 von 80 (49 %) |
|
||||
| **Summe** | **32** | **348** | **305** | **35** | **8** | **203 von 348 (58 %)** | **55 von 348 (16 %)** | **249 von 413 (60 %)** |
|
||||
|
||||
### Legende
|
||||
|
||||
@@ -31,6 +32,7 @@ Fachlich geprüft: 55 von 348 (16 %) aktuell, 0 veraltet seit Codeänderung, 293
|
||||
- **übersprungen** – per `@unittest.skip`/`skipIf`/`skipUnless` abgeschaltet; der Grund steht dabei.
|
||||
- Ein Testfall heißt `Testgruppe.test_name`; die Testgruppe fasst zusammengehörige Testfälle einer Datei zusammen.
|
||||
- **geprüft** – Prüf-Audit (`scripts/test_audit.py`): **nie geprüft** kein Eintrag in `docs/status/testpruefungen.json`; **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.
|
||||
- **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.
|
||||
|
||||
## Paket `sn_basis`
|
||||
|
||||
@@ -980,3 +982,400 @@ Keine Lücken.
|
||||
- `TestVlnApiLogicUpload.test_upload_returns_false_without_qgis_modules`
|
||||
- `TestVlnApiLogicUpload.test_upload_calls_save_feature_collection`
|
||||
|
||||
## Ungetestete Funktionen (strukturelle Lücke)
|
||||
|
||||
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.
|
||||
|
||||
### `sn_basis` – 176 von 229 (77 %) ohne Testbezug
|
||||
|
||||
#### `functions/dialog_wrapper.py` (12)
|
||||
|
||||
- `show_info_dialog` (Zeile 42)
|
||||
- `ask_text` (Zeile 56)
|
||||
- `ask_ergaenzen_entfernen_ersetzen_abbrechen` (Zeile 131)
|
||||
- `ask_detailpruefung_oder_vg_laden` (Zeile 185)
|
||||
- `ask_detailpruefung_oder_abbrechen` (Zeile 235)
|
||||
- `ProgressDialog.set_total` (Zeile 328)
|
||||
- `ProgressDialog.set_value` (Zeile 336)
|
||||
- `ProgressDialog.set_label` (Zeile 348)
|
||||
- `ProgressDialog.is_canceled` (Zeile 357)
|
||||
- `ProgressDialog.close` (Zeile 366)
|
||||
- `ProgressDialog.raise_to_front` (Zeile 373)
|
||||
- `create_progress_dialog` (Zeile 390)
|
||||
|
||||
#### `functions/landkreise.py` (1)
|
||||
|
||||
- `_lade_landkreise` (Zeile 20)
|
||||
|
||||
#### `functions/ly_existence_wrapper.py` (2)
|
||||
|
||||
- `layer_is_valid` (Zeile 10)
|
||||
- `layer_is_usable` (Zeile 27)
|
||||
|
||||
#### `functions/ly_style_wrapper.py` (2)
|
||||
|
||||
- `apply_style_from_path` (Zeile 8)
|
||||
- `apply_style` (Zeile 27)
|
||||
|
||||
#### `functions/message_wrapper.py` (5)
|
||||
|
||||
- `_get_message_bar` (Zeile 15)
|
||||
- `push_message` (Zeile 42)
|
||||
- `info` (Zeile 67)
|
||||
- `error` (Zeile 77)
|
||||
- `success` (Zeile 82)
|
||||
|
||||
#### `functions/os_wrapper.py` (6)
|
||||
|
||||
- `normalize_path` (Zeile 43)
|
||||
- `get_home_dir` (Zeile 53)
|
||||
- `is_absolute_path` (Zeile 60)
|
||||
- `basename` (Zeile 68)
|
||||
- `is_case_sensitive_fs` (Zeile 80)
|
||||
- `path_suffix` (Zeile 96)
|
||||
|
||||
#### `functions/qgiscore_wrapper.py` (4)
|
||||
|
||||
- `get_layer_extent` (Zeile 916)
|
||||
- `create_buffer_layer` (Zeile 947)
|
||||
- `layer_exists_in_gpkg` (Zeile 1013)
|
||||
- `polygon_zu_linie` (Zeile 1052)
|
||||
|
||||
#### `functions/qgisui_wrapper.py` (9)
|
||||
|
||||
- `get_main_window` (Zeile 131)
|
||||
- `add_dock_widget` (Zeile 149)
|
||||
- `remove_dock_widget` (Zeile 156)
|
||||
- `find_dock_widgets` (Zeile 163)
|
||||
- `add_menu` (Zeile 178)
|
||||
- `remove_menu` (Zeile 188)
|
||||
- `add_toolbar` (Zeile 201)
|
||||
- `remove_toolbar` (Zeile 208)
|
||||
- `add_layer_to_project` (Zeile 217)
|
||||
|
||||
#### `functions/qt_wrapper.py` (2)
|
||||
|
||||
- `exec_dialog` (Zeile 60)
|
||||
- `debug_qt_status` (Zeile 64)
|
||||
|
||||
#### `functions/sys_wrapper.py` (5)
|
||||
|
||||
- `join_path` (Zeile 36)
|
||||
- `ensure_dir` (Zeile 56)
|
||||
- `read_text` (Zeile 69)
|
||||
- `write_text` (Zeile 79)
|
||||
- `getattr_safe` (Zeile 102)
|
||||
|
||||
#### `functions/verfahrensgebiet_manager.py` (5)
|
||||
|
||||
- `_on_layers_added` (Zeile 109)
|
||||
- `_on_layer_removed` (Zeile 114)
|
||||
- `_on_project_changed` (Zeile 123)
|
||||
- `setup_verfahrensgebiet_listener` (Zeile 132)
|
||||
- `teardown_verfahrensgebiet_listener` (Zeile 176)
|
||||
|
||||
#### `modules/DataGrabber.py` (4)
|
||||
|
||||
- `DataGrabber.set_source` (Zeile 68)
|
||||
- `DataGrabber._process_database_source` (Zeile 309)
|
||||
- `DataGrabber._process_service_source` (Zeile 320)
|
||||
- `DataGrabber._aggregate_results` (Zeile 330)
|
||||
|
||||
#### `modules/Dateipruefer.py` (5)
|
||||
|
||||
- `Dateipruefer.erkenne_dateityp` (Zeile 77)
|
||||
- `Dateipruefer._pfad` (Zeile 91)
|
||||
- `Dateipruefer._ist_leer` (Zeile 98)
|
||||
- `Dateipruefer._ist_gueltiger_gpkg_pfad` (Zeile 113)
|
||||
- `Dateipruefer._handle_leere_eingabe` (Zeile 227)
|
||||
|
||||
#### `modules/Datenabruf.py` (4)
|
||||
|
||||
- `Datenabruf.datenabruf` (Zeile 56)
|
||||
- `Datenabruf._determine_spatial_filter` (Zeile 223)
|
||||
- `Datenabruf._build_provider_url` (Zeile 278)
|
||||
- `Datenabruf._fetch_features` (Zeile 421)
|
||||
|
||||
#### `modules/Datenschreiber.py` (6)
|
||||
|
||||
- `Datenschreiber._resolve_style_path` (Zeile 62)
|
||||
- `Datenschreiber._store_style_in_gpkg` (Zeile 77)
|
||||
- `Datenschreiber.schreibe_Daten` (Zeile 152)
|
||||
- `Datenschreiber.lade_Layer` (Zeile 249)
|
||||
- `Datenschreiber.schreibe_log` (Zeile 344)
|
||||
- `Datenschreiber._write_layer_to_gpkg` (Zeile 384)
|
||||
|
||||
#### `modules/excel_importer.py` (1)
|
||||
|
||||
- `ExcelImporter.import_xlsx` (Zeile 32)
|
||||
|
||||
#### `modules/LayerLoader.py` (9)
|
||||
|
||||
- `LayerLoader._was_canceled` (Zeile 42)
|
||||
- `LayerLoader._process_events` (Zeile 50)
|
||||
- `LayerLoader._transform_geometry_to_layer_crs` (Zeile 57)
|
||||
- `LayerLoader._transform_extent_to_layer_crs` (Zeile 85)
|
||||
- `LayerLoader.create_layer` (Zeile 110)
|
||||
- `LayerLoader.apply_style` (Zeile 190)
|
||||
- `LayerLoader.filter_by_extent` (Zeile 230)
|
||||
- `LayerLoader.filter_by_layer` (Zeile 319)
|
||||
- `LayerLoader.add_to_project` (Zeile 419)
|
||||
|
||||
#### `modules/linkpruefer.py` (4)
|
||||
|
||||
- `Linkpruefer._pfad` (Zeile 33)
|
||||
- `Linkpruefer._ist_url` (Zeile 41)
|
||||
- `Linkpruefer._pruefe_url` (Zeile 81)
|
||||
- `Linkpruefer._pruefe_dateipfad` (Zeile 115)
|
||||
|
||||
#### `modules/print_layout.py` (11)
|
||||
|
||||
- `PrintLayout._create_map_item` (Zeile 98)
|
||||
- `PrintLayout._set_item_frame_stroke_width` (Zeile 119)
|
||||
- `PrintLayout._fit_extent_to_map_item` (Zeile 154)
|
||||
- `PrintLayout.add_text_label` (Zeile 211)
|
||||
- `PrintLayout._setup_layout_objects` (Zeile 242)
|
||||
- `PrintLayout._create_label_item` (Zeile 373)
|
||||
- `PrintLayout._resolve_visible_map_layers` (Zeile 470)
|
||||
- `PrintLayout._apply_grid_common_settings` (Zeile 791)
|
||||
- `PrintLayout._safe_grid_call` (Zeile 797)
|
||||
- `PrintLayout._apply_grid_annotation_text_style` (Zeile 808)
|
||||
- `PrintLayout._create_rectangle_box_item` (Zeile 908)
|
||||
|
||||
#### `modules/print_layout_baufreigabe.py` (3)
|
||||
|
||||
- `PrintLayoutBaufreigabe.create_baufreigabe_layout` (Zeile 90)
|
||||
- `PrintLayoutBaufreigabe._setup_layout_objects_baufreigabe` (Zeile 262)
|
||||
- `PrintLayoutBaufreigabe._calc_atlas_margin_percent` (Zeile 394)
|
||||
|
||||
#### `modules/print_logic.py` (27)
|
||||
|
||||
- `PrintLogic._failed_result` (Zeile 123)
|
||||
- `PrintLogic._show_target_size_too_small` (Zeile 126)
|
||||
- `PrintLogic._is_enabled_flag` (Zeile 133)
|
||||
- `PrintLogic._resolve_layer_from_project` (Zeile 138)
|
||||
- `PrintLogic._resolve_active_layer` (Zeile 158)
|
||||
- `PrintLogic._get_druckpuffer_cm` (Zeile 167)
|
||||
- `PrintLogic._get_druckpuffer_m` (Zeile 175)
|
||||
- `PrintLogic._import_qgis_symbol` (Zeile 178)
|
||||
- `PrintLogic._build_rectangle` (Zeile 185)
|
||||
- `PrintLogic._clone_extent` (Zeile 194)
|
||||
- `PrintLogic._get_existing_layer_by_name` (Zeile 260)
|
||||
- `PrintLogic._create_druckrechteck_layer` (Zeile 273)
|
||||
- `PrintLogic._clear_layer_features` (Zeile 301)
|
||||
- `PrintLogic.create_atlas_layer_from_selection` (Zeile 325)
|
||||
- `PrintLogic.configure_atlas_for_baufreigabe_layout` (Zeile 468)
|
||||
- `PrintLogic._store_druckrechteck` (Zeile 614)
|
||||
- `PrintLogic.activate_druckrechteck_capture` (Zeile 662)
|
||||
- `PrintLogic._resolve_selection_layer` (Zeile 792)
|
||||
- `PrintLogic._get_selected_extent` (Zeile 805)
|
||||
- `PrintLogic.set_kartenname_for_auswahl` (Zeile 961)
|
||||
- `PrintLogic.set_plotmassstab_for_auswahl` (Zeile 965)
|
||||
- `PrintLogic.set_view_for_auswahl` (Zeile 976)
|
||||
- `PrintLogic.set_zielgroesse_for_auswahl` (Zeile 982)
|
||||
- `PrintLogic.set_formfaktor` (Zeile 985)
|
||||
- `PrintLogic._get_plot_dimensions_mm` (Zeile 1052)
|
||||
- `PrintLogic._get_required_map_dimensions_mm` (Zeile 1061)
|
||||
- `PrintLogic.druckvorlage_anlegen` (Zeile 1161)
|
||||
|
||||
#### `modules/pruef_ergebnis.py` (3)
|
||||
|
||||
- `pruef_ergebnis.ist_verfahrens_db_aktion` (Zeile 149)
|
||||
- `pruef_ergebnis.dateipfad` (Zeile 167)
|
||||
- `pruef_ergebnis.erlaubte_persistierung` (Zeile 181)
|
||||
|
||||
#### `modules/Pruefmanager.py` (8)
|
||||
|
||||
- `Pruefmanager.report_error` (Zeile 20)
|
||||
- `Pruefmanager.report_summary` (Zeile 45)
|
||||
- `Pruefmanager.zeige_hinweis` (Zeile 62)
|
||||
- `Pruefmanager.frage_ja_nein` (Zeile 67)
|
||||
- `Pruefmanager.frage_text` (Zeile 73)
|
||||
- `Pruefmanager._handle_datei_existiert` (Zeile 83)
|
||||
- `Pruefmanager._handle_verfahrensgebiet_layer_existiert` (Zeile 230)
|
||||
- `Pruefmanager._ask_use_or_replace_pufferlayer` (Zeile 273)
|
||||
|
||||
#### `ui/base_dockwidget.py` (1)
|
||||
|
||||
- `BaseDockWidget.closeEvent` (Zeile 97)
|
||||
|
||||
#### `ui/dockmanager.py` (1)
|
||||
|
||||
- `DockManager.show` (Zeile 30)
|
||||
|
||||
#### `ui/navigation.py` (6)
|
||||
|
||||
- `Navigation.init_ui` (Zeile 33)
|
||||
- `Navigation.add_action` (Zeile 68)
|
||||
- `Navigation.finalize_menu_and_toolbar` (Zeile 81)
|
||||
- `Navigation.set_active_plugin` (Zeile 94)
|
||||
- `Navigation.remove_action` (Zeile 104)
|
||||
- `Navigation.remove_all` (Zeile 115)
|
||||
|
||||
#### `ui/tabs/print_tab.py` (27)
|
||||
|
||||
- `PrintTab.set_services` (Zeile 94)
|
||||
- `PrintTab._build_ui` (Zeile 115)
|
||||
- `PrintTab._restore_state` (Zeile 267)
|
||||
- `PrintTab._on_kartenname_changed` (Zeile 334)
|
||||
- `PrintTab._on_massstab_changed` (Zeile 347)
|
||||
- `PrintTab._on_thema_changed` (Zeile 352)
|
||||
- `PrintTab._on_zielgroesse_changed` (Zeile 357)
|
||||
- `PrintTab._on_formfaktor_changed` (Zeile 362)
|
||||
- `PrintTab._on_druckbereich_changed` (Zeile 368)
|
||||
- `PrintTab._on_atlas_erzeugen_changed` (Zeile 377)
|
||||
- `PrintTab._on_druckpuffer_clicked` (Zeile 392)
|
||||
- `PrintTab._format_druckpuffer_label` (Zeile 425)
|
||||
- `PrintTab._on_vorlage_erstellen` (Zeile 428)
|
||||
- `PrintTab._disconnect_project_signals` (Zeile 441)
|
||||
- `PrintTab.cleanup` (Zeile 459)
|
||||
- `PrintTab._connect_project_signals` (Zeile 464)
|
||||
- `PrintTab._on_project_changed` (Zeile 482)
|
||||
- `PrintTab._toggle_atlas` (Zeile 489)
|
||||
- `PrintTab._restore_atlas_layer_selection` (Zeile 495)
|
||||
- `PrintTab._disconnect_theme_collection_signals` (Zeile 519)
|
||||
- `PrintTab._connect_theme_collection_signals` (Zeile 538)
|
||||
- `PrintTab._refresh_thema_combo_live` (Zeile 565)
|
||||
- `PrintTab._get_gespeicherte_themen` (Zeile 585)
|
||||
- `PrintTab._get_current_canvas_scale` (Zeile 602)
|
||||
- `PrintTab._on_atlas_layer_changed` (Zeile 612)
|
||||
- `PrintTab._on_auswahl_as_atlas_clicked` (Zeile 641)
|
||||
- `PrintTab._populate_atlas_layer_combo` (Zeile 679)
|
||||
|
||||
#### `ui/tabs/settings_tab.py` (3)
|
||||
|
||||
- `SettingsTab._build_input` (Zeile 118)
|
||||
- `SettingsTab.save_data` (Zeile 130)
|
||||
- `SettingsTab.load_data` (Zeile 149)
|
||||
|
||||
### `sn_verfahrensgebiet` – 34 von 104 (33 %) ohne Testbezug
|
||||
|
||||
#### `functions/beteiligung_pruefung.py` (5)
|
||||
|
||||
- `_baue_nenner_kennzeichen` (Zeile 139)
|
||||
- `_hole_flurstuecke_layer` (Zeile 253)
|
||||
- `_flurstueckskennzeichen_geometrien` (Zeile 258)
|
||||
- `_try1_listenabgleich` (Zeile 279)
|
||||
- `_lese_vef_vkzf` (Zeile 380)
|
||||
|
||||
#### `functions/flurstueck_alkis.py` (1)
|
||||
|
||||
- `_vorhandene_kennzeichen` (Zeile 80)
|
||||
|
||||
#### `functions/gemarkung_alkis.py` (1)
|
||||
|
||||
- `gemarkung_schluessel_von_vg_geometrie` (Zeile 212)
|
||||
|
||||
#### `functions/grenzpunkt_alkis.py` (4)
|
||||
|
||||
- `_punktort_ta_bbox_param` (Zeile 115)
|
||||
- `_hat_geometrie` (Zeile 573)
|
||||
- `_lade_gemarkung_extents_mit_puffer` (Zeile 637)
|
||||
- `_lade_punktort_ta_batches_per_gemarkung` (Zeile 784)
|
||||
|
||||
#### `functions/knickpunkt_pruefung.py` (2)
|
||||
|
||||
- `_setze_kategorien_symbologie` (Zeile 158)
|
||||
- `erstelle_korrigiertes_polygon` (Zeile 604)
|
||||
|
||||
#### `functions/umringspruefung.py` (3)
|
||||
|
||||
- `_zerlege_in_teile` (Zeile 55)
|
||||
- `_setze_kategorien_symbologie` (Zeile 112)
|
||||
- `umringspruefung` (Zeile 152)
|
||||
|
||||
#### `functions/umringstatistik_grenzpunkte.py` (3)
|
||||
|
||||
- `_setze_kategorien_symbologie` (Zeile 74)
|
||||
- `_klassifiziere` (Zeile 119)
|
||||
- `umringstatistik_grenzpunkte` (Zeile 164)
|
||||
|
||||
#### `functions/verfahrensgebiet_alkis.py` (1)
|
||||
|
||||
- `_baue_layer_aus_kombinationen` (Zeile 144)
|
||||
|
||||
#### `functions/verfahrensgebiet_alkis_komplett.py` (2)
|
||||
|
||||
- `_entferne_hilfs_layer` (Zeile 100)
|
||||
- `_flaeche_des_layers` (Zeile 210)
|
||||
|
||||
#### `functions/verfahrensgebiet_dialoge.py` (1)
|
||||
|
||||
- `ask_landkreis_objekte` (Zeile 101)
|
||||
|
||||
#### `functions/verfahrensgebiet_shape_komplett.py` (6)
|
||||
|
||||
- `_normalisiere_feldname` (Zeile 110)
|
||||
- `_finde_fbschl_index` (Zeile 132)
|
||||
- `_ermittle_gebietstyp_aus_fbschl` (Zeile 161)
|
||||
- `_wende_vergleich_stil_an` (Zeile 211)
|
||||
- `_erstelle_vergleich_layer` (Zeile 251)
|
||||
- `_finde_alle_shp` (Zeile 342)
|
||||
|
||||
#### `ui/tabs/working_tab.py` (5)
|
||||
|
||||
- `WorkingTab.handle_beteiligung_pruefen` (Zeile 297)
|
||||
- `WorkingTab._haken_label` (Zeile 349)
|
||||
- `WorkingTab._restore_verf_layer_selection` (Zeile 374)
|
||||
- `WorkingTab._uebernimm_flurstuecke_ergebnis` (Zeile 390)
|
||||
- `WorkingTab.handle_umringstatistik` (Zeile 777)
|
||||
|
||||
### `sn_plan41` – 39 von 80 (49 %) ohne Testbezug
|
||||
|
||||
#### `modules/vln_api_client.py` (3)
|
||||
|
||||
- `KartenApiClient.load_feature_collection` (Zeile 154)
|
||||
- `KartenApiClient._request` (Zeile 176)
|
||||
- `KartenApiClient._error_detail` (Zeile 235)
|
||||
|
||||
#### `modules/vln_api_logic.py` (4)
|
||||
|
||||
- `VlnApiLogic.get_p41_layers` (Zeile 233)
|
||||
- `VlnApiLogic.remove_p41_layers` (Zeile 237)
|
||||
- `VlnApiLogic._apply_p41_styles` (Zeile 277)
|
||||
- `VlnApiLogic.upload_summary` (Zeile 347)
|
||||
|
||||
#### `modules/vln_layer_manager.py` (2)
|
||||
|
||||
- `_family` (Zeile 58)
|
||||
- `memory_layer_from_features` (Zeile 73)
|
||||
|
||||
#### `ui/dockwidget.py` (1)
|
||||
|
||||
- `DockWidget._init_tabs_with_services` (Zeile 42)
|
||||
|
||||
#### `ui/tab_a_logic.py` (15)
|
||||
|
||||
- `TabALogic._log` (Zeile 90)
|
||||
- `TabALogic._create_silent_pruefmanager` (Zeile 93)
|
||||
- `TabALogic._zeige_verfahrensgebiet_hinweis` (Zeile 127)
|
||||
- `TabALogic._close_progress_dialog` (Zeile 139)
|
||||
- `TabALogic.write_layer_to_verfahrens_db` (Zeile 169)
|
||||
- `TabALogic.is_valid_verfahrensgebiet_layer` (Zeile 242)
|
||||
- `TabALogic._load_dienste_aus_result_dict` (Zeile 391)
|
||||
- `TabALogic._create_local_layer_from_fetched_features` (Zeile 639)
|
||||
- `TabALogic._write_html_log` (Zeile 779)
|
||||
- `TabALogic._clone_layer_with_extent` (Zeile 934)
|
||||
- `TabALogic._handle_pufferlayer` (Zeile 1062)
|
||||
- `TabALogic._load_existing_pufferlayer` (Zeile 1084)
|
||||
- `TabALogic._create_pufferlayer` (Zeile 1092)
|
||||
- `TabALogic._pufferlayer_exists_in_source` (Zeile 1119)
|
||||
- `TabALogic.Pufferlayer_erstellen` (Zeile 1133)
|
||||
|
||||
#### `ui/tab_a_ui.py` (14)
|
||||
|
||||
- `TabA._build_ui` (Zeile 113)
|
||||
- `TabA._restore_state` (Zeile 274)
|
||||
- `TabA._restore_vln_state` (Zeile 296)
|
||||
- `TabA._toggle_vln_group` (Zeile 307)
|
||||
- `TabA._update_vln_ui_state` (Zeile 311)
|
||||
- `TabA._populate_verfahren_combo` (Zeile 347)
|
||||
- `TabA._on_vln_login` (Zeile 374)
|
||||
- `TabA._on_vln_logout` (Zeile 405)
|
||||
- `TabA._on_vln_verfahren_changed` (Zeile 416)
|
||||
- `TabA._on_p41_laden` (Zeile 424)
|
||||
- `TabA._on_upload_active` (Zeile 498)
|
||||
- `TabA._create_new_gpkg` (Zeile 595)
|
||||
- `TabA._update_group_color` (Zeile 608)
|
||||
- `TabA._on_load_fachdaten` (Zeile 618)
|
||||
|
||||
|
||||
+174
-5
@@ -128,6 +128,16 @@ 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
|
||||
|
||||
|
||||
@dataclass
|
||||
class Paketkatalog:
|
||||
"""Alle Testdateien eines Pakets samt Kennzahlen."""
|
||||
@@ -136,6 +146,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]:
|
||||
@@ -284,6 +295,82 @@ 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("__")
|
||||
|
||||
|
||||
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))
|
||||
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)
|
||||
)
|
||||
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]:
|
||||
return {
|
||||
"gesamt": len(funktionen),
|
||||
"referenziert": sum(1 for f in funktionen if f.referenziert),
|
||||
"ohne_bezug": sum(1 for f in funktionen if not f.referenziert),
|
||||
}
|
||||
|
||||
|
||||
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}"
|
||||
@@ -391,6 +478,7 @@ def erzeuge_markdown(
|
||||
) -> 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)
|
||||
@@ -427,24 +515,33 @@ def erzeuge_markdown(
|
||||
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 | fachlich geprüft |")
|
||||
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"{anteil(k['aktuell_geprueft'], 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['aktuell_geprueft'], summe['gesamt'])}** |"
|
||||
f"**{anteil(summe['aktuell_geprueft'], summe['gesamt'])}** | "
|
||||
f"**{anteil(summe_qf['ohne_bezug'], summe_qf['gesamt'])}** |"
|
||||
)
|
||||
z.append("")
|
||||
if summe["erwartet"]:
|
||||
@@ -463,6 +560,13 @@ def erzeuge_markdown(
|
||||
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."
|
||||
)
|
||||
z.append("")
|
||||
|
||||
for katalog in kataloge:
|
||||
@@ -527,6 +631,37 @@ def erzeuge_markdown(
|
||||
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("")
|
||||
for katalog in kataloge:
|
||||
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
|
||||
z.append(f"### `{katalog.paket}` – {anteil(qf['ohne_bezug'], qf['gesamt'])} ohne Testbezug")
|
||||
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:
|
||||
z.append(f"- `{funktion.name}` (Zeile {funktion.zeile})")
|
||||
z.append("")
|
||||
return "\n".join(z) + "\n"
|
||||
|
||||
|
||||
@@ -660,6 +795,33 @@ def _html_karte_fuer_datei(datei: Testdatei) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _html_luecken_fuer_paket(katalog: Paketkatalog) -> str:
|
||||
"""Statischer Block je Paket: Quellfunktionen ohne erkennbaren Testbezug."""
|
||||
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
|
||||
if not qf["gesamt"]:
|
||||
return ""
|
||||
luecken = [f for f in katalog.quellfunktionen if not f.referenziert]
|
||||
if not luecken:
|
||||
koerper = '<p class="empty-note">Keine Lücken.</p>'
|
||||
else:
|
||||
zeilen = "\n".join(
|
||||
f'<tr><td class="mono">{html.escape(f.datei)}</td><td class="mono">{html.escape(f.name)}</td>'
|
||||
f"<td>{f.zeile}</td></tr>"
|
||||
for f in luecken
|
||||
)
|
||||
koerper = (
|
||||
'<table class="test-table"><thead><tr><th>Datei</th><th>Funktion/Methode</th><th>Zeile</th></tr></thead>'
|
||||
f"<tbody>{zeilen}</tbody></table>"
|
||||
)
|
||||
return f"""
|
||||
<div class="luecken-block" style="margin-top:18px">
|
||||
<h3 style="font-size:15px">Ungetestete Funktionen (strukturell) – {qf['ohne_bezug']} von {qf['gesamt']}</h3>
|
||||
<p class="empty-note">Heuristik: Name kommt in keiner Testdatei des Pakets vor. Details siehe docs/tests/testkatalog.md.</p>
|
||||
{koerper}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _html_abschnitt_fuer_paket(katalog: Paketkatalog) -> str:
|
||||
k = zaehle(katalog.faelle)
|
||||
rolle = ROLLENTEXT.get(katalog.rolle, katalog.rolle or "Plugin")
|
||||
@@ -680,6 +842,7 @@ def _html_abschnitt_fuer_paket(katalog: Paketkatalog) -> str:
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-grid">{karten}</div>
|
||||
{_html_luecken_fuer_paket(katalog)}
|
||||
</section>
|
||||
"""
|
||||
|
||||
@@ -931,6 +1094,9 @@ def erzeuge_html(
|
||||
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>')
|
||||
@@ -1030,10 +1196,13 @@ def main() -> int:
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""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
|
||||
'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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_und_ungetestete_funktionen_korrekt(self):
|
||||
"""Prüft, dass zaehle_quellfunktionen Gesamtzahl, referenziert und ohne_bezug korrekt zählt."""
|
||||
funktionen = [
|
||||
tk.Quellfunktion(datei="modul.py", name="a", zeile=1, referenziert=True),
|
||||
tk.Quellfunktion(datei="modul.py", name="b", zeile=2, referenziert=False),
|
||||
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})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user