Files
Plugin_SN_Verfahrensgebiet/tests/test_knickpunkt_pruefung.py
T

476 lines
17 KiB
Python
Raw Normal View History

"""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 im 3cm-Puffer einer Verbindungslinie zwischen zwei gematchten PunktortTA → 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: _fehlende_knickpunkte_via_verbindungslinien
# ---------------------------------------------------------------------------
class TestFehlendKnickpunkteViaVerbindungslinien(unittest.TestCase):
"""Direkte Tests für :func:`_fehlende_knickpunkte_via_verbindungslinien`."""
def _fn(self, *args, **kwargs):
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
_fehlende_knickpunkte_via_verbindungslinien,
)
return _fehlende_knickpunkte_via_verbindungslinien(*args, **kwargs)
def test_leere_union_gibt_null_zurueck(self):
"""bror_union.isEmpty() == True → (0, [])."""
bror_union = MagicMock()
bror_union.isEmpty.return_value = True
with patch("qgis.core.QgsPointXY", create=True), \
patch("qgis.core.QgsProject", create=True):
count, feats = self._fn(
bror_union, MagicMock(), {}, set(), MagicMock(), "EPSG:25833"
)
self.assertEqual(count, 0)
self.assertEqual(feats, [])
def test_weniger_als_zwei_pta_auf_ring_kein_orange(self):
"""Nur 1 gematchter PunktortTA auf Ring → keine fehlenden Knickpunkte."""
bror_union = MagicMock()
bror_union.isEmpty.return_value = False
bror_union.isMultipart.return_value = False
bror_union.asPolygon.return_value = [
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)]
]
def _mock_ptxy(x, y):
pt = MagicMock()
pt.x.return_value = x
pt.y.return_value = y
return pt
pta_geom = MagicMock()
pta_pt = MagicMock()
pta_pt.x.return_value = 0.0
pta_pt.y.return_value = 0.0
pta_geom.asPoint.return_value = pta_pt
geom_map = {1: pta_geom}
matched_pta_ids = {1} # nur 1 → < 2
with patch("qgis.core.QgsPointXY", create=True, side_effect=_mock_ptxy), \
patch("qgis.core.QgsProject", create=True):
count, feats = self._fn(
bror_union, MagicMock(), geom_map, matched_pta_ids,
MagicMock(), "EPSG:25833"
)
self.assertEqual(count, 0)
self.assertEqual(len(feats), 0)
@patch(f"{_MOD}.QgsGeometry")
@patch(f"{_MOD}.QgsVectorLayer")
def test_ungematchter_pta_im_puffer_wird_orange(
self, mock_vl_cls, mock_geom_cls
):
"""Nicht-gematchter PunktortTA innerhalb 3-cm-Puffer → orange gezählt."""
bror_union = MagicMock()
bror_union.isEmpty.return_value = False
bror_union.isMultipart.return_value = False
bror_union.asPolygon.return_value = [
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)]
]
def _mock_ptxy(x, y):
pt = MagicMock()
pt.x.return_value = x
pt.y.return_value = y
return pt
# Zwei gematchte PunktortTA (fid 1 und 2)
pta_geom_1 = MagicMock()
pta_pt_1 = MagicMock()
pta_pt_1.x.return_value = 0.0
pta_pt_1.y.return_value = 0.0
pta_geom_1.asPoint.return_value = pta_pt_1
pta_geom_2 = MagicMock()
pta_pt_2 = MagicMock()
pta_pt_2.x.return_value = 10.0
pta_pt_2.y.return_value = 0.0
pta_geom_2.asPoint.return_value = pta_pt_2
# Nicht-gematchter PunktortTA (fid=3) liegt im Puffer
pta_geom_3 = MagicMock()
pta_geom_3.isEmpty.return_value = False
pta_geom_3.within.return_value = True
geom_map = {1: pta_geom_1, 2: pta_geom_2, 3: pta_geom_3}
matched_pta_ids = {1, 2}
# Spatial-Index liefert fid=3 als Kandidat
pta_index = MagicMock()
buffer_geom = MagicMock()
buffer_geom.isEmpty.return_value = False
buffer_geom.boundingBox.return_value = MagicMock()
pta_index.intersects.return_value = [3]
line_geom = MagicMock()
line_geom.isEmpty.return_value = False
line_geom.buffer.return_value = buffer_geom
mock_geom_cls.fromPolylineXY.return_value = line_geom
pr_mock = MagicMock()
pr_mock.addFeatures.return_value = True
layer_mock = MagicMock()
layer_mock.dataProvider.return_value = pr_mock
mock_vl_cls.return_value = layer_mock
with patch("qgis.core.QgsPointXY", create=True, side_effect=_mock_ptxy), \
patch("qgis.core.QgsProject", create=True) as mock_proj:
mock_proj.instance.return_value = MagicMock()
count, feats = self._fn(
bror_union, pta_index, geom_map, matched_pta_ids,
MagicMock(), "EPSG:25833"
)
self.assertEqual(count, 1)
self.assertEqual(len(feats), 1)
@patch(f"{_MOD}.QgsGeometry")
@patch(f"{_MOD}.QgsVectorLayer")
def test_ungematchter_pta_ausserhalb_puffer_kein_orange(
self, mock_vl_cls, mock_geom_cls
):
"""Nicht-gematchter PunktortTA außerhalb Puffer → nicht orange gezählt."""
bror_union = MagicMock()
bror_union.isEmpty.return_value = False
bror_union.isMultipart.return_value = False
bror_union.asPolygon.return_value = [
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)]
]
def _mock_ptxy(x, y):
pt = MagicMock()
pt.x.return_value = x
pt.y.return_value = y
return pt
pta_geom_1 = MagicMock()
pta_pt_1 = MagicMock()
pta_pt_1.x.return_value = 0.0
pta_pt_1.y.return_value = 0.0
pta_geom_1.asPoint.return_value = pta_pt_1
pta_geom_2 = MagicMock()
pta_pt_2 = MagicMock()
pta_pt_2.x.return_value = 10.0
pta_pt_2.y.return_value = 0.0
pta_geom_2.asPoint.return_value = pta_pt_2
# fid=3 liegt NICHT im Puffer
pta_geom_3 = MagicMock()
pta_geom_3.within.return_value = False
geom_map = {1: pta_geom_1, 2: pta_geom_2, 3: pta_geom_3}
matched_pta_ids = {1, 2}
pta_index = MagicMock()
buffer_geom = MagicMock()
buffer_geom.isEmpty.return_value = False
buffer_geom.boundingBox.return_value = MagicMock()
pta_index.intersects.return_value = [3]
line_geom = MagicMock()
line_geom.isEmpty.return_value = False
line_geom.buffer.return_value = buffer_geom
mock_geom_cls.fromPolylineXY.return_value = line_geom
pr_mock = MagicMock()
layer_mock = MagicMock()
layer_mock.dataProvider.return_value = pr_mock
mock_vl_cls.return_value = layer_mock
with patch("qgis.core.QgsPointXY", create=True, side_effect=_mock_ptxy), \
patch("qgis.core.QgsProject", create=True) as mock_proj:
mock_proj.instance.return_value = MagicMock()
count, feats = self._fn(
bror_union, pta_index, geom_map, matched_pta_ids,
MagicMock(), "EPSG:25833"
)
self.assertEqual(count, 0)
self.assertEqual(len(feats), 0)
if __name__ == "__main__":
unittest.main()