diff --git a/changelog.txt b/changelog.txt index c6cbabb..834dd29 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- Restliche „ungeklärt"-Funktionen aus dem Prüfstand entschieden: Tests für Dateipruefer (`_ist_leer`, Priorität in `_handle_leere_eingabe`), Datenschreiber (`_store_style_in_gpkg` – Tabelle anlegen, Default-Stil zurücksetzen, Geometriespalte übernehmen) und Pruefmanager (`_handle_verfahrensgebiet_layer_existiert` „entfernen"/„ersetzen"); Linkpruefer (`_ist_url`, `_pruefe_url`, `_pruefe_dateipfad`), `Pruefmanager._handle_datei_existiert` und `Pruefmanager._ask_use_or_replace_pufferlayer` (toter Code, Bug #60) bekamen den Vermerk „von Test abgesehen" – sn_basis steht jetzt bei 0 ungeklärten Funktionen (#70) - Kategorie „vielleicht testen" abgeschlossen: 30 weitere Funktionen bekamen Tests (os_wrapper, qgiscore_wrapper, qgisui_wrapper, DataGrabber, pruef_ergebnis, Navigation, SettingsTab, PrintTab – Zustandslogik, Signal-Handling, Atlas-/Theme-Verwaltung), `PrintTab._build_ui` bekam den Vermerk „Test ausgesetzt, zunächst Refactor" (#67) - Testkatalog-Klassifizierung in Docstrings festgehalten: 43 als „vernachlässigbar" eingestufte Funktionen tragen jetzt den Vermerk „von Test abgesehen, weil vernachlässigbar", 41 Print-Bereich-Funktionen (`print_logic.py`, `print_layout.py`, `print_layout_baufreigabe.py`) den Vermerk „Test ausgesetzt, zunächst Refactor" – keine Verhaltensänderung - Tests für weitere zentrale Funktionen ergänzt: Verfahrensgebiet-Zustandsverwaltung, Dock-Verwaltung, Pruefmanager-/Dateipruefer-Entscheidungswege, Fundament-Wrapper (Pfade, Landkreise, Nachrichten, Layer-Existenz/-Stil, GPKG-Layer-Prüfung, QGIS-3/4-Geometriekompatibilität) und Projekteigenschaften-Tab (#65) diff --git a/modules/Pruefmanager.py b/modules/Pruefmanager.py index fed4e63..5dbb959 100644 --- a/modules/Pruefmanager.py +++ b/modules/Pruefmanager.py @@ -102,8 +102,11 @@ class Pruefmanager: pruef_ergebnis Ergebnis mit Aktion: - ``datei_existiert_ueberschreiben`` - - ``datei_existiert_anhaengen`` + - ``datei_existiert_anhaengen`` - ``datei_existiert_ueberspringen`` (für Cancel-Fall) + + von Test abgesehen, weil bereits über verarbeite() in test_pruefmanager.py vollständig + abgedeckt (overwrite/append/cancel/non-qgis); HD 21.09.2026 """ if self.ui_modus != "qgis": return ergebnis @@ -283,6 +286,10 @@ class Pruefmanager: ------- str "verwenden", "ersetzen" oder "abbrechen" + + von Test abgesehen, weil toter Code: referenziert self.pruefmanager, das auf + Pruefmanager-Instanzen nie existiert (AttributeError bei jedem Aufruf), bereits als + Bug in #60 erfasst; erst nach dessen Behebung testen; HD 21.09.2026 """ ergebnis = pruef_ergebnis( ok=False, diff --git a/modules/linkpruefer.py b/modules/linkpruefer.py index 1033630..c983e0d 100644 --- a/modules/linkpruefer.py +++ b/modules/linkpruefer.py @@ -41,6 +41,9 @@ class Linkpruefer: def _ist_url(self, text: str) -> bool: """ Einfache URL-Erkennung. + + von Test abgesehen, weil bereits über pruefe() in test_linkpruefer.py indirekt + abgedeckt (http-URL vs. lokaler Pfad); HD 21.09.2026 """ return text.startswith("http://") or text.startswith("https://") @@ -81,6 +84,9 @@ class Linkpruefer: def _pruefe_url(self, url: str) -> pruef_ergebnis: """ Prüft eine URL über einen HEAD-Request. + + von Test abgesehen, weil bereits über pruefe() in test_linkpruefer.py vollständig + abgedeckt (ok/Fehlerantwort/kein Netz); HD 21.09.2026 """ reply = network_head(url) @@ -115,6 +121,9 @@ class Linkpruefer: def _pruefe_dateipfad(self, eingabe: str) -> pruef_ergebnis: """ Prüft einen lokalen Pfad. + + von Test abgesehen, weil bereits über pruefe() in test_linkpruefer.py vollständig + abgedeckt (gefunden/nicht gefunden); HD 21.09.2026 """ pfad = self._pfad(eingabe) diff --git a/tests/test_dateipruefer.py b/tests/test_dateipruefer.py index 4b9f120..07134bf 100644 --- a/tests/test_dateipruefer.py +++ b/tests/test_dateipruefer.py @@ -160,6 +160,41 @@ class TestDateipruefer(unittest.TestCase): self.assertFalse(pruefer._ist_gueltiger_gpkg_pfad(Path("C:/nicht/vorhanden/verfahren.gpkg"))) + # ----------------------------------------------------- + # 10. _ist_leer: Sonderfälle jenseits von "" + # ----------------------------------------------------- + def test_ist_leer_erkennt_none_und_nicht_string_als_leer(self): + """Prüft, dass _ist_leer sowohl None als auch einen Nicht-String-Wert als leer meldet.""" + self.assertTrue(Dateipruefer(pfad=None)._ist_leer()) + self.assertTrue(Dateipruefer(pfad=123)._ist_leer()) + + def test_ist_leer_erkennt_reinen_whitespace_als_leer(self): + """Prüft, dass _ist_leer eine Eingabe aus reinen Leerzeichen als leer meldet.""" + self.assertTrue(Dateipruefer(pfad=" ")._ist_leer()) + + def test_ist_leer_meldet_pfad_mit_sichtbaren_zeichen_nicht_als_leer(self): + """Prüft, dass _ist_leer einen Pfad mit sichtbaren Zeichen nicht als leer meldet.""" + self.assertFalse(Dateipruefer(pfad=" a.gpkg ")._ist_leer()) + + # ----------------------------------------------------- + # 11. _handle_leere_eingabe: Priorität bei mehreren gesetzten Flags + # ----------------------------------------------------- + def test_leere_eingabe_bevorzugt_leereingabe_erlaubt_vor_standarddatei(self): + """Prüft, dass bei leerer Eingabe leereingabe_erlaubt Vorrang vor einer gesetzten Standarddatei hat.""" + pruefer = Dateipruefer(pfad="", leereingabe_erlaubt=True, standarddatei="/tmp/std.txt") + + result = pruefer._handle_leere_eingabe() + + self.assertEqual(result.aktion, "leereingabe_erlaubt") + + def test_leere_eingabe_bevorzugt_standarddatei_vor_temporaer_erlaubt(self): + """Prüft, dass bei leerer Eingabe eine gesetzte Standarddatei Vorrang vor temporaer_erlaubt hat.""" + pruefer = Dateipruefer(pfad="", standarddatei="/tmp/std.txt", temporaer_erlaubt=True) + + result = pruefer._handle_leere_eingabe() + + self.assertEqual(result.aktion, "standarddatei_vorschlagen") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_datenschreiber.py b/tests/test_datenschreiber.py index 7aff5a1..d00ac86 100644 --- a/tests/test_datenschreiber.py +++ b/tests/test_datenschreiber.py @@ -1,6 +1,9 @@ """Tests für sn_basis/modules/Datenschreiber.py (Schreib-/Ladepipeline, Issue #63).""" +import gc import json +import shutil +import sqlite3 import tempfile import unittest from pathlib import Path @@ -193,5 +196,103 @@ class SchreibeLogTests(unittest.TestCase): self.assertIn("standarddatei_vorschlagen", aktionen) +class StoreStyleInGpkgTests(unittest.TestCase): + """Tests für Datenschreiber._store_style_in_gpkg (Issue #70, nachgezogen aus #63).""" + + def test_legt_tabelle_an_und_speichert_stil_als_default(self): + """Prüft, dass _store_style_in_gpkg die layer_styles-Tabelle anlegt und den Stil als Default speichert.""" + tmp = tempfile.mkdtemp() + try: + gpkg_path = str(Path(tmp) / "ziel.gpkg") + style_path = Path(tmp) / "stil.qml" + style_path.write_text("Test-QML", encoding="utf-8") + schreiber, _pm = _make_schreiber(gpkg_path=gpkg_path) + + schreiber._store_style_in_gpkg("mein_layer", str(style_path)) + + with sqlite3.connect(gpkg_path) as conn: + zeile = conn.execute( + "SELECT f_table_name, styleName, styleQML, useAsDefault FROM layer_styles" + ).fetchone() + conn.close() + finally: + # _store_style_in_gpkg schließt seine eigene sqlite3-Verbindung nicht (Produktionscode, + # nicht Teil von Issue #70) – ohne gc.collect() bleibt die .gpkg-Datei unter Windows + # gesperrt und shutil.rmtree schlägt fehl. + gc.collect() + shutil.rmtree(tmp, ignore_errors=True) + + self.assertEqual(zeile[0], "mein_layer") + self.assertEqual(zeile[1], "stil.qml") + self.assertEqual(zeile[2], "Test-QML") + self.assertEqual(zeile[3], 1) + + def test_setzt_bisherigen_default_stil_desselben_layers_zurueck(self): + """Prüft, dass _store_style_in_gpkg den useAsDefault-Wert vorhandener Stile desselben Layers zurücksetzt.""" + tmp = tempfile.mkdtemp() + try: + gpkg_path = str(Path(tmp) / "ziel.gpkg") + style_path = Path(tmp) / "stil.qml" + style_path.write_text("Neuer Stil", encoding="utf-8") + schreiber, _pm = _make_schreiber(gpkg_path=gpkg_path) + + schreiber._store_style_in_gpkg("mein_layer", str(style_path)) + schreiber._store_style_in_gpkg("mein_layer", str(style_path)) + + with sqlite3.connect(gpkg_path) as conn: + werte = [ + row[0] + for row in conn.execute( + "SELECT useAsDefault FROM layer_styles WHERE f_table_name = ? ORDER BY id", + ("mein_layer",), + ).fetchall() + ] + conn.close() + finally: + gc.collect() + shutil.rmtree(tmp, ignore_errors=True) + + self.assertEqual(werte, [0, 1]) + + def test_liest_geometriespalte_ueber_geometrycolumn_aus(self): + """Prüft, dass _store_style_in_gpkg die Geometriespalte eines Layers mit geometryColumn() übernimmt.""" + + class _LayerMitGeometrieSpalte: + def geometryColumn(self) -> str: + return "geom" + + tmp = tempfile.mkdtemp() + try: + gpkg_path = str(Path(tmp) / "ziel.gpkg") + style_path = Path(tmp) / "stil.qml" + style_path.write_text("", encoding="utf-8") + schreiber, _pm = _make_schreiber(gpkg_path=gpkg_path) + + schreiber._store_style_in_gpkg("mein_layer", str(style_path), layer=_LayerMitGeometrieSpalte()) + + with sqlite3.connect(gpkg_path) as conn: + spalte = conn.execute( + "SELECT f_geometry_column FROM layer_styles WHERE f_table_name = ?", + ("mein_layer",), + ).fetchone()[0] + conn.close() + finally: + gc.collect() + shutil.rmtree(tmp, ignore_errors=True) + + self.assertEqual(spalte, "geom") + + def test_fehlender_stilpfad_meldet_fehler_ueber_pruefmanager_statt_absturz(self): + """Prüft, dass ein nicht lesbarer Stilpfad über den Pruefmanager gemeldet wird statt eine Exception zu werfen.""" + with tempfile.TemporaryDirectory() as tmp: + gpkg_path = str(Path(tmp) / "ziel.gpkg") + schreiber, pruefmanager = _make_schreiber(gpkg_path=gpkg_path) + + schreiber._store_style_in_gpkg("mein_layer", str(Path(tmp) / "nicht_vorhanden.qml")) + + aktionen = [getattr(e, "aktion", None) for e in pruefmanager.ergebnisse] + self.assertIn("style_gpkg_speichern_fehlgeschlagen", aktionen) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_pruefmanager.py b/tests/test_pruefmanager.py index ef87807..b1424f0 100644 --- a/tests/test_pruefmanager.py +++ b/tests/test_pruefmanager.py @@ -210,6 +210,24 @@ class TestPruefmanager(unittest.TestCase): self.assertFalse(entscheidung.ok) self.assertEqual(entscheidung.aktion, "verfahrensgebiet_abbrechen") + @patch("sn_basis.modules.Pruefmanager.ask_ergaenzen_entfernen_ersetzen_abbrechen", return_value="entfernen") + def test_verfahrensgebiet_layer_existiert_entfernen(self, _mock_choice): + """Prüft, dass die Nutzerwahl 'entfernen' bei bestehendem Verfahrensgebiet zur Aktion 'verfahrensgebiet_entfernen' führt.""" + ergebnis = pruef_ergebnis(False, "VG existiert", "verfahrensgebiet_layer_existiert", None) + entscheidung = self.manager._handle_verfahrensgebiet_layer_existiert(ergebnis) + + self.assertTrue(entscheidung.ok) + self.assertEqual(entscheidung.aktion, "verfahrensgebiet_entfernen") + + @patch("sn_basis.modules.Pruefmanager.ask_ergaenzen_entfernen_ersetzen_abbrechen", return_value="ersetzen") + def test_verfahrensgebiet_layer_existiert_ersetzen(self, _mock_choice): + """Prüft, dass die Nutzerwahl 'ersetzen' bei bestehendem Verfahrensgebiet zur Aktion 'verfahrensgebiet_ersetzen' führt.""" + ergebnis = pruef_ergebnis(False, "VG existiert", "verfahrensgebiet_layer_existiert", None) + entscheidung = self.manager._handle_verfahrensgebiet_layer_existiert(ergebnis) + + self.assertTrue(entscheidung.ok) + self.assertEqual(entscheidung.aktion, "verfahrensgebiet_ersetzen") + def test_verfahrensgebiet_layer_existiert_in_non_qgis_mode_returns_input(self): """Prüft, dass außerhalb des QGIS-Modus bei bestehendem Verfahrensgebiet keine Rückfrage erfolgt.""" manager = Pruefmanager(ui_modus="test")