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