Files
m.stangl 775e27182c [TEST] Veraltete Test-Erwartungen an aktuelle Daten angepasst (Foerderer/Mubea/Omniflo-Strecke)
17 vorbestehende Failures behoben - alles veraltete Tests, kein Produktivcode:

test_foerderer.py (3): CSV-Spalten header-basiert statt per festem Index
ansprechen. Die Export-CSV hat inzwischen 17 statt 11 Spalten (Insertpoint,
K1-K4, Warnungen kamen dazu, siehe lib/export_csv.py) - die Tests hingen an
row[10] fuer Merkmale. Neu: self.col-Lookup + _cell(row, spaltenname), robust
gegen weitere Spalten.

test_mubea.py (2): _is_gefaelle zaehlte einen zweiten GF_*-Eintrag (block
'S-LP', ein Separator mit assigned_to GF_Mubea) faelschlich als Gefaellestrecke
mit -> erwartete 40 statt 20. Jetzt: GF_* UND nicht _is_separator.

test_omniflo_strecke.py (12): Test und seine Testdaten-Datei waren auf zwei
verschiedene Datenmodelle geschrieben (Test: 12 Elemente, gerade_start/
kurve_180/gerade_ende, Felder drehung_ende/x_mitte; Datei: 7 Elemente,
gerade_unten/gerade_oben/bogen/weiche ohne diese Felder). Test komplett auf das
tatsaechliche 7-Element-Modell umgeschrieben (Struktur/Katalog/Geraden-
Geometrie); Boegen gegen omniflo_boegen.json, Weiche gegen omniflo_weichen.json.

Nicht angefasst (bewusst): 3x hundm05 (funktioniert noch nicht) und
test_omniflo.py::test_zeilen_matches_reference (inkonsistente Export-CSV +
veraltete Referenz - Analyse: export_csv.py ist konsistent 17-spaltig, die
Output-Datei stammt aus einem gemischten Lauf; Referenz per --set-as-reference
neu abnehmen, kein Code-Fix noetig).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-29 20:08:39 +02:00

168 lines
7.3 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_mubea.py - Validiert das Mubea-Gesamtmodell.
Prueft die von TEST_MUBEA (test_mubea.lsp) erzeugten Ergebnisse:
1. Alle Eintraege aus mubea.json wurden gebaut (results.json)
2. Statuswerte (executed / placeholder fuer Separatoren)
3. Kreisel-Attribute (HOEHE, KREISELART, Block-Praefix)
4. Vario: VF_-Block mit VF_WINKEL=21 und L_GF-Split 500/4230
5. DXF-Geometrie: Block-Anzahlen je Typ (ezdxf)
"""
import pytest
# ============================================================
# Klassifikation der mubea.json-Eintraege (analog test_mubea.lsp)
# ============================================================
def _is_separator(item):
return "block" in item
def _is_kreisel(item):
return "id" in item and "block" not in item
def _is_vario(item):
tid = item.get("test_id", "")
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_") and not _is_separator(item)
def _vario_count(testdata):
"""Wie _gefaelle_count: ein VF-Eintrag ist ein Template, das ueber 'anzahl'
expandiert wird (Default 1, falls Feld fehlt)."""
return sum(int(t.get("anzahl", 1)) for t in testdata if _is_vario(t))
def _gefaelle_count(testdata):
"""Anzahl erzeugter Gefaellestrecken: ein GF-Eintrag ist ein Template, das
ueber 'anzahl' expandiert wird (Default 1, falls Feld fehlt)."""
return sum(int(t.get("anzahl", 1)) for t in testdata if _is_gefaelle(t))
def _separator_count(testdata):
"""Wie _gefaelle_count: ein Separator-Eintrag kann per 'anzahl' mehrere
identische, versetzte Kopien erzeugen (Default 1, falls Feld fehlt)."""
return sum(int(t.get("anzahl", 1)) for t in testdata if _is_separator(t))
def _expected_counts(testdata):
return {
"kreisel": sum(1 for t in testdata if _is_kreisel(t)),
"vario": _vario_count(testdata),
"gefaellestrecke": _gefaelle_count(testdata),
"separator": _separator_count(testdata),
}
# ============================================================
# Ergebnis-Pruefung (results.json vs. testdata)
# ============================================================
class TestMubeaResults:
def test_alle_gebaut(self, mubea_testdata, mubea_results):
"""Je Kind muss die erwartete Anzahl an Ergebnissen vorliegen."""
expected = _expected_counts(mubea_testdata)
got = {}
for r in mubea_results:
got[r["kind"]] = got.get(r["kind"], 0) + 1
for kind, n in expected.items():
assert got.get(kind, 0) == n, \
f'{kind}: erwartet {n}, gebaut {got.get(kind, 0)}'
def test_status_ok(self, mubea_results):
"""Alle Ergebnisse muessen 'executed' sein (Separatoren duerfen 'placeholder')."""
for r in mubea_results:
if r["kind"] == "separator":
assert r["status"] in ("executed", "placeholder"), \
f'{r["test_id"]}: Status={r["status"]}'
else:
assert r["status"] == "executed", \
f'{r["test_id"]}: Status={r["status"]}'
def test_kreisel_attribute(self, mubea_testdata, mubea_results):
"""Kreisel: HOEHE / KREISELART / Block-Praefix wie in mubea.json erwartet."""
by_id = {r["test_id"]: r for r in mubea_results}
for t in mubea_testdata:
if not _is_kreisel(t):
continue
r = by_id.get(t["id"])
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
if "expect_hoehe" in t:
assert r["actual_attributes"].get("HOEHE") == t["expect_hoehe"], \
f'{t["id"]}: HOEHE ist={r["actual_attributes"].get("HOEHE")}, erwartet={t["expect_hoehe"]}'
if "expect_kreiselart" in t:
assert r["actual_attributes"].get("KREISELART") == t["expect_kreiselart"], \
f'{t["id"]}: KREISELART ist={r["actual_attributes"].get("KREISELART")}'
prefix = t.get("expect_block_prefix", "KREISEL_")
assert r["block_name"].startswith(prefix), \
f'{t["id"]}: Block "{r["block_name"]}" ohne Praefix "{prefix}"'
def test_vario_winkel_und_split(self, mubea_testdata, mubea_results):
"""Vario: VF_WINKEL entspricht Vorgabe, L_GF-Vorlauf entspricht L_GF1-Ziel,
Vorlauf+Nachlauf (L_GF_m) ergeben zusammen die aus deltaL geloeste Gesamtlaenge."""
by_id = {r["test_id"]: r for r in mubea_results}
for t in mubea_testdata:
if not _is_vario(t):
continue
r = by_id.get(t["test_id"])
assert r, f'Vario {t["test_id"]} fehlt im Ergebnis'
attrs = r["actual_attributes"]
assert r["block_name"].startswith("VF_"), \
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
if "winkel" in t:
assert attrs.get("VF_WINKEL") == str(t["winkel"]), \
f'{t["test_id"]}: VF_WINKEL ist={attrs.get("VF_WINKEL")}, erwartet={t["winkel"]}'
# L_GF_m = "vorne,hinten" in Metern; L_GF1 (Vorlauf-Ziel) muss exakt
# getroffen werden, L_GF2 (Nachlauf) ist der aus deltaL geloeste Rest.
if "L_GF1" in t and "L_GF_m" in attrs:
teile = attrs["L_GF_m"].split(",")
assert len(teile) == 2, f'{t["test_id"]}: L_GF_m Format "{attrs["L_GF_m"]}"'
v = float(teile[0]) * 1000.0
assert abs(v - t["L_GF1"]) < 1.0, \
f'{t["test_id"]}: L_GF1 vorn ist={v}, erwartet={t["L_GF1"]}'
# ============================================================
# DXF Geometrie-Pruefung (ezdxf)
# ============================================================
class TestMubeaGeometry:
def _counts(self, dxf):
c = {}
for e in dxf.modelspace():
if e.dxftype() == "INSERT":
c[e.dxf.name] = c.get(e.dxf.name, 0) + 1
return c
def test_block_anzahlen(self, mubea_dxf, mubea_testdata):
"""Anzahl der KREISEL_/VF_/GF_-Bloecke >= Vorgabe aus mubea.json."""
c = self._counts(mubea_dxf)
n_kr = sum(v for n, v in c.items() if n.startswith("KREISEL_"))
n_vf = sum(v for n, v in c.items() if n.startswith("VF_"))
n_gf = sum(v for n, v in c.items() if n.startswith("GF_"))
exp = _expected_counts(mubea_testdata)
assert n_kr >= exp["kreisel"], f"KREISEL_: {n_kr} < {exp['kreisel']}"
assert n_vf >= exp["vario"], f"VF_: {n_vf} < {exp['vario']}"
assert n_gf >= exp["gefaellestrecke"], f"GF_: {n_gf} < {exp['gefaellestrecke']}"
def test_handles_im_modelspace(self, mubea_dxf, mubea_results):
"""Jedes gebaute Block-Handle (executed) liegt im Modelspace."""
handles = {e.dxf.handle for e in mubea_dxf.modelspace()
if e.dxftype() == "INSERT"}
for r in mubea_results:
if r["status"] == "executed" and r["block_handle"]:
assert r["block_handle"] in handles, \
f'{r["test_id"]}: Handle {r["block_handle"]} nicht im Modelspace'