[CHANGE] Nur noch EIN Test baut die HundM-Geometrie: Spec wird TEST_HUNDM05

Nach dem gruenen VF_SPEC_BAU-Lauf bauten zwei Tests dieselben 5 Ketten. Der
Spec-Pfad ist jetzt der routinemaessige Bau-Test, der Aufzeichnungs-Pfad der
abgeschaltete Regressionstest fuer das Journal-Format.

Umbenennungen:
- tests/testdata/vf_spec_hundm05.json -> tests/testdata/hundm05.json
  Die Spec ist das Eingabeformat und die Datei, in der weitere Ketten der
  Anlage nachgetragen werden. spec_ids jetzt VF_hundm05_LZ_* (wie im
  Aufzeichnungs-Protokoll), damit sich die Ergebnisse beider Laeufe paaren
  lassen.
- tests/testdata/hundm05.json -> tests/testdata/hm_recformat.json
  tests/test_hundm05.lsp -> tests/test_hm_recformat.lsp (TEST_HM_RECFORMAT,
  Praefix hmrec:, Export hm_recformat:export-results)
  tests/test_hundm05.py -> tests/test_hm_recformat.py
  In alltests.json auf "disabled": true.

Neu: tests/test_hundm05.lsp (TEST_HUNDM05) baut die Spec - OHNE eigene
Bau-Logik, es ruft vsp-bau-datei aus Lisp/vf_spec.lsp. Damit gibt es genau
einen Code-Pfad, der aus einer Spec Geometrie erzeugt; c:VF_SPEC_BAU nutzt
denselben. tests/test_hundm05.py prueft das Ergebnis (die 9 Tests, die
vorher in test_vf_spec.py standen); test_vf_spec.py ist jetzt reiner
Uebersetzer-Test ohne CAD.

Warum das Aufzeichnungsformat bleibt (Details in
doc/TODO-plan-vf-interactive.md Abschnitt 4.7):
- hm_recformat.json ist die EINZIGE eingecheckte Kopie der Originaldaten
  (data/polylines.dxf ist mit 124 MB per .gitignore ausgeschlossen). Die Spec
  ist daraus abgeleitet; ohne die Aufzeichnung faellt die rechte Seite des
  Rundlauf-Beweises weg.
- Es prueft eine ANDERE Invariante: das Journal kommt roh aus der XDATA einer
  Kundenzeichnung. TEST_HM_RECFORMAT ist damit der einzige Test dafuer, dass
  ein BESTEHENDER VF_n-Block weiter abspielbar ist - also dass
  Doppelklick-Edit und 2D/3D-Konvertierung an Altbestand funktionieren. Ein
  spec-gebautes Journal kann das nicht zeigen, es kommt aus dem Uebersetzer.
- Es dokumentiert die Frage-Reihenfolge (kommentar je Eintrag); die Spec
  verbirgt den Dialog, das ist ihr Zweck.

Weitere Anpassungen:
- Ergebnisdatei heisst nach der Spec-Datei (<basisname>_results.json), damit
  Befehl und Testrunner in dieselbe Datei schreiben. Verzeichnis: tests/output
  (Override DXFM_VF_SPEC_OUT); NICHT DXFM_RESULTS - das sind die Sivas-/
  CSV-Exporte, dort landete die Datei ausserhalb des Testbaums.
- Neu *vsp-dim-override* fuer TEST_HUNDM05_2D/_3D: wird je Kette angewandt,
  weil der Abbruch-Handler in vf-linienzug-modus *ssg-ils-dim* zurueck setzt.
- Das Anlagenkuerzel der test_id in lib/vf_journal_export.py kam aus dem
  Namen der Ziel-JSON. Nach der Umbenennung haette eine Regenerierung die ids
  stillschweigend auf VF_hm_recformat_LZ_* geaendert und die Paarung der
  beiden Laeufe zerlegt - jetzt feste Konstante TEST_ID_ANLAGE.
- conftest: hundm05_* Fixtures -> hmrec_* (die Spec-Fixtures stehen in
  tests/test_hundm05.py).

Verifiziert: 98 pytest-Tests gruen (die 9 Ergebnis-Tests warten auf einen
neuen TEST_HUNDM05-Lauf), alle .lsp lint-sauber, Spec-Regenerierung
idempotent, TEST_VF_SPEC in BricsCAD 28 PASS / 0 FAIL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 11:11:12 +02:00
parent 428f1ced70
commit 5902343352
20 changed files with 3221 additions and 2953 deletions
+5 -5
View File
File diff suppressed because one or more lines are too long
+59 -27
View File
@@ -77,6 +77,11 @@
(if (not (boundp '*vsp-erster-dl*)) (setq *vsp-erster-dl* T))
(if (not (boundp '*vsp-wo*)) (setq *vsp-wo* "Spec"))
(if (not (boundp '*vsp-prompts*)) (setq *vsp-prompts* 0))
;; Erzwungene Bau-Dimension ("2D"/"3D") fuer einen ganzen Lauf. nil = die
;; Spec bzw. die Sitzung entscheidet. Muss JE KETTE neu gesetzt werden,
;; darum in vsp-bau-aus-spec und nicht einmal aussen: der Abbruch-Handler in
;; vf-linienzug-modus setzt *ssg-ils-dim* zurueck.
(if (not (boundp '*vsp-dim-override*)) (setq *vsp-dim-override* nil))
;; Fehler sammeln (aeltester zuerst) statt beim ersten abzubrechen: eine
;; handgeschriebene Spec hat selten genau einen Tippfehler.
@@ -684,7 +689,7 @@
;; Eine Kette aus einer Spec bauen. Rueckgabe: Ergebnis-Record (Alist).
;; Die Klammer um vf-linienzug-modus ist dieselbe wie in
;; tests/test_hundm05.lsp: *error* sichern, fangen, *error* zurueck. Weil
;; tests/test_hm_recformat.lsp: *error* sichern, fangen, *error* zurueck. Weil
;; gefangen wird, laeuft der Abbruch-Handler der Modus-Funktion NICHT - eine
;; abgebrochene Kette bleibt also als lose Geometrie liegen und ist per
;; c:VF_SEKTION_RESTORE rettbar (vfl-segment-xdata-sichern hat pro Iteration
@@ -716,7 +721,11 @@
;; Dimension je Kette neu setzen: der Abbruch-Handler in
;; vf-linienzug-modus setzt *ssg-ils-dim* zurueck, eine abgebrochene
;; Kette wuerde die folgenden sonst in der falschen Dimension bauen.
(setq dim (ssg-val kopf "dim"))
;; Vorrang: expliziter Lauf-Override (Testbefehle _2D/_3D) vor dem
;; Feld in der Spec. Ist keines gesetzt, gilt die Sitzung.
(setq dim (if (and (boundp '*vsp-dim-override*) *vsp-dim-override*)
*vsp-dim-override*
(ssg-val kopf "dim")))
(if dim (setq *ssg-ils-dim* dim))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
(princ (strcat " -> " (itoa (length journal)) " Journal-Eintraege, "
@@ -811,7 +820,7 @@
;; TEIL 8: BEFEHL
;; ============================================================
;; Fragt nichts. Spec-Datei aus DXFM_VF_SPEC, sonst
;; tests/testdata/vf_spec_hundm05.json; Ergebnisse nach DXFM_RESULTS bzw.
;; tests/testdata/hundm05.json (die Ketten der Anlage HundM);
;; tests/output.
;; ssg-ensure gibt es nur, wenn das Menue geladen wurde (SSG_LIB.mnl). Im
@@ -831,30 +840,23 @@
"\n Ergebnis: " (itoa ok) " OK, " (itoa rest) " Fehler"
"\n================================================")))
(defun c:VF_SPEC_BAU ( / datei ziel specs records alt-dim)
;; Eine Spec-Datei bauen. Rueckgabe: Liste der Ergebnis-Records (nil, wenn
;; die Datei nicht lesbar war). Schreibt NICHTS - das entscheidet der
;; Aufrufer (der Befehl legt die Datei selbst ab, der Testrunner ueber
;; <name>:export-results). Genutzt von c:VF_SPEC_BAU UND von
;; tests/test_hundm05.lsp, damit es nur EINEN Weg gibt, der Geometrie aus
;; einer Spec erzeugt.
(defun vsp-bau-datei (datei / specs records alt-dim)
(vsp-modul-sichern "Gefaellestrecke")
(setq datei (getenv "DXFM_VF_SPEC"))
(if (or (null datei) (= datei ""))
(setq datei (strcat (getenv "DXFMAKRO")
"/tests/testdata/vf_spec_hundm05.json")))
;; Ergebnis-JSON gehoert nach tests/output - dort liegen alle
;; Testergebnisse (Konvention test_run_all.lsp, von den
;; pytest-Fixtures gelesen, per .gitignore ausgeschlossen).
;; NICHT DXFM_RESULTS: das ist das Verzeichnis der Sivas-/CSV-
;; Exporte, dort wuerde die Datei ausserhalb des Testbaums landen.
(setq ziel (getenv "DXFM_VF_SPEC_OUT"))
(if (or (null ziel) (= ziel ""))
(setq ziel (strcat (getenv "DXFMAKRO") "/tests/output")))
(setq ziel (strcat ziel "/vf_spec_results.json"))
(princ (strcat "\n================================================"
"\n VF_SPEC_BAU - " datei
"\n================================================"))
(setq specs (vsp-json-laden datei))
(if (null specs)
(princ "\n[VF_SPEC] Keine Specs gelesen - Abbruch.")
(progn
(cond
((null specs)
(princ "\n[VF_SPEC] Keine Specs gelesen - Abbruch.")
nil)
(T
(ssg-start "VF_SPEC_BAU" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
;; Ohne ATTREQ/ATTDIA 0 fragt jedes (command "_.INSERT" ...) nach
;; Attributwerten - der wahrscheinlichste Haenger im Batch.
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
@@ -873,10 +875,40 @@
(princ (strcat "\n[VF_SPEC] FEHLER in der Spec-Schleife: "
(vl-catch-all-error-message records)))
(setq records '())))
(vl-mkdir (vl-filename-directory ziel))
(vsp-results-schreiben records ziel)
(vsp-bilanz records)
(ssg-end)))
(ssg-end)
records)))
;; Standard-Spec-Datei: DXFM_VF_SPEC, sonst die HundM-Ketten.
(defun vsp-standard-datei ( / datei)
(setq datei (getenv "DXFM_VF_SPEC"))
(if (or (null datei) (= datei ""))
(setq datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json")))
datei)
;; Ergebnis-Datei zur Spec-Datei: gleicher Basisname, Endung _results.json,
;; im Testausgabe-Verzeichnis. Damit schreibt ein Lauf mit hundm05.json nach
;; hundm05_results.json - dieselbe Datei, die der Testrunner erwartet, also
;; keine zweite Kopie derselben Ergebnisse.
;; NICHT DXFM_RESULTS als Verzeichnis: das sind die Sivas-/CSV-Exporte, dort
;; wuerde die Datei ausserhalb des Testbaums landen.
(defun vsp-ziel-datei (spec-datei / verz)
(setq verz (getenv "DXFM_VF_SPEC_OUT"))
(if (or (null verz) (= verz ""))
(setq verz (strcat (getenv "DXFMAKRO") "/tests/output")))
(strcat verz "/" (vl-filename-base spec-datei) "_results.json"))
(defun c:VF_SPEC_BAU ( / datei ziel records)
(setq datei (vsp-standard-datei))
(setq ziel (vsp-ziel-datei datei))
(princ (strcat "\n================================================"
"\n VF_SPEC_BAU - " datei
"\n================================================"))
(setq records (vsp-bau-datei datei))
(if records
(progn
(vl-mkdir (vl-filename-directory ziel))
(vsp-results-schreiben records ziel)))
(princ))
(princ "\n[vf_spec] geladen (VF_SPEC_BAU)")
+47
View File
@@ -434,6 +434,53 @@ Zwei Nacharbeiten aus dem Lauf:
`(load (strcat (getenv "DXFMAKRO") "/tests/test_vf_spec.lsp"))` - oder
ueber `SSG_RUN_ALL_TESTS`, wo `vf_spec` inzwischen in `alltests.json` steht.
### 4.7 Aufteilung der Testfaelle (2026-09-03)
Nach dem gruenen `VF_SPEC_BAU`-Lauf gab es zwei Tests, die dieselbe Geometrie
bauen. Aufgeloest:
| | Aufzeichnungsformat | Spec |
|---|---|---|
| Daten | `tests/testdata/hm_recformat.json` (281 Objekte) | `tests/testdata/hundm05.json` (52 Objekte) |
| Treiber | `tests/test_hm_recformat.lsp`, `TEST_HM_RECFORMAT` | `tests/test_hundm05.lsp`, `TEST_HUNDM05` |
| pytest | `tests/test_hm_recformat.py` | `tests/test_hundm05.py` |
| In `alltests.json` | **abgeschaltet** | aktiv |
Der Spec-Test ist damit der routinemaessige Bau-Test; die Spec ist auch die
Datei, in der weitere Ketten der Anlage nachgetragen werden.
**Warum die Aufzeichnung bleibt** (und nicht durch die Spec ersetzt wird):
1. Sie ist die **einzige eingecheckte Kopie der Originaldaten** -
`data/polylines.dxf` ist mit 124 MB per `.gitignore` ausgeschlossen. Die
Spec ist daraus abgeleitet; ohne die Aufzeichnung faellt die rechte Seite
des Rundlauf-Beweises weg und die Spec belegt nur noch, dass der Emitter
mit sich selbst uebereinstimmt.
2. Sie prueft eine **andere Invariante**: ihr Journal kommt roh aus der XDATA
einer Kundenzeichnung. Damit ist `TEST_HM_RECFORMAT` der einzige Test
dafuer, dass ein BESTEHENDER `VF_n`-Block weiter abspielbar ist - also
dass Doppelklick-Edit und 2D/3D-Konvertierung an Altbestand funktionieren
(I1). Ein spec-gebautes Journal kann das nicht zeigen, es kommt aus dem
Uebersetzer. Bei Aenderungen am Journal-Format, an den
`vfl-in-*`-Wrappern oder am XDATA-Layout also bewusst einschalten.
3. Sie **dokumentiert die Frage-Reihenfolge**: jeder Eintrag traegt einen
`kommentar`, der die zugehoerige Frage nennt. Die Spec verbirgt den Dialog -
das ist ihr Zweck - und kann diese Rolle nicht uebernehmen.
Damit es nur EINEN Code-Pfad gibt, der aus einer Spec Geometrie erzeugt, hat
`tests/test_hundm05.lsp` keine eigene Bau-Logik: es ruft `vsp-bau-datei` aus
`Lisp/vf_spec.lsp` - dieselbe Funktion, die auch `c:VF_SPEC_BAU` nutzt. Die
Ergebnisdatei heisst jetzt nach der Spec-Datei
(`<basisname>_results.json`), damit Befehl und Testrunner in dieselbe Datei
schreiben und keine zweite Kopie derselben Ergebnisse entsteht.
Nebenbei behoben: das Anlagenkuerzel der `test_id` in
`lib/vf_journal_export.py` kam aus dem Namen der Ziel-JSON. Nach der
Umbenennung haette eine Regenerierung die ids stillschweigend von
`VF_hundm05_LZ_*` auf `VF_hm_recformat_LZ_*` geaendert - und damit die
Paarung zwischen den beiden Testlaeufen zerlegt. Jetzt feste Konstante
`TEST_ID_ANLAGE`.
**Noch offen in Stufe 1**: die Uebersetzer fuer Modus 2 (`pfad-handles` +
Segmentliste, Abschnitt 4.4) und Modus 3 sind NICHT gebaut - Modus 2 braucht
laut Reihenfolge (Punkt 11) erst 2-3 echte `"linienzug2"`-Journale als
+2 -2
View File
@@ -550,11 +550,11 @@ echten HundM-Ketten - er beweist, dass die Spec das Journal verlustfrei
abbildet. Modus 2 und 3 haben noch keinen Uebersetzer (Modus 2 braucht erst
echte `"linienzug2"`-Journale als Referenz).
Beispiel: `tests/test_hundm05.lsp` (5 echte Ketten aus `data/polylines.dxf`)
Beispiel: `tests/test_hm_recformat.lsp` (5 echte Ketten aus `data/polylines.dxf`)
setzt alle drei Schalter mit Save/Restore, ersetzt `getpoint`/`getstring`/
`getint`/`getreal`/`alert` zusaetzlich durch **zaehlende** Stubs (Netz fuer
Prompts ausserhalb der Wrapper) und schreibt `prompts`, `headless_fehler` und
`meldungen` in `tests/output/hundm05_results.json`. Noch nicht umgestellt:
`meldungen` in `tests/output/hm_recformat_results.json`. Noch nicht umgestellt:
`vfl-konvertiere-ent` (Batch-2D/3D-Konvertierung) laeuft weiter ohne
`*vfl-headless*` - dort ist eine Live-Rueckfrage bisher gewolltes Verhalten.
+15 -7
View File
@@ -35,9 +35,9 @@ Aufruf:
python vf_journal_export.py <dxf> <output.json> [--csv <sivas_export.csv>]
[--kein-startpunkt-fix]
Beispiel (erzeugt tests/testdata/hundm05.json):
Beispiel (erzeugt tests/testdata/hm_recformat.json):
python lib/vf_journal_export.py data/polylines.dxf \
tests/testdata/hundm05.json --csv results/HundM_export.csv
tests/testdata/hm_recformat.json --csv results/HundM_export.csv
"""
import argparse
@@ -57,6 +57,9 @@ TRENNER = "^J"
RUND = 6
# Anlagenkuerzel in den test_ids (VF_<kuerzel>_LZ_01 ...).
TEST_ID_ANLAGE = "hundm05"
# Antwort-Klartexte der Menues aus vf_linienzug.lsp (nur zur Annotation).
JA_NEIN = {"1": "Ja", "2": "Nein"}
SEITE = {"1": "links", "2": "rechts"}
@@ -531,12 +534,17 @@ def baue_testfaelle(inserts, strecken, startpunkt_fix, quelle_datei,
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei ZEILENWEISE und
behandelt jedes "{" als Objektanfang. Ein verschachteltes Array wuerde die
Kopf-Objekte verschlucken. Der Treiber gruppiert wieder ueber "test_id"
(siehe hundm05:gruppiere in tests/test_hundm05.lsp).
(siehe hmrec:gruppiere in tests/test_hm_recformat.lsp).
"""
# Anlagenkuerzel der test_id kommt aus dem Namen der Ziel-JSON (die
# Testfall-Sammlung), nicht aus dem Zeichnungsnamen: tests/testdata/
# hundm05.json -> VF_hundm05_LZ_01 .. _nn.
praefix = os.path.splitext(os.path.basename(ziel_datei))[0]
# Anlagenkuerzel der test_id: FEST, nicht aus dem Dateinamen
# abgeleitet. Die id benennt die ANLAGE, nicht die Datei - und sie
# muss stabil bleiben, weil dieselbe Kette in beiden Testlaeufen
# (Aufzeichnungsformat und Spec) denselben Namen tragen soll:
# tests/testdata/hundm05.json (die Spec) wird aus dieser Datei
# erzeugt und uebernimmt die ids. Frueher kam das Kuerzel aus dem
# Namen der Ziel-JSON; nach der Umbenennung in hm_recformat.json
# haette eine Regenerierung die ids stillschweigend geaendert.
praefix = TEST_ID_ANLAGE
faelle = []
for nr, e in enumerate(sorted(inserts, key=lambda i: i["x"]), start=1):
eintraege = Journal(e["journal_roh"]).dekodiere()
+13 -10
View File
@@ -29,8 +29,8 @@ lib/vf_journal_export.py (Journal.dekodiere) und ist dort kommentiert.
Aufruf (erzeugt die Spec-Testdaten aus dem Protokoll der echten Ketten):
python lib/vf_spec_export.py tests/testdata/hundm05.json \
tests/testdata/vf_spec_hundm05.json
python lib/vf_spec_export.py tests/testdata/hm_recformat.json \
tests/testdata/hundm05.json
"""
import argparse
@@ -111,7 +111,7 @@ def _ja_code(wert):
# STR Menue-/Wertantwort STEP Glied-Marker (Label = Segmenttyp)
def tokens_aus_protokoll(eintraege):
"""Das Frage-Antwort-Protokoll (Schema tests/testdata/hundm05.json)
"""Das Frage-Antwort-Protokoll (Schema tests/testdata/hm_recformat.json)
zurueck in Journal-Tokens uebersetzen - die Umkehrung von
vf_journal_export.Journal.add."""
tok = []
@@ -577,8 +577,8 @@ def journal_aus_spec(spec):
# Flaches JSON (vom LISP-Parser lesbar)
# ============================================================
# ssg-load-json/ssg-parse-json-array liest ZEILENWEISE und FLACH: jedes "{"
# beginnt ein Objekt, Verschachtelung ist unmoeglich. Also dieselbe Form wie
# tests/testdata/hundm05.json - Kopf-Objekt ("spec_id"), dann Sektionen
# beginnt ein Objekt, Verschachtelung ist unmoeglich. Also dieselbe flache
# Form wie die Aufzeichnung: ein Kopf-Objekt ("spec_id"), dann Sektionen
# ("glied") und deren Sub-Knoten ("sub") in Reihenfolge.
def flache_objekte(spec_id, spec, kopf_extra=None):
@@ -626,7 +626,7 @@ def flache_objekte(spec_id, spec, kopf_extra=None):
def spec_aus_flachen_objekten(objekte):
"""Umkehrung von flache_objekte - dieselbe Gruppierregel wie
hundm05:gruppiere in LISP (Kopf, dann glied/sub in Reihenfolge)."""
hmrec:gruppiere in LISP (Kopf, dann glied/sub in Reihenfolge)."""
specs = []
for obj in objekte:
if "spec_id" in obj:
@@ -680,9 +680,9 @@ def spec_aus_flachen_objekten(objekte):
def main():
p = argparse.ArgumentParser(
description="Frage-Antwort-Protokoll (hundm05.json) in Specs "
description="Frage-Antwort-Protokoll (hm_recformat.json) in Specs "
"uebersetzen und den Rundlauf pruefen.")
p.add_argument("protokoll", help="Eingabe (Schema hundm05.json)")
p.add_argument("protokoll", help="Eingabe (Schema hm_recformat.json)")
p.add_argument("ziel", help="Ausgabe (flaches Spec-JSON)")
args = p.parse_args()
@@ -720,8 +720,11 @@ def main():
"planquadrat", "block", "dxf_handle", "quelle"):
if feld in kopf:
extra[feld] = kopf[feld]
alle.extend(flache_objekte(tid.replace("hundm05_LZ", "spec_LZ"),
spec, extra))
# Die spec_id bleibt die Kettenkennung aus dem Protokoll: dieselbe
# Kette soll in beiden Testlaeufen (Spec-Pfad und Aufzeichnungs-
# Pfad) denselben Namen tragen, sonst laesst sich das Ergebnis
# nicht paaren.
alle.extend(flache_objekte(tid, spec, extra))
print("%s %-20s %2d Sektionen, %3d Tokens - Rundlauf ok"
% (PREFIX, tid, len(spec["sektionen"]), len(tok)))
+14 -3
View File
@@ -102,7 +102,7 @@ Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module.
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer" }
]
```
@@ -160,7 +160,16 @@ tests/
test_foerderer.py # pytest-Validierung Foerderer (Status/Mathematik/CSV/Sivas)
test_gefaellestrecke.py # pytest-Validierung Gefaellestrecke
test_mubea.py # pytest-Validierung Mubea-Gesamtmodell
test_hundm05.py # pytest-Validierung HundM05 (5 Linienzuege)
test_hundm05.lsp # LISP: baut die 5 HundM-Ketten aus ihrer
# Spec (testdata/hundm05.json) - ruft
# vsp-bau-datei, keine eigene Bau-Logik
test_hundm05.py # pytest: die so gebaute Anlage pruefen
test_hm_recformat.lsp # LISP: dieselben Ketten aus dem ROHEN
# Journal der Kundenzeichnung - der
# Regressionstest fuers Aufzeichnungs-
# format (in alltests.json abgeschaltet,
# baut sonst dieselbe Geometrie zweimal)
test_hm_recformat.py # pytest-Validierung dazu
test_vf_spec.lsp # LISP-Unittest: Spec-Uebersetzer (vsp-journal),
# je Glied-Typ + die 5 echten HundM-Ketten
# gegen ihr aufgezeichnetes Journal
@@ -179,7 +188,9 @@ tests/
kreisel_tests.json # Testfall-Definitionen fuer Kreisel
omniflo_tests.json # Testfall-Definitionen fuer Omniflo
omniflo_strecke_tests.json # Streckenzug: 4 Geraden + 2 Boegen + 1 Weiche (7 Elemente)
foerderer_tests.json / mubea.json / hundm05.json / ...
hm_recformat.json # Aufzeichnung: rohes Journal der 5 Ketten
hundm05.json # daraus abgeleitete Spec (wird erweitert)
foerderer_tests.json / mubea.json / ...
output/ # Ergebnisse aus BricsCAD (nicht in Git)
<name>_results.json # JSON-Ergebnisse pro Testmodul (immer)
<name>_tests.dxf/.dwg # Zeichnung pro Testmodul (gemaess save in alltests.json)
+2 -1
View File
@@ -8,5 +8,6 @@
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer" },
{ "name": "hm_recformat", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
]
+19 -17
View File
@@ -189,35 +189,37 @@ def mubea_dxf():
return ezdxf.readfile(path)
# --- HundM05 Fixtures ---
# --- Aufzeichnungsformat-Fixtures (TEST_HM_RECFORMAT) ---
# hm_recformat.json ist das ROHE Eingabe-Journal der 5 HundM-Ketten,
# ausgelesen aus der XDATA der Kundenzeichnung (lib/vf_journal_export.py).
# Die daraus abgeleitete Spec liegt als testdata/hundm05.json - die Fixtures
# dafuer stehen in tests/test_hundm05.py.
@pytest.fixture
def hundm05_testdata():
"""Laedt die HundM05-Testdefinitionen aus testdata/hundm05.json.
Erzeugt von lib/dxf_scan_components.py aus results/ST500592_05.dxf
(Fenster 216000 144000 228000 156000, Kreisel-Abschnitt).
"""
path = os.path.join(_testdata_dir(), "hundm05.json")
def hmrec_testdata():
"""Laedt das Frage-Antwort-Protokoll aus testdata/hm_recformat.json."""
path = os.path.join(_testdata_dir(), "hm_recformat.json")
return _load_json(path)
@pytest.fixture
def hundm05_results():
"""Laedt die HundM05-Ergebnisse aus output/hundm05_results.json."""
path = os.path.join(_output_dir(), "hundm05_results.json")
def hmrec_results():
"""Laedt die Ergebnisse aus output/hm_recformat_results.json."""
path = os.path.join(_output_dir(), "hm_recformat_results.json")
if not os.path.exists(path):
pytest.skip("hundm05_results.json nicht vorhanden - TEST_HUNDM05 in BricsCAD ausfuehren")
pytest.skip("hm_recformat_results.json nicht vorhanden - "
"TEST_HM_RECFORMAT in BricsCAD ausfuehren")
data = _load_json(path)
if not data:
pytest.skip("hundm05_results.json ist leer")
pytest.skip("hm_recformat_results.json ist leer")
return data
@pytest.fixture
def hundm05_dxf():
"""Laedt die erzeugte HundM05-DXF aus output/hundm05_tests.dxf."""
path = os.path.join(_output_dir(), "hundm05_tests.dxf")
def hmrec_dxf():
"""Laedt die erzeugte DXF aus output/hm_recformat_tests.dxf."""
path = os.path.join(_output_dir(), "hm_recformat_tests.dxf")
if not os.path.exists(path):
pytest.skip("hundm05_tests.dxf nicht vorhanden - TESTRUN (save:dxf) in BricsCAD ausfuehren")
pytest.skip("hm_recformat_tests.dxf nicht vorhanden - TESTRUN "
"(save:dxf) in BricsCAD ausfuehren")
return ezdxf.readfile(path)
+683
View File
@@ -0,0 +1,683 @@
;; ============================================================
;; test_hm_recformat.lsp - Regressionstest fuer das AUFZEICHNUNGSFORMAT
;; (rec = recorded): baut die 5 VarioFoerderer-Linienzuege der Anlage HundM
;; aus den Journalen nach, die in der Kundenzeichnung stehen.
;;
;; ABGRENZUNG zu TEST_HUNDM05 (tests/test_hundm05.lsp): dort werden dieselben
;; 5 Ketten aus einer SPEC gebaut (tests/testdata/hundm05.json, das neue
;; Eingabeformat) - das ist der Test fuer den Bau-Pfad und der, der
;; routinemaessig laeuft. HIER geht es um etwas anderes: das Journal kommt
;; roh aus der XDATA einer echten Zeichnung. Damit ist dies der einzige Test,
;; der belegt, dass ein BESTEHENDER VF_n-Block weiter abspielbar bleibt -
;; also dass Doppelklick-Editieren und 2D/3D-Konvertierung an Altbestand
;; funktionieren (Invariante I1 in doc/TODO-plan-vf-interactive.md). Ein
;; spec-gebautes Journal kann das nicht zeigen, es kommt aus dem Uebersetzer.
;;
;; Darum in tests/alltests.json auf "disabled": true - er baut dieselbe
;; Geometrie wie TEST_HUNDM05 und kostet doppelte CAD-Zeit. Bei Aenderungen
;; am Journal-Format, an den vfl-in-*-Wrappern oder am XDATA-Layout aber
;; bewusst einschalten: dann ist er der Test, der zaehlt.
;;
;; Datenquelle: tests/testdata/hm_recformat.json, erzeugt von
;; python lib/vf_journal_export.py data/polylines.dxf \
;; tests/testdata/hm_recformat.json --csv results/HundM_export.csv
;; Das sind die EINGABE-JOURNALE der echten Ketten - ausgelesen aus der XDATA
;; (App SSG_VF_EDIT, Marker "linienzug") der VF_n-Bloecke in data/polylines.dxf,
;; also genau die Werte, mit denen die Ketten in BricsCAD gebaut wurden.
;;
;; JSON-Aufbau (FLACH, weil ssg-load-json zeilenweise liest - Details in
;; tests/testdata/object_data.md Abschnitt 8):
;; Kopf-Objekt (traegt "test_id"): test_id, anlagetyp, modus,
;; anzahl_eingaben, block, dxf_handle, startpunkt_mm,
;; rotation_grad, beschreibung, sivas_* , erwartung_hinweis
;; Eingabe-Objekt (traegt "typ"): point_abs | point_rel | real | int |
;; string | step - in exakter Frage-Reihenfolge
;; Ein Kopf-Objekt beginnt eine Kette, die folgenden Eingabe-Objekte gehoeren
;; dazu (hmrec:gruppiere).
;;
;; WARUM REPLAY UND KEINE EINGABE-MOCKS (Unterschied zu test_linienzug.lsp):
;; test_linienzug.lsp ersetzt getpoint/getstring/getint/getreal durch Mocks und
;; rechnet Punkte aus "dL"+"hz" selbst aus. Das geht nur, weil
;; linienzug_tests.json zu JEDEM Segment ein "hz" mitfuehrt. Ein echtes Journal
;; tut das nicht: vfl-in-abstand journalisiert die Fahrtrichtung NUR beim
;; allerersten Segment der Kette, jedes weitere erbt sie vom Vorgaenger. Ein
;; Mock kann diese geerbte Richtung nicht kennen - er wuerde einen Punkt in der
;; falschen Richtung liefern und die Projektion in vfl-neue-linie-messen ergaebe
;; eine falsche (bis auf 0 zusammenfallende) Laenge. Darum wird hier derselbe
;; Weg genommen, den die Produktion beim Editieren/Konvertieren nutzt:
;; vfl-journal-replay-start + vf-linienzug-modus (siehe vfl-konvertiere-ent in
;; Lisp/vf_linienzug.lsp). Die vfl-in-*-Wrapper ziehen ihre Werte dann direkt
;; aus der Replay-Queue - inklusive der Sonderregel fuer "hz".
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (VarioFoerderer inkl. vf_linienzug, Gefaellestrecke,
;; ssg_core, ssg_dbg)
;; - Umgebungsvariable DXFMAKRO gesetzt
;;
;; Speichert (via SSG_RUN_ALL_TESTS bei "save":"dxf"):
;; tests/output/hm_recformat_tests.dxf
;; tests/output/hm_recformat_results.json (via hm_recformat:export-results)
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_hm_recformat.lsp"))
;; TEST_HM_RECFORMAT ; in der aktuellen Dimension (ssg-ils-dim-aktuell)
;; TEST_HM_RECFORMAT_2D ; erzwungen 2D
;; TEST_HM_RECFORMAT_3D ; erzwungen 3D
;;
;; Debug-Datei (nur wenn eingeschaltet): (dbg-schalter-on "hm_recformat")
;; schreibt hm_recformat.dbg ins DXFM_LOG-Verzeichnis (Journal je Kette als JSON).
;; ============================================================
;; ============================================================
;; Ergebnis-JSON
;; ============================================================
;; --- JSON-Hilfen fuer Diagnosefelder (Strings/String-Listen) ---
;; Die extra-Felder in hmrec:result-json werden ROH in das JSON gesetzt,
;; ein Text muss also selbst seine Anfuehrungszeichen mitbringen und die
;; JSON-Sonderzeichen maskieren.
(defun hmrec:json-escape (s / out i c)
(setq out "" i 1)
(while (<= i (strlen s))
(setq c (substr s i 1))
(setq out (cond ((= c "\"") (strcat out "\\\""))
((= c "\\") (strcat out "\\\\"))
((= c "\n") (strcat out " "))
(T (strcat out c))))
(setq i (1+ i)))
out)
(defun hmrec:json-str (s)
(if s (strcat "\"" (hmrec:json-escape s) "\"") "null"))
(defun hmrec:json-liste (lst / s first)
(setq s "[" first T)
(foreach x lst
(if (not first) (setq s (strcat s ", ")))
(setq s (strcat s (hmrec:json-str x)) first nil))
(strcat s "]"))
;; --- Headless-Diagnose der Produktion lesen ---
;; vfl-headless-abbruch (Lisp/vf_linienzug.lsp) legt bei erschoepfter
;; Replay-Queue Art der Eingabe, Glied-Nummer und Eingabe-Nummer ab. Das ist
;; die eigentliche Desync-Auskunft - ohne sie weiss man nur, DASS Werte offen
;; blieben, nicht WO der Ablauf abgewichen ist.
(defun hmrec:headless-fehler-text ( / f)
(if (and (boundp '*vfl-headless-fehler*) *vfl-headless-fehler*)
(progn
(setq f *vfl-headless-fehler*)
(strcat (cdr (assoc "was" f)) " @ " (cdr (assoc "ort" f))))
nil))
;; Zaehler der trotz allem erreichten Live-Eingaben (muss 0 bleiben).
(defun hmrec:prompts ()
(if (and (boundp '*hmrec-prompts*) *hmrec-prompts*) *hmrec-prompts* 0))
;; "dimension" haelt fest, in welcher Dimension (2D/3D) gebaut wurde. Das ist
;; KEINE Eigenschaft der Eingabedaten: hm_recformat.json beschreibt die Ketten
;; dimensionsfrei. Womit unsere Makros bauen, entscheidet ssg-ils-dim-aktuell -
;; unsere Bibliothek hat von jedem Block eine _2D- und eine _3D-Variante.
;; extra: Alist zusaetzlicher Felder ((name . wert-string)), wird als
;; JSON-Zahlen/Strings so uebernommen, wie sie uebergeben werden.
(defun hmrec:result-json (test-id kind status block-name block-handle
insert-point attribs extra /
json tag val first)
(setq json (strcat " {\n"
" \"test_id\": \"" test-id "\",\n"
" \"kind\": \"" kind "\",\n"
" \"status\": \"" status "\",\n"
" \"dimension\": \""
(if (and (boundp '*hmrec-dimension*) *hmrec-dimension*)
*hmrec-dimension* "?") "\",\n"
" \"block_name\": \"" (if block-name block-name "") "\",\n"
" \"block_handle\": \"" (if block-handle block-handle "") "\",\n"
" \"insert_point\": ["
(if insert-point
(strcat (rtos (car insert-point) 2 1) ", "
(rtos (cadr insert-point) 2 1) ", "
(rtos (if (caddr insert-point) (caddr insert-point) 0.0) 2 1))
"0.0, 0.0, 0.0")
"],\n"))
(foreach f extra
(setq json (strcat json " \"" (car f) "\": " (cdr f) ",\n")))
(setq json (strcat json " \"actual_attributes\": {"))
(setq first T)
(foreach att attribs
(setq tag (car att) val (cdr att))
(if (not first) (setq json (strcat json ",")))
(setq json (strcat json "\n \"" tag "\": \"" (if val val "") "\""))
(setq first nil))
(setq json (strcat json "\n }\n }"))
json
)
;; --- Aus einer Block-ENAME ein Ergebnis-JSON erzeugen ---
;; status: "executed" bei sauberem Lauf; ein abweichender Status (z.B. bei
;; einem Queue-Desync) wird durchgereicht, die Block-Angaben bleiben dabei
;; erhalten - sonst waere der betroffene Block im Ergebnis nicht auffindbar.
(defun hmrec:ent-json-status (test-id kind status ent extra / ed)
(if ent
(progn
(setq ed (entget ent))
(hmrec:result-json test-id kind status
(cdr (assoc 2 ed)) (cdr (assoc 5 ed)) (cdr (assoc 10 ed))
(ssg-attrib-read ent) extra))
(hmrec:result-json test-id kind "failed" nil nil nil nil extra))
)
(defun hmrec:ent-json (test-id kind ent extra)
(hmrec:ent-json-status test-id kind "executed" ent extra)
)
;; --- Letzten INSERT mit Blocknamen-Praefix suchen ---
;; AutoLISP kennt KEIN entprev (nur entnext und entlast) - rueckwaerts durch
;; die Zeichnung laufen geht also nicht. Gesucht wird darum VORWAERTS, wobei
;; der letzte Treffer gewinnt. Ein blosses (entlast) genuegt nicht: nach dem
;; Bau koennen weitere Objekte entstehen (z.B. Beschriftungstexte), entlast
;; ist dann nicht der gesuchte Block - genau daran scheiterte der Testlauf.
;; ab-ent = Stand der Zeichnung VOR dem Bau. Damit prueft die Suche nur die
;; neu entstandenen Objekte: schnell, und sie kann nicht versehentlich den
;; Block eines frueheren Baus liefern (siehe Kommentar bei -ab).
(defun hmrec:insert-prefix-p (ent pref / ed bn)
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
(and ed (= (cdr (assoc 0 ed)) "INSERT") bn
(>= (strlen bn) (strlen pref))
(= (substr bn 1 (strlen pref)) pref)))
;; Vorwaertslauf ab ab-ent (nil = ganze Zeichnung); der LETZTE Treffer
;; gewinnt. Der entget-Test faengt ein inzwischen ungueltiges ab-ent ab
;; (entget liefert dann nil, entnext wuerde einen Fehler werfen).
(defun hmrec:suche-vorwaerts (pref ab-ent / ent found)
(setq ent (if (and ab-ent (entget ab-ent)) (entnext ab-ent) (entnext)))
(while ent
(if (hmrec:insert-prefix-p ent pref) (setq found ent))
(setq ent (entnext ent)))
found)
(defun hmrec:last-insert-prefix-ab (pref ab-ent / ent)
(if ab-ent
;; MIT Marker nur die neu entstandenen Objekte pruefen - und BEWUSST
;; ohne entlast-Kurzschluss: hat dieser Bau keinen Block erzeugt, waere
;; entlast der Block eines FRUEHEREN Baus und das Ergebnis wuerde der
;; falschen Kette zugeordnet. Kein Treffer = kein Block, das ist die
;; richtige Antwort.
(hmrec:suche-vorwaerts pref ab-ent)
(progn
;; Ohne Marker: erst entlast (der haeufige Fall, das gerade
;; eingefuegte Objekt), sonst die ganze Zeichnung.
(setq ent (entlast))
(if (and ent (hmrec:insert-prefix-p ent pref))
ent
(hmrec:suche-vorwaerts pref nil)))))
(defun hmrec:last-insert-prefix (pref)
(hmrec:last-insert-prefix-ab pref nil))
;; ============================================================
;; JSON-Eintraege -> Ketten
;; ============================================================
;; Das flache JSON-Array in Ketten gruppieren: ein Objekt mit "test_id" ist
;; ein Kopf und beginnt eine neue Kette, die folgenden Objekte mit "typ" sind
;; ihre Eingaben (Reihenfolge = Frage-Reihenfolge, bleibt erhalten).
;; Rueckgabe: Liste von (kopf eingaben-liste).
(defun hmrec:gruppiere (daten / ketten kopf eingaben)
(setq ketten '() kopf nil eingaben '())
(foreach obj daten
(cond
((ssg-val obj "test_id")
(if kopf
(setq ketten (cons (list kopf (reverse eingaben)) ketten)))
(setq kopf obj eingaben '()))
((ssg-val obj "typ")
(if kopf (setq eingaben (cons obj eingaben))))
)
)
(if kopf (setq ketten (cons (list kopf (reverse eingaben)) ketten)))
(reverse ketten)
)
;; --- Wert als String lesen (auch wenn er als Zahl im JSON steht) ---
;; Die Menue-Antworten sind Strings ("1"/"2"/"90"/"30"). Steht in einer von
;; Hand gepflegten Datei versehentlich 1 statt "1", wuerde ein Zahlenwert in
;; die Replay-Queue wandern und der Vergleich (= antwort "1") schlagen fehl -
;; darum hier immer nach String wandeln.
(defun hmrec:als-string (v)
(cond
((null v) "")
((= (type v) 'STR) v)
((= (type v) 'INT) (itoa v))
((= (type v) 'REAL) (rtos v 2 6))
(T "")
)
)
;; --- Einen Eingabe-Eintrag in Journal-Eintraege uebersetzen ---
;; Journal-Format (vfl-entry->string in Lisp/vf_linienzug.lsp):
;; ("PT" x y z) / ("REAL" . r) / ("DL" . r) / ("INT" . i) / ("STR" . s) /
;; ("STEP" . label)
;; Ein "point_rel" wird zu EINEM oder ZWEI Eintraegen: immer die Laenge (DL),
;; zusaetzlich die Fahrtrichtung (REAL) nur dort, wo sie im Journal steht
;; (allererstes Segment der Kette, siehe Kopfkommentar).
;; Rueckgabe: Liste von Journal-Eintraegen (kann leer sein).
(defun hmrec: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" (hmrec:als-string (ssg-val e "wert")))))
((= typ "step")
(list (cons "STEP" (hmrec:als-string (ssg-val e "wert")))))
(T nil)
)
)
;; --- Alle Eingaben einer Kette in ein Vorwaerts-Journal uebersetzen ---
;; Vorwaerts = Bau-Reihenfolge, genau so erwartet es vfl-journal-replay-start.
(defun hmrec:journal-bauen (eingaben / out)
(setq out '())
(foreach e eingaben
(foreach j (hmrec:eintrag->journal e)
(setq out (cons j out))))
(reverse out)
)
;; --- Nicht verbrauchte Werte in der Replay-Queue zaehlen ---
;; STEP-Marker zaehlen nicht mit: vfl-replay-pop ueberspringt sie, sie sind
;; also kein offener Wert. Bleibt hier etwas uebrig, ist der Bau-Ablauf vom
;; aufgezeichneten abgewichen (Desync) - das MUSS auffallen.
(defun hmrec:queue-rest ( / n)
(setq n 0)
(if (and (boundp '*vfl-replay-queue*) *vfl-replay-queue*)
(foreach e *vfl-replay-queue*
(if (/= (car e) "STEP") (setq n (1+ n)))))
n
)
;; ============================================================
;; Eine Kette bauen
;; ============================================================
;; Spielt das Journal einer Kette ab. Ablauf wie vfl-konvertiere-ent
;; (Lisp/vf_linienzug.lsp): Queue setzen, vf-linienzug-modus laufen lassen,
;; danach den entstandenen VF_n-Block einsammeln.
;;
;; vl-catch-all-apply ist hier BEWUSST gesetzt (anders als im interaktiven
;; Editier-Pfad vfl-edit-ent): bricht eine Kette ab, muss die Testschleife
;; ueber die restlichen Ketten weiterlaufen. Der Preis ist derselbe wie beim
;; Batch-Konverter - der *error*-Handler in vf-linienzug-modus (der die
;; Teil-Geometrie zu einem Block wickelt) kommt dann nicht zum Zug und die
;; Teilgeometrie bleibt lose in der Zeichnung liegen. Der Testfall wird darum
;; klar als "failed" samt Fehlertext gemeldet.
;; *error* wird um den Aufruf herum gesichert: nach einem abgefangenen Abbruch
;; steht dort noch der Handler-Lambda von vf-linienzug-modus (es kommt nicht
;; mehr zu dessen (setq *error* old-error)) - ohne Restore wuerde die naechste
;; Kette auf einem Handler mit toten dynamischen Bindungen aufsetzen.
(defun hmrec:build-linienzug (kopf eingaben / tid journal anz offen err ent
alt-error extra hl-fehler meldungen vor-ent)
(setq tid (ssg-val kopf "test_id"))
(setq journal (hmrec:journal-bauen eingaben))
(setq anz (length journal))
(princ (strcat "\n [LINIENZUG] " tid " -> " (itoa (length eingaben))
" Eingaben, " (itoa anz) " Journal-Eintraege"))
(if (ssg-val kopf "beschreibung")
(princ (strcat "\n " (ssg-val kopf "beschreibung"))))
(if (and (boundp '*hmrec-dbg*) *hmrec-dbg*)
(progn
(dbgmsg (strcat "KETTE " tid " JOURNAL " (vfl-journal->json journal)))
(dbgflush)))
(cond
;; Ohne Journal (oder ohne Startpunkt an erster Stelle) nichts bauen: ein
;; Replay ohne PT wuerde den Startpunkt interaktiv erfragen und die ganze
;; Kette an der falschen Stelle aufbauen.
((null journal)
(princ (strcat "\n FEHLER: " tid " - keine Eingaben lesbar."
" Stehen die Zahlen-Arrays in EINER Zeile?"
" (ssg-cfg-parse-array kann keine umgebrochenen lesen)"))
(hmrec:result-json tid "linienzug" "failed" nil nil nil nil
(list (cons "eingaben_gesamt" (itoa anz))
(cons "eingaben_offen" (itoa anz)))))
((/= (car (car journal)) "PT")
(princ (strcat "\n FEHLER: " tid " - erster Eintrag ist kein"
" Startpunkt (point_abs), nichts gebaut."))
(hmrec:result-json tid "linienzug" "failed" nil nil nil nil
(list (cons "eingaben_gesamt" (itoa anz))
(cons "eingaben_offen" (itoa anz)))))
(T
;; Stand der Zeichnung VOR dem Bau: der fertige VF_-Block wird danach
;; ab hier gesucht (siehe hmrec:last-insert-prefix-ab).
(setq vor-ent (entlast))
(setq alt-error *error*)
(vfl-journal-reset)
(vfl-journal-replay-start journal)
(setq err (vl-catch-all-apply 'vf-linienzug-modus '()))
(setq offen (hmrec:queue-rest))
;; Diagnose VOR dem Reset sichern: vfl-journal-reset loescht
;; *vfl-headless-fehler* und *vfl-meldungen* mit (beide gehoeren zum
;; einzelnen Lauf, nicht zur Sitzung).
(setq hl-fehler (hmrec:headless-fehler-text))
(setq meldungen (if (boundp '*vfl-meldungen*) *vfl-meldungen* nil))
(setq *error* alt-error)
(vfl-journal-reset)
(setq extra (list (cons "eingaben_gesamt" (itoa anz))
(cons "eingaben_offen" (itoa offen))
(cons "prompts" (itoa (hmrec:prompts)))
(cons "headless_fehler" (hmrec:json-str hl-fehler))
(cons "meldungen" (hmrec:json-liste meldungen))))
(cond
;; Der Headless-Riegel der Produktion hat gegriffen: die Journal-Daten
;; passen nicht zum tatsaechlichen Bau-Ablauf. Praeziser als "failed",
;; weil hl-fehler Glied UND Eingabe-Nummer nennt.
(hl-fehler
(princ (strcat "\n DESYNC: " tid " - " hl-fehler))
(hmrec:ent-json-status tid "linienzug" "desync"
(hmrec:last-insert-prefix-ab "VF_" vor-ent) extra))
((vl-catch-all-error-p err)
(princ (strcat "\n FEHLER: " tid " abgebrochen -> "
(vl-catch-all-error-message err)))
(hmrec:result-json tid "linienzug" "failed" nil nil nil nil extra))
;; Kette gebaut, aber der Ablauf hat nicht alle aufgezeichneten Werte
;; abgerufen: die Geometrie kann nicht der Vorlage entsprechen.
((> offen 0)
(princ (strcat "\n FEHLER: " tid " - " (itoa offen)
" Journal-Werte NICHT verbraucht (Ablauf weicht"
" von der Aufzeichnung ab)."))
(hmrec:ent-json-status tid "linienzug" "desync"
(hmrec:last-insert-prefix-ab "VF_" vor-ent) extra))
(T
(setq ent (hmrec:last-insert-prefix-ab "VF_" vor-ent))
(if ent
(princ (strcat " -> Block " (cdr (assoc 2 (entget ent)))))
(princ "\n FEHLER: kein VF_-Block gefunden"))
(hmrec:ent-json tid "linienzug" ent extra)))))
)
;; ============================================================
;; Headless-Absicherung: Dialoge aus, leere Queue bricht ab
;; ============================================================
;; Die Journale sind vollstaendig, es darf also keine Live-Eingabe geben.
;; Falls doch (Desync), wuerde ein getpoint/getstring den Testlauf blockieren
;; und ein alert-Fenster ihn haengen lassen. Darum werden die Eingabe-
;; Funktionen fuer die Dauer des Tests durch Abbruch-Stubs ersetzt (liefern
;; nil = Abbruch, was vf-linienzug-modus sauber beendet) und alert auf die
;; Konsole umgeleitet. Alles mit Save/Restore, siehe hmrec:stubs-aus.
;; Die Stubs haben feste 1- bzw. 2-Arity (AutoLISP kennt keine optionalen
;; Parameter) - genau wie die Mocks in test_linienzug.lsp. Ruft der Bau-Ablauf
;; eine der Funktionen mit anderer Argumentzahl auf, gibt es einen
;; Argumentfehler statt einer Warnung; beides landet im vl-catch-all-apply um
;; vf-linienzug-modus und meldet die Kette als "failed". Wichtig ist nur, dass
;; kein Prompt und kein Dialog den Lauf blockiert.
(defun hmrec:stub-warnung (was)
(setq *hmrec-prompts* (1+ (hmrec:prompts)))
(princ (strcat "\n WARNUNG: unerwartete Live-Eingabe (" was
") - Replay-Queue leer, Kette wird abgebrochen."))
nil
)
(defun hmrec:stub-getpoint (a b) (hmrec:stub-warnung "getpoint"))
(defun hmrec:stub-getstring (a) (hmrec:stub-warnung "getstring"))
(defun hmrec:stub-getint (a) (hmrec:stub-warnung "getint"))
(defun hmrec:stub-getreal (a) (hmrec:stub-warnung "getreal"))
(defun hmrec:stub-alert (msg)
(princ (strcat "\n ALERT (unterdrueckt): " (if msg msg "")))
(princ)
)
(defun hmrec:stubs-an ( / )
(setq *hmrec-alt-getpoint* vfl-getpoint
*hmrec-alt-getstring* getstring
*hmrec-alt-getint* getint
*hmrec-alt-getreal* getreal
*hmrec-alt-alert* alert)
(setq vfl-getpoint hmrec:stub-getpoint
getstring hmrec:stub-getstring
getint hmrec:stub-getint
getreal hmrec:stub-getreal
alert hmrec:stub-alert)
;; Wizard-Dialoge aus (waehrend eines Replays greifen sie ohnehin nicht,
;; siehe vfl-wizard-aktiv - aber nach einem Desync wuerde ein Dialog
;; aufgehen und warten).
(setq *hmrec-alt-wizard* (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*))
(setq *vfl-wizard-mode* nil)
(if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus))
;; Headless-Riegel der Produktion einschalten: eine erschoepfte Replay-Queue
;; ist dann ein harter, lokalisierter Abbruch statt eines stillen Rueckfalls
;; auf Live-Eingabe (siehe vfl-headless-abbruch). Die Stubs oben bleiben als
;; zweites Netz - sie fangen die Prompts ausserhalb der vfl-in-*-Wrapper
;; (Modus 3, Nebenpfade) und zaehlen sie.
(setq *hmrec-alt-headless* (if (boundp '*vfl-headless*) *vfl-headless*))
(setq *vfl-headless* T)
(setq *hmrec-prompts* 0)
(princ)
)
(defun hmrec:stubs-aus ( / )
(setq vfl-getpoint *hmrec-alt-getpoint*
getstring *hmrec-alt-getstring*
getint *hmrec-alt-getint*
getreal *hmrec-alt-getreal*
alert *hmrec-alt-alert*)
(setq *vfl-wizard-mode* *hmrec-alt-wizard*)
(setq *vfl-headless* *hmrec-alt-headless*)
(if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an))
(princ)
)
;; ============================================================
;; Alle Ketten bauen
;; ============================================================
;; Eigene Funktion, damit der Aufrufer sie in vl-catch-all-apply klammern und
;; die Eingabe-Stubs GARANTIERT zuruecknehmen kann (ein hier entkommender
;; Fehler wuerde sonst getstring/getpoint/alert der laufenden BricsCAD-Sitzung
;; ersetzt zuruecklassen).
;; Rueckgabe: Ergebnis-Liste in Bau-Reihenfolge.
(defun hmrec:ketten-bauen (ketten / kette res out tid fehlertext)
(setq out '())
(foreach kette ketten
;; Dimensions-Override je Kette neu setzen: der Abbruch-Handler in
;; vf-linienzug-modus setzt *ssg-ils-dim* zurueck, ein Abbruch wuerde die
;; folgenden Ketten sonst in der falschen Dimension bauen.
(if (and (boundp '*hmrec-dim-override*) *hmrec-dim-override*)
(setq *ssg-ils-dim* *hmrec-dim-override*))
;; Je Kette gefangen: ein Fehler NACH dem Bau (z.B. beim Auswerten des
;; Ergebnisses) darf nicht die restlichen Ketten mitnehmen - sonst steht
;; am Ende "0 OK, 0 Fehler" und man sieht nicht einmal, welche Kette
;; gebaut wurde. Der Bau selbst ist in hmrec:build-linienzug bereits
;; separat gefangen; dieser Riegel hier deckt alles danach ab.
(setq tid (ssg-val (car kette) "test_id"))
(setq res (vl-catch-all-apply 'hmrec:build-linienzug
(list (car kette) (cadr kette))))
(if (vl-catch-all-error-p res)
(progn
(setq fehlertext (vl-catch-all-error-message res))
(princ (strcat "\n FEHLER: " tid " - Ausnahme: " fehlertext))
(setq res (hmrec:result-json tid "linienzug" "failed"
nil nil nil nil
(list (cons "fehler_text"
(hmrec:json-str fehlertext)))))))
(if res (setq out (cons res out))))
(reverse out)
)
;; ============================================================
;; JSON-Export der Ergebnisse
;; ============================================================
(defun hm_recformat:export-results (tests-out-dir / out-json f first)
(if (null *hmrec-test-results*)
(princ "\n Keine Ergebnisse vorhanden.")
(progn
(vl-mkdir tests-out-dir)
(setq out-json (strcat tests-out-dir "/hm_recformat_results.json"))
(setq f (open out-json "w"))
(if f
(progn
(write-line "[" f)
(setq first T)
(foreach r *hmrec-test-results*
(if (not first) (write-line "," f))
(write-line r f)
(setq first nil))
(write-line "]" f)
(close f)
(princ (strcat "\n Ergebnisse: " out-json)))
(princ (strcat "\n FEHLER: Kann " out-json " nicht schreiben!")))))
)
;; ============================================================
;; C:TEST_HM_RECFORMAT - Hauptbefehl
;; ============================================================
(defun c:TEST_HM_RECFORMAT ( / json-datei daten ketten results-list res
anz-ok anz-fehler)
;; Debug-Datei nur bei eingeschaltetem Schalter: (dbg-schalter-on "hm_recformat")
(setq *hmrec-dbg*
(and (car (atoms-family 1 '("DBG-SCHALTER-OPEN")))
(dbg-schalter-open "hm_recformat" "hm_recformat.dbg" "DXFM_LOG")))
;; Benoetigte Feature-Module laden (Linienzug braucht Vario + Gefaelle)
(ssg-ensure "VarioFoerderer")
(ssg-ensure "Gefaellestrecke")
(if (null (car (atoms-family 1 '("VF-LINIENZUG-MODUS"))))
(progn
(princ "\n[TEST_HM_RECFORMAT] FEHLER: vf-linienzug-modus nicht geladen"
" (VarioFoerderer/Gefaellestrecke laden).")
(if *hmrec-dbg* (dbgclose))
(exit)))
(ssg-start "TEST_HM_RECFORMAT" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; Block-Bibliothek (Bogen-/AS-/ES-Masse) initialisieren
(if (or (not *lib-initialized*) (null bogen-auf))
(init-bibliothek))
;; Bau-Dimension festhalten. Die Eingabedaten sind dimensionsfrei; welche
;; Blockvariante (_2D/_3D) verwendet wird, loest ssg-ils-dim-aktuell auf
;; (Override *ssg-ils-dim* -> Umgebungsvariable DXFM_DIM -> Default "3D").
(setq *hmrec-dimension* (ssg-ils-dim-aktuell))
(princ (strcat "\n Bau-Dimension: " *hmrec-dimension*
" (umschalten: TEST_HM_RECFORMAT_2D / TEST_HM_RECFORMAT_3D)"))
;; Testdaten laden
(setq json-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hm_recformat.json"))
(if (not (findfile json-datei))
(progn
(princ (strcat "\n[TEST_HM_RECFORMAT] FEHLER: " json-datei " nicht gefunden!"))
(ssg-end)
(if *hmrec-dbg* (dbgclose))
(exit)))
(setq daten (ssg-load-json json-datei))
(if (null daten)
(progn
(princ "\n[TEST_HM_RECFORMAT] FEHLER: Testdaten konnten nicht geladen werden.")
(ssg-end)
(if *hmrec-dbg* (dbgclose))
(exit)))
(setq ketten (hmrec:gruppiere daten))
(if (null ketten)
(progn
(princ "\n[TEST_HM_RECFORMAT] FEHLER: keine Kette gefunden - erwartet werden"
" Kopf-Objekte mit \"test_id\" und Eingaben mit \"typ\".")
(ssg-end)
(if *hmrec-dbg* (dbgclose))
(exit)))
(princ "\n\n================================================================")
(princ "\n TEST_HM_RECFORMAT - 5 Linienzuege der Anlage HundM (polylines.dxf)")
(princ (strcat "\n " (itoa (length daten)) " JSON-Objekte, "
(itoa (length ketten)) " Ketten"))
(princ "\n================================================================")
(setq anz-ok 0 anz-fehler 0)
(hmrec:stubs-an)
;; Stubs MUESSEN auch nach einem unerwarteten Fehler zurueckgenommen werden.
(setq results-list (vl-catch-all-apply 'hmrec:ketten-bauen (list ketten)))
(hmrec:stubs-aus)
(if (vl-catch-all-error-p results-list)
(progn
(princ (strcat "\n[TEST_HM_RECFORMAT] FEHLER in der Kettenschleife: "
(vl-catch-all-error-message results-list)))
(setq results-list '())))
(foreach res results-list
(if (vl-string-search "\"status\": \"executed\"" res)
(setq anz-ok (1+ anz-ok))
(setq anz-fehler (1+ anz-fehler))))
(princ "\n================================================================")
(princ (strcat "\n Ergebnis: " (itoa anz-ok) " OK, "
(itoa anz-fehler) " Fehler"))
(princ "\n================================================================")
(setq *hmrec-test-results* results-list)
(ssg-end)
(princ "\n TEST_HM_RECFORMAT abgeschlossen.")
(if *hmrec-dbg* (dbgclose))
(princ)
)
;; ============================================================
;; Dieselbe Anlage in einer bestimmten Dimension bauen
;; ============================================================
;; Die Eingabedaten (hm_recformat.json) sind dimensionsfrei - sie beschreiben die
;; Eingaben, mit denen die Ketten gebaut wurden. Ob daraus flache 2D-Symbole
;; oder 3D-Modelle werden, entscheidet allein die Blockvariante (_2D/_3D), die
;; ssg-ils-dim-aktuell aufloest. Unsere Bibliothek hat beide, also laesst sich
;; dieselbe Anlage ohne Datenaenderung in 2D ODER 3D aufbauen.
;; *hmrec-dim-override* wird in der Kettenschleife je Kette neu gesetzt
;; (siehe dort).
(defun hmrec:mit-dimension (dim / alt ergebnis)
(setq alt (if (boundp '*ssg-ils-dim*) *ssg-ils-dim*))
(setq *ssg-ils-dim* dim)
(setq *hmrec-dim-override* dim)
(princ (strcat "\n[TEST_HM_RECFORMAT] Dimension voruebergehend auf " dim
" gesetzt."))
(setq ergebnis (c:TEST_HM_RECFORMAT))
;; Override wieder auf den vorherigen Stand - sonst baut ein folgender Test
;; unbemerkt in der hier gewaehlten Dimension weiter.
(setq *ssg-ils-dim* alt)
(setq *hmrec-dim-override* nil)
(princ (strcat "\n[TEST_HM_RECFORMAT] Dimension zurueckgesetzt auf "
(if alt alt "Umgebung/Default") "."))
ergebnis
)
(defun c:TEST_HM_RECFORMAT_2D ( / ) (hmrec:mit-dimension "2D"))
(defun c:TEST_HM_RECFORMAT_3D ( / ) (hmrec:mit-dimension "3D"))
+380
View File
@@ -0,0 +1,380 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_hm_recformat.py - Validiert die 5 VarioFoerderer-Linienzuege der Anlage HundM.
Zwei Ebenen:
1. Die Testdaten selbst (tests/testdata/hm_recformat.json): Struktur der
Eingabe-Journale und Lesbarkeit durch den LISP-Parser.
2. Die von TEST_HM_RECFORMAT (test_hm_recformat.lsp) erzeugten Ergebnisse
(output/hmrec_results.json + output/hm_recformat_tests.dxf): jede Kette
gebaut, kein Journal-Wert offen, Attribute passen zu den Eingaben.
Die Testdaten stammen aus lib/vf_journal_export.py (Journale der VF_n-Bloecke
aus data/polylines.dxf, XDATA-App SSG_VF_EDIT) und nicht aus Handarbeit -
Schema siehe tests/testdata/object_data.md Abschnitt 8.
"""
import json
import os
import pytest
EINGABE_TYPEN = ("point_abs", "point_rel", "real", "int", "string", "step")
# ============================================================
# Hilfsfunktionen: flaches JSON in Ketten gruppieren
# ============================================================
# Gleiche Regel wie hmrec:gruppiere in test_hm_recformat.lsp: ein Objekt mit
# "test_id" ist ein Kopf und beginnt eine Kette, die folgenden Objekte mit
# "typ" sind ihre Eingaben.
def _ketten(testdata):
ketten = []
for obj in testdata:
if "test_id" in obj:
ketten.append((obj, []))
elif "typ" in obj and ketten:
ketten[-1][1].append(obj)
return ketten
def _journal_laenge(eingaben):
"""Anzahl der Journal-Eintraege, die der Treiber daraus baut: ein
point_rel mit "hz" wird zu ZWEI Eintraegen (DL + REAL)."""
return sum(2 if (e["typ"] == "point_rel" and "hz" in e) else 1
for e in eingaben)
def _pfad(*teile):
return os.path.join(os.path.dirname(os.path.abspath(__file__)), *teile)
# ============================================================
# Testdaten-Pruefung (hm_recformat.json selbst)
# ============================================================
class TestAufzeichnungTestdaten:
def test_fuenf_ketten(self, hmrec_testdata):
"""Die Anlage hat fuenf am Kreisel haengende Linienzuege."""
ketten = _ketten(hmrec_testdata)
assert len(ketten) == 5, \
f"{len(ketten)} Ketten in hm_recformat.json, erwartet 5"
def test_jedes_objekt_zuordenbar(self, hmrec_testdata):
"""Kein Objekt darf zwischen Kopf und Eingabe fallen - sonst
verschluckt hmrec:gruppiere es stillschweigend."""
for i, obj in enumerate(hmrec_testdata):
assert ("test_id" in obj) or ("typ" in obj), \
f"Objekt {i} ist weder Kopf noch Eingabe: {sorted(obj)}"
def test_kopf_felder(self, hmrec_testdata):
for kopf, _ in _ketten(hmrec_testdata):
assert kopf["test_id"].startswith("VF_"), kopf["test_id"]
# "typ" wuerde den Kopf zu einer Eingabe machen (siehe
# hmrec:gruppiere) - der Anlagetyp heisst darum "anlagetyp".
assert "typ" not in kopf, \
f'{kopf["test_id"]}: Kopf darf kein Feld "typ" tragen'
assert kopf["anlagetyp"] == "linienzug", kopf["test_id"]
assert kopf["modus"] == 1, kopf["test_id"]
assert kopf["block"].startswith("VF_"), kopf["block"]
assert len(kopf["startpunkt_mm"]) == 3, kopf["test_id"]
def test_anzahl_eingaben_stimmt(self, hmrec_testdata):
"""anzahl_eingaben ist die Selbstauskunft des Kopfes - weicht sie ab,
fehlen Eingaben (z.B. durch eine Handaenderung)."""
for kopf, eingaben in _ketten(hmrec_testdata):
assert kopf["anzahl_eingaben"] == len(eingaben), \
f'{kopf["test_id"]}: anzahl_eingaben=' \
f'{kopf["anzahl_eingaben"]}, gezaehlt {len(eingaben)}'
def test_eingabe_typen_bekannt(self, hmrec_testdata):
for kopf, eingaben in _ketten(hmrec_testdata):
for i, e in enumerate(eingaben):
assert e["typ"] in EINGABE_TYPEN, \
f'{kopf["test_id"]} Eingabe {i}: unbekannter Typ ' \
f'{e["typ"]}'
assert e.get("kommentar"), \
f'{kopf["test_id"]} Eingabe {i}: kein Kommentar'
def test_kette_beginnt_mit_startpunkt(self, hmrec_testdata):
"""Erste Eingabe = absoluter Startpunkt, zweite = Starthoehe. Fehlt
der Punkt, wuerde das Replay ihn interaktiv erfragen und die Kette an
der falschen Stelle aufbauen (siehe hmrec:build-linienzug)."""
for kopf, eingaben in _ketten(hmrec_testdata):
assert eingaben[0]["typ"] == "point_abs", kopf["test_id"]
assert eingaben[1]["typ"] == "real", kopf["test_id"]
punkt = eingaben[0]["wert"]
assert len(punkt) == 3, kopf["test_id"]
assert punkt == kopf["startpunkt_mm"], \
f'{kopf["test_id"]}: point_abs {punkt} != startpunkt_mm ' \
f'{kopf["startpunkt_mm"]}'
assert eingaben[1]["wert"] == punkt[2], \
f'{kopf["test_id"]}: Starthoehe {eingaben[1]["wert"]} != ' \
f'Z des Startpunkts {punkt[2]}'
def test_hz_nur_beim_ersten_segment(self, hmrec_testdata):
"""vfl-in-abstand journalisiert die Fahrtrichtung NUR beim ersten
Segment der Kette; jedes weitere erbt sie vom Vorgaenger. Ein "hz" an
spaeterer Stelle waere ein Wert zu viel in der Replay-Queue und
verschiebt alle folgenden Antworten (Desync)."""
for kopf, eingaben in _ketten(hmrec_testdata):
rel = [e for e in eingaben if e["typ"] == "point_rel"]
assert rel, f'{kopf["test_id"]}: kein Segment'
assert "hz" in rel[0], \
f'{kopf["test_id"]}: erstes Segment ohne hz'
for i, e in enumerate(rel[1:], start=2):
assert "hz" not in e, \
f'{kopf["test_id"]}: Segment {i} traegt hz - nur das ' \
f'erste Segment darf eines haben'
def test_segmentlaengen_positiv(self, hmrec_testdata):
for kopf, eingaben in _ketten(hmrec_testdata):
for e in eingaben:
if e["typ"] == "point_rel":
assert e["dL"] > 0, f'{kopf["test_id"]}: dL={e["dL"]}'
def test_menue_antworten_sind_strings(self, hmrec_testdata):
"""Menue-Antworten muessen Strings bleiben ("1"/"2"/"90"/"30"): der
Produktionscode vergleicht mit (= antwort "1"), eine Zahl wuerde
stumm in den Default-Zweig laufen."""
for kopf, eingaben in _ketten(hmrec_testdata):
for i, e in enumerate(eingaben):
if e["typ"] in ("string", "step"):
assert isinstance(e["wert"], str), \
f'{kopf["test_id"]} Eingabe {i}: {e["wert"]!r} ist ' \
f'kein String'
def test_glieder_vorhanden(self, hmrec_testdata):
"""Jede Kette hat Glied-Marker (STEP) - sie sind der Anker fuer den
Sektions-/Glied-Edit und die Kurzbeschreibung."""
for kopf, eingaben in _ketten(hmrec_testdata):
glieder = [e["wert"] for e in eingaben if e["typ"] == "step"]
assert glieder, f'{kopf["test_id"]}: kein STEP-Marker'
for g in glieder:
assert g in ("Linie-GF", "Linie-VF", "Linie", "GF-Bogen",
"Horizontal-VF", "Vario-Kurve", "ES"), \
f'{kopf["test_id"]}: unbekanntes Glied {g}'
def test_arrays_einzeilig(self):
"""Jedes Array muss komplett in EINER Zeile stehen.
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei zeilenweise,
und ssg-cfg-parse-array braucht das ganze Array in einem String. Ein
von json.dump(indent=2) umgebrochenes Array wird zu nil - der Treiber
baut die Kette dann nicht an ihrer Position. Das ist genau der
Fehler, der die Anlage einmal komplett am Ursprung aufgebaut hat, und
er ist in der Zeichnung nicht als Datenfehler zu erkennen.
"""
with open(_pfad("testdata", "hm_recformat.json"), encoding="utf-8") as fh:
for nr, zeile in enumerate(fh, start=1):
offen = zeile.count("[") - zeile.count("]")
# Die aeusserste Klammer des Dokuments darf allein stehen.
if zeile.strip() in ("[", "]"):
continue
assert offen == 0, \
f"Zeile {nr}: Array ueber mehrere Zeilen umgebrochen - " \
f"ssg-cfg-parse-array kann das nicht lesen: {zeile.strip()!r}"
def test_flach_fuer_lisp_parser(self, hmrec_testdata):
"""Die Datei muss FLACH bleiben.
ssg-load-json / ssg-parse-json-array behandelt jedes "{" als
Objektanfang und jedes "}"/"}," als Objektende - es kennt keine
Verschachtelung. Ein Objekt in einem Objekt (oder ein Array von
Objekten wie "eingaben": [...]) wuerde das umgebende Objekt
verschlucken. Dieser Test zaehlt die Objekte so, wie der LISP-Parser
sie zaehlen wuerde, und vergleicht mit dem echten JSON.
"""
objekte = 0
with open(_pfad("testdata", "hm_recformat.json"), encoding="utf-8") as fh:
for zeile in fh:
gestrippt = zeile.strip()
if gestrippt == "{":
objekte += 1
elif gestrippt in ("}", "},"):
continue
elif gestrippt.startswith('"'):
# Ein Wert, der ein Objekt oder eine Objektliste eroeffnet,
# macht die Datei fuer den LISP-Parser unlesbar.
assert not gestrippt.endswith(("{", "[")), \
f"verschachtelter Wert: {gestrippt!r}"
assert objekte == len(hmrec_testdata), \
f"LISP-Parser saehe {objekte} Objekte, json.load sieht " \
f"{len(hmrec_testdata)}"
# ============================================================
# Ergebnis-Pruefung (results.json vs. testdata)
# ============================================================
class TestAufzeichnungResults:
def test_je_kette_ein_ergebnis(self, hmrec_testdata, hmrec_results):
soll = [k["test_id"] for k, _ in _ketten(hmrec_testdata)]
ist = [r["test_id"] for r in hmrec_results]
assert ist == soll, f"Ergebnisse {ist}, erwartet {soll}"
for r in hmrec_results:
assert r["kind"] == "linienzug", f'{r["test_id"]}: {r["kind"]}'
def test_status_executed(self, hmrec_results):
""""desync" (Journal-Werte nicht verbraucht) und "failed" (Abbruch)
sind beide echte Fehler - siehe hmrec:build-linienzug."""
for r in hmrec_results:
assert r["status"] == "executed", \
f'{r["test_id"]}: Status={r["status"]}'
def test_journal_vollstaendig_verbraucht(self, hmrec_testdata,
hmrec_results):
"""Alle aufgezeichneten Werte muessen im Bau abgerufen worden sein.
Bleibt etwas offen, ist der Ablauf von der Aufzeichnung abgewichen und
die Geometrie kann der Vorlage nicht entsprechen."""
soll = {k["test_id"]: _journal_laenge(e)
for k, e in _ketten(hmrec_testdata)}
for r in hmrec_results:
assert "eingaben_offen" in r, (
f'{r["test_id"]}: kein eingaben_offen im Ergebnis - die Kette '
f'ist vor der Auswertung abgebrochen (status='
f'{r.get("status")!r}, {r.get("fehler_text")})')
assert r["eingaben_offen"] == 0, \
f'{r["test_id"]}: {r["eingaben_offen"]} Journal-Werte offen'
assert r["eingaben_gesamt"] == soll[r["test_id"]], \
f'{r["test_id"]}: {r["eingaben_gesamt"]} Journal-Eintraege ' \
f'abgespielt, erwartet {soll[r["test_id"]]}'
def test_keine_live_eingabe(self, hmrec_results):
"""Kein Prompt, kein Dialog: der Treiber schaltet *vfl-headless* ein
(harter Abbruch statt stillem Rueckfall auf Live-Eingabe, siehe
vfl-headless-abbruch in Lisp/vf_linienzug.lsp) UND ersetzt zusaetzlich
getpoint/getstring/getint/getreal durch zaehlende Stubs. Beide Wege
muessen leer bleiben - "prompts" ist der positive Beweis, dass keine
Eingabe ausserhalb der vfl-in-*-Wrapper erreicht wurde."""
for r in hmrec_results:
if "prompts" in r:
assert r["prompts"] == 0, \
f'{r["test_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
assert not r.get("headless_fehler"), \
f'{r["test_id"]}: Headless-Abbruch bei {r["headless_fehler"]}'
def test_keine_geometrie_meldungen(self, hmrec_results):
"""*vfl-meldungen* sammelt die abgewiesenen Sektionen (frueher
blockierende alert-Fenster, jetzt vfl-meldung). Eine Meldung heisst:
ein Glied liess sich geometrisch nicht bauen - der Nachbau weicht dann
von der Vorlage ab, auch wenn das Journal aufgeht."""
for r in hmrec_results:
assert not r.get("meldungen"), \
f'{r["test_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
def test_vf_block_erzeugt(self, hmrec_results):
for r in hmrec_results:
assert r["block_name"].startswith("VF_"), \
f'{r["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
assert r["block_handle"], f'{r["test_id"]}: kein Handle'
def test_dimension_einheitlich(self, hmrec_results):
"""Alle Ergebnisse eines Laufs stammen aus EINER Bau-Dimension.
Die Eingabedaten sind dimensionsfrei; ob _2D- oder _3D-Blockvarianten
verwendet werden, loest ssg-ils-dim-aktuell auf. Ein Lauf, in dem beide
vorkommen, waere ein halb umgeschalteter Zustand (z.B. weil ein
vorheriger Test *ssg-ils-dim* nicht zurueckgesetzt hat).
"""
dims = {r.get("dimension") for r in hmrec_results}
assert len(dims) == 1, f"gemischte Bau-Dimensionen im Lauf: {dims}"
dim = dims.pop()
assert dim in ("2D", "3D"), \
f'Bau-Dimension "{dim}" ist weder 2D noch 3D'
def test_startpunkt_getroffen(self, hmrec_testdata, hmrec_results):
"""Der VF_n-Block wird mit dem Kettenstartpunkt als Basispunkt
gewickelt (ssg-block-wrap-welt in vfl-block-erstellen) - sein
Einfuegepunkt muss also der Startpunkt aus den Eingaben sein."""
soll = {k["test_id"]: k["startpunkt_mm"]
for k, _ in _ketten(hmrec_testdata)}
for r in hmrec_results:
p = soll[r["test_id"]]
ist = r["insert_point"]
for achse, i in (("X", 0), ("Y", 1), ("Z", 2)):
assert abs(ist[i] - p[i]) < 1.0, \
f'{r["test_id"]}: Einfuegepunkt {achse} {ist[i]} != ' \
f'{p[i]}'
def test_as_es_attribute_aus_eingaben(self, hmrec_testdata,
hmrec_results):
"""WINKEL_AS/SEITE_AS und WINKEL_ES/SEITE_ES muessen die Antworten aus
dem Journal wiedergeben (AS steht in der Praeambel, ES im ES-Glied)."""
seiten = {"1": "links", "2": "rechts"}
nach_id = {r["test_id"]: r for r in hmrec_results}
for kopf, eingaben in _ketten(hmrec_testdata):
r = nach_id[kopf["test_id"]]
attrs = r["actual_attributes"]
# Praeambel: [0] Punkt, [1] Hoehe, [2] AS setzen, [3] Winkel,
# [4] Seite
if eingaben[2]["wert"] == "1":
assert attrs.get("WINKEL_AS") == eingaben[3]["wert"], \
f'{kopf["test_id"]}: WINKEL_AS=' \
f'{attrs.get("WINKEL_AS")}, erwartet {eingaben[3]["wert"]}'
assert attrs.get("SEITE_AS") == seiten[eingaben[4]["wert"]], \
f'{kopf["test_id"]}: SEITE_AS={attrs.get("SEITE_AS")}'
# ES: die beiden Strings direkt nach dem STEP-Marker "ES"
for i, e in enumerate(eingaben):
if e["typ"] == "step" and e["wert"] == "ES":
assert attrs.get("WINKEL_ES") == eingaben[i + 1]["wert"], \
f'{kopf["test_id"]}: WINKEL_ES=' \
f'{attrs.get("WINKEL_ES")}'
assert attrs.get("SEITE_ES") == \
seiten[eingaben[i + 2]["wert"]], \
f'{kopf["test_id"]}: SEITE_ES={attrs.get("SEITE_ES")}'
break
def test_vario_kurven_gezaehlt(self, hmrec_testdata, hmrec_results):
"""Jede Vario-Kurve im Journal muss in den VF_Bogen_*-Attributen
auftauchen (vfl-insert-vario-kurve-block zaehlt sie mit)."""
nach_id = {r["test_id"]: r for r in hmrec_results}
for kopf, eingaben in _ketten(hmrec_testdata):
soll = sum(1 for e in eingaben
if e["typ"] == "step" and e["wert"] == "Vario-Kurve")
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
ist = sum(int(attrs.get(f"VF_Bogen_{lage}_{w}", 0) or 0)
for lage in ("A", "I") for w in (90, 60, 30))
assert ist == soll, \
f'{kopf["test_id"]}: {ist} Vario-Kurven in den Attributen, ' \
f'{soll} im Journal'
def test_hoehe_von_ist_starthoehe(self, hmrec_testdata,
hmrec_results):
nach_id = {r["test_id"]: r for r in hmrec_results}
for kopf, eingaben in _ketten(hmrec_testdata):
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
if "HOEHE_VON_mm" not in attrs:
continue
assert abs(int(attrs["HOEHE_VON_mm"]) - eingaben[1]["wert"]) <= 1, \
f'{kopf["test_id"]}: HOEHE_VON_mm={attrs["HOEHE_VON_mm"]}, ' \
f'Starthoehe {eingaben[1]["wert"]}'
# ============================================================
# DXF Geometrie-Pruefung (ezdxf)
# ============================================================
class TestAufzeichnungGeometry:
def test_block_anzahl(self, hmrec_dxf, hmrec_testdata):
"""Je Kette muss ein VF_-Block im Modelspace stehen."""
n_vf = sum(1 for e in hmrec_dxf.modelspace()
if e.dxftype() == "INSERT" and e.dxf.name.startswith("VF_"))
soll = len(_ketten(hmrec_testdata))
assert n_vf >= soll, f"VF_: {n_vf} < {soll}"
def test_handles_im_modelspace(self, hmrec_dxf, hmrec_results):
"""Jedes gebaute Block-Handle (executed) liegt im Modelspace."""
handles = {e.dxf.handle for e in hmrec_dxf.modelspace()
if e.dxftype() == "INSERT"}
for r in hmrec_results:
if r["status"] == "executed" and r["block_handle"]:
assert r["block_handle"] in handles, \
f'{r["test_id"]}: Handle {r["block_handle"]} nicht im ' \
f'Modelspace'
+79 -636
View File
@@ -1,41 +1,32 @@
;; ============================================================
;; test_hundm05.lsp - Integrationstest: baut die 5 VarioFoerderer-Linienzuege
;; der Anlage HundM (Kreisel mit fuenf daran haengenden Ketten) nach.
;; der Anlage HundM (Kreisel mit fuenf daran haengenden Ketten) aus ihrer
;; SPEC nach.
;;
;; Datenquelle: tests/testdata/hundm05.json, erzeugt von
;; python lib/vf_journal_export.py data/polylines.dxf \
;; tests/testdata/hundm05.json --csv results/HundM_export.csv
;; Das sind die EINGABE-JOURNALE der echten Ketten - ausgelesen aus der XDATA
;; (App SSG_VF_EDIT, Marker "linienzug") der VF_n-Bloecke in data/polylines.dxf,
;; also genau die Werte, mit denen die Ketten in BricsCAD gebaut wurden.
;; Datenquelle: tests/testdata/hundm05.json - die Ketten im Spec-Format
;; (Domaenenwerte statt Menue-Codes, siehe Lisp/vf_spec.lsp und
;; doc/variofoerderer.md Abschnitt 8b). Erzeugt aus den echten
;; Aufzeichnungen mit
;; python lib/vf_spec_export.py tests/testdata/hm_recformat.json \
;; tests/testdata/hundm05.json
;; und damit nachweislich dasselbe Journal wie die Vorlage
;; (Rundlauf-Beweis in tests/test_vf_spec.py). Die Datei ist die Stelle, an
;; der weitere Ketten der Anlage nachgetragen werden.
;;
;; JSON-Aufbau (FLACH, weil ssg-load-json zeilenweise liest - Details in
;; tests/testdata/object_data.md Abschnitt 8):
;; Kopf-Objekt (traegt "test_id"): test_id, anlagetyp, modus,
;; anzahl_eingaben, block, dxf_handle, startpunkt_mm,
;; rotation_grad, beschreibung, sivas_* , erwartung_hinweis
;; Eingabe-Objekt (traegt "typ"): point_abs | point_rel | real | int |
;; string | step - in exakter Frage-Reihenfolge
;; Ein Kopf-Objekt beginnt eine Kette, die folgenden Eingabe-Objekte gehoeren
;; dazu (hundm05:gruppiere).
;; Dieser Test hat KEINE eigene Bau-Logik: er ruft vsp-bau-datei aus
;; Lisp/vf_spec.lsp - denselben Weg, den auch der Befehl VF_SPEC_BAU nimmt.
;; Es gibt also genau einen Code-Pfad, der aus einer Spec Geometrie erzeugt.
;;
;; WARUM REPLAY UND KEINE EINGABE-MOCKS (Unterschied zu test_linienzug.lsp):
;; test_linienzug.lsp ersetzt getpoint/getstring/getint/getreal durch Mocks und
;; rechnet Punkte aus "dL"+"hz" selbst aus. Das geht nur, weil
;; linienzug_tests.json zu JEDEM Segment ein "hz" mitfuehrt. Ein echtes Journal
;; tut das nicht: vfl-in-abstand journalisiert die Fahrtrichtung NUR beim
;; allerersten Segment der Kette, jedes weitere erbt sie vom Vorgaenger. Ein
;; Mock kann diese geerbte Richtung nicht kennen - er wuerde einen Punkt in der
;; falschen Richtung liefern und die Projektion in vfl-neue-linie-messen ergaebe
;; eine falsche (bis auf 0 zusammenfallende) Laenge. Darum wird hier derselbe
;; Weg genommen, den die Produktion beim Editieren/Konvertieren nutzt:
;; vfl-journal-replay-start + vf-linienzug-modus (siehe vfl-konvertiere-ent in
;; Lisp/vf_linienzug.lsp). Die vfl-in-*-Wrapper ziehen ihre Werte dann direkt
;; aus der Replay-Queue - inklusive der Sonderregel fuer "hz".
;; ABGRENZUNG zu TEST_HM_RECFORMAT (tests/test_hm_recformat.lsp): dort werden
;; dieselben Ketten aus dem ROHEN Journal der Kundenzeichnung gebaut. Das ist
;; der Regressionstest fuer das Aufzeichnungsformat (bleibt ein bestehender
;; VF_n-Block abspielbar - Doppelklick-Edit, 2D/3D-Konvertierung?) und
;; deshalb dort auf "disabled": true, um nicht zweimal dieselbe Geometrie zu
;; bauen.
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (VarioFoerderer inkl. vf_linienzug, Gefaellestrecke,
;; ssg_core, ssg_dbg)
;; Voraussetzung:
;; - SSG_LIB geladen (VarioFoerderer inkl. vf_linienzug + vf_spec,
;; Gefaellestrecke, ssg_core, ssg_dbg)
;; - Umgebungsvariable DXFMAKRO gesetzt
;;
;; Speichert (via SSG_RUN_ALL_TESTS bei "save":"dxf"):
@@ -47,621 +38,73 @@
;; TEST_HUNDM05 ; in der aktuellen Dimension (ssg-ils-dim-aktuell)
;; TEST_HUNDM05_2D ; erzwungen 2D
;; TEST_HUNDM05_3D ; erzwungen 3D
;;
;; Debug-Datei (nur wenn eingeschaltet): (dbg-schalter-on "hundm05")
;; schreibt hundm05.dbg ins DXFM_LOG-Verzeichnis (Journal je Kette als JSON).
;; ============================================================
(if (not (boundp '*hundm05-records*)) (setq *hundm05-records* nil))
(defun hundm05:spec-datei ()
(strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json"))
;; ============================================================
;; Ergebnis-JSON
;; Testlauf
;; ============================================================
;; --- JSON-Hilfen fuer Diagnosefelder (Strings/String-Listen) ---
;; Die extra-Felder in hundm05:result-json werden ROH in das JSON gesetzt,
;; ein Text muss also selbst seine Anfuehrungszeichen mitbringen und die
;; JSON-Sonderzeichen maskieren.
(defun hundm05:json-escape (s / out i c)
(setq out "" i 1)
(while (<= i (strlen s))
(setq c (substr s i 1))
(setq out (cond ((= c "\"") (strcat out "\\\""))
((= c "\\") (strcat out "\\\\"))
((= c "\n") (strcat out " "))
(T (strcat out c))))
(setq i (1+ i)))
out)
(defun hundm05:json-str (s)
(if s (strcat "\"" (hundm05:json-escape s) "\"") "null"))
(defun hundm05:json-liste (lst / s first)
(setq s "[" first T)
(foreach x lst
(if (not first) (setq s (strcat s ", ")))
(setq s (strcat s (hundm05:json-str x)) first nil))
(strcat s "]"))
;; --- Headless-Diagnose der Produktion lesen ---
;; vfl-headless-abbruch (Lisp/vf_linienzug.lsp) legt bei erschoepfter
;; Replay-Queue Art der Eingabe, Glied-Nummer und Eingabe-Nummer ab. Das ist
;; die eigentliche Desync-Auskunft - ohne sie weiss man nur, DASS Werte offen
;; blieben, nicht WO der Ablauf abgewichen ist.
(defun hundm05:headless-fehler-text ( / f)
(if (and (boundp '*vfl-headless-fehler*) *vfl-headless-fehler*)
(progn
(setq f *vfl-headless-fehler*)
(strcat (cdr (assoc "was" f)) " @ " (cdr (assoc "ort" f))))
nil))
;; Zaehler der trotz allem erreichten Live-Eingaben (muss 0 bleiben).
(defun hundm05:prompts ()
(if (and (boundp '*hundm05-prompts*) *hundm05-prompts*) *hundm05-prompts* 0))
;; "dimension" haelt fest, in welcher Dimension (2D/3D) gebaut wurde. Das ist
;; KEINE Eigenschaft der Eingabedaten: hundm05.json beschreibt die Ketten
;; dimensionsfrei. Womit unsere Makros bauen, entscheidet ssg-ils-dim-aktuell -
;; unsere Bibliothek hat von jedem Block eine _2D- und eine _3D-Variante.
;; extra: Alist zusaetzlicher Felder ((name . wert-string)), wird als
;; JSON-Zahlen/Strings so uebernommen, wie sie uebergeben werden.
(defun hundm05:result-json (test-id kind status block-name block-handle
insert-point attribs extra /
json tag val first)
(setq json (strcat " {\n"
" \"test_id\": \"" test-id "\",\n"
" \"kind\": \"" kind "\",\n"
" \"status\": \"" status "\",\n"
" \"dimension\": \""
(if (and (boundp '*hundm05-dimension*) *hundm05-dimension*)
*hundm05-dimension* "?") "\",\n"
" \"block_name\": \"" (if block-name block-name "") "\",\n"
" \"block_handle\": \"" (if block-handle block-handle "") "\",\n"
" \"insert_point\": ["
(if insert-point
(strcat (rtos (car insert-point) 2 1) ", "
(rtos (cadr insert-point) 2 1) ", "
(rtos (if (caddr insert-point) (caddr insert-point) 0.0) 2 1))
"0.0, 0.0, 0.0")
"],\n"))
(foreach f extra
(setq json (strcat json " \"" (car f) "\": " (cdr f) ",\n")))
(setq json (strcat json " \"actual_attributes\": {"))
(setq first T)
(foreach att attribs
(setq tag (car att) val (cdr att))
(if (not first) (setq json (strcat json ",")))
(setq json (strcat json "\n \"" tag "\": \"" (if val val "") "\""))
(setq first nil))
(setq json (strcat json "\n }\n }"))
json
)
;; --- Aus einer Block-ENAME ein Ergebnis-JSON erzeugen ---
;; status: "executed" bei sauberem Lauf; ein abweichender Status (z.B. bei
;; einem Queue-Desync) wird durchgereicht, die Block-Angaben bleiben dabei
;; erhalten - sonst waere der betroffene Block im Ergebnis nicht auffindbar.
(defun hundm05:ent-json-status (test-id kind status ent extra / ed)
(if ent
(progn
(setq ed (entget ent))
(hundm05:result-json test-id kind status
(cdr (assoc 2 ed)) (cdr (assoc 5 ed)) (cdr (assoc 10 ed))
(ssg-attrib-read ent) extra))
(hundm05:result-json test-id kind "failed" nil nil nil nil extra))
)
(defun hundm05:ent-json (test-id kind ent extra)
(hundm05:ent-json-status test-id kind "executed" ent extra)
)
;; --- Letzten INSERT mit Blocknamen-Praefix suchen ---
;; AutoLISP kennt KEIN entprev (nur entnext und entlast) - rueckwaerts durch
;; die Zeichnung laufen geht also nicht. Gesucht wird darum VORWAERTS, wobei
;; der letzte Treffer gewinnt. Ein blosses (entlast) genuegt nicht: nach dem
;; Bau koennen weitere Objekte entstehen (z.B. Beschriftungstexte), entlast
;; ist dann nicht der gesuchte Block - genau daran scheiterte der Testlauf.
;; ab-ent = Stand der Zeichnung VOR dem Bau. Damit prueft die Suche nur die
;; neu entstandenen Objekte: schnell, und sie kann nicht versehentlich den
;; Block eines frueheren Baus liefern (siehe Kommentar bei -ab).
(defun hundm05:insert-prefix-p (ent pref / ed bn)
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
(and ed (= (cdr (assoc 0 ed)) "INSERT") bn
(>= (strlen bn) (strlen pref))
(= (substr bn 1 (strlen pref)) pref)))
;; Vorwaertslauf ab ab-ent (nil = ganze Zeichnung); der LETZTE Treffer
;; gewinnt. Der entget-Test faengt ein inzwischen ungueltiges ab-ent ab
;; (entget liefert dann nil, entnext wuerde einen Fehler werfen).
(defun hundm05:suche-vorwaerts (pref ab-ent / ent found)
(setq ent (if (and ab-ent (entget ab-ent)) (entnext ab-ent) (entnext)))
(while ent
(if (hundm05:insert-prefix-p ent pref) (setq found ent))
(setq ent (entnext ent)))
found)
(defun hundm05:last-insert-prefix-ab (pref ab-ent / ent)
(if ab-ent
;; MIT Marker nur die neu entstandenen Objekte pruefen - und BEWUSST
;; ohne entlast-Kurzschluss: hat dieser Bau keinen Block erzeugt, waere
;; entlast der Block eines FRUEHEREN Baus und das Ergebnis wuerde der
;; falschen Kette zugeordnet. Kein Treffer = kein Block, das ist die
;; richtige Antwort.
(hundm05:suche-vorwaerts pref ab-ent)
(progn
;; Ohne Marker: erst entlast (der haeufige Fall, das gerade
;; eingefuegte Objekt), sonst die ganze Zeichnung.
(setq ent (entlast))
(if (and ent (hundm05:insert-prefix-p ent pref))
ent
(hundm05:suche-vorwaerts pref nil)))))
(defun hundm05:last-insert-prefix (pref)
(hundm05:last-insert-prefix-ab pref nil))
;; ============================================================
;; JSON-Eintraege -> Ketten
;; ============================================================
;; Das flache JSON-Array in Ketten gruppieren: ein Objekt mit "test_id" ist
;; ein Kopf und beginnt eine neue Kette, die folgenden Objekte mit "typ" sind
;; ihre Eingaben (Reihenfolge = Frage-Reihenfolge, bleibt erhalten).
;; Rueckgabe: Liste von (kopf eingaben-liste).
(defun hundm05:gruppiere (daten / ketten kopf eingaben)
(setq ketten '() kopf nil eingaben '())
(foreach obj daten
(cond
((ssg-val obj "test_id")
(if kopf
(setq ketten (cons (list kopf (reverse eingaben)) ketten)))
(setq kopf obj eingaben '()))
((ssg-val obj "typ")
(if kopf (setq eingaben (cons obj eingaben))))
)
)
(if kopf (setq ketten (cons (list kopf (reverse eingaben)) ketten)))
(reverse ketten)
)
;; --- Wert als String lesen (auch wenn er als Zahl im JSON steht) ---
;; Die Menue-Antworten sind Strings ("1"/"2"/"90"/"30"). Steht in einer von
;; Hand gepflegten Datei versehentlich 1 statt "1", wuerde ein Zahlenwert in
;; die Replay-Queue wandern und der Vergleich (= antwort "1") schlagen fehl -
;; darum hier immer nach String wandeln.
(defun hundm05:als-string (v)
(cond
((null v) "")
((= (type v) 'STR) v)
((= (type v) 'INT) (itoa v))
((= (type v) 'REAL) (rtos v 2 6))
(T "")
)
)
;; --- Einen Eingabe-Eintrag in Journal-Eintraege uebersetzen ---
;; Journal-Format (vfl-entry->string in Lisp/vf_linienzug.lsp):
;; ("PT" x y z) / ("REAL" . r) / ("DL" . r) / ("INT" . i) / ("STR" . s) /
;; ("STEP" . label)
;; Ein "point_rel" wird zu EINEM oder ZWEI Eintraegen: immer die Laenge (DL),
;; zusaetzlich die Fahrtrichtung (REAL) nur dort, wo sie im Journal steht
;; (allererstes Segment der Kette, siehe Kopfkommentar).
;; Rueckgabe: Liste von Journal-Eintraegen (kann leer sein).
(defun hundm05: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" (hundm05:als-string (ssg-val e "wert")))))
((= typ "step")
(list (cons "STEP" (hundm05:als-string (ssg-val e "wert")))))
(T nil)
)
)
;; --- Alle Eingaben einer Kette in ein Vorwaerts-Journal uebersetzen ---
;; Vorwaerts = Bau-Reihenfolge, genau so erwartet es vfl-journal-replay-start.
(defun hundm05:journal-bauen (eingaben / out)
(setq out '())
(foreach e eingaben
(foreach j (hundm05:eintrag->journal e)
(setq out (cons j out))))
(reverse out)
)
;; --- Nicht verbrauchte Werte in der Replay-Queue zaehlen ---
;; STEP-Marker zaehlen nicht mit: vfl-replay-pop ueberspringt sie, sie sind
;; also kein offener Wert. Bleibt hier etwas uebrig, ist der Bau-Ablauf vom
;; aufgezeichneten abgewichen (Desync) - das MUSS auffallen.
(defun hundm05:queue-rest ( / n)
(setq n 0)
(if (and (boundp '*vfl-replay-queue*) *vfl-replay-queue*)
(foreach e *vfl-replay-queue*
(if (/= (car e) "STEP") (setq n (1+ n)))))
n
)
;; ============================================================
;; Eine Kette bauen
;; ============================================================
;; Spielt das Journal einer Kette ab. Ablauf wie vfl-konvertiere-ent
;; (Lisp/vf_linienzug.lsp): Queue setzen, vf-linienzug-modus laufen lassen,
;; danach den entstandenen VF_n-Block einsammeln.
;;
;; vl-catch-all-apply ist hier BEWUSST gesetzt (anders als im interaktiven
;; Editier-Pfad vfl-edit-ent): bricht eine Kette ab, muss die Testschleife
;; ueber die restlichen Ketten weiterlaufen. Der Preis ist derselbe wie beim
;; Batch-Konverter - der *error*-Handler in vf-linienzug-modus (der die
;; Teil-Geometrie zu einem Block wickelt) kommt dann nicht zum Zug und die
;; Teilgeometrie bleibt lose in der Zeichnung liegen. Der Testfall wird darum
;; klar als "failed" samt Fehlertext gemeldet.
;; *error* wird um den Aufruf herum gesichert: nach einem abgefangenen Abbruch
;; steht dort noch der Handler-Lambda von vf-linienzug-modus (es kommt nicht
;; mehr zu dessen (setq *error* old-error)) - ohne Restore wuerde die naechste
;; Kette auf einem Handler mit toten dynamischen Bindungen aufsetzen.
(defun hundm05:build-linienzug (kopf eingaben / tid journal anz offen err ent
alt-error extra hl-fehler meldungen vor-ent)
(setq tid (ssg-val kopf "test_id"))
(setq journal (hundm05:journal-bauen eingaben))
(setq anz (length journal))
(princ (strcat "\n [LINIENZUG] " tid " -> " (itoa (length eingaben))
" Eingaben, " (itoa anz) " Journal-Eintraege"))
(if (ssg-val kopf "beschreibung")
(princ (strcat "\n " (ssg-val kopf "beschreibung"))))
(if (and (boundp '*hundm05-dbg*) *hundm05-dbg*)
(progn
(dbgmsg (strcat "KETTE " tid " JOURNAL " (vfl-journal->json journal)))
(dbgflush)))
(cond
;; Ohne Journal (oder ohne Startpunkt an erster Stelle) nichts bauen: ein
;; Replay ohne PT wuerde den Startpunkt interaktiv erfragen und die ganze
;; Kette an der falschen Stelle aufbauen.
((null journal)
(princ (strcat "\n FEHLER: " tid " - keine Eingaben lesbar."
" Stehen die Zahlen-Arrays in EINER Zeile?"
" (ssg-cfg-parse-array kann keine umgebrochenen lesen)"))
(hundm05:result-json tid "linienzug" "failed" nil nil nil nil
(list (cons "eingaben_gesamt" (itoa anz))
(cons "eingaben_offen" (itoa anz)))))
((/= (car (car journal)) "PT")
(princ (strcat "\n FEHLER: " tid " - erster Eintrag ist kein"
" Startpunkt (point_abs), nichts gebaut."))
(hundm05:result-json tid "linienzug" "failed" nil nil nil nil
(list (cons "eingaben_gesamt" (itoa anz))
(cons "eingaben_offen" (itoa anz)))))
(T
;; Stand der Zeichnung VOR dem Bau: der fertige VF_-Block wird danach
;; ab hier gesucht (siehe hundm05:last-insert-prefix-ab).
(setq vor-ent (entlast))
(setq alt-error *error*)
(vfl-journal-reset)
(vfl-journal-replay-start journal)
(setq err (vl-catch-all-apply 'vf-linienzug-modus '()))
(setq offen (hundm05:queue-rest))
;; Diagnose VOR dem Reset sichern: vfl-journal-reset loescht
;; *vfl-headless-fehler* und *vfl-meldungen* mit (beide gehoeren zum
;; einzelnen Lauf, nicht zur Sitzung).
(setq hl-fehler (hundm05:headless-fehler-text))
(setq meldungen (if (boundp '*vfl-meldungen*) *vfl-meldungen* nil))
(setq *error* alt-error)
(vfl-journal-reset)
(setq extra (list (cons "eingaben_gesamt" (itoa anz))
(cons "eingaben_offen" (itoa offen))
(cons "prompts" (itoa (hundm05:prompts)))
(cons "headless_fehler" (hundm05:json-str hl-fehler))
(cons "meldungen" (hundm05:json-liste meldungen))))
(cond
;; Der Headless-Riegel der Produktion hat gegriffen: die Journal-Daten
;; passen nicht zum tatsaechlichen Bau-Ablauf. Praeziser als "failed",
;; weil hl-fehler Glied UND Eingabe-Nummer nennt.
(hl-fehler
(princ (strcat "\n DESYNC: " tid " - " hl-fehler))
(hundm05:ent-json-status tid "linienzug" "desync"
(hundm05:last-insert-prefix-ab "VF_" vor-ent) extra))
((vl-catch-all-error-p err)
(princ (strcat "\n FEHLER: " tid " abgebrochen -> "
(vl-catch-all-error-message err)))
(hundm05:result-json tid "linienzug" "failed" nil nil nil nil extra))
;; Kette gebaut, aber der Ablauf hat nicht alle aufgezeichneten Werte
;; abgerufen: die Geometrie kann nicht der Vorlage entsprechen.
((> offen 0)
(princ (strcat "\n FEHLER: " tid " - " (itoa offen)
" Journal-Werte NICHT verbraucht (Ablauf weicht"
" von der Aufzeichnung ab)."))
(hundm05:ent-json-status tid "linienzug" "desync"
(hundm05:last-insert-prefix-ab "VF_" vor-ent) extra))
(T
(setq ent (hundm05:last-insert-prefix-ab "VF_" vor-ent))
(if ent
(princ (strcat " -> Block " (cdr (assoc 2 (entget ent)))))
(princ "\n FEHLER: kein VF_-Block gefunden"))
(hundm05:ent-json tid "linienzug" ent extra)))))
)
;; ============================================================
;; Headless-Absicherung: Dialoge aus, leere Queue bricht ab
;; ============================================================
;; Die Journale sind vollstaendig, es darf also keine Live-Eingabe geben.
;; Falls doch (Desync), wuerde ein getpoint/getstring den Testlauf blockieren
;; und ein alert-Fenster ihn haengen lassen. Darum werden die Eingabe-
;; Funktionen fuer die Dauer des Tests durch Abbruch-Stubs ersetzt (liefern
;; nil = Abbruch, was vf-linienzug-modus sauber beendet) und alert auf die
;; Konsole umgeleitet. Alles mit Save/Restore, siehe hundm05:stubs-aus.
;; Die Stubs haben feste 1- bzw. 2-Arity (AutoLISP kennt keine optionalen
;; Parameter) - genau wie die Mocks in test_linienzug.lsp. Ruft der Bau-Ablauf
;; eine der Funktionen mit anderer Argumentzahl auf, gibt es einen
;; Argumentfehler statt einer Warnung; beides landet im vl-catch-all-apply um
;; vf-linienzug-modus und meldet die Kette als "failed". Wichtig ist nur, dass
;; kein Prompt und kein Dialog den Lauf blockiert.
(defun hundm05:stub-warnung (was)
(setq *hundm05-prompts* (1+ (hundm05:prompts)))
(princ (strcat "\n WARNUNG: unerwartete Live-Eingabe (" was
") - Replay-Queue leer, Kette wird abgebrochen."))
nil
)
(defun hundm05:stub-getpoint (a b) (hundm05:stub-warnung "getpoint"))
(defun hundm05:stub-getstring (a) (hundm05:stub-warnung "getstring"))
(defun hundm05:stub-getint (a) (hundm05:stub-warnung "getint"))
(defun hundm05:stub-getreal (a) (hundm05:stub-warnung "getreal"))
(defun hundm05:stub-alert (msg)
(princ (strcat "\n ALERT (unterdrueckt): " (if msg msg "")))
(princ)
)
(defun hundm05:stubs-an ( / )
(setq *hundm05-alt-getpoint* vfl-getpoint
*hundm05-alt-getstring* getstring
*hundm05-alt-getint* getint
*hundm05-alt-getreal* getreal
*hundm05-alt-alert* alert)
(setq vfl-getpoint hundm05:stub-getpoint
getstring hundm05:stub-getstring
getint hundm05:stub-getint
getreal hundm05:stub-getreal
alert hundm05:stub-alert)
;; Wizard-Dialoge aus (waehrend eines Replays greifen sie ohnehin nicht,
;; siehe vfl-wizard-aktiv - aber nach einem Desync wuerde ein Dialog
;; aufgehen und warten).
(setq *hundm05-alt-wizard* (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*))
(setq *vfl-wizard-mode* nil)
(if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus))
;; Headless-Riegel der Produktion einschalten: eine erschoepfte Replay-Queue
;; ist dann ein harter, lokalisierter Abbruch statt eines stillen Rueckfalls
;; auf Live-Eingabe (siehe vfl-headless-abbruch). Die Stubs oben bleiben als
;; zweites Netz - sie fangen die Prompts ausserhalb der vfl-in-*-Wrapper
;; (Modus 3, Nebenpfade) und zaehlen sie.
(setq *hundm05-alt-headless* (if (boundp '*vfl-headless*) *vfl-headless*))
(setq *vfl-headless* T)
(setq *hundm05-prompts* 0)
(princ)
)
(defun hundm05:stubs-aus ( / )
(setq vfl-getpoint *hundm05-alt-getpoint*
getstring *hundm05-alt-getstring*
getint *hundm05-alt-getint*
getreal *hundm05-alt-getreal*
alert *hundm05-alt-alert*)
(setq *vfl-wizard-mode* *hundm05-alt-wizard*)
(setq *vfl-headless* *hundm05-alt-headless*)
(if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an))
(princ)
)
;; ============================================================
;; Alle Ketten bauen
;; ============================================================
;; Eigene Funktion, damit der Aufrufer sie in vl-catch-all-apply klammern und
;; die Eingabe-Stubs GARANTIERT zuruecknehmen kann (ein hier entkommender
;; Fehler wuerde sonst getstring/getpoint/alert der laufenden BricsCAD-Sitzung
;; ersetzt zuruecklassen).
;; Rueckgabe: Ergebnis-Liste in Bau-Reihenfolge.
(defun hundm05:ketten-bauen (ketten / kette res out tid fehlertext)
(setq out '())
(foreach kette ketten
;; Dimensions-Override je Kette neu setzen: der Abbruch-Handler in
;; vf-linienzug-modus setzt *ssg-ils-dim* zurueck, ein Abbruch wuerde die
;; folgenden Ketten sonst in der falschen Dimension bauen.
(if (and (boundp '*hundm05-dim-override*) *hundm05-dim-override*)
(setq *ssg-ils-dim* *hundm05-dim-override*))
;; Je Kette gefangen: ein Fehler NACH dem Bau (z.B. beim Auswerten des
;; Ergebnisses) darf nicht die restlichen Ketten mitnehmen - sonst steht
;; am Ende "0 OK, 0 Fehler" und man sieht nicht einmal, welche Kette
;; gebaut wurde. Der Bau selbst ist in hundm05:build-linienzug bereits
;; separat gefangen; dieser Riegel hier deckt alles danach ab.
(setq tid (ssg-val (car kette) "test_id"))
(setq res (vl-catch-all-apply 'hundm05:build-linienzug
(list (car kette) (cadr kette))))
(if (vl-catch-all-error-p res)
(progn
(setq fehlertext (vl-catch-all-error-message res))
(princ (strcat "\n FEHLER: " tid " - Ausnahme: " fehlertext))
(setq res (hundm05:result-json tid "linienzug" "failed"
nil nil nil nil
(list (cons "fehler_text"
(hundm05:json-str fehlertext)))))))
(if res (setq out (cons res out))))
(reverse out)
)
;; ============================================================
;; JSON-Export der Ergebnisse
;; ============================================================
(defun hundm05:export-results (tests-out-dir / out-json f first)
(if (null *hundm05-test-results*)
(princ "\n Keine HundM05-Ergebnisse vorhanden.")
(progn
(vl-mkdir tests-out-dir)
(setq out-json (strcat tests-out-dir "/hundm05_results.json"))
(setq f (open out-json "w"))
(if f
(progn
(write-line "[" f)
(setq first T)
(foreach r *hundm05-test-results*
(if (not first) (write-line "," f))
(write-line r f)
(setq first nil))
(write-line "]" f)
(close f)
(princ (strcat "\n Ergebnisse: " out-json)))
(princ (strcat "\n FEHLER: Kann " out-json " nicht schreiben!")))))
)
;; ============================================================
;; C:TEST_HUNDM05 - Hauptbefehl
;; ============================================================
(defun c:TEST_HUNDM05 ( / json-datei daten ketten results-list res
anz-ok anz-fehler)
;; Debug-Datei nur bei eingeschaltetem Schalter: (dbg-schalter-on "hundm05")
(setq *hundm05-dbg*
(and (car (atoms-family 1 '("DBG-SCHALTER-OPEN")))
(dbg-schalter-open "hundm05" "hundm05.dbg" "DXFM_LOG")))
;; Benoetigte Feature-Module laden (Linienzug braucht Vario + Gefaelle)
(ssg-ensure "VarioFoerderer")
(ssg-ensure "Gefaellestrecke")
(if (null (car (atoms-family 1 '("VF-LINIENZUG-MODUS"))))
(progn
(princ "\n[TEST_HUNDM05] FEHLER: vf-linienzug-modus nicht geladen"
" (VarioFoerderer/Gefaellestrecke laden).")
(if *hundm05-dbg* (dbgclose))
(exit)))
(ssg-start "TEST_HUNDM05" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; Block-Bibliothek (Bogen-/AS-/ES-Masse) initialisieren
(if (or (not *lib-initialized*) (null bogen-auf))
(init-bibliothek))
;; Bau-Dimension festhalten. Die Eingabedaten sind dimensionsfrei; welche
;; Blockvariante (_2D/_3D) verwendet wird, loest ssg-ils-dim-aktuell auf
;; (Override *ssg-ils-dim* -> Umgebungsvariable DXFM_DIM -> Default "3D").
(setq *hundm05-dimension* (ssg-ils-dim-aktuell))
(princ (strcat "\n Bau-Dimension: " *hundm05-dimension*
" (umschalten: TEST_HUNDM05_2D / TEST_HUNDM05_3D)"))
;; Testdaten laden
(setq json-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json"))
(if (not (findfile json-datei))
(progn
(princ (strcat "\n[TEST_HUNDM05] FEHLER: " json-datei " nicht gefunden!"))
(ssg-end)
(if *hundm05-dbg* (dbgclose))
(exit)))
(setq daten (ssg-load-json json-datei))
(if (null daten)
(progn
(princ "\n[TEST_HUNDM05] FEHLER: Testdaten konnten nicht geladen werden.")
(ssg-end)
(if *hundm05-dbg* (dbgclose))
(exit)))
(setq ketten (hundm05:gruppiere daten))
(if (null ketten)
(progn
(princ "\n[TEST_HUNDM05] FEHLER: keine Kette gefunden - erwartet werden"
" Kopf-Objekte mit \"test_id\" und Eingaben mit \"typ\".")
(ssg-end)
(if *hundm05-dbg* (dbgclose))
(exit)))
(defun c:TEST_HUNDM05 ( / datei)
(setq datei (hundm05:spec-datei))
(princ "\n\n================================================================")
(princ "\n TEST_HUNDM05 - 5 Linienzuege der Anlage HundM (polylines.dxf)")
(princ (strcat "\n " (itoa (length daten)) " JSON-Objekte, "
(itoa (length ketten)) " Ketten"))
(princ "\n TEST_HUNDM05 - 5 Linienzuege der Anlage HundM (aus der Spec)")
(princ (strcat "\n " datei))
(princ "\n================================================================")
(setq anz-ok 0 anz-fehler 0)
(hundm05:stubs-an)
;; Stubs MUESSEN auch nach einem unerwarteten Fehler zurueckgenommen werden.
(setq results-list (vl-catch-all-apply 'hundm05:ketten-bauen (list ketten)))
(hundm05:stubs-aus)
(if (vl-catch-all-error-p results-list)
(progn
(princ (strcat "\n[TEST_HUNDM05] FEHLER in der Kettenschleife: "
(vl-catch-all-error-message results-list)))
(setq results-list '())))
(foreach res results-list
(if (vl-string-search "\"status\": \"executed\"" res)
(setq anz-ok (1+ anz-ok))
(setq anz-fehler (1+ anz-fehler))))
(princ "\n================================================================")
(princ (strcat "\n Ergebnis: " (itoa anz-ok) " OK, "
(itoa anz-fehler) " Fehler"))
(princ "\n================================================================")
(setq *hundm05-test-results* results-list)
(ssg-end)
(princ "\n TEST_HUNDM05 abgeschlossen.")
(if *hundm05-dbg* (dbgclose))
(princ)
)
(cond
((null (car (atoms-family 1 '("VSP-BAU-DATEI"))))
(princ "\n[TEST_HUNDM05] FEHLER: vf_spec.lsp nicht geladen"
" (VarioFoerderer laden).")
(setq *hundm05-records* nil))
((null (findfile datei))
(princ (strcat "\n[TEST_HUNDM05] FEHLER: " datei " nicht gefunden!"))
(setq *hundm05-records* nil))
(T
(setq *hundm05-records* (vsp-bau-datei datei))))
(princ))
;; ============================================================
;; Dieselbe Anlage in einer bestimmten Dimension bauen
;; ============================================================
;; Die Eingabedaten (hundm05.json) sind dimensionsfrei - sie beschreiben die
;; Eingaben, mit denen die Ketten gebaut wurden. Ob daraus flache 2D-Symbole
;; oder 3D-Modelle werden, entscheidet allein die Blockvariante (_2D/_3D), die
;; ssg-ils-dim-aktuell aufloest. Unsere Bibliothek hat beide, also laesst sich
;; dieselbe Anlage ohne Datenaenderung in 2D ODER 3D aufbauen.
;; *hundm05-dim-override* wird in der Kettenschleife je Kette neu gesetzt
;; (siehe dort).
(defun hundm05:mit-dimension (dim / alt ergebnis)
(setq alt (if (boundp '*ssg-ils-dim*) *ssg-ils-dim*))
(setq *ssg-ils-dim* dim)
(setq *hundm05-dim-override* dim)
(princ (strcat "\n[TEST_HUNDM05] Dimension voruebergehend auf " dim
" gesetzt."))
(setq ergebnis (c:TEST_HUNDM05))
;; Override wieder auf den vorherigen Stand - sonst baut ein folgender Test
;; unbemerkt in der hier gewaehlten Dimension weiter.
(setq *ssg-ils-dim* alt)
(setq *hundm05-dim-override* nil)
(princ (strcat "\n[TEST_HUNDM05] Dimension zurueckgesetzt auf "
(if alt alt "Umgebung/Default") "."))
ergebnis
)
;; Die Spec ist dimensionsfrei - sie beschreibt die Eingaben, mit denen die
;; Ketten gebaut wurden. Ob daraus flache 2D-Symbole oder 3D-Modelle werden,
;; entscheidet allein die Blockvariante (_2D/_3D), die ssg-ils-dim-aktuell
;; aufloest. Unsere Bibliothek hat beide, also laesst sich dieselbe Anlage
;; ohne Datenaenderung in 2D ODER 3D aufbauen.
;; *vsp-dim-override* wird von vsp-bau-aus-spec JE KETTE angewandt (der
;; Abbruch-Handler in vf-linienzug-modus setzt *ssg-ils-dim* zurueck).
(defun c:TEST_HUNDM05_2D ( / ) (hundm05:mit-dimension "2D"))
(defun c:TEST_HUNDM05_3D ( / ) (hundm05:mit-dimension "3D"))
(defun hundm05:mit-dimension (dim / alt)
(setq alt (if (boundp '*vsp-dim-override*) *vsp-dim-override*))
(setq *vsp-dim-override* dim)
(vl-catch-all-apply 'c:TEST_HUNDM05 '())
(setq *vsp-dim-override* alt)
(princ))
(defun c:TEST_HUNDM05_2D () (hundm05:mit-dimension "2D"))
(defun c:TEST_HUNDM05_3D () (hundm05:mit-dimension "3D"))
;; ============================================================
;; Ergebnis-Export (Konvention <name>:export-results fuer test_run_all.lsp)
;; ============================================================
;; Geschrieben wird hier und nicht in c:TEST_HUNDM05, damit der Testrunner
;; das Zielverzeichnis bestimmt (er uebergibt tests/output).
(defun hundm05:export-results (out-dir / pfad)
(if (null *hundm05-records*)
(princ "\n Keine HundM05-Ergebnisse vorhanden.")
(progn
(vl-mkdir out-dir)
(setq pfad (strcat out-dir "/hundm05_results.json"))
(vsp-results-schreiben *hundm05-records* pfad)))
(princ))
(princ "\nTEST_HUNDM05 geladen. Aufruf: TEST_HUNDM05 (bzw. _2D / _3D)")
(princ)
+100 -348
View File
@@ -1,18 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_hundm05.py - Validiert die 5 VarioFoerderer-Linienzuege der Anlage HundM.
test_hundm05.py - Validiert die aus der SPEC gebaute Anlage HundM.
Zwei Ebenen:
1. Die Testdaten selbst (tests/testdata/hundm05.json): Struktur der
Eingabe-Journale und Lesbarkeit durch den LISP-Parser.
2. Die von TEST_HUNDM05 (test_hundm05.lsp) erzeugten Ergebnisse
(output/hundm05_results.json + output/hundm05_tests.dxf): jede Kette
gebaut, kein Journal-Wert offen, Attribute passen zu den Eingaben.
Eingabe: tests/testdata/hundm05.json (die 5 Ketten im Spec-Format).
Gebaut von TEST_HUNDM05 bzw. VF_SPEC_BAU in BricsCAD, Ergebnis in
tests/output/hundm05_results.json.
Die Testdaten stammen aus lib/vf_journal_export.py (Journale der VF_n-Bloecke
aus data/polylines.dxf, XDATA-App SSG_VF_EDIT) und nicht aus Handarbeit -
Schema siehe tests/testdata/object_data.md Abschnitt 8.
Diese Tests brauchen also einen CAD-Lauf und werden sonst uebersprungen.
Den UEBERSETZER (Spec -> Journal) prueft tests/test_vf_spec.py ohne CAD;
das Aufzeichnungsformat prueft tests/test_hm_recformat.py.
Der Ergebnis-Record je Kette kommt aus Lisp/vf_spec.lsp (vsp-result-json).
"""
import json
@@ -20,361 +19,114 @@ import os
import pytest
HIER = os.path.dirname(__file__)
SPEC_JSON = os.path.join(HIER, "testdata", "hundm05.json")
EINGABE_TYPEN = ("point_abs", "point_rel", "real", "int", "string", "step")
# TEST_HUNDM05 schreibt nach tests/output. Ein Lauf von VF_SPEC_BAU mit
# gesetztem DXFM_VF_SPEC_OUT kann die Datei woanders ablegen - darum beide
# ueblichen Orte pruefen, statt den Test stumm zu ueberspringen.
ERGEBNIS_ORTE = [os.path.join(HIER, "output", "hundm05_results.json"),
os.path.join(HIER, "..", "results", "hundm05_results.json")]
# ============================================================
# Hilfsfunktionen: flaches JSON in Ketten gruppieren
# ============================================================
# Gleiche Regel wie hundm05:gruppiere in test_hundm05.lsp: ein Objekt mit
# "test_id" ist ein Kopf und beginnt eine Kette, die folgenden Objekte mit
# "typ" sind ihre Eingaben.
def _ketten(testdata):
ketten = []
for obj in testdata:
if "test_id" in obj:
ketten.append((obj, []))
elif "typ" in obj and ketten:
ketten[-1][1].append(obj)
return ketten
@pytest.fixture(scope="module")
def spec_objekte():
if not os.path.isfile(SPEC_JSON):
pytest.skip("%s fehlt (lib/vf_spec_export.py erzeugt es)" % SPEC_JSON)
with open(SPEC_JSON, encoding="utf-8") as fh:
return json.load(fh)
def _journal_laenge(eingaben):
"""Anzahl der Journal-Eintraege, die der Treiber daraus baut: ein
point_rel mit "hz" wird zu ZWEI Eintraegen (DL + REAL)."""
return sum(2 if (e["typ"] == "point_rel" and "hz" in e) else 1
for e in eingaben)
@pytest.fixture(scope="module")
def spec_ergebnisse():
for pfad in ERGEBNIS_ORTE:
if os.path.isfile(pfad):
with open(pfad, encoding="utf-8") as fh:
return json.load(fh)
pytest.skip("hundm05_results.json fehlt - TEST_HUNDM05 in BricsCAD "
"ausfuehren")
def _pfad(*teile):
return os.path.join(os.path.dirname(os.path.abspath(__file__)), *teile)
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
# ============================================================
# Testdaten-Pruefung (hundm05.json selbst)
# ============================================================
class TestHundM05Testdaten:
def test_fuenf_ketten(self, hundm05_testdata):
"""Die Anlage hat fuenf am Kreisel haengende Linienzuege."""
ketten = _ketten(hundm05_testdata)
assert len(ketten) == 5, \
f"{len(ketten)} Ketten in hundm05.json, erwartet 5"
def test_jedes_objekt_zuordenbar(self, hundm05_testdata):
"""Kein Objekt darf zwischen Kopf und Eingabe fallen - sonst
verschluckt hundm05:gruppiere es stillschweigend."""
for i, obj in enumerate(hundm05_testdata):
assert ("test_id" in obj) or ("typ" in obj), \
f"Objekt {i} ist weder Kopf noch Eingabe: {sorted(obj)}"
def test_kopf_felder(self, hundm05_testdata):
for kopf, _ in _ketten(hundm05_testdata):
assert kopf["test_id"].startswith("VF_"), kopf["test_id"]
# "typ" wuerde den Kopf zu einer Eingabe machen (siehe
# hundm05:gruppiere) - der Anlagetyp heisst darum "anlagetyp".
assert "typ" not in kopf, \
f'{kopf["test_id"]}: Kopf darf kein Feld "typ" tragen'
assert kopf["anlagetyp"] == "linienzug", kopf["test_id"]
assert kopf["modus"] == 1, kopf["test_id"]
assert kopf["block"].startswith("VF_"), kopf["block"]
assert len(kopf["startpunkt_mm"]) == 3, kopf["test_id"]
def test_anzahl_eingaben_stimmt(self, hundm05_testdata):
"""anzahl_eingaben ist die Selbstauskunft des Kopfes - weicht sie ab,
fehlen Eingaben (z.B. durch eine Handaenderung)."""
for kopf, eingaben in _ketten(hundm05_testdata):
assert kopf["anzahl_eingaben"] == len(eingaben), \
f'{kopf["test_id"]}: anzahl_eingaben=' \
f'{kopf["anzahl_eingaben"]}, gezaehlt {len(eingaben)}'
def test_eingabe_typen_bekannt(self, hundm05_testdata):
for kopf, eingaben in _ketten(hundm05_testdata):
for i, e in enumerate(eingaben):
assert e["typ"] in EINGABE_TYPEN, \
f'{kopf["test_id"]} Eingabe {i}: unbekannter Typ ' \
f'{e["typ"]}'
assert e.get("kommentar"), \
f'{kopf["test_id"]} Eingabe {i}: kein Kommentar'
def test_kette_beginnt_mit_startpunkt(self, hundm05_testdata):
"""Erste Eingabe = absoluter Startpunkt, zweite = Starthoehe. Fehlt
der Punkt, wuerde das Replay ihn interaktiv erfragen und die Kette an
der falschen Stelle aufbauen (siehe hundm05:build-linienzug)."""
for kopf, eingaben in _ketten(hundm05_testdata):
assert eingaben[0]["typ"] == "point_abs", kopf["test_id"]
assert eingaben[1]["typ"] == "real", kopf["test_id"]
punkt = eingaben[0]["wert"]
assert len(punkt) == 3, kopf["test_id"]
assert punkt == kopf["startpunkt_mm"], \
f'{kopf["test_id"]}: point_abs {punkt} != startpunkt_mm ' \
f'{kopf["startpunkt_mm"]}'
assert eingaben[1]["wert"] == punkt[2], \
f'{kopf["test_id"]}: Starthoehe {eingaben[1]["wert"]} != ' \
f'Z des Startpunkts {punkt[2]}'
def test_hz_nur_beim_ersten_segment(self, hundm05_testdata):
"""vfl-in-abstand journalisiert die Fahrtrichtung NUR beim ersten
Segment der Kette; jedes weitere erbt sie vom Vorgaenger. Ein "hz" an
spaeterer Stelle waere ein Wert zu viel in der Replay-Queue und
verschiebt alle folgenden Antworten (Desync)."""
for kopf, eingaben in _ketten(hundm05_testdata):
rel = [e for e in eingaben if e["typ"] == "point_rel"]
assert rel, f'{kopf["test_id"]}: kein Segment'
assert "hz" in rel[0], \
f'{kopf["test_id"]}: erstes Segment ohne hz'
for i, e in enumerate(rel[1:], start=2):
assert "hz" not in e, \
f'{kopf["test_id"]}: Segment {i} traegt hz - nur das ' \
f'erste Segment darf eines haben'
def test_segmentlaengen_positiv(self, hundm05_testdata):
for kopf, eingaben in _ketten(hundm05_testdata):
for e in eingaben:
if e["typ"] == "point_rel":
assert e["dL"] > 0, f'{kopf["test_id"]}: dL={e["dL"]}'
def test_menue_antworten_sind_strings(self, hundm05_testdata):
"""Menue-Antworten muessen Strings bleiben ("1"/"2"/"90"/"30"): der
Produktionscode vergleicht mit (= antwort "1"), eine Zahl wuerde
stumm in den Default-Zweig laufen."""
for kopf, eingaben in _ketten(hundm05_testdata):
for i, e in enumerate(eingaben):
if e["typ"] in ("string", "step"):
assert isinstance(e["wert"], str), \
f'{kopf["test_id"]} Eingabe {i}: {e["wert"]!r} ist ' \
f'kein String'
def test_glieder_vorhanden(self, hundm05_testdata):
"""Jede Kette hat Glied-Marker (STEP) - sie sind der Anker fuer den
Sektions-/Glied-Edit und die Kurzbeschreibung."""
for kopf, eingaben in _ketten(hundm05_testdata):
glieder = [e["wert"] for e in eingaben if e["typ"] == "step"]
assert glieder, f'{kopf["test_id"]}: kein STEP-Marker'
for g in glieder:
assert g in ("Linie-GF", "Linie-VF", "Linie", "GF-Bogen",
"Horizontal-VF", "Vario-Kurve", "ES"), \
f'{kopf["test_id"]}: unbekanntes Glied {g}'
def test_arrays_einzeilig(self):
"""Jedes Array muss komplett in EINER Zeile stehen.
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei zeilenweise,
und ssg-cfg-parse-array braucht das ganze Array in einem String. Ein
von json.dump(indent=2) umgebrochenes Array wird zu nil - der Treiber
baut die Kette dann nicht an ihrer Position. Das ist genau der
Fehler, der die Anlage einmal komplett am Ursprung aufgebaut hat, und
er ist in der Zeichnung nicht als Datenfehler zu erkennen.
"""
with open(_pfad("testdata", "hundm05.json"), encoding="utf-8") as fh:
for nr, zeile in enumerate(fh, start=1):
offen = zeile.count("[") - zeile.count("]")
# Die aeusserste Klammer des Dokuments darf allein stehen.
if zeile.strip() in ("[", "]"):
continue
assert offen == 0, \
f"Zeile {nr}: Array ueber mehrere Zeilen umgebrochen - " \
f"ssg-cfg-parse-array kann das nicht lesen: {zeile.strip()!r}"
def test_flach_fuer_lisp_parser(self, hundm05_testdata):
"""Die Datei muss FLACH bleiben.
ssg-load-json / ssg-parse-json-array behandelt jedes "{" als
Objektanfang und jedes "}"/"}," als Objektende - es kennt keine
Verschachtelung. Ein Objekt in einem Objekt (oder ein Array von
Objekten wie "eingaben": [...]) wuerde das umgebende Objekt
verschlucken. Dieser Test zaehlt die Objekte so, wie der LISP-Parser
sie zaehlen wuerde, und vergleicht mit dem echten JSON.
"""
objekte = 0
with open(_pfad("testdata", "hundm05.json"), encoding="utf-8") as fh:
for zeile in fh:
gestrippt = zeile.strip()
if gestrippt == "{":
objekte += 1
elif gestrippt in ("}", "},"):
continue
elif gestrippt.startswith('"'):
# Ein Wert, der ein Objekt oder eine Objektliste eroeffnet,
# macht die Datei fuer den LISP-Parser unlesbar.
assert not gestrippt.endswith(("{", "[")), \
f"verschachtelter Wert: {gestrippt!r}"
assert objekte == len(hundm05_testdata), \
f"LISP-Parser saehe {objekte} Objekte, json.load sieht " \
f"{len(hundm05_testdata)}"
# ============================================================
# Ergebnis-Pruefung (results.json vs. testdata)
# ============================================================
class TestHundM05Results:
def test_je_kette_ein_ergebnis(self, hundm05_testdata, hundm05_results):
soll = [k["test_id"] for k, _ in _ketten(hundm05_testdata)]
ist = [r["test_id"] for r in hundm05_results]
assert ist == soll, f"Ergebnisse {ist}, erwartet {soll}"
for r in hundm05_results:
assert r["kind"] == "linienzug", f'{r["test_id"]}: {r["kind"]}'
def test_status_executed(self, hundm05_results):
""""desync" (Journal-Werte nicht verbraucht) und "failed" (Abbruch)
sind beide echte Fehler - siehe hundm05:build-linienzug."""
for r in hundm05_results:
def test_status_executed(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["status"] == "executed", \
f'{r["test_id"]}: Status={r["status"]}'
f'{r["spec_id"]}: Status={r["status"]}, {r.get("fehler_text")}'
def test_journal_vollstaendig_verbraucht(self, hundm05_testdata,
hundm05_results):
"""Alle aufgezeichneten Werte muessen im Bau abgerufen worden sein.
Bleibt etwas offen, ist der Ablauf von der Aufzeichnung abgewichen und
die Geometrie kann der Vorlage nicht entsprechen."""
soll = {k["test_id"]: _journal_laenge(e)
for k, e in _ketten(hundm05_testdata)}
for r in hundm05_results:
assert "eingaben_offen" in r, (
f'{r["test_id"]}: kein eingaben_offen im Ergebnis - die Kette '
f'ist vor der Auswertung abgebrochen (status='
f'{r.get("status")!r}, {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["test_id"]}: {r["eingaben_offen"]} Journal-Werte offen'
assert r["eingaben_gesamt"] == soll[r["test_id"]], \
f'{r["test_id"]}: {r["eingaben_gesamt"]} Journal-Eintraege ' \
f'abgespielt, erwartet {soll[r["test_id"]]}'
f'{r["spec_id"]}: {r["eingaben_offen"]} Werte offen'
assert r["eingaben_gesamt"] > 0
def test_keine_live_eingabe(self, hundm05_results):
"""Kein Prompt, kein Dialog: der Treiber schaltet *vfl-headless* ein
(harter Abbruch statt stillem Rueckfall auf Live-Eingabe, siehe
vfl-headless-abbruch in Lisp/vf_linienzug.lsp) UND ersetzt zusaetzlich
getpoint/getstring/getint/getreal durch zaehlende Stubs. Beide Wege
muessen leer bleiben - "prompts" ist der positive Beweis, dass keine
Eingabe ausserhalb der vfl-in-*-Wrapper erreicht wurde."""
for r in hundm05_results:
if "prompts" in r:
assert r["prompts"] == 0, \
f'{r["test_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
assert not r.get("headless_fehler"), \
f'{r["test_id"]}: Headless-Abbruch bei {r["headless_fehler"]}'
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_geometrie_meldungen(self, hundm05_results):
"""*vfl-meldungen* sammelt die abgewiesenen Sektionen (frueher
blockierende alert-Fenster, jetzt vfl-meldung). Eine Meldung heisst:
ein Glied liess sich geometrisch nicht bauen - der Nachbau weicht dann
von der Vorlage ab, auch wenn das Journal aufgeht."""
for r in hundm05_results:
assert not r.get("meldungen"), \
f'{r["test_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
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, hundm05_results):
for r in hundm05_results:
def test_vf_block_erzeugt(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["block_name"].startswith("VF_"), \
f'{r["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
assert r["block_handle"], f'{r["test_id"]}: kein Handle'
f'{r["spec_id"]}: Block {r["block_name"]!r}'
assert r["block_handle"]
def test_dimension_einheitlich(self, hundm05_results):
"""Alle Ergebnisse eines Laufs stammen aus EINER Bau-Dimension.
Die Eingabedaten sind dimensionsfrei; ob _2D- oder _3D-Blockvarianten
verwendet werden, loest ssg-ils-dim-aktuell auf. Ein Lauf, in dem beide
vorkommen, waere ein halb umgeschalteter Zustand (z.B. weil ein
vorheriger Test *ssg-ils-dim* nicht zurueckgesetzt hat).
"""
dims = {r.get("dimension") for r in hundm05_results}
assert len(dims) == 1, f"gemischte Bau-Dimensionen im Lauf: {dims}"
dim = dims.pop()
assert dim in ("2D", "3D"), \
f'Bau-Dimension "{dim}" ist weder 2D noch 3D'
def test_startpunkt_getroffen(self, hundm05_testdata, hundm05_results):
"""Der VF_n-Block wird mit dem Kettenstartpunkt als Basispunkt
gewickelt (ssg-block-wrap-welt in vfl-block-erstellen) - sein
Einfuegepunkt muss also der Startpunkt aus den Eingaben sein."""
soll = {k["test_id"]: k["startpunkt_mm"]
for k, _ in _ketten(hundm05_testdata)}
for r in hundm05_results:
p = soll[r["test_id"]]
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, i in (("X", 0), ("Y", 1), ("Z", 2)):
assert abs(ist[i] - p[i]) < 1.0, \
f'{r["test_id"]}: Einfuegepunkt {achse} {ist[i]} != ' \
f'{p[i]}'
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_as_es_attribute_aus_eingaben(self, hundm05_testdata,
hundm05_results):
"""WINKEL_AS/SEITE_AS und WINKEL_ES/SEITE_ES muessen die Antworten aus
dem Journal wiedergeben (AS steht in der Praeambel, ES im ES-Glied)."""
seiten = {"1": "links", "2": "rechts"}
nach_id = {r["test_id"]: r for r in hundm05_results}
for kopf, eingaben in _ketten(hundm05_testdata):
r = nach_id[kopf["test_id"]]
attrs = r["actual_attributes"]
# Praeambel: [0] Punkt, [1] Hoehe, [2] AS setzen, [3] Winkel,
# [4] Seite
if eingaben[2]["wert"] == "1":
assert attrs.get("WINKEL_AS") == eingaben[3]["wert"], \
f'{kopf["test_id"]}: WINKEL_AS=' \
f'{attrs.get("WINKEL_AS")}, erwartet {eingaben[3]["wert"]}'
assert attrs.get("SEITE_AS") == seiten[eingaben[4]["wert"]], \
f'{kopf["test_id"]}: SEITE_AS={attrs.get("SEITE_AS")}'
# ES: die beiden Strings direkt nach dem STEP-Marker "ES"
for i, e in enumerate(eingaben):
if e["typ"] == "step" and e["wert"] == "ES":
assert attrs.get("WINKEL_ES") == eingaben[i + 1]["wert"], \
f'{kopf["test_id"]}: WINKEL_ES=' \
f'{attrs.get("WINKEL_ES")}'
assert attrs.get("SEITE_ES") == \
seiten[eingaben[i + 2]["wert"]], \
f'{kopf["test_id"]}: SEITE_ES={attrs.get("SEITE_ES")}'
break
def test_vario_kurven_gezaehlt(self, hundm05_testdata, hundm05_results):
"""Jede Vario-Kurve im Journal muss in den VF_Bogen_*-Attributen
auftauchen (vfl-insert-vario-kurve-block zaehlt sie mit)."""
nach_id = {r["test_id"]: r for r in hundm05_results}
for kopf, eingaben in _ketten(hundm05_testdata):
soll = sum(1 for e in eingaben
if e["typ"] == "step" and e["wert"] == "Vario-Kurve")
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
ist = sum(int(attrs.get(f"VF_Bogen_{lage}_{w}", 0) or 0)
for lage in ("A", "I") for w in (90, 60, 30))
assert ist == soll, \
f'{kopf["test_id"]}: {ist} Vario-Kurven in den Attributen, ' \
f'{soll} im Journal'
def test_hoehe_von_ist_starthoehe(self, hundm05_testdata,
hundm05_results):
nach_id = {r["test_id"]: r for r in hundm05_results}
for kopf, eingaben in _ketten(hundm05_testdata):
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
if "HOEHE_VON_mm" not in attrs:
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", "hm_recformat_results.json")
if not os.path.isfile(pfad):
pytest.skip("tests/output/hm_recformat_results.json fehlt "
"(TEST_HM_RECFORMAT 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
assert abs(int(attrs["HOEHE_VON_mm"]) - eingaben[1]["wert"]) <= 1, \
f'{kopf["test_id"]}: HOEHE_VON_mm={attrs["HOEHE_VON_mm"]}, ' \
f'Starthoehe {eingaben[1]["wert"]}'
# ============================================================
# DXF Geometrie-Pruefung (ezdxf)
# ============================================================
class TestHundM05Geometry:
def test_block_anzahl(self, hundm05_dxf, hundm05_testdata):
"""Je Kette muss ein VF_-Block im Modelspace stehen."""
n_vf = sum(1 for e in hundm05_dxf.modelspace()
if e.dxftype() == "INSERT" and e.dxf.name.startswith("VF_"))
soll = len(_ketten(hundm05_testdata))
assert n_vf >= soll, f"VF_: {n_vf} < {soll}"
def test_handles_im_modelspace(self, hundm05_dxf, hundm05_results):
"""Jedes gebaute Block-Handle (executed) liegt im Modelspace."""
handles = {e.dxf.handle for e in hundm05_dxf.modelspace()
if e.dxftype() == "INSERT"}
for r in hundm05_results:
if r["status"] == "executed" and r["block_handle"]:
assert r["block_handle"] in handles, \
f'{r["test_id"]}: Handle {r["block_handle"]} nicht im ' \
f'Modelspace'
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}'
+1 -1
View File
@@ -8,7 +8,7 @@ Warum: der Dekoder ist die ZWEITE Implementierung der Linienzug-Grammatik
(erste ist der Bau-Ablauf in Lisp/vf_linienzug.lsp selbst). Nur wenn beide
dieselbe Frage-Reihenfolge kennen, laesst sich ein Journal aus einer echten
Zeichnung verlustfrei in das Testdatenschema zuruecklesen
(tests/testdata/hundm05.json).
(tests/testdata/hm_recformat.json).
Die 5 echten HundM-Ketten deckten zwei Zweige NICHT ab, weil ihre Daten sie
nicht enthalten:
+9 -8
View File
@@ -15,8 +15,8 @@
;; (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.
;; tests/testdata/hundm05.json muss dieselben Journale ergeben
;; wie das aufgezeichnete Protokoll tests/testdata/hm_recformat.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).
;;
@@ -26,8 +26,9 @@
;; (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.
;; Dieser Test baut KEINE Geometrie - er prueft nur den Uebersetzer.
;; Ergebnis: tests/output/vf_spec_unit_results.json (Pass/Fail-Zaehler).
;; Die gebaute Anlage prueft TEST_HUNDM05 (tests/test_hundm05.lsp).
;; ============================================================
(setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '())
@@ -362,8 +363,8 @@
;; ============================================================
;; 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
;; Das aufgezeichnete Protokoll (hm_recformat.json) in Journal-Tokens
;; zurueckuebersetzen - dieselbe Umrechnung wie in test_hm_recformat.lsp, hier
;; bewusst dupliziert, damit der Test ohne den anderen Treiber laeuft.
(defun tsp-als-string (v)
@@ -410,9 +411,9 @@
(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 p-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hm_recformat.json"))
(setq s-datei (strcat (getenv "DXFMAKRO")
"/tests/testdata/vf_spec_hundm05.json"))
"/tests/testdata/hundm05.json"))
(cond
((null (findfile p-datei))
(princ (strcat "\n [SKIP] " p-datei " fehlt")))
+16 -120
View File
@@ -1,20 +1,21 @@
#!/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:
test_vf_spec.py - Validiert den Spec-UEBERSETZER des VF-Linienzugs.
Laeuft komplett ohne BricsCAD.
1. Rundlauf: fuer jede der 5 echten HundM-Ketten muss
journal_aus_spec(spec_aus_journal(tokens)) == tokens gelten. Das
beweist, dass die Spec das Eingabe-Journal VERLUSTFREI abbildet - sonst
waere ein spec-gebauter Nachbau eine andere Kette als das Original.
2. Die Spec-Testdaten (tests/testdata/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.
2. Die Spec-Testdaten (tests/testdata/hundm05.json): Struktur, Lesbarkeit
durch den LISP-Parser, und dass sie dieselben Journale ergeben wie die
Aufzeichnung tests/testdata/hm_recformat.json.
3. Validierung: was die Spec nicht hergibt, muss ein Fehler sein.
4. Die Erwartungswerte in tests/test_vf_spec.lsp stimmen mit dem hier
geprueften Emitter ueberein.
Die GEBAUTE Geometrie prueft tests/test_hundm05.py (braucht BricsCAD).
Der LISP-Emitter (vsp-journal in Lisp/vf_spec.lsp) ist die zweite
Implementierung derselben Grammatik; er wird in tests/test_vf_spec.lsp gegen
@@ -32,14 +33,11 @@ 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")]
# Die AUFZEICHNUNG (rohes Journal aus der Kundenzeichnung, Frage-Antwort-
# Protokoll) und die daraus abgeleitete SPEC. Erstere ist die Vorlage, an
# der die zweite gemessen wird.
PROTOKOLL = os.path.join(HIER, "testdata", "hm_recformat.json")
SPEC_JSON = os.path.join(HIER, "testdata", "hundm05.json")
def _lade(pfad, was):
@@ -49,14 +47,6 @@ def _lade(pfad, was):
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")
@@ -67,13 +57,8 @@ 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."""
"""Gleiche Gruppierregel wie hmrec:gruppiere in LISP."""
ketten = []
for obj in protokoll:
if "test_id" in obj:
@@ -458,92 +443,3 @@ class TestLispErwartungen:
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}'
+1492
View File
File diff suppressed because it is too large Load Diff
+267 -1341
View File
File diff suppressed because it is too large Load Diff
+18 -9
View File
@@ -407,7 +407,7 @@ Dasselbe Protokoll steht produktionsseitig als **XDATA am fertigen
wiederabspielbar (`vfl-journal-replay-start` + `vf-linienzug-modus`, genutzt
vom Doppelklick-Editieren). Ein Journal aus einer echten Zeichnung laesst
sich darum 1:1 in dieses Schema zuruecklesen — genau das tut
`lib/vf_journal_export.py` (siehe Abschnitt 8, `hundm05.json`). Zwei
`lib/vf_journal_export.py` (siehe Abschnitt 8, `hm_recformat.json`). Zwei
Unterschiede zum handgeschriebenen `linienzug_tests.json`:
- Zusaetzlicher Eintragstyp `step` (Glied-Marker aus `vfl-journal-mark`,
@@ -427,7 +427,7 @@ blieb an einem `getpoint` haengen. Mit `*vfl-headless*` = T ist das jetzt ein
harter Abbruch mit Fundstelle (`vfl-headless-abbruch` -> `*vfl-headless-fehler*`
nennt Art der Eingabe, Glied-Nummer und Eingabe-Nummer). Die Meldungen der
geometrisch abgewiesenen Sektionen laufen ueber `vfl-meldung` statt `alert` und
stehen in `*vfl-meldungen*`, blockieren also nichts mehr. `tests/test_hundm05.lsp`
stehen in `*vfl-meldungen*`, blockieren also nichts mehr. `tests/test_hm_recformat.lsp`
schaltet beides ein; die Diagnose landet im Ergebnis-JSON
(`headless_fehler`, `meldungen`, `prompts`).
@@ -615,7 +615,7 @@ Vollstaendiges JSON:
"endpunkt": [220499.01, 154926.53, 0.0]
}
```
Einen Treiber dafuer gibt es derzeit nicht mehr: `tests/test_hundm05.lsp`
Einen Treiber dafuer gibt es derzeit nicht mehr: `tests/test_hm_recformat.lsp`
spielt seit dem Umbau auf die Linienzug-Journale nur noch Ketten ab (siehe
Abschnitt 8); der fruehere `hundm05:build-gerade` steht in der Git-Historie.
@@ -640,9 +640,18 @@ Abschnitt 8); der fruehere `hundm05:build-gerade` steht in der Git-Historie.
| Omniflo TV_*/APBW_* | - | **nein** (nicht implementiert, DUMMY) |
| Separator/Scanner | `ils-insert-sensor` | **nein** (Schleife mit `getpoint`/`pause`) |
## 8. HundM05 — die 5 Linienzuege am Kreisel (`hundm05.json`)
## 8. HundM05 — die 5 Linienzuege am Kreisel
`tests/testdata/hundm05.json` enthaelt die **Eingabe-Journale der 5
Zwei Dateien beschreiben dieselben 5 Ketten, in zwei Formaten:
| Datei | Inhalt | Test |
|---|---|---|
| `tests/testdata/hm_recformat.json` | die **Aufzeichnung**: rohes Eingabe-Journal aus der XDATA der Kundenzeichnung, als Frage-Antwort-Protokoll | `TEST_HM_RECFORMAT` (in `alltests.json` abgeschaltet) |
| `tests/testdata/hundm05.json` | die daraus abgeleitete **Spec** (Domaenenwerte statt Menue-Codes, 52 statt 281 Objekte) - hier werden weitere Ketten nachgetragen | `TEST_HUNDM05` |
Die Spec ist das Eingabeformat, die Aufzeichnung die Vorlage, an der sie gemessen wird (Rundlauf-Beweis in `tests/test_vf_spec.py`). Warum beide bleiben: die Aufzeichnung ist die einzige eingecheckte Kopie der Originaldaten (`data/polylines.dxf` ist zu gross fuer das Repo) UND der einzige Test dafuer, dass ein BESTEHENDER `VF_n`-Block weiter abspielbar ist - also dass Doppelklick-Edit und 2D/3D-Konvertierung an Altbestand funktionieren. Gebaut wird routinemaessig nur noch aus der Spec, damit nicht zweimal dieselbe Geometrie entsteht.
`tests/testdata/hm_recformat.json` enthaelt die **Eingabe-Journale der 5
VarioFoerderer-Linienzuege** aus `data/polylines.dxf` (die Anlage HundM, ein
Kreisel mit fuenf daran haengenden Ketten). Quelle ist nicht die gezeichnete
Geometrie, sondern die XDATA `SSG_VF_EDIT` (Marker `"linienzug"`) an den
@@ -675,7 +684,7 @@ Neu erzeugen:
```
python lib/vf_journal_export.py data/polylines.dxf \
tests/testdata/hundm05.json --csv results/HundM_export.csv
tests/testdata/hm_recformat.json --csv results/HundM_export.csv
```
Die frueheren Inhalte (Kreisel/Gefaellestrecke/Sensor-Eintraege aus dem
@@ -690,10 +699,10 @@ Kopf-Objekte verschlucken. Darum steht je Kette ein Kopf-Objekt (traegt
`linienzug_tests.json`. Der Anlagetyp im Kopf heisst `anlagetyp` und NICHT
`typ`, damit Kopf und Eingabe unterscheidbar bleiben.
Abgespielt wird das per `tests/test_hundm05.lsp` (`TEST_HUNDM05`,
Abgespielt wird das per `tests/test_hm_recformat.lsp` (`TEST_HM_RECFORMAT`,
`TEST_HUNDM05_2D`/`_3D`): der Treiber gruppiert die Objekte
(`hundm05:gruppiere`), baut daraus das Vorwaerts-Journal
(`hundm05:eintrag->journal`) und spielt es je Kette ueber
(`hmrec:gruppiere`), baut daraus das Vorwaerts-Journal
(`hmrec:eintrag->journal`) und spielt es je Kette ueber
`vfl-journal-replay-start` + `vf-linienzug-modus` ab — denselben Weg nimmt die
Produktion beim Editieren/Konvertieren (`vfl-konvertiere-ent`). Bewusst KEINE
Eingabe-Mocks wie in `test_linienzug.lsp`: ein Mock muesste zu jedem Segment
-418
View File
@@ -1,418 +0,0 @@
[
{
"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"
}
]