Merge pull request 'Tests für restliche priorisierte sn_basis-Funktionen ergänzt (#65)' (#66) from feature/65-tests-restliche-funktionen into unstable

Reviewed-on: #66
This commit was merged in pull request #66.
This commit is contained in:
2026-09-18 09:27:39 +00:00
16 changed files with 1076 additions and 0 deletions
+1
View File
@@ -1,3 +1,4 @@
- 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)
- Tests für die Kernpipeline ergänzt: Datenabruf (WFS/REST/OGR-URL-Bau, Raumfilter), Datenschreiber (GPKG-Schreiben, Laden, Log), LayerLoader (CRS-Transformation, Raum-/Geometriefilter) und excel_importer (Linklisten-Import) waren zuvor ohne Testdatei (#63)
- Bugfix: `get_plugin_root()` löst keine Symlinks/Junctions mehr auf – im per `link_qgis_profile.py` verknüpften Dev-Setup zeigte die Funktion sonst auf den Workspace-Root statt auf den QGIS-Plugin-Ordner, wodurch Assets von Fachplugins (z. B. QML-Stile in sn_verfahrensgebiet/sn_plan41) nicht gefunden wurden (Verfahrensgebiet #50)
- Bezeichner-Bereinigung (Layernamen) um Bindestrich ergänzt und in eine zentrale Funktion `sanitize_bezeichner` (`functions/text_wrapper.py`) ausgelagert, damit Umlaute/Sonderzeichen nicht mehr pro Aufrufstelle vergessen werden (#42)
+1
View File
@@ -470,6 +470,7 @@ except (ImportError, AttributeError):
def objectName(self) -> str: return self._object_name
def show(self) -> None: pass
def deleteLater(self) -> None: pass
def closeEvent(self, event) -> None: pass
QDockWidget = _MockQDockWidget
+99
View File
@@ -0,0 +1,99 @@
"""Tests für sn_basis/ui/base_dockwidget.py::closeEvent (Issue #65)."""
import unittest
from sn_basis.ui.base_dockwidget import BaseDockWidget
class _FakeTab:
def __init__(self, mit_cleanup: bool = True) -> None:
self.cleanup_aufgerufen = False
if mit_cleanup:
self.cleanup = self._cleanup
def _cleanup(self) -> None:
self.cleanup_aufgerufen = True
class _FehlerhafterTab:
def cleanup(self) -> None:
raise RuntimeError("cleanup kaputt")
class _FakeTabWidget:
def __init__(self, tabs) -> None:
self._tabs = tabs
def count(self) -> int:
return len(self._tabs)
def widget(self, index: int):
return self._tabs[index]
class _FakeAction:
def __init__(self) -> None:
self.checked = None
def setChecked(self, value: bool) -> None:
self.checked = value
def _neues_dock() -> BaseDockWidget:
"""Erzeugt ein BaseDockWidget ohne den echten __init__ (keine Qt-Tab-Erzeugung nötig für closeEvent)."""
dock = BaseDockWidget.__new__(BaseDockWidget)
dock.action = None
return dock
class CloseEventTests(unittest.TestCase):
def test_ruft_cleanup_aller_tabs_auf(self):
"""Prüft, dass closeEvent cleanup() auf allen Tabs des Dock-Widgets aufruft."""
dock = _neues_dock()
tab1, tab2 = _FakeTab(), _FakeTab()
dock.widget = lambda: _FakeTabWidget([tab1, tab2])
dock.closeEvent(None)
self.assertTrue(tab1.cleanup_aufgerufen)
self.assertTrue(tab2.cleanup_aufgerufen)
def test_ignoriert_tabs_ohne_cleanup(self):
"""Prüft, dass ein Tab ohne cleanup()-Methode nicht zu einem Fehler führt."""
dock = _neues_dock()
dock.widget = lambda: _FakeTabWidget([_FakeTab(mit_cleanup=False)])
dock.closeEvent(None) # darf keine Ausnahme werfen
def test_fehler_in_cleanup_wird_abgefangen_und_andere_tabs_laufen_weiter(self):
"""Prüft, dass ein Fehler in cleanup() eines Tabs nicht die übrigen Tabs verhindert."""
dock = _neues_dock()
funktionierender_tab = _FakeTab()
dock.widget = lambda: _FakeTabWidget([_FehlerhafterTab(), funktionierender_tab])
dock.closeEvent(None)
self.assertTrue(funktionierender_tab.cleanup_aufgerufen)
def test_setzt_toolbar_action_zurueck(self):
"""Prüft, dass closeEvent die verknüpfte Toolbar-Action auf 'nicht gedrückt' zurücksetzt."""
dock = _neues_dock()
dock.widget = lambda: _FakeTabWidget([])
action = _FakeAction()
dock.action = action
dock.closeEvent(None)
self.assertFalse(action.checked)
def test_ohne_action_wird_nichts_zurueckgesetzt(self):
"""Prüft, dass closeEvent ohne verknüpfte Toolbar-Action keine Ausnahme wirft."""
dock = _neues_dock()
dock.widget = lambda: _FakeTabWidget([])
dock.action = None
dock.closeEvent(None) # darf keine Ausnahme werfen
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -2,6 +2,7 @@
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from sn_basis.modules.Dateipruefer import Dateipruefer
@@ -111,6 +112,54 @@ class TestDateipruefer(unittest.TestCase):
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.kontext, Path("/tmp/test.txt"))
# -----------------------------------------------------
# 7. erkenne_dateityp
# -----------------------------------------------------
def test_erkenne_dateityp_unterscheidet_excel_datenbank_und_unbekannt(self):
"""Prüft, dass erkenne_dateityp .xlsx als 'excel', .gpkg/.sqlite als 'datenbank' und Rest als 'unbekannt' erkennt."""
pruefer = Dateipruefer(pfad="ignoriert")
self.assertEqual(pruefer.erkenne_dateityp(Path("liste.xlsx")), "excel")
self.assertEqual(pruefer.erkenne_dateityp(Path("daten.gpkg")), "datenbank")
self.assertEqual(pruefer.erkenne_dateityp(Path("daten.sqlite")), "datenbank")
self.assertEqual(pruefer.erkenne_dateityp(Path("text.txt")), "unbekannt")
# -----------------------------------------------------
# 8. _pfad: relativ vs. absolut
# -----------------------------------------------------
def test_pfad_verbindet_relativen_pfad_mit_basispfad(self):
"""Prüft, dass _pfad einen relativen Pfad mit dem Basisverzeichnis verbindet."""
pruefer = Dateipruefer(pfad="ignoriert", basis_pfad="C:/verfahren")
self.assertEqual(pruefer._pfad("daten.gpkg"), Path("C:/verfahren/daten.gpkg"))
def test_pfad_laesst_absoluten_pfad_unveraendert(self):
"""Prüft, dass _pfad einen bereits absoluten Pfad nicht mit dem Basisverzeichnis verbindet."""
pruefer = Dateipruefer(pfad="ignoriert", basis_pfad="C:/verfahren")
self.assertEqual(pruefer._pfad("/absolut/daten.gpkg"), Path("/absolut/daten.gpkg"))
# -----------------------------------------------------
# 9. _ist_gueltiger_gpkg_pfad
# -----------------------------------------------------
def test_ist_gueltiger_gpkg_pfad_akzeptiert_vorhandenes_verzeichnis(self):
"""Prüft, dass ein .gpkg-Pfad mit vorhandenem Zielverzeichnis als gültig gilt."""
pruefer = Dateipruefer(pfad="ignoriert")
with TemporaryDirectory() as tmp:
self.assertTrue(pruefer._ist_gueltiger_gpkg_pfad(Path(tmp) / "verfahren.gpkg"))
def test_ist_gueltiger_gpkg_pfad_lehnt_falsche_endung_ab(self):
"""Prüft, dass ein Pfad ohne .gpkg-Endung als ungültig gilt, auch bei vorhandenem Verzeichnis."""
pruefer = Dateipruefer(pfad="ignoriert")
with TemporaryDirectory() as tmp:
self.assertFalse(pruefer._ist_gueltiger_gpkg_pfad(Path(tmp) / "verfahren.txt"))
def test_ist_gueltiger_gpkg_pfad_lehnt_fehlendes_verzeichnis_ab(self):
"""Prüft, dass ein .gpkg-Pfad in einem nicht vorhandenen Verzeichnis als ungültig gilt."""
pruefer = Dateipruefer(pfad="ignoriert")
self.assertFalse(pruefer._ist_gueltiger_gpkg_pfad(Path("C:/nicht/vorhanden/verfahren.gpkg")))
if __name__ == "__main__":
unittest.main()
+97
View File
@@ -0,0 +1,97 @@
"""Tests für sn_basis/ui/dockmanager.py (nur ein Dock gleichzeitig sichtbar, Issue #65)."""
import unittest
from unittest.mock import patch
from sn_basis.ui.dockmanager import DockManager
from sn_basis.functions.qt_wrapper import DockAreaRight
class _FakeDock:
def __init__(self, name: str = "") -> None:
self._name = name
self.shown = False
def objectName(self) -> str:
return self._name
def setObjectName(self, name: str) -> None:
self._name = name
def show(self) -> None:
self.shown = True
def deleteLater(self) -> None:
return None
class DockManagerShowTests(unittest.TestCase):
@patch("sn_basis.ui.dockmanager.error")
@patch("sn_basis.ui.dockmanager.add_dock_widget")
def test_none_dock_meldet_fehler_und_zeigt_nichts_an(self, mock_add, mock_error):
"""Prüft, dass ein fehlendes Dock-Widget (None) einen Fehler meldet und nichts anzeigt."""
DockManager.show(None)
mock_error.assert_called_once()
mock_add.assert_not_called()
@patch("sn_basis.ui.dockmanager.find_dock_widgets", return_value=[])
@patch("sn_basis.ui.dockmanager.add_dock_widget")
def test_setzt_objectname_wenn_leer(self, mock_add, mock_find):
"""Prüft, dass ein Dock ohne Namen einen technischen Namen mit dem Präfix 'sn_dock_' erhält."""
dock = _FakeDock(name="")
DockManager.show(dock)
self.assertTrue(dock.objectName().startswith("sn_dock_"))
@patch("sn_basis.ui.dockmanager.remove_dock_widget")
@patch("sn_basis.ui.dockmanager.add_dock_widget")
def test_entfernt_andere_sn_docks_aber_nicht_sich_selbst(self, mock_add, mock_remove):
"""Prüft, dass andere sn_basis-Docks entfernt werden, das neue Dock und fremde Widgets aber unangetastet bleiben."""
neues_dock = _FakeDock(name="sn_dock_neu")
altes_dock = _FakeDock(name="sn_dock_alt")
fremdes_widget = _FakeDock(name="irgendein_anderes_widget")
with patch("sn_basis.ui.dockmanager.find_dock_widgets", return_value=[neues_dock, altes_dock, fremdes_widget]):
DockManager.show(neues_dock)
mock_remove.assert_called_once_with(altes_dock)
@patch("sn_basis.ui.dockmanager.find_dock_widgets", return_value=[])
@patch("sn_basis.ui.dockmanager.add_dock_widget")
def test_zeigt_neues_dock_mit_default_area_an(self, mock_add, mock_find):
"""Prüft, dass ein neues Dock mit der Standard-Area (DockAreaRight) angezeigt wird."""
dock = _FakeDock(name="sn_dock_x")
DockManager.show(dock)
mock_add.assert_called_once_with(DockAreaRight, dock)
self.assertTrue(dock.shown)
@patch("sn_basis.ui.dockmanager.warning")
@patch("sn_basis.ui.dockmanager.add_dock_widget")
@patch("sn_basis.ui.dockmanager.find_dock_widgets", side_effect=RuntimeError("kaputt"))
def test_fehler_beim_entfernen_wird_als_warnung_gemeldet(self, mock_find, mock_add, mock_warning):
"""Prüft, dass ein Fehler beim Entfernen alter Docks nur gewarnt und das neue Dock trotzdem angezeigt wird."""
dock = _FakeDock(name="sn_dock_x")
DockManager.show(dock)
mock_warning.assert_called_once()
mock_add.assert_called_once()
@patch("sn_basis.ui.dockmanager.error")
@patch("sn_basis.ui.dockmanager.add_dock_widget", side_effect=RuntimeError("kaputt"))
@patch("sn_basis.ui.dockmanager.find_dock_widgets", return_value=[])
def test_fehler_beim_anzeigen_wird_gemeldet(self, mock_find, mock_add, mock_error):
"""Prüft, dass ein Fehler beim Anzeigen des Docks als Fehler gemeldet wird."""
dock = _FakeDock(name="sn_dock_x")
DockManager.show(dock)
mock_error.assert_called_once()
if __name__ == "__main__":
unittest.main()
+28
View File
@@ -0,0 +1,28 @@
"""Tests für sn_basis/functions/landkreise.py::_lade_landkreise (Issue #65)."""
import unittest
from unittest.mock import patch
from sn_basis.functions import landkreise
class LadeLandkreiseTests(unittest.TestCase):
def test_laedt_die_echte_landkreisliste_nicht_leer(self):
"""Prüft, dass _lade_landkreise die echte landkreise.json als nicht-leere Liste von (Kennzahl, Name)-Tupeln liefert."""
ergebnis = landkreise._lade_landkreise()
self.assertTrue(ergebnis)
kennzahl, name = ergebnis[0]
self.assertIsInstance(kennzahl, str)
self.assertIsInstance(name, str)
def test_fehlende_datei_liefert_leere_liste(self):
"""Prüft, dass eine nicht lesbare Datenquelle eine leere Liste liefert, statt eine Ausnahme zu werfen."""
with patch.object(landkreise, "_DATA_DATEI", "C:/nicht/vorhanden/landkreise.json"):
ergebnis = landkreise._lade_landkreise()
self.assertEqual(ergebnis, [])
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -0,0 +1,56 @@
"""Tests für sn_basis/functions/ly_existence_wrapper.py::layer_is_valid/layer_is_usable (Issue #65)."""
import unittest
from sn_basis.functions.ly_existence_wrapper import layer_is_usable, layer_is_valid
class _FakeLayer:
def __init__(self, valid: bool = True, wirft_fehler: bool = False) -> None:
self._valid = valid
self._wirft_fehler = wirft_fehler
def isValid(self) -> bool:
if self._wirft_fehler:
raise RuntimeError("kaputt")
return self._valid
class LayerIsValidTests(unittest.TestCase):
def test_none_ist_nicht_gueltig(self):
"""Prüft, dass layer_is_valid für None False liefert."""
self.assertFalse(layer_is_valid(None))
def test_objekt_ohne_isvalid_ist_nicht_gueltig(self):
"""Prüft, dass ein Objekt ohne isValid()-Methode als ungültig gilt."""
self.assertFalse(layer_is_valid(object()))
def test_gueltiger_layer_liefert_true(self):
"""Prüft, dass ein Layer mit isValid()==True als gültig erkannt wird."""
self.assertTrue(layer_is_valid(_FakeLayer(valid=True)))
def test_ungueltiger_layer_liefert_false(self):
"""Prüft, dass ein Layer mit isValid()==False als ungültig erkannt wird."""
self.assertFalse(layer_is_valid(_FakeLayer(valid=False)))
def test_ausnahme_bei_isvalid_liefert_false(self):
"""Prüft, dass eine Ausnahme in isValid() abgefangen wird und False liefert."""
self.assertFalse(layer_is_valid(_FakeLayer(wirft_fehler=True)))
class LayerIsUsableTests(unittest.TestCase):
def test_none_ist_nicht_nutzbar(self):
"""Prüft, dass layer_is_usable für None False liefert."""
self.assertFalse(layer_is_usable(None))
def test_gueltiger_layer_ist_nutzbar(self):
"""Prüft, dass ein vorhandener, gültiger Layer als nutzbar gilt."""
self.assertTrue(layer_is_usable(_FakeLayer(valid=True)))
def test_ungueltiger_layer_ist_nicht_nutzbar(self):
"""Prüft, dass ein vorhandener, aber ungültiger Layer als nicht nutzbar gilt."""
self.assertFalse(layer_is_usable(_FakeLayer(valid=False)))
if __name__ == "__main__":
unittest.main()
+54
View File
@@ -0,0 +1,54 @@
"""Tests für sn_basis/functions/ly_style_wrapper.py::apply_style_from_path (Issue #65).
`apply_style` bleibt ausgeklammert: hardcodiert 'sn_verfahrensgebiet' in platformneutralem
sn_basis-Code (mutmaßlicher Bug, siehe docs/tests/testkatalog.md und Issue-Historie #63).
"""
import unittest
from sn_basis.functions.ly_style_wrapper import apply_style_from_path
class _FakeLayer:
def __init__(self, ergebnis=(True, ""), wirft_fehler: bool = False) -> None:
self._ergebnis = ergebnis
self._wirft_fehler = wirft_fehler
self.repaint_aufgerufen = False
def loadNamedStyle(self, path: str):
if self._wirft_fehler:
raise RuntimeError("kaputt")
return self._ergebnis
def triggerRepaint(self) -> None:
self.repaint_aufgerufen = True
class ApplyStyleFromPathTests(unittest.TestCase):
def test_fehlender_layer_liefert_false(self):
"""Prüft, dass ein fehlender Layer (None) False liefert, ohne einen Stil zu laden."""
self.assertFalse(apply_style_from_path(None, "stil.qml"))
def test_erfolgreiches_laden_liefert_true_und_triggert_repaint(self):
"""Prüft, dass ein erfolgreich geladener Stil True liefert und den Layer neu zeichnet."""
layer = _FakeLayer(ergebnis=(True, ""))
self.assertTrue(apply_style_from_path(layer, "stil.qml"))
self.assertTrue(layer.repaint_aufgerufen)
def test_fehlgeschlagenes_laden_liefert_false_ohne_repaint(self):
"""Prüft, dass ein fehlgeschlagenes Laden False liefert, ohne den Layer neu zu zeichnen."""
layer = _FakeLayer(ergebnis=(False, "ungültiges QML"))
self.assertFalse(apply_style_from_path(layer, "stil.qml"))
self.assertFalse(layer.repaint_aufgerufen)
def test_ausnahme_beim_laden_wird_abgefangen(self):
"""Prüft, dass eine Ausnahme beim Laden des Stils abgefangen wird und False liefert."""
layer = _FakeLayer(wirft_fehler=True)
self.assertFalse(apply_style_from_path(layer, "stil.qml"))
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -0,0 +1,49 @@
"""Tests für sn_basis/functions/message_wrapper.py (Issue #65)."""
import unittest
from unittest.mock import patch
from sn_basis.functions import message_wrapper
class GetMessageBarTests(unittest.TestCase):
def test_liefert_objekt_mit_pushmessage_methode(self):
"""Prüft, dass _get_message_bar ein Objekt mit aufrufbarer pushMessage()-Methode liefert."""
bar = message_wrapper._get_message_bar()
self.assertTrue(callable(getattr(bar, "pushMessage", None)))
def test_faellt_bei_fehlerhaftem_iface_auf_mock_bar_zurueck(self):
"""Prüft, dass ein Fehler beim Zugriff auf iface.messageBar() auf eine funktionierende Mock-MessageBar zurückfällt."""
class _KaputterIface:
def messageBar(self):
raise RuntimeError("keine Message-Bar verfügbar")
with patch.object(message_wrapper, "iface", _KaputterIface()):
bar = message_wrapper._get_message_bar()
ergebnis = bar.pushMessage("Titel", "Text")
self.assertEqual(ergebnis["title"], "Titel")
class PushMessageTests(unittest.TestCase):
def test_liefert_dict_mit_titel_text_level_und_dauer(self):
"""Prüft, dass push_message im Mock-Modus ein Dict mit den übergebenen Werten liefert."""
ergebnis = message_wrapper.push_message(2, "Titel", "Text", duration=7)
self.assertEqual(ergebnis, {"title": "Titel", "text": "Text", "level": 2, "duration": 7})
def test_fehler_in_der_message_bar_wird_abgefangen(self):
"""Prüft, dass eine fehlschlagende MessageBar push_message nicht zum Absturz bringt, sondern None liefert."""
class _KaputteMessageBar:
def pushMessage(self, *args, **kwargs):
raise RuntimeError("kaputt")
with patch.object(message_wrapper, "_get_message_bar", return_value=_KaputteMessageBar()):
ergebnis = message_wrapper.push_message(0, "Titel", "Text")
self.assertIsNone(ergebnis)
if __name__ == "__main__":
unittest.main()
+47
View File
@@ -0,0 +1,47 @@
"""Tests für sn_basis/modules/pruef_ergebnis.py::ist_verfahrens_db_aktion/erlaubte_persistierung (Issue #65)."""
import unittest
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class IstVerfahrensDbAktionTests(unittest.TestCase):
def test_liefert_true_fuer_alle_verfahrens_db_aktionen(self):
"""Prüft, dass ist_verfahrens_db_aktion für alle vier Verfahrens-DB-Aktionscodes True liefert."""
for aktion in (
"datei_wird_erzeugt",
"datei_existiert",
"datei_existiert_ueberschreiben",
"datei_existiert_anhaengen",
"datei_existiert_ueberspringen",
):
with self.subTest(aktion=aktion):
ergebnis = pruef_ergebnis(ok=True, aktion=aktion)
self.assertTrue(ergebnis.ist_verfahrens_db_aktion)
def test_liefert_false_fuer_andere_aktionen(self):
"""Prüft, dass ist_verfahrens_db_aktion für Nicht-Verfahrens-DB-Aktionen und ohne Aktion False liefert."""
for aktion in ("ok", "datenabruf", None):
with self.subTest(aktion=aktion):
ergebnis = pruef_ergebnis(ok=True, aktion=aktion)
self.assertFalse(ergebnis.ist_verfahrens_db_aktion)
class ErlaubtePersistierungTests(unittest.TestCase):
def test_liefert_true_fuer_erzeugen_ueberschreiben_anhaengen(self):
"""Prüft, dass erlaubte_persistierung für Erzeugen/Überschreiben/Anhängen True liefert."""
for aktion in ("datei_wird_erzeugt", "datei_existiert_ueberschreiben", "datei_existiert_anhaengen"):
with self.subTest(aktion=aktion):
ergebnis = pruef_ergebnis(ok=True, aktion=aktion)
self.assertTrue(ergebnis.erlaubte_persistierung)
def test_liefert_false_fuer_ueberspringen_und_reines_existieren(self):
"""Prüft, dass erlaubte_persistierung bei Überspringen und reinem 'datei_existiert' False liefert."""
for aktion in ("datei_existiert_ueberspringen", "datei_existiert", "ok", None):
with self.subTest(aktion=aktion):
ergebnis = pruef_ergebnis(ok=True, aktion=aktion)
self.assertFalse(ergebnis.erlaubte_persistierung)
if __name__ == "__main__":
unittest.main()
+105
View File
@@ -189,6 +189,111 @@ class TestPruefmanager(unittest.TestCase):
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "datei_existiert")
# -----------------------------------------------------
# 11. Verfahrensgebiet-Layer existiert bereits
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_ergaenzen_entfernen_ersetzen_abbrechen", return_value="ergaenzen")
def test_verfahrensgebiet_layer_existiert_ergaenzen(self, _mock_choice):
"""Prüft, dass die Nutzerwahl 'ergänzen' bei bestehendem Verfahrensgebiet zur Aktion 'verfahrensgebiet_ergaenzen' führt."""
ergebnis = pruef_ergebnis(False, "VG existiert", "verfahrensgebiet_layer_existiert", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "verfahrensgebiet_ergaenzen")
@patch("sn_basis.modules.Pruefmanager.ask_ergaenzen_entfernen_ersetzen_abbrechen", return_value="abbrechen")
def test_verfahrensgebiet_layer_existiert_abbrechen(self, _mock_choice):
"""Prüft, dass die Nutzerwahl 'abbrechen' bei bestehendem Verfahrensgebiet ok=False mit Aktion 'verfahrensgebiet_abbrechen' liefert."""
ergebnis = pruef_ergebnis(False, "VG existiert", "verfahrensgebiet_layer_existiert", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "verfahrensgebiet_abbrechen")
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")
ergebnis = pruef_ergebnis(False, "VG existiert", "verfahrensgebiet_layer_existiert", None)
entscheidung = manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "verfahrensgebiet_layer_existiert")
# -----------------------------------------------------
# 12. report_error: Einstufung nach Aktionscode
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.error")
@patch("sn_basis.modules.Pruefmanager.warning")
def test_report_error_kritische_aktion_ruft_error_auf(self, mock_warning, mock_error):
"""Prüft, dass eine kritische Aktion (z. B. 'netzwerkfehler') über error() gemeldet wird, nicht über warning()."""
self.manager.report_error("Thema", "Meldung", aktion="netzwerkfehler")
mock_error.assert_called_once_with("Thema", "Meldung")
mock_warning.assert_not_called()
@patch("sn_basis.modules.Pruefmanager.error")
@patch("sn_basis.modules.Pruefmanager.warning")
def test_report_error_warn_aktion_ruft_warning_auf(self, mock_warning, mock_error):
"""Prüft, dass eine Warn-Aktion (z. B. 'datei_nicht_gefunden') über warning() gemeldet wird, nicht über error()."""
self.manager.report_error("Thema", "Meldung", aktion="datei_nicht_gefunden")
mock_warning.assert_called_once_with("Thema", "Meldung")
mock_error.assert_not_called()
@patch("sn_basis.modules.Pruefmanager.error")
@patch("sn_basis.modules.Pruefmanager.warning")
def test_report_error_unbekannte_aktion_faellt_auf_warning_zurueck(self, mock_warning, mock_error):
"""Prüft, dass eine nicht eingestufte Aktion als Fallback über warning() gemeldet wird."""
self.manager.report_error("Thema", "Meldung", aktion="voellig_unbekannt")
mock_warning.assert_called_once_with("Thema", "Meldung")
mock_error.assert_not_called()
# -----------------------------------------------------
# 13. frage_ja_nein
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_frage_ja_nein_im_qgis_modus_fragt_nutzer(self, mock_ask):
"""Prüft, dass frage_ja_nein im QGIS-Modus den Nutzer fragt und dessen Antwort liefert."""
ergebnis = self.manager.frage_ja_nein("Titel", "Meldung", default=False)
self.assertTrue(ergebnis)
mock_ask.assert_called_once()
@patch("sn_basis.modules.Pruefmanager.ask_yes_no")
def test_frage_ja_nein_ausserhalb_qgis_liefert_default_ohne_rueckfrage(self, mock_ask):
"""Prüft, dass frage_ja_nein außerhalb des QGIS-Modus den Default-Wert liefert, ohne den Nutzer zu fragen."""
manager = Pruefmanager(ui_modus="test")
ergebnis = manager.frage_ja_nein("Titel", "Meldung", default=True)
self.assertTrue(ergebnis)
mock_ask.assert_not_called()
# -----------------------------------------------------
# 14. frage_text
# -----------------------------------------------------
@patch("sn_basis.functions.dialog_wrapper.ask_text", return_value=("Neuer Text", True))
def test_frage_text_im_qgis_modus_fragt_nutzer(self, mock_ask):
"""Prüft, dass frage_text im QGIS-Modus den Nutzer fragt und Text plus OK-Status liefert."""
text, ok = self.manager.frage_text("Titel", "Meldung", default_text="Alt")
self.assertEqual(text, "Neuer Text")
self.assertTrue(ok)
mock_ask.assert_called_once()
@patch("sn_basis.functions.dialog_wrapper.ask_text")
def test_frage_text_ausserhalb_qgis_liefert_default_ohne_rueckfrage(self, mock_ask):
"""Prüft, dass frage_text außerhalb des QGIS-Modus den Standardtext mit OK=True liefert, ohne den Nutzer zu fragen."""
manager = Pruefmanager(ui_modus="test")
text, ok = manager.frage_text("Titel", "Meldung", default_text="Standard")
self.assertEqual(text, "Standard")
self.assertTrue(ok)
mock_ask.assert_not_called()
if __name__ == "__main__":
unittest.main()
+168
View File
@@ -0,0 +1,168 @@
"""Tests für sn_basis/functions/qgiscore_wrapper.py::polygon_zu_linie/layer_exists_in_gpkg (Issue #65)."""
import sqlite3
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from sn_basis.functions import qgiscore_wrapper as qc
class _FakeGeomMitBoundary:
def __init__(self, boundary_ergebnis) -> None:
self._boundary_ergebnis = boundary_ergebnis
def isEmpty(self) -> bool:
return False
def boundary(self):
return self._boundary_ergebnis
class _FakeLinie:
def __init__(self, empty: bool = False) -> None:
self._empty = empty
def isEmpty(self) -> bool:
return self._empty
class _FakeGeomOhneBoundary:
def __init__(
self,
multipart: bool = False,
polygon=None,
multipolygon=None,
aspolygon_fehler: bool = False,
convert_ergebnis=None,
convert_fehler: bool = False,
) -> None:
self._multipart = multipart
self._polygon = polygon or []
self._multipolygon = multipolygon or []
self._aspolygon_fehler = aspolygon_fehler
self._convert_ergebnis = convert_ergebnis
self._convert_fehler = convert_fehler
def isEmpty(self) -> bool:
return False
def isMultipart(self) -> bool:
return self._multipart
def asPolygon(self):
if self._aspolygon_fehler:
raise RuntimeError("kaputt")
return self._polygon
def asMultiPolygon(self):
return self._multipolygon
def convertToType(self, typ, dest_multipart):
if self._convert_fehler:
raise RuntimeError("kaputt")
return self._convert_ergebnis
class _FakeQgsGeometryKlasse:
@staticmethod
def fromPolylineXY(ring):
return ("linie", ring)
@staticmethod
def unaryUnion(lines):
return ("union", list(lines))
class PolygonZuLinieTests(unittest.TestCase):
def test_none_liefert_none(self):
"""Prüft, dass polygon_zu_linie für None-Eingabe None liefert."""
self.assertIsNone(qc.polygon_zu_linie(None))
def test_leere_geometrie_liefert_none(self):
"""Prüft, dass eine leere Geometrie None liefert."""
self.assertIsNone(qc.polygon_zu_linie(_FakeLinie(empty=True)))
def test_stufe1_nutzt_boundary_wenn_vorhanden(self):
"""Prüft, dass eine Geometrie mit boundary() (QGIS 3.x-Pfad) deren Ergebnis direkt liefert."""
umring = _FakeLinie(empty=False)
geom = _FakeGeomMitBoundary(umring)
self.assertIs(qc.polygon_zu_linie(geom), umring)
def test_stufe2_extrahiert_ringe_bei_einfachem_polygon(self):
"""Prüft, dass ohne boundary() die Ringe eines einfachen Polygons manuell extrahiert werden."""
geom = _FakeGeomOhneBoundary(multipart=False, polygon=[["ring1"], ["ring2"]])
with patch.object(qc, "QgsGeometry", _FakeQgsGeometryKlasse):
ergebnis = qc.polygon_zu_linie(geom)
self.assertEqual(ergebnis, ("union", [("linie", ["ring1"]), ("linie", ["ring2"])]))
def test_stufe2_extrahiert_alle_ringe_bei_multipolygon(self):
"""Prüft, dass bei einem MultiPolygon die Ringe aller Teilpolygone extrahiert werden."""
geom = _FakeGeomOhneBoundary(multipart=True, multipolygon=[[["a"], ["b"]], [["c"]]])
with patch.object(qc, "QgsGeometry", _FakeQgsGeometryKlasse):
ergebnis = qc.polygon_zu_linie(geom)
self.assertEqual(len(ergebnis[1]), 3)
def test_stufe3_fallback_wenn_keine_ringe_extrahierbar(self):
"""Prüft, dass bei fehlgeschlagener Ringextraktion auf convertToType zurückgefallen wird."""
ergebnis_konvertiert = _FakeLinie(empty=False)
geom = _FakeGeomOhneBoundary(aspolygon_fehler=True, convert_ergebnis=ergebnis_konvertiert)
with patch.object(qc, "QgsGeometry", _FakeQgsGeometryKlasse):
ergebnis = qc.polygon_zu_linie(geom)
self.assertIs(ergebnis, ergebnis_konvertiert)
def test_alle_stufen_scheitern_liefert_none(self):
"""Prüft, dass polygon_zu_linie None liefert, wenn Ringextraktion und convertToType beide scheitern."""
geom = _FakeGeomOhneBoundary(aspolygon_fehler=True, convert_fehler=True)
with patch.object(qc, "QgsGeometry", _FakeQgsGeometryKlasse):
ergebnis = qc.polygon_zu_linie(geom)
self.assertIsNone(ergebnis)
class LayerExistsInGpkgTests(unittest.TestCase):
def _erzeuge_gpkg(self, tmp_dir: str, mit_layer: str | None) -> str:
pfad = str(Path(tmp_dir) / "verfahren.gpkg")
conn = sqlite3.connect(pfad)
conn.execute("CREATE TABLE gpkg_contents (table_name TEXT)")
if mit_layer:
conn.execute("INSERT INTO gpkg_contents (table_name) VALUES (?)", (mit_layer,))
conn.commit()
conn.close()
return pfad
def test_fehlende_datei_liefert_false(self):
"""Prüft, dass eine nicht vorhandene GPKG-Datei False liefert."""
self.assertFalse(qc.layer_exists_in_gpkg("C:/nicht/vorhanden.gpkg", "Flurstuecke"))
def test_leerer_pfad_oder_layername_liefert_false(self):
"""Prüft, dass ein leerer Pfad oder Layername False liefert."""
self.assertFalse(qc.layer_exists_in_gpkg("", "Flurstuecke"))
self.assertFalse(qc.layer_exists_in_gpkg("C:/tmp/x.gpkg", ""))
def test_vorhandener_layer_liefert_true(self):
"""Prüft, dass ein in gpkg_contents eingetragener Layername als vorhanden erkannt wird."""
with tempfile.TemporaryDirectory() as tmp:
pfad = self._erzeuge_gpkg(tmp, mit_layer="Flurstuecke")
self.assertTrue(qc.layer_exists_in_gpkg(pfad, "Flurstuecke"))
def test_fehlender_layer_liefert_false(self):
"""Prüft, dass ein nicht in gpkg_contents eingetragener Layername als nicht vorhanden gilt."""
with tempfile.TemporaryDirectory() as tmp:
pfad = self._erzeuge_gpkg(tmp, mit_layer=None)
self.assertFalse(qc.layer_exists_in_gpkg(pfad, "Flurstuecke"))
if __name__ == "__main__":
unittest.main()
+38
View File
@@ -0,0 +1,38 @@
"""Tests für sn_basis/functions/qgisui_wrapper.py::add_layer_to_project (Issue #65)."""
import unittest
from unittest.mock import MagicMock, patch
from sn_basis.functions import qgisui_wrapper as qui
class AddLayerToProjectTests(unittest.TestCase):
def test_none_layer_liefert_false(self):
"""Prüft, dass add_layer_to_project für einen fehlenden Layer (None) False liefert."""
self.assertFalse(qui.add_layer_to_project(None))
def test_mock_modus_liefert_true_ohne_projektzugriff(self):
"""Prüft, dass im Mock-Modus (ohne QGIS) ein gültiger Layer True liefert, ohne das Projekt anzufassen."""
with patch.object(qui, "QGIS_AVAILABLE", False):
self.assertTrue(qui.add_layer_to_project(object()))
def test_mit_qgis_fuegt_layer_dem_projekt_hinzu(self):
"""Prüft, dass bei verfügbarem QGIS der Layer über QgsProject.instance().addMapLayer hinzugefügt wird."""
layer = object()
mock_project = MagicMock()
with patch.object(qui, "QGIS_AVAILABLE", True), patch.object(qui, "QgsProject", mock_project):
ergebnis = qui.add_layer_to_project(layer)
self.assertTrue(ergebnis)
mock_project.instance.return_value.addMapLayer.assert_called_once_with(layer)
def test_mit_qgis_fehler_liefert_false(self):
"""Prüft, dass ein Fehler beim Hinzufügen zum Projekt abgefangen wird und False liefert."""
mock_project = MagicMock()
mock_project.instance.side_effect = RuntimeError("kaputt")
with patch.object(qui, "QGIS_AVAILABLE", True), patch.object(qui, "QgsProject", mock_project):
self.assertFalse(qui.add_layer_to_project(object()))
if __name__ == "__main__":
unittest.main()
+117
View File
@@ -0,0 +1,117 @@
"""Tests für sn_basis/ui/tabs/settings_tab.py::save_data/load_data (Issue #65)."""
import unittest
from sn_basis.functions.qt_wrapper import QComboBox, QLineEdit
from sn_basis.ui.tabs.settings_tab import SettingsTab
class _FakeLogic:
def __init__(self, daten=None) -> None:
self.gespeichert = None
self._daten = daten or {}
def save(self, fields) -> None:
self.gespeichert = fields
def load(self) -> dict:
return self._daten
def _neuer_tab(logic, user_inputs=None, project_inputs=None) -> SettingsTab:
"""Erzeugt einen SettingsTab ohne den echten __init__ (der Qt-Widgets aufbaut)."""
tab = SettingsTab.__new__(SettingsTab)
tab.logic = logic
tab.user_inputs = user_inputs or {}
tab.project_inputs = project_inputs or {}
return tab
def _combo_mit_landkreisen() -> QComboBox:
combo = QComboBox()
combo.addItem("", None)
combo.addItem("Landkreis A", "01")
combo.addItem("Landkreis B", "02")
return combo
class SaveDataTests(unittest.TestCase):
def test_sammelt_text_und_combobox_werte_inklusive_landkreiskennzahl(self):
"""Prüft, dass save_data Text- und Dropdown-Werte sammelt und die Landkreiskennzahl separat ergänzt."""
amt_feld = QLineEdit()
amt_feld.setText("Meißen")
landkreis_combo = _combo_mit_landkreisen()
landkreis_combo.setCurrentIndex(1) # "Landkreis A" / "01"
name_feld = QLineEdit()
name_feld.setText("Verfahren X")
logic = _FakeLogic()
tab = _neuer_tab(
logic,
user_inputs={"amt": amt_feld, "landkreis_user": landkreis_combo},
project_inputs={"name": name_feld},
)
tab.save_data()
self.assertEqual(
logic.gespeichert,
{"amt": "Meißen", "landkreis_user": "Landkreis A", "name": "Verfahren X", "landkreis_kennzahl": "01"},
)
def test_ohne_landkreis_dropdown_wird_keine_kennzahl_ergaenzt(self):
"""Prüft, dass ohne Landkreis-Dropdown im Formular keine 'landkreis_kennzahl' ergänzt wird."""
logic = _FakeLogic()
tab = _neuer_tab(logic, user_inputs={"amt": QLineEdit()}, project_inputs={})
tab.save_data()
self.assertNotIn("landkreis_kennzahl", logic.gespeichert)
class LoadDataTests(unittest.TestCase):
def test_befuellt_textfelder_aus_gespeicherten_daten(self):
"""Prüft, dass load_data ein Textfeld mit dem gespeicherten Wert befüllt."""
amt_feld = QLineEdit()
tab = _neuer_tab(_FakeLogic({"amt": "Dresden"}), user_inputs={"amt": amt_feld}, project_inputs={})
tab.load_data()
self.assertEqual(amt_feld.text(), "Dresden")
def test_ohne_gespeicherten_wert_bleibt_textfeld_leer(self):
"""Prüft, dass load_data ohne gespeicherten Wert das Textfeld auf einen leeren Text setzt."""
amt_feld = QLineEdit()
tab = _neuer_tab(_FakeLogic({}), user_inputs={"amt": amt_feld}, project_inputs={})
tab.load_data()
self.assertEqual(amt_feld.text(), "")
def test_waehlt_combobox_eintrag_bei_bekanntem_namen(self):
"""Prüft, dass load_data im Landkreis-Dropdown den gespeicherten Landkreisnamen auswählt."""
combo = _combo_mit_landkreisen()
tab = _neuer_tab(
_FakeLogic({"landkreis_user": "Landkreis B"}), user_inputs={"landkreis_user": combo}, project_inputs={}
)
tab.load_data()
self.assertEqual(combo.currentText(), "Landkreis B")
def test_unbekannter_gespeicherter_name_waehlt_ersten_eintrag(self):
"""Prüft, dass load_data bei einem nicht mehr vorhandenen Landkreisnamen auf den ersten Eintrag zurückfällt."""
combo = _combo_mit_landkreisen()
tab = _neuer_tab(
_FakeLogic({"landkreis_user": "Nicht mehr vorhanden"}),
user_inputs={"landkreis_user": combo},
project_inputs={},
)
tab.load_data()
self.assertEqual(combo.currentText(), "")
if __name__ == "__main__":
unittest.main()
+42
View File
@@ -52,5 +52,47 @@ class TestGetPluginRoot(unittest.TestCase):
self.assertEqual(ergebnis, verlinkter_ordner)
class JoinPathTests(unittest.TestCase):
def test_verbindet_mehrere_pfadbestandteile(self):
"""Prüft, dass join_path mehrere Bestandteile OS-sicher zu einem Path zusammenfügt."""
ergebnis = sys_wrapper.join_path("a", "b", "c.txt")
self.assertEqual(ergebnis, Path("a") / "b" / "c.txt")
class EnsureDirTests(unittest.TestCase):
def test_legt_fehlendes_verzeichnis_inklusive_eltern_an(self):
"""Prüft, dass ensure_dir ein fehlendes Verzeichnis samt Elternverzeichnissen anlegt."""
with TemporaryDirectory() as tmp:
ziel = Path(tmp) / "a" / "b" / "c"
ergebnis = sys_wrapper.ensure_dir(ziel)
self.assertTrue(ziel.is_dir())
self.assertEqual(ergebnis, ziel)
def test_vorhandenes_verzeichnis_wirft_keinen_fehler(self):
"""Prüft, dass ensure_dir bei einem bereits vorhandenen Verzeichnis keinen Fehler wirft."""
with TemporaryDirectory() as tmp:
sys_wrapper.ensure_dir(Path(tmp)) # darf nicht scheitern
class GetattrSafeTests(unittest.TestCase):
def test_liefert_vorhandenes_attribut(self):
"""Prüft, dass getattr_safe ein vorhandenes Attribut liefert."""
class _Beispiel:
wert = 42
self.assertEqual(sys_wrapper.getattr_safe(_Beispiel(), "wert"), 42)
def test_liefert_default_bei_fehlendem_attribut(self):
"""Prüft, dass getattr_safe bei fehlendem Attribut den Default-Wert liefert."""
class _Beispiel:
pass
self.assertEqual(sys_wrapper.getattr_safe(_Beispiel(), "fehlt", "ersatz"), "ersatz")
self.assertIsNone(sys_wrapper.getattr_safe(_Beispiel(), "fehlt"))
if __name__ == "__main__":
unittest.main()
+125
View File
@@ -1,7 +1,10 @@
"""Tests für sn_basis/functions/verfahrensgebiet_manager.py (zentrale Layer-Verfolgung, Issue #65)."""
import unittest
from unittest.mock import patch
from sn_basis.functions import verfahrensgebiet_manager
import sn_basis.functions.verfahrensgebiet_manager as vgm
class _Scope:
@@ -69,5 +72,127 @@ class TestVerfahrensgebietManager(unittest.TestCase):
mock_set_variable.assert_called_once_with("verfahrensgebiet_layer", "layer_99", scope="project")
class _FakeSignal:
def __init__(self) -> None:
self.connected: list = []
def connect(self, fn) -> None:
self.connected.append(fn)
def disconnect(self, fn) -> None:
self.connected.remove(fn)
class _FakeLayer:
def __init__(self, layer_id: str, name: str = "Verfahrensgebiet") -> None:
self._id = layer_id
self._name = name
def id(self) -> str:
return self._id
class _FakeProject:
def __init__(self, layers=None) -> None:
self.layersAdded = _FakeSignal()
self.layerRemoved = _FakeSignal()
self.readProject = _FakeSignal()
self.newProjectCreated = _FakeSignal()
self.cleared = _FakeSignal()
self._layers = layers or []
def mapLayersByName(self, name: str):
return [l for l in self._layers if l._name == name]
def _mit_projekt(projekt: _FakeProject):
return patch.object(vgm, "QgsProject", type("_P", (), {"instance": staticmethod(lambda: projekt)}))
class OnHandlerTests(unittest.TestCase):
def setUp(self) -> None:
patch.object(vgm, "QGIS_AVAILABLE", True).start()
self.addCleanup(patch.stopall)
vgm.set_variable("verfahrensgebiet_layer", "", scope="project")
def test_on_project_changed_ohne_layer_setzt_variable_leer(self):
"""Prüft, dass _on_project_changed die Variable leert, wenn kein Verfahrensgebiet-Layer existiert."""
vgm.set_variable("verfahrensgebiet_layer", "alter_layer", scope="project")
with _mit_projekt(_FakeProject(layers=[])):
vgm._on_project_changed()
self.assertIsNone(vgm.get_verfahrensgebiet_layer_id())
def test_on_project_changed_mit_layer_setzt_variable(self):
"""Prüft, dass _on_project_changed die Layer-ID eines gefundenen Verfahrensgebiet-Layers übernimmt."""
with _mit_projekt(_FakeProject(layers=[_FakeLayer("layer123")])):
vgm._on_project_changed()
self.assertEqual(vgm.get_verfahrensgebiet_layer_id(), "layer123")
def test_on_layers_added_findet_neuen_layer(self):
"""Prüft, dass _on_layers_added nach neu hinzugefügten Layern sucht und die Variable aktualisiert."""
with _mit_projekt(_FakeProject(layers=[_FakeLayer("layer456")])):
vgm._on_layers_added(["irrelevant"])
self.assertEqual(vgm.get_verfahrensgebiet_layer_id(), "layer456")
@patch.object(vgm, "_update_verfahrensgebiet_from_project")
def test_on_layer_removed_bei_aktuellem_layer_sucht_neu(self, mock_update):
"""Prüft, dass das Entfernen des aktuell aktiven Verfahrensgebiet-Layers eine Neusuche auslöst."""
vgm.set_variable("verfahrensgebiet_layer", "layer123", scope="project")
vgm._on_layer_removed("layer123")
mock_update.assert_called_once()
@patch.object(vgm, "_update_verfahrensgebiet_from_project")
def test_on_layer_removed_bei_anderem_layer_aendert_nichts(self, mock_update):
"""Prüft, dass das Entfernen eines anderen Layers keine Neusuche auslöst."""
vgm.set_variable("verfahrensgebiet_layer", "layer123", scope="project")
vgm._on_layer_removed("ganz_anderer_layer")
mock_update.assert_not_called()
class ListenerSetupTests(unittest.TestCase):
def setUp(self) -> None:
patch.object(vgm, "_listener_installed", False).start()
self.addCleanup(patch.stopall)
def test_setup_ohne_qgis_tut_nichts(self):
"""Prüft, dass setup_verfahrensgebiet_listener ohne QGIS keine Signale registriert."""
with patch.object(vgm, "QGIS_AVAILABLE", False):
vgm.setup_verfahrensgebiet_listener()
self.assertFalse(vgm._listener_installed)
def test_setup_verbindet_signale_und_ist_idempotent(self):
"""Prüft, dass setup_verfahrensgebiet_listener die Projektsignale verbindet und ein zweiter Aufruf nichts doppelt registriert."""
projekt = _FakeProject(layers=[])
with patch.object(vgm, "QGIS_AVAILABLE", True), _mit_projekt(projekt):
vgm.setup_verfahrensgebiet_listener()
vgm.setup_verfahrensgebiet_listener()
self.assertEqual(len(projekt.layersAdded.connected), 1)
self.assertEqual(len(projekt.layerRemoved.connected), 1)
self.assertEqual(len(projekt.readProject.connected), 1)
self.assertTrue(vgm._listener_installed)
def test_teardown_trennt_alle_signale_und_ist_idempotent(self):
"""Prüft, dass teardown_verfahrensgebiet_listener alle Signale trennt und mehrfacher Aufruf nicht fehlschlägt."""
projekt = _FakeProject(layers=[])
with patch.object(vgm, "QGIS_AVAILABLE", True), _mit_projekt(projekt):
vgm.setup_verfahrensgebiet_listener()
vgm.teardown_verfahrensgebiet_listener()
vgm.teardown_verfahrensgebiet_listener()
self.assertEqual(projekt.layersAdded.connected, [])
self.assertEqual(projekt.layerRemoved.connected, [])
self.assertEqual(projekt.readProject.connected, [])
self.assertFalse(vgm._listener_installed)
if __name__ == "__main__":
unittest.main()