From 2e72f1780694ed29610dd87d96663a49c4975fcb Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 11:21:06 +0200 Subject: [PATCH] =?UTF-8?q?Neue=20Testklassen=20f=C3=BCr=20den=20F=C3=B6rd?= =?UTF-8?q?erer=20gebaut:=20Status,=20Mathematik,=20ExportCSV=20und=20Expo?= =?UTF-8?q?rtSivas.=20Lisp/export.lsp=20wurde=20um=20VF=5F1,=20VF=5F2=20Bl?= =?UTF-8?q?=C3=B6cke=20erweitert,=20um=20ein=20csv=20zu=20erhalten.=20Expo?= =?UTF-8?q?rt=5Fsivas=20und=20export=5Fcsv=20entsprechend=20um=20die=20n?= =?UTF-8?q?=C3=B6tigen=20Daten=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 22 +++ tests/test_foerderer.py | 340 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 tests/test_foerderer.py diff --git a/tests/conftest.py b/tests/conftest.py index 5d3b1f5..9651941 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,28 @@ def _load_json(path): return json.load(f) +# --- Foerderer Fixtures --- + +@pytest.fixture +def foerderer_testdata(): + """Laedt die Foerderer-Testdefinitionen aus testdata/foerderer_tests.json.""" + path = os.path.join(_testdata_dir(), "foerderer_tests.json") + data = _load_json(path) + return [t for t in data if "test_id" in t] + + +@pytest.fixture +def foerderer_results(): + """Laedt die Foerderer-Ergebnisse aus output/foerderer_results.json.""" + 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") + data = _load_json(path) + if not data: + pytest.skip("foerderer_results.json ist leer") + return data + + # --- Omniflo Fixtures --- @pytest.fixture diff --git a/tests/test_foerderer.py b/tests/test_foerderer.py new file mode 100644 index 0000000..0cff335 --- /dev/null +++ b/tests/test_foerderer.py @@ -0,0 +1,340 @@ +#!/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_export.csv, falls vorhanden) + 5. Sivas-Export-Struktur (foerderer_sivas.csv, falls vorhanden) +""" + +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_export.csv) +# --------------------------------------------------------------------------- + +class TestFoerdererExportCSV: + """Prueft den CSV-Export aus BricsCAD (foerderer_export.csv).""" + + @pytest.fixture(autouse=True) + def setup(self): + csv_path = os.path.join(_output_dir(), "foerderer_export.csv") + if not os.path.exists(csv_path): + pytest.skip( + "foerderer_export.csv nicht vorhanden - " + "TEST_FOERDERER in BricsCAD ausfuehren (exportiert automatisch)" + ) + 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_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_sivas.csv) +# --------------------------------------------------------------------------- + +class TestFoerdererExportSivas: + """Prueft den Sivas-Export aus BricsCAD (foerderer_sivas.csv).""" + + @pytest.fixture(autouse=True) + def setup(self): + csv_path = os.path.join(_output_dir(), "foerderer_sivas.csv") + if not os.path.exists(csv_path): + pytest.skip( + "foerderer_sivas.csv nicht vorhanden - " + "TEST_FOERDERER in BricsCAD ausfuehren (exportiert automatisch)" + ) + 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", + "Dispgruppe", "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_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