Files
Plugin_SN_Verfahrensgebiet/tests/test_beteiligung_pruefung.py
T
Michael OttoandClaude Sonnet 5 d5db43bc57 Beteiligungsprüfung portiert (ohne automatisches Hintergrund-Laden)
Portiert aus origin/unstable (Feature "Beteiligung prüfen": Abgleich einer
BI-Server-CSV-Exportliste mit dem ALKIS-Flurstücksbestand des
Verfahrensgebiets), bewusst OHNE den dortigen silent-Modus für
verfahrensgebiet_alkis()/verfahrensgebiet_alkis_komplett():

- try1 (Verfahrensgebiet-Layer mit Kennzeichen vorhanden) bleibt unverändert
  – schneller Mengenvergleich ohne WFS-Zugriff.
- try2 (Layer vorhanden, aber ohne verwertbare Kennzeichen) und try3 (kein
  Layer vorhanden) laden nicht mehr automatisch/silent über die komplette
  ALKIS-Pipeline, sondern fordern den Nutzer per Meldung auf, das
  Verfahrensgebiet zunächst regulär über "Aus ALKIS laden" zu laden und die
  Beteiligungsprüfung danach erneut zu starten (landet dann automatisch bei
  try1). Grund: ein automatischer Durchlauf ohne Nutzerinteraktion
  (mehrdeutige BROR-Treffer, Flächenabweichungen) hat sich in der Praxis
  nicht als zuverlässig genug erwiesen.
- verfahrensgebiet_alkis.py/verfahrensgebiet_alkis_komplett.py bleiben dadurch
  unangetastet – kein silent-Parameter, kein Konflikt mit dem heutigen
  Zwei-Dialog-Split und dem Landkreis-Fallback.
- Neuer Button "Beteiligung prüfen" in working_tab.py, delegiert an
  beteiligung_pruefen().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 06:33:09 +02:00

305 lines
12 KiB
Python

"""sn_verfahrensgebiet/tests/test_beteiligung_pruefung.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.beteiligung_pruefung`.
Getestete Szenarien:
baue_flurstueck_csv:
- Normalfall mit führenden Nullen bei Zähler/Nenner
- Nenner = 0 → Platzhalter "____"
- Nenner leer → Platzhalter "____"
- Ungültiges Format (nicht 4 Teile) → None
- Leerer Wert → None
baue_flurstueck_csv_liste:
- Bildet Liste nur aus gültigen Zeilen, schreibt Flurstueck_csv in jede Zeile zurück
lade_beteiligung_csv:
- Dateidialog abgebrochen → None
- Spalte FLURSTUECK fehlt → BeteiligungPruefungFehler
- Leere Datei → BeteiligungPruefungFehler
- Erfolgsfall → (zeilen, spaltennamen)
try2/try3 (kein automatisches Laden mehr, siehe Modul-Docstring):
- Verfahrensgebiet-Layer ohne Kennzeichen vorhanden → Aufforderung statt Auto-Laden
- Kein Verfahrensgebiet-Layer vorhanden → Verfahrensnummer aus CSV wird gesetzt,
dann Aufforderung statt Auto-Laden
"""
import csv
import os
import sys
import tempfile
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.beteiligung_pruefung import (
baue_flurstueck_csv,
baue_flurstueck_csv_liste,
lade_beteiligung_csv,
beteiligung_pruefen,
BeteiligungPruefungFehler,
FELD_FLURSTUECK_CSV,
CSV_SPALTE_FLURSTUECK,
CSV_SPALTE_VEF_VKZF,
_fordere_manuelles_laden_an,
_try3_bootstrap,
_hole_verfahrensgebiet_layer,
)
_MOD = "sn_verfahrensgebiet.functions.beteiligung_pruefung"
class TestBaueFlurstueckCsv(unittest.TestCase):
def test_normalfall_mit_fuehrenden_nullen(self):
ergebnis = baue_flurstueck_csv("1234/123/45/00", "14")
self.assertEqual(ergebnis, "141234___00123004500")
def test_nenner_null_wird_zu_unterstrichen(self):
ergebnis = baue_flurstueck_csv("1234/123/0/00", "14")
self.assertEqual(ergebnis, "141234___00123____00")
def test_nenner_leer_wird_zu_unterstrichen(self):
ergebnis = baue_flurstueck_csv("1234//45/00", "14")
# Gemarkung 1234, Zähler leer -> "00000"? -> zfill auf leeren String ergibt "00000"
self.assertTrue(ergebnis.startswith("141234___00000"))
def test_gemarkung_ohne_fuehrende_nullen_wird_aufgefuellt(self):
ergebnis = baue_flurstueck_csv("12/1/1/00", "14")
self.assertTrue(ergebnis.startswith("140012___"))
def test_ungueltiges_format_gibt_none(self):
self.assertIsNone(baue_flurstueck_csv("1234/123/45", "14"))
self.assertIsNone(baue_flurstueck_csv("1234/123/45/00/99", "14"))
def test_leerer_wert_gibt_none(self):
self.assertIsNone(baue_flurstueck_csv("", "14"))
self.assertIsNone(baue_flurstueck_csv(None, "14"))
def test_nenner_9001_wird_zu_buchstabe_a(self):
ergebnis = baue_flurstueck_csv("1234/123/9001/00", "14")
self.assertEqual(ergebnis, "141234___00123000a00")
def test_nenner_9002_wird_zu_buchstabe_b(self):
ergebnis = baue_flurstueck_csv("1234/123/9002/00", "14")
self.assertEqual(ergebnis, "141234___00123000b00")
def test_nenner_9026_wird_zu_buchstabe_z(self):
ergebnis = baue_flurstueck_csv("1234/123/9026/00", "14")
self.assertEqual(ergebnis, "141234___00123000z00")
def test_nenner_ausserhalb_buchstaben_bereich_bleibt_numerisch(self):
ergebnis = baue_flurstueck_csv("1234/123/9027/00", "14")
self.assertEqual(ergebnis, "141234___00123902700")
def test_nenner_9001_mit_fuehrenden_nullen_wird_erkannt(self):
ergebnis = baue_flurstueck_csv("1234/123/09001/00", "14")
self.assertEqual(ergebnis, "141234___00123000a00")
def test_einstellige_folge_wird_auf_2_stellen_aufgefuellt(self):
ergebnis = baue_flurstueck_csv("1234/123/45/5", "14")
self.assertEqual(ergebnis, "141234___00123004505")
class TestBaueFlurstueckCsvListe(unittest.TestCase):
def test_nur_gueltige_zeilen_in_liste(self):
zeilen = [
{CSV_SPALTE_FLURSTUECK: "1234/123/45/00"},
{CSV_SPALTE_FLURSTUECK: "ungueltig"},
{CSV_SPALTE_FLURSTUECK: "5678/1/0/01"},
]
ergebnis = baue_flurstueck_csv_liste(zeilen, "14")
self.assertEqual(len(ergebnis), 2)
self.assertEqual(zeilen[0][FELD_FLURSTUECK_CSV], "141234___00123004500")
self.assertEqual(zeilen[1][FELD_FLURSTUECK_CSV], "")
self.assertEqual(zeilen[2][FELD_FLURSTUECK_CSV], "145678___00001____01")
class TestHoleVerfahrensgebietLayer(unittest.TestCase):
@patch(f"{_MOD}.QgsProject")
def test_layer_per_name_hat_vorrang_vor_combo_auswahl(self, mock_project_cls):
namens_layer = MagicMock(name="verfahrensgebiet_layer")
mock_project = MagicMock()
mock_project.mapLayersByName.return_value = [namens_layer]
mock_project_cls.instance.return_value = mock_project
andere_auswahl = MagicMock(name="andere_layer_in_combo")
andere_auswahl.isValid.return_value = True
tab_widget = MagicMock()
tab_widget.verf_layer_combo.currentLayer.return_value = andere_auswahl
ergebnis = _hole_verfahrensgebiet_layer(tab_widget)
self.assertIs(ergebnis, namens_layer)
@patch(f"{_MOD}.QgsProject")
def test_combo_auswahl_als_fallback_ohne_namens_layer(self, mock_project_cls):
mock_project = MagicMock()
mock_project.mapLayersByName.return_value = []
mock_project_cls.instance.return_value = mock_project
combo_layer = MagicMock(name="combo_layer")
combo_layer.isValid.return_value = True
tab_widget = MagicMock()
tab_widget.verf_layer_combo.currentLayer.return_value = combo_layer
ergebnis = _hole_verfahrensgebiet_layer(tab_widget)
self.assertIs(ergebnis, combo_layer)
@patch(f"{_MOD}.QgsProject")
def test_kein_layer_gibt_none(self, mock_project_cls):
mock_project = MagicMock()
mock_project.mapLayersByName.return_value = []
mock_project_cls.instance.return_value = mock_project
tab_widget = MagicMock()
tab_widget.verf_layer_combo.currentLayer.return_value = None
self.assertIsNone(_hole_verfahrensgebiet_layer(tab_widget))
class TestFordereManuellesLadenAn(unittest.TestCase):
"""try2/try3 laden nicht mehr automatisch, sondern fordern den Nutzer auf,
den regulären interaktiven Ablauf ('Aus ALKIS laden') selbst zu starten."""
def test_ohne_vef_vkzf_nennt_fehlende_kennzeichen(self):
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
_fordere_manuelles_laden_an()
self.assertIn("keine verwertbaren", str(ctx.exception))
self.assertIn("Aus ALKIS laden", str(ctx.exception))
def test_mit_vef_vkzf_nennt_verfahrensnummer(self):
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
_fordere_manuelles_laden_an("12345")
self.assertIn("12345", str(ctx.exception))
self.assertIn("Aus ALKIS laden", str(ctx.exception))
class TestTry3Bootstrap(unittest.TestCase):
@patch(f"{_MOD}.set_variable")
def test_setzt_verfahrensnummer_und_fordert_manuelles_laden_an(self, mock_set_variable):
zeilen = [{CSV_SPALTE_VEF_VKZF: "12345", CSV_SPALTE_FLURSTUECK: "1234/1/1/00"}]
spaltennamen = [CSV_SPALTE_VEF_VKZF, CSV_SPALTE_FLURSTUECK]
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
_try3_bootstrap(object(), zeilen, spaltennamen)
mock_set_variable.assert_called_once_with("verfahrensnummer", "12345", scope="project")
self.assertIn("12345", str(ctx.exception))
class TestBeteiligungPruefenDispatch(unittest.TestCase):
"""Dispatch-Logik in beteiligung_pruefen(): try1 unverändert, try2/try3
fordern jetzt auf statt automatisch zu laden."""
@patch(f"{_MOD}._hat_verwendbare_kennzeichen", return_value=False)
@patch(f"{_MOD}._hole_verfahrensgebiet_layer")
@patch(f"{_MOD}.baue_flurstueck_csv_liste")
@patch(f"{_MOD}.get_laenderkennung", return_value="14")
@patch(f"{_MOD}.lade_beteiligung_csv")
def test_layer_ohne_kennzeichen_fordert_manuelles_laden_an(
self, mock_lade_csv, mock_laenderkennung, mock_liste, mock_hole_layer, mock_hat_kz
):
mock_lade_csv.return_value = ([{}], ["FLURSTUECK"])
mock_liste.return_value = ["145678___00001____01"]
mock_hole_layer.return_value = MagicMock(name="verf_layer_ohne_kz")
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
beteiligung_pruefen(object())
self.assertIn("keine verwertbaren", str(ctx.exception))
@patch(f"{_MOD}.set_variable")
@patch(f"{_MOD}._hole_verfahrensgebiet_layer", return_value=None)
@patch(f"{_MOD}.baue_flurstueck_csv_liste")
@patch(f"{_MOD}.get_laenderkennung", return_value="14")
@patch(f"{_MOD}.lade_beteiligung_csv")
def test_kein_layer_setzt_verfahrensnummer_und_fordert_laden_an(
self, mock_lade_csv, mock_laenderkennung, mock_liste, mock_hole_layer, mock_set_variable
):
zeilen = [{CSV_SPALTE_VEF_VKZF: "12345", CSV_SPALTE_FLURSTUECK: "1234/1/1/00"}]
mock_lade_csv.return_value = (zeilen, [CSV_SPALTE_VEF_VKZF, CSV_SPALTE_FLURSTUECK])
mock_liste.return_value = ["145678___00001____01"]
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
beteiligung_pruefen(object())
mock_set_variable.assert_called_once_with("verfahrensnummer", "12345", scope="project")
self.assertIn("12345", str(ctx.exception))
class TestLadeBeteiligungCsv(unittest.TestCase):
def setUp(self):
self._tmp_files = []
def tearDown(self):
for pfad in self._tmp_files:
try:
os.remove(pfad)
except OSError:
pass
def _schreibe_csv(self, header, zeilen):
fd, pfad = tempfile.mkstemp(suffix=".csv")
self._tmp_files.append(pfad)
with os.fdopen(fd, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(header)
for zeile in zeilen:
writer.writerow(zeile)
return pfad
@patch(f"{_MOD}.QFileDialog")
def test_dialog_abgebrochen_gibt_none(self, mock_fd):
mock_fd.getOpenFileName.return_value = ("", "")
self.assertIsNone(lade_beteiligung_csv(None))
@patch(f"{_MOD}.QFileDialog")
def test_fehlende_flurstueck_spalte_wirft_fehler(self, mock_fd):
pfad = self._schreibe_csv(["FOO", "BAR"], [["1", "2"]])
mock_fd.getOpenFileName.return_value = (pfad, "")
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
lade_beteiligung_csv(None)
self.assertIn(CSV_SPALTE_FLURSTUECK, str(ctx.exception))
@patch(f"{_MOD}.QFileDialog")
def test_leere_datei_wirft_fehler(self, mock_fd):
fd, pfad = tempfile.mkstemp(suffix=".csv")
os.close(fd)
self._tmp_files.append(pfad)
mock_fd.getOpenFileName.return_value = (pfad, "")
with self.assertRaises(BeteiligungPruefungFehler):
lade_beteiligung_csv(None)
@patch(f"{_MOD}.QFileDialog")
def test_erfolgsfall_gibt_zeilen_und_spalten_zurueck(self, mock_fd):
pfad = self._schreibe_csv(
[CSV_SPALTE_VEF_VKZF, "FLA_Beteiligung", CSV_SPALTE_FLURSTUECK],
[["12345", "1", "1234/123/45/00"]],
)
mock_fd.getOpenFileName.return_value = (pfad, "")
zeilen, spalten = lade_beteiligung_csv(None)
self.assertEqual(len(zeilen), 1)
self.assertIn(CSV_SPALTE_FLURSTUECK, spalten)
self.assertEqual(zeilen[0][CSV_SPALTE_FLURSTUECK], "1234/123/45/00")
if __name__ == "__main__":
unittest.main()