308 lines
11 KiB
Python
308 lines
11 KiB
Python
"""sn_verfahrensgebiet/tests/test_knickpunkt_pruefung.py
|
||
|
||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.knickpunkt_pruefung`.
|
||
|
||
Getestete Szenarien:
|
||
|
||
_extrahiere_knickpunkte:
|
||
- Leerer Layer → leere Liste
|
||
- Polygon mit 4 Knickpunkten → korrekte Anzahl (ohne doppelten Schlusspunkt)
|
||
- Keine Geometrie → leere Liste
|
||
|
||
knickpunkt_pruefung:
|
||
- Leere Knickpunktliste → (None, 0, 0, 0)
|
||
- Kein PunktortTA-Layer → (None, 0, 0, 0)
|
||
- Identischer Punkt → grün gezählt
|
||
- Kein passender PunktortTA → rot gezählt
|
||
- PunktortTA nahe Umring auf Flurstücksgrenze → orange gezählt
|
||
- Abbruch via progress.is_canceled() → (None, 0, 0, 0)
|
||
"""
|
||
|
||
import sys
|
||
import types
|
||
import unittest
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Minimales QGIS-Mock-Setup (identisch mit test_verfahrensgebiet_alkis_komplett)
|
||
# ---------------------------------------------------------------------------
|
||
if "qgis" not in sys.modules:
|
||
qgis_mod = types.ModuleType("qgis")
|
||
qgis_core = types.ModuleType("qgis.core")
|
||
# Attribute vorab setzen, damit patch() sie überschreiben kann
|
||
qgis_core.QgsPointXY = None
|
||
qgis_core.QgsRectangle = None
|
||
qgis_core.QgsSpatialIndex = None
|
||
qgis_core.QgsMarkerSymbol = None
|
||
qgis_core.QgsCategorizedSymbolRenderer = None
|
||
qgis_core.QgsRendererCategory = None
|
||
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.knickpunkt_pruefung import (
|
||
knickpunkt_pruefung,
|
||
_extrahiere_knickpunkte,
|
||
WERT_IDENTISCH,
|
||
WERT_KEIN_PTA,
|
||
WERT_FEHLENDER_KNICK,
|
||
)
|
||
|
||
_MOD = "sn_verfahrensgebiet.functions.knickpunkt_pruefung"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Hilfsfunktionen
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _make_layer(valid=True, crs="EPSG:25833", lid="L1"):
|
||
layer = MagicMock()
|
||
layer.isValid.return_value = valid
|
||
layer.id.return_value = lid
|
||
mock_crs = MagicMock()
|
||
mock_crs.authid.return_value = crs
|
||
layer.crs.return_value = mock_crs
|
||
layer.getFeatures.return_value = []
|
||
return layer
|
||
|
||
|
||
def _make_progress(canceled_after: int = 9999):
|
||
"""Progress-Mock: gibt is_canceled() nach N Aufrufen True zurück."""
|
||
call_count = [0]
|
||
|
||
def _is_canceled():
|
||
call_count[0] += 1
|
||
return call_count[0] > canceled_after
|
||
|
||
prog = MagicMock()
|
||
prog.is_canceled.side_effect = _is_canceled
|
||
return prog
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: _extrahiere_knickpunkte
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestExtrahiereKnickpunkte(unittest.TestCase):
|
||
|
||
def test_leerer_layer_gibt_leere_liste(self):
|
||
layer = _make_layer()
|
||
layer.getFeatures.return_value = []
|
||
result = _extrahiere_knickpunkte(layer)
|
||
self.assertEqual(result, [])
|
||
|
||
def test_keine_gueltige_geometrie_gibt_leere_liste(self):
|
||
feat = MagicMock()
|
||
feat.geometry.return_value = None
|
||
layer = _make_layer()
|
||
layer.getFeatures.return_value = [feat]
|
||
result = _extrahiere_knickpunkte(layer)
|
||
self.assertEqual(result, [])
|
||
|
||
def test_feature_mit_leerer_geometrie_gibt_leere_liste(self):
|
||
feat = MagicMock()
|
||
geom = MagicMock()
|
||
geom.isEmpty.return_value = True
|
||
feat.geometry.return_value = geom
|
||
layer = _make_layer()
|
||
layer.getFeatures.return_value = [feat]
|
||
result = _extrahiere_knickpunkte(layer)
|
||
self.assertEqual(result, [])
|
||
|
||
def test_qgis_nicht_verfuegbar_gibt_leere_liste(self):
|
||
"""Wenn qgis.core kein QgsPointXY hat (ImportError in Funktion), leere Liste."""
|
||
layer = _make_layer()
|
||
feat = MagicMock()
|
||
geom = MagicMock()
|
||
geom.isEmpty.return_value = False
|
||
feat.geometry.return_value = geom
|
||
layer.getFeatures.return_value = [feat]
|
||
|
||
# QgsGeometry.unaryUnion wirft eine Exception → leere Liste erwartet
|
||
with patch(f"{_MOD}.QgsGeometry") as mock_geom_cls:
|
||
mock_geom_cls.unaryUnion.side_effect = ImportError("kein QGIS")
|
||
result = _extrahiere_knickpunkte(layer)
|
||
self.assertEqual(result, [])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: knickpunkt_pruefung – Eingabevalidierung
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestKnickpunktPruefungInputvalidierung(unittest.TestCase):
|
||
|
||
def test_keine_knickpunkte_gibt_none_zurueck(self):
|
||
bror = _make_layer()
|
||
bror.getFeatures.return_value = []
|
||
result = knickpunkt_pruefung(bror, MagicMock(), None)
|
||
layer, identisch, kein_pta, fehlend = result
|
||
self.assertIsNone(layer)
|
||
self.assertEqual(identisch, 0)
|
||
self.assertEqual(kein_pta, 0)
|
||
self.assertEqual(fehlend, 0)
|
||
|
||
def test_kein_punktort_layer_gibt_none_zurueck(self):
|
||
bror = _make_layer()
|
||
result = knickpunkt_pruefung(bror, None, None, knickpunkte=[MagicMock()])
|
||
layer, identisch, kein_pta, fehlend = result
|
||
self.assertIsNone(layer)
|
||
|
||
def test_abbruch_via_progress_gibt_none_zurueck(self):
|
||
bror = _make_layer()
|
||
pta = _make_layer()
|
||
prog = _make_progress(canceled_after=0) # sofort abbrechen
|
||
result = knickpunkt_pruefung(
|
||
bror, pta, None,
|
||
progress=prog,
|
||
knickpunkte=[MagicMock()],
|
||
)
|
||
self.assertIsNone(result[0])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: knickpunkt_pruefung – Klassifizierungslogik (mit QGIS-Mocks)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestKnickpunktPruefungKlassifizierung(unittest.TestCase):
|
||
"""Testet die Grün/Rot/Orange-Klassifizierung mit gesteuertem QGIS-Mock."""
|
||
|
||
def _make_pta_layer_mit_punkt(self, x: float, y: float, fid: int = 1):
|
||
"""Erstellt einen PunktortTA-Layer-Mock mit einem Punkt bei (x, y)."""
|
||
layer = _make_layer(lid=f"PTA_{fid}")
|
||
geom = MagicMock()
|
||
geom.isEmpty.return_value = False
|
||
pt = MagicMock()
|
||
pt.x.return_value = x
|
||
pt.y.return_value = y
|
||
geom.asPoint.return_value = pt
|
||
feat = MagicMock()
|
||
feat.id.return_value = fid
|
||
feat.geometry.return_value = geom
|
||
layer.getFeatures.return_value = [feat]
|
||
return layer
|
||
|
||
def _make_bror_layer(self, x: float = 0.0, y: float = 0.0):
|
||
"""BROR-Layer mit einer minimalen Polygon-Geometrie."""
|
||
layer = _make_layer()
|
||
geom = MagicMock()
|
||
geom.isEmpty.return_value = False
|
||
geom.area.return_value = 10000.0
|
||
feat = MagicMock()
|
||
feat.geometry.return_value = geom
|
||
layer.getFeatures.return_value = [feat]
|
||
return layer
|
||
|
||
@patch(f"{_MOD}.QgsGeometry")
|
||
@patch(f"{_MOD}._baue_pta_index")
|
||
@patch(f"{_MOD}._erstelle_ergebnis_layer")
|
||
@patch(f"qgis.core.QgsRectangle", create=True)
|
||
@patch(f"qgis.core.QgsPointXY", create=True)
|
||
def test_identischer_punkt_wird_gruen_gezaehlt(
|
||
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
|
||
):
|
||
"""PunktortTA exakt am Knickpunkt → identisch_count == 1."""
|
||
# Knickpunkt bei (100, 200)
|
||
knick = MagicMock()
|
||
knick.x.return_value = 100.0
|
||
knick.y.return_value = 200.0
|
||
|
||
# PunktortTA ebenfalls bei (100, 200) – exakter Treffer
|
||
pta_geom = MagicMock()
|
||
pta_geom.isEmpty.return_value = False
|
||
pta_pt = MagicMock()
|
||
pta_pt.x.return_value = 100.0
|
||
pta_pt.y.return_value = 200.0
|
||
pta_geom.asPoint.return_value = pta_pt
|
||
|
||
# Spatial-Index gibt Feature-ID 1 zurück
|
||
mock_idx = MagicMock()
|
||
mock_idx.intersects.return_value = [1]
|
||
mock_index.return_value = (mock_idx, {1: pta_geom})
|
||
|
||
# Ergebnis-Layer
|
||
pr = MagicMock()
|
||
pr.addFeatures.return_value = True
|
||
ergebnis = MagicMock()
|
||
ergebnis.dataProvider.return_value = pr
|
||
ergebnis.isValid.return_value = True
|
||
mock_erstelle.return_value = ergebnis
|
||
|
||
bror = self._make_bror_layer()
|
||
pta = self._make_pta_layer_mit_punkt(100.0, 200.0)
|
||
|
||
result = knickpunkt_pruefung(
|
||
bror, pta, None, knickpunkte=[knick]
|
||
)
|
||
_, identisch, kein_pta, _ = result
|
||
self.assertEqual(identisch, 1)
|
||
self.assertEqual(kein_pta, 0)
|
||
|
||
@patch(f"{_MOD}.QgsGeometry")
|
||
@patch(f"{_MOD}._baue_pta_index")
|
||
@patch(f"{_MOD}._erstelle_ergebnis_layer")
|
||
@patch(f"qgis.core.QgsRectangle", create=True)
|
||
@patch(f"qgis.core.QgsPointXY", create=True)
|
||
def test_kein_pta_an_knickpunkt_wird_rot_gezaehlt(
|
||
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
|
||
):
|
||
"""Kein PunktortTA am Knickpunkt → kein_pta_count == 1."""
|
||
knick = MagicMock()
|
||
knick.x.return_value = 100.0
|
||
knick.y.return_value = 200.0
|
||
|
||
# Spatial-Index gibt leere Liste zurück → kein Treffer
|
||
mock_idx = MagicMock()
|
||
mock_idx.intersects.return_value = []
|
||
mock_index.return_value = (mock_idx, {})
|
||
|
||
pr = MagicMock()
|
||
pr.addFeatures.return_value = True
|
||
ergebnis = MagicMock()
|
||
ergebnis.dataProvider.return_value = pr
|
||
ergebnis.isValid.return_value = True
|
||
mock_erstelle.return_value = ergebnis
|
||
|
||
bror = self._make_bror_layer()
|
||
pta = self._make_pta_layer_mit_punkt(999.0, 999.0) # weit weg
|
||
|
||
result = knickpunkt_pruefung(
|
||
bror, pta, None, knickpunkte=[knick]
|
||
)
|
||
_, identisch, kein_pta, _ = result
|
||
self.assertEqual(identisch, 0)
|
||
self.assertEqual(kein_pta, 1)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: _baue_flst_vertex_set
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestBaueFlstVertexSet(unittest.TestCase):
|
||
|
||
def test_none_layer_gibt_leeres_set(self):
|
||
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import _baue_flst_vertex_set
|
||
result = _baue_flst_vertex_set(None, MagicMock())
|
||
self.assertEqual(result, set())
|
||
|
||
def test_layer_ohne_schnitt_mit_puffer_ignoriert(self):
|
||
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import _baue_flst_vertex_set
|
||
layer = _make_layer()
|
||
geom = MagicMock()
|
||
geom.isEmpty.return_value = False
|
||
geom.intersects.return_value = False # kein Schnitt
|
||
feat = MagicMock()
|
||
feat.geometry.return_value = geom
|
||
layer.getFeatures.return_value = [feat]
|
||
buffer_geom = MagicMock()
|
||
result = _baue_flst_vertex_set(layer, buffer_geom)
|
||
self.assertEqual(result, set())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|