From 906740eff6508bdbe62ba4451296f3ad1d9be1ef Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 18 Sep 2026 07:37:40 +0200 Subject: [PATCH] =?UTF-8?q?Testkatalog:=20strukturelle=20L=C3=BCckenanalys?= =?UTF-8?q?e=20(ungetestete=20Funktionen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../0009-tests-definition-of-done.md | 8 + docs/leitfaden/06_tests.md | 6 + docs/tests/testkatalog.html | 268 ++++++++++++ docs/tests/testkatalog.md | 411 +++++++++++++++++- scripts/testkatalog.py | 179 +++++++- .../tests/test_testkatalog_quellfunktionen.py | 146 +++++++ 6 files changed, 1007 insertions(+), 11 deletions(-) create mode 100644 scripts/tests/test_testkatalog_quellfunktionen.py diff --git a/docs/entscheidungen/0009-tests-definition-of-done.md b/docs/entscheidungen/0009-tests-definition-of-done.md index 1210e35..d34703e 100644 --- a/docs/entscheidungen/0009-tests-definition-of-done.md +++ b/docs/entscheidungen/0009-tests-definition-of-done.md @@ -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. diff --git a/docs/leitfaden/06_tests.md b/docs/leitfaden/06_tests.md index c61fea6..3a49eb1 100644 --- a/docs/leitfaden/06_tests.md +++ b/docs/leitfaden/06_tests.md @@ -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. diff --git a/docs/tests/testkatalog.html b/docs/tests/testkatalog.html index c4d77da..aebb3bf 100644 --- a/docs/tests/testkatalog.html +++ b/docs/tests/testkatalog.html @@ -128,6 +128,7 @@
58%mit Docstring
16%aktuell geprüft
32Testdateien
+
60%ohne Testbezug
Verteilung je Paket
sn_basis15 Dateien · 96 Testfälle
@@ -477,6 +478,188 @@ classDef ok fill:#dcefdc,stroke:#3f7d4a,stroke-width:1px,color:#1c3a20; classDef warn fill:#f6ead9,stroke:#b5722a,stroke-width:1px,color:#4a2c08; classDef skip fill:#e6e6ee,stroke:#6f7285,stroke-width:1px,color:#2b2d38;
TestfallprüftStatusgeprüft
TestVerfahrensgebietManager
test_get_verfahrensgebiet_layer_id_returns_value
Prüft, dass get_verfahrensgebiet_layer_id() die in der Projektvariable gespeicherte Layer-ID liefert.grünnie geprüft
TestVerfahrensgebietManager
test_get_verfahrensgebiet_layer_id_returns_none_for_empty_value
Prüft, dass get_verfahrensgebiet_layer_id() bei leerer Projektvariable None statt eines leeren Strings liefert.grünnie geprüft
TestVerfahrensgebietManager
test_set_verfahrensgebiet_layer_id_writes_project_variable
Prüft, dass set_verfahrensgebiet_layer_id() die Layer-ID als Projektvariable 'verfahrensgebiet_layer' speichert.grünnie geprüft
TestVerfahrensgebietManager
test_update_without_layer_does_not_write_when_already_empty
Prüft, dass ohne gefundenen Verfahrensgebiet-Layer keine Schreibaktion erfolgt, wenn die Variable bereits leer ist.grünnie geprüft
TestVerfahrensgebietManager
test_update_with_layer_writes_variable
Prüft, dass ein gefundener Verfahrensgebiet-Layer seine ID als Projektvariable speichert.grünnie geprüft
+ +
+

Ungetestete Funktionen (strukturell) – 176 von 229

+

Heuristik: Name kommt in keiner Testdatei des Pakets vor. Details siehe docs/tests/testkatalog.md.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateiFunktion/MethodeZeile
functions/dialog_wrapper.pyshow_info_dialog42
functions/dialog_wrapper.pyask_text56
functions/dialog_wrapper.pyask_ergaenzen_entfernen_ersetzen_abbrechen131
functions/dialog_wrapper.pyask_detailpruefung_oder_vg_laden185
functions/dialog_wrapper.pyask_detailpruefung_oder_abbrechen235
functions/dialog_wrapper.pyProgressDialog.set_total328
functions/dialog_wrapper.pyProgressDialog.set_value336
functions/dialog_wrapper.pyProgressDialog.set_label348
functions/dialog_wrapper.pyProgressDialog.is_canceled357
functions/dialog_wrapper.pyProgressDialog.close366
functions/dialog_wrapper.pyProgressDialog.raise_to_front373
functions/dialog_wrapper.pycreate_progress_dialog390
functions/landkreise.py_lade_landkreise20
functions/ly_existence_wrapper.pylayer_is_valid10
functions/ly_existence_wrapper.pylayer_is_usable27
functions/ly_style_wrapper.pyapply_style_from_path8
functions/ly_style_wrapper.pyapply_style27
functions/message_wrapper.py_get_message_bar15
functions/message_wrapper.pypush_message42
functions/message_wrapper.pyinfo67
functions/message_wrapper.pyerror77
functions/message_wrapper.pysuccess82
functions/os_wrapper.pynormalize_path43
functions/os_wrapper.pyget_home_dir53
functions/os_wrapper.pyis_absolute_path60
functions/os_wrapper.pybasename68
functions/os_wrapper.pyis_case_sensitive_fs80
functions/os_wrapper.pypath_suffix96
functions/qgiscore_wrapper.pyget_layer_extent916
functions/qgiscore_wrapper.pycreate_buffer_layer947
functions/qgiscore_wrapper.pylayer_exists_in_gpkg1013
functions/qgiscore_wrapper.pypolygon_zu_linie1052
functions/qgisui_wrapper.pyget_main_window131
functions/qgisui_wrapper.pyadd_dock_widget149
functions/qgisui_wrapper.pyremove_dock_widget156
functions/qgisui_wrapper.pyfind_dock_widgets163
functions/qgisui_wrapper.pyadd_menu178
functions/qgisui_wrapper.pyremove_menu188
functions/qgisui_wrapper.pyadd_toolbar201
functions/qgisui_wrapper.pyremove_toolbar208
functions/qgisui_wrapper.pyadd_layer_to_project217
functions/qt_wrapper.pyexec_dialog60
functions/qt_wrapper.pydebug_qt_status64
functions/sys_wrapper.pyjoin_path36
functions/sys_wrapper.pyensure_dir56
functions/sys_wrapper.pyread_text69
functions/sys_wrapper.pywrite_text79
functions/sys_wrapper.pygetattr_safe102
functions/verfahrensgebiet_manager.py_on_layers_added109
functions/verfahrensgebiet_manager.py_on_layer_removed114
functions/verfahrensgebiet_manager.py_on_project_changed123
functions/verfahrensgebiet_manager.pysetup_verfahrensgebiet_listener132
functions/verfahrensgebiet_manager.pyteardown_verfahrensgebiet_listener176
modules/DataGrabber.pyDataGrabber.set_source68
modules/DataGrabber.pyDataGrabber._process_database_source309
modules/DataGrabber.pyDataGrabber._process_service_source320
modules/DataGrabber.pyDataGrabber._aggregate_results330
modules/Dateipruefer.pyDateipruefer.erkenne_dateityp77
modules/Dateipruefer.pyDateipruefer._pfad91
modules/Dateipruefer.pyDateipruefer._ist_leer98
modules/Dateipruefer.pyDateipruefer._ist_gueltiger_gpkg_pfad113
modules/Dateipruefer.pyDateipruefer._handle_leere_eingabe227
modules/Datenabruf.pyDatenabruf.datenabruf56
modules/Datenabruf.pyDatenabruf._determine_spatial_filter223
modules/Datenabruf.pyDatenabruf._build_provider_url278
modules/Datenabruf.pyDatenabruf._fetch_features421
modules/Datenschreiber.pyDatenschreiber._resolve_style_path62
modules/Datenschreiber.pyDatenschreiber._store_style_in_gpkg77
modules/Datenschreiber.pyDatenschreiber.schreibe_Daten152
modules/Datenschreiber.pyDatenschreiber.lade_Layer249
modules/Datenschreiber.pyDatenschreiber.schreibe_log344
modules/Datenschreiber.pyDatenschreiber._write_layer_to_gpkg384
modules/excel_importer.pyExcelImporter.import_xlsx32
modules/LayerLoader.pyLayerLoader._was_canceled42
modules/LayerLoader.pyLayerLoader._process_events50
modules/LayerLoader.pyLayerLoader._transform_geometry_to_layer_crs57
modules/LayerLoader.pyLayerLoader._transform_extent_to_layer_crs85
modules/LayerLoader.pyLayerLoader.create_layer110
modules/LayerLoader.pyLayerLoader.apply_style190
modules/LayerLoader.pyLayerLoader.filter_by_extent230
modules/LayerLoader.pyLayerLoader.filter_by_layer319
modules/LayerLoader.pyLayerLoader.add_to_project419
modules/linkpruefer.pyLinkpruefer._pfad33
modules/linkpruefer.pyLinkpruefer._ist_url41
modules/linkpruefer.pyLinkpruefer._pruefe_url81
modules/linkpruefer.pyLinkpruefer._pruefe_dateipfad115
modules/print_layout.pyPrintLayout._create_map_item98
modules/print_layout.pyPrintLayout._set_item_frame_stroke_width119
modules/print_layout.pyPrintLayout._fit_extent_to_map_item154
modules/print_layout.pyPrintLayout.add_text_label211
modules/print_layout.pyPrintLayout._setup_layout_objects242
modules/print_layout.pyPrintLayout._create_label_item373
modules/print_layout.pyPrintLayout._resolve_visible_map_layers470
modules/print_layout.pyPrintLayout._apply_grid_common_settings791
modules/print_layout.pyPrintLayout._safe_grid_call797
modules/print_layout.pyPrintLayout._apply_grid_annotation_text_style808
modules/print_layout.pyPrintLayout._create_rectangle_box_item908
modules/print_layout_baufreigabe.pyPrintLayoutBaufreigabe.create_baufreigabe_layout90
modules/print_layout_baufreigabe.pyPrintLayoutBaufreigabe._setup_layout_objects_baufreigabe262
modules/print_layout_baufreigabe.pyPrintLayoutBaufreigabe._calc_atlas_margin_percent394
modules/print_logic.pyPrintLogic._failed_result123
modules/print_logic.pyPrintLogic._show_target_size_too_small126
modules/print_logic.pyPrintLogic._is_enabled_flag133
modules/print_logic.pyPrintLogic._resolve_layer_from_project138
modules/print_logic.pyPrintLogic._resolve_active_layer158
modules/print_logic.pyPrintLogic._get_druckpuffer_cm167
modules/print_logic.pyPrintLogic._get_druckpuffer_m175
modules/print_logic.pyPrintLogic._import_qgis_symbol178
modules/print_logic.pyPrintLogic._build_rectangle185
modules/print_logic.pyPrintLogic._clone_extent194
modules/print_logic.pyPrintLogic._get_existing_layer_by_name260
modules/print_logic.pyPrintLogic._create_druckrechteck_layer273
modules/print_logic.pyPrintLogic._clear_layer_features301
modules/print_logic.pyPrintLogic.create_atlas_layer_from_selection325
modules/print_logic.pyPrintLogic.configure_atlas_for_baufreigabe_layout468
modules/print_logic.pyPrintLogic._store_druckrechteck614
modules/print_logic.pyPrintLogic.activate_druckrechteck_capture662
modules/print_logic.pyPrintLogic._resolve_selection_layer792
modules/print_logic.pyPrintLogic._get_selected_extent805
modules/print_logic.pyPrintLogic.set_kartenname_for_auswahl961
modules/print_logic.pyPrintLogic.set_plotmassstab_for_auswahl965
modules/print_logic.pyPrintLogic.set_view_for_auswahl976
modules/print_logic.pyPrintLogic.set_zielgroesse_for_auswahl982
modules/print_logic.pyPrintLogic.set_formfaktor985
modules/print_logic.pyPrintLogic._get_plot_dimensions_mm1052
modules/print_logic.pyPrintLogic._get_required_map_dimensions_mm1061
modules/print_logic.pyPrintLogic.druckvorlage_anlegen1161
modules/pruef_ergebnis.pypruef_ergebnis.ist_verfahrens_db_aktion149
modules/pruef_ergebnis.pypruef_ergebnis.dateipfad167
modules/pruef_ergebnis.pypruef_ergebnis.erlaubte_persistierung181
modules/Pruefmanager.pyPruefmanager.report_error20
modules/Pruefmanager.pyPruefmanager.report_summary45
modules/Pruefmanager.pyPruefmanager.zeige_hinweis62
modules/Pruefmanager.pyPruefmanager.frage_ja_nein67
modules/Pruefmanager.pyPruefmanager.frage_text73
modules/Pruefmanager.pyPruefmanager._handle_datei_existiert83
modules/Pruefmanager.pyPruefmanager._handle_verfahrensgebiet_layer_existiert230
modules/Pruefmanager.pyPruefmanager._ask_use_or_replace_pufferlayer273
ui/base_dockwidget.pyBaseDockWidget.closeEvent97
ui/dockmanager.pyDockManager.show30
ui/navigation.pyNavigation.init_ui33
ui/navigation.pyNavigation.add_action68
ui/navigation.pyNavigation.finalize_menu_and_toolbar81
ui/navigation.pyNavigation.set_active_plugin94
ui/navigation.pyNavigation.remove_action104
ui/navigation.pyNavigation.remove_all115
ui/tabs/print_tab.pyPrintTab.set_services94
ui/tabs/print_tab.pyPrintTab._build_ui115
ui/tabs/print_tab.pyPrintTab._restore_state267
ui/tabs/print_tab.pyPrintTab._on_kartenname_changed334
ui/tabs/print_tab.pyPrintTab._on_massstab_changed347
ui/tabs/print_tab.pyPrintTab._on_thema_changed352
ui/tabs/print_tab.pyPrintTab._on_zielgroesse_changed357
ui/tabs/print_tab.pyPrintTab._on_formfaktor_changed362
ui/tabs/print_tab.pyPrintTab._on_druckbereich_changed368
ui/tabs/print_tab.pyPrintTab._on_atlas_erzeugen_changed377
ui/tabs/print_tab.pyPrintTab._on_druckpuffer_clicked392
ui/tabs/print_tab.pyPrintTab._format_druckpuffer_label425
ui/tabs/print_tab.pyPrintTab._on_vorlage_erstellen428
ui/tabs/print_tab.pyPrintTab._disconnect_project_signals441
ui/tabs/print_tab.pyPrintTab.cleanup459
ui/tabs/print_tab.pyPrintTab._connect_project_signals464
ui/tabs/print_tab.pyPrintTab._on_project_changed482
ui/tabs/print_tab.pyPrintTab._toggle_atlas489
ui/tabs/print_tab.pyPrintTab._restore_atlas_layer_selection495
ui/tabs/print_tab.pyPrintTab._disconnect_theme_collection_signals519
ui/tabs/print_tab.pyPrintTab._connect_theme_collection_signals538
ui/tabs/print_tab.pyPrintTab._refresh_thema_combo_live565
ui/tabs/print_tab.pyPrintTab._get_gespeicherte_themen585
ui/tabs/print_tab.pyPrintTab._get_current_canvas_scale602
ui/tabs/print_tab.pyPrintTab._on_atlas_layer_changed612
ui/tabs/print_tab.pyPrintTab._on_auswahl_as_atlas_clicked641
ui/tabs/print_tab.pyPrintTab._populate_atlas_layer_combo679
ui/tabs/settings_tab.pySettingsTab._build_input118
ui/tabs/settings_tab.pySettingsTab.save_data130
ui/tabs/settings_tab.pySettingsTab.load_data149
+
+ @@ -1071,6 +1254,46 @@ classDef ok fill:#dcefdc,stroke:#3f7d4a,stroke-width:1px,color:#1c3a20; classDef warn fill:#f6ead9,stroke:#b5722a,stroke-width:1px,color:#4a2c08; classDef skip fill:#e6e6ee,stroke:#6f7285,stroke-width:1px,color:#2b2d38;
TestfallprüftStatusgeprüft
TestWorkingTab
test_set_busy_updates_button_state
— (Docstring fehlt)grünnie geprüft
TestWorkingTab
test_on_verf_layer_changed_valid_sets_variable
— (Docstring fehlt)grünnie geprüft
TestWorkingTab
test_on_verf_layer_changed_none_clears_variable
— (Docstring fehlt)grünnie geprüft
TestWorkingTab
test_handle_verf_alkis_success_path
verfahrensgebiet_alkis_komplett → 4-Tupel (enriched, flst, bror, FIRST).rotnie geprüft
TestWorkingTab
test_handle_verf_alkis_failure_clears_layer_var
Status NONE → Layer-Variable wird auf '' gesetzt.grünnie geprüft
TestWorkingTab
test_handle_flurst_alkis_first_load_uebernimmt_layer
Erstmaliges Laden über 'Flurstücke laden' fügt den Layer dem Projekt hinzu.grünnie geprüft
TestWorkingTab
test_handle_flurst_aus_gemarkungen_ruft_flurstueck_alkis_ohne_schluessel_abfrage
Der Gemarkungen-Auswahl-Button ruft dieselbe Ladefunktion mit ohne_schluessel_abfrage=True auf (#47).grünnie geprüft
TestWorkingTab
test_handle_flurst_aus_gemarkungen_abbruch_zeigt_keine_zusaetzliche_meldung
Bricht flurstueck_alkis() ab (None, NONE), zeigt der Button keine eigene Meldung – flurstueck_alkis() hat das schon getan (#47).grünnie geprüft
TestWorkingTab
test_handle_grenz_alkis_success_sets_haken
Grenzpunkte erfolgreich geladen → haken_grenz wird gesetzt.rotnie geprüft
TestWorkingTab
test_handle_grenz_alkis_none_result_sets_no_haken
grenzpunkt_alkis liefert (None, None) → kein Haken.rotnie geprüft
TestWorkingTab
test_handle_flurst_alkis_success_sets_haken_and_style
— (Docstring fehlt)grünnie geprüft
TestWorkingTab
test_handle_flurst_alkis_existing_layer_keep_path
— (Docstring fehlt)grünnie geprüft
TestWorkingTab
test_handle_flurst_alkis_invalid_input_returns_early
Status NONE (z. B. Nutzerabbruch) → kein Layer, kein Haken.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_requires_active_layer
Kein aktiver Layer → warning, kein Layer wird erstellt.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_requires_required_fields
Layer ohne Pflichtfelder → warning, kein Layer wird erstellt.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_requires_selection
Keine Flurstück-Auswahl → warning, kein Layer erstellt.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_new_layer_success
Erstlauf ohne bestehenden Layer → Layer wird erstellt und hinzugefügt.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_existing_layer_ergaenzen_dispatch
Entscheidung 'ergaenzen' → ergaenze_verfahrensgebiet wird aufgerufen.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_existing_layer_entfernen_dispatch
Entscheidung 'entfernen' → entferne_aus_verfahrensgebiet wird aufgerufen.grünnie geprüft
TestWorkingTab
test_handle_verfahrensgebiet_from_selection_existing_layer_ersetzen_dispatch
Entscheidung 'ersetzen' → alter Layer entfernt, neuer hinzugefügt.grünnie geprüft
TestWorkingTab
test_aus_liste_button_shows_not_implemented_hint
— (Docstring fehlt)grünnie geprüft
TestWorkingTab
test_handle_gem_alkis_service_reachable_adds_layer
Dienst erreichbar → Layer ins Projekt, Stil angewendet, Haken gesetzt.grünnie geprüft
TestWorkingTab
test_handle_gem_alkis_service_unreachable_shows_warning
Dienst nicht erreichbar (None) → warning, kein addMapLayer.grünnie geprüft
TestWorkingTab
test_handle_gem_alkis_layer_already_exists_shows_info
Layer 'Gemarkungen' bereits vorhanden → info, kein erneutes Laden.grünnie geprüft
TestHandleVerfShape
test_status_none_clears_layer_variable
Status NONE → Layer-Variable wird auf '' gesetzt.grünnie geprüft
TestHandleVerfShape
test_button_always_re_enabled_on_failure
Button muss nach Status NONE wieder freigegeben sein (finally).grünnie geprüft
TestHandleVerfShape
test_verfahrensnummer_nicht_gesetzt_nein_bricht_ab
Verfahrensnummer leer + Nutzer wählt 'Nein' → Vorgang abgebrochen.rotnie geprüft
TestHandleVerfShape
test_verfahrensnummer_nicht_gesetzt_ja_fuehrt_fort
Verfahrensnummer leer + Nutzer wählt 'Ja' → Pipeline wird aufgerufen.rotnie geprüft
TestHandleVerfShape
test_verfahrensnummer_gesetzt_kein_rueckfrage_dialog
Verfahrensnummer gesetzt → kein ask_yes_no-Dialog.grünnie geprüft
TestHandleVerfShape
test_success_path_applies_style_and_sets_haken
Erfolg → verfahrensgebiet.qml angewendet, haken_verf gesetzt, success aufgerufen.grünnie geprüft
TestHandleVerfShape
test_bror_layer_erhaelt_qml_stil
Wenn bror_layer zurückgegeben → BauRaumOderBodenordnungsrecht.qml angewendet.grünnie geprüft
TestHandleVerfShape
test_enriched_layer_already_in_project_not_added_again
Layer bereits im Projekt → kein erneutes addMapLayer für VG-Layer.grünnie geprüft
TestHandleVerfShape
test_busy_state_set_during_execution
Button wird während der Ausführung auf busy gesetzt.grünnie geprüft
+ +
+

Ungetestete Funktionen (strukturell) – 34 von 104

+

Heuristik: Name kommt in keiner Testdatei des Pakets vor. Details siehe docs/tests/testkatalog.md.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateiFunktion/MethodeZeile
functions/beteiligung_pruefung.py_baue_nenner_kennzeichen139
functions/beteiligung_pruefung.py_hole_flurstuecke_layer253
functions/beteiligung_pruefung.py_flurstueckskennzeichen_geometrien258
functions/beteiligung_pruefung.py_try1_listenabgleich279
functions/beteiligung_pruefung.py_lese_vef_vkzf380
functions/flurstueck_alkis.py_vorhandene_kennzeichen80
functions/gemarkung_alkis.pygemarkung_schluessel_von_vg_geometrie212
functions/grenzpunkt_alkis.py_punktort_ta_bbox_param115
functions/grenzpunkt_alkis.py_hat_geometrie573
functions/grenzpunkt_alkis.py_lade_gemarkung_extents_mit_puffer637
functions/grenzpunkt_alkis.py_lade_punktort_ta_batches_per_gemarkung784
functions/knickpunkt_pruefung.py_setze_kategorien_symbologie158
functions/knickpunkt_pruefung.pyerstelle_korrigiertes_polygon604
functions/umringspruefung.py_zerlege_in_teile55
functions/umringspruefung.py_setze_kategorien_symbologie112
functions/umringspruefung.pyumringspruefung152
functions/umringstatistik_grenzpunkte.py_setze_kategorien_symbologie74
functions/umringstatistik_grenzpunkte.py_klassifiziere119
functions/umringstatistik_grenzpunkte.pyumringstatistik_grenzpunkte164
functions/verfahrensgebiet_alkis.py_baue_layer_aus_kombinationen144
functions/verfahrensgebiet_alkis_komplett.py_entferne_hilfs_layer100
functions/verfahrensgebiet_alkis_komplett.py_flaeche_des_layers210
functions/verfahrensgebiet_dialoge.pyask_landkreis_objekte101
functions/verfahrensgebiet_shape_komplett.py_normalisiere_feldname110
functions/verfahrensgebiet_shape_komplett.py_finde_fbschl_index132
functions/verfahrensgebiet_shape_komplett.py_ermittle_gebietstyp_aus_fbschl161
functions/verfahrensgebiet_shape_komplett.py_wende_vergleich_stil_an211
functions/verfahrensgebiet_shape_komplett.py_erstelle_vergleich_layer251
functions/verfahrensgebiet_shape_komplett.py_finde_alle_shp342
ui/tabs/working_tab.pyWorkingTab.handle_beteiligung_pruefen297
ui/tabs/working_tab.pyWorkingTab._haken_label349
ui/tabs/working_tab.pyWorkingTab._restore_verf_layer_selection374
ui/tabs/working_tab.pyWorkingTab._uebernimm_flurstuecke_ergebnis390
ui/tabs/working_tab.pyWorkingTab.handle_umringstatistik777
+
+ @@ -1230,6 +1453,51 @@ classDef ok fill:#dcefdc,stroke:#3f7d4a,stroke-width:1px,color:#1c3a20; classDef warn fill:#f6ead9,stroke:#b5722a,stroke-width:1px,color:#4a2c08; classDef skip fill:#e6e6ee,stroke:#6f7285,stroke-width:1px,color:#2b2d38;
TestfallprüftStatusgeprüft
TestVlnApiLogicAvailability
test_is_available_reflects_import_state
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicAvailability
test_is_available_false_when_vln_karten_missing
Wenn VLN_KARTEN_AVAILABLE=False, liefert is_available() False.grünnie geprüft
TestVlnApiLogicCredentials
test_save_api_key_calls_qsettings
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicCredentials
test_clear_api_key_calls_remove
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicCredentials
test_load_stored_credentials_returns_api_key_and_mail
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicCredentials
test_load_stored_credentials_returns_none_for_empty
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLogin
test_login_sets_client_and_saves_key
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLogin
test_login_raises_runtime_error_without_qgis_modules
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLogin
test_login_propagates_api_error
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLogin
test_logout_clears_client
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLogin
test_is_authenticated_false_after_logout
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLogin
test_handle_session_expired_clears_key_and_client
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicVerfahren
test_get_verfahren_raises_when_not_authenticated
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicVerfahren
test_get_verfahren_delegates_to_client
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicVkz
test_load_stored_vkz_reads_from_project
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicVkz
test_load_stored_vkz_returns_none_for_empty
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicVkz
test_save_vkz_writes_to_project
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicVkz
test_save_vkz_does_nothing_for_none
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLoadP41
test_load_p41_raises_without_vln_karten
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLoadP41
test_load_p41_applies_styles_to_polygon_and_line_layers
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicLoadP41
test_load_p41_no_style_for_point_layer
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicUpload
test_upload_returns_error_for_layer_without_api_path
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicUpload
test_upload_returns_error_for_none_layer
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicUpload
test_upload_returns_false_without_qgis_modules
— (Docstring fehlt)grünnie geprüft
TestVlnApiLogicUpload
test_upload_calls_save_feature_collection
— (Docstring fehlt)grünnie geprüft
+ +
+

Ungetestete Funktionen (strukturell) – 39 von 80

+

Heuristik: Name kommt in keiner Testdatei des Pakets vor. Details siehe docs/tests/testkatalog.md.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateiFunktion/MethodeZeile
modules/vln_api_client.pyKartenApiClient.load_feature_collection154
modules/vln_api_client.pyKartenApiClient._request176
modules/vln_api_client.pyKartenApiClient._error_detail235
modules/vln_api_logic.pyVlnApiLogic.get_p41_layers233
modules/vln_api_logic.pyVlnApiLogic.remove_p41_layers237
modules/vln_api_logic.pyVlnApiLogic._apply_p41_styles277
modules/vln_api_logic.pyVlnApiLogic.upload_summary347
modules/vln_layer_manager.py_family58
modules/vln_layer_manager.pymemory_layer_from_features73
ui/dockwidget.pyDockWidget._init_tabs_with_services42
ui/tab_a_logic.pyTabALogic._log90
ui/tab_a_logic.pyTabALogic._create_silent_pruefmanager93
ui/tab_a_logic.pyTabALogic._zeige_verfahrensgebiet_hinweis127
ui/tab_a_logic.pyTabALogic._close_progress_dialog139
ui/tab_a_logic.pyTabALogic.write_layer_to_verfahrens_db169
ui/tab_a_logic.pyTabALogic.is_valid_verfahrensgebiet_layer242
ui/tab_a_logic.pyTabALogic._load_dienste_aus_result_dict391
ui/tab_a_logic.pyTabALogic._create_local_layer_from_fetched_features639
ui/tab_a_logic.pyTabALogic._write_html_log779
ui/tab_a_logic.pyTabALogic._clone_layer_with_extent934
ui/tab_a_logic.pyTabALogic._handle_pufferlayer1062
ui/tab_a_logic.pyTabALogic._load_existing_pufferlayer1084
ui/tab_a_logic.pyTabALogic._create_pufferlayer1092
ui/tab_a_logic.pyTabALogic._pufferlayer_exists_in_source1119
ui/tab_a_logic.pyTabALogic.Pufferlayer_erstellen1133
ui/tab_a_ui.pyTabA._build_ui113
ui/tab_a_ui.pyTabA._restore_state274
ui/tab_a_ui.pyTabA._restore_vln_state296
ui/tab_a_ui.pyTabA._toggle_vln_group307
ui/tab_a_ui.pyTabA._update_vln_ui_state311
ui/tab_a_ui.pyTabA._populate_verfahren_combo347
ui/tab_a_ui.pyTabA._on_vln_login374
ui/tab_a_ui.pyTabA._on_vln_logout405
ui/tab_a_ui.pyTabA._on_vln_verfahren_changed416
ui/tab_a_ui.pyTabA._on_p41_laden424
ui/tab_a_ui.pyTabA._on_upload_active498
ui/tab_a_ui.pyTabA._create_new_gpkg595
ui/tab_a_ui.pyTabA._update_group_color608
ui/tab_a_ui.pyTabA._on_load_fachdaten618
+
+
Erzeugt von python scripts/testkatalog.py aus den unittest-Suiten der sn_*-Plugins. Diagramme gruppieren Testfälle je Testdatei nach unittest.TestCase-Klasse; Farbe zeigt den Baseline-Status (grün / rot / übersprungen), nicht die fachliche Wichtigkeit.
diff --git a/docs/tests/testkatalog.md b/docs/tests/testkatalog.md index eb180e5..b46d301 100644 --- a/docs/tests/testkatalog.md +++ b/docs/tests/testkatalog.md @@ -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) + diff --git a/scripts/testkatalog.py b/scripts/testkatalog.py index 588c1a7..bfba14b 100644 --- a/scripts/testkatalog.py +++ b/scripts/testkatalog.py @@ -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 = '

Keine Lücken.

' + else: + zeilen = "\n".join( + f'{html.escape(f.datei)}{html.escape(f.name)}' + f"{f.zeile}" + for f in luecken + ) + koerper = ( + '' + f"{zeilen}
DateiFunktion/MethodeZeile
" + ) + return f""" +
+

Ungetestete Funktionen (strukturell) – {qf['ohne_bezug']} von {qf['gesamt']}

+

Heuristik: Name kommt in keiner Testdatei des Pakets vor. Details siehe docs/tests/testkatalog.md.

+ {koerper} +
+""" + + 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:
{karten}
+ {_html_luecken_fuer_paket(katalog)} """ @@ -931,6 +1094,9 @@ def erzeuge_html( teile.append(f'
{doc_pct}%mit Docstring
') teile.append(f'
{audit_pct}%aktuell geprüft
') teile.append(f'
{sum(len(k.dateien) for k in kataloge)}Testdateien
') + 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'
{qf_pct}%ohne Testbezug
') teile.append("") teile.append('
Verteilung je Paket
') @@ -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) diff --git a/scripts/tests/test_testkatalog_quellfunktionen.py b/scripts/tests/test_testkatalog_quellfunktionen.py new file mode 100644 index 0000000..dba050e --- /dev/null +++ b/scripts/tests/test_testkatalog_quellfunktionen.py @@ -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()