From bf3996435aff8db4b3dca789fbbbb956ea3bff2f Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 10:00:17 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Weitere=20TOS=20Nummern=20f=C3=BCr=20Zusatz?= =?UTF-8?q?modul,=20Weichen=20Strecke=20und=20Automation=20in=20den=20Code?= =?UTF-8?q?=20f=C3=BCr=20Export=20eingepflegt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/export_sivas.py | 69 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 3242e12..f5b34a9 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -103,7 +103,18 @@ def build_kreisel_details(block): # --- Bekannte Bloecke ignorieren --- SKIP_BLOCKS = {"K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"} -VARIO_BLOCKS = { +# Sivas-Nummern und Bezeichnungen je ILS-Elementtyp +ILS_SIVASNR = { + "kreisel": ("6200", "ILS 2.0 Kreisel"), + "eckrad": ("6210", "ILS 2.0 Eckrad"), + "zusatzmodul":("6215", "ILS 2.0 Zusatzmodul"), + "ils_weiche": ("6216", "ILS 2.0 Weichen"), + "strecke": ("6220", "ILS 2.0 Strecke - Module"), + "automation": ("6269", "ILS 2.0 Automation und Pneumatik - Gesamtanlage"), +} + +# Strecken-Module: Blocknamen-Fragmente fuer Vario/Staustrecke-Elemente +STRECKE_BLOCKS = { "AUS_Element_links", "EIN_Element_links", "Staustrecke_SP_1000_mm", "Staustrecke_Separator_SP_300_mm", "Vario_Spannstation_SP_500mm", "Vario_Motorstation_SP_500mm", @@ -233,9 +244,14 @@ def process_blocks(blocks, lookup): counters["anzahl_streckengruppen"] = 1 continue - # --- VarioFoerderer --- - for vb in VARIO_BLOCKS: - if vb in bname: + # --- ILS Eckrad --- + if bname.startswith("ECKRAD_"): + counters["anzahl_eckraeder"] += 1 + continue + + # --- ILS Strecke-Module (Vario/Staustrecke) --- + for sb in STRECKE_BLOCKS: + if sb in bname: counters["anzahl_variofoerderer"] += 1 break @@ -286,10 +302,11 @@ def process_blocks(blocks, lookup): nr += 1 for block in kreisel_list: + sivasnr, teileart = ILS_SIVASNR["kreisel"] items.append({ "nr": nr, - "teileart": "ILS 2.0 Kreisel", - "sivasnr": "6200", + "teileart": teileart, + "sivasnr": sivasnr, "sivasnr_quoted": True, "dispgruppe": 20, "anzahl": 1, @@ -299,8 +316,41 @@ def process_blocks(blocks, lookup): }) nr += 1 + if counters["anzahl_eckraeder"] > 0: + sivasnr, teileart = ILS_SIVASNR["eckrad"] + items.append({ + "nr": nr, + "teileart": teileart, + "sivasnr": sivasnr, + "sivasnr_quoted": True, + "dispgruppe": 20, + "anzahl": counters["anzahl_eckraeder"], + "laenge_mm": "", + "hoehe_m": "", + "details": {}, + }) + nr += 1 + + if counters["anzahl_variofoerderer"] > 0: + sivasnr, teileart = ILS_SIVASNR["strecke"] + items.append({ + "nr": nr, + "teileart": teileart, + "sivasnr": sivasnr, + "sivasnr_quoted": True, + "dispgruppe": 20, + "anzahl": counters["anzahl_variofoerderer"], + "laenge_mm": "", + "hoehe_m": "", + "details": {}, + }) + nr += 1 + # ILS Automation Summierungszeile (nur wenn ILS-Elemente vorhanden) - if counters["anzahl_kreisel"] > 0 or counters["anzahl_variofoerderer"] > 0: + has_ils = (counters["anzahl_kreisel"] > 0 + or counters["anzahl_eckraeder"] > 0 + or counters["anzahl_variofoerderer"] > 0) + if has_ils: automation_details = { "Anzahl Kreisel": counters["anzahl_kreisel"], "Anzahl Kreisel mit Pinband": counters["anzahl_kreisel_pinband"], @@ -323,10 +373,11 @@ def process_blocks(blocks, lookup): "Anzahl aller BTBeladung": counters["anzahl_btbeladung"], "Anzahl aller Abwurfstationen": counters["anzahl_abwurfstationen"], } + auto_sivasnr, auto_teileart = ILS_SIVASNR["automation"] items.append({ "nr": nr, - "teileart": "ILS 2.0 Automation", - "sivasnr": "6270", + "teileart": auto_teileart, + "sivasnr": auto_sivasnr, "sivasnr_quoted": True, "dispgruppe": 20, "anzahl": 1, From 8db7b2bfbfad95a9369e11e0a2f038707a202c01 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 10:35:31 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Veraltetes=20Men=C3=BC=20Etagenf=C3=B6rdere?= =?UTF-8?q?r=20entfernt.=20Testdaten=20f=C3=BCr=20den=20F=C3=B6rderertest?= =?UTF-8?q?=20wird=20jetzt=20aus=20.json=20Datei=20geladen.=20Testfunktion?= =?UTF-8?q?en=20des=20F=C3=B6rderers=20bauen=20jetzt=20auch=20Block=20um?= =?UTF-8?q?=20jeden=20F=C3=B6rderer,=20damit=20ein=20Export=20m=C3=B6glich?= =?UTF-8?q?=20ist=20.=20Fix=20einiger=20Funktionsaufrufe=20in=20den=20Vari?= =?UTF-8?q?of=C3=B6rderer=20Funktionen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lisp/SSG_LIB_Commands.lsp | 5 -- Lisp/ssg_core.lsp | 2 +- Lisp/vf_standard.lsp | 2 +- README.md | 1 - menu/SSG_LIB.mnu | 1 - tests/test_foerderer.lsp | 118 +++++++++++++--------------- tests/testdata/foerderer_tests.json | 15 ++++ 7 files changed, 70 insertions(+), 74 deletions(-) create mode 100644 tests/testdata/foerderer_tests.json diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index 5aee241..ef5afc8 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -16,11 +16,6 @@ ) ;; --- ILS / Foerderer --- -(defun c:ILS_ETF_Etagenfoerderer () - (princ "\n[DUMMY] ILS_ETF_Etagenfoerderer aufgerufen") - (princ) -) - (defun c:ILS_TEF_Strecke () (princ "\n[DUMMY] ILS_TEF_Strecke aufgerufen") (princ) diff --git a/Lisp/ssg_core.lsp b/Lisp/ssg_core.lsp index b3e2132..20eaf1f 100644 --- a/Lisp/ssg_core.lsp +++ b/Lisp/ssg_core.lsp @@ -370,7 +370,7 @@ (progn (setq tag (cdr (assoc 2 ed))) (setq wert (cdr (assoc tag attrib-alist))) - (if (and wert (> (strlen wert) 0)) + (if (and wert (= (type wert) 'STR) (> (strlen wert) 0)) (progn (entmod (subst (cons 1 wert) (assoc 1 ed) ed)) (entupd obj) diff --git a/Lisp/vf_standard.lsp b/Lisp/vf_standard.lsp index 738045f..d49274e 100644 --- a/Lisp/vf_standard.lsp +++ b/Lisp/vf_standard.lsp @@ -70,7 +70,7 @@ ) (setq missing '()) (foreach bname required-blocks - (setq datei (strcat block-pfad bname ".dxf")) + (setq datei (strcat block-pfad bname ".dwg")) (if (not (findfile datei)) (setq missing (cons datei missing)) ) diff --git a/README.md b/README.md index 7f4d524..ddaafcb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ SSG_LIB │ │ ├── BTMT-Beladung → ILS_BTMT_Beladung │ │ └── BTMT-Entladung → ILS_BTMT_Entladung │ ├── Foerderer -│ │ ├── ETF Etagenfoerderer → ILS_ETF_Etagenfoerderer │ │ ├── TEF Strecke → ILS_TEF_Strecke │ │ ├── VarioFoerderer → FOERDERANLAGE │ │ ├── Kurven angetr. Aussen → ILS_K{90,60,30}{LA,RA} diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index 55f2bab..582ce67 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -12,7 +12,6 @@ [BTMT-Beladung]^C^C(ssg-ensure "SSG_LIB_Commands") ILS_BTMT_Beladung [<-BTMT-Entladung]^C^C(ssg-ensure "SSG_LIB_Commands") ILS_BTMT_Entladung [->Foerderer] -[ETF Etagenfoerderer]^C^CI(ssg-ensure "SSG_LIB_Commands") LS_ETF_Etagenfoerderer [Durch TEF angetriebene Strecke]^C^C(ssg-ensure "SSG_LIB_Commands") ILS_TEF_Strecke [VarioFoerderer]^C^CVarioFoerderer [EtageVarioFoerderer]^C^CETAGEVARIOFOERDERER diff --git a/tests/test_foerderer.lsp b/tests/test_foerderer.lsp index 0b554a9..e112d9f 100644 --- a/tests/test_foerderer.lsp +++ b/tests/test_foerderer.lsp @@ -1,10 +1,9 @@ ;; ============================================================ ;; test_foerderer.lsp - Automatischer Integrationstest fuer VarioFoerderer ;; -;; Erzeugt 12 Variofoerderer (6x Auf, 6x Ab) mit deltaL=7000 -;; und verschiedenen Hoehenstufen. Speichert: +;; Liest Testfaelle aus tests/testdata/foerderer_tests.json. +;; Speichert: ;; - tests/output/foerderer_results.json (Ergebnisse) -;; - tests/output/foerderer_tests.dwg (Zeichnung) ;; ;; Voraussetzungen: ;; - SSG_LIB geladen (VarioFoerderer.lsp, ssg_core.lsp) @@ -70,12 +69,12 @@ ;; ============================================================ ;; C:TEST_FOERDERER - Hauptbefehl ;; ============================================================ -(defun c:TEST_FOERDERER ( / deltaL deltaH richtung ergebnis - best-winkel L_GF L_VF - y-offset hoehen-liste idx - anz-gebaut anz-nicht-gebaut - results-list - test-id) +(defun c:TEST_FOERDERER ( / json-datei testdaten cfg tests + eintrag test-id richtung deltaL deltaH + ergebnis best-winkel L_GF L_VF + y-offset y-abstand idx + anz-gebaut anz-nicht-gebaut results-list + startpunkt vf-nummer hoehe-von hoehe-bis lastEnt) ;; Bibliothek initialisieren (laedt Block-Library und extrahiert Masse) (if (not *lib-initialized*) @@ -87,8 +86,30 @@ (setvar "ATTREQ" 0) (setvar "ATTDIA" 0) - (setq deltaL 7000) - (setq hoehen-liste '(0 1000 2000 3000 4000 5000)) + ;; Testdaten aus JSON laden + (setq json-datei + (strcat (vl-string-translate "\\" "/" (getenv "DXFMAKRO")) + "/tests/testdata/foerderer_tests.json")) + (setq testdaten (ssg-load-json json-datei)) + (if (null testdaten) + (progn + (alert (strcat "Testdaten nicht gefunden:\n" json-datei)) + (ssg-end) + (exit) + ) + ) + + ;; Konfiguration aus erstem Eintrag (ohne test_id) + (setq cfg (car testdaten)) + (setq y-abstand (ssg-val cfg "y_abstand")) + (if (or (null y-abstand) (= y-abstand 0)) (setq y-abstand 400)) + + ;; Nur Eintraege mit test_id sind Testfaelle + (setq tests + (vl-remove-if-not + '(lambda (e) (ssg-val e "test_id")) + testdaten)) + (setq y-offset 0) (setq idx 0) (setq results-list '()) @@ -99,66 +120,26 @@ (princ "\n================================================================") (princ "\n TEST_FOERDERER - Variofoerderer Integrationstest") (princ "\n================================================================") - (princ (strcat "\n Distanz dL = " (rtos deltaL 2 0) " mm")) - (princ "\n Hoehenstufen: 0, 1000, 2000, 3000, 4000, 5000 mm") - (princ "\n Richtungen: Auf + Ab") + (princ (strcat "\n Testdaten: " json-datei)) + (princ (strcat "\n Testfaelle: " (itoa (length tests)))) + (princ (strcat "\n Y-Abstand: " (rtos y-abstand 2 0) " mm")) (princ "\n================================================================") ;; ========================================================== - ;; TEIL 1: Aufwaerts-Tests (6 Foerderer) + ;; Alle Testfaelle aus JSON ausfuehren ;; ========================================================== - (foreach deltaH hoehen-liste + (foreach eintrag tests + (setq test-id (ssg-val eintrag "test_id")) + (setq richtung (ssg-val eintrag "richtung")) + (setq deltaL (float (ssg-val eintrag "deltaL"))) + (setq deltaH (float (ssg-val eintrag "deltaH"))) (setq idx (1+ idx)) - (setq richtung "Auf") - (setq test-id (strcat "VF_Auf_" (rtos deltaH 2 0))) - (setq ergebnis (berechne-alle-winkel deltaL deltaH richtung)) - (setq best-winkel (car ergebnis)) - (setq L_GF (cadr ergebnis)) - (setq L_VF (caddr ergebnis)) - - (if (and best-winkel L_GF L_VF (> L_GF 0) (> L_VF 0)) - (progn - (setq anz-gebaut (1+ anz-gebaut)) - (princ (strcat "\n " (itoa idx) ". " test-id " -> GEBAUT " - (itoa best-winkel) " Grad L_GF=" (rtos L_GF 2 1) - " L_VF=" (rtos L_VF 2 1))) - - (variofoerderer-einfuegen deltaL deltaH richtung best-winkel - (/ L_GF 2.0) (/ L_GF 2.0) L_VF - (list 0 y-offset 0) "links" idx 0) - - (setq results-list (cons - (foerderer:test-result-json test-id richtung deltaL deltaH - "GEBAUT" best-winkel L_GF L_VF) - results-list)) - ) - (progn - (setq anz-nicht-gebaut (1+ anz-nicht-gebaut)) - (princ (strcat "\n " (itoa idx) ". " test-id " -> NICHT_GEBAUT")) - - (setq results-list (cons - (foerderer:test-result-json test-id richtung deltaL deltaH - "NICHT_GEBAUT" nil nil nil) - results-list)) - ) - ) - (setq y-offset (- y-offset 200)) - ) - - ;; ========================================================== - ;; TEIL 2: Abwaerts-Tests (6 Foerderer) - ;; ========================================================== - (foreach deltaH hoehen-liste - (setq idx (1+ idx)) - (setq richtung "Ab") - (setq test-id (strcat "VF_Ab_" (rtos deltaH 2 0))) - - (if (< deltaH 1) + ;; Geometrisch unmoeglich: Ab mit deltaH=0 + (if (and (= richtung "Ab") (< deltaH 1)) (progn (setq anz-nicht-gebaut (1+ anz-nicht-gebaut)) (princ (strcat "\n " (itoa idx) ". " test-id " -> GEOMETRISCH_UNMOEGLICH")) - (setq results-list (cons (foerderer:test-result-json test-id richtung deltaL deltaH "GEOMETRISCH_UNMOEGLICH" nil nil nil) @@ -177,9 +158,17 @@ (itoa best-winkel) " Grad L_GF=" (rtos L_GF 2 1) " L_VF=" (rtos L_VF 2 1))) + (setq startpunkt (list 0 y-offset 0)) + (setq vf-nummer idx) + (setq hoehe-von 0) + (setq hoehe-bis (if (= richtung "Auf") deltaH (- deltaH))) + (setq lastEnt (entlast)) (variofoerderer-einfuegen deltaL deltaH richtung best-winkel (/ L_GF 2.0) (/ L_GF 2.0) L_VF - (list 0 y-offset 0) "links" idx 0) + startpunkt "links") + (vf-block-erstellen "standard" "links" vf-nummer 0 + hoehe-von hoehe-bis deltaH deltaL L_VF L_GF + richtung best-winkel startpunkt lastEnt) (setq results-list (cons (foerderer:test-result-json test-id richtung deltaL deltaH @@ -189,7 +178,6 @@ (progn (setq anz-nicht-gebaut (1+ anz-nicht-gebaut)) (princ (strcat "\n " (itoa idx) ". " test-id " -> NICHT_GEBAUT")) - (setq results-list (cons (foerderer:test-result-json test-id richtung deltaL deltaH "NICHT_GEBAUT" nil nil nil) @@ -198,7 +186,7 @@ ) ) ) - (setq y-offset (- y-offset 200)) + (setq y-offset (- y-offset y-abstand)) ) ;; ========================================================== diff --git a/tests/testdata/foerderer_tests.json b/tests/testdata/foerderer_tests.json new file mode 100644 index 0000000..091157f --- /dev/null +++ b/tests/testdata/foerderer_tests.json @@ -0,0 +1,15 @@ +[ + { "y_abstand": 400 }, + { "test_id": "VF_Auf_0", "richtung": "Auf", "deltaL": 7000, "deltaH": 0 }, + { "test_id": "VF_Auf_1000", "richtung": "Auf", "deltaL": 7000, "deltaH": 1000 }, + { "test_id": "VF_Auf_2000", "richtung": "Auf", "deltaL": 7000, "deltaH": 2000 }, + { "test_id": "VF_Auf_3000", "richtung": "Auf", "deltaL": 7000, "deltaH": 3000 }, + { "test_id": "VF_Auf_4000", "richtung": "Auf", "deltaL": 7000, "deltaH": 4000 }, + { "test_id": "VF_Auf_5000", "richtung": "Auf", "deltaL": 7000, "deltaH": 5000 }, + { "test_id": "VF_Ab_0", "richtung": "Ab", "deltaL": 7000, "deltaH": 0 }, + { "test_id": "VF_Ab_1000", "richtung": "Ab", "deltaL": 7000, "deltaH": 1000 }, + { "test_id": "VF_Ab_2000", "richtung": "Ab", "deltaL": 7000, "deltaH": 2000 }, + { "test_id": "VF_Ab_3000", "richtung": "Ab", "deltaL": 7000, "deltaH": 3000 }, + { "test_id": "VF_Ab_4000", "richtung": "Ab", "deltaL": 7000, "deltaH": 4000 }, + { "test_id": "VF_Ab_5000", "richtung": "Ab", "deltaL": 7000, "deltaH": 5000 } +] From 2009eb1a2c6e74ae8cb4a9a25eb2fb10283aa245 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 11:04:50 +0200 Subject: [PATCH 3/6] =?UTF-8?q?Readme=20der=20lisp=20Tests=20aktualisiert.?= =?UTF-8?q?=20Foerderer=20Tests=20korrigiert.=20Flaches=20.json=20ben?= =?UTF-8?q?=C3=B6tigt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/LispTests.md | 404 +++++++++++++++------------- tests/test_foerderer.lsp | 17 +- tests/testdata/foerderer_tests.json | 88 +++++- 3 files changed, 305 insertions(+), 204 deletions(-) diff --git a/doc/LispTests.md b/doc/LispTests.md index 840707c..7803cdb 100644 --- a/doc/LispTests.md +++ b/doc/LispTests.md @@ -5,234 +5,281 @@ Die SSG_LIB AutoLISP-Makros werden ueber einen zweistufigen Hybrid-Ansatz getestet: 1. **AutoLISP** (in BricsCAD): Liest JSON-Testfaelle, fuehrt die Funktionen aus, - schreibt Ergebnisse als JSON und DXF. + schreibt Ergebnisse als JSON und DXF/DWG. 2. **Python** (pytest + ezdxf): Validiert die Ergebnisse automatisch gegen - erwartete Werte und optionale Referenz-DXFs. + erwartete Werte und optionale Referenzdaten. -``` +```text Python (pytest) AutoLISP (BricsCAD) Python (pytest) =============== ==================== =============== Testfall-Definitionen --> Ausfuehrung der --> Validierung: als JSON bereitstellen LISP-Funktionen - JSON-Ergebnisse - + DXF + results.json - DXF-Geometrie + + DXF/DWG + results.json - DXF-Geometrie - Referenz-Vergleich ``` ## Verzeichnisstruktur -``` +```text tests/ -├── testdata/ # JSON-Testfall-Definitionen (versioniert) -│ ├── kreisel_tests.json -│ ├── vario_tests.json -│ └── omniflo_tests.json -├── reference/ # Abgenommene Referenz-DXFs (versioniert) -│ └── kreisel_ref.dxf -├── output/ # Generierte Ergebnisse (in .gitignore) -│ ├── kreisel_tests.dxf -│ └── kreisel_results.json -├── ssg_testrunner.lsp # LISP Test-Runner +├── alltests.json # Zentrale Testregistry (welche Module + Speicherformat) +├── test_run_all.lsp # C:SSG_RUN_ALL_TESTS - fuehrt alle Tests aus +├── test_kreisel.lsp # C:TEST_KREISEL - Kreisel-Testrunner +├── test_foerderer.lsp # C:TEST_FOERDERER - Variofoerderer-Testrunner +├── test_kseinaus.lsp # C:TEST_KSEINAUS - KreiselEinAus-Testrunner +├── test_omniflo.lsp # C:TEST_OMNIFLO - Omniflo-Testrunner ├── conftest.py # pytest Fixtures ├── test_kreisel.py # pytest Kreisel-Validierung -├── test_vario.py # pytest Vario-Validierung ├── test_omniflo.py # pytest Omniflo-Validierung -└── requirements.txt # Python-Abhaengigkeiten (ezdxf, pytest) +├── create_testbase.py # Hilfsskript (optional, i.d.R. nicht benoetigt) +├── requirements.txt # Python-Abhaengigkeiten (ezdxf, pytest) +├── testdata/ # JSON-Testfall-Definitionen (versioniert) +│ ├── kreisel_tests.json +│ ├── foerderer_tests.json +│ └── omniflo_tests.json +├── reference/ # Abgenommene Referenzdaten (versioniert) +│ └── omniflo_export_reference.csv +└── output/ # Generierte Ergebnisse (nicht in Git) + ├── kreisel_results.json + ├── kreisel_tests.dxf + ├── foerderer_results.json + ├── foerderer_tests.dwg + ├── kseinaus_results.json + ├── omniflo_results.json + ├── omniflo_tests.dxf + └── omniflo_export.csv ``` +## Testregistry (alltests.json) + +Die Datei `tests/alltests.json` ist die zentrale Registry aller Testmodule. +`SSG_RUN_ALL_TESTS` liest sie beim Start — die Testliste ist nicht hartcodiert. + +```json +[ + { "name": "kreisel", "save": "dxf", "module": "KreiselInsert" }, + { "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" }, + { "name": "kseinaus", "save": null, "module": "VarioFoerderer" }, + { "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" } +] +``` + +| Feld | Typ | Bedeutung | +| --- | --- | --- | +| `name` | String | Basisname des Testmoduls (Pflicht) | +| `save` | `"dxf"` / `"dwg"` / `null` | Speicherformat der Zeichnung | +| `module` | String | LISP-Modul (wird von ssg-ensure geladen) | + +Aus dem `name` werden alle Dateinamen abgeleitet: + +| Element | Schema | Beispiel (`kreisel`) | +| --- | --- | --- | +| LISP-Datei | `tests/test_.lsp` | `tests/test_kreisel.lsp` | +| BricsCAD-Befehl | `TEST_` | `TEST_KREISEL` | +| Export-Funktion | `:export-results` | `kreisel:export-results` | +| JSON-Ergebnis | `output/_results.json` | `output/kreisel_results.json` | +| DXF/DWG-Datei | `output/_tests.dxf/.dwg` | `output/kreisel_tests.dxf` | + ## Schritt 1: Testfaelle definieren (JSON) -Jedes Modul hat eine eigene JSON-Datei in `tests/testdata/`. Aufbau: +Jedes Modul hat eine eigene JSON-Datei in `tests/testdata/`. Format: **flaches JSON-Array** +(kompatibel mit `omni:load-json`). Kein Nesting, keine Wrapper-Objekte. + +### Kreisel-Format (kreisel_tests.json) ```json -{ - "module": "Kreisel", - "version": "1.0", - "description": "ILS Kreisel Testfaelle", - "tests": [ - { - "id": "KR_Horiz_ObenUnten", - "function": "draw-module", - "params": { - "basepoint": [500.0, 5000.0, 2500.0], - "abstand": 4200.0, - "rotation": 270.0, - "attribs": { - "KREISELART": "STANDARD", - "NAME": "TEST_H_ObenUnten" - } - }, - "expect": { - "attributes": { - "KREISELART": "STANDARD", - "DREHUNG": "270", - "ABSTAND": "4200" - }, - "block_prefix": "KR_", - "min_entities": 6 - } - } - ] -} +[ + { + "id": "KR_Kreuz_Q1_0Grad", + "function": "insert", + "x": 500, "y": 500, "z": 2500, + "abstand": 2000, + "rotation": 0.0, + "typ": "STANDARD", + "expect_block_prefix": "KREISEL_", + "expect_hoehe": "2500", + "expect_kreiselart": "STANDARD" + } +] ``` -### Felder +Felder: -| Feld | Beschreibung | -|---|---| -| `id` | Eindeutiger Testfall-Name | -| `function` | Aufzurufende LISP-Funktion | -| `params` | Uebergabewerte an die Funktion | -| `expect.attributes` | Erwartete Block-Attribute nach Ausfuehrung | -| `expect.block_prefix` | Erwartetes Praefix des erzeugten Block-Namens | -| `expect.min_entities` | Mindestanzahl Entities im erzeugten Block | +- `id` — Eindeutiger Testfall-Name +- `function` — `"insert"` oder `"connect"` +- Eingabeparameter: `x`, `y`, `z`, `abstand`, `rotation`, `typ` +- `expect_*` — Erwartete Ergebniswerte (fuer Python-Validierung) -## Schritt 2: LISP Test-Runner (ssg_testrunner.lsp) +### Foerderer-Format (foerderer_tests.json) -Der LISP Test-Runner liest die JSON-Testfaelle, fuehrt die Funktionen in -BricsCAD aus und schreibt die Ergebnisse. - -### Aufruf in BricsCAD - -``` -Command: TESTRUN "kreisel" +```json +[ + { "y_abstand": 400 }, + { + "test_id": "VF_Auf_0", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 0 + } +] ``` -Der Befehl: -1. Liest `tests/testdata/kreisel_tests.json` -2. Erstellt eine neue leere Zeichnung -3. Fuehrt fuer jeden Testfall die angegebene Funktion mit den Parametern aus -4. Sammelt Ergebnisse: Block-Handle, tatsaechliche Attribute, Insertionspunkt -5. Schreibt `tests/output/kreisel_results.json` -6. Speichert die Zeichnung als `tests/output/kreisel_tests.dxf` +- Erster Eintrag (ohne `test_id`): Konfiguration (`y_abstand` zwischen Foerderern) +- Felder: `test_id`, `richtung` (`"Auf"` / `"Ab"`), `deltaL`, `deltaH` +- Ergebnisstatus: `GEBAUT`, `NICHT_GEBAUT`, `GEOMETRISCH_UNMOEGLICH` + +### Omniflo-Format (omniflo_tests.json) + +```json +[ + { + "id": "821104025", + "type": "bogen", + "sivasnr": "821104025", + "x": 219.4, "y": 223.06, + "hoehe": 2000, + "drehung": 0.0, + "description": "APB 110 R 550/22,5 – 84/522", + "row": "Boegen 22.5" + } +] +``` + +## Schritt 2: LISP-Testrunner ausfuehren (BricsCAD) + +### Alle Tests auf einmal + +```lisp +(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) +SSG_RUN_ALL_TESTS +``` + +`SSG_RUN_ALL_TESTS` liest `alltests.json` und fuehrt fuer jedes Modul aus: + +1. Zeichnung leeren (ERASE ALL + PURGE x5) +2. `test_.lsp` laden und `TEST_` ausfuehren +3. JSON-Export: `:export-results` aufrufen (immer) +4. Zeichnung speichern gemaess `save`-Feld (`"dxf"`, `"dwg"` oder kein Speichern) +5. Bei Fehler: weiter mit naechstem Test (Fehlerisolation) + +### Einzeltest (z.B. nur Kreisel) + +```lisp +(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) +TEST_KREISEL +(kreisel:export-results (strcat (getenv "DXFMAKRO") "/tests/output")) +``` ### Ausgabe: results.json ```json [ { - "test_id": "KR_Horiz_ObenUnten", - "status": "executed", - "block_handle": "A3F", - "block_name": "KR_STANDARD_01", - "insert_point": [500.0, 5000.0, 2500.0], - "rotation": 270.0, - "actual_attributes": { - "KREISELART": "STANDARD", - "DREHUNG": "270", - "ABSTAND": "4200", - "NAME": "TEST_H_ObenUnten" - } + "test_id": "KR_Kreuz_Q1_0Grad", + "status": "GEBAUT", + "block_name": "KREISEL_STANDARD", + "insert_point": [500.0, 500.0, 2500.0], + "rotation": 0.0, + "actual_kreiselart": "STANDARD", + "actual_hoehe": "2500" } ] ``` ## Schritt 3: Python-Validierung (pytest) -Die Python-Tests lesen `results.json` und die erzeugte DXF-Datei und pruefen: +```cmd +bin\run_tests.bat --runall +``` -### 3a. Attribut-Pruefung (results.json) +Oder einzelnes Testmodul: -Vergleicht `actual_attributes` aus results.json gegen `expect.attributes` -aus den Testdefinitionen. Keine BricsCAD-Instanz noetig. +```cmd +bin\run_tests.bat --runall test_kreisel.py +``` -### 3b. Geometrie-Pruefung (ezdxf) +### test_kreisel.py -Oeffnet die erzeugte DXF-Datei mit der Python-Bibliothek `ezdxf` und prueft: +| Klasse | Prueft | +| --- | --- | +| `TestKreiselAttributes` | Attribute (KREISELART, HOEHE), Block-Prefixe, Status | +| `TestKreiselGeometry` | DXF-Bloecke, Positionen, Kreis-Radien (400mm) | +| `TestKreiselReference` | Block-Anzahl und -Typen gegen Referenz-DXF | -- **Block-Existenz**: Alle erwarteten Bloecke im Modelspace vorhanden -- **Entity-Anzahl**: Mindestanzahl Entities pro Block -- **Kreisradien**: AN8/SP8-Kreise haben korrekten Radius (400mm) -- **Linienlaengen**: Tangenten haben korrekte Laenge (= Abstand) -- **Layer-Zuordnung**: Entities auf korrektem Layer -- **Insertionspunkte**: Bloecke an erwarteter Position +### test_omniflo.py -### 3c. Referenz-Vergleich +| Klasse | Prueft | +| --- | --- | +| `TestOmnifloExportUnit` | Mock-Export: Merkmale, Datentypen, Vollstaendigkeit (ohne BricsCAD) | +| `TestOmnifloExportCSV` | CSV aus BricsCAD: Header, Spalten, JSON-Merkmale | -Vergleicht die Ergebnis-DXF gegen eine abgenommene Referenz-DXF: - -- Gleiche Anzahl Bloecke im Modelspace -- Gleiche Block-Typen -- Gleiche Attributwerte +Tests, deren Eingabedaten fehlen (kein BricsCAD-Lauf), werden automatisch +uebersprungen (`pytest.skip`). ## Schritt 4: Referenz-Management -### Neue Referenz erstellen (erster Lauf oder nach Aenderung) - -```bash -# 1. Tests in BricsCAD ausfuehren -# Command: TESTRUN "kreisel" - -# 2. Ergebnis-DXF visuell pruefen (in BricsCAD oeffnen) - -# 3. Wenn OK: Als neue Referenz speichern -copy tests\output\kreisel_tests.dxf tests\reference\kreisel_ref.dxf +```cmd +# Aktuelle Ergebnisse als neue Referenz speichern +bin\run_tests.bat --set-as-reference ``` -### Referenz aktualisieren +Kopiert alle Dateien aus `output/` nach `reference/`. Nur bei bestandenen Tests verwenden. -Wenn sich die LISP-Logik aendert und die neuen Ergebnisse korrekt sind: +## Modulspezifische Testfaelle -```bash -# 1. Tests in BricsCAD ausfuehren -# 2. Ergebnis visuell pruefen -# 3. Python-Tests zeigen Referenz-Abweichung: -pytest tests/test_kreisel.py -v -# FAILED test_block_count_matches_reference - 6 != 5 +### Kreisel (kreisel_tests.json) -# 4. Neue Referenz speichern -copy tests\output\kreisel_tests.dxf tests\reference\kreisel_ref.dxf +| Test-ID | Beschreibung | Params | +| --- | --- | --- | +| KR_Kreuz_Q1_0Grad | Standard Kreisel, 0 Grad | rot=0, z=2500, abstand=2000 | +| KR_Kreuz_Q2_90Grad | Standard Kreisel, 90 Grad | rot=90, z=2500, abstand=3000 | +| KR_Kreuz_Q3_180Grad | Standard Kreisel, 180 Grad | rot=180, z=2500, abstand=4000 | +| KR_Kreuz_Q4_270Grad | Standard Kreisel, 270 Grad | rot=270, z=2500, abstand=5000 | +| KR_Schraeg_Q1_30Grad | Standard Kreisel, schraeg | rot=30, z=3000, abstand=3000 | +| KR_Pin_Q1_0Grad | Pin-Kreisel, 0 Grad | rot=0, typ=PIN, z=1500 | +| KR_Pin_Q1_90Grad | Pin-Kreisel, 90 Grad | rot=90, typ=PIN, z=1500 | +| KR_Pin_Q1_30Grad_Schraeg | Pin-Kreisel, schraeg | rot=30, typ=PIN, z=1500 | -# 5. Tests erneut ausfuehren - jetzt PASS -pytest tests/test_kreisel.py -v -``` +### Foerderer (foerderer_tests.json) -## Ausfuehrung +| Test-ID | Richtung | deltaL | deltaH | +| --- | --- | --- | --- | +| VF_Auf_0 | Auf | 7000 | 0 | +| VF_Auf_1000 | Auf | 7000 | 1000 | +| VF_Auf_2000 | Auf | 7000 | 2000 | +| VF_Auf_3000 | Auf | 7000 | 3000 | +| VF_Auf_4000 | Auf | 7000 | 4000 | +| VF_Auf_5000 | Auf | 7000 | 5000 | +| VF_Ab_0 | Ab | 7000 | 0 (GEOMETRISCH_UNMOEGLICH) | +| VF_Ab_1000 | Ab | 7000 | 1000 | +| VF_Ab_2000 | Ab | 7000 | 2000 | +| VF_Ab_3000 | Ab | 7000 | 3000 | +| VF_Ab_4000 | Ab | 7000 | 4000 | +| VF_Ab_5000 | Ab | 7000 | 5000 | -### Kompletter Testlauf +### Omniflo (omniflo_tests.json) -```bash -# Umgebung setzen -bin\setenv.bat - -# 1. Testdaten-DXF erzeugen (Python, optional wenn Basis-DXF benoetigt) -python tests/create_testbase.py - -# 2. BricsCAD starten und Tests ausfuehren -# In BricsCAD: TESTRUN "kreisel" -# In BricsCAD: TESTRUN "vario" -# In BricsCAD: TESTRUN "omniflo" - -# 3. Python-Validierung -pytest tests/ -v -``` - -### Nur Python-Validierung (ohne BricsCAD) - -```bash -# Setzt voraus, dass output/ bereits erzeugt wurde -pytest tests/ -v -``` - -### Einzelnes Modul testen - -```bash -pytest tests/test_kreisel.py -v -``` +Testfaelle basieren auf echten Sivas-Nummern (Boegen und Zusatzmodule). +Beispiele: `821104025`, `821104025+0_B10090`, `821104025+0_B10091`, ... ## Umgebungsvariablen -| Variable | Pfad | Beschreibung | -|---|---|---| -| `DXFM_TESTS` | `/tests` | Test-Wurzelverzeichnis | -| `DXFM_TESTDATA` | `/tests/testdata` | JSON-Testdefinitionen | -| `DXFM_TESTOUT` | `/tests/output` | Generierte Ergebnisse | -| `DXFM_TESTREF` | `/tests/reference` | Abgenommene Referenzen | +Werden via `bin\setenv.bat` gesetzt. Fuer Tests relevant: + +| Variable | Beschreibung | +| --- | --- | +| `DXFMAKRO` | Projektwurzel | +| `DXFM_TESTS` | tests/-Verzeichnis | +| `DXFM_TESTDATA` | testdata/-Verzeichnis | +| `DXFM_TESTOUT` | output/-Verzeichnis | +| `DXFM_TESTREF` | reference/-Verzeichnis | +| `DXFM_OMNIFLO` | Pfad zu Omniflo-DXF-Dateien | ## Abhaengigkeiten ### Python (tests/requirements.txt) -``` +```text ezdxf>=1.0 pytest>=7.0 ``` @@ -240,34 +287,13 @@ pytest>=7.0 ### AutoLISP - SSG_LIB muss geladen sein (Menue per `_MENULOAD` eingerichtet, siehe CLAUDE.md) -- `ssg_testrunner.lsp` wird manuell per `(load "...")` oder ueber das Menue geladen +- `test_run_all.lsp` laedt die einzelnen Testrunner automatisch -## Modulspezifische Testfaelle +## Neuen Test erstellen -### Kreisel (kreisel_tests.json) +1. **Testdaten**: `testdata/_tests.json` erstellen (flaches JSON-Array) +2. **LISP-Testrunner**: `test_.lsp` erstellen mit `c:TEST_` und `:export-results` +3. **Testregistry**: Eintrag in `alltests.json` hinzufuegen +4. **Python-Validierung** (optional): `test_.py` und Fixtures in `conftest.py` -| Test-ID | Beschreibung | Params | -|---|---|---| -| KR_Horiz_ObenUnten | Horizontaler Kreisel, oben nach unten | rot=270, art=STANDARD | -| KR_Horiz_UntenOben | Horizontaler Kreisel, unten nach oben | rot=90, art=PIN | -| KR_Vert_LinksRechts | Vertikaler Kreisel, links nach rechts | rot=0, art=PIN | -| KR_Vert_RechtsLinks | Vertikaler Kreisel, rechts nach links | rot=180, art=STANDARD | -| KR_Insert_Schraeg | Kreisel mit 10 Grad Drehung | rot=10, abstand=6000 | - -### Vario (vario_tests.json) - -| Test-ID | Beschreibung | Params | -|---|---|---| -| VF_Gerade_1Segment | Einzelnes gerades Segment | laenge=2000 | -| VF_Gerade_3Segmente | Drei gerade Segmente | laenge=6000 | -| VF_Gefaelle_Standard | Gefaellestrecke Standard | hoehe=500 | -| VF_Bogen_Links | Linksbogen | winkel=90 | -| VF_Bogen_Rechts | Rechtsbogen | winkel=-90 | - -### Omniflo (omniflo_tests.json) - -| Test-ID | Beschreibung | Params | -|---|---|---| -| OM_Bogen_90 | 90-Grad Bogen | winkel=90 | -| OM_Weiche_Links | Linksweiche | seite=L | -| OM_Weiche_Rechts | Rechtsweiche | seite=R | +Detailliertes Vorgehen und Codevorlagen: siehe `tests/README.md`. diff --git a/tests/test_foerderer.lsp b/tests/test_foerderer.lsp index e112d9f..32b13e4 100644 --- a/tests/test_foerderer.lsp +++ b/tests/test_foerderer.lsp @@ -86,13 +86,25 @@ (setvar "ATTREQ" 0) (setvar "ATTDIA" 0) + ;; Debug-Logging starten + (dbgopen "test_foerderer.dbg" "DXFM_LOG") + ;; Testdaten aus JSON laden (setq json-datei - (strcat (vl-string-translate "\\" "/" (getenv "DXFMAKRO")) - "/tests/testdata/foerderer_tests.json")) + (strcat (getenv "DXFMAKRO") + "\\tests\\testdata\\foerderer_tests.json")) + + (dbgmsg (strcat "DXFMAKRO=" (if (getenv "DXFMAKRO") (getenv "DXFMAKRO") "nil"))) + (dbgmsg (strcat "json-datei=" json-datei)) + (dbgmsg (strcat "file-exists=" (if (findfile json-datei) "JA" "NEIN"))) + (dbgflush) + (setq testdaten (ssg-load-json json-datei)) (if (null testdaten) (progn + (dbgmsg "FEHLER: testdaten ist nil nach ssg-load-json") + (dbgflush) + (dbgclose) (alert (strcat "Testdaten nicht gefunden:\n" json-datei)) (ssg-end) (exit) @@ -200,6 +212,7 @@ ;; Ergebnisse in globaler Variable speichern (setq *foerderer-test-results* (reverse results-list)) + (dbgclose) (ssg-end) (princ "\n================================================================") (princ "\n TEST_FOERDERER abgeschlossen.") diff --git a/tests/testdata/foerderer_tests.json b/tests/testdata/foerderer_tests.json index 091157f..84e7692 100644 --- a/tests/testdata/foerderer_tests.json +++ b/tests/testdata/foerderer_tests.json @@ -1,15 +1,77 @@ [ - { "y_abstand": 400 }, - { "test_id": "VF_Auf_0", "richtung": "Auf", "deltaL": 7000, "deltaH": 0 }, - { "test_id": "VF_Auf_1000", "richtung": "Auf", "deltaL": 7000, "deltaH": 1000 }, - { "test_id": "VF_Auf_2000", "richtung": "Auf", "deltaL": 7000, "deltaH": 2000 }, - { "test_id": "VF_Auf_3000", "richtung": "Auf", "deltaL": 7000, "deltaH": 3000 }, - { "test_id": "VF_Auf_4000", "richtung": "Auf", "deltaL": 7000, "deltaH": 4000 }, - { "test_id": "VF_Auf_5000", "richtung": "Auf", "deltaL": 7000, "deltaH": 5000 }, - { "test_id": "VF_Ab_0", "richtung": "Ab", "deltaL": 7000, "deltaH": 0 }, - { "test_id": "VF_Ab_1000", "richtung": "Ab", "deltaL": 7000, "deltaH": 1000 }, - { "test_id": "VF_Ab_2000", "richtung": "Ab", "deltaL": 7000, "deltaH": 2000 }, - { "test_id": "VF_Ab_3000", "richtung": "Ab", "deltaL": 7000, "deltaH": 3000 }, - { "test_id": "VF_Ab_4000", "richtung": "Ab", "deltaL": 7000, "deltaH": 4000 }, - { "test_id": "VF_Ab_5000", "richtung": "Ab", "deltaL": 7000, "deltaH": 5000 } + { + "y_abstand": 400 + }, + { + "test_id": "VF_Auf_0", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 0 + }, + { + "test_id": "VF_Auf_1000", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 1000 + }, + { + "test_id": "VF_Auf_2000", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 2000 + }, + { + "test_id": "VF_Auf_3000", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 3000 + }, + { + "test_id": "VF_Auf_4000", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 4000 + }, + { + "test_id": "VF_Auf_5000", + "richtung": "Auf", + "deltaL": 7000, + "deltaH": 5000 + }, + { + "test_id": "VF_Ab_0", + "richtung": "Ab", + "deltaL": 7000, + "deltaH": 0 + }, + { + "test_id": "VF_Ab_1000", + "richtung": "Ab", + "deltaL": 7000, + "deltaH": 1000 + }, + { + "test_id": "VF_Ab_2000", + "richtung": "Ab", + "deltaL": 7000, + "deltaH": 2000 + }, + { + "test_id": "VF_Ab_3000", + "richtung": "Ab", + "deltaL": 7000, + "deltaH": 3000 + }, + { + "test_id": "VF_Ab_4000", + "richtung": "Ab", + "deltaL": 7000, + "deltaH": 4000 + }, + { + "test_id": "VF_Ab_5000", + "richtung": "Ab", + "deltaL": 7000, + "deltaH": 5000 + } ] From 9b2bfed255672c02a03140df510a6c05a572e420 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 11:05:16 +0200 Subject: [PATCH 4/6] Debug Erzeugung aus dem Foerderer lisp Test entfernt --- tests/test_foerderer.lsp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test_foerderer.lsp b/tests/test_foerderer.lsp index 32b13e4..a8da863 100644 --- a/tests/test_foerderer.lsp +++ b/tests/test_foerderer.lsp @@ -86,25 +86,13 @@ (setvar "ATTREQ" 0) (setvar "ATTDIA" 0) - ;; Debug-Logging starten - (dbgopen "test_foerderer.dbg" "DXFM_LOG") - ;; Testdaten aus JSON laden (setq json-datei (strcat (getenv "DXFMAKRO") "\\tests\\testdata\\foerderer_tests.json")) - - (dbgmsg (strcat "DXFMAKRO=" (if (getenv "DXFMAKRO") (getenv "DXFMAKRO") "nil"))) - (dbgmsg (strcat "json-datei=" json-datei)) - (dbgmsg (strcat "file-exists=" (if (findfile json-datei) "JA" "NEIN"))) - (dbgflush) - (setq testdaten (ssg-load-json json-datei)) (if (null testdaten) (progn - (dbgmsg "FEHLER: testdaten ist nil nach ssg-load-json") - (dbgflush) - (dbgclose) (alert (strcat "Testdaten nicht gefunden:\n" json-datei)) (ssg-end) (exit) @@ -212,7 +200,6 @@ ;; Ergebnisse in globaler Variable speichern (setq *foerderer-test-results* (reverse results-list)) - (dbgclose) (ssg-end) (princ "\n================================================================") (princ "\n TEST_FOERDERER abgeschlossen.") From b7025e5523bc6bd403d66c31e6cf70bcbddb3180 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 11:19:01 +0200 Subject: [PATCH 5/6] =?UTF-8?q?aktuelle=20OMniflo=20Export=20Liste=20als?= =?UTF-8?q?=20Referenz=20f=C3=BCr=20den=20Test=20eingestellt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/reference/omniflo_export_reference.csv | 108 +++++++++++++++++++ tests/test_foerderer.lsp | 18 +++- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/reference/omniflo_export_reference.csv diff --git a/tests/reference/omniflo_export_reference.csv b/tests/reference/omniflo_export_reference.csv new file mode 100644 index 0000000..2c2c2d7 --- /dev/null +++ b/tests/reference/omniflo_export_reference.csv @@ -0,0 +1,108 @@ +Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale +1;"Omniflo Kurve";"0000";"OFBogen :1";1;{"Kurvenwinkel": 22.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104025"} +2;"Omniflo Kurve";"0000";"OFBogen :2";1;{"Kurvenwinkel": 22.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104025+0_B10090"} +3;"Omniflo Kurve";"0000";"OFBogen :3";1;{"Kurvenwinkel": 22.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104025+0_B10091"} +4;"Omniflo Kurve";"0000";"OFBogen :4";1;{"Kurvenwinkel": 45.0, "Radius": 400.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104021"} +5;"Omniflo Kurve";"0000";"OFBogen :5";1;{"Kurvenwinkel": 45.0, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104026"} +6;"Omniflo Kurve";"0000";"OFBogen :6";1;{"Kurvenwinkel": 45.0, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104026+0_B10071"} +7;"Omniflo Kurve";"0000";"OFBogen :7";1;{"Kurvenwinkel": 45.0, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104037"} +8;"Omniflo Kurve";"0000";"OFBogen :8";1;{"Kurvenwinkel": 45.0, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104029"} +9;"Omniflo Kurve";"0000";"OFBogen :9";1;{"Kurvenwinkel": 45.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104031"} +10;"Omniflo Kurve";"0000";"OFBogen :10";1;{"Kurvenwinkel": 45.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104031+0_B10060"} +11;"Omniflo Kurve";"0000";"OFBogen :11";1;{"Kurvenwinkel": 45.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104031+0_B10070"} +12;"Omniflo Kurve";"0000";"OFBogen :12";1;{"Kurvenwinkel": 67.5, "Radius": 400.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104024"} +13;"Omniflo Kurve";"0000";"OFBogen :13";1;{"Kurvenwinkel": 67.5, "Radius": 400.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104024+0_B10081"} +14;"Omniflo Kurve";"0000";"OFBogen :14";1;{"Kurvenwinkel": 67.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104027"} +15;"Omniflo Kurve";"0000";"OFBogen :15";1;{"Kurvenwinkel": 67.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104027+0_B10080"} +16;"Omniflo Kurve";"0000";"OFBogen :16";1;{"Kurvenwinkel": 67.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104028"} +17;"Omniflo Kurve";"0000";"OFBogen :17";1;{"Kurvenwinkel": 67.5, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104065"} +18;"Omniflo Kurve";"0000";"OFBogen :18";1;{"Kurvenwinkel": 90.0, "Radius": 400.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104022"} +19;"Omniflo Kurve";"0000";"OFBogen :19";1;{"Kurvenwinkel": 90.0, "Radius": 500.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104030"} +20;"Omniflo Kurve";"0000";"OFBogen :20";1;{"Kurvenwinkel": 90.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104033"} +21;"Omniflo Kurve";"0000";"OFBogen :21";1;{"Kurvenwinkel": 90.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104033+0_B10040"} +22;"Omniflo Kurve";"0000";"OFBogen :22";1;{"Kurvenwinkel": 90.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104033+0_B10050"} +23;"Omniflo Kurve";"0000";"OFBogen :23";1;{"Kurvenwinkel": 90.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104035"} +24;"Omniflo Kurve";"0000";"OFBogen :24";1;{"Kurvenwinkel": 90.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104041"} +25;"Omniflo Kurve";"0000";"OFBogen :25";1;{"Kurvenwinkel": 90.0, "Radius": 630.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104034"} +26;"Omniflo Kurve";"0000";"OFBogen :26";1;{"Kurvenwinkel": 90.0, "Radius": 550.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104066"} +27;"Omniflo Kurve";"0000";"OFBogen :27";1;{"Kurvenwinkel": 180.0, "Radius": 650.0, "Höhe": "2000", "Drehung": 0.0, "SivasNummer": "821104043"} +28;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342011"} +29;"Omniflo Weiche";"0000";"OFWeiche :2";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342001"} +30;"Omniflo Weiche";"0000";"OFWeiche :3";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342012"} +31;"Omniflo Weiche";"0000";"OFWeiche :4";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342002"} +32;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342100"} +33;"Omniflo Weiche";"0000";"OFWeiche :2";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342101"} +34;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342200"} +35;"Omniflo Weiche";"0000";"OFWeiche :2";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 22.5, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834342201"} +36;"Omniflo Weiche";"0000";"OFWeiche :5";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372001"} +37;"Omniflo Weiche";"0000";"OFWeiche :6";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372002"} +38;"Omniflo Weiche";"0000";"OFWeiche :7";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372002+0_BG090090"} +39;"Omniflo Weiche";"0000";"OFWeiche :8";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372004"} +40;"Omniflo Weiche";"0000";"OFWeiche :9";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372005"} +41;"Omniflo Weiche";"0000";"OFWeiche :10";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372005+0_BG090090"} +42;"Omniflo Weiche";"0000";"OFWeiche :11";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372007"} +43;"Omniflo Weiche";"0000";"OFWeiche :12";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372008"} +44;"Omniflo Weiche";"0000";"OFWeiche :13";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372008+0_BG190090"} +45;"Omniflo Weiche";"0000";"OFWeiche :14";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372010"} +46;"Omniflo Weiche";"0000";"OFWeiche :15";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372011"} +47;"Omniflo Weiche";"0000";"OFWeiche :16";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372011+0_BG190090"} +48;"Omniflo Weiche";"0000";"OFWeiche :17";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372021"} +49;"Omniflo Weiche";"0000";"OFWeiche :18";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372022"} +50;"Omniflo Weiche";"0000";"OFWeiche :19";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372024"} +51;"Omniflo Weiche";"0000";"OFWeiche :20";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372025"} +52;"Omniflo Weiche";"0000";"OFWeiche :21";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372027"} +53;"Omniflo Weiche";"0000";"OFWeiche :22";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372028"} +54;"Omniflo Weiche";"0000";"OFWeiche :23";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372028+0_BG080090"} +55;"Omniflo Weiche";"0000";"OFWeiche :24";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372030"} +56;"Omniflo Weiche";"0000";"OFWeiche :25";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372031"} +57;"Omniflo Weiche";"0000";"OFWeiche :26";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372031+0_BG080090"} +58;"Omniflo Weiche";"0000";"OFWeiche :27";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372047"} +59;"Omniflo Weiche";"0000";"OFWeiche :28";1;{"Weichentyp": "Einzelweiche", "Richtung": "1", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372048"} +60;"Omniflo Weiche";"0000";"OFWeiche :29";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372050"} +61;"Omniflo Weiche";"0000";"OFWeiche :30";1;{"Weichentyp": "Einzelweiche", "Richtung": "2", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372051"} +62;"Omniflo Weiche";"0000";"OFWeiche :3";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372100"} +63;"Omniflo Weiche";"0000";"OFWeiche :4";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372101"} +64;"Omniflo Weiche";"0000";"OFWeiche :5";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG090090+834372101"} +65;"Omniflo Weiche";"0000";"OFWeiche :6";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372101+0_BG090090"} +66;"Omniflo Weiche";"0000";"OFWeiche :7";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG090090+834372101+0_BG090090"} +67;"Omniflo Weiche";"0000";"OFWeiche :8";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372103"} +68;"Omniflo Weiche";"0000";"OFWeiche :9";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372104"} +69;"Omniflo Weiche";"0000";"OFWeiche :10";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG190090+834372104"} +70;"Omniflo Weiche";"0000";"OFWeiche :11";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372104+0_BG190090"} +71;"Omniflo Weiche";"0000";"OFWeiche :12";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG190090+834372104+0_BG190090"} +72;"Omniflo Weiche";"0000";"OFWeiche :13";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372106"} +73;"Omniflo Weiche";"0000";"OFWeiche :14";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372107"} +74;"Omniflo Weiche";"0000";"OFWeiche :15";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372109"} +75;"Omniflo Weiche";"0000";"OFWeiche :16";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372110"} +76;"Omniflo Weiche";"0000";"OFWeiche :17";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG080090+834372110"} +77;"Omniflo Weiche";"0000";"OFWeiche :18";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372110+0_BG080090"} +78;"Omniflo Weiche";"0000";"OFWeiche :19";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG080090+834372110+0_BG080090"} +79;"Omniflo Weiche";"0000";"OFWeiche :20";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372115"} +80;"Omniflo Weiche";"0000";"OFWeiche :21";1;{"Weichentyp": "Doppelweiche", "Richtung": "3", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372116"} +81;"Omniflo Weiche";"0000";"OFWeiche :3";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372200"} +82;"Omniflo Weiche";"0000";"OFWeiche :4";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372201"} +83;"Omniflo Weiche";"0000";"OFWeiche :5";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG090090+834372201"} +84;"Omniflo Weiche";"0000";"OFWeiche :6";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372201+0_BG090090"} +85;"Omniflo Weiche";"0000";"OFWeiche :7";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG090090+834372201+0_BG090090"} +86;"Omniflo Weiche";"0000";"OFWeiche :8";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372203"} +87;"Omniflo Weiche";"0000";"OFWeiche :9";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372204"} +88;"Omniflo Weiche";"0000";"OFWeiche :10";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG190090+834372204"} +89;"Omniflo Weiche";"0000";"OFWeiche :11";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372204+0_BG190090"} +90;"Omniflo Weiche";"0000";"OFWeiche :12";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 45.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG190090+834372204+0_BG190090"} +91;"Omniflo Weiche";"0000";"OFWeiche :13";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372206"} +92;"Omniflo Weiche";"0000";"OFWeiche :14";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372207"} +93;"Omniflo Weiche";"0000";"OFWeiche :15";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372209"} +94;"Omniflo Weiche";"0000";"OFWeiche :16";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372210"} +95;"Omniflo Weiche";"0000";"OFWeiche :17";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG080090+834372210"} +96;"Omniflo Weiche";"0000";"OFWeiche :18";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372210+0_BG080090"} +97;"Omniflo Weiche";"0000";"OFWeiche :19";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG080090+834372210+0_BG080090"} +98;"Omniflo Weiche";"0000";"OFWeiche :20";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372215"} +99;"Omniflo Weiche";"0000";"OFWeiche :21";1;{"Weichentyp": "Dreiwegeweiche", "Richtung": "7", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372216"} +100;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372400"} +101;"Omniflo Weiche";"0000";"OFWeiche :2";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372403"} +102;"Omniflo Weiche";"0000";"OFWeiche :3";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372404"} +103;"Omniflo Weiche";"0000";"OFWeiche :4";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG071090+834372404+0_BG071090"} +104;"Omniflo Weiche";"0000";"OFWeiche :5";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "0_BG071090+834372404"} +105;"Omniflo Weiche";"0000";"OFWeiche :6";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372404+0_BG071090"} +106;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Deltaweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372401"} +107;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Sternweiche", "Richtung": "3", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372420"} diff --git a/tests/test_foerderer.lsp b/tests/test_foerderer.lsp index a8da863..1aa4483 100644 --- a/tests/test_foerderer.lsp +++ b/tests/test_foerderer.lsp @@ -74,7 +74,8 @@ ergebnis best-winkel L_GF L_VF y-offset y-abstand idx anz-gebaut anz-nicht-gebaut results-list - startpunkt vf-nummer hoehe-von hoehe-bis lastEnt) + startpunkt vf-nummer hoehe-von hoehe-bis lastEnt + tests-out-dir old-dxfm-results) ;; Bibliothek initialisieren (laedt Block-Library und extrahiert Masse) (if (not *lib-initialized*) @@ -200,6 +201,21 @@ ;; Ergebnisse in globaler Variable speichern (setq *foerderer-test-results* (reverse results-list)) + ;; CSV- und Sivas-Export nach tests/output/ + (setq tests-out-dir (strcat (getenv "DXFMAKRO") "/tests/output")) + (vl-mkdir tests-out-dir) + (setq old-dxfm-results (getenv "DXFM_RESULTS")) + (setenv "DXFM_RESULTS" tests-out-dir) + + (princ "\n================================================================") + (princ "\n[TEST_FOERDERER] CSV-Export -> foerderer_export.csv") + (csv:run-export "FOERDERER_CSV" "export_csv.py" "foerderer_export.csv") + + (princ "\n[TEST_FOERDERER] Sivas-Export -> foerderer_sivas.csv") + (csv:run-export "FOERDERER_SIVAS" "export_sivas.py" "foerderer_sivas.csv") + + (setenv "DXFM_RESULTS" (if old-dxfm-results old-dxfm-results "")) + (ssg-end) (princ "\n================================================================") (princ "\n TEST_FOERDERER abgeschlossen.") From 2e72f1780694ed29610dd87d96663a49c4975fcb Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 30 Jun 2026 11:21:06 +0200 Subject: [PATCH 6/6] =?UTF-8?q?Neue=20Testklassen=20f=C3=BCr=20den=20F?= =?UTF-8?q?=C3=B6rderer=20gebaut:=20Status,=20Mathematik,=20ExportCSV=20un?= =?UTF-8?q?d=20ExportSivas.=20Lisp/export.lsp=20wurde=20um=20VF=5F1,=20VF?= =?UTF-8?q?=5F2=20Bl=C3=B6cke=20erweitert,=20um=20ein=20csv=20zu=20erhalte?= =?UTF-8?q?n.=20Export=5Fsivas=20und=20export=5Fcsv=20entsprechend=20um=20?= =?UTF-8?q?die=20n=C3=B6tigen=20Daten=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 22 +++ tests/test_foerderer.py | 340 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 tests/test_foerderer.py diff --git a/tests/conftest.py b/tests/conftest.py index 5d3b1f5..9651941 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,28 @@ def _load_json(path): return json.load(f) +# --- Foerderer Fixtures --- + +@pytest.fixture +def foerderer_testdata(): + """Laedt die Foerderer-Testdefinitionen aus testdata/foerderer_tests.json.""" + path = os.path.join(_testdata_dir(), "foerderer_tests.json") + data = _load_json(path) + return [t for t in data if "test_id" in t] + + +@pytest.fixture +def foerderer_results(): + """Laedt die Foerderer-Ergebnisse aus output/foerderer_results.json.""" + path = os.path.join(_output_dir(), "foerderer_results.json") + if not os.path.exists(path): + pytest.skip("foerderer_results.json nicht vorhanden - TEST_FOERDERER in BricsCAD ausfuehren") + data = _load_json(path) + if not data: + pytest.skip("foerderer_results.json ist leer") + return data + + # --- Omniflo Fixtures --- @pytest.fixture diff --git a/tests/test_foerderer.py b/tests/test_foerderer.py new file mode 100644 index 0000000..0cff335 --- /dev/null +++ b/tests/test_foerderer.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_foerderer.py - Validiert VarioFoerderer Testergebnisse. + +Prueft: + 1. Status-Werte und Vollstaendigkeit (foerderer_results.json) + 2. Mathematische Invarianten (Laengen, Winkel, Richtung) + 3. Geometrisch unmoegliche Faelle + 4. CSV-Export-Struktur (foerderer_export.csv, falls vorhanden) + 5. Sivas-Export-Struktur (foerderer_sivas.csv, falls vorhanden) +""" + +import csv +import json +import os +import pytest + + +# --------------------------------------------------------------------------- +# Hilfsfunktionen +# --------------------------------------------------------------------------- + +def _testdata_dir(): + return os.getenv("DXFM_TESTDATA", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata")) + + +def _output_dir(): + return os.getenv("DXFM_TESTOUT", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")) + + +def _load_json(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def _load_foerderer_results(): + path = os.path.join(_output_dir(), "foerderer_results.json") + if not os.path.exists(path): + pytest.skip( + "foerderer_results.json nicht vorhanden - " + "TEST_FOERDERER in BricsCAD ausfuehren" + ) + return _load_json(path) + + +def _load_foerderer_testdata(): + path = os.path.join(_testdata_dir(), "foerderer_tests.json") + return _load_json(path) + + +# --------------------------------------------------------------------------- +# Klasse 1: Status und Vollstaendigkeit +# --------------------------------------------------------------------------- + +class TestFoerdererStatus: + """Prueft Status-Werte und Vollstaendigkeit der Ergebnisse.""" + + @pytest.fixture(autouse=True) + def setup(self): + self.results = _load_foerderer_results() + testdata = _load_foerderer_testdata() + self.tests = [t for t in testdata if "test_id" in t] + self.results_by_id = {r["test_id"]: r for r in self.results} + + def test_alle_testfaelle_vorhanden(self): + """Alle Testfaelle muessen in den Ergebnissen vorhanden sein.""" + expected_ids = {t["test_id"] for t in self.tests} + result_ids = {r["test_id"] for r in self.results} + missing = expected_ids - result_ids + assert not missing, f"Fehlende Testfaelle in Ergebnissen: {missing}" + + def test_status_werte_gueltig(self): + """Status muss GEBAUT, NICHT_GEBAUT oder GEOMETRISCH_UNMOEGLICH sein.""" + gueltige_status = {"GEBAUT", "NICHT_GEBAUT", "GEOMETRISCH_UNMOEGLICH"} + for r in self.results: + assert r["status"] in gueltige_status, ( + f'{r["test_id"]}: Unbekannter Status "{r["status"]}"' + ) + + def test_geometrisch_unmoeglich_ab_ohne_hoehe(self): + """VF_Ab_0 (Abwaerts ohne Hoehenunterschied) muss GEOMETRISCH_UNMOEGLICH sein.""" + r = self.results_by_id.get("VF_Ab_0") + assert r is not None, "VF_Ab_0 fehlt in den Ergebnissen" + assert r["status"] == "GEOMETRISCH_UNMOEGLICH", ( + f'VF_Ab_0: erwartet GEOMETRISCH_UNMOEGLICH, ist "{r["status"]}"' + ) + + def test_aufwaerts_flach_gebaut(self): + """VF_Auf_0 (Aufwaerts, kein Hoehenunterschied) muss GEBAUT sein.""" + r = self.results_by_id.get("VF_Auf_0") + assert r is not None, "VF_Auf_0 fehlt in den Ergebnissen" + assert r["status"] == "GEBAUT", ( + f'VF_Auf_0: erwartet GEBAUT, ist "{r["status"]}"' + ) + + def test_mindestens_ein_gebaut(self): + """Mindestens ein Testfall muss GEBAUT sein.""" + gebaut = [r for r in self.results if r["status"] == "GEBAUT"] + assert len(gebaut) > 0, "Kein einziger Foerderer konnte gebaut werden" + + def test_richtung_vorhanden(self): + """Jedes Ergebnis muss das Feld 'richtung' enthalten.""" + for r in self.results: + assert "richtung" in r, f'{r["test_id"]}: Feld "richtung" fehlt' + assert r["richtung"] in ("Auf", "Ab"), ( + f'{r["test_id"]}: Richtung "{r["richtung"]}" ist nicht "Auf" oder "Ab"' + ) + + +# --------------------------------------------------------------------------- +# Klasse 2: Mathematische Invarianten +# --------------------------------------------------------------------------- + +class TestFoerdererMathematik: + """Prueft mathematische Korrektheit der berechneten Werte.""" + + @pytest.fixture(autouse=True) + def setup(self): + self.results = _load_foerderer_results() + self.gebaut = [r for r in self.results if r["status"] == "GEBAUT"] + + def test_gebaut_hat_winkel_und_laengen(self): + """GEBAUT-Ergebnisse muessen winkel, L_GF und L_VF enthalten.""" + for r in self.gebaut: + assert "winkel" in r, f'{r["test_id"]}: Feld "winkel" fehlt' + assert "L_GF" in r, f'{r["test_id"]}: Feld "L_GF" fehlt' + assert "L_VF" in r, f'{r["test_id"]}: Feld "L_VF" fehlt' + + def test_laengen_positiv(self): + """L_GF und L_VF muessen beide groesser als 0 sein.""" + for r in self.gebaut: + assert r["L_GF"] > 0, ( + f'{r["test_id"]}: L_GF={r["L_GF"]} muss > 0 sein' + ) + assert r["L_VF"] > 0, ( + f'{r["test_id"]}: L_VF={r["L_VF"]} muss > 0 sein' + ) + + def test_winkel_gueltig(self): + """Winkel muss aus dem festen Satz gueltiger Foerderer-Winkel stammen.""" + gueltige_winkel = {9, 15, 21, 27, 33, 39, 45, 51} + for r in self.gebaut: + assert r["winkel"] in gueltige_winkel, ( + f'{r["test_id"]}: Winkel={r["winkel"]} nicht in {sorted(gueltige_winkel)}' + ) + + def test_aufwaerts_groesserer_deltaH_groesserer_winkel(self): + """Aufwaerts: groesserer Hoehenunterschied erfordert groesseren oder gleichen Winkel.""" + aufwaerts_gebaut = sorted( + [r for r in self.results if r["richtung"] == "Auf" and r["status"] == "GEBAUT"], + key=lambda r: r["deltaH"] + ) + for i in range(1, len(aufwaerts_gebaut)): + prev = aufwaerts_gebaut[i - 1] + curr = aufwaerts_gebaut[i] + assert curr["winkel"] >= prev["winkel"], ( + f'{curr["test_id"]}: Winkel={curr["winkel"]} < ' + f'vorheriger Winkel={prev["winkel"]} bei groesserem deltaH' + ) + + def test_kein_winkel_bei_nicht_gebaut(self): + """NICHT_GEBAUT und GEOMETRISCH_UNMOEGLICH duerfen keinen Winkel haben.""" + for r in self.results: + if r["status"] in ("NICHT_GEBAUT", "GEOMETRISCH_UNMOEGLICH"): + assert "winkel" not in r, ( + f'{r["test_id"]}: Status={r["status"]} hat unerwartet Winkel' + ) + + def test_deltaH_und_deltaL_vorhanden(self): + """Alle Ergebnisse muessen deltaH und deltaL enthalten.""" + for r in self.results: + assert "deltaH" in r, f'{r["test_id"]}: Feld "deltaH" fehlt' + assert "deltaL" in r, f'{r["test_id"]}: Feld "deltaL" fehlt' + assert r["deltaL"] > 0, f'{r["test_id"]}: deltaL muss > 0 sein' + assert r["deltaH"] >= 0, f'{r["test_id"]}: deltaH muss >= 0 sein' + + +# --------------------------------------------------------------------------- +# Klasse 3: CSV-Export-Pruefung (foerderer_export.csv) +# --------------------------------------------------------------------------- + +class TestFoerdererExportCSV: + """Prueft den CSV-Export aus BricsCAD (foerderer_export.csv).""" + + @pytest.fixture(autouse=True) + def setup(self): + csv_path = os.path.join(_output_dir(), "foerderer_export.csv") + if not os.path.exists(csv_path): + pytest.skip( + "foerderer_export.csv nicht vorhanden - " + "TEST_FOERDERER in BricsCAD ausfuehren (exportiert automatisch)" + ) + self.rows = [] + with open(csv_path, "r", encoding="utf-8") as f: + reader = csv.reader(f, delimiter=";") + self.header = next(reader) + for row in reader: + self.rows.append(row) + + def test_header_felder(self): + """CSV-Header muss korrekte Spaltenbezeichnungen haben.""" + expected = ["Elementnummer", "TeileArt", "TeileId", + "Bezeichnung", "Anzahl", "Merkmale"] + for i, name in enumerate(expected): + assert self.header[i] == name, ( + f"Header[{i}]: erwartet '{name}', ist '{self.header[i]}'" + ) + + def test_hat_eintraege(self): + """CSV muss mindestens einen Dateneintrag enthalten.""" + assert len(self.rows) > 0, "foerderer_export.csv enthaelt keine Daten" + + def test_vario_teileart(self): + """Alle Eintraege muessen TeileArt 'VarioFoerderer' haben.""" + for i, row in enumerate(self.rows): + if len(row) < 2: + continue + assert row[1].strip('"') == "VarioFoerderer", ( + f"Zeile {i + 2}: TeileArt='{row[1]}', erwartet 'VarioFoerderer'" + ) + + def test_anzahl_spalte_ist_eins(self): + """Anzahl-Spalte muss fuer alle Zeilen '1' sein.""" + for i, row in enumerate(self.rows): + if len(row) < 5: + continue + assert row[4] == "1", ( + f"Zeile {i + 2}: Anzahl={row[4]}, erwartet 1" + ) + + def test_merkmale_sind_valides_json(self): + """Merkmale-Spalte muss valides JSON enthalten.""" + for i, row in enumerate(self.rows): + if len(row) < 6: + continue + try: + json.loads(row[5]) + except json.JSONDecodeError as e: + pytest.fail(f"Zeile {i + 2}: Merkmale ist kein valides JSON: {e}") + + def test_merkmale_felder(self): + """Merkmale muessen alle Pflichtfelder enthalten.""" + required = {"Typ", "Winkel", "DeltaH_mm", "DeltaL_mm", + "L_VF_m", "Hoehe_Von_mm", "Hoehe_Bis_mm"} + for i, row in enumerate(self.rows): + if len(row) < 6: + continue + merkmale = json.loads(row[5]) + missing = required - set(merkmale.keys()) + assert not missing, ( + f"Zeile {i + 2}: Fehlende Merkmale-Felder: {missing}" + ) + + def test_anzahl_csv_zeilen_entspricht_gebaut(self): + """CSV-Zeilenanzahl muss Anzahl GEBAUT-Testfaellen entsprechen.""" + results = _load_foerderer_results() + anzahl_gebaut = sum(1 for r in results if r["status"] == "GEBAUT") + assert len(self.rows) == anzahl_gebaut, ( + f"CSV hat {len(self.rows)} Zeilen, " + f"erwartet {anzahl_gebaut} (Anzahl GEBAUT-Faelle)" + ) + + +# --------------------------------------------------------------------------- +# Klasse 4: Sivas-Export-Pruefung (foerderer_sivas.csv) +# --------------------------------------------------------------------------- + +class TestFoerdererExportSivas: + """Prueft den Sivas-Export aus BricsCAD (foerderer_sivas.csv).""" + + @pytest.fixture(autouse=True) + def setup(self): + csv_path = os.path.join(_output_dir(), "foerderer_sivas.csv") + if not os.path.exists(csv_path): + pytest.skip( + "foerderer_sivas.csv nicht vorhanden - " + "TEST_FOERDERER in BricsCAD ausfuehren (exportiert automatisch)" + ) + self.rows = [] + with open(csv_path, "r", encoding="utf-8") as f: + reader = csv.reader(f, delimiter=";") + self.header = next(reader) + for row in reader: + self.rows.append(row) + + def test_header_felder(self): + """Sivas-CSV-Header muss korrekte Spalten haben.""" + expected = ["Nr", "TeileArt", "SivasNummer", + "Dispgruppe", "Anzahl", "Laenge[mm]", "Hoehe[m]", "details"] + for i, name in enumerate(expected): + assert self.header[i] == name, ( + f"Header[{i}]: erwartet '{name}', ist '{self.header[i]}'" + ) + + def test_hat_eintraege(self): + """Sivas-CSV muss mindestens einen Dateneintrag enthalten.""" + assert len(self.rows) > 0, "foerderer_sivas.csv enthaelt keine Daten" + + def test_strecke_zeile_vorhanden(self): + """Eine Zeile mit TeileArt 'ILS 2.0 Strecke - Module' muss vorhanden sein.""" + teilearten = [row[1].strip('"') for row in self.rows if len(row) >= 2] + assert "ILS 2.0 Strecke - Module" in teilearten, ( + f"Keine Strecke-Zeile gefunden. Vorhandene: {teilearten}" + ) + + def test_automation_zeile_vorhanden(self): + """Eine ILS Automation Summierungszeile muss vorhanden sein.""" + teilearten = [row[1].strip('"') for row in self.rows if len(row) >= 2] + assert "ILS 2.0 Automation und Pneumatik - Gesamtanlage" in teilearten, ( + f"Keine Automation-Zeile gefunden. Vorhandene: {teilearten}" + ) + + def test_details_sind_valides_json(self): + """details-Spalte muss valides JSON enthalten.""" + for i, row in enumerate(self.rows): + if len(row) < 8: + continue + try: + json.loads(row[7]) + except json.JSONDecodeError as e: + pytest.fail(f"Zeile {i + 2}: details ist kein valides JSON: {e}") + + def test_automation_details_felder(self): + """Automation-Zeile muss Schluessel-Felder in details haben.""" + required = {"Anzahl der einzelnen Variofoerderer", + "Laengen aller Variofoerderer", + "Anzahl aller Separatoren"} + for row in self.rows: + if len(row) < 8: + continue + if row[1].strip('"') == "ILS 2.0 Automation und Pneumatik - Gesamtanlage": + details = json.loads(row[7]) + missing = required - set(details.keys()) + assert not missing, ( + f"Automation-details: fehlende Felder: {missing}" + ) + break