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:
2026-09-18 07:37:40 +02:00
co-authored by Claude Sonnet 5
parent e5e1b6e768
commit 906740eff6
6 changed files with 1007 additions and 11 deletions
@@ -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 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 Wissensbasis sonst vermeidet); Testabdeckung in Prozent als Ziel (misst durchlaufene Zeilen, nicht fachliche
Vollständigkeit, und verleitet zu Tests ohne Aussage). 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.
+6
View File
@@ -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 Abschnitt „Lücken" am Ende listet alle Tests ohne Docstring — das ist die Arbeitsliste, um den Katalog
vollständig lesbar zu machen. 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` Nach dem Hinzufügen von Tests den Katalog neu erzeugen und mit committen. `python scripts/testkatalog.py --pruefen`
meldet, wenn er veraltet ist. meldet, wenn er veraltet ist.
File diff suppressed because one or more lines are too long
+405 -6
View File
@@ -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. Gesamt: 3 Pakete, 32 Testdateien, 348 Testfälle – 305 grün, 35 rot (Baseline), 8 übersprungen.
Mit Beschreibung (Docstring): 203 von 348 (58 %). Mit Beschreibung (Docstring): 203 von 348 (58 %).
Fachlich geprüft: 55 von 348 (16 %) aktuell, 0 veraltet seit Codeänderung, 293 nie geprüft. 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 ## Zusammenfassung
| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung | fachlich geprüft | | 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 %) | | `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 %) | | `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 %) | | `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 %)** | | **Summe** | **32** | **348** | **305** | **35** | **8** | **203 von 348 (58 %)** | **55 von 348 (16 %)** | **249 von 413 (60 %)** |
### Legende ### 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. - **ü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. - 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. - **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` ## Paket `sn_basis`
@@ -980,3 +982,400 @@ Keine Lücken.
- `TestVlnApiLogicUpload.test_upload_returns_false_without_qgis_modules` - `TestVlnApiLogicUpload.test_upload_returns_false_without_qgis_modules`
- `TestVlnApiLogicUpload.test_upload_calls_save_feature_collection` - `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
View File
@@ -128,6 +128,16 @@ class Testdatei:
return [f for g in self.gruppen for f in g.faelle] 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 @dataclass
class Paketkatalog: class Paketkatalog:
"""Alle Testdateien eines Pakets samt Kennzahlen.""" """Alle Testdateien eines Pakets samt Kennzahlen."""
@@ -136,6 +146,7 @@ class Paketkatalog:
ordner: str ordner: str
rolle: str rolle: str
dateien: list[Testdatei] = field(default_factory=list) dateien: list[Testdatei] = field(default_factory=list)
quellfunktionen: list[Quellfunktion] = field(default_factory=list)
@property @property
def faelle(self) -> list[Testfall]: def faelle(self) -> list[Testfall]:
@@ -284,6 +295,82 @@ def sammle_paket(paket: str, repo: dict) -> Paketkatalog | None:
return katalog 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: def voller_testpfad(datei: Testdatei, fall: Testfall) -> str:
"""Voller Schlüssel eines Testfalls – identisch zum Format in testbaseline.json/testpruefungen.json.""" """Voller Schlüssel eines Testfalls – identisch zum Format in testbaseline.json/testpruefungen.json."""
return f"{datei.modulpfad}.{fall.bezeichnung}" return f"{datei.modulpfad}.{fall.bezeichnung}"
@@ -391,6 +478,7 @@ def erzeuge_markdown(
) -> str: ) -> str:
alle = [f for k in kataloge for f in k.faelle] alle = [f for k in kataloge for f in k.faelle]
summe = zaehle(alle) 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) baseline_anzeige = baseline_pfad.relative_to(ROOT).as_posix() if baseline_pfad.is_relative_to(ROOT) else str(baseline_pfad)
pruefungen_anzeige = ( pruefungen_anzeige = (
pruefungen_pfad.relative_to(ROOT).as_posix() if pruefungen_pfad.is_relative_to(ROOT) else str(pruefungen_pfad) 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"Fachlich geprüft: {anteil(summe['aktuell_geprueft'], summe['gesamt'])} aktuell, "
f"{summe['veraltet_geprueft']} veraltet seit Codeänderung, {summe['nie_geprueft']} nie geprüft." 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("")
z.append("## Zusammenfassung") z.append("## Zusammenfassung")
z.append("") 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: for katalog in kataloge:
k = zaehle(katalog.faelle) k = zaehle(katalog.faelle)
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
z.append( z.append(
f"| `{katalog.paket}` | {len(katalog.dateien)} | {k['gesamt']} | {k['gruen']} | " 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'])} |" f"{anteil(k['aktuell_geprueft'], k['gesamt'])} | {anteil(qf['ohne_bezug'], qf['gesamt'])} |"
) )
z.append( z.append(
f"| **Summe** | **{sum(len(k.dateien) for k in kataloge)}** | **{summe['gesamt']}** | " f"| **Summe** | **{sum(len(k.dateien) for k in kataloge)}** | **{summe['gesamt']}** | "
f"**{summe['gruen']}** | **{summe['rot']}** | **{summe['uebersprungen']}** | " 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['aktuell_geprueft'], summe['gesamt'])}** | "
f"**{anteil(summe_qf['ohne_bezug'], summe_qf['gesamt'])}** |"
) )
z.append("") z.append("")
if summe["erwartet"]: 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 " 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." "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("") z.append("")
for katalog in kataloge: for katalog in kataloge:
@@ -527,6 +631,37 @@ def erzeuge_markdown(
for fall in luecken: for fall in luecken:
z.append(f"- `{fall.bezeichnung}`") z.append(f"- `{fall.bezeichnung}`")
z.append("") 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" 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: def _html_abschnitt_fuer_paket(katalog: Paketkatalog) -> str:
k = zaehle(katalog.faelle) k = zaehle(katalog.faelle)
rolle = ROLLENTEXT.get(katalog.rolle, katalog.rolle or "Plugin") rolle = ROLLENTEXT.get(katalog.rolle, katalog.rolle or "Plugin")
@@ -680,6 +842,7 @@ def _html_abschnitt_fuer_paket(katalog: Paketkatalog) -> str:
</div> </div>
</div> </div>
<div class="card-grid">{karten}</div> <div class="card-grid">{karten}</div>
{_html_luecken_fuer_paket(katalog)}
</section> </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">{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">{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>') 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>")
teile.append('<div class="pkg-bars"><div class="pkg-bars-title">Verteilung je Paket</div>') teile.append('<div class="pkg-bars"><div class="pkg-bars-title">Verteilung je Paket</div>')
@@ -1030,10 +1196,13 @@ def main() -> int:
continue continue
setze_status(katalog, rot_je_paket.get(paket, set())) setze_status(katalog, rot_je_paket.get(paket, set()))
setze_pruefstatus(katalog, pruefungen) setze_pruefstatus(katalog, pruefungen)
setze_referenzstatus(katalog)
k = zaehle(katalog.faelle) k = zaehle(katalog.faelle)
qf = zaehle_quellfunktionen(katalog.quellfunktionen)
print(f"[{paket}] {len(katalog.dateien)} Testdateien, {k['gesamt']} Testfälle " print(f"[{paket}] {len(katalog.dateien)} Testdateien, {k['gesamt']} Testfälle "
f"({k['gruen']} grün, {k['rot']} rot, {k['uebersprungen']} übersprungen, " 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: if not katalog.dateien:
print(f" Keine Datei tests/test_*.py gefunden.") print(f" Keine Datei tests/test_*.py gefunden.")
kataloge.append(katalog) 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()