"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_edit_service.py Unit-Tests für das Verfahrensgebiet-Edit-Service-Modul. Getestet werden: - pruefe_flurstueck_layer (über Layerpruefer-Integration) - pruefe_auswahl - erstelle_verfahrensgebiet_aus_auswahl: Erfolgs- und Fehlerpfade - ergaenze_verfahrensgebiet: nur neue IDs werden hinzugefügt - entferne_aus_verfahrensgebiet: nur vorhandene IDs werden entfernt - ersetze_verfahrensgebiet: Ergebnis entspricht Erstellen - get_kennzeichen_menge: Hilfsfunktion - Zähler (erfolgreich/fehlgeschlagen/bereits_vorhanden/nicht_enthalten) """ import sys import types import unittest from unittest.mock import patch, MagicMock # --------------------------------------------------------------------------- # Minimales QGIS-Mock-Setup (muss vor allen sn_*-Importen erfolgen) # --------------------------------------------------------------------------- if "qgis" not in sys.modules: qgis_mod = types.ModuleType("qgis") qgis_core = types.ModuleType("qgis.core") qgis_utils = types.ModuleType("qgis.utils") qgis_utils.iface = types.SimpleNamespace() qgis_utils.plugins = {} qgis_mod.utils = qgis_utils qgis_mod.core = qgis_core sys.modules["qgis"] = qgis_mod sys.modules["qgis.core"] = qgis_core sys.modules["qgis.utils"] = qgis_utils from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import ( pruefe_flurstueck_layer, pruefe_auswahl, erstelle_verfahrensgebiet_aus_auswahl, ergaenze_verfahrensgebiet, entferne_aus_verfahrensgebiet, ersetze_verfahrensgebiet, get_kennzeichen_menge, FELD_KENNZEICHEN, FELD_GEMARKUNG, ) # --------------------------------------------------------------------------- # Test-Dummies # --------------------------------------------------------------------------- class _DummyGeom: def __init__(self, empty=False): self._empty = empty def isEmpty(self): return self._empty def difference(self, other): """Dummy-Differenz: immer nicht-leeres Ergebnis (kein echter Geometrieschnitt).""" return _DummyGeom(empty=False) class _DummyFeature: """Feature-Dummy mit dict-artigem Attributzugriff und konfigurierbarer Geometrie.""" def __init__(self, kz: str, gemarkung: str = "1234", geom_empty: bool = False, feat_id: int = 0): self._attrs = {FELD_KENNZEICHEN: kz, FELD_GEMARKUNG: gemarkung} self._geom_empty = geom_empty self._id = feat_id self._set_geom = None self._set_attrs = None def __getitem__(self, key): return self._attrs.get(key, None) def geometry(self): return _DummyGeom(self._geom_empty) def id(self): return self._id def setGeometry(self, g): self._set_geom = g def setAttributes(self, attrs): self._set_attrs = attrs class _DummyProvider: """DataProvider-Dummy für Single-Feature-Verfahrensgebiet-Layer.""" def __init__(self, layer_ref): self._layer = layer_ref def addAttributes(self, attrs): pass def addFeatures(self, feats): self._layer._features.extend(feats) return True def deleteFeatures(self, ids): self._layer._deleted_ids.extend(ids) return True def changeAttributeValues(self, changes: dict): """changes: {fid: {field_idx: value}}""" for feat in self._layer._features: fid = feat.id() if callable(getattr(feat, "id", None)) else None if fid in changes: for field_idx, value in changes[fid].items(): feat._attr_by_idx[field_idx] = value return True def changeGeometryValues(self, changes: dict): """changes: {fid: new_geom}""" for feat in self._layer._features: fid = feat.id() if callable(getattr(feat, "id", None)) else None if fid in changes: feat._set_geom = changes[fid] return True class _DummySingleFeature: """Single-Feature-Dummy für den Verfahrensgebiet-Layer. Speichert Kennzeichen als kommagetrennte Zeichenkette (new-style). Feld-Indizes: 0=FELD_KENNZEICHEN, 1=FELD_GEMARKUNG """ def __init__(self, kz_komma: str, gemarkung: str = "", feat_id: int = 0, geom_empty: bool = False): self._kz_komma = kz_komma self._gemarkung = gemarkung self._id = feat_id self._geom_empty = geom_empty self._set_geom = None # Attribut-Mapping nach Index für changeAttributeValues self._attr_by_idx: dict = {} def __getitem__(self, key): if key == FELD_KENNZEICHEN: return self._attr_by_idx.get(0, self._kz_komma) if key == FELD_GEMARKUNG: return self._attr_by_idx.get(1, self._gemarkung) return None def geometry(self): return _DummyGeom(self._geom_empty) def id(self): return self._id def setGeometry(self, g): self._set_geom = g def setAttributes(self, attrs): if len(attrs) > 0: self._kz_komma = attrs[0] if len(attrs) > 1: self._gemarkung = attrs[1] class _DummyVerfLayer: """Memory-Layer-Dummy für Verfahrensgebiet (Single-Feature-Modell).""" def __init__(self, single_feature=None): self._features = [single_feature] if single_feature else [] self._deleted_ids = [] def getFeatures(self): deleted = set(self._deleted_ids) return [ f for f in self._features if (f.id() if callable(getattr(f, "id", None)) else None) not in deleted ] def dataProvider(self): return _DummyProvider(self) def fields(self): class _Fields: def indexFromName(self, name): if name == FELD_KENNZEICHEN: return 0 if name == FELD_GEMARKUNG: return 1 return -1 return _Fields() def updateExtents(self): pass def triggerRepaint(self): pass def updateFields(self): pass def isValid(self): return True def crs(self): class _Crs: def authid(self): return "EPSG:25833" return _Crs() class _DummySourceLayer: """Flurstück-Quell-Layer-Dummy mit konfigurierbarer Auswahl.""" def __init__(self, selected=None, all_features=None, feature_count=None): self._selected = list(selected or []) self._all = list(all_features or []) self._count = feature_count if feature_count is not None else len(self._selected) def selectedFeatures(self): return self._selected def selectedFeatureCount(self): return self._count def getFeatures(self): return self._all def crs(self): class _Crs: def authid(self): return "EPSG:25833" return _Crs() class _MockLayerpruefer: """Steuerbarer Mock für Layerpruefer.pruefe().""" def __init__(self, ok, aktion="ok", meldung=""): self._ok = ok self._aktion = aktion self._meldung = meldung def pruefe(self): from sn_basis.modules.pruef_ergebnis import pruef_ergebnis return pruef_ergebnis(ok=self._ok, aktion=self._aktion, meldung=self._meldung) # --------------------------------------------------------------------------- # Tests: pruefe_flurstueck_layer # --------------------------------------------------------------------------- class TestPruefeFlurstueckLayer(unittest.TestCase): def test_layer_ok_passes(self): """Gültiger Polygon-Vektor-Layer mit Pflichtfeldern → ok=True.""" import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module original = module.Layerpruefer class _PassPruefer: def __init__(self, **_): pass def pruefe(self): from sn_basis.modules.pruef_ergebnis import pruef_ergebnis return pruef_ergebnis(ok=True, aktion="ok") module.Layerpruefer = _PassPruefer try: ergebnis = pruefe_flurstueck_layer(object()) self.assertTrue(ergebnis.ok) self.assertEqual(ergebnis.aktion, "ok") finally: module.Layerpruefer = original def test_layer_missing_field_fails(self): """Layer ohne Pflichtfelder → ok=False, aktion=felder_fehlen.""" import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module original = module.Layerpruefer class _FailPruefer: def __init__(self, **_): pass def pruefe(self): from sn_basis.modules.pruef_ergebnis import pruef_ergebnis return pruef_ergebnis(ok=False, aktion="felder_fehlen", meldung="Feld fehlt") module.Layerpruefer = _FailPruefer try: ergebnis = pruefe_flurstueck_layer(object()) self.assertFalse(ergebnis.ok) self.assertEqual(ergebnis.aktion, "felder_fehlen") finally: module.Layerpruefer = original def test_layer_wrong_geometry_fails(self): """Layer mit falschem Geometrietyp → ok=False, aktion=falscher_geotyp.""" import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module original = module.Layerpruefer class _WrongGeomPruefer: def __init__(self, **_): pass def pruefe(self): from sn_basis.modules.pruef_ergebnis import pruef_ergebnis return pruef_ergebnis(ok=False, aktion="falscher_geotyp", meldung="Kein Polygon") module.Layerpruefer = _WrongGeomPruefer try: ergebnis = pruefe_flurstueck_layer(object()) self.assertFalse(ergebnis.ok) self.assertEqual(ergebnis.aktion, "falscher_geotyp") finally: module.Layerpruefer = original # --------------------------------------------------------------------------- # Tests: pruefe_auswahl # --------------------------------------------------------------------------- class TestPruefeAuswahl(unittest.TestCase): def test_no_selection_fails(self): layer = _DummySourceLayer(selected=[], feature_count=0) ergebnis = pruefe_auswahl(layer) self.assertFalse(ergebnis.ok) self.assertEqual(ergebnis.aktion, "layer_leer") def test_with_selection_passes(self): feats = [_DummyFeature("1000-001")] layer = _DummySourceLayer(selected=feats) ergebnis = pruefe_auswahl(layer) self.assertTrue(ergebnis.ok) self.assertIn("1", ergebnis.meldung) # "1 Flurstücke ausgewählt." def test_selectedFeatureCount_exception_treated_as_zero(self): class _BrokenLayer: def selectedFeatureCount(self): raise RuntimeError("broken") ergebnis = pruefe_auswahl(_BrokenLayer()) self.assertFalse(ergebnis.ok) # --------------------------------------------------------------------------- # Tests: get_kennzeichen_menge # --------------------------------------------------------------------------- class TestGetKennzeichenMenge(unittest.TestCase): def test_returns_set_of_ids(self): """Kommagetrennte Kennzeichen im Single-Feature werden korrekt geparst.""" feat = _DummySingleFeature("A001,A002,A001", feat_id=1) # Duplikat → set layer = _DummyVerfLayer(feat) result = get_kennzeichen_menge(layer) self.assertEqual(result, {"A001", "A002"}) def test_empty_layer_returns_empty_set(self): layer = _DummyVerfLayer() result = get_kennzeichen_menge(layer) self.assertEqual(result, set()) def test_single_kennzeichen_without_comma(self): """Einzelnes Kennzeichen ohne Komma wird korrekt erkannt.""" feat = _DummySingleFeature("X999", feat_id=1) layer = _DummyVerfLayer(feat) result = get_kennzeichen_menge(layer) self.assertEqual(result, {"X999"}) # --------------------------------------------------------------------------- # Tests: erstelle_verfahrensgebiet_aus_auswahl # --------------------------------------------------------------------------- class TestErstelle(unittest.TestCase): def _patch_all(self, test_func, union_empty=False): """Patcht _erstelle_memory_layer und QgsGeometry.unaryUnion.""" import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module orig_erstelle = module._erstelle_memory_layer orig_union = module.QgsGeometry.unaryUnion mem = _DummyVerfLayer() module._erstelle_memory_layer = lambda _crs: mem module.QgsGeometry.unaryUnion = staticmethod( lambda _geoms: _DummyGeom(empty=union_empty) ) try: test_func(mem) finally: module._erstelle_memory_layer = orig_erstelle module.QgsGeometry.unaryUnion = staticmethod(orig_union) def test_success_counts(self): """3 gültige Features → erfolgreich=3, fehlgeschlagen=0, 1 Union-Feature im Layer.""" feats = [ _DummyFeature("A001", feat_id=0), _DummyFeature("A002", feat_id=1), _DummyFeature("A003", feat_id=2), ] source = _DummySourceLayer(selected=feats) def run(mem): layer, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source) self.assertIs(layer, mem) self.assertEqual(ok, 3) self.assertEqual(fail, 0) # Single-Feature-Modell: genau 1 Union-Feature self.assertEqual(len(mem._features), 1) self._patch_all(run) def test_partial_failure_on_empty_geometry(self): """2 ok + 1 leere Geometrie → erfolgreich=2, fehlgeschlagen=1.""" feats = [ _DummyFeature("A001"), _DummyFeature("A002", geom_empty=True), _DummyFeature("A003"), ] source = _DummySourceLayer(selected=feats) def run(mem): _, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source) self.assertEqual(ok, 2) self.assertEqual(fail, 1) self._patch_all(run) def test_all_fail_on_empty_geometries(self): """Alle Features haben leere Geometrie → erfolgreich=0, kein Feature im Layer.""" feats = [_DummyFeature("A001", geom_empty=True)] source = _DummySourceLayer(selected=feats) def run(mem): _, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source) self.assertEqual(ok, 0) self.assertEqual(fail, 1) self.assertEqual(len(mem._features), 0) # kein Union-Feature self._patch_all(run) # --------------------------------------------------------------------------- # Tests: ergaenze_verfahrensgebiet # --------------------------------------------------------------------------- class TestErgaenzeVerfahrensgebiet(unittest.TestCase): def _patch_union(self): import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module orig = module.QgsGeometry.unaryUnion module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False)) return module, orig def test_adds_only_new_ids(self): """Auswahl enthält 1 neue + 1 bereits vorhandene ID → hinzugefügt=1.""" verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1)) selected = [ _DummyFeature("A001"), # bereits vorhanden _DummyFeature("A002"), # neu ] source = _DummySourceLayer(selected=selected) module, orig = self._patch_union() try: _, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet( verf, source ) finally: module.QgsGeometry.unaryUnion = staticmethod(orig) self.assertEqual(hinzugefuegt, 1) self.assertEqual(bereits_vorhanden, 1) self.assertEqual(fehlgeschlagen, 0) def test_skip_empty_geometry_counts_as_failed(self): """Neue ID mit leerer Geometrie → fehlgeschlagen=1, hinzugefügt=0.""" verf = _DummyVerfLayer() # leer selected = [_DummyFeature("A001", geom_empty=True)] source = _DummySourceLayer(selected=selected) _, hinzugefuegt, _, fehlgeschlagen = ergaenze_verfahrensgebiet(verf, source) self.assertEqual(hinzugefuegt, 0) self.assertEqual(fehlgeschlagen, 1) def test_all_already_present(self): """Alle ausgewählten IDs bereits vorhanden → bereits_vorhanden=n, hinzugefügt=0.""" verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1)) selected = [_DummyFeature("A001"), _DummyFeature("A002")] source = _DummySourceLayer(selected=selected) _, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet( verf, source ) self.assertEqual(hinzugefuegt, 0) self.assertEqual(bereits_vorhanden, 2) self.assertEqual(fehlgeschlagen, 0) # --------------------------------------------------------------------------- # Tests: entferne_aus_verfahrensgebiet # --------------------------------------------------------------------------- class TestEntferneAusVerfahrensgebiet(unittest.TestCase): def test_removes_present_ids(self): """Auswahl enthält 1 vorhandene ID → entfernt=1, nicht_enthalten=0.""" verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1)) selected = [_DummyFeature("A001")] source = _DummySourceLayer(selected=selected) _, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet( verf, source ) self.assertEqual(entfernt, 1) self.assertEqual(nicht_enthalten, 0) self.assertEqual(fehlgeschlagen, 0) def test_counts_ids_not_in_layer(self): """Auswahl enthält ID, die nicht im Verfahrensgebiet ist → nicht_enthalten=1.""" verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1)) selected = [ _DummyFeature("A001"), # vorhanden _DummyFeature("A999"), # nicht enthalten ] source = _DummySourceLayer(selected=selected) _, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet( verf, source ) self.assertEqual(entfernt, 1) self.assertEqual(nicht_enthalten, 1) def test_none_present_in_layer(self): """Auswahl hat IDs, die alle nicht im Layer sind → entfernt=0, nicht_enthalten=n.""" verf = _DummyVerfLayer(_DummySingleFeature("X001", feat_id=1)) selected = [_DummyFeature("A999"), _DummyFeature("B888")] source = _DummySourceLayer(selected=selected) _, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet( verf, source ) self.assertEqual(entfernt, 0) self.assertEqual(nicht_enthalten, 2) def test_empty_selection_removes_nothing(self): """Leere Auswahl → nichts entfernt, nichts nicht_enthalten.""" verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1)) source = _DummySourceLayer(selected=[]) _, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet( verf, source ) self.assertEqual(entfernt, 0) self.assertEqual(nicht_enthalten, 0) def test_removes_last_id_deletes_feature(self): """Wird das letzte Kennzeichen entfernt, wird das Feature gelöscht.""" verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1)) selected = [_DummyFeature("A001")] source = _DummySourceLayer(selected=selected) _, entfernt, _, _ = entferne_aus_verfahrensgebiet(verf, source) self.assertEqual(entfernt, 1) # Feature soll gelöscht worden sein self.assertIn(1, verf._deleted_ids) # --------------------------------------------------------------------------- # Tests: ersetze_verfahrensgebiet # --------------------------------------------------------------------------- class TestErsetze(unittest.TestCase): def test_ersetze_delegates_to_erstelle(self): """ersetze_verfahrensgebiet gibt dasselbe Ergebnis wie erstelle zurück.""" feats = [_DummyFeature("A001")] source = _DummySourceLayer(selected=feats) import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module orig_erstelle = module._erstelle_memory_layer orig_union = module.QgsGeometry.unaryUnion mem = _DummyVerfLayer() module._erstelle_memory_layer = lambda _crs: mem module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False)) try: layer, ok, fail = ersetze_verfahrensgebiet(source) self.assertIs(layer, mem) self.assertEqual(ok, 1) self.assertEqual(fail, 0) finally: module._erstelle_memory_layer = orig_erstelle module.QgsGeometry.unaryUnion = staticmethod(orig_union) if __name__ == "__main__": unittest.main()