From 600a3c7aff5db9f417887a95225d62fba1b48c34 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 20 Apr 2026 15:36:29 +0200 Subject: [PATCH] =?UTF-8?q?test=20=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_listenauswerter.py | 84 ++++++++++++++++++ tests/test_tab_a_logic.py | 156 ++++++++++++++++++---------------- tests/test_tab_a_ui.py | 131 ++++++++++++++++++++-------- 3 files changed, 261 insertions(+), 110 deletions(-) create mode 100644 tests/test_listenauswerter.py diff --git a/tests/test_listenauswerter.py b/tests/test_listenauswerter.py new file mode 100644 index 0000000..1eadfcd --- /dev/null +++ b/tests/test_listenauswerter.py @@ -0,0 +1,84 @@ +import unittest +from typing import Any + +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_plan41.modules.listenauswerter import Listenauswerter + + +class _DummyPruefmanager: + def __init__(self) -> None: + self.seen = [] + + def verarbeite(self, ergebnis: Any): + self.seen.append(ergebnis) + return ergebnis + + +class _DummyStilPruefer: + def __init__(self, ok: bool = True) -> None: + self.ok = ok + + def pruefe(self, _path: str) -> pruef_ergebnis: + if self.ok: + return pruef_ergebnis(ok=True, meldung="ok", aktion="ok", kontext=None) + return pruef_ergebnis(ok=False, meldung="stil fehler", aktion="falsche_endung", kontext=None) + + +class TestListenauswerter(unittest.TestCase): + + def test_validate_rows_accepts_valid_row_and_normalizes_provider(self): + manager = _DummyPruefmanager() + validator = Listenauswerter(manager, _DummyStilPruefer(ok=True)) + + data = {"rows": [{"ident": "A1", "Inhalt": "Test", "Link": "http://x", "Provider": "wfs"}]} + validated, processed = validator.validate_rows(data) + + self.assertEqual(len(validated["rows"]), 1) + self.assertEqual(validated["rows"][0]["Provider"], "WFS") + self.assertEqual(processed, []) + + def test_validate_rows_rejects_missing_required_fields(self): + manager = _DummyPruefmanager() + validator = Listenauswerter(manager, _DummyStilPruefer(ok=True)) + + data = {"rows": [{"ident": "", "Link": "", "Provider": ""}]} + validated, processed = validator.validate_rows(data) + + self.assertEqual(validated["rows"], []) + self.assertEqual(len(processed), 1) + self.assertEqual(processed[0].aktion, "pflichtfelder_fehlen") + + def test_validate_rows_sets_stildatei_none_on_style_failure(self): + manager = _DummyPruefmanager() + validator = Listenauswerter(manager, _DummyStilPruefer(ok=False)) + + data = { + "rows": [ + { + "ident": "A1", + "Inhalt": "Thema", + "Link": "http://x", + "Provider": "WMS", + "Stildatei": "broken.qml", + } + ] + } + validated, processed = validator.validate_rows(data) + + self.assertEqual(len(validated["rows"]), 1) + self.assertIsNone(validated["rows"][0]["stildatei"]) + self.assertEqual(len(processed), 1) + self.assertEqual(processed[0].aktion, "falsche_endung") + + def test_validate_rows_handles_non_mapping_rows_edge_case(self): + manager = _DummyPruefmanager() + validator = Listenauswerter(manager, _DummyStilPruefer(ok=True)) + + validated, processed = validator.validate_rows({"rows": ["invalid", 123, None]}) + + self.assertEqual(validated, {"rows": []}) + self.assertEqual(processed, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tab_a_logic.py b/tests/test_tab_a_logic.py index 94c3bd8..595d4d0 100644 --- a/tests/test_tab_a_logic.py +++ b/tests/test_tab_a_logic.py @@ -1,99 +1,109 @@ import unittest from pathlib import Path from tempfile import TemporaryDirectory +from typing import Any, cast +from unittest.mock import patch -from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore -from sn_basis.functions.variable_wrapper import get_variable # type: ignore -from sn_basis.functions.sys_wrapper import file_exists # type: ignore +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_basis.functions.variable_wrapper import get_variable +from sn_plan41.ui.tab_a_logic import TabALogic + + +class _DummyPruefmanager: + def verarbeite(self, ergebnis): + return ergebnis class TestTabALogic(unittest.TestCase): - # ----------------------------------------------------- - # 1. Verfahrens-DB setzen und laden - # ----------------------------------------------------- - def test_verfahrens_db_set_and_load(self): - logic = TabALogic() + def _make_logic(self) -> TabALogic: + return TabALogic( + pruefmanager=cast(Any, _DummyPruefmanager()), + link_pruefer=cast(Any, object()), + stil_pruefer=cast(Any, object()), + ) - with TemporaryDirectory() as tmp: - db_path = Path(tmp) / "test.gpkg" - db_path.write_text("") + def test_verfahrens_db_set_and_load_roundtrip(self): + logic = self._make_logic() + logic.set_verfahrens_db("C:/tmp/v_db.gpkg") + self.assertEqual(logic.load_verfahrens_db(), "C:/tmp/v_db.gpkg") - logic.set_verfahrens_db(str(db_path)) - - stored = get_variable("verfahrens_db", scope="project") - self.assertEqual(stored, str(db_path)) - - loaded = logic.load_verfahrens_db() - self.assertEqual(loaded, str(db_path)) - - # ----------------------------------------------------- - # 2. Verfahrens-DB löschen - # ----------------------------------------------------- def test_verfahrens_db_clear(self): - logic = TabALogic() - + logic = self._make_logic() logic.set_verfahrens_db(None) + self.assertEqual(get_variable("verfahrens_db", scope="project"), "") - stored = get_variable("verfahrens_db", scope="project") - self.assertEqual(stored, "") - - # ----------------------------------------------------- - # 3. Neue Verfahrens-DB anlegen - # ----------------------------------------------------- - def test_create_new_verfahrens_db(self): - logic = TabALogic() - - with TemporaryDirectory() as tmp: - db_path = Path(tmp) / "neu.gpkg" - - result = logic.create_new_verfahrens_db(str(db_path)) - - self.assertTrue(result) - self.assertTrue(file_exists(db_path)) - - stored = get_variable("verfahrens_db", scope="project") - self.assertEqual(stored, str(db_path)) - - def test_create_new_verfahrens_db_with_none_path(self): - logic = TabALogic() - result = logic.create_new_verfahrens_db(None) - self.assertFalse(result) - - - - # ----------------------------------------------------- - # 4. Linkliste setzen und laden - # ----------------------------------------------------- - def test_linkliste_set_and_load(self): - logic = TabALogic() - + def test_linkliste_set_and_load_existing_path(self): + logic = self._make_logic() with TemporaryDirectory() as tmp: link_path = Path(tmp) / "links.xlsx" - link_path.write_text("dummy") + link_path.write_text("x", encoding="utf-8") logic.set_linkliste(str(link_path)) + self.assertEqual(logic.load_linkliste(), str(link_path)) - stored = get_variable("linkliste", scope="project") - self.assertEqual(stored, str(link_path)) + def test_linkliste_load_returns_none_when_file_missing(self): + logic = self._make_logic() + logic.set_linkliste("C:/tmp/missing.xlsx") + self.assertIsNone(logic.load_linkliste()) - loaded = logic.load_linkliste() - self.assertEqual(loaded, str(link_path)) + @patch("sn_plan41.ui.tab_a_logic.get_plugin_root", return_value="C:/plugins") + @patch("sn_plan41.ui.tab_a_logic.join_path", return_value="C:/plugins/sn_plan41/assets/Linkliste.xlsx") + def test_resolve_linkliste_empty_uses_standard_and_persists(self, _mock_join_path, _mock_root): + logic = self._make_logic() + resolved = logic._resolve_linkliste(None) - # ----------------------------------------------------- - # 5. Linkliste löschen - # ----------------------------------------------------- - def test_linkliste_clear(self): - logic = TabALogic() + self.assertEqual(resolved, "C:/plugins/sn_plan41/assets/Linkliste.xlsx") + self.assertEqual(get_variable("linkliste", scope="project"), "C:/plugins/sn_plan41/assets/Linkliste.xlsx") - logic.set_linkliste(None) + @patch("sn_plan41.ui.tab_a_logic.Dateipruefer") + def test_resolve_linkliste_returns_none_when_pruefmanager_rejects(self, mock_dateipruefer): + logic = self._make_logic() + mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis( + ok=False, + meldung="Ungültig", + aktion="datei_nicht_gefunden", + kontext=None, + ) - stored = get_variable("linkliste", scope="project") - self.assertEqual(stored, "") + resolved = logic._resolve_linkliste("C:/tmp/missing.xlsx") + self.assertIsNone(resolved) - # ----------------------------------------------------- - # 6. Layer-ID speichern - # ----------------------------------------------------- + def test_resolve_raumfilter_without_filter_returns_none(self): + logic = self._make_logic() + result = logic._resolve_raumfilter("ohne", "C:/tmp/v_db.gpkg") + self.assertIsNone(result) + + @patch.object(TabALogic, "_get_verfahrensgebiet_layer", return_value="layer_obj") + def test_resolve_raumfilter_verfahrensgebiet_uses_layer_lookup(self, mock_get_layer): + logic = self._make_logic() + result = logic._resolve_raumfilter("Verfahrensgebiet", "C:/tmp/v_db.gpkg") + + self.assertEqual(result, "layer_obj") + mock_get_layer.assert_called_once() + + def test_extract_datenabruf_error_maps_merges_reason_maps(self): + logic = self._make_logic() + results = [ + pruef_ergebnis( + ok=False, + meldung="Head failed", + aktion="url_nicht_erreichbar", + kontext={"ident": "A1", "thema": "Dienst A", "error": "Timeout"}, + ), + pruef_ergebnis( + ok=False, + meldung="Fetch failed", + aktion="datenabruf", + kontext={"fehler": {"Dienst B": "HTTP 500", "Dienst A": "ignored"}}, + ), + ] + + merged = logic._extract_datenabruf_error_maps(results) + + self.assertEqual(merged["by_ident"]["A1"], "Timeout") + self.assertEqual(merged["by_thema"]["Dienst A"], "Timeout") + self.assertEqual(merged["by_thema"]["Dienst B"], "HTTP 500") if __name__ == "__main__": diff --git a/tests/test_tab_a_ui.py b/tests/test_tab_a_ui.py index 4911949..1fa7022 100644 --- a/tests/test_tab_a_ui.py +++ b/tests/test_tab_a_ui.py @@ -1,56 +1,113 @@ -""" -Smoke-Tests für TabA UI (sn_plan41/ui/tab_a_ui.py) - -Ziel: -- UI kann erstellt werden -- Callbacks crashen nicht -- Keine Qt-Verhaltensprüfung -""" - import unittest +from unittest.mock import patch -from sn_plan41.ui.tab_a_ui import TabA #type:ignore +from sn_basis.functions.variable_wrapper import get_variable +from sn_plan41.ui.tab_a_ui import TabA + + +class _DummySignal: + def connect(self, _callback): + return None + + +class _DummyToggleButton: + def __init__(self): + self.arrow = None + self.style = "" + + def setArrowType(self, arrow): + self.arrow = arrow + + def setStyleSheet(self, style): + self.style = style + + +class _DummyWidget: + def __init__(self): + self.visible = None + + def setVisible(self, visible): + self.visible = visible + + +class _DummyFileWidget: + def __init__(self): + self._path = "" + + def setFilePath(self, path): + self._path = path + + def filePath(self): + return self._path + + +class _DummyCombo: + def __init__(self, value="Pufferlayer"): + self._value = value + + def currentText(self): + return self._value + + +class _DummyPruefmanager: + pass + + +class _DummyDataGrabber: + pass class TestTabAUI(unittest.TestCase): - # ----------------------------------------------------- - # 1. UI kann erstellt werden - # ----------------------------------------------------- - def test_tab_a_ui_can_be_created(self): - tab = TabA(parent=None,build_ui=False) + def _make_tab_without_qt(self): + tab = TabA.__new__(TabA) + tab.group_button = _DummyToggleButton() + tab.group_content = _DummyWidget() + tab.optional_button = _DummyToggleButton() + tab.optional_content = _DummyWidget() + tab.file_widget = _DummyFileWidget() + tab.linkliste_widget = _DummyFileWidget() + tab._raumfilter_combo = _DummyCombo("ohne") + tab.verfahrens_db = None + tab.lokale_linkliste = None + tab.logic = None + tab.pruefmanager = None + tab.data_grabber = None + return tab - self.assertIsNotNone(tab) - self.assertEqual(tab.tab_title, "Daten") - - # ----------------------------------------------------- - # 2. Toggle-Callbacks crashen nicht - # ----------------------------------------------------- - def test_tab_a_toggle_callbacks_do_not_crash(self): - tab = TabA(parent=None,build_ui=False) + def test_tab_a_callbacks_can_be_called_without_crash(self): + tab = self._make_tab_without_qt() tab._toggle_group(True) tab._toggle_group(False) - tab._toggle_optional(True) tab._toggle_optional(False) - - # ----------------------------------------------------- - # 3. Datei-Callbacks akzeptieren leere Eingaben - # ----------------------------------------------------- - def test_tab_a_file_callbacks_accept_empty_input(self): - tab = TabA(parent=None,build_ui=False) - tab._on_verfahrens_db_changed("") tab._on_linkliste_changed("") - # ----------------------------------------------------- - # 4. Layer-Callback akzeptiert None - # ----------------------------------------------------- - def test_tab_a_layer_callback_accepts_none(self): - tab = TabA(parent=None,build_ui=False) + self.assertEqual(tab.group_content.visible, False) + self.assertEqual(tab.optional_content.visible, False) - tab._on_layer_changed(None) + def test_tab_a_raumfilter_callback_persists_value(self): + tab = self._make_tab_without_qt() + + tab._on_raumfilter_changed("ohne") + self.assertEqual(get_variable("Raumfilter", scope="project"), "ohne") + + @patch("sn_plan41.ui.tab_a_ui.TabALogic") + def test_set_services_initializes_logic_and_injects_data_grabber(self, mock_logic_cls): + tab = self._make_tab_without_qt() + mock_logic = mock_logic_cls.return_value + + pm = _DummyPruefmanager() + dg = _DummyDataGrabber() + + tab.set_services(pruefmanager=pm, data_grabber=dg) + + self.assertIs(tab.pruefmanager, pm) + self.assertIs(tab.data_grabber, dg) + self.assertIs(tab.logic, mock_logic) + self.assertIs(tab.logic.data_grabber, dg) if __name__ == "__main__":