Files
Plugin_SN_Verfahrensgebiet/tests/test_verfahrensgebiet_edit_service.py
T
2026-04-21 21:50:26 +02:00

596 lines
21 KiB
Python

"""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()