forked from AG_QGIS/Plugin_SN_Basis
Tests überarbeitet, Mocks und coverage eingefügt
This commit is contained in:
12
.coveragerc
Normal file
12
.coveragerc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[run]
|
||||||
|
source = modules
|
||||||
|
omit =
|
||||||
|
*/test/*
|
||||||
|
*/__init__.py
|
||||||
|
|
||||||
|
[report]
|
||||||
|
show_missing = True
|
||||||
|
skip_covered = False
|
||||||
|
|
||||||
|
[html]
|
||||||
|
directory = coverage_html
|
||||||
@@ -6,3 +6,4 @@ graph TD
|
|||||||
|
|
||||||
M1 --> M2
|
M1 --> M2
|
||||||
M1 --> M3
|
M1 --> M3
|
||||||
|
```
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#Modul zur Prüfung und zum Exception Handling für Dateieingaben
|
||||||
|
#Dateipruefer.py
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
|
||||||
|
|||||||
1
modules/Datenbankpruefer.py
Normal file
1
modules/Datenbankpruefer.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#Datenbankpruefer.py
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
#Pruefmanager.py
|
||||||
from Dateipruefer import DateiEntscheidung
|
from modules.qt_compat import QMessageBox, QFileDialog, YES, NO, CANCEL, ICON_QUESTION, exec_dialog
|
||||||
|
from modules.Dateipruefer import DateiEntscheidung
|
||||||
|
|
||||||
class PruefManager:
|
class PruefManager:
|
||||||
|
|
||||||
@@ -8,40 +9,40 @@ class PruefManager:
|
|||||||
self.plugin_pfad = plugin_pfad
|
self.plugin_pfad = plugin_pfad
|
||||||
|
|
||||||
def frage_datei_ersetzen_oder_anhaengen(self, pfad: str) -> DateiEntscheidung:
|
def frage_datei_ersetzen_oder_anhaengen(self, pfad: str) -> DateiEntscheidung:
|
||||||
"""Fragt den Nutzer, ob die vorhandene Datei ersetzt, angehängt oder abgebrochen werden soll."""
|
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
msg.setIcon(QMessageBox.Question)
|
msg.setIcon(ICON_QUESTION)
|
||||||
msg.setWindowTitle("Datei existiert")
|
msg.setWindowTitle("Datei existiert")
|
||||||
msg.setText(f"Die Datei '{pfad}' existiert bereits.\nWas möchtest du tun?")
|
msg.setText(f"Die Datei '{pfad}' existiert bereits.\nWas möchtest du tun?")
|
||||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
|
|
||||||
msg.setDefaultButton(QMessageBox.Yes)
|
|
||||||
msg.button(QMessageBox.Yes).setText("Ersetzen")
|
|
||||||
msg.button(QMessageBox.No).setText("Anhängen")
|
|
||||||
msg.button(QMessageBox.Cancel).setText("Abbrechen")
|
|
||||||
|
|
||||||
result = msg.exec_()
|
msg.setStandardButtons(YES | NO | CANCEL)
|
||||||
|
msg.setDefaultButton(YES)
|
||||||
|
|
||||||
if result == QMessageBox.Yes:
|
msg.button(YES).setText("Ersetzen")
|
||||||
|
msg.button(NO).setText("Anhängen")
|
||||||
|
msg.button(CANCEL).setText("Abbrechen")
|
||||||
|
|
||||||
|
result = exec_dialog(msg)
|
||||||
|
|
||||||
|
if result == YES:
|
||||||
return DateiEntscheidung.ERSETZEN
|
return DateiEntscheidung.ERSETZEN
|
||||||
elif result == QMessageBox.No:
|
elif result == NO:
|
||||||
return DateiEntscheidung.ANHAENGEN
|
return DateiEntscheidung.ANHAENGEN
|
||||||
else:
|
else:
|
||||||
return DateiEntscheidung.ABBRECHEN
|
return DateiEntscheidung.ABBRECHEN
|
||||||
|
|
||||||
def frage_temporär_verwenden(self) -> bool:
|
def frage_temporär_verwenden(self) -> bool:
|
||||||
"""Fragt den Nutzer, ob mit temporären Layern gearbeitet werden soll."""
|
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
msg.setIcon(QMessageBox.Question)
|
msg.setIcon(ICON_QUESTION)
|
||||||
msg.setWindowTitle("Temporäre Layer")
|
msg.setWindowTitle("Temporäre Layer")
|
||||||
msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?")
|
msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?")
|
||||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
|
||||||
msg.setDefaultButton(QMessageBox.Yes)
|
|
||||||
|
|
||||||
result = msg.exec_()
|
msg.setStandardButtons(YES | NO)
|
||||||
return result == QMessageBox.Yes
|
msg.setDefaultButton(YES)
|
||||||
|
|
||||||
|
result = exec_dialog(msg)
|
||||||
|
return result == YES
|
||||||
|
|
||||||
def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str:
|
def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str:
|
||||||
"""Öffnet einen QFileDialog zur Dateiauswahl."""
|
|
||||||
pfad, _ = QFileDialog.getSaveFileName(
|
pfad, _ = QFileDialog.getSaveFileName(
|
||||||
parent=None,
|
parent=None,
|
||||||
caption=titel,
|
caption=titel,
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
# Importiert den Event-Loop und URL-Objekte aus der PyQt-Bibliothek von QGIS
|
# Linkpruefer.py – Qt5/Qt6-kompatibel über qt_compat
|
||||||
from qgis.PyQt.QtCore import QEventLoop, QUrl
|
|
||||||
# Importiert den NetworkAccessManager aus dem QGIS Core-Modul
|
from modules.qt_compat import (
|
||||||
from qgis.core import QgsNetworkAccessManager
|
QEventLoop,
|
||||||
# Importiert das QNetworkRequest-Objekt für HTTP-Anfragen
|
QUrl,
|
||||||
from qgis.PyQt.QtNetwork import QNetworkRequest
|
QNetworkRequest,
|
||||||
# Importiert die Klasse für das Ergebnisobjekt der Prüfung
|
QNetworkReply
|
||||||
from pruef_ergebnis import PruefErgebnis
|
)
|
||||||
|
|
||||||
|
from qgis.core import QgsNetworkAccessManager
|
||||||
|
from modules.pruef_ergebnis import PruefErgebnis
|
||||||
|
|
||||||
|
|
||||||
# Definiert die Klasse zum Prüfen von Links
|
|
||||||
class Linkpruefer:
|
class Linkpruefer:
|
||||||
"""Prüft den Link mit QgsNetworkAccessManager und klassifiziert Anbieter nach Attribut."""
|
"""Prüft den Link mit QgsNetworkAccessManager und klassifiziert Anbieter nach Attribut."""
|
||||||
|
|
||||||
# Statische Zuordnung möglicher Anbietertypen als Konstanten
|
|
||||||
ANBIETER_TYPEN: dict[str, str] = {
|
ANBIETER_TYPEN: dict[str, str] = {
|
||||||
"REST": "REST",
|
"REST": "REST",
|
||||||
"WFS": "WFS",
|
"WFS": "WFS",
|
||||||
@@ -19,76 +21,57 @@ class Linkpruefer:
|
|||||||
"OGR": "OGR"
|
"OGR": "OGR"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Konstruktor zum Initialisieren der Instanz
|
|
||||||
def __init__(self, link: str, anbieter: str):
|
def __init__(self, link: str, anbieter: str):
|
||||||
# Speichert den übergebenen Link als Instanzvariable
|
|
||||||
self.link = link
|
self.link = link
|
||||||
|
|
||||||
# Speichert den Anbietertyp, bereinigt und in Großbuchstaben (auch wenn leer oder None)
|
|
||||||
self.anbieter = anbieter.upper().strip() if anbieter else ""
|
self.anbieter = anbieter.upper().strip() if anbieter else ""
|
||||||
# Erstellt einen neuen NetworkAccessManager für Netzwerkverbindungen
|
|
||||||
self.network_manager = QgsNetworkAccessManager()
|
self.network_manager = QgsNetworkAccessManager()
|
||||||
|
|
||||||
# Methode zur Klassifizierung des Anbieters und der Quelle
|
|
||||||
def klassifiziere_anbieter(self):
|
def klassifiziere_anbieter(self):
|
||||||
# Bestimmt den Typ auf Basis der vorgegebenen Konstante oder nimmt den Rohwert
|
|
||||||
typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter)
|
typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter)
|
||||||
# Unterscheidet zwischen "remote" (http/https) oder "local" (Dateipfad)
|
|
||||||
quelle = "remote" if self.link.startswith(("http://", "https://")) else "local"
|
quelle = "remote" if self.link.startswith(("http://", "https://")) else "local"
|
||||||
# Gibt Typ und Quelle als Dictionary zurück
|
return {"typ": typ, "quelle": quelle}
|
||||||
return {
|
|
||||||
"typ": typ,
|
|
||||||
"quelle": quelle
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Prüft die Erreichbarkeit und Plausibilität des Links
|
|
||||||
def pruefe_link(self):
|
def pruefe_link(self):
|
||||||
# Initialisiert Listen für Fehler und Warnungen
|
|
||||||
fehler = []
|
fehler = []
|
||||||
warnungen = []
|
warnungen = []
|
||||||
|
|
||||||
# Prüft, ob ein Link übergeben wurde
|
|
||||||
if not self.link:
|
if not self.link:
|
||||||
fehler.append("Link fehlt.")
|
fehler.append("Link fehlt.")
|
||||||
return PruefErgebnis(False, fehler=fehler, warnungen=warnungen)
|
return PruefErgebnis(False, fehler=fehler, warnungen=warnungen)
|
||||||
|
|
||||||
# Prüft, ob ein Anbieter angegeben ist
|
|
||||||
if not self.anbieter or not self.anbieter.strip():
|
if not self.anbieter or not self.anbieter.strip():
|
||||||
fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.")
|
fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.")
|
||||||
|
|
||||||
# Prüfung für Remote-Links (http/https)
|
# Remote-Links prüfen
|
||||||
if self.link.startswith(("http://", "https://")):
|
if self.link.startswith(("http://", "https://")):
|
||||||
# Erstellt eine HTTP-Anfrage mit dem Link
|
|
||||||
request = QNetworkRequest(QUrl(self.link))
|
request = QNetworkRequest(QUrl(self.link))
|
||||||
# Startet eine HEAD-Anfrage über den NetworkManager
|
|
||||||
reply = self.network_manager.head(request)
|
reply = self.network_manager.head(request)
|
||||||
|
|
||||||
# Wartet synchron auf die Netzwerkanwort (Event Loop)
|
|
||||||
loop = QEventLoop()
|
loop = QEventLoop()
|
||||||
reply.finished.connect(loop.quit)
|
reply.finished.connect(loop.quit)
|
||||||
loop.exec_()
|
loop.exec() # Qt5/Qt6-kompatibel über qt_compat
|
||||||
|
|
||||||
# Prüft auf Netzwerkfehler
|
# Fehlerprüfung Qt5/Qt6-kompatibel
|
||||||
if reply.error():
|
if reply.error() != QNetworkReply.NetworkError.NoError:
|
||||||
fehler.append(f"Verbindungsfehler: {reply.errorString()}")
|
fehler.append(f"Verbindungsfehler: {reply.errorString()}")
|
||||||
else:
|
else:
|
||||||
# Holt den HTTP-Statuscode aus der Antwort
|
status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||||
status = reply.attribute(reply.HttpStatusCodeAttribute)
|
|
||||||
# Prüft, ob der Status außerhalb des Erfolgsbereichs liegt
|
|
||||||
if status is None or status < 200 or status >= 400:
|
if status is None or status < 200 or status >= 400:
|
||||||
fehler.append(f"Link nicht erreichbar: HTTP {status}")
|
fehler.append(f"Link nicht erreichbar: HTTP {status}")
|
||||||
# Räumt die Antwort auf (Vermeidung von Speicherlecks)
|
|
||||||
reply.deleteLater()
|
reply.deleteLater()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Plausibilitäts-Check für lokale Links (Dateien), prüft auf Dateiendung
|
# Lokale Pfade: Plausibilitätscheck
|
||||||
if "." not in self.link.split("/")[-1]:
|
if "." not in self.link.split("/")[-1]:
|
||||||
warnungen.append("Der lokale Link sieht ungewöhnlich aus.")
|
warnungen.append("Der lokale Link sieht ungewöhnlich aus.")
|
||||||
|
|
||||||
# Gibt das Ergebnisobjekt mit allen gesammelten Informationen zurück
|
return PruefErgebnis(
|
||||||
return PruefErgebnis(len(fehler) == 0, daten=self.klassifiziere_anbieter(), fehler=fehler, warnungen=warnungen)
|
len(fehler) == 0,
|
||||||
|
daten=self.klassifiziere_anbieter(),
|
||||||
|
fehler=fehler,
|
||||||
|
warnungen=warnungen
|
||||||
|
)
|
||||||
|
|
||||||
# Führt die Linkprüfung als externe Methode aus
|
|
||||||
def ausfuehren(self):
|
def ausfuehren(self):
|
||||||
# Gibt das Ergebnis der Prüf-Methode zurück
|
|
||||||
return self.pruefe_link()
|
return self.pruefe_link()
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
|
|
||||||
class PruefErgebnis:
|
|
||||||
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
|
|
||||||
self.erfolgreich = erfolgreich
|
|
||||||
self.daten = daten or {}
|
|
||||||
self.fehler = fehler or []
|
|
||||||
self.warnungen = warnungen or []
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (f"PruefErgebnis(erfolgreich={self.erfolgreich}, "
|
|
||||||
f"daten={self.daten}, fehler={self.fehler}, warnungen={self.warnungen})")
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#pruef_ergebnis.py
|
||||||
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
|
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
|
||||||
class PruefErgebnis:
|
class PruefErgebnis:
|
||||||
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
|
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
|
||||||
|
|||||||
111
modules/qt_compat.py
Normal file
111
modules/qt_compat.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
qt_compat.py – Einheitliche Qt-Kompatibilitätsschicht für QGIS-Plugins.
|
||||||
|
|
||||||
|
Ziele:
|
||||||
|
- PyQt6 bevorzugt
|
||||||
|
- Fallback auf PyQt5
|
||||||
|
- Mock-Modus, wenn kein Qt verfügbar ist (z. B. in Unittests)
|
||||||
|
- OR-fähige Fake-Enums im Mock-Modus
|
||||||
|
"""
|
||||||
|
|
||||||
|
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Versuch: PyQt6 importieren
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
try:
|
||||||
|
from PyQt6.QtWidgets import QMessageBox, QFileDialog
|
||||||
|
from PyQt6.QtCore import Qt, QEventLoop, QUrl
|
||||||
|
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
||||||
|
|
||||||
|
YES = QMessageBox.StandardButton.Yes
|
||||||
|
NO = QMessageBox.StandardButton.No
|
||||||
|
CANCEL = QMessageBox.StandardButton.Cancel
|
||||||
|
ICON_QUESTION = QMessageBox.Icon.Question
|
||||||
|
|
||||||
|
QT_VERSION = 6
|
||||||
|
|
||||||
|
def exec_dialog(dialog):
|
||||||
|
"""Einheitliche Ausführung eines Dialogs."""
|
||||||
|
return dialog.exec()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Versuch: PyQt5 importieren
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
||||||
|
from PyQt5.QtCore import Qt, QEventLoop, QUrl
|
||||||
|
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||||
|
|
||||||
|
YES = QMessageBox.Yes
|
||||||
|
NO = QMessageBox.No
|
||||||
|
CANCEL = QMessageBox.Cancel
|
||||||
|
ICON_QUESTION = QMessageBox.Question
|
||||||
|
|
||||||
|
QT_VERSION = 5
|
||||||
|
|
||||||
|
def exec_dialog(dialog):
|
||||||
|
return dialog.exec_()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Mock-Modus (kein Qt verfügbar)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
except Exception:
|
||||||
|
QT_VERSION = 0
|
||||||
|
|
||||||
|
class FakeEnum(int):
|
||||||
|
"""Ein OR-fähiger Enum-Ersatz für den Mock-Modus."""
|
||||||
|
def __or__(self, other):
|
||||||
|
return FakeEnum(int(self) | int(other))
|
||||||
|
|
||||||
|
class QMessageBox:
|
||||||
|
Yes = FakeEnum(1)
|
||||||
|
No = FakeEnum(2)
|
||||||
|
Cancel = FakeEnum(4)
|
||||||
|
Question = FakeEnum(8)
|
||||||
|
|
||||||
|
class QFileDialog:
|
||||||
|
"""Minimaler Mock für QFileDialog."""
|
||||||
|
@staticmethod
|
||||||
|
def getOpenFileName(*args, **kwargs):
|
||||||
|
return ("", "") # kein Dateipfad
|
||||||
|
|
||||||
|
YES = QMessageBox.Yes
|
||||||
|
NO = QMessageBox.No
|
||||||
|
CANCEL = QMessageBox.Cancel
|
||||||
|
ICON_QUESTION = QMessageBox.Question
|
||||||
|
|
||||||
|
def exec_dialog(dialog):
|
||||||
|
"""Mock-Ausführung: gibt YES zurück, außer Tests patchen es."""
|
||||||
|
return YES
|
||||||
|
# -------------------------
|
||||||
|
# Mock Netzwerk-Klassen
|
||||||
|
# -------------------------
|
||||||
|
class QEventLoop:
|
||||||
|
def exec(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class QUrl(str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class QNetworkRequest:
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
class QNetworkReply:
|
||||||
|
def __init__(self):
|
||||||
|
self._data = b""
|
||||||
|
|
||||||
|
def readAll(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def exec_dialog(dialog):
|
||||||
|
return YES
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
#stilpruefer.py
|
||||||
import os
|
import os
|
||||||
from pruef_ergebnis import PruefErgebnis
|
from modules.pruef_ergebnis import PruefErgebnis
|
||||||
|
|
||||||
|
|
||||||
class Stilpruefer:
|
class Stilpruefer:
|
||||||
|
|||||||
@@ -1,12 +1,106 @@
|
|||||||
|
#run_tests.py
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
import datetime
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
# Farben
|
||||||
|
RED = "\033[91m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
MAGENTA = "\033[95m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
# Globaler Testzähler
|
||||||
|
GLOBAL_TEST_COUNTER = 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Eigene TestResult-Klasse (färbt Fehler/Skipped/OK)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class ColoredTestResult(unittest.TextTestResult):
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
"""Vor jedem Test eine Nummer ausgeben."""
|
||||||
|
global GLOBAL_TEST_COUNTER
|
||||||
|
GLOBAL_TEST_COUNTER += 1
|
||||||
|
self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n")
|
||||||
|
super().startTest(test)
|
||||||
|
|
||||||
|
def startTestRun(self):
|
||||||
|
"""Wird einmal zu Beginn des gesamten Testlaufs ausgeführt."""
|
||||||
|
super().startTestRun()
|
||||||
|
|
||||||
|
def startTestClass(self, test):
|
||||||
|
"""Wird aufgerufen, wenn eine neue Testklasse beginnt."""
|
||||||
|
cls = test.__class__
|
||||||
|
file = inspect.getfile(cls)
|
||||||
|
filename = os.path.basename(file)
|
||||||
|
|
||||||
|
self.stream.write(
|
||||||
|
f"\n{MAGENTA}{'='*70}\n"
|
||||||
|
f"Starte Testklasse: {filename} → {cls.__name__}\n"
|
||||||
|
f"{'='*70}{RESET}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def addError(self, test, err):
|
||||||
|
super().addError(test, err)
|
||||||
|
self.stream.write(f"{RED}ERROR{RESET}\n")
|
||||||
|
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
super().addFailure(test, err)
|
||||||
|
self.stream.write(f"{RED}FAILURE{RESET}\n")
|
||||||
|
|
||||||
|
def addSkip(self, test, reason):
|
||||||
|
super().addSkip(test, reason)
|
||||||
|
self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n")
|
||||||
|
|
||||||
|
# unittest ruft diese Methode nicht automatisch auf → wir patchen es unten
|
||||||
|
def addSuccess(self, test):
|
||||||
|
super().addSuccess(test)
|
||||||
|
self.stream.write(f"{GREEN}OK{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Eigener TestRunner, der unser ColoredTestResult nutzt
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class ColoredTestRunner(unittest.TextTestRunner):
|
||||||
|
resultclass = ColoredTestResult
|
||||||
|
|
||||||
|
def _makeResult(self):
|
||||||
|
result = super()._makeResult()
|
||||||
|
|
||||||
|
# Patch: unittest ruft startTestClass nicht automatisch auf
|
||||||
|
original_start_test = result.startTest
|
||||||
|
|
||||||
|
def patched_start_test(test):
|
||||||
|
# Wenn neue Klasse → Kopf ausgeben
|
||||||
|
if not hasattr(result, "_last_test_class") or \
|
||||||
|
result._last_test_class != test.__class__:
|
||||||
|
result.startTestClass(test)
|
||||||
|
result._last_test_class = test.__class__
|
||||||
|
|
||||||
|
original_start_test(test)
|
||||||
|
|
||||||
|
result.startTest = patched_start_test
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Testlauf starten
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print(f"{CYAN}Testlauf gestartet am: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}")
|
||||||
|
print("="*70 + "\n")
|
||||||
|
|
||||||
# Projekt-Root dem Suchpfad hinzufügen
|
# Projekt-Root dem Suchpfad hinzufügen
|
||||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
if project_root not in sys.path:
|
if project_root not in sys.path:
|
||||||
sys.path.insert(0, project_root)
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
loader = unittest.TestLoader()
|
loader = unittest.TestLoader()
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
@@ -15,15 +109,17 @@ def main():
|
|||||||
"test_dateipruefer",
|
"test_dateipruefer",
|
||||||
"test_stilpruefer",
|
"test_stilpruefer",
|
||||||
"test_linkpruefer",
|
"test_linkpruefer",
|
||||||
# "test_pruefmanager" enthält QGIS-spezifische Funktionen
|
"test_qt_compat",
|
||||||
|
"test_pruefmanager",
|
||||||
]
|
]
|
||||||
|
|
||||||
for mod_name in test_modules:
|
for mod_name in test_modules:
|
||||||
mod = __import__(mod_name)
|
mod = __import__(mod_name)
|
||||||
suite.addTests(loader.loadTestsFromModule(mod))
|
suite.addTests(loader.loadTestsFromModule(mod))
|
||||||
|
|
||||||
runner = unittest.TextTestRunner(verbosity=2)
|
runner = ColoredTestRunner(verbosity=2)
|
||||||
runner.run(suite)
|
runner.run(suite)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#test_dateipruefer.py
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import sys
|
import sys
|
||||||
|
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
from Dateipruefer import (
|
sys.path.insert(0, ROOT)
|
||||||
|
from modules.Dateipruefer import (
|
||||||
Dateipruefer,
|
Dateipruefer,
|
||||||
LeererPfadModus,
|
LeererPfadModus,
|
||||||
DateiEntscheidung,
|
DateiEntscheidung,
|
||||||
|
|||||||
@@ -1,125 +1,78 @@
|
|||||||
# test/test_linkpruefer.py
|
#test_linkpruefer.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import sys
|
from unittest.mock import MagicMock, patch
|
||||||
from unittest.mock import patch
|
|
||||||
from qgis.PyQt.QtCore import QCoreApplication, QTimer
|
|
||||||
from qgis.PyQt.QtNetwork import QNetworkRequest
|
|
||||||
|
|
||||||
from linkpruefer import Linkpruefer
|
# QGIS-Module mocken, damit der Import funktioniert
|
||||||
|
with patch.dict("sys.modules", {
|
||||||
# Stelle sicher, dass eine Qt-App existiert
|
"qgis": MagicMock(),
|
||||||
app = QCoreApplication.instance()
|
"qgis.PyQt": MagicMock(),
|
||||||
if app is None:
|
"qgis.PyQt.QtCore": MagicMock(),
|
||||||
app = QCoreApplication(sys.argv)
|
"qgis.PyQt.QtNetwork": MagicMock(),
|
||||||
|
"qgis.core": MagicMock(),
|
||||||
|
}):
|
||||||
class DummyReply:
|
from modules.linkpruefer import Linkpruefer
|
||||||
"""Fake-Reply, um Netzwerkabfragen zu simulieren"""
|
|
||||||
HttpStatusCodeAttribute = QNetworkRequest.HttpStatusCodeAttribute
|
|
||||||
|
|
||||||
def __init__(self, error, status_code):
|
|
||||||
self._error = error
|
|
||||||
self._status_code = status_code
|
|
||||||
self.finished = self # Fake-Signal
|
|
||||||
|
|
||||||
def connect(self, slot):
|
|
||||||
# sorgt dafür, dass loop.quit() nach Start von loop.exec_() ausgeführt wird
|
|
||||||
QTimer.singleShot(0, slot)
|
|
||||||
|
|
||||||
def error(self):
|
|
||||||
return self._error
|
|
||||||
|
|
||||||
def errorString(self):
|
|
||||||
return "Simulierter Fehler" if self._error != 0 else ""
|
|
||||||
|
|
||||||
def attribute(self, attr):
|
|
||||||
if attr == self.HttpStatusCodeAttribute:
|
|
||||||
return self._status_code
|
|
||||||
return None
|
|
||||||
|
|
||||||
def deleteLater(self):
|
|
||||||
# kein echtes QObject → wir tun einfach nichts
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestLinkpruefer(unittest.TestCase):
|
class TestLinkpruefer(unittest.TestCase):
|
||||||
"""Tests für alle Funktionen des Linkpruefer"""
|
|
||||||
|
|
||||||
# ----------------------------
|
@patch("modules.linkpruefer.QNetworkReply")
|
||||||
# Remote-Tests mit DummyReply
|
@patch("modules.linkpruefer.QNetworkRequest")
|
||||||
# ----------------------------
|
@patch("modules.linkpruefer.QUrl")
|
||||||
@patch('linkpruefer.QgsNetworkAccessManager.head')
|
@patch("modules.linkpruefer.QEventLoop")
|
||||||
def test_remote_link_success(self, mock_head):
|
@patch("modules.linkpruefer.QgsNetworkAccessManager")
|
||||||
mock_head.return_value = DummyReply(0, 200)
|
def test_remote_link_ok(
|
||||||
|
self, mock_manager, mock_loop, mock_url, mock_request, mock_reply
|
||||||
|
):
|
||||||
|
# Setup: simulate successful HEAD request
|
||||||
|
reply_instance = MagicMock()
|
||||||
|
reply_instance.error.return_value = mock_reply.NetworkError.NoError
|
||||||
|
reply_instance.attribute.return_value = 200
|
||||||
|
|
||||||
checker = Linkpruefer("https://example.com/service", "REST")
|
mock_manager.return_value.head.return_value = reply_instance
|
||||||
result = checker.ausfuehren()
|
|
||||||
|
lp = Linkpruefer("http://example.com", "REST")
|
||||||
|
result = lp.pruefe_link()
|
||||||
|
|
||||||
self.assertTrue(result.erfolgreich)
|
self.assertTrue(result.erfolgreich)
|
||||||
self.assertEqual(result.daten['typ'], 'REST')
|
self.assertEqual(result.daten["quelle"], "remote")
|
||||||
self.assertEqual(result.daten['quelle'], 'remote')
|
|
||||||
self.assertEqual(result.fehler, [])
|
|
||||||
self.assertEqual(result.warnungen, [])
|
|
||||||
|
|
||||||
@patch('linkpruefer.QgsNetworkAccessManager.head')
|
@patch("modules.linkpruefer.QNetworkReply")
|
||||||
def test_remote_link_failure(self, mock_head):
|
@patch("modules.linkpruefer.QNetworkRequest")
|
||||||
mock_head.return_value = DummyReply(1, 404)
|
@patch("modules.linkpruefer.QUrl")
|
||||||
|
@patch("modules.linkpruefer.QEventLoop")
|
||||||
|
@patch("modules.linkpruefer.QgsNetworkAccessManager")
|
||||||
|
def test_remote_link_error(
|
||||||
|
self, mock_manager, mock_loop, mock_url, mock_request, mock_reply
|
||||||
|
):
|
||||||
|
# Fake-Reply erzeugen
|
||||||
|
reply_instance = MagicMock()
|
||||||
|
reply_instance.error.return_value = mock_reply.NetworkError.ConnectionRefusedError
|
||||||
|
reply_instance.errorString.return_value = "Connection refused"
|
||||||
|
|
||||||
checker = Linkpruefer("https://example.com/kaputt", "WMS")
|
# WICHTIG: finished-Signal simulieren
|
||||||
result = checker.ausfuehren()
|
reply_instance.finished = MagicMock()
|
||||||
|
reply_instance.finished.connect = MagicMock()
|
||||||
|
|
||||||
|
# Wenn loop.exec() aufgerufen wird, rufen wir loop.quit() sofort auf
|
||||||
|
mock_loop.return_value.exec.side_effect = lambda: mock_loop.return_value.quit()
|
||||||
|
|
||||||
|
# Manager gibt unser Fake-Reply zurück
|
||||||
|
mock_manager.return_value.head.return_value = reply_instance
|
||||||
|
|
||||||
|
lp = Linkpruefer("http://example.com", "REST")
|
||||||
|
result = lp.pruefe_link()
|
||||||
|
|
||||||
self.assertFalse(result.erfolgreich)
|
self.assertFalse(result.erfolgreich)
|
||||||
self.assertIn("Verbindungsfehler: Simulierter Fehler", result.fehler)
|
self.assertIn("Verbindungsfehler", result.fehler[0])
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Klassifizierungstests
|
|
||||||
# ----------------------------
|
|
||||||
def test_klassifiziere_anbieter_remote(self):
|
|
||||||
checker = Linkpruefer("https://beispiel.de", "wms")
|
|
||||||
daten = checker.klassifiziere_anbieter()
|
|
||||||
self.assertEqual(daten["typ"], "WMS")
|
|
||||||
self.assertEqual(daten["quelle"], "remote")
|
|
||||||
|
|
||||||
def test_klassifiziere_anbieter_local(self):
|
def test_local_link_warning(self):
|
||||||
checker = Linkpruefer("C:/daten/test.shp", "ogr")
|
lp = Linkpruefer("/path/to/file_without_extension", "OGR")
|
||||||
daten = checker.klassifiziere_anbieter()
|
result = lp.pruefe_link()
|
||||||
self.assertEqual(daten["typ"], "OGR")
|
|
||||||
self.assertEqual(daten["quelle"], "local")
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Lokale Links
|
|
||||||
# ----------------------------
|
|
||||||
def test_pruefe_link_local_ok(self):
|
|
||||||
checker = Linkpruefer("C:/daten/test.shp", "OGR")
|
|
||||||
result = checker.pruefe_link()
|
|
||||||
self.assertTrue(result.erfolgreich)
|
|
||||||
self.assertEqual(result.warnungen, [])
|
|
||||||
|
|
||||||
def test_pruefe_link_local_warnung(self):
|
|
||||||
checker = Linkpruefer("C:/daten/ordner/", "OGR")
|
|
||||||
result = checker.pruefe_link()
|
|
||||||
self.assertTrue(result.erfolgreich)
|
self.assertTrue(result.erfolgreich)
|
||||||
self.assertIn("ungewöhnlich", result.warnungen[0])
|
self.assertIn("ungewöhnlich", result.warnungen[0])
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Sonderfall: leerer Link
|
|
||||||
# ----------------------------
|
|
||||||
def test_pruefe_link_empty(self):
|
|
||||||
checker = Linkpruefer("", "REST")
|
|
||||||
result = checker.pruefe_link()
|
|
||||||
self.assertFalse(result.erfolgreich)
|
|
||||||
self.assertIn("Link fehlt.", result.fehler)
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# leerer Anbieter
|
|
||||||
# ----------------------------
|
|
||||||
def test_pruefe_link_leerer_anbieter(self):
|
|
||||||
checker = Linkpruefer("https://example.com/service", "")
|
|
||||||
result = checker.pruefe_link()
|
|
||||||
self.assertFalse(result.erfolgreich)
|
|
||||||
self.assertIn("Anbieter muss gesetzt werden und darf nicht leer sein.", result.fehler)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,36 +1,87 @@
|
|||||||
|
#test_pruefmanager.py
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
|
||||||
from pruefmanager import PruefManager
|
|
||||||
from Dateipruefer import DateiEntscheidung
|
|
||||||
import sys
|
import sys
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
from modules.Pruefmanager import PruefManager
|
||||||
|
from modules.Dateipruefer import DateiEntscheidung
|
||||||
|
import modules.qt_compat as qt_compat
|
||||||
|
|
||||||
|
|
||||||
|
# Skip-Decorator für Mock-Modus
|
||||||
|
def skip_if_mock(reason):
|
||||||
|
return unittest.skipIf(
|
||||||
|
qt_compat.QT_VERSION == 0,
|
||||||
|
f"{reason} — MOCK-Modus erkannt. "
|
||||||
|
"Bitte diesen Test in einer echten QGIS-Umgebung ausführen."
|
||||||
|
)
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
||||||
|
|
||||||
class TestPruefManager(unittest.TestCase):
|
class TestPruefManager(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.manager = PruefManager(plugin_pfad="/tmp")
|
self.manager = PruefManager(plugin_pfad="/tmp")
|
||||||
|
|
||||||
@patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.Yes)
|
# ---------------------------------------------------------
|
||||||
def test_frage_datei_ersetzen(self, mock_msgbox):
|
# Tests für frage_datei_ersetzen_oder_anhaengen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
|
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.YES)
|
||||||
|
def test_frage_datei_ersetzen(self, mock_exec):
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ERSETZEN)
|
self.assertEqual(entscheidung, DateiEntscheidung.ERSETZEN)
|
||||||
|
|
||||||
@patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.No)
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
def test_frage_datei_anhaengen(self, mock_msgbox):
|
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.NO)
|
||||||
|
def test_frage_datei_anhaengen(self, mock_exec):
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ANHAENGEN)
|
self.assertEqual(entscheidung, DateiEntscheidung.ANHAENGEN)
|
||||||
|
|
||||||
@patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.Cancel)
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
def test_frage_datei_abbrechen(self, mock_msgbox):
|
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.CANCEL)
|
||||||
|
def test_frage_datei_abbrechen(self, mock_exec):
|
||||||
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
||||||
self.assertEqual(entscheidung, DateiEntscheidung.ABBRECHEN)
|
self.assertEqual(entscheidung, DateiEntscheidung.ABBRECHEN)
|
||||||
|
|
||||||
@patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.Yes)
|
# ---------------------------------------------------------
|
||||||
def test_frage_temporär_verwenden_ja(self, mock_msgbox):
|
# Fehlerfall: exec_dialog liefert etwas Unerwartetes
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
|
@patch("modules.qt_compat.exec_dialog", return_value=999)
|
||||||
|
def test_frage_datei_unbekannte_antwort(self, mock_exec):
|
||||||
|
entscheidung = self.manager.frage_datei_ersetzen_oder_anhaengen("dummy.gpkg")
|
||||||
|
self.assertEqual(entscheidung, DateiEntscheidung.ABBRECHEN)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Tests für frage_temporär_verwenden
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
|
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.YES)
|
||||||
|
def test_frage_temporär_verwenden_ja(self, mock_exec):
|
||||||
self.assertTrue(self.manager.frage_temporär_verwenden())
|
self.assertTrue(self.manager.frage_temporär_verwenden())
|
||||||
|
|
||||||
@patch("PyQt5.QtWidgets.QMessageBox.exec_", return_value=QMessageBox.No)
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
def test_frage_temporär_verwenden_nein(self, mock_msgbox):
|
@patch("modules.qt_compat.exec_dialog", return_value=qt_compat.NO)
|
||||||
|
def test_frage_temporär_verwenden_nein(self, mock_exec):
|
||||||
self.assertFalse(self.manager.frage_temporär_verwenden())
|
self.assertFalse(self.manager.frage_temporär_verwenden())
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Fehlerfall: exec_dialog liefert etwas Unerwartetes
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
|
@patch("modules.qt_compat.exec_dialog", return_value=None)
|
||||||
|
def test_frage_temporär_verwenden_unbekannt(self, mock_exec):
|
||||||
|
self.assertFalse(self.manager.frage_temporär_verwenden())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|||||||
52
test/test_qgis.bat
Normal file
52
test/test_qgis.bat
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
echo BATCH WIRD AUSGEFÜHRT
|
||||||
|
pause
|
||||||
|
|
||||||
|
echo ================================================
|
||||||
|
echo Starte Tests in QGIS-Python-Umgebung
|
||||||
|
echo ================================================
|
||||||
|
|
||||||
|
REM Pfad zur QGIS-Installation
|
||||||
|
set QGIS_BIN=D:\OSGeo\bin
|
||||||
|
|
||||||
|
REM Prüfen, ob python-qgis.bat existiert
|
||||||
|
if not exist "%QGIS_BIN%\python-qgis.bat" (
|
||||||
|
echo.
|
||||||
|
echo [FEHLER] python-qgis.bat wurde nicht gefunden!
|
||||||
|
echo Erwarteter Pfad:
|
||||||
|
echo %QGIS_BIN%\python-qgis.bat
|
||||||
|
echo.
|
||||||
|
echo Bitte korrigiere den Pfad in test_qgis.bat.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [INFO] QGIS-Python gefunden. Starte Tests...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
"%QGIS_BIN%\python-qgis.bat" -m coverage run run_tests.py
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo [FEHLER] Testlauf fehlgeschlagen.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ================================================
|
||||||
|
echo Coverage HTML-Bericht wird erzeugt...
|
||||||
|
echo ================================================
|
||||||
|
|
||||||
|
"%QGIS_BIN%\python-qgis.bat" -m coverage html
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Fertig!
|
||||||
|
echo Öffne jetzt: coverage_html\index.html
|
||||||
|
echo ================================================
|
||||||
|
|
||||||
|
pause
|
||||||
|
endlocal
|
||||||
100
test/test_qt_compat.py
Normal file
100
test/test_qt_compat.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#test_qt_compat.py
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
import modules.qt_compat as qt_compat
|
||||||
|
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
def skip_if_mock(reason):
|
||||||
|
"""Decorator: überspringt Test, wenn qt_compat im Mock-Modus läuft."""
|
||||||
|
return unittest.skipIf(
|
||||||
|
qt_compat.QT_VERSION == 0,
|
||||||
|
f"{reason} — MOCK-Modus erkannt."
|
||||||
|
f"Bitte diesen Test in einer echten QGIS-Umgebung ausführen."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestQtCompat(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_exports_exist(self):
|
||||||
|
"""Prüft, ob alle erwarteten Symbole exportiert werden."""
|
||||||
|
expected = {
|
||||||
|
"QMessageBox",
|
||||||
|
"QFileDialog",
|
||||||
|
"QEventLoop",
|
||||||
|
"QUrl",
|
||||||
|
"QNetworkRequest",
|
||||||
|
"QNetworkReply",
|
||||||
|
"YES",
|
||||||
|
"NO",
|
||||||
|
"CANCEL",
|
||||||
|
"ICON_QUESTION",
|
||||||
|
"exec_dialog",
|
||||||
|
"QT_VERSION",
|
||||||
|
}
|
||||||
|
|
||||||
|
for symbol in expected:
|
||||||
|
self.assertTrue(
|
||||||
|
hasattr(qt_compat, symbol),
|
||||||
|
f"qt_compat sollte '{symbol}' exportieren"
|
||||||
|
)
|
||||||
|
|
||||||
|
@skip_if_mock("QT_VERSION kann im Mock-Modus nicht 5 oder 6 sein")
|
||||||
|
def test_qt_version_flag(self):
|
||||||
|
"""QT_VERSION muss 5 oder 6 sein."""
|
||||||
|
self.assertIn(qt_compat.QT_VERSION, (5, 6))
|
||||||
|
|
||||||
|
@skip_if_mock("Qt-Enums können im Mock-Modus nicht OR-kombiniert werden")
|
||||||
|
def test_enums_are_valid(self):
|
||||||
|
"""Prüft, ob die Enums gültige QMessageBox-Werte sind."""
|
||||||
|
|
||||||
|
msg = qt_compat.QMessageBox()
|
||||||
|
try:
|
||||||
|
msg.setStandardButtons(
|
||||||
|
qt_compat.YES |
|
||||||
|
qt_compat.NO |
|
||||||
|
qt_compat.CANCEL
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.fail(f"Qt-Enums sollten OR-kombinierbar sein, Fehler: {e}")
|
||||||
|
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
@skip_if_mock("exec_dialog benötigt echtes Qt-Verhalten")
|
||||||
|
def test_exec_dialog_calls_correct_method(self):
|
||||||
|
"""Prüft, ob exec_dialog() die richtige Methode aufruft."""
|
||||||
|
|
||||||
|
mock_msg = MagicMock()
|
||||||
|
|
||||||
|
if qt_compat.QT_VERSION == 6:
|
||||||
|
qt_compat.exec_dialog(mock_msg)
|
||||||
|
mock_msg.exec.assert_called_once()
|
||||||
|
|
||||||
|
elif qt_compat.QT_VERSION == 5:
|
||||||
|
qt_compat.exec_dialog(mock_msg)
|
||||||
|
mock_msg.exec_.assert_called_once()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.fail("QT_VERSION hat einen unerwarteten Wert.")
|
||||||
|
|
||||||
|
@skip_if_mock("Qt-Klassen können im Mock-Modus nicht real instanziiert werden")
|
||||||
|
def test_qt_classes_importable(self):
|
||||||
|
"""Prüft, ob die wichtigsten Qt-Klassen instanziierbar sind."""
|
||||||
|
|
||||||
|
loop = qt_compat.QEventLoop()
|
||||||
|
self.assertIsNotNone(loop)
|
||||||
|
|
||||||
|
url = qt_compat.QUrl("http://example.com")
|
||||||
|
self.assertTrue(url.isValid())
|
||||||
|
|
||||||
|
req = qt_compat.QNetworkRequest(url)
|
||||||
|
self.assertIsNotNone(req)
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(qt_compat.QNetworkReply, "NetworkError"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
|
#test_stilpruefer.py
|
||||||
import unittest
|
import unittest
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
from stilpruefer import Stilpruefer
|
import sys
|
||||||
from pruef_ergebnis import PruefErgebnis
|
|
||||||
|
|
||||||
|
|
||||||
|
# Plugin-Root ermitteln (ein Verzeichnis über "test")
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
from modules.stilpruefer import Stilpruefer
|
||||||
|
from modules.pruef_ergebnis import PruefErgebnis
|
||||||
class TestStilpruefer(unittest.TestCase):
|
class TestStilpruefer(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.pruefer = Stilpruefer()
|
self.pruefer = Stilpruefer()
|
||||||
|
|||||||
Reference in New Issue
Block a user