#!/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