Files
dxfmakros/tests/test_foerderer.py

341 lines
14 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_foerderer.py - Validiert VarioFoerderer Testergebnisse.
Prueft:
1. Status-Werte und Vollstaendigkeit (foerderer_results.json)
2. Mathematische Invarianten (Laengen, Winkel, Richtung)
3. Geometrisch unmoegliche Faelle
4. CSV-Export-Struktur (foerderer_tests_export.csv, aus TEST_EXPORT_ALL)
5. Sivas-Export-Struktur (foerderer_tests_sivas.csv, aus TEST_EXPORT_ALL)
"""
import csv
import json
import os
import pytest
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def _testdata_dir():
return os.getenv("DXFM_TESTDATA",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata"))
def _output_dir():
return os.getenv("DXFM_TESTOUT",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "output"))
def _load_json(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def _load_foerderer_results():
path = os.path.join(_output_dir(), "foerderer_results.json")
if not os.path.exists(path):
pytest.skip(
"foerderer_results.json nicht vorhanden - "
"TEST_FOERDERER in BricsCAD ausfuehren"
)
return _load_json(path)
def _load_foerderer_testdata():
path = os.path.join(_testdata_dir(), "foerderer_tests.json")
return _load_json(path)
# ---------------------------------------------------------------------------
# Klasse 1: Status und Vollstaendigkeit
# ---------------------------------------------------------------------------
class TestFoerdererStatus:
"""Prueft Status-Werte und Vollstaendigkeit der Ergebnisse."""
@pytest.fixture(autouse=True)
def setup(self):
self.results = _load_foerderer_results()
testdata = _load_foerderer_testdata()
self.tests = [t for t in testdata if "test_id" in t]
self.results_by_id = {r["test_id"]: r for r in self.results}
def test_alle_testfaelle_vorhanden(self):
"""Alle Testfaelle muessen in den Ergebnissen vorhanden sein."""
expected_ids = {t["test_id"] for t in self.tests}
result_ids = {r["test_id"] for r in self.results}
missing = expected_ids - result_ids
assert not missing, f"Fehlende Testfaelle in Ergebnissen: {missing}"
def test_status_werte_gueltig(self):
"""Status muss GEBAUT, NICHT_GEBAUT oder GEOMETRISCH_UNMOEGLICH sein."""
gueltige_status = {"GEBAUT", "NICHT_GEBAUT", "GEOMETRISCH_UNMOEGLICH"}
for r in self.results:
assert r["status"] in gueltige_status, (
f'{r["test_id"]}: Unbekannter Status "{r["status"]}"'
)
def test_geometrisch_unmoeglich_ab_ohne_hoehe(self):
"""VF_Ab_0 (Abwaerts ohne Hoehenunterschied) muss GEOMETRISCH_UNMOEGLICH sein."""
r = self.results_by_id.get("VF_Ab_0")
assert r is not None, "VF_Ab_0 fehlt in den Ergebnissen"
assert r["status"] == "GEOMETRISCH_UNMOEGLICH", (
f'VF_Ab_0: erwartet GEOMETRISCH_UNMOEGLICH, ist "{r["status"]}"'
)
def test_aufwaerts_flach_gebaut(self):
"""VF_Auf_0 (Aufwaerts, kein Hoehenunterschied) muss GEBAUT sein."""
r = self.results_by_id.get("VF_Auf_0")
assert r is not None, "VF_Auf_0 fehlt in den Ergebnissen"
assert r["status"] == "GEBAUT", (
f'VF_Auf_0: erwartet GEBAUT, ist "{r["status"]}"'
)
def test_mindestens_ein_gebaut(self):
"""Mindestens ein Testfall muss GEBAUT sein."""
gebaut = [r for r in self.results if r["status"] == "GEBAUT"]
assert len(gebaut) > 0, "Kein einziger Foerderer konnte gebaut werden"
def test_richtung_vorhanden(self):
"""Jedes Ergebnis muss das Feld 'richtung' enthalten."""
for r in self.results:
assert "richtung" in r, f'{r["test_id"]}: Feld "richtung" fehlt'
assert r["richtung"] in ("Auf", "Ab"), (
f'{r["test_id"]}: Richtung "{r["richtung"]}" ist nicht "Auf" oder "Ab"'
)
# ---------------------------------------------------------------------------
# Klasse 2: Mathematische Invarianten
# ---------------------------------------------------------------------------
class TestFoerdererMathematik:
"""Prueft mathematische Korrektheit der berechneten Werte."""
@pytest.fixture(autouse=True)
def setup(self):
self.results = _load_foerderer_results()
self.gebaut = [r for r in self.results if r["status"] == "GEBAUT"]
def test_gebaut_hat_winkel_und_laengen(self):
"""GEBAUT-Ergebnisse muessen winkel, L_GF und L_VF enthalten."""
for r in self.gebaut:
assert "winkel" in r, f'{r["test_id"]}: Feld "winkel" fehlt'
assert "L_GF" in r, f'{r["test_id"]}: Feld "L_GF" fehlt'
assert "L_VF" in r, f'{r["test_id"]}: Feld "L_VF" fehlt'
def test_laengen_positiv(self):
"""L_GF und L_VF muessen beide groesser als 0 sein."""
for r in self.gebaut:
assert r["L_GF"] > 0, (
f'{r["test_id"]}: L_GF={r["L_GF"]} muss > 0 sein'
)
assert r["L_VF"] > 0, (
f'{r["test_id"]}: L_VF={r["L_VF"]} muss > 0 sein'
)
def test_winkel_gueltig(self):
"""Winkel muss aus dem festen Satz gueltiger Foerderer-Winkel stammen."""
gueltige_winkel = {9, 15, 21, 27, 33, 39, 45, 51}
for r in self.gebaut:
assert r["winkel"] in gueltige_winkel, (
f'{r["test_id"]}: Winkel={r["winkel"]} nicht in {sorted(gueltige_winkel)}'
)
def test_aufwaerts_groesserer_deltaH_groesserer_winkel(self):
"""Aufwaerts: groesserer Hoehenunterschied erfordert groesseren oder gleichen Winkel."""
aufwaerts_gebaut = sorted(
[r for r in self.results if r["richtung"] == "Auf" and r["status"] == "GEBAUT"],
key=lambda r: r["deltaH"]
)
for i in range(1, len(aufwaerts_gebaut)):
prev = aufwaerts_gebaut[i - 1]
curr = aufwaerts_gebaut[i]
assert curr["winkel"] >= prev["winkel"], (
f'{curr["test_id"]}: Winkel={curr["winkel"]} < '
f'vorheriger Winkel={prev["winkel"]} bei groesserem deltaH'
)
def test_kein_winkel_bei_nicht_gebaut(self):
"""NICHT_GEBAUT und GEOMETRISCH_UNMOEGLICH duerfen keinen Winkel haben."""
for r in self.results:
if r["status"] in ("NICHT_GEBAUT", "GEOMETRISCH_UNMOEGLICH"):
assert "winkel" not in r, (
f'{r["test_id"]}: Status={r["status"]} hat unerwartet Winkel'
)
def test_deltaH_und_deltaL_vorhanden(self):
"""Alle Ergebnisse muessen deltaH und deltaL enthalten."""
for r in self.results:
assert "deltaH" in r, f'{r["test_id"]}: Feld "deltaH" fehlt'
assert "deltaL" in r, f'{r["test_id"]}: Feld "deltaL" fehlt'
assert r["deltaL"] > 0, f'{r["test_id"]}: deltaL muss > 0 sein'
assert r["deltaH"] >= 0, f'{r["test_id"]}: deltaH muss >= 0 sein'
# ---------------------------------------------------------------------------
# Klasse 3: CSV-Export-Pruefung (foerderer_tests_export.csv)
# ---------------------------------------------------------------------------
class TestFoerdererExportCSV:
"""Prueft den CSV-Export aus BricsCAD (foerderer_tests_export.csv, aus TEST_EXPORT_ALL)."""
@pytest.fixture(autouse=True)
def setup(self):
csv_path = os.path.join(_output_dir(), "foerderer_tests_export.csv")
if not os.path.exists(csv_path):
pytest.skip(
"foerderer_tests_export.csv nicht vorhanden - "
"TEST_FOERDERER und anschliessend TEST_EXPORT_ALL in BricsCAD ausfuehren"
)
self.rows = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
self.header = next(reader)
for row in reader:
self.rows.append(row)
def test_header_felder(self):
"""CSV-Header muss korrekte Spaltenbezeichnungen haben."""
expected = ["Elementnummer", "TeileArt", "TeileId",
"Bezeichnung", "Anzahl", "Merkmale"]
for i, name in enumerate(expected):
assert self.header[i] == name, (
f"Header[{i}]: erwartet '{name}', ist '{self.header[i]}'"
)
def test_hat_eintraege(self):
"""CSV muss mindestens einen Dateneintrag enthalten."""
assert len(self.rows) > 0, "foerderer_tests_export.csv enthaelt keine Daten"
def test_vario_teileart(self):
"""Alle Eintraege muessen TeileArt 'VarioFoerderer' haben."""
for i, row in enumerate(self.rows):
if len(row) < 2:
continue
assert row[1].strip('"') == "VarioFoerderer", (
f"Zeile {i + 2}: TeileArt='{row[1]}', erwartet 'VarioFoerderer'"
)
def test_anzahl_spalte_ist_eins(self):
"""Anzahl-Spalte muss fuer alle Zeilen '1' sein."""
for i, row in enumerate(self.rows):
if len(row) < 5:
continue
assert row[4] == "1", (
f"Zeile {i + 2}: Anzahl={row[4]}, erwartet 1"
)
def test_merkmale_sind_valides_json(self):
"""Merkmale-Spalte muss valides JSON enthalten."""
for i, row in enumerate(self.rows):
if len(row) < 6:
continue
try:
json.loads(row[5])
except json.JSONDecodeError as e:
pytest.fail(f"Zeile {i + 2}: Merkmale ist kein valides JSON: {e}")
def test_merkmale_felder(self):
"""Merkmale muessen alle Pflichtfelder enthalten."""
required = {"Typ", "Winkel", "DeltaH_mm", "DeltaL_mm",
"L_VF_m", "Hoehe_Von_mm", "Hoehe_Bis_mm"}
for i, row in enumerate(self.rows):
if len(row) < 6:
continue
merkmale = json.loads(row[5])
missing = required - set(merkmale.keys())
assert not missing, (
f"Zeile {i + 2}: Fehlende Merkmale-Felder: {missing}"
)
def test_anzahl_csv_zeilen_entspricht_gebaut(self):
"""CSV-Zeilenanzahl muss Anzahl GEBAUT-Testfaellen entsprechen."""
results = _load_foerderer_results()
anzahl_gebaut = sum(1 for r in results if r["status"] == "GEBAUT")
assert len(self.rows) == anzahl_gebaut, (
f"CSV hat {len(self.rows)} Zeilen, "
f"erwartet {anzahl_gebaut} (Anzahl GEBAUT-Faelle)"
)
# ---------------------------------------------------------------------------
# Klasse 4: Sivas-Export-Pruefung (foerderer_tests_sivas.csv)
# ---------------------------------------------------------------------------
class TestFoerdererExportSivas:
"""Prueft den Sivas-Export aus BricsCAD (foerderer_tests_sivas.csv, aus TEST_EXPORT_ALL)."""
@pytest.fixture(autouse=True)
def setup(self):
csv_path = os.path.join(_output_dir(), "foerderer_tests_sivas.csv")
if not os.path.exists(csv_path):
pytest.skip(
"foerderer_tests_sivas.csv nicht vorhanden - "
"TEST_FOERDERER und anschliessend TEST_EXPORT_ALL in BricsCAD ausfuehren"
)
self.rows = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
self.header = next(reader)
for row in reader:
self.rows.append(row)
def test_header_felder(self):
"""Sivas-CSV-Header muss korrekte Spalten haben."""
expected = ["Nr", "TeileArt", "SivasNummer",
"Dispogruppe", "Anzahl", "Laenge[mm]", "Hoehe[m]", "details"]
for i, name in enumerate(expected):
assert self.header[i] == name, (
f"Header[{i}]: erwartet '{name}', ist '{self.header[i]}'"
)
def test_hat_eintraege(self):
"""Sivas-CSV muss mindestens einen Dateneintrag enthalten."""
assert len(self.rows) > 0, "foerderer_tests_sivas.csv enthaelt keine Daten"
def test_strecke_zeile_vorhanden(self):
"""Eine Zeile mit TeileArt 'ILS 2.0 Strecke - Module' muss vorhanden sein."""
teilearten = [row[1].strip('"') for row in self.rows if len(row) >= 2]
assert "ILS 2.0 Strecke - Module" in teilearten, (
f"Keine Strecke-Zeile gefunden. Vorhandene: {teilearten}"
)
def test_automation_zeile_vorhanden(self):
"""Eine ILS Automation Summierungszeile muss vorhanden sein."""
teilearten = [row[1].strip('"') for row in self.rows if len(row) >= 2]
assert "ILS 2.0 Automation und Pneumatik - Gesamtanlage" in teilearten, (
f"Keine Automation-Zeile gefunden. Vorhandene: {teilearten}"
)
def test_details_sind_valides_json(self):
"""details-Spalte muss valides JSON enthalten."""
for i, row in enumerate(self.rows):
if len(row) < 8:
continue
try:
json.loads(row[7])
except json.JSONDecodeError as e:
pytest.fail(f"Zeile {i + 2}: details ist kein valides JSON: {e}")
def test_automation_details_felder(self):
"""Automation-Zeile muss Schluessel-Felder in details haben."""
required = {"Anzahl der einzelnen Variofoerderer",
"Laengen aller Variofoerderer",
"Anzahl aller Separatoren"}
for row in self.rows:
if len(row) < 8:
continue
if row[1].strip('"') == "ILS 2.0 Automation und Pneumatik - Gesamtanlage":
details = json.loads(row[7])
missing = required - set(details.keys())
assert not missing, (
f"Automation-details: fehlende Felder: {missing}"
)
break