Files
dxfmakros/tests/test_vf_spec.py
T
s.ayadi 428f1ced70 [FEAT] VF-Linienzug aus Eingabedaten bauen (Stufe 1: Spec -> Journal -> Replay)
Stufe 1 des Fahrplans in doc/TODO-plan-vf-interactive.md, fertig fuer
Modus 1. Eine SPEC beschreibt eine Kette in Domaenenwerten ("links",
"winkel", "aussen", ja/nein) statt in Menue-Codes; vsp-journal uebersetzt sie
in ein Eingabe-Journal, vsp-bau-aus-spec spielt es per
vfl-journal-replay-start durch den UNVERAENDERTEN vf-linienzug-modus. Der
fertige Block traegt damit dasselbe SSG_VF_EDIT-Journal wie eine handgebaute
Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein
Journal-Konsument muss etwas von Specs wissen.

CAD-Lauf VF_SPEC_BAU: 5 OK, 0 Fehler. Je Kette status "executed",
prompts 0 (keine Live-Eingabe erreicht), eingaben_offen 0, keine
Spec-Fehler, keine Bau-Meldungen, glieder_ist == glieder_soll,
Einfuegepunkt gleich dem Kettenstart der Spec.

Neu:
- Lisp/vf_spec.lsp: Code-Tabellen, Emitter vsp-journal, Validierung
  vsp-pruefen, flacher JSON-Loader, Headless-Rahmen vsp-headless-an/-aus,
  Ergebnis-Record, vsp-bau-aus-spec/-liste, c:VF_SPEC_BAU. Von vf_core.lsp
  NACH vf_linienzug.lsp geladen - damit sind beide Ladewege abgedeckt (Menue
  und der .scr-Loader), das MNL bleibt unveraendert.
- lib/vf_spec_export.py: derselbe Emitter in Python PLUS die Umkehrung
  (spec_aus_journal), erzeugt die Spec-Testdaten.
- tests/testdata/vf_spec_hundm05.json: die 5 echten Ketten als Spec,
  erzeugt statt handgeschrieben.
- tests/test_vf_spec.lsp (C:TEST_VF_SPEC, in alltests.json): 7 Faelle je
  Glied-Typ, 7 Validierungsfaelle, Gruppieren, und die 5 echten Ketten gegen
  ihr aufgezeichnetes Journal.
- tests/test_vf_spec.py: 36 Tests (Rundlauf, Spec-Daten, Validierung,
  Erwartungswert-Abgleich, Ergebnisse von VF_SPEC_BAU).

Drei Regeln, die der Uebersetzer besitzen muss - jede wuerde sonst die ganze
Replay-Queue verschieben:
- Der Menue-Code haengt am Frame: erste Sektion 4 Optionen (kein GF-Bogen,
  es gibt noch keine Richtung), jede spaetere 5.
- hz steht genau EINMAL im Journal, beim ersten Segment (vfl-in-abstand
  journalisiert die Richtung nur bei freier Richtungswahl).
- Nur ein HORIZONTALER Erstkoerper einer VF-Einheit stellt die Separator-/
  Endpunktfragen vorab (winkel1=0 laeuft durch vfl-baue-horizontal-koerper);
  ein gewinkelter wird ohne jede Frage gebaut.

Wie die Richtigkeit belegt ist - drei Implementierungen derselben Grammatik,
die einander pruefen:
- Rundlauf gegen echte Daten: journal_aus_spec(spec_aus_journal(tok)) == tok
  fuer alle 5 HundM-Ketten, tokenweise numerisch verglichen. Damit ist
  bewiesen, dass die Spec das Journal verlustfrei abbildet.
- Der Weg, den LISP nimmt (flaches JSON -> Spec -> Journal), ergibt separat
  geprueft dieselben Journale wie das aufgezeichnete Protokoll.
- Der unabhaengige Dekoder aus lib/vf_journal_export.py verdaut alle 7
  synthetischen Emitter-Ausgaben restlos, auch die zwei Zweige, die in den
  echten Daten fehlen (Linie-VF, gewinkelter Erstkoerper).
- Die Erwartungswerte im LISP-Test kommen aus dem geprueften Python-Emitter;
  TestLispErwartungen liest sie zurueck und vergleicht erneut. Ohne das
  koennte ein Uebertragungsfehler einen falschen Erwartungswert
  festschreiben und der CAD-Lauf waere gruen, obwohl der Emitter falsch
  liegt.

Abweichungen von der Planung (Begruendungen in doc/TODO-plan-vf-interactive.md
Abschnitt 4.6): vsp-pruefen ruft den Emitter statt eine zweite Regelmenge zu
pflegen; das vollstaendige Journal steht nicht im Record (statt dessen
glieder_soll/glieder_ist als Desync-Detektor); Zusatzfeld segment_typ am
Glied "Linie", weil dort vfl-segment-entscheidung selbst zwischen GF und VF
waehlt; vf_ende-Werte "automatisch"/"zielpunkt-ohne-es" fuer die Faelle ohne
Frage; Praefix vsp- statt vfs- (das gehoert vf_standard.lsp).

Zusaetzlich:
- tests/test_vf_headless_statisch.py prueft jetzt auch, dass
  Lisp/vf_spec.lsp UEBERHAUPT keine Eingabe-/Dialogfunktion aufruft - beim
  Daten-Pfad ist die Null die Vorgabe.
- tests/testdata/mubea.json: die drei einzeln eingefuegten S-LP-Separatoren
  entfernt (Fortsetzung von 699744e - die Separatoren stecken jetzt in den
  Staustreckenbloecken).

Noch offen: die Uebersetzer fuer Modus 2 und 3. Modus 2 braucht laut
Fahrplan erst 2-3 echte "linienzug2"-Journale als Referenz -
tests/testdata/hundm05.json enthaelt ausschliesslich Modus-1-Ketten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:44:38 +02:00

550 lines
23 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_vf_spec.py - Validiert die Spec-Ebene des VF-Linienzugs.
Drei Ebenen, die ersten zwei laufen 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/vf_spec_hundm05.json): Struktur,
Lesbarkeit durch den LISP-Parser, und dass sie dieselben Journale
ergeben wie tests/testdata/hundm05.json.
3. Die Ergebnisse von VF_SPEC_BAU (tests/output/vf_spec_results.json):
jede Kette gebaut, kein Prompt, kein offener Journal-Wert, Glied-Folge
wie geplant.
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__)
PROTOKOLL = os.path.join(HIER, "testdata", "hundm05.json")
SPEC_JSON = os.path.join(HIER, "testdata", "vf_spec_hundm05.json")
# VF_SPEC_BAU schreibt nach tests/output. Aeltere Laeufe (und ein Lauf mit
# gesetztem DXFM_VF_SPEC_OUT) koennen die Datei woanders haben - darum beide
# ueblichen Orte pruefen, statt den Test stumm zu ueberspringen.
ERGEBNIS_ORTE = [os.path.join(HIER, "output", "vf_spec_results.json"),
os.path.join(HIER, "..", "results", "vf_spec_results.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)
def _lade_erstes(orte, was):
for pfad in orte:
if os.path.isfile(pfad):
with open(pfad, encoding="utf-8") as fh:
return json.load(fh)
pytest.skip("%s fehlt (%s)" % (" bzw. ".join(orte), was))
@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")
@pytest.fixture(scope="module")
def spec_ergebnisse():
return _lade_erstes(ERGEBNIS_ORTE, "VF_SPEC_BAU in BricsCAD erzeugt es")
def _ketten(protokoll):
"""Gleiche Gruppierregel wie hundm05: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
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."""
for kopf, eintraege in _ketten(protokoll):
tok = vse.tokens_aus_protokoll(eintraege)
spec = vse.spec_aus_journal(tok)
zurueck = vse.journal_aus_spec(spec)
idx, was = vse.erste_abweichung(tok, zurueck)
assert vse.gleich(tok, zurueck), \
f'{kopf["test_id"]}: Abweichung bei Token {idx} - {was}'
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
def test_gleiche_journale_wie_protokoll(self, protokoll, spec_objekte):
"""Der Weg, den LISP nimmt: flaches JSON -> Spec -> Journal. Er muss
dieselben Journale liefern wie das aufgezeichnete Protokoll."""
specs = vse.spec_aus_flachen_objekten(spec_objekte)
ketten = _ketten(protokoll)
assert len(specs) == len(ketten)
for (kopf, eintraege), (skopf, spec) in zip(ketten, specs):
tok = vse.tokens_aus_protokoll(eintraege)
neu = vse.journal_aus_spec(spec)
idx, was = vse.erste_abweichung(tok, neu)
assert vse.gleich(tok, neu), \
f'{skopf["spec_id"]} vs {kopf["test_id"]}: Abweichung bei ' \
f'Token {idx} - {was}'
def test_objekte_zuordenbar(self, spec_objekte):
"""Jedes Objekt ist Kopf, Sektion oder Sub - die Gruppierregel von
vsp-gruppieren kennt nichts anderes."""
for i, obj in enumerate(spec_objekte):
assert ("spec_id" in obj) or ("glied" in obj) or ("sub" in obj), \
f"Objekt {i} ist weder Kopf, Sektion noch Sub: {obj}"
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."""
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_fehler(self, spec_objekte):
spec = self._spec(spec_objekte)
spec["sektionen"][1]["hz"] = 90.0
with pytest.raises(ValueError, match="hz"):
vse.journal_aus_spec(spec)
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):
spec = self._spec(spec_objekte)
spec["sektionen"][-1]["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", "dl": 5500.0, "hz": 0.0,
"gefaelle": "winkel", "winkel": 3.0, "ende": "ja-mit-es"},
_es()]},
"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": [
{"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) == 7, \
f"7 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.")
# ============================================================
# 5. Ergebnisse von VF_SPEC_BAU (braucht BricsCAD)
# ============================================================
class TestSpecBau:
def test_je_spec_ein_ergebnis(self, spec_objekte, spec_ergebnisse):
soll = [o["spec_id"] for o in spec_objekte if "spec_id" in o]
ist = [r["spec_id"] for r in spec_ergebnisse]
assert ist == soll
def test_status_executed(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["status"] == "executed", \
f'{r["spec_id"]}: Status={r["status"]}, {r.get("fehler_text")}'
def test_keine_spec_fehler(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert not r["spec_fehler"], \
f'{r["spec_id"]}: {r["spec_fehler"]}'
def test_kein_prompt(self, spec_ergebnisse):
"""Der positive Beweis fuer "kein Dialog, keine Konsolenfrage": die
zaehlenden Stubs wurden nie erreicht."""
for r in spec_ergebnisse:
assert r["prompts"] == 0, \
f'{r["spec_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
def test_journal_vollstaendig_verbraucht(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["eingaben_offen"] == 0, \
f'{r["spec_id"]}: {r["eingaben_offen"]} Werte offen'
assert r["eingaben_gesamt"] > 0
def test_glieder_wie_geplant(self, spec_ergebnisse):
"""Der Desync-Detektor, der auch dann greift, wenn die Queue aufgeht:
eine geometrisch abgewiesene Sektion verbraucht nur weniger
Eintraege, die Glied-Folge weicht dann aber ab."""
for r in spec_ergebnisse:
assert r["glieder_ist"] == r["glieder_soll"], \
f'{r["spec_id"]}: gebaut {r["glieder_ist"]}, ' \
f'geplant {r["glieder_soll"]}'
def test_keine_meldungen(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert not r["meldungen"], \
f'{r["spec_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
def test_vf_block_erzeugt(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["block_name"].startswith("VF_"), \
f'{r["spec_id"]}: Block {r["block_name"]!r}'
assert r["block_handle"]
def test_startpunkt_getroffen(self, spec_objekte, spec_ergebnisse):
"""Der Einfuegepunkt des VF_-Blocks ist der Kettenstart aus der
Spec - er ist der einzige absolute Anker der ganzen Kette."""
soll = dict((o["spec_id"], o["start_punkt"]) for o in spec_objekte
if "spec_id" in o)
for r in spec_ergebnisse:
erwartet = soll[r["spec_id"]]
ist = r["insert_point"]
for achse, (e, i) in enumerate(zip(erwartet, ist)):
assert abs(e - i) < 1.0, \
f'{r["spec_id"]}: Achse {achse} ist {i}, erwartet {e}'
def test_gleiche_ergebnisse_wie_journal_pfad(self, spec_ergebnisse):
"""Spec-Pfad == Journal-Pfad: dieselben Ketten, gebaut aus der Spec
statt aus dem aufgezeichneten Journal, muessen dieselben Bloecke mit
denselben Attributen ergeben. Vergleich gegen die Ergebnisse von
TEST_HUNDM05, falls beide Laeufe vorliegen."""
pfad = os.path.join(HIER, "output", "hundm05_results.json")
if not os.path.isfile(pfad):
pytest.skip("tests/output/hundm05_results.json fehlt "
"(TEST_HUNDM05 nicht gelaufen)")
with open(pfad, encoding="utf-8") as fh:
journal_ergebnisse = json.load(fh)
if len(journal_ergebnisse) != len(spec_ergebnisse):
pytest.skip("unterschiedlich viele Ketten in den beiden Laeufen")
for spec_r, journal_r in zip(spec_ergebnisse, journal_ergebnisse):
if journal_r["status"] != "executed":
continue
for tag, wert in journal_r["actual_attributes"].items():
assert spec_r["actual_attributes"].get(tag) == wert, \
f'{spec_r["spec_id"]}: Attribut {tag} ist ' \
f'{spec_r["actual_attributes"].get(tag)!r}, ' \
f'Journal-Pfad hatte {wert!r}'