;; ============================================================ ;; 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 :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)