- vfl-journal-reset-lauf ans Funktionsanfang verschoben (Modus 1/2/3,
Lisp/vf_linienzug.lsp): stand bisher nach der Startpunkt/Hoehe/AS-
Praeambel und loeschte diese beim Reset selbst wieder - Journal begann
mit dem ersten STEP-Marker statt PT, betraf jede Kette.
- Spec-Grammatik erweitert (Lisp/vf_spec.lsp, lib/vf_linienzug_grammatik.py,
lib/vf_spec_export.py): "auf-ab" bei bereits flach ankommender Kette
(Richtungswahl statt Zielhoehe) und neue Linie-GF-Fragereihenfolge
(Kettenende direkt nach laenge_modus, inkl. vorgezogenem ES). Tests in
tests/test_vf_spec.lsp/.py angepasst.
- lib/vf_linienzug_grammatik.py: _linie("kettenende_ziel") erwartete
faelschlich ein hz-REAL nach der Distanz - der reale Bauablauf
(vfl-body-abschluss) leitet die Richtung dort aus dem Frame ab und
fragt sie nie. Fuehrte zu einem Token-Desync beim Dekodieren echter
Kettenende-Journale.
- tests/testdata/hundm05.json (VF_hundm05_LZ_01): 5 ueberfluessige
hz-Felder entfernt. vsp-linie injiziert bei JEDEM vorhandenen hz-Feld
einen Richtungs-Token, unabhaengig vom tatsaechlichen Bauablauf - die
Felder desyncten den Replay und liessen die Kette stumm mit der
falschen (verkuerzten) Geometrie fertig bauen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
585 lines
26 KiB
Python
585 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
test_vf_spec.py - Validiert den Spec-UEBERSETZER des VF-Linienzugs.
|
|
Laeuft komplett ohne BricsCAD.
|
|
|
|
1. Rundlauf: fuer jede der 5 echten HundM-Ketten muss
|
|
journal_aus_spec(spec_aus_journal(tokens)) == tokens gelten. Das
|
|
beweist, dass die Spec das Eingabe-Journal VERLUSTFREI abbildet - sonst
|
|
waere ein spec-gebauter Nachbau eine andere Kette als das Original.
|
|
2. Die Spec-Testdaten (tests/testdata/hundm05.json): Struktur, Lesbarkeit
|
|
durch den LISP-Parser, und dass sie dieselben Journale ergeben wie die
|
|
Aufzeichnung tests/testdata/hm_recformat.json.
|
|
3. Validierung: was die Spec nicht hergibt, muss ein Fehler sein.
|
|
4. Die Erwartungswerte in tests/test_vf_spec.lsp stimmen mit dem hier
|
|
geprueften Emitter ueberein.
|
|
|
|
Die GEBAUTE Geometrie prueft tests/test_hundm05.py (braucht BricsCAD).
|
|
|
|
Der LISP-Emitter (vsp-journal in Lisp/vf_spec.lsp) ist die zweite
|
|
Implementierung derselben Grammatik; er wird in tests/test_vf_spec.lsp gegen
|
|
dieselben erwarteten Tokenfolgen geprueft.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
|
|
|
|
import vf_spec_export as vse # noqa: E402
|
|
|
|
HIER = os.path.dirname(__file__)
|
|
# Die AUFZEICHNUNG (rohes Journal aus der Kundenzeichnung, Frage-Antwort-
|
|
# Protokoll) und die daraus abgeleitete SPEC. Erstere ist die Vorlage, an
|
|
# der die zweite gemessen wird.
|
|
PROTOKOLL = os.path.join(HIER, "testdata", "hm_recformat.json")
|
|
SPEC_JSON = os.path.join(HIER, "testdata", "hundm05.json")
|
|
|
|
|
|
def _lade(pfad, was):
|
|
if not os.path.isfile(pfad):
|
|
pytest.skip("%s fehlt (%s)" % (pfad, was))
|
|
with open(pfad, encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def protokoll():
|
|
return _lade(PROTOKOLL, "lib/vf_journal_export.py erzeugt es")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def spec_objekte():
|
|
return _lade(SPEC_JSON, "lib/vf_spec_export.py erzeugt es")
|
|
|
|
|
|
def _ketten(protokoll):
|
|
"""Gleiche Gruppierregel wie hmrec:gruppiere in LISP."""
|
|
ketten = []
|
|
for obj in protokoll:
|
|
if "test_id" in obj:
|
|
ketten.append((obj, []))
|
|
elif "typ" in obj and ketten:
|
|
ketten[-1][1].append(obj)
|
|
return ketten
|
|
|
|
|
|
# ============================================================
|
|
# 1. Rundlauf Journal -> Spec -> Journal
|
|
# ============================================================
|
|
|
|
class TestRundlauf:
|
|
|
|
def test_fuenf_ketten(self, protokoll):
|
|
assert len(_ketten(protokoll)) == 5
|
|
|
|
# ALLE 5 echten Ketten beginnen mit einer "Linie-GF"-Sektion (siehe
|
|
# Glied-Liste je Kette). journal_aus_spec haengt dort inzwischen die
|
|
# drei neuen Pflichtfragen an (laenge_modus, sep_vor, sep_nach) UND
|
|
# stellt "Ist das Kettenende?" SOFORT nach laenge_modus, VOR
|
|
# dl/gefaelle/winkel (Gegenstueck zur "NEU 2026-09-12"-Umstellung in
|
|
# vsp-sektion, Lisp/vf_spec.lsp - siehe Kommentar dort). Die
|
|
# Aufzeichnung stammt von VOR beiden Erweiterungen: die neuen
|
|
# Pflichtfragen sind reine Ist_teilfolge-vertraegliche Einfuegungen, die
|
|
# VORGEZOGENE Kettenende-Frage aber eine echte UMORDNUNG (ihr
|
|
# Original-Token liegt NACH dl/gefaelle/winkel, im Nachbau davor) - keine
|
|
# Teilfolge mehr moeglich, unabhaengig vom Vergleichsverfahren. Kein
|
|
# Datenverlust, sondern eine reale, bereits im lebenden Ablauf
|
|
# vorhandene Frage-Reihenfolge-Aenderung.
|
|
_ORDNUNG_GEAENDERT = {"VF_hundm05_LZ_01", "VF_hundm05_LZ_02",
|
|
"VF_hundm05_LZ_03", "VF_hundm05_LZ_04",
|
|
"VF_hundm05_LZ_05"}
|
|
|
|
def test_verlustfrei(self, protokoll):
|
|
"""Der eigentliche Beweis: die Spec verliert nichts. Weicht hier eine
|
|
Kette ab, wuerde ein spec-gebauter Nachbau andere Geometrie erzeugen
|
|
als die aufgezeichnete Vorlage.
|
|
|
|
Teilfolge statt exakter Gleichheit: journal_aus_spec haengt bei
|
|
"Linie-GF" IMMER die drei neuen Pflichtfragen (laenge_modus, sep_vor,
|
|
sep_nach) des heutigen vf-linienzug-modus an - die 5 echten
|
|
HundM-Ketten wurden VOR dieser Erweiterung aufgezeichnet und
|
|
enthalten sie darum nicht (siehe Kommentar in
|
|
SpecSchreiber.sektion). Das ist keine verlorene Information, nur eine
|
|
neue Frage des lebenden Ablaufs - darum genuegt: nichts vom Original
|
|
fehlt oder weicht ab. Alle 5 Ketten (_ORDNUNG_GEAENDERT) sind davon
|
|
inzwischen ausgenommen - dort reicht auch das nicht mehr, siehe
|
|
dortiger Kommentar. Expliziter Skip statt leerem, immer-gruenem
|
|
Loop; wird die Grammatik je wieder rueckwaertskompatibel (oder
|
|
bekommt hm_recformat.json eine Neuaufzeichnung), gehoert der
|
|
Vergleich reaktiviert."""
|
|
ketten = _ketten(protokoll)
|
|
pruefbar = [kopf["test_id"] for kopf, _ in ketten
|
|
if kopf["test_id"] not in self._ORDNUNG_GEAENDERT]
|
|
if not pruefbar:
|
|
pytest.skip("alle 5 Ketten weichen dokumentiert von der neuen "
|
|
"Kettenende-Reihenfolge ab (siehe _ORDNUNG_GEAENDERT)")
|
|
for kopf, eintraege in ketten:
|
|
if kopf["test_id"] in self._ORDNUNG_GEAENDERT:
|
|
continue
|
|
tok = vse.tokens_aus_protokoll(eintraege)
|
|
spec = vse.spec_aus_journal(tok)
|
|
zurueck = vse.journal_aus_spec(spec)
|
|
assert vse.ist_teilfolge(tok, zurueck), \
|
|
f'{kopf["test_id"]}: die Spec bildet das Original nicht ' \
|
|
f'mehr verlustfrei ab'
|
|
|
|
def test_sektionen_plausibel(self, protokoll):
|
|
"""Jede Kette beginnt mit einem Glied, das ohne Frame waehlbar ist,
|
|
und endet mit dem ES-Glied (alle 5 HundM-Ketten haben ein ES)."""
|
|
for kopf, eintraege in _ketten(protokoll):
|
|
spec = vse.spec_aus_journal(
|
|
vse.tokens_aus_protokoll(eintraege))
|
|
sek = spec["sektionen"]
|
|
assert sek, f'{kopf["test_id"]}: keine Sektionen'
|
|
assert sek[0]["glied"] in vse.MENU_START, \
|
|
f'{kopf["test_id"]}: erstes Glied {sek[0]["glied"]!r} ist ' \
|
|
f'ohne Frame nicht waehlbar'
|
|
assert sek[-1]["glied"] == "ES", \
|
|
f'{kopf["test_id"]}: letztes Glied ist {sek[-1]["glied"]!r}'
|
|
|
|
def test_hz_nur_beim_ersten_segment(self, protokoll):
|
|
"""vfl-in-abstand journalisiert die Fahrtrichtung nur beim ersten
|
|
Segment der Kette; jedes weitere erbt sie vom Vorgaenger."""
|
|
for kopf, eintraege in _ketten(protokoll):
|
|
spec = vse.spec_aus_journal(
|
|
vse.tokens_aus_protokoll(eintraege))
|
|
mit_hz = []
|
|
for i, sek in enumerate(spec["sektionen"]):
|
|
if "hz" in sek:
|
|
mit_hz.append(("sektion", i))
|
|
for j, sub in enumerate((sek.get("vf") or {}).get("subs", [])):
|
|
if "hz" in sub:
|
|
mit_hz.append(("sub", i, j))
|
|
assert mit_hz == [("sektion", 0)], \
|
|
f'{kopf["test_id"]}: hz an {mit_hz}, erwartet nur in der ' \
|
|
f'ersten Sektion'
|
|
|
|
def test_vf_einheiten_terminiert(self, protokoll):
|
|
"""Eine VF-Einheit muss mit motorstation oder kettenende enden -
|
|
sonst laesst sich die Fortsetzungsschleife nicht schliessen."""
|
|
for kopf, eintraege in _ketten(protokoll):
|
|
spec = vse.spec_aus_journal(
|
|
vse.tokens_aus_protokoll(eintraege))
|
|
for sek in spec["sektionen"]:
|
|
vf = sek.get("vf")
|
|
if not vf:
|
|
continue
|
|
subs = vf["subs"]
|
|
assert subs, f'{kopf["test_id"]}: VF-Einheit ohne subs'
|
|
assert subs[-1]["sub"] in ("motorstation", "kettenende"), \
|
|
f'{kopf["test_id"]}: VF-Einheit endet mit ' \
|
|
f'{subs[-1]["sub"]!r}'
|
|
for sub in subs[:-1]:
|
|
assert sub["sub"] not in ("motorstation", "kettenende")
|
|
|
|
|
|
# ============================================================
|
|
# 2. Die Spec-Testdaten
|
|
# ============================================================
|
|
|
|
class TestSpecDaten:
|
|
|
|
def test_fuenf_specs(self, spec_objekte):
|
|
specs = vse.spec_aus_flachen_objekten(spec_objekte)
|
|
assert len(specs) == 5
|
|
|
|
# Ketten, die absichtlich (LZ_01) oder aus laenger bestehenden Gruenden
|
|
# (LZ_02-05) nicht mehr Token-fuer-Token mit hm_recformat.json
|
|
# uebereinstimmen:
|
|
#
|
|
# LZ_01: nach der Ableitung von Hand angepasst, um die NEUE
|
|
# Frage-Freiheit "eingang_hoehe": "auto" zu testen (Gegenstueck zu
|
|
# vsp-eingang-hoehe in Lisp/vf_spec.lsp - NIL-Token statt REAL, der
|
|
# Bauablauf berechnet seinen eigenen Vorschlag) - eine Faehigkeit, die
|
|
# es zur Aufzeichnungszeit noch nicht gab. Als Folge fehlt fuer die
|
|
# betroffenen Horizontal-VF-Einheiten auch "gf_verteilung" (heutiger
|
|
# Ablauf fragt sie dort nicht mehr). Kein Datenverlust der Spec,
|
|
# sondern der Beweis, dass die neue Faehigkeit greift.
|
|
#
|
|
# LZ_02-05: ihr "quelle"-Feld sagt es selbst - sie sind NICHT aus
|
|
# hm_recformat.json abgeleitet, sondern nachtraeglich gegen die
|
|
# echten DXF-Einfuegepunkte der Anlage handkorrigiert ("ersetzt die
|
|
# alte, behelfsmaessig nachgebaute Fassung"; siehe Startpunkte: z.B.
|
|
# LZ_02 hat in hundm05.json [9570.9, 1167.5], im Protokoll aber
|
|
# [8572.181, 1226.825] - keine Rundungsdifferenz, eine andere Kette).
|
|
# Dieser Abgleich gegen das Protokoll ist fuer sie strukturell nicht
|
|
# mehr aussagekraeftig - ein laenger bestehender Zustand, nicht durch
|
|
# die eingang_hoehe-Erweiterung verursacht.
|
|
_ABSICHTLICH_ABWEICHEND = {"VF_hundm05_LZ_01", "VF_hundm05_LZ_02",
|
|
"VF_hundm05_LZ_03", "VF_hundm05_LZ_04",
|
|
"VF_hundm05_LZ_05"}
|
|
|
|
def test_gleiche_journale_wie_protokoll(self, protokoll, spec_objekte):
|
|
"""Der Weg, den LISP nimmt: flaches JSON -> Spec -> Journal. Soll
|
|
dieselben Journale liefern wie das aufgezeichnete Protokoll - das
|
|
gilt fuer KEINE der 5 Ketten in der aktuellen hundm05.json mehr (alle
|
|
in _ABSICHTLICH_ABWEICHEND, siehe dortiger Kommentar), darum hier nur
|
|
noch ein expliziter Skip statt eines leeren, immer-gruenen Loops.
|
|
Sollte hundm05.json wieder eine Kette bekommen, die 1:1 aus
|
|
hm_recformat.json abgeleitet ist, gehoert der Vergleich (Rumpf siehe
|
|
Git-Historie dieser Datei) fuer sie reaktiviert."""
|
|
specs = vse.spec_aus_flachen_objekten(spec_objekte)
|
|
ketten = _ketten(protokoll)
|
|
assert len(specs) == len(ketten)
|
|
pruefbar = [skopf["spec_id"] for _, (skopf, _) in zip(ketten, specs)
|
|
if skopf["spec_id"] not in self._ABSICHTLICH_ABWEICHEND]
|
|
if not pruefbar:
|
|
pytest.skip("alle 5 Ketten in hundm05.json weichen dokumentiert "
|
|
"vom Protokoll ab (siehe _ABSICHTLICH_ABWEICHEND)")
|
|
for (kopf, eintraege), (skopf, spec) in zip(ketten, specs):
|
|
if skopf["spec_id"] in self._ABSICHTLICH_ABWEICHEND:
|
|
continue
|
|
tok = vse.tokens_aus_protokoll(eintraege)
|
|
neu = vse.journal_aus_spec(spec)
|
|
# Toleranz 0.1 statt der sonst ueblichen 1e-6: hundm05.json fuehrt
|
|
# dl/Punkte gerundet auf eine Nachkommastelle (runde_mm_felder in
|
|
# vf_spec_export.py), das Protokoll in voller Praezision.
|
|
assert vse.ist_teilfolge(tok, neu, toleranz=0.1), \
|
|
f'{skopf["spec_id"]} vs {kopf["test_id"]}: die Spec bildet ' \
|
|
f'das Original nicht mehr verlustfrei ab'
|
|
|
|
def test_objekte_zuordenbar(self, spec_objekte):
|
|
"""Jedes Objekt ist Kopf, Sektion, Sub - oder ein von Hand
|
|
ergaenztes Anlagenteil (erkennbar an "function", z.B. ein Kreisel).
|
|
Etwas anderes kennt weder vsp-gruppieren noch der Generator."""
|
|
for i, obj in enumerate(spec_objekte):
|
|
assert ("spec_id" in obj) or ("glied" in obj) or ("sub" in obj) \
|
|
or ("function" in obj), \
|
|
f"Objekt {i} ist weder Kette noch Anlagenteil: {obj}"
|
|
|
|
def test_fremde_objekte_stoeren_die_ketten_nicht(self, spec_objekte):
|
|
"""Die von Hand ergaenzten Anlagenteile duerfen die Ketten-Zerlegung
|
|
nicht beeinflussen - auch nicht, wenn sie mitten zwischen den
|
|
Sektionen stehen."""
|
|
fremde = [o for o in spec_objekte if "function" in o]
|
|
ohne = [o for o in spec_objekte if "function" not in o]
|
|
assert (len(vse.spec_aus_flachen_objekten(spec_objekte))
|
|
== len(vse.spec_aus_flachen_objekten(ohne))), \
|
|
f"{len(fremde)} Zusatzobjekt(e) veraendern die Kettenzahl"
|
|
|
|
def test_kopf_felder(self, spec_objekte):
|
|
for obj in spec_objekte:
|
|
if "spec_id" not in obj:
|
|
continue
|
|
for feld in ("modus", "start_punkt", "start_hoehe", "as"):
|
|
assert feld in obj, f'{obj["spec_id"]}: {feld} fehlt'
|
|
assert len(obj["start_punkt"]) == 3
|
|
assert obj["as"] in (0, 1), "Wahrheitswerte als 1/0 (der " \
|
|
"LISP-Parser kennt kein true/false)"
|
|
if obj["as"]:
|
|
assert obj["as_winkel"] in ("30", "90")
|
|
assert obj["as_seite"] in ("links", "rechts")
|
|
|
|
def test_glied_und_sub_werte_bekannt(self, spec_objekte):
|
|
for obj in spec_objekte:
|
|
if "glied" in obj:
|
|
assert obj["glied"] in vse.GLIEDER, \
|
|
f'unbekanntes Glied {obj["glied"]!r}'
|
|
elif "sub" in obj:
|
|
erlaubt = set(vse.IM_VF.values()) | {"motorstation",
|
|
"kettenende"}
|
|
assert obj["sub"] in erlaubt, \
|
|
f'unbekannter sub {obj["sub"]!r}'
|
|
|
|
def test_domaenenwerte_statt_codes(self, spec_objekte):
|
|
"""Die Spec soll lesbar sein: Seiten/Varianten als Wort, nicht als
|
|
Menue-Index. Rohe Codes wuerde der Emitter zwar durchreichen, in den
|
|
erzeugten Testdaten haben sie aber nichts zu suchen."""
|
|
for obj in spec_objekte:
|
|
for feld, erlaubt in (("seite", ("links", "rechts")),
|
|
("variante", ("aussen", "innen")),
|
|
("gefaelle", ("hoehe", "winkel")),
|
|
("gf_verteilung", ("haelfte", "einlauf"))):
|
|
if feld in obj:
|
|
assert obj[feld] in erlaubt, \
|
|
f'{feld}={obj[feld]!r} ist kein Domaenenwert'
|
|
|
|
def test_arrays_einzeilig(self):
|
|
"""ssg-cfg-parse-array kann keine umgebrochenen Zahlen-Arrays lesen -
|
|
start_punkt MUSS in einer Zeile stehen."""
|
|
if not os.path.isfile(SPEC_JSON):
|
|
pytest.skip("Spec-JSON fehlt")
|
|
with open(SPEC_JSON, encoding="utf-8") as fh:
|
|
for nr, zeile in enumerate(fh, 1):
|
|
offen = zeile.count("[") - zeile.count("]")
|
|
assert offen == 0 or zeile.strip() in ("[", "]"), \
|
|
f"Zeile {nr} hat ein unvollstaendiges Array: " \
|
|
f"{zeile.rstrip()}"
|
|
|
|
def test_flach_fuer_lisp_parser(self, spec_objekte):
|
|
"""ssg-parse-json-array liest zeilenweise: ein "{" beginnt ein
|
|
Objekt, Verschachtelung ist unmoeglich. Gilt auch fuer die von Hand
|
|
ergaenzten Anlagenteile."""
|
|
for obj in spec_objekte:
|
|
for k, v in obj.items():
|
|
assert not isinstance(v, dict), \
|
|
f"{k} ist verschachtelt - der LISP-Parser kann das nicht"
|
|
if isinstance(v, list):
|
|
assert all(isinstance(x, (int, float)) for x in v), \
|
|
f"{k} darf nur Zahlen enthalten"
|
|
|
|
|
|
# ============================================================
|
|
# 3. Validierung: die Spec darf nichts raten
|
|
# ============================================================
|
|
|
|
class TestValidierung:
|
|
|
|
def _spec(self, spec_objekte, nr=0):
|
|
return json.loads(json.dumps(
|
|
vse.spec_aus_flachen_objekten(spec_objekte)[nr][1]))
|
|
|
|
def test_hz_in_spaeterer_sektion_ist_erlaubt(self, spec_objekte):
|
|
"""War frueher ein Fehler (Annahme: hz nur beim allerersten Segment
|
|
der Kette) - zwei echte Aufzeichnungen (linienzug_tests.json UND
|
|
-stufe2.json, beide 2026-09-12) widerlegen das: der heutige Ablauf
|
|
journalisiert die Richtung bei JEDEM Segment neu, auch nach einer
|
|
Vario-Kurve. Ein hz in einer spaeteren Sektion ist darum kein
|
|
Spec-Fehler mehr, sondern ein gueltiges, self-describing Feld -
|
|
SpecSchreiber.linie emittiert es einfach (siehe dortiger
|
|
Kommentar)."""
|
|
spec = self._spec(spec_objekte)
|
|
spec["sektionen"][1]["hz"] = 90.0
|
|
tok = vse.journal_aus_spec(spec)
|
|
assert ("REAL", "90.0") in tok
|
|
|
|
def test_fehlendes_hz_ist_fehler(self, spec_objekte):
|
|
spec = self._spec(spec_objekte)
|
|
del spec["sektionen"][0]["hz"]
|
|
with pytest.raises(ValueError, match="hz"):
|
|
vse.journal_aus_spec(spec)
|
|
|
|
def test_gf_bogen_als_erste_sektion_ist_fehler(self, spec_objekte):
|
|
"""Die erste Sektion laeuft ohne Frame - ein GF-Bogen wird dort nicht
|
|
angeboten (es gibt noch keine Richtung, an die er anschliesst)."""
|
|
spec = self._spec(spec_objekte)
|
|
spec["sektionen"].insert(0, {"glied": "GF-Bogen", "winkel": 90,
|
|
"seite": "links"})
|
|
with pytest.raises(ValueError, match="nicht waehlbar"):
|
|
vse.journal_aus_spec(spec)
|
|
|
|
def test_unbekannter_domaenenwert_ist_fehler(self, spec_objekte):
|
|
"""Ein unbekannter Domaenenwert fuer "seite" muss scheitern - egal ob
|
|
er an einer Top-Level-Sektion (ES-Glied) oder an einem Sub
|
|
(Vario-Kurve) steht. Sucht sich die erste Stelle mit einem
|
|
"seite"-Feld, statt eine feste Position anzunehmen - welche Sektion
|
|
eines liefert, haengt an der Struktur der konkreten Kette."""
|
|
spec = self._spec(spec_objekte)
|
|
ziel = None
|
|
for sek in spec["sektionen"]:
|
|
if "seite" in sek:
|
|
ziel = sek
|
|
break
|
|
for sub in (sek.get("vf") or {}).get("subs", []):
|
|
if "seite" in sub:
|
|
ziel = sub
|
|
break
|
|
if ziel:
|
|
break
|
|
assert ziel is not None, "keine Sektion/Sub mit Feld \"seite\" gefunden"
|
|
ziel["seite"] = "mitte"
|
|
with pytest.raises(ValueError, match="seite"):
|
|
vse.journal_aus_spec(spec)
|
|
|
|
def test_unterminierte_vf_einheit_ist_fehler(self, spec_objekte):
|
|
spec = self._spec(spec_objekte)
|
|
for sek in spec["sektionen"]:
|
|
if sek.get("vf"):
|
|
sek["vf"]["subs"] = sek["vf"]["subs"][:-1]
|
|
break
|
|
with pytest.raises(ValueError, match="motorstation|kettenende"):
|
|
vse.journal_aus_spec(spec)
|
|
|
|
def test_fehlendes_pflichtfeld_ist_fehler(self, spec_objekte):
|
|
spec = self._spec(spec_objekte)
|
|
del spec["start"]["hoehe"]
|
|
with pytest.raises(ValueError, match="hoehe"):
|
|
vse.journal_aus_spec(spec)
|
|
|
|
|
|
# ============================================================
|
|
# 4. Die erwarteten Tokenfolgen in tests/test_vf_spec.lsp
|
|
# ============================================================
|
|
# tests/test_vf_spec.lsp prueft den LISP-Emitter gegen von Hand
|
|
# eingetragene Tokenfolgen. Diese Folgen sind mit dem hier geprueften
|
|
# Python-Emitter erzeugt worden - und genau das haelt dieser Test fest:
|
|
# eine Uebertragungsfehler im LISP-Test wuerde sonst einen falschen
|
|
# Erwartungswert festschreiben, und der CAD-Lauf waere gruen, obwohl der
|
|
# Emitter falsch liegt.
|
|
#
|
|
# Die Specs stehen dafuer zweimal da (hier und im LISP-Test). Das ist
|
|
# gewollt: nur zwei unabhaengige Niederschriften koennen einander pruefen.
|
|
|
|
LISP_TEST = os.path.join(HIER, "test_vf_spec.lsp")
|
|
|
|
_PRE_MIT_AS = [("PT", None), ("REAL", 3000.0), ("STR", "1"), ("STR", "90"),
|
|
("STR", "1")]
|
|
_PRE_OHNE_AS = [("PT", None), ("REAL", 3000.0), ("STR", "2")]
|
|
|
|
|
|
def _start(as_=True):
|
|
s = {"punkt": [1000.0, 2000.0, 3000.0], "hoehe": 3000.0, "as": as_}
|
|
if as_:
|
|
s["as_winkel"] = "90"
|
|
s["as_seite"] = "links"
|
|
return s
|
|
|
|
|
|
def _es(winkel="90", seite="rechts"):
|
|
return {"glied": "ES", "winkel": winkel, "seite": seite}
|
|
|
|
|
|
# Name im LISP-Test (Anfang genuegt) -> Spec
|
|
LISP_FAELLE = {
|
|
"Linie-GF + ES": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Linie-GF", "laenge_modus": "endpunkt",
|
|
"dl": 5500.0, "hz": 0.0, "sep_vor": False, "sep_nach": False,
|
|
"gefaelle": "winkel", "winkel": 3.0, "ende": "ja-mit-es"},
|
|
_es()]},
|
|
"Linie-GF, Zielhoehe zuerst": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Linie-GF", "laenge_modus": "hoehe",
|
|
"hz": 0.0, "sep_vor": True, "sep_nach": False,
|
|
"hoehe": 1800.0, "winkel": 3.0, "ende": "nein"}]},
|
|
"Linie-GF, Steigend + Endpunkt": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Linie-GF", "laenge_modus": "steigend",
|
|
"steigend_modus": "endpunkt",
|
|
"dl": 800.0, "hz": 0.0, "winkel": 2.0, "ende": "nein"}]},
|
|
"Linie-GF, Steigend + Zielhoehe": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Linie-GF", "laenge_modus": "steigend",
|
|
"steigend_modus": "hoehe",
|
|
"hz": 0.0, "hoehe": 1810.0, "winkel": 1.5, "ende": "nein"}]},
|
|
"Horizontal-VF + Motorstation + ES": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Horizontal-VF", "dl": 8000.0, "hz": 90.0,
|
|
"vf": {"gf_verteilung": "einlauf", "sep_vor": False,
|
|
"sep_nach": False, "ende": "ja-mit-es",
|
|
"subs": [{"sub": "motorstation"}]}},
|
|
_es()]},
|
|
"Linie-VF + Vario-Kurve": {
|
|
"start": _start(as_=False),
|
|
"sektionen": [
|
|
{"glied": "Linie-VF", "dl": 12000.0, "hz": 0.0, "hoehe": 2000.0,
|
|
"winkel_idx": 2,
|
|
"vf": {"gf_verteilung": "haelfte", "ende": "nein",
|
|
"separator": True,
|
|
"subs": [{"sub": "vario-kurve", "winkel": 90,
|
|
"seite": "links", "variante": "aussen"},
|
|
{"sub": "motorstation"}]}}]},
|
|
"GF-Bogen als zweite Sektion": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
# laenge_modus/sep_vor/sep_nach fehlen absichtlich (Default
|
|
# "endpunkt"/nein/nein, deckt alte Specs ab).
|
|
{"glied": "Linie-GF", "dl": 3000.0, "hz": 180.0,
|
|
"gefaelle": "hoehe", "hoehe": 2500.0, "ende": "nein"},
|
|
{"glied": "GF-Bogen", "winkel": 60, "seite": "rechts"}]},
|
|
"Linie -> GF + ES": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Linie", "dl": 4000.0, "hz": 270.0, "hoehe": 2000.0,
|
|
"segment_typ": "GF"},
|
|
_es("30", "links")]},
|
|
"Linie -> VF (auto-Ende": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Linie", "dl": 9000.0, "hz": 0.0, "hoehe": 1500.0,
|
|
"segment_typ": "VF",
|
|
"vf": {"gf_verteilung": "einlauf", "ende": "automatisch",
|
|
"subs": [{"sub": "auf-ab", "dl": 3000.0,
|
|
"hoehe": 1200.0},
|
|
{"sub": "motorstation"}]}},
|
|
_es()]},
|
|
"Kettenende am Zielpunkt ohne ES": {
|
|
"start": _start(),
|
|
"sektionen": [
|
|
{"glied": "Horizontal-VF", "dl": 7000.0, "hz": 0.0,
|
|
"vf": {"gf_verteilung": "einlauf", "sep_vor": True,
|
|
"sep_nach": True, "ende": "zielpunkt-ohne-es",
|
|
"subs": [{"sub": "kettenende", "es": False,
|
|
"dl": 2500.0, "hoehe": 1000.0}]}}]},
|
|
}
|
|
|
|
|
|
def _lisp_erwartungen():
|
|
"""Die Erwartungswerte aus den tsp-journal-eq-Aufrufen des LISP-Tests
|
|
lesen: {Testname: [(Art, Wert), ...]}."""
|
|
import re
|
|
if not os.path.isfile(LISP_TEST):
|
|
pytest.skip("tests/test_vf_spec.lsp fehlt")
|
|
with open(LISP_TEST, encoding="cp1252") as fh:
|
|
text = fh.read()
|
|
# LISP-Kommentare weg (die Erwartungslisten sind kommentiert)
|
|
text = re.sub(r";[^\n]*", "", text)
|
|
raus = {}
|
|
for treffer in re.finditer(r'\(tsp-journal-eq\s+"([^"]+)"(.*?)'
|
|
r'\(vsp-journal spec\)\)', text, re.S):
|
|
name, block = treffer.group(1), treffer.group(2)
|
|
tok = []
|
|
if "tsp-pre-mit-as" in block:
|
|
tok.extend(_PRE_MIT_AS)
|
|
elif "tsp-pre-ohne-as" in block:
|
|
tok.extend(_PRE_OHNE_AS)
|
|
for art, wert in re.findall(r'\(cons\s+"([A-Z]+)"\s+'
|
|
r'("[^"]*"|-?[\d.]+)\)', block):
|
|
if wert.startswith('"'):
|
|
tok.append((art, wert.strip('"')))
|
|
else:
|
|
tok.append((art, float(wert) if "." in wert else int(wert)))
|
|
raus[name] = tok
|
|
return raus
|
|
|
|
|
|
def _norm(tok):
|
|
"""Python-Tokens (Art, Text) in die Vergleichsform bringen: PT-Werte
|
|
interessieren hier nicht (sie stehen im LISP-Test in der Praeambel-
|
|
Hilfsfunktion), Zahlen numerisch."""
|
|
raus = []
|
|
for art, wert in tok:
|
|
if art == "PT":
|
|
raus.append(("PT", None))
|
|
elif art in ("REAL", "DL"):
|
|
raus.append((art, float(wert)))
|
|
elif art == "INT":
|
|
raus.append((art, int(wert)))
|
|
else:
|
|
raus.append((art, wert))
|
|
return raus
|
|
|
|
|
|
class TestLispErwartungen:
|
|
|
|
def test_alle_faelle_gefunden(self):
|
|
erwartungen = _lisp_erwartungen()
|
|
assert len(erwartungen) == 10, \
|
|
f"10 tsp-journal-eq-Faelle erwartet, {len(erwartungen)} gefunden: " \
|
|
f"{sorted(erwartungen)}"
|
|
|
|
@pytest.mark.parametrize("praefix", sorted(LISP_FAELLE))
|
|
def test_lisp_erwartung_stimmt(self, praefix):
|
|
erwartungen = _lisp_erwartungen()
|
|
passend = [n for n in erwartungen if n.startswith(praefix)]
|
|
assert len(passend) == 1, \
|
|
f"{praefix!r} passt auf {passend} - Testname im LISP-Test " \
|
|
f"geaendert?"
|
|
soll = _norm(vse.journal_aus_spec(LISP_FAELLE[praefix]))
|
|
ist = erwartungen[passend[0]]
|
|
assert ist == soll, (
|
|
f"Die Erwartung im LISP-Test weicht vom Python-Emitter ab.\n"
|
|
f" LISP: {ist}\n Python: {soll}\n"
|
|
f"Entweder ist die Tokenfolge im LISP-Test falsch uebertragen, "
|
|
f"oder die Spec hier und dort beschreibt nicht dasselbe.")
|