#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ test_omniflo_strecke.py - Validiert den Omniflo-Streckenzug. Testfall (tests/testdata/omniflo_strecke_tests.json): 4 Geraden -> 180-Grad-Kurve aus 4x 45-Grad-Boegen -> 4 Geraden Prueft (ohne BricsCAD): 1. Struktur: 4 + 4 + 4 = 12 Elemente in der richtigen Reihenfolge 2. Katalog: die verwendeten Bogen-SivasNummern existieren in data/json/omniflo_boegen.json mit passendem Radius/Kurvenwinkel 3. Geometrie: die Elemente sind lueckenlos verkettet (Ende[i] == Anfang[i+1]), die Kurve summiert auf genau 180 Grad, die Boegen liegen auf einem gemeinsamen Kreismittelpunkt (echte Halbkreis-Kurve) Optional (nach BricsCAD-Lauf c:TEST_OMNIFLO_STRECKE): 4. Integrationstest gegen tests/output/omniflo_strecke_results.json """ import json import math import os import pytest _TOL = 0.05 # mm / Grad Toleranz (JSON ist auf 2 Nachkommastellen gerundet) # --------------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------------- def _data_dir(): base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, "data") 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): try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except UnicodeDecodeError: with open(path, "r", encoding="cp1252") as f: return json.load(f) def _load_strecke(): return _load_json(os.path.join(_testdata_dir(), "omniflo_strecke_tests.json")) def _load_boegen_lookup(): boegen = _load_json(os.path.join(_data_dir(), "json", "omniflo_boegen.json")) return {str(b["Sivasnr"]): b for b in boegen} # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def strecke(): return _load_strecke() @pytest.fixture def boegen_lookup(): return _load_boegen_lookup() # --------------------------------------------------------------------------- # 1. Struktur des Streckenzugs # --------------------------------------------------------------------------- class TestStreckeStruktur: def test_gesamtzahl(self, strecke): """4 Geraden + 4 Boegen + 4 Geraden = 12 Elemente.""" assert len(strecke) == 12, f"Erwartet 12 Elemente, erhalten {len(strecke)}" def test_seq_lueckenlos(self, strecke): """seq muss lueckenlos bei 1 beginnend aufsteigen.""" for i, el in enumerate(strecke): assert el["seq"] == i + 1, f"Element {i}: seq={el['seq']}, erwartet {i + 1}" def test_gruppen_reihenfolge(self, strecke): """Reihenfolge: 4x gerade_start, 4x kurve_180, 4x gerade_ende.""" erwartet = (["gerade_start"] * 4 + ["kurve_180"] * 4 + ["gerade_ende"] * 4) ist = [el["gruppe"] for el in strecke] assert ist == erwartet, f"Gruppenfolge abweichend:\n Ist: {ist}\n Soll: {erwartet}" def test_typen(self, strecke): """gerade_* -> type 'gerade', kurve_180 -> type 'bogen'.""" for el in strecke: erwartet = "bogen" if el["gruppe"] == "kurve_180" else "gerade" assert el["type"] == erwartet, ( f"seq {el['seq']}: type={el['type']}, erwartet {erwartet}") def test_vier_geraden_je_seite(self, strecke): assert sum(1 for el in strecke if el["gruppe"] == "gerade_start") == 4 assert sum(1 for el in strecke if el["gruppe"] == "gerade_ende") == 4 def test_kurve_aus_mehreren_boegen(self, strecke): """Die 180-Grad-Kurve muss aus mehr als einem Bogen bestehen.""" boegen = [el for el in strecke if el["gruppe"] == "kurve_180"] assert len(boegen) > 1, "Kurve muss aus mehreren Boegen aufgebaut sein" # --------------------------------------------------------------------------- # 2. Katalog-Abgleich der Boegen # --------------------------------------------------------------------------- class TestStreckeKatalog: def test_bogen_sivasnr_im_katalog(self, strecke, boegen_lookup): for el in strecke: if el["type"] != "bogen": continue assert el["sivasnr"] in boegen_lookup, ( f"SivasNr {el['sivasnr']} nicht in omniflo_boegen.json") def test_bogen_radius_und_winkel(self, strecke, boegen_lookup): for el in strecke: if el["type"] != "bogen": continue eintrag = boegen_lookup[el["sivasnr"]] assert float(eintrag["Radius"]) == pytest.approx(el["radius"], abs=_TOL), ( f"{el['sivasnr']}: Radius Katalog={eintrag['Radius']}, Test={el['radius']}") assert float(eintrag["KurvenWinkel"]) == pytest.approx(el["winkel"], abs=_TOL), ( f"{el['sivasnr']}: Winkel Katalog={eintrag['KurvenWinkel']}, Test={el['winkel']}") # --------------------------------------------------------------------------- # 3. Geometrie der Verkettung # --------------------------------------------------------------------------- class TestStreckeGeometrie: def test_verkettung_lueckenlos(self, strecke): """Endpunkt jedes Elements == Anfangspunkt des naechsten (verkettet).""" for a, b in zip(strecke, strecke[1:]): assert a["x_ende"] == pytest.approx(b["x"], abs=_TOL), ( f"Luecke x zwischen seq {a['seq']} und {b['seq']}: " f"{a['x_ende']} != {b['x']}") assert a["y_ende"] == pytest.approx(b["y"], abs=_TOL), ( f"Luecke y zwischen seq {a['seq']} und {b['seq']}: " f"{a['y_ende']} != {b['y']}") assert a["drehung_ende"] == pytest.approx(b["drehung"], abs=_TOL), ( f"Richtungssprung zwischen seq {a['seq']} und {b['seq']}: " f"{a['drehung_ende']} != {b['drehung']}") def test_geraden_behalten_richtung(self, strecke): """Eine Gerade aendert die Fahrtrichtung nicht.""" for el in strecke: if el["type"] != "gerade": continue assert el["drehung"] == pytest.approx(el["drehung_ende"], abs=_TOL), ( f"seq {el['seq']}: Gerade darf Richtung nicht aendern") def test_geraden_laenge(self, strecke): """Die euklidische Distanz Anfang->Ende entspricht der Laenge.""" for el in strecke: if el["type"] != "gerade": continue dist = math.hypot(el["x_ende"] - el["x"], el["y_ende"] - el["y"]) assert dist == pytest.approx(el["laenge"], abs=0.5), ( f"seq {el['seq']}: Distanz {dist:.2f} != Laenge {el['laenge']}") def test_kurve_summiert_180_grad(self, strecke): """Die Boegen der Kurve summieren auf exakt 180 Grad.""" summe = sum(el["winkel"] for el in strecke if el["gruppe"] == "kurve_180") assert summe == pytest.approx(180.0, abs=_TOL), ( f"Kurvensumme {summe} Grad, erwartet 180") def test_kurve_dreht_fahrtrichtung_um_180(self, strecke): """Fahrtrichtung vor der Kurve + 180 == Fahrtrichtung danach.""" boegen = [el for el in strecke if el["gruppe"] == "kurve_180"] vor = boegen[0]["drehung"] nach = boegen[-1]["drehung_ende"] assert (nach - vor) == pytest.approx(180.0, abs=_TOL), ( f"Kurve dreht um {nach - vor} Grad, erwartet 180") def test_boegen_gemeinsamer_mittelpunkt(self, strecke): """Gleichradige 45-Grad-Boegen einer 180-Grad-Kurve teilen einen Mittelpunkt.""" mitten = [(el["x_mitte"], el["y_mitte"]) for el in strecke if el["gruppe"] == "kurve_180"] mx0, my0 = mitten[0] for mx, my in mitten[1:]: assert mx == pytest.approx(mx0, abs=_TOL) and my == pytest.approx(my0, abs=_TOL), ( f"Bogen-Mittelpunkt ({mx}, {my}) weicht von ({mx0}, {my0}) ab") def test_bogenpunkte_liegen_auf_kreis(self, strecke): """Anfangs-/Endpunkte der Boegen liegen im Radius-Abstand vom Mittelpunkt.""" for el in strecke: if el["gruppe"] != "kurve_180": continue for px, py in ((el["x"], el["y"]), (el["x_ende"], el["y_ende"])): d = math.hypot(px - el["x_mitte"], py - el["y_mitte"]) assert d == pytest.approx(el["radius"], abs=0.5), ( f"seq {el['seq']}: Punkt ({px}, {py}) Abstand {d:.2f} != Radius {el['radius']}") # --------------------------------------------------------------------------- # 4. Integrationstest gegen BricsCAD-Ergebnis (optional) # --------------------------------------------------------------------------- class TestStreckeResults: """Validiert omniflo_strecke_results.json aus c:TEST_OMNIFLO_STRECKE.""" @pytest.fixture def results(self): path = os.path.join(_output_dir(), "omniflo_strecke_results.json") if not os.path.exists(path): pytest.skip("omniflo_strecke_results.json nicht vorhanden - " "TEST_OMNIFLO_STRECKE in BricsCAD ausfuehren") data = _load_json(path) if not data: pytest.skip("omniflo_strecke_results.json ist leer") return data def test_alle_elemente_ausgefuehrt(self, strecke, results): assert len(results) == len(strecke), ( f"Erwartet {len(strecke)} Ergebnisse, erhalten {len(results)}") def test_alle_ok(self, results): for r in results: assert r["status"] == "OK", ( f'{r.get("test_id")} (seq {r.get("seq")}): Status={r["status"]}')