Flst_Laden, VerfGebiet bilden

This commit is contained in:
2026-04-21 21:50:26 +02:00
parent 72c9d6fa15
commit 189855e63b
8 changed files with 2247 additions and 124 deletions
+223
View File
@@ -0,0 +1,223 @@
"""sn_verfahrensgebiet/tests/test_flurstueck_alkis.py
Unit-Tests für das flurstueck_alkis-Modul.
Getestet werden:
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
- Ungültiger WFS-Layer → (None, LoadStatus.NONE)
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD
- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP
"""
import sys
import types
import unittest
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Minimales QGIS-Mock-Setup
# ---------------------------------------------------------------------------
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.flurstueck_alkis import flurstueck_alkis, FLST_LAYER_NAME
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyLayer:
def __init__(self, valid: bool, layer_id="L1"):
self._valid = valid
self._id = layer_id
def isValid(self):
return self._valid
def id(self):
return self._id
def extent(self):
return object()
class _DummyProject:
def __init__(self, named_layers=None):
self._named_layers = named_layers or []
self.removed = []
def mapLayersByName(self, name):
return self._named_layers
def removeMapLayer(self, lid):
self.removed.append(lid)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ #
# 1. Nutzerabbruch (ask_text liefert ok=False / None) #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=False, text="" → (None, NONE)."""
mock_ask_text.return_value = ("", False)
mock_project.return_value = _DummyProject()
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=True, aber Text leer → (None, NONE)."""
mock_ask_text.return_value = (" ", True)
mock_project.return_value = _DummyProject()
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# 2. Ungültiger WFS-Layer #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_invalid_wfs_layer_returns_none(
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
):
"""WFS-Layer nicht valide → (None, NONE)."""
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# 3. Erfolgreicher Erstladevorgang #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_success_returns_layer_and_first_status(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""Kein bestehender Layer, WFS valide → (layer, FIRST)."""
mock_ask_text.return_value = ("1234, 5678", True)
mock_project.return_value = _DummyProject()
valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok")
mock_wfs_layer_cls.return_value = valid_layer
layer, status = flurstueck_alkis(None)
self.assertIs(layer, valid_layer)
self.assertEqual(status, LoadStatus.FIRST)
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_sql_filter_contains_all_schluessel(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel."""
mock_ask_text.return_value = ("1111, 2222, 3333", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None)
# Erster Positionsarg von QgsVectorLayer ist der URI-String
call_args = mock_wfs_layer_cls.call_args
uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "")
self.assertIn("1111", uri_str)
self.assertIn("2222", uri_str)
self.assertIn("3333", uri_str)
# ------------------------------------------------------------------ #
# 4. Bestehender Layer – Nutzer wählt "Neu laden" #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_existing_layer_reload_yes(
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_wfs_layer_cls
):
"""Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD."""
existing = _DummyLayer(valid=True, layer_id="old_flst")
project = _DummyProject(named_layers=[existing])
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = project
# StandardButton als int, damit | Operator funktioniert
mock_msgbox_cls.StandardButton.Yes = 0x4000
mock_msgbox_cls.StandardButton.No = 0x10000
# QMessageBox.question → Yes
mock_msgbox_cls.question.return_value = 0x4000
new_layer = _DummyLayer(valid=True, layer_id="new_flst")
mock_wfs_layer_cls.return_value = new_layer
layer, status = flurstueck_alkis(None)
self.assertIn(existing.id(), project.removed)
self.assertIs(layer, new_layer)
self.assertEqual(status, LoadStatus.RELOAD)
# ------------------------------------------------------------------ #
# 5. Bestehender Layer – Nutzer wählt "Behalten" #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_existing_layer_keep_no(
self, mock_ask_text, mock_project, mock_msgbox_cls
):
"""Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP."""
existing = _DummyLayer(valid=True, layer_id="keep_flst")
project = _DummyProject(named_layers=[existing])
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = project
# StandardButton als int, damit | Operator funktioniert
mock_msgbox_cls.StandardButton.Yes = 0x4000
mock_msgbox_cls.StandardButton.No = 0x10000
# QMessageBox.question → No
mock_msgbox_cls.question.return_value = 0x10000
layer, status = flurstueck_alkis(None)
self.assertIs(layer, existing)
self.assertEqual(status, LoadStatus.KEEP)
self.assertEqual(project.removed, []) # nichts gelöscht
if __name__ == "__main__":
unittest.main()
+595
View File
@@ -0,0 +1,595 @@
"""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()
+283
View File
@@ -82,9 +82,17 @@ class TestWorkingTab(unittest.TestCase):
def _make_tab(self):
tab = WorkingTab.__new__(WorkingTab)
tab.btn_verf_alkis = _DummyButton()
tab.btn_flurst_alkis = _DummyButton()
tab.btn_verf_aus_auswahl = _DummyButton()
tab.btn_verf_aus_liste = _DummyButton()
tab.verf_layer_combo = _DummyCombo()
tab.haken_verf = object()
tab.haken_flurst = object()
tab.setze_haken = lambda *_args, **_kwargs: None
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
tab.pruefmanager = types.SimpleNamespace(
verarbeite=lambda ergebnis: ergebnis
)
return tab
def test_set_busy_updates_button_state(self):
@@ -151,6 +159,281 @@ class TestWorkingTab(unittest.TestCase):
mock_set_layer_id.assert_called_once_with("")
# ------------------------------------------------------------------
# handle_flurst_alkis
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
def test_handle_flurst_alkis_success_sets_haken_and_style(
self, mock_alkis, mock_iface, mock_project, mock_apply_style, mock_success
):
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
layer = _DummyLayer(valid=True)
mock_alkis.return_value = (layer, LoadStatus.FIRST)
mock_project.return_value = _DummyProject()
mock_iface.mapCanvas.return_value = _DummyCanvas()
tab.handle_flurst_alkis()
mock_apply_style.assert_called_once_with(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
mock_success.assert_called_once()
self.assertEqual(len(haken_calls), 1)
self.assertTrue(haken_calls[0][1]) # haken gesetzt
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.info")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
def test_handle_flurst_alkis_existing_layer_keep_path(self, mock_alkis, mock_info):
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
layer = _DummyLayer(valid=True)
mock_alkis.return_value = (layer, LoadStatus.KEEP)
tab.handle_flurst_alkis()
mock_info.assert_called_once()
self.assertEqual(len(haken_calls), 1)
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
def test_handle_flurst_alkis_invalid_input_returns_early(self, mock_alkis):
"""Status NONE (z. B. Nutzerabbruch) → kein Layer, kein Haken."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
mock_alkis.return_value = (None, LoadStatus.NONE)
tab.handle_flurst_alkis()
self.assertEqual(len(haken_calls), 0)
# Button muss am Ende wieder freigegeben sein (finally)
self.assertTrue(tab.btn_flurst_alkis.isEnabled())
# ------------------------------------------------------------------
# handle_verf_aus_auswahl – Vorbedingungsprüfungen
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_requires_active_layer(
self, mock_iface, mock_warning
):
"""Kein aktiver Layer → warning, kein Layer wird erstellt."""
tab = self._make_tab()
mock_iface.activeLayer.return_value = None
tab.handle_verf_aus_auswahl()
mock_warning.assert_called_once()
self.assertIn("aktiver Layer", mock_warning.call_args[0][1])
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_requires_required_fields(
self, mock_iface, mock_pruefe, mock_warning
):
"""Layer ohne Pflichtfelder → warning, kein Layer wird erstellt."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
tab = self._make_tab()
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_pruefe.return_value = PruefErg(
ok=False, aktion="felder_fehlen", meldung="Feld fehlt"
)
tab.handle_verf_aus_auswahl()
mock_warning.assert_called_once()
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_requires_selection(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl, mock_warning
):
"""Keine Flurstück-Auswahl → warning, kein Layer erstellt."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
tab = self._make_tab()
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
mock_pruefe_auswahl.return_value = PruefErg(
ok=False, aktion="layer_leer", meldung="Keine Auswahl"
)
tab.handle_verf_aus_auswahl()
mock_warning.assert_called_once()
# ------------------------------------------------------------------
# handle_verf_aus_auswahl – Erstlauf (kein bestehender Layer)
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.erstelle_verfahrensgebiet_aus_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_new_layer_success(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_erstelle, mock_project_inst, mock_apply_style, mock_msgbox
):
"""Erstlauf ohne bestehenden Layer → Layer wird erstellt und hinzugefügt."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append(v)
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_iface.mapCanvas.return_value = _DummyCanvas()
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="3 Flurstücke")
new_layer = _DummyLayer(valid=True, layer_id="new_verf")
mock_erstelle.return_value = (new_layer, 3, 0)
project = _DummyProject()
project.mapLayersByName = lambda _name: [] # kein bestehender Layer
mock_project_inst.return_value = project
tab.handle_verf_aus_auswahl()
self.assertIn(new_layer, project.added)
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
self.assertTrue(haken_calls[0])
# ------------------------------------------------------------------
# handle_verf_aus_auswahl – Ergänzen / Entfernen / Ersetzen
# ------------------------------------------------------------------
def _make_entscheidungs_ergebnis(self, aktion: str):
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
return PruefErg(ok=True, aktion=aktion)
def _setup_existierenden_layer(self, tab, mock_iface, mock_pruefe_layer,
mock_pruefe_auswahl, mock_project_inst,
mock_canvas, entscheidung: str):
"""Gemeinsames Setup für Delta-Operationen mit bestehendem Layer."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_iface.mapCanvas.return_value = mock_canvas
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="2 Flurstücke")
verf_layer = _DummyLayer(valid=True, layer_id="verf_existing")
project = _DummyProject()
project.added = []
project.mapLayersByName = lambda _: [verf_layer]
project.removeMapLayer = lambda lid: None
mock_project_inst.return_value = project
# Pruefmanager gibt Entscheidung zurück
tab.pruefmanager.verarbeite = lambda _e: self._make_entscheidungs_ergebnis(entscheidung)
return active, verf_layer, project
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ergaenze_verfahrensgebiet")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_existing_layer_ergaenzen_dispatch(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, mock_ergaenze, mock_apply_style, mock_msgbox
):
"""Entscheidung 'ergaenzen' → ergaenze_verfahrensgebiet wird aufgerufen."""
tab = self._make_tab()
canvas = _DummyCanvas()
active, verf_layer, _ = self._setup_existierenden_layer(
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, canvas, "verfahrensgebiet_ergaenzen"
)
mock_ergaenze.return_value = (verf_layer, 2, 0, 0)
tab.handle_verf_aus_auswahl()
mock_ergaenze.assert_called_once_with(verf_layer, active)
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.entferne_aus_verfahrensgebiet")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_existing_layer_entfernen_dispatch(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, mock_entferne, mock_apply_style, mock_msgbox
):
"""Entscheidung 'entfernen' → entferne_aus_verfahrensgebiet wird aufgerufen."""
tab = self._make_tab()
canvas = _DummyCanvas()
active, verf_layer, _ = self._setup_existierenden_layer(
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, canvas, "verfahrensgebiet_entfernen"
)
mock_entferne.return_value = (verf_layer, 1, 1, 0)
tab.handle_verf_aus_auswahl()
mock_entferne.assert_called_once_with(verf_layer, active)
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ersetze_verfahrensgebiet")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_existing_layer_ersetzen_dispatch(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, mock_ersetze, mock_apply_style, mock_msgbox
):
"""Entscheidung 'ersetzen' → alter Layer entfernt, neuer hinzugefügt."""
tab = self._make_tab()
canvas = _DummyCanvas()
active, verf_layer, project = self._setup_existierenden_layer(
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, canvas, "verfahrensgebiet_ersetzen"
)
removed = []
project.removeMapLayer = lambda lid: removed.append(lid)
new_layer = _DummyLayer(valid=True, layer_id="new_verf_replaced")
mock_ersetze.return_value = (new_layer, 3, 0)
tab.handle_verf_aus_auswahl()
mock_ersetze.assert_called_once_with(active)
self.assertIn(verf_layer.id(), removed)
self.assertIn(new_layer, project.added)
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
# ------------------------------------------------------------------
# handle_verf_aus_liste
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.show_info_dialog")
def test_aus_liste_button_shows_not_implemented_hint(self, mock_info):
tab = self._make_tab()
tab.handle_verf_aus_liste()
mock_info.assert_called_once()
if __name__ == "__main__":
unittest.main()