#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ test_gefaellestrecke.py - Validiert GEFAELLESTRECKE Testergebnisse. Prueft: 1. Status-Werte und Vollstaendigkeit (gefaellestrecke_results.json) 2. Mathematische Invarianten (Hoehen, Winkel, deltaH) 3. Konsistenz zwischen den Modi (Modus 1 Richtungsunabhaengigkeit, Modus 3 uebernimmt deltaH/deltaL aus Modus 2) """ 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_gefaellestrecke_results(): path = os.path.join(_output_dir(), "gefaellestrecke_results.json") if not os.path.exists(path): pytest.skip( "gefaellestrecke_results.json nicht vorhanden - " "TEST_GEFAELLESTRECKE in BricsCAD ausfuehren" ) data = _load_json(path) if not data: pytest.skip("gefaellestrecke_results.json ist leer") return data def _load_gefaellestrecke_testdata(): path = os.path.join(_testdata_dir(), "gefaellestrecke_tests.json") return _load_json(path) # --------------------------------------------------------------------------- # Klasse 1: Status und Vollstaendigkeit # --------------------------------------------------------------------------- class TestGefaellestreckeStatus: """Prueft Status-Werte und Vollstaendigkeit der Ergebnisse.""" @pytest.fixture(autouse=True) def setup(self): self.results = _load_gefaellestrecke_results() testdata = _load_gefaellestrecke_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 oder FEHLER (bzw. FEHLER_LINIE) sein.""" gueltige_status = {"GEBAUT", "FEHLER", "FEHLER_LINIE"} for r in self.results: assert r["status"] in gueltige_status, ( f'{r["test_id"]}: Unbekannter Status "{r["status"]}"' ) def test_alle_modus1_gebaut(self): """Alle vier Modus-1-Richtungen (hz=0/90/180/270) muessen GEBAUT sein.""" modus1 = [r for r in self.results if r["modus"] == 1] assert len(modus1) == 4, f"Erwartet 4 Modus-1-Testfaelle, gefunden {len(modus1)}" for r in modus1: assert r["status"] == "GEBAUT", ( f'{r["test_id"]}: erwartet GEBAUT, ist "{r["status"]}"' ) def test_modus2_und_modus3_gebaut(self): """Modus 2 und Modus 3 muessen GEBAUT sein.""" for test_id in ("M2_hz000", "M3_hz270"): r = self.results_by_id.get(test_id) assert r is not None, f"{test_id} fehlt in den Ergebnissen" assert r["status"] == "GEBAUT", ( f'{test_id}: 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, "Keine einzige Gefaellestrecke konnte gebaut werden" # --------------------------------------------------------------------------- # Klasse 2: Mathematische Invarianten # --------------------------------------------------------------------------- class TestGefaellestreckeMathematik: """Prueft mathematische Korrektheit der berechneten Werte.""" @pytest.fixture(autouse=True) def setup(self): self.results = _load_gefaellestrecke_results() self.gebaut = [r for r in self.results if r["status"] == "GEBAUT"] self.results_by_id = {r["test_id"]: r for r in self.results} def test_gebaut_hat_pflichtfelder(self): """GEBAUT-Ergebnisse muessen alle Kernfelder enthalten.""" pflichtfelder = { "modus", "hz_grad", "deltaL_mm", "winkel_grad", "hoehe_von_mm", "hoehe_bis_mm", "delta_h_mm", } for r in self.gebaut: missing = pflichtfelder - set(r.keys()) assert not missing, f'{r["test_id"]}: fehlende Felder {missing}' def test_deltaL_positiv(self): """deltaL_mm muss fuer alle GEBAUT-Faelle groesser als 0 sein.""" for r in self.gebaut: assert r["deltaL_mm"] > 0, ( f'{r["test_id"]}: deltaL_mm={r["deltaL_mm"]} muss > 0 sein' ) def test_winkel_positiv(self): """Der berechnete Gefaellewinkel muss groesser als 0 Grad sein.""" for r in self.gebaut: assert r["winkel_grad"] > 0, ( f'{r["test_id"]}: winkel_grad={r["winkel_grad"]} muss > 0 sein' ) def test_delta_h_konsistent_mit_hoehen(self): """delta_h_mm muss der absoluten Differenz von hoehe_von/hoehe_bis entsprechen.""" for r in self.gebaut: erwartet = abs(r["hoehe_von_mm"] - r["hoehe_bis_mm"]) assert erwartet == r["delta_h_mm"], ( f'{r["test_id"]}: delta_h_mm={r["delta_h_mm"]}, ' f'erwartet |{r["hoehe_von_mm"]}-{r["hoehe_bis_mm"]}|={erwartet}' ) def test_hoehe_bis_kleiner_hoehe_von(self): """Bei einer Gefaellestrecke muss die Endhoehe unter der Starthoehe liegen.""" for r in self.gebaut: assert r["hoehe_bis_mm"] < r["hoehe_von_mm"], ( f'{r["test_id"]}: hoehe_bis_mm={r["hoehe_bis_mm"]} ' f'nicht < hoehe_von_mm={r["hoehe_von_mm"]}' ) # --------------------------------------------------------------------------- # Klasse 3: Konsistenz zwischen den Modi # --------------------------------------------------------------------------- class TestGefaellestreckeModusKonsistenz: """Prueft Zusammenhaenge zwischen Modus 1/2/3 (Richtungsunabhaengigkeit, Uebernahme von deltaH/deltaL aus Modus 2 in Modus 3).""" # Toleranz fuer die 2-Pass-Winkelkorrektur in Modus 3 (mm) DELTA_H_TOLERANZ_MM = 5 @pytest.fixture(autouse=True) def setup(self): self.results = _load_gefaellestrecke_results() self.results_by_id = {r["test_id"]: r for r in self.results} self.testdata = _load_gefaellestrecke_testdata() self.tests_by_id = {t["test_id"]: t for t in self.testdata if "test_id" in t} def test_modus1_richtungsunabhaengig(self): """Alle vier Modus-1-Richtungen muessen dieselbe Hoehe/Winkel liefern, da nur die Fahrtrichtung (hz), nicht die Geometrie variiert.""" modus1 = [r for r in self.results if r["modus"] == 1 and r["status"] == "GEBAUT"] assert len(modus1) >= 2, "Zu wenige Modus-1-Ergebnisse fuer Vergleich" referenz = modus1[0] for r in modus1[1:]: assert r["delta_h_mm"] == referenz["delta_h_mm"], ( f'{r["test_id"]}: delta_h_mm={r["delta_h_mm"]} weicht von ' f'{referenz["test_id"]}={referenz["delta_h_mm"]} ab (Richtungsabhaengigkeit!)' ) assert r["winkel_grad"] == referenz["winkel_grad"], ( f'{r["test_id"]}: winkel_grad={r["winkel_grad"]} weicht von ' f'{referenz["test_id"]}={referenz["winkel_grad"]} ab' ) def test_modus3_uebernimmt_deltaH_aus_modus2(self): """Modus 3 (Linienzug mit 2-Pass-Korrektur) muss innerhalb einer kleinen Toleranz dieselbe Hoehendifferenz wie die Quelle aus Modus 2 erreichen.""" m3_test = self.tests_by_id.get("M3_hz270") if m3_test is None: pytest.skip("M3_hz270 nicht in Testdaten definiert") quelle_id = m3_test.get("deltaH_quelle") m2 = self.results_by_id.get(quelle_id) m3 = self.results_by_id.get("M3_hz270") if m2 is None or m3 is None: pytest.skip("M2- oder M3-Ergebnis fehlt") if m2["status"] != "GEBAUT" or m3["status"] != "GEBAUT": pytest.skip("M2 oder M3 nicht GEBAUT") abweichung = abs(m3["delta_h_mm"] - m2["delta_h_mm"]) assert abweichung <= self.DELTA_H_TOLERANZ_MM, ( f'M3_hz270: delta_h_mm={m3["delta_h_mm"]} weicht um {abweichung} mm ' f'von {quelle_id}={m2["delta_h_mm"]} ab ' f'(Toleranz {self.DELTA_H_TOLERANZ_MM} mm)' )