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