[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>
This commit is contained in:
2026-09-03 10:44:38 +02:00
parent ba01455d52
commit 428f1ced70
13 changed files with 3268 additions and 31 deletions
+6
View File
@@ -161,6 +161,12 @@ tests/
test_gefaellestrecke.py # pytest-Validierung Gefaellestrecke
test_mubea.py # pytest-Validierung Mubea-Gesamtmodell
test_hundm05.py # pytest-Validierung HundM05 (5 Linienzuege)
test_vf_spec.lsp # LISP-Unittest: Spec-Uebersetzer (vsp-journal),
# je Glied-Typ + die 5 echten HundM-Ketten
# gegen ihr aufgezeichnetes Journal
test_vf_spec.py # pytest: Rundlauf Journal->Spec->Journal, die
# Spec-Testdaten, und die Ergebnisse von
# VF_SPEC_BAU (letztere brauchen BricsCAD)
test_vf_journal_grammatik.py # pytest ohne CAD: Journal-Dekoder aus
# lib/vf_journal_export.py gegen handgebaute
# Journale (Linie-VF, gewinkelter Erstkoerper)
+1
View File
@@ -3,6 +3,7 @@
{ "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" },
{ "name": "linienzug", "save": "dwg", "module": "VarioFoerderer" },
{ "name": "vfl_schema", "module": "VarioFoerderer" },
{ "name": "vf_spec", "module": "VarioFoerderer" },
{ "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
+22
View File
@@ -268,6 +268,28 @@ def test_journal_reset_loescht_diagnose(quelltext):
assert "*vfl-headless-fehler*" in koerper
# ============================================================
# 3b. vf_spec.lsp fragt ueberhaupt nichts
# ============================================================
# Das Spec-Modul ist der Daten-Pfad: es baut aus einer Datei und darf nie
# einen Prompt oder Dialog oeffnen. Anders als bei vf_linienzug.lsp (das den
# interaktiven Pfad ja weiter braucht) ist hier die Null die Vorgabe. Seine
# Stubs SETZEN getstring & Co. nur - sie rufen sie nicht auf, der Scanner
# zaehlt also korrekt null Treffer.
SPEC = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_spec.lsp")
def test_vf_spec_ohne_eingaben():
if not os.path.isfile(SPEC):
pytest.skip("Lisp/vf_spec.lsp nicht gefunden")
inv = funktions_inventar(io.open(SPEC, encoding="cp1252").read())
treffer = dict((name, dict(z)) for name, z in inv.items() if z)
assert not treffer, (
"Lisp/vf_spec.lsp ruft Eingabe-/Dialogfunktionen auf: %s. Der "
"Daten-Pfad baut aus einer Datei und darf nichts fragen." % treffer)
# ============================================================
# 4. Funktionen, die es in AutoLISP nicht gibt
# ============================================================
+491
View File
@@ -0,0 +1,491 @@
;; ============================================================
;; test_vf_spec.lsp - Unittest fuer den Spec-Uebersetzer (vf_spec.lsp)
;;
;; Prueft vsp-journal OHNE Geometrie und ohne Zeichnung: aus einer Spec muss
;; genau die Tokenfolge entstehen, die der interaktive Ablauf aufzeichnen
;; wuerde. Weicht sie ab, baut der Spec-Pfad eine ANDERE Kette als die
;; Vorlage - der Fehler waere in der Zeichnung schwer zu sehen und im
;; Sivas-Export falsch.
;;
;; Zwei Arten von Testfaellen:
;; 1. Sieben handgebaute Specs, je ein Glied-Typ bzw. Sonderfall. Die
;; erwarteten Tokenfolgen sind NICHT von Hand abgeleitet, sondern mit
;; dem geprueften Python-Emitter erzeugt (lib/vf_spec_export.py) und
;; zusaetzlich durch den unabhaengigen Python-DEKODER gelaufen
;; (lib/vf_journal_export.py) - drei Implementierungen derselben
;; Grammatik, die einander pruefen.
;; 2. Die 5 echten HundM-Ketten: die Spec-Datei
;; tests/testdata/vf_spec_hundm05.json muss dieselben Journale ergeben
;; wie das aufgezeichnete Protokoll tests/testdata/hundm05.json.
;; Das ist der Beweis "Spec-Pfad == Journal-Pfad" auf Datenebene; der
;; Beweis auf Geometrie-Ebene ist VF_SPEC_BAU (siehe tests/test_vf_spec.py).
;;
;; Voraussetzung: VarioFoerderer.lsp geladen (vf_core laedt vf_spec mit).
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_vf_spec.lsp"))
;; TEST_VF_SPEC
;;
;; Speichert tests/output/vf_spec_results.json ist NICHT dieser Test -
;; das schreibt VF_SPEC_BAU. Hier: tests/output/vf_spec_unit_results.json.
;; ============================================================
(setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '())
(defun tsp-eq (name erwartet ist)
(if (equal erwartet ist)
(progn (setq *tsp-pass* (1+ *tsp-pass*))
(princ (strcat "\n [PASS] " name)))
(progn (setq *tsp-fail* (1+ *tsp-fail*))
(setq *tsp-fails* (cons name *tsp-fails*))
(princ (strcat "\n [FAIL] " name
"\n erwartet: " (vl-princ-to-string erwartet)
"\n ist: " (vl-princ-to-string ist))))))
(defun tsp-truthy (name ist)
(if ist
(progn (setq *tsp-pass* (1+ *tsp-pass*))
(princ (strcat "\n [PASS] " name)))
(progn (setq *tsp-fail* (1+ *tsp-fail*))
(setq *tsp-fails* (cons name *tsp-fails*))
(princ (strcat "\n [FAIL] " name " (erwartet nicht-nil, war nil)")))))
(defun tsp-nil (name ist)
(if (null ist)
(progn (setq *tsp-pass* (1+ *tsp-pass*))
(princ (strcat "\n [PASS] " name)))
(progn (setq *tsp-fail* (1+ *tsp-fail*))
(setq *tsp-fails* (cons name *tsp-fails*))
(princ (strcat "\n [FAIL] " name
" (erwartet nil, war " (vl-princ-to-string ist) ")")))))
;; Journal-Vergleich mit Fundstelle: bei 15-70 Tokens ist "erwartet (...)
;; ist (...)" unlesbar, der Index der ersten Abweichung dagegen sofort
;; brauchbar.
(defun tsp-journal-eq (name erwartet ist / i a b diff)
(setq i 0 diff nil)
(while (and (null diff) (or erwartet ist))
(setq i (1+ i) a (car erwartet) b (car ist))
(if (not (equal a b))
(setq diff (strcat "Token " (itoa i) ": erwartet "
(vl-princ-to-string a) ", ist "
(vl-princ-to-string b))))
(setq erwartet (cdr erwartet) ist (cdr ist)))
(if diff
(progn (setq *tsp-fail* (1+ *tsp-fail*))
(setq *tsp-fails* (cons name *tsp-fails*))
(princ (strcat "\n [FAIL] " name "\n " diff)))
(progn (setq *tsp-pass* (1+ *tsp-pass*))
(princ (strcat "\n [PASS] " name)))))
;; ============================================================
;; Spec-Bausteine
;; ============================================================
;; Eine Spec ist (kopf sektionen), eine Sektion (felder subs) - genau die
;; Struktur, die vsp-gruppieren aus dem flachen JSON baut.
(defun tsp-kopf (as-p)
(if as-p
(list (cons "spec_id" "TEST") (cons "modus" 1)
(cons "start_punkt" (list 1000.0 2000.0 3000.0))
(cons "start_hoehe" 3000.0) (cons "as" 1)
(cons "as_winkel" "90") (cons "as_seite" "links"))
(list (cons "spec_id" "TEST") (cons "modus" 1)
(cons "start_punkt" (list 1000.0 2000.0 3000.0))
(cons "start_hoehe" 3000.0) (cons "as" 0))))
(defun tsp-sek (felder subs) (list felder subs))
(defun tsp-es (winkel seite)
(tsp-sek (list (cons "glied" "ES") (cons "winkel" winkel)
(cons "seite" seite)) nil))
(defun tsp-spec (as-p sektionen) (list (tsp-kopf as-p) sektionen))
;; Praeambel-Tokens (kommen in fast jedem Fall vor)
(defun tsp-pre-mit-as ()
(list (cons "PT" (list 1000.0 2000.0 3000.0))
(cons "REAL" 3000.0)
(cons "STR" "1")
(cons "STR" "90")
(cons "STR" "1")))
(defun tsp-pre-ohne-as ()
(list (cons "PT" (list 1000.0 2000.0 3000.0))
(cons "REAL" 3000.0)
(cons "STR" "2")))
;; ============================================================
;; 1. Handgebaute Faelle je Glied-Typ
;; ============================================================
(defun tsp-tests-glieder ( / spec)
;; --- Linie-GF, Kettenende mit ES ---
;; Erste Sektion: Menue-Code "1" (4 Optionen, ohne Frame).
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 5500.0)
(cons "hz" 0.0) (cons "gefaelle" "winkel")
(cons "winkel" 3.0) (cons "ende" "ja-mit-es")) nil)
(tsp-es "90" "rechts"))))
(tsp-journal-eq "Linie-GF + ES"
(append (tsp-pre-mit-as)
(list (cons "STEP" "Linie-GF") (cons "STR" "1")
(cons "DL" 5500.0) (cons "REAL" 0.0)
(cons "STR" "2") (cons "REAL" 3.0) (cons "STR" "1")
(cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2")))
(vsp-journal spec))
;; --- Horizontal-VF: horizontaler Erstkoerper ---
;; Nur hier stehen Separator vor/nach VOR der Endpunkt-Antwort
;; (vfl-baue-horizontal-koerper im Ziel-Modus).
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 8000.0)
(cons "hz" 90.0) (cons "gf_verteilung" "einlauf")
(cons "vf_sep_vor" 0) (cons "vf_sep_nach" 0)
(cons "vf_ende" "ja-mit-es"))
(list (list (cons "sub" "motorstation"))))
(tsp-es "90" "rechts"))))
(tsp-journal-eq "Horizontal-VF + Motorstation + ES"
(append (tsp-pre-mit-as)
(list (cons "STEP" "Horizontal-VF") (cons "STR" "3")
(cons "DL" 8000.0) (cons "REAL" 90.0)
(cons "STR" "2") ; GF-Verteilung: Einlauf
(cons "STR" "2") (cons "STR" "2") ; Separator vor / nach
(cons "STR" "1") ; Endpunkt: Motorstation
(cons "STR" "1") ; Kettenende: ja mit ES
(cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2")))
(vsp-journal spec))
;; --- Linie-VF: GEWINKELTER Erstkoerper ---
;; Kein Separator-Fragenpaar (der Koerper wird ohne Frage gebaut), dafuer
;; Zielhoehe + optionale Winkelwahl. Danach eine Vario-Kurve und ein
;; Abschluss "nein" mit Separator.
(setq spec (tsp-spec nil (list
(tsp-sek (list (cons "glied" "Linie-VF") (cons "dl" 12000.0)
(cons "hz" 0.0) (cons "hoehe" 2000.0)
(cons "winkel_idx" 2) (cons "gf_verteilung" "haelfte")
(cons "vf_ende" "nein") (cons "vf_separator" 1))
(list (list (cons "sub" "vario-kurve") (cons "winkel" 90)
(cons "seite" "links") (cons "variante" "aussen"))
(list (cons "sub" "motorstation")))))))
(tsp-journal-eq "Linie-VF + Vario-Kurve (kein Separator-Paar)"
(append (tsp-pre-ohne-as)
(list (cons "STEP" "Linie-VF") (cons "STR" "2")
(cons "DL" 12000.0) (cons "REAL" 0.0)
(cons "REAL" 2000.0) (cons "INT" 2)
(cons "STR" "1") ; GF-Verteilung: Haelfte
(cons "STR" "2") (cons "STR" "2") ; weiter -> Vario-Kurve
(cons "STEP" "Vario-Kurve") (cons "INT" 90)
(cons "STR" "1") (cons "STR" "1") ; links, aussen
(cons "STR" "1") ; Endpunkt: Motorstation
(cons "STR" "2") (cons "STR" "1"))) ; kein Kettenende, Separator
(vsp-journal spec))
;; --- GF-Bogen als ZWEITE Sektion ---
;; Ab der zweiten Sektion gilt die 5-Optionen-Tabelle: Linie-GF ist dort
;; "2" (nicht mehr "1"), GF-Bogen ist "1".
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0)
(cons "hz" 180.0) (cons "gefaelle" "hoehe")
(cons "hoehe" 2500.0) (cons "ende" "nein")) nil)
(tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 60)
(cons "seite" "rechts")) nil))))
(tsp-journal-eq "GF-Bogen als zweite Sektion (Menue-Codes am Frame)"
(append (tsp-pre-mit-as)
(list (cons "STEP" "Linie-GF") (cons "STR" "1")
(cons "DL" 3000.0) (cons "REAL" 180.0)
(cons "STR" "1") (cons "REAL" 2500.0) (cons "STR" "3")
(cons "STEP" "GF-Bogen") (cons "STR" "1")
(cons "INT" 60) (cons "STR" "2")))
(vsp-journal spec))
;; --- Linie bis Kettenende, als reine Gefaellestrecke aufgeloest ---
;; Kein VF-Block: nach Laenge und Zielhoehe folgt direkt das ES-Glied.
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie") (cons "dl" 4000.0)
(cons "hz" 270.0) (cons "hoehe" 2000.0)
(cons "segment_typ" "GF")) nil)
(tsp-es "30" "links"))))
(tsp-journal-eq "Linie -> GF + ES"
(append (tsp-pre-mit-as)
(list (cons "STEP" "Linie") (cons "STR" "4")
(cons "DL" 4000.0) (cons "REAL" 270.0) (cons "REAL" 2000.0)
(cons "STEP" "ES") (cons "STR" "30") (cons "STR" "1")))
(vsp-journal spec))
;; --- Linie bis Kettenende, als VF-Einheit aufgeloest ---
;; auto_ende: das ES-Glied folgt OHNE Kettenende-Frage.
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie") (cons "dl" 9000.0)
(cons "hz" 0.0) (cons "hoehe" 1500.0)
(cons "segment_typ" "VF")
(cons "gf_verteilung" "einlauf")
(cons "vf_ende" "automatisch"))
(list (list (cons "sub" "auf-ab") (cons "dl" 3000.0)
(cons "hoehe" 1200.0))
(list (cons "sub" "motorstation"))))
(tsp-es "90" "rechts"))))
(tsp-journal-eq "Linie -> VF (auto-Ende, ES ohne Frage)"
(append (tsp-pre-mit-as)
(list (cons "STEP" "Linie") (cons "STR" "4")
(cons "DL" 9000.0) (cons "REAL" 0.0) (cons "REAL" 1500.0)
(cons "STR" "2") ; GF-Verteilung: Einlauf
(cons "STR" "2") (cons "STR" "3") ; weiter -> Auf/Ab
(cons "DL" 3000.0) (cons "REAL" 1200.0)
(cons "STR" "1") ; Endpunkt: Motorstation
(cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2")))
(vsp-journal spec))
;; --- Kettenende am Zielpunkt OHNE ES ---
;; Die Kette endet direkt am Zielpunkt: kein Separator, kein ES, keine
;; weitere Frage.
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 7000.0)
(cons "hz" 0.0) (cons "gf_verteilung" "einlauf")
(cons "vf_sep_vor" 1) (cons "vf_sep_nach" 1)
(cons "vf_ende" "zielpunkt-ohne-es"))
(list (list (cons "sub" "kettenende") (cons "es" 0)
(cons "dl" 2500.0) (cons "hoehe" 1000.0)))))))
(tsp-journal-eq "Kettenende am Zielpunkt ohne ES"
(append (tsp-pre-mit-as)
(list (cons "STEP" "Horizontal-VF") (cons "STR" "3")
(cons "DL" 7000.0) (cons "REAL" 0.0)
(cons "STR" "2") ; GF-Verteilung: Einlauf
(cons "STR" "1") (cons "STR" "1") ; Separator vor / nach: ja
(cons "STR" "3") ; Endpunkt: Kettenende
(cons "STR" "2") ; ES setzen? nein
(cons "DL" 2500.0) (cons "REAL" 1000.0)))
(vsp-journal spec))
(princ))
;; ============================================================
;; 2. Validierung: die Spec darf nichts raten
;; ============================================================
(defun tsp-tests-validierung ( / spec)
;; --- hz nur beim ERSTEN Segment ---
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0)
(cons "hz" 0.0) (cons "gefaelle" "winkel")
(cons "winkel" 3.0) (cons "ende" "nein")) nil)
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 2000.0)
(cons "hz" 90.0) ; <- verboten
(cons "gefaelle" "winkel") (cons "winkel" 3.0)
(cons "ende" "nein")) nil))))
(tsp-nil "hz in spaeterer Sektion: kein Journal" (vsp-journal spec))
(tsp-truthy "hz in spaeterer Sektion: Fehler gemeldet" *vsp-fehler*)
;; --- hz fehlt beim ersten Segment ---
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0)
(cons "gefaelle" "winkel") (cons "winkel" 3.0)
(cons "ende" "nein")) nil))))
(tsp-nil "hz fehlt: kein Journal" (vsp-journal spec))
;; --- GF-Bogen in der ERSTEN Sektion (ohne Frame nicht waehlbar) ---
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 90)
(cons "seite" "links")) nil))))
(tsp-nil "GF-Bogen als erste Sektion: kein Journal" (vsp-journal spec))
;; --- unbekannter Domaenenwert ---
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 90)
(cons "seite" "mitte")) nil))))
(tsp-nil "Seite \"mitte\": kein Journal" (vsp-journal spec))
;; --- VF-Einheit ohne Abschluss-Knoten ---
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 8000.0)
(cons "hz" 0.0) (cons "gf_verteilung" "einlauf")
(cons "vf_sep_vor" 0) (cons "vf_sep_nach" 0)
(cons "vf_ende" "ja-mit-es"))
(list (list (cons "sub" "vario-kurve") (cons "winkel" 90)
(cons "seite" "links") (cons "variante" "innen")))))))
(tsp-nil "VF-Einheit endet mit Vario-Kurve: kein Journal" (vsp-journal spec))
;; --- Pflichtfeld fehlt ---
(setq spec (list (list (cons "spec_id" "X")
(cons "start_punkt" (list 0.0 0.0 0.0)))
(list (tsp-sek (list (cons "glied" "Linie-GF")
(cons "dl" 1000.0) (cons "hz" 0.0)
(cons "gefaelle" "winkel")
(cons "winkel" 3.0)
(cons "ende" "nein")) nil))))
(tsp-nil "start_hoehe fehlt: kein Journal" (vsp-journal spec))
;; --- gueltige Spec: vsp-pruefen meldet nichts ---
(setq spec (tsp-spec T (list
(tsp-sek (list (cons "glied" "Linie") (cons "dl" 4000.0)
(cons "hz" 0.0) (cons "hoehe" 2000.0)
(cons "segment_typ" "GF")) nil)
(tsp-es "90" "links"))))
(tsp-nil "gueltige Spec: keine Fehler" (vsp-pruefen spec))
(princ))
;; ============================================================
;; 3. Gruppieren des flachen JSON
;; ============================================================
(defun tsp-tests-gruppieren ( / objekte specs sek)
(setq objekte (list
(list (cons "spec_id" "A") (cons "start_punkt" (list 0.0 0.0 0.0)))
(list (cons "glied" "Horizontal-VF") (cons "nr" 1))
(list (cons "sub" "vario-kurve"))
(list (cons "sub" "motorstation"))
(list (cons "glied" "ES") (cons "nr" 2))
(list (cons "spec_id" "B") (cons "start_punkt" (list 1.0 1.0 1.0)))
(list (cons "glied" "Linie") (cons "nr" 1))))
(setq specs (vsp-gruppieren objekte))
(tsp-eq "gruppieren: 2 Specs" 2 (length specs))
(tsp-eq "gruppieren: Spec A hat 2 Sektionen" 2
(length (vsp-sektionen (car specs))))
(setq sek (car (vsp-sektionen (car specs))))
(tsp-eq "gruppieren: erste Sektion ist Horizontal-VF" "Horizontal-VF"
(ssg-val (vsp-sek-felder sek) "glied"))
(tsp-eq "gruppieren: sie hat 2 subs in Reihenfolge"
(list "vario-kurve" "motorstation")
(mapcar '(lambda (s) (ssg-val s "sub")) (vsp-sek-subs sek)))
(tsp-eq "gruppieren: ES-Sektion hat keine subs" nil
(vsp-sek-subs (cadr (vsp-sektionen (car specs)))))
(tsp-eq "gruppieren: Spec B hat 1 Sektion" 1
(length (vsp-sektionen (cadr specs))))
(princ))
;; ============================================================
;; 4. Die 5 echten HundM-Ketten: Spec == aufgezeichnetes Journal
;; ============================================================
;; Das aufgezeichnete Protokoll (hundm05.json) in Journal-Tokens
;; zurueckuebersetzen - dieselbe Umrechnung wie in test_hundm05.lsp, hier
;; bewusst dupliziert, damit der Test ohne den anderen Treiber laeuft.
(defun tsp-als-string (v)
(cond ((null v) "")
((= (type v) 'STR) v)
((= (type v) 'INT) (itoa v))
((= (type v) 'REAL) (rtos v 2 6))
(T (vl-princ-to-string v))))
(defun tsp-eintrag->journal (e / typ w hz)
(setq typ (ssg-val e "typ"))
(cond
((= typ "point_abs")
(setq w (ssg-val e "wert"))
(if (and w (listp w) (>= (length w) 3))
(list (cons "PT" (mapcar 'float w))) nil))
((= typ "point_rel")
(setq w (ssg-val e "dL") hz (ssg-val e "hz"))
(cond ((not (numberp w)) nil)
((numberp hz) (list (cons "DL" (float w)) (cons "REAL" (float hz))))
(T (list (cons "DL" (float w))))))
((= typ "real")
(setq w (ssg-val e "wert"))
(if (numberp w) (list (cons "REAL" (float w))) nil))
((= typ "int")
(setq w (ssg-val e "wert"))
(if (numberp w) (list (cons "INT" (fix w))) nil))
((= typ "string") (list (cons "STR" (tsp-als-string (ssg-val e "wert")))))
((= typ "step") (list (cons "STEP" (tsp-als-string (ssg-val e "wert")))))
(T nil)))
(defun tsp-protokoll-journale (daten / ketten aktuell journal)
(setq ketten '() journal '())
(foreach obj daten
(cond
((ssg-val obj "test_id")
(if aktuell (setq ketten (cons (list aktuell (reverse journal)) ketten)))
(setq aktuell (ssg-val obj "test_id") journal '()))
((ssg-val obj "typ")
(foreach j (tsp-eintrag->journal obj) (setq journal (cons j journal))))
(T nil)))
(if aktuell (setq ketten (cons (list aktuell (reverse journal)) ketten)))
(reverse ketten))
(defun tsp-tests-echte-ketten ( / p-datei s-datei p-daten specs journale i n
spec soll ist sid)
(setq p-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json"))
(setq s-datei (strcat (getenv "DXFMAKRO")
"/tests/testdata/vf_spec_hundm05.json"))
(cond
((null (findfile p-datei))
(princ (strcat "\n [SKIP] " p-datei " fehlt")))
((null (findfile s-datei))
(princ (strcat "\n [SKIP] " s-datei " fehlt")))
(T
(setq p-daten (ssg-load-json p-datei))
(setq journale (tsp-protokoll-journale p-daten))
(setq specs (vsp-json-laden s-datei))
(tsp-eq "echte Ketten: 5 Protokolle" 5 (length journale))
(tsp-eq "echte Ketten: 5 Specs" 5 (length specs))
(setq n (min (length journale) (length specs)) i 0)
(while (< i n)
(setq spec (nth i specs))
(setq soll (cadr (nth i journale)))
(setq sid (vsp-spec-id (vsp-kopf spec)))
(setq ist (vsp-journal spec))
(if (null ist)
(progn
(tsp-truthy (strcat sid ": Spec fehlerfrei") nil)
(foreach f *vsp-fehler* (princ (strcat "\n - " f))))
(tsp-journal-eq (strcat sid " == aufgezeichnetes Journal ("
(car (nth i journale)) ")")
soll ist))
(setq i (1+ i)))))
(princ))
;; ============================================================
;; Ergebnis-Export (Konvention <name>:export-results fuer test_run_all.lsp)
;; ============================================================
(defun vf_spec:export-results (out-dir / f pfad)
(setq pfad (strcat out-dir "/vf_spec_unit_results.json"))
(setq f (open pfad "w"))
(if f
(progn
(write-line "{" f)
(write-line (strcat " \"pass\": " (itoa *tsp-pass*) ",") f)
(write-line (strcat " \"fail\": " (itoa *tsp-fail*) ",") f)
(write-line (strcat " \"status\": \""
(if (= *tsp-fail* 0) "OK" "FEHLER") "\"") f)
(write-line "}" f)
(close f)
(princ (strcat "\n Ergebnis: " pfad))))
(princ))
(defun c:TEST_VF_SPEC ( / )
(setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '())
(princ "\n================================================================")
(princ "\n TEST_VF_SPEC - Spec-Uebersetzer (vf_spec.lsp)")
(princ "\n================================================================")
(if (null (car (atoms-family 1 '("VSP-JOURNAL"))))
(princ "\n FEHLER: vf_spec.lsp nicht geladen (VarioFoerderer laden).")
(progn
(princ "\n\n-- Glied-Typen --")
(tsp-tests-glieder)
(princ "\n\n-- Validierung --")
(tsp-tests-validierung)
(princ "\n\n-- Gruppieren --")
(tsp-tests-gruppieren)
(princ "\n\n-- Echte HundM-Ketten --")
(tsp-tests-echte-ketten)))
(princ "\n\n----------------------------------------------------------------")
(princ (strcat "\n Ergebnis: " (itoa *tsp-pass*) " PASS, "
(itoa *tsp-fail*) " FAIL"))
(if (> *tsp-fail* 0)
(progn
(princ "\n Fehlgeschlagen:")
(foreach nm (reverse *tsp-fails*) (princ (strcat "\n - " nm)))))
(princ "\n================================================================")
(princ))
(princ "\nTEST_VF_SPEC geladen. Aufruf: TEST_VF_SPEC")
(princ)
+549
View File
@@ -0,0 +1,549 @@
#!/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}'
-30
View File
@@ -161,16 +161,6 @@
"z": 1467,
"comment": "unterer Förderer für die Teile"
},
{
"comment": "Separator Förderer oben",
"function": "insert",
"block": "S-LP",
"x": 8922,
"y": 13509,
"z": 1941,
"rotation": 180,
"assigned_to": "VF_Schnitt_C"
},
{
"test_id": "VF_Mubea_Etage_0061",
"typ": "etage",
@@ -261,16 +251,6 @@
"rotation": 270,
"assigned_to": "Kreisel_unten"
},
{
"comment": "Separator S-LP #11 Y-Abgleich mit Gefaellestrecken (Sep-Y=8777, GF_Mubea_04-Y=9126, diff=349mm, enger Treffer)",
"function": "insert",
"block": "S-LP",
"x": 9510,
"y": 14334,
"z": 1941,
"rotation": 270,
"assigned_to": "Kreisel_rechts"
},
{
"comment": "Separator Eingang 1 Rechter_Kreisel",
"function": "insert",
@@ -311,16 +291,6 @@
"rotation": 180,
"assigned_to": "GF_Mubea"
},
{
"comment": "Separator S-LP #8 Y-Abgleich mit Gefaellestrecken (Sep-Y=-4823, GF_Mubea_20-Y=-3673, diff=1150mm, naechstgelegene Reihe, kein enger Treffer)",
"function": "insert",
"block": "S-LP",
"x": 9510,
"y": -3636,
"z": 1941,
"rotation": 270,
"assigned_to": "GF_Mubea_20"
},
{
"comment": "Scanner linken Kreisel unten",
"function": "insert",
+418
View File
@@ -0,0 +1,418 @@
[
{
"spec_id": "VF_spec_LZ_01",
"modus": 1,
"start_punkt": [4912.239129, 1230.886425, 2154.0],
"start_hoehe": 2154.0,
"as": 1,
"as_winkel": "90",
"as_seite": "links",
"beschreibung": "Linienzug (Modus 1), Start Z2154: GF-Linie 2249 mm / 3 Grad -> Horizontal-VF 3257 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Horizontal-VF 1210 mm -> Vario-Kurve 30 rechts innen -> Vario-Kurve 30 links innen -> ES 90 links",
"sivas_id": "0025",
"sivas_bezeichnung": "VarioFoerderer :1",
"planquadrat": "B/1",
"block": "VF_87",
"dxf_handle": "1BEB5",
"quelle": "INSERT 1BEB5 (Block VF_87) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [4912.239129, 1230.886425, 0.000000]"
},
{
"glied": "Linie-GF",
"nr": 1,
"dl": 2249.25106,
"hz": 90.0,
"gefaelle": "winkel",
"winkel": 3.0,
"ende": "nein"
},
{
"glied": "Horizontal-VF",
"nr": 2,
"dl": 3256.853781,
"gf_verteilung": "einlauf",
"vf_sep_vor": 0,
"vf_sep_nach": 0,
"vf_ende": "nein",
"vf_separator": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "horizontal",
"dl": 1434.100265,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "motorstation"
},
{
"glied": "Horizontal-VF",
"nr": 3,
"dl": 1210.083164,
"gf_verteilung": "einlauf",
"vf_sep_vor": 0,
"vf_sep_nach": 0,
"vf_ende": "ja-mit-es"
},
{
"sub": "vario-kurve",
"winkel": 30,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "horizontal",
"dl": 1830.286247,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "vario-kurve",
"winkel": 30,
"seite": "links",
"variante": "innen"
},
{
"sub": "motorstation"
},
{
"glied": "ES",
"nr": 4,
"winkel": "90",
"seite": "links"
},
{
"spec_id": "VF_spec_LZ_02",
"modus": 1,
"start_punkt": [8571.95068, 1276.061424, 2494.930831],
"start_hoehe": 2494.930831,
"as": 1,
"as_winkel": "90",
"as_seite": "links",
"beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
"sivas_id": "0019",
"sivas_bezeichnung": "VarioFoerderer :5",
"planquadrat": "D/1",
"block": "VF_46",
"dxf_handle": "EFD4",
"quelle": "INSERT EFD4 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
},
{
"glied": "Linie-GF",
"nr": 1,
"dl": 3149.399248,
"hz": 90.0,
"gefaelle": "winkel",
"winkel": 3.0,
"ende": "nein"
},
{
"glied": "Horizontal-VF",
"nr": 2,
"dl": 1574.912802,
"gf_verteilung": "einlauf",
"vf_sep_vor": 0,
"vf_sep_nach": 0,
"vf_ende": "nein",
"vf_separator": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "horizontal",
"dl": 338.966171,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "auf-ab",
"dl": 4528.583638,
"hoehe": 2335.0,
"winkel_idx": 4
},
{
"sub": "horizontal",
"dl": 1443.864872,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "motorstation"
},
{
"glied": "Linie",
"nr": 3,
"dl": 1153.407829,
"hoehe": 2154.0,
"segment_typ": "GF"
},
{
"glied": "ES",
"nr": 4,
"winkel": "90",
"seite": "links"
},
{
"spec_id": "VF_spec_LZ_03",
"modus": 1,
"start_punkt": [12916.465798, 1276.061424, 2494.930831],
"start_hoehe": 2494.930831,
"as": 1,
"as_winkel": "90",
"as_seite": "links",
"beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
"sivas_id": "0018",
"sivas_bezeichnung": "VarioFoerderer :4",
"planquadrat": "F/1",
"block": "VF_46",
"dxf_handle": "FA5A",
"quelle": "INSERT FA5A (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
},
{
"glied": "Linie-GF",
"nr": 1,
"dl": 3149.399248,
"hz": 90.0,
"gefaelle": "winkel",
"winkel": 3.0,
"ende": "nein"
},
{
"glied": "Horizontal-VF",
"nr": 2,
"dl": 1574.912802,
"gf_verteilung": "einlauf",
"vf_sep_vor": 0,
"vf_sep_nach": 0,
"vf_ende": "nein",
"vf_separator": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "horizontal",
"dl": 338.966171,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "auf-ab",
"dl": 4528.583638,
"hoehe": 2335.0,
"winkel_idx": 4
},
{
"sub": "horizontal",
"dl": 1443.864872,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "motorstation"
},
{
"glied": "Linie",
"nr": 3,
"dl": 1153.407829,
"hoehe": 2154.0,
"segment_typ": "GF"
},
{
"glied": "ES",
"nr": 4,
"winkel": "90",
"seite": "links"
},
{
"spec_id": "VF_spec_LZ_04",
"modus": 1,
"start_punkt": [17201.778323, 1205.360995, 2154.0],
"start_hoehe": 2154.0,
"as": 1,
"as_winkel": "90",
"as_seite": "links",
"beschreibung": "Linienzug (Modus 1), Start Z2154: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
"sivas_id": "0020",
"sivas_bezeichnung": "VarioFoerderer :6",
"planquadrat": "H/1",
"block": "VF_46",
"dxf_handle": "E498",
"quelle": "INSERT E498 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
},
{
"glied": "Linie-GF",
"nr": 1,
"dl": 3149.399248,
"hz": 90.0,
"gefaelle": "winkel",
"winkel": 3.0,
"ende": "nein"
},
{
"glied": "Horizontal-VF",
"nr": 2,
"dl": 1574.912802,
"gf_verteilung": "einlauf",
"vf_sep_vor": 0,
"vf_sep_nach": 0,
"vf_ende": "nein",
"vf_separator": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "horizontal",
"dl": 338.966171,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "auf-ab",
"dl": 4528.583638,
"hoehe": 2335.0,
"winkel_idx": 4
},
{
"sub": "horizontal",
"dl": 1443.864872,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "motorstation"
},
{
"glied": "Linie",
"nr": 3,
"dl": 1153.407829,
"hoehe": 2154.0,
"segment_typ": "GF"
},
{
"glied": "ES",
"nr": 4,
"winkel": "90",
"seite": "links"
},
{
"spec_id": "VF_spec_LZ_05",
"modus": 1,
"start_punkt": [21490.273781, 1195.739767, 2494.930831],
"start_hoehe": 2494.930831,
"as": 1,
"as_winkel": "90",
"as_seite": "links",
"beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
"sivas_id": "0008",
"sivas_bezeichnung": "VarioFoerderer :3",
"planquadrat": "J/1",
"block": "VF_46",
"dxf_handle": "104E0",
"quelle": "INSERT 104E0 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
},
{
"glied": "Linie-GF",
"nr": 1,
"dl": 3149.399248,
"hz": 90.0,
"gefaelle": "winkel",
"winkel": 3.0,
"ende": "nein"
},
{
"glied": "Horizontal-VF",
"nr": 2,
"dl": 1574.912802,
"gf_verteilung": "einlauf",
"vf_sep_vor": 0,
"vf_sep_nach": 0,
"vf_ende": "nein",
"vf_separator": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "horizontal",
"dl": 338.966171,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "vario-kurve",
"winkel": 90,
"seite": "rechts",
"variante": "innen"
},
{
"sub": "auf-ab",
"dl": 4528.583638,
"hoehe": 2335.0,
"winkel_idx": 4
},
{
"sub": "horizontal",
"dl": 1443.864872,
"sep_vor": 0,
"sep_nach": 0
},
{
"sub": "motorstation"
},
{
"glied": "Linie",
"nr": 3,
"dl": 1153.407829,
"hoehe": 2154.0,
"segment_typ": "GF"
},
{
"glied": "ES",
"nr": 4,
"winkel": "90",
"seite": "links"
}
]