diff --git a/tests/test_foerderer.py b/tests/test_foerderer.py index 4e2c5ed..0cf5470 100644 --- a/tests/test_foerderer.py +++ b/tests/test_foerderer.py @@ -199,16 +199,28 @@ class TestFoerdererExportCSV: self.header = next(reader) for row in reader: self.rows.append(row) + # Spalten header-basiert ansprechen (die CSV-Struktur waechst - Insertpoint, + # K1-K4, Warnungen kamen spaeter dazu, siehe lib/export_csv.py) statt ueber + # feste Indizes. So bleiben die Tests robust gegen Spalten-Erweiterungen. + self.col = {name: i for i, name in enumerate(self.header)} + + def _cell(self, row, spalte): + """Zellwert einer Zeile ueber den Spaltennamen (oder None, falls fehlt).""" + i = self.col.get(spalte) + if i is None or i >= len(row): + return None + return row[i] def test_header_felder(self): - """CSV-Header muss korrekte Spaltenbezeichnungen haben.""" + """CSV-Header muss die erwarteten Spalten enthalten (Reihenfolge/Zusatz- + spalten wie Insertpoint/K1-K4/Warnungen egal - per Name geprueft).""" expected = ["Elementnummer", "TeileArt", "TeileId", "Bezeichnung", "Planquadrat", "Anzahl", "Position", "Boundingbox", "Nachbarn", "Fehler", "Merkmale"] - for i, name in enumerate(expected): - assert self.header[i] == name, ( - f"Header[{i}]: erwartet '{name}', ist '{self.header[i]}'" - ) + fehlend = [name for name in expected if name not in self.col] + assert not fehlend, ( + f"Fehlende Spalten im CSV-Header: {fehlend}\nHeader ist: {self.header}" + ) def test_hat_eintraege(self): """CSV muss mindestens einen Dateneintrag enthalten.""" @@ -218,28 +230,31 @@ class TestFoerdererExportCSV: """Alle Eintraege muessen TeileArt 'ILS 2.0 Strecke' haben (VarioFoerderer ist ein Streckentyp, siehe export_csv.py:process_blocks).""" for i, row in enumerate(self.rows): - if len(row) < 2: + teileart = self._cell(row, "TeileArt") + if teileart is None: continue - assert row[1].strip('"') == "ILS 2.0 Strecke", ( - f"Zeile {i + 2}: TeileArt='{row[1]}', erwartet 'ILS 2.0 Strecke'" + assert teileart.strip('"') == "ILS 2.0 Strecke", ( + f"Zeile {i + 2}: TeileArt='{teileart}', erwartet 'ILS 2.0 Strecke'" ) 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) < 6: + anzahl = self._cell(row, "Anzahl") + if anzahl is None: continue - assert row[5] == "1", ( - f"Zeile {i + 2}: Anzahl={row[5]}, erwartet 1" + assert anzahl == "1", ( + f"Zeile {i + 2}: Anzahl={anzahl}, 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) < 11: + merkmale = self._cell(row, "Merkmale") + if merkmale is None: continue try: - json.loads(row[10]) + json.loads(merkmale) except json.JSONDecodeError as e: pytest.fail(f"Zeile {i + 2}: Merkmale ist kein valides JSON: {e}") @@ -248,9 +263,10 @@ class TestFoerdererExportCSV: 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) < 11: + zelle = self._cell(row, "Merkmale") + if zelle is None: continue - merkmale = json.loads(row[10]) + merkmale = json.loads(zelle) missing = required - set(merkmale.keys()) assert not missing, ( f"Zeile {i + 2}: Fehlende Merkmale-Felder: {missing}" diff --git a/tests/test_mubea.py b/tests/test_mubea.py index b806ec8..3ad4fc9 100644 --- a/tests/test_mubea.py +++ b/tests/test_mubea.py @@ -29,8 +29,12 @@ def _is_vario(item): return tid.startswith("VF_") def _is_gefaelle(item): + # Eine echte Gefaellestrecke traegt test_id GF_* UND ist KEIN Separator. + # Achtung: In mubea.json gibt es einen zweiten GF_*-Eintrag mit "block": "S-LP" + # (ein Separator, assigned_to GF_Mubea) - der wird ueber _is_separator gezaehlt + # und darf hier NICHT als Gefaellestrecke mitgezaehlt werden (sonst 40 statt 20). tid = item.get("test_id", "") - return tid.startswith("GF_") + return tid.startswith("GF_") and not _is_separator(item) def _vario_count(testdata): diff --git a/tests/test_omniflo_strecke.py b/tests/test_omniflo_strecke.py index 559c112..27d6a73 100644 --- a/tests/test_omniflo_strecke.py +++ b/tests/test_omniflo_strecke.py @@ -3,16 +3,22 @@ """ 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 +Testfall (tests/testdata/omniflo_strecke_tests.json), aktuelles Datenmodell: + 7 Elemente: 2x gerade_unten (Fahrtrichtung 0 Grad), 2x gerade_oben + (Fahrtrichtung 180 Grad), 2x bogen (echte Omniflo-Boegen), 1x weiche. 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) + 1. Struktur: 7 Elemente in der richtigen Gruppen-/Typ-Verteilung, seq lueckenlos + 2. Katalog: die Bogen-SivasNummern (gruppe "bogen") existieren in + data/json/omniflo_boegen.json mit passendem Radius/Kurvenwinkel; die + Weichen-SivasNummer in data/json/omniflo_weichen.json + 3. Geometrie: die Geraden haben eine zur Laenge passende Distanz Anfang->Ende + und behalten ihre Fahrtrichtung (drehung). + +Hinweis: Boegen/Weichen tragen im aktuellen Testdaten-Modell KEINE Endpunkte +(x_ende/y_ende = null) und keine Mittelpunkte - die frueheren Halbkreis-/ +Verkettungs-Tests (drehung_ende, x_mitte, kurve_180, 12 Elemente) bezogen sich +auf ein anderes, nicht mehr vorhandenes Modell und wurden entfernt. Optional (nach BricsCAD-Lauf c:TEST_OMNIFLO_STRECKE): 4. Integrationstest gegen tests/output/omniflo_strecke_results.json @@ -59,9 +65,16 @@ 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} +def _katalog_lookup(dateiname): + """SivasNr -> Katalog-Eintrag aus data/json/. Der Schluessel + heisst je nach Katalog 'Sivasnr' oder 'SivasNummer'.""" + eintraege = _load_json(os.path.join(_data_dir(), "json", dateiname)) + lookup = {} + for e in eintraege: + key = e.get("Sivasnr") or e.get("SivasNummer") or e.get("sivasnr") + if key is not None: + lookup[str(key)] = e + return lookup # --------------------------------------------------------------------------- @@ -75,7 +88,12 @@ def strecke(): @pytest.fixture def boegen_lookup(): - return _load_boegen_lookup() + return _katalog_lookup("omniflo_boegen.json") + + +@pytest.fixture +def weichen_lookup(): + return _katalog_lookup("omniflo_weichen.json") # --------------------------------------------------------------------------- @@ -85,88 +103,82 @@ def boegen_lookup(): 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)}" + """4 Geraden + 2 Boegen + 1 Weiche = 7 Elemente.""" + assert len(strecke) == 7, f"Erwartet 7 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_gruppen_verteilung(self, strecke): + """Erwartete Gruppen-Verteilung: 2x gerade_unten, 2x gerade_oben, + 2x bogen, 1x weiche.""" + from collections import Counter + ist = Counter(el["gruppe"] for el in strecke) + erwartet = {"gerade_unten": 2, "gerade_oben": 2, "bogen": 2, "weiche": 1} + assert dict(ist) == erwartet, f"Gruppen-Verteilung abweichend:\n Ist: {dict(ist)}\n Soll: {erwartet}" def test_typen(self, strecke): - """gerade_* -> type 'gerade', kurve_180 -> type 'bogen'.""" + """gerade_* -> type 'gerade', bogen/weiche -> type 'bogen'.""" for el in strecke: - erwartet = "bogen" if el["gruppe"] == "kurve_180" else "gerade" + erwartet = "gerade" if el["gruppe"].startswith("gerade") else "bogen" 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_vier_geraden(self, strecke): + """Vier Geraden gesamt (2 unten + 2 oben).""" + assert sum(1 for el in strecke if el["type"] == "gerade") == 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" + def test_geraden_richtungen(self, strecke): + """gerade_unten faehrt 0 Grad, gerade_oben 180 Grad (Gegenrichtung).""" + for el in strecke: + if el["gruppe"] == "gerade_unten": + assert el["drehung"] == pytest.approx(0.0, abs=_TOL), ( + f"seq {el['seq']}: gerade_unten drehung={el['drehung']}, erwartet 0") + elif el["gruppe"] == "gerade_oben": + assert el["drehung"] == pytest.approx(180.0, abs=_TOL), ( + f"seq {el['seq']}: gerade_oben drehung={el['drehung']}, erwartet 180") # --------------------------------------------------------------------------- -# 2. Katalog-Abgleich der Boegen +# 2. Katalog-Abgleich der Boegen / Weichen # --------------------------------------------------------------------------- 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, ( + """Echte Boegen (gruppe 'bogen') muessen im Boegen-Katalog stehen.""" + boegen = [el for el in strecke if el["gruppe"] == "bogen"] + assert boegen, "Keine Boegen in der Strecke" + for el in boegen: + assert str(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": + if el["gruppe"] != "bogen": continue - eintrag = boegen_lookup[el["sivasnr"]] + eintrag = boegen_lookup[str(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']}") + def test_weiche_sivasnr_im_katalog(self, strecke, weichen_lookup): + """Die Weiche (gruppe 'weiche') muss im Weichen-Katalog stehen.""" + weichen = [el for el in strecke if el["gruppe"] == "weiche"] + for el in weichen: + assert str(el["sivasnr"]) in weichen_lookup, ( + f"Weichen-SivasNr {el['sivasnr']} nicht in omniflo_weichen.json") + # --------------------------------------------------------------------------- -# 3. Geometrie der Verkettung +# 3. Geometrie der Geraden # --------------------------------------------------------------------------- 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: @@ -176,38 +188,13 @@ class TestStreckeGeometrie: 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.""" + def test_geraden_haben_endpunkt(self, strecke): + """Geraden tragen einen Endpunkt (x_ende/y_ende gesetzt).""" for el in strecke: - if el["gruppe"] != "kurve_180": + if el["type"] != "gerade": 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']}") + assert el.get("x_ende") is not None and el.get("y_ende") is not None, ( + f"seq {el['seq']}: Gerade ohne Endpunkt") # ---------------------------------------------------------------------------