Files
dxfmakros/tests
y.wang b709f89404 test: U-Form-Faelle, Formkriterium laengs und Reihenanordnung der Linienzug-Tests
Drei neue Modus-2-Testfaelle aus echten Laeufen, ein Kriterium, das eine
heute eingebaute Fehlgeometrie gefunden haette, und ein aufgeraeumtes
Layout der Testketten.

1. Drei U-Form-Faelle (tests/testdata/linienzug_modus2_uform_*.json),
   aufgezeichnet mit lib/vf_journal_export.py. Gefaltete Laeufe: der Pfad
   wechselt ueber zwei 90-Grad-Vario-Kurven dreimal die Fahrtrichtung, die
   Kletterstrecke liegt in einem ANDEREN Schenkel als der Lauf-Eingang -
   genau der am 2026-10-29 korrigierte Fall. Variiert werden GF-Verteilung
   (alles am Einlauf vs. 1/2+1/2) und Winkel; beides ist noetig, weil die
   Verteilung entscheidet, ob ueberhaupt GF im fremden Schenkel steht, und
   der Restfehler der alten Bilanz mit L_GF skalierte.
   c:TEST_LINIENZUG_MODUS2 ist dafuer tabellengesteuert - ein weiterer Fall
   ist jetzt eine Zeile in *tlz-m2-faelle*.

2. RUECKNAHME des Schluss-Solves (vfl-zsolve-schluss-ziel, heute frueh
   eingebaut). Er loeste den letzten Fueller gegen Richtung UND Laenge und
   traf den Zielpunkt dadurch in X und Y exakt - um den Preis, dass er die
   letzte Gerade um 6.15 Grad verdrehte (hz-plan -90 -> -96.15), in der
   Zeichnung als schiefer Schenkel sichtbar. Das widerspricht der
   Entscheidung vom 2026-10-26: quer MUSS 0 sein, LAENGS DARF ABWEICHEN -
   der Hauptstrang bleibt gerade, der seitliche Versatz des ES-Elements ist
   dessen Bauform. Die Messwerte von heute (0.56 / 0.7 mm quer) stammen
   nicht von dieser Aenderung, sondern von der Laengenbilanz des gefalteten
   Laufs (35ebee1) - der Zweig blieb dort ungenutzt. Die Begruendung steht
   jetzt an der Aufrufstelle, damit niemand den Solve erneut baut.

3. laengs wird geprueft - als FORM-, nicht als Genauigkeitskriterium
   (erwartung_laengs_mm, Toleranz 5 mm). Der Wert ist die Ausladung des
   ES-Elements und je Fall eine Konstante (104.68 bei ES_Element_30,
   637.58 bei ES_Element_90). Punkt 2 war am Querwert NICHT erkennbar - die
   Verdrehung macht ihn gerade null, der Test meldete OK, gefunden wurde sie
   mit dem Auge. Ein verdrehter Hauptstrang zielt auf den Endpunkt und
   drueckt laengs auf ~0; daran faellt er auf. Gegengeprueft: mit kuenstlich
   auf 0.01 mm gesetzter Toleranz kippen alle fuenf Faelle auf ABWEICHUNG.

4. Alle sieben Ketten des Menueeintrags entstehen als Reihe auf Y=0, 20 m
   Abstand, statt verstreut. Die Modus-1-Skripte brauchen dafuer nur ihren
   EINEN point_abs (alles Weitere ist point_rel mit dL/hz); bei Modus 2
   mussten PT-Eintraege UND die Pfadkoordinaten in SEGS (Felder 6..9)
   mitwandern, sonst zeigt der Pfad woanders hin als Start und Ziel.
   Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind
   davon unabhaengig; Z blieb stehen, weil die Starthoehe ein Eingabewert
   des Solvers ist. Geprueft: erstes Segment beginnt exakt am Startpunkt,
   letztes endet exakt am Zielpunkt, Messwerte unveraendert.

5. Die drei letzten Verweise auf doc/VarioFoerderer_Linienzug_Prinzipien.md
   (bewusst unversioniert) tragen ihre Begruendung jetzt selbst: Entkopplung
   Neigung/XY, warum es genau EINEN Kletterer je Lauf gibt, und warum ein
   VF-Sub-Segment auf der 3-Grad-Basis beginnen und enden muss.

Geprueft: TEST_UNIT 897/897 (die 7 Tests der entfernten Funktion sind mit
ihr gegangen), TEST_LINIENZUG_MODUS2 5 von 5 OK, voller Menueablauf baut
sieben Ketten an den vorgesehenen Stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-29 14:28:57 +01:00
..

Tests - SSG_LIB Testarchitektur

Uebersicht

Die Tests folgen einer zweistufigen Architektur:

  1. LISP-Testrunner (.lsp) — laufen in BricsCAD, erzeugen Ergebnisdateien
  2. Python-Validierung (.py) — pruefen die Ergebnisse mit pytest + ezdxf
tests/alltests.json
   |
   v
BricsCAD                          Kommandozeile
   |                                   |
   |  SSG_RUN_ALL_TESTS                |
   |  (fuehrt je Eintrag aus           |
   |   alltests.json den passenden     |
   |   TEST_<NAME> aus)                |
   |        |                          |
   v        v                          |
tests/output/                          |
   <name>_results.json  (immer)        |
   <name>_tests.dxf/.dwg (gemaess save)|
                                       |
                              bin\run_tests.bat --check
                                       |
                                  pytest -v
                                       |
                              test_<name>.py je Modul

LISP-Unit-Tests fuer reine Standardfunktionen (--lisp)

Neben den zeichnungsbasierten Integrationstests gibt es Unit-Tests fuer einfache LISP-Standardfunktionen — reine Funktionen ohne Zeichnungsdatenbank, Auswahlsaetze, Dialoge oder Blockdateien (String-/Zahl-/Vektor-/Alist-Helfer). Sie funktionieren analog zu den Python-Unittests: jeder Testfall vergleicht einen Funktionsaufruf gegen ein erwartetes Ergebnis.

  • Testrunner: tests/test_unit.lsp — Befehl TEST_UNIT, Mini-Framework mit tu-eq (exakt), tu-eqf (Fliesskomma-Toleranz), tu-true, tu-nil.
  • Startskript: tests/test_unit.scr — laedt SSG_LIB + die Tests, ruft TEST_UNIT auf und beendet BricsCAD (pfadunabhaengig ueber DXFMAKRO).
  • Ergebnis: Konsole (PASS/FAIL je Test) und tests/output/unit_results.txt (letzte Zeile RESULT: OK / RESULT: FAIL).

Ausfuehrung ueber die Kommandozeile (startet BricsCAD headless):

bin\run_tests.bat --lisp

run_tests.bat --lisp loescht das alte Ergebnis, startet BricsCAD mit test_unit.scr, gibt danach unit_results.txt aus und liefert Exit-Code 1, wenn ein Test fehlschlaegt oder kein Report erzeugt wurde.

Direkt in BricsCAD (SSG_LIB geladen):

(load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp"))
TEST_UNIT

Getestete Module: ssg_core, ssg_lang, ssg_id, vf_core, export, Gefaellestrecke, OmniModulInsert, KreiselInsert.

Zeichnungsbasierte Integrationstests

Jedes Testmodul laeuft in einer eigenen, neuen Zeichnung (statt die aktuelle zu leeren/wiederzuverwenden). SSG_RUN_ALL_TESTS fragt beim Start interaktiv nach dem gewuenschten Modus:

  • Kontroll-Modus (Default, Enter/Nein): Zeichnung wird gespeichert, per _.ZOOM _Extents auf den Inhalt gezoomt und bleibt als eigener Tab offen — der naechste Test startet in einer weiteren neuen Zeichnung. Am Ende des Laufs sind alle Testzeichnungen gleichzeitig offen und direkt sichtbar. Sinnvoll, wenn neue/geaenderte Testfaelle visuell geprueft werden sollen. c:EXPORTCSV/c:EXPORTSIVAS laufen nicht automatisch mit — CSV/Sivas- Export danach bei Bedarf manuell ueber TEST_EXPORT_ALL (tests/test_export_all.lsp, Menuepunkt "Export CSV/Sivas ALL"), das alle vorhandenen DXF/DWG-Dateien aus tests/output/ einliest und je Datei <basis>_export.csv/<basis>_sivas.csv erzeugt.
  • Automatik-Modus (Ja): Zeichnung wird gespeichert, direkt per c:EXPORTCSV/c:EXPORTSIVAS exportiert (<name>_tests_export.csv/ <name>_tests_sivas.csv) und danach sofort wieder geschlossen. Kompletter Lauf ohne manuellen Zusatzschritt, aber ohne gleichzeitige visuelle Kontrolle aller Zeichnungen am Ende. Sinnvoll fuer reine CI-artige Validierung.

Anordnung der Linienzug-Testketten

Der Menueeintrag Tests -> Foerderer-Linienzug faehrt drei Befehle nacheinander (TEST_LINIENZUG, TEST_LINIENZUG_STUFE2, TEST_LINIENZUG_MODUS2) und baut dabei sieben Ketten. Damit sie nebeneinander auf einer Grundlinie entstehen statt verstreut, tragen die Testdaten versetzte Startpunkte - 20 m Abstand, alle auf Y = 0:

X Z Kette Datei
0 2154 Modus 1, Standardfall linienzug_tests.json
20000 2154 Modus 1, Stufe-2-Fallback linienzug_tests_stufe2.json
40000 0 Variante A (Vario-Kurve) linienzug_modus2_variokurve.json
60000 0 Variante B (GF-Bogen) linienzug_modus2_gfbogen.json
80000 355 U-Form 1 (GF alles am Einlauf) ..._uform_gf1.json
100000 711 U-Form 2 (1/2 + 1/2) ..._uform_halb.json
120000 709 U-Form 3 (steilerer Winkel) ..._uform_halb_steil.json

Die Koordinaten sind also nicht die der aufgezeichneten Laeufe, die uebrige Geometrie schon. Verschoben wurde rein translatorisch: Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig. Z blieb jeweils stehen - die Starthoehe ist ein Eingabewert des Solvers, kein Layout-Parameter.

Technisch unterscheiden sich die beiden Datenarten: die Modus-1-Skripte brauchen nur ihren EINEN point_abs (alles Weitere ist point_rel mit dL/hz), waehrend bei Modus 2 sowohl die PT-Eintraege ALS AUCH die Pfadkoordinaten in SEGS (Felder 6..9) mitwandern muessen - sonst zeigt der Pfad woanders hin als Start und Ziel.

Referenzfaelle Linienzug Modus 2 (c:TEST_LINIENZUG_MODUS2)

Fuenf aufgezeichnete Laeufe, die als zwei Paare gebildet sind: A/B unterscheiden die Eck-BAUTEILART, U-Form 1..3 den gefalteten Lauf nach GF-Verteilung und Winkel. Ausgefuehrt ueber Menue -> Tests -> Foerderer-Linienzug.

Erster und zweiter Fall: Eck-Bauteilart

Zwei Laeufe auf demselben gezeichneten Pfad, die sich nur in der Segment-Klassifizierung unterscheiden. Sie decken die beiden Eck-Bauteilarten ab (waagerecht eingebaute Vario-Kurve und geneigt eingebauter Gefaellebogen) und haben am 2026-10-26 sieben Budget-Fehler aufgedeckt, die ein einzelner Lauf nicht gezeigt haette - erst der VERGLEICH zweier Varianten machte sichtbar, dass eine Korrektur am falschen Segment landete.

Gezeichneter Pfad (5 Objekte, Boegen als R500-Verrundung):

Segment Art Mass
1 Linie L = 9700 mm, hz = 0
2 Bogen 90 Grad rechts, R500
3 Linie L = 4296 mm, hz = -90
4 Bogen 30 Grad links, R500
5 Linie L = 1466 mm, hz = -60

Gemeinsame Eingaben: AS-Element ja, 90 Grad, links. ES-Element ja, 30 Grad, rechts. Lauf beginnen mit "Umlenkstation mit GF1 + Separator", GF-Verteilung "alles am Einlauf", keine zusaetzlichen Separatoren.

Variante A Variante B
Segment 2 Vario-Kurve (aussen) GF-Bogen
Segment 3 VF (automatisch nach Kurve) GF
Segment 4 GF-Bogen GF-Bogen
Segment 5 GF GF
VF-Lauf Segment 1..3 Segment 1..1

Soll-Ergebnis fuer BEIDE Varianten:

  • Quer zur Achse ~0 (gemessen 0.2 mm - das ist die Aufloesung der Blockmessung, kein Restfehler)
  • dZ 0
  • Laengs KS_AUS-Achse darf abweichen (~104 mm) - das ist die Ausladung des ES-Elements und ausdruecklich zulaessig.

Die Regel dahinter: quer zur KS_AUS-Achse MUSS 0 sein, laengs darf abweichen, dZ MUSS 0 sein. Beides zugleich geht nicht - entweder liegt die letzte Gerade auf der gezeichneten Linie und der ES-KS_AUS landet seitlich daneben, oder umgekehrt. Entschieden ist: der Hauptstrang bleibt gerade, der seitliche Versatz des ES-Elements ist dessen Bauform und kein Fehler.

laengs wird trotzdem geprueft - nicht als Genauigkeits-, sondern als FORMkriterium (erwartung_laengs_mm in den Testdaten, Toleranz 5 mm). Der Wert ist die Ausladung des ES-Elements und damit je Fall eine Konstante: 104.68 mm bei ES_Element_30, 637.58 mm bei ES_Element_90. Grund: am 2026-10-29 loeste ein Experiment den Schluss-Fueller gegen Richtung UND Laenge und traf den Zielpunkt dadurch in X und Y exakt - um den Preis, dass es die letzte Gerade um 6.15 Grad verdrehte. quer und dZ waren dabei NULL, der Test meldete brav OK, und der schiefe Schenkel fiel nur beim Blick in die Zeichnung auf. Ein verdrehter Hauptstrang zielt auf den Endpunkt und drueckt laengs von ~638 mm auf ~0 - genau daran ist er zu erkennen. Die Pruefung selbst ist gegengeprueft: mit einer kuenstlich auf 0.01 mm gesetzten Toleranz kippen alle fuenf Faelle auf ABWEICHUNG.

Im Protokoll muessen die Zuschlags-Zeilen erscheinen: AS-Fussabdruck, Eck-Korrektur je Ecke, und bei Variante A zusaetzlich Abschluss-Fussabdruck. Eine FEHLENDE Zeile ist das Warnsignal - sie bedeutet, dass ein Eck-Bauteil nicht messbar war und die Ecke unkorrigiert blieb (so wurde der 2.4-mm-Rest bei Variante B gefunden).

Automatisiert seit 2026-10-27 als c:TEST_LINIENZUG_MODUS2 (Menue: Tests -> Foerderer-Linienzug). Der Pfad muss dafuer NICHT gezeichnet werden: ein Modus-2-Journal traegt einen SEGS-Eintrag mit der kompletten Segment-Essenz und ist damit von den Original-Objekten unabhaengig - der Test spielt schlicht das mit lib/vf_journal_export.py ausgelesene Journal ab. Die reinen Rechenkerne sind zusaetzlich file-frei abgedeckt (tu-tests-vfl-parallel in test_unit.lsp: vfl-eck-zerlegen, vfl-es-achs-reserve, vfl-projiziere-neigung - jeweils mit den echten Messwerten aus diesen beiden Laeufen).

Dritter bis fuenfter Fall: gefalteter Lauf (U-Form)

Aufgezeichnet 2026-10-29, Datendateien tests/testdata/linienzug_modus2_uform_*.json.

Der Pfad wechselt ueber zwei 90-Grad-Vario-Kurven dreimal die Fahrtrichtung; die Kletterstrecke liegt dadurch in einem ANDEREN Schenkel als der Lauf-Eingang. Genau diese Konstellation war bis 2026-10-29 falsch bilanziert (gemessen 1424 mm quer), weil Posten dem falschen Schenkel zugerechnet wurden.

Segment Art Mass
1 Linie L = 5855 mm, hz = 90
2 Bogen 90 Grad rechts, R500 -> Vario-Kurve aussen
3 Linie L = 270 mm, hz = 0 (automatisch VF)
4 Bogen 90 Grad rechts, R500 -> Vario-Kurve aussen
5 Linie L = 7806 mm, hz = -90 (der Kletterer)
Fall GF-Verteilung Winkel-Option Testdatei
U-Form 1 alles am Einlauf (GF1) 2 ..._uform_gf1.json
U-Form 2 1/2 GF1 + 1/2 GF2 2 (= 9 Grad) ..._uform_halb.json
U-Form 3 1/2 GF1 + 1/2 GF2 3 (steiler) ..._uform_halb_steil.json

Warum drei und nicht einer: die GF-VERTEILUNG entscheidet, ob ueberhaupt Gefaellestrecke im fremden Schenkel steht (und damit, ob Nachrechner und das unterdrueckte Fueller-A greifen) - mit "alles am Einlauf" liegt das gesamte GF1 dort, mit 1/2+1/2 nur die Haelfte. Der WINKEL ist noetig, weil der Restfehler der alten Bilanz mit L_GF skalierte: bei 6 Grad waren es 90 mm, bei 9 Grad 934 mm. Zwei Laeufe mit demselben Winkel haetten den Fehler also beinahe verdeckt.

Soll-Ergebnis wie oben: quer und dZ jeweils ~0 (gemessen 0.56 mm bei 9 Grad und 0.7 mm bei der steileren Option), laengs ~638 mm - das ist die Ausladung des ES_Element_90_links und in allen drei Faellen gleich.

alltests.json — Testregistry

Die Datei tests/alltests.json ist die zentrale Registry aller Test-Module. SSG_RUN_ALL_TESTS liest sie beim Start — die Testliste ist nicht hartcodiert in test_run_all.lsp.

[
  { "name": "kreisel",         "save": "dxf", "module": "KreiselInsert"   },
  { "name": "foerderer",       "save": "dwg", "module": "VarioFoerderer"  },
  { "name": "linienzug",       "save": "dwg", "module": "VarioFoerderer"  },
  { "name": "vfl_schema",                     "module": "VarioFoerderer"  },
  { "name": "omniflo",         "save": "dxf", "module": "OmniModulInsert" },
  { "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" }
]

Felder pro Eintrag:

Feld Typ Bedeutung
name String Basisname des Testmoduls (Pflicht)
save "dxf" / "dwg" / null Ob und als welches Format die Zeichnung gespeichert wird
module String LISP-Feature-Modul, das vor dem Test per ssg-ensure geladen wird
disabled true (optional) Schaltet den Eintrag ab: SSG_RUN_ALL_TESTS ueberspringt ihn komplett (kein Bauen/Speichern/Export) und gibt nur einen Hinweis auf der Konsole aus. Zum Reaktivieren das Feld entfernen.

JSON-Export laeuft immer — unabhaengig vom save-Feld. Die Datei <name>_results.json wird in tests/output/ geschrieben, sobald der Test abgeschlossen ist.

GUI-Aus waehrend des Laufs: SSG_RUN_ALL_TESTS (und der Einzeltest TEST_LINIENZUG) rufen (ssg-gui-aus) (ssg_core.lsp) auf, damit keine DCL-/Wizard-Dialoge den automatischen Lauf blockieren; danach wird (ssg-gui-an) wieder gesetzt. Bei abgeschalteter GUI fallen die Eingaben auf den Konsolen-/Mock-/Replay-Pfad zurueck. Interaktiver Betrieb (FOERDERANLAGE etc.) ist davon unberuehrt (GUI standardmaessig an).

Wann welches save verwenden:

  • "dxf" — wenn ein Python-Test (ezdxf) die Geometrie der Zeichnung pruefen soll
  • "dwg" — wenn die Zeichnung zur manuellen Kontrolle oder als DWG-Referenz erhalten bleiben soll
  • null (oder Feld weglassen) — wenn nur der JSON-Export benoetigt wird

Ablauf fuer den Anwender

  1. BricsCAD starten (ueber bin\start_briscad.bat) und die LISP-Testrunner ausfuehren (siehe Abschnitt "LISP-Tests in BricsCAD ausfuehren"). Die Testrunner erzeugen Bloecke in der Zeichnung und speichern Ergebnisse nach tests/output/.

  2. Optische Kontrolle in BricsCAD — pruefen ob die erzeugten Bloecke korrekt aussehen (Positionen, Drehungen, Beschriftungen). Jede Testzeichnung bleibt als eigener Tab offen (auf den Inhalt gezoomt) und kann direkt visuell inspiziert werden.

  3. Kommandozeile: Ergebnisse validieren:

    • bin\run_tests.bat --check — pytest prueft die Ergebnisse aus output/ gegen Testdefinitionen und Referenzdaten (optional: einzelnes Modul, z.B. --check test_kreisel.py)
    • bin\run_tests.bat --diff_references — vergleicht die CSVs in output/ gegen reference/ (gleicher Dateiname, siehe Abschnitt "Referenz-Namenskonvention") und oeffnet bei Unterschieden das Diff-Tool (meld); optional eine einzelne Datei, z.B. --diff_references kreisel_tests_export.csv
    • Referenz aktualisieren (nach geprueftem Lauf): die betreffende(n) Datei(en) aus output/ unter DEMSELBEN Namen nach reference/ kopieren (es gibt keinen eigenen Schalter dafuer)

Verzeichnisstruktur

tests/
  conftest.py              # Gemeinsame pytest-Fixtures (Pfade, JSON/DXF-Laden)
  create_testbase.py       # Erzeugt Basis-DXF (optional, wird nicht mehr benoetigt)
  requirements.txt         # Python-Abhaengigkeiten (ezdxf, pytest)
  test_run_all.lsp         # Fuehrt alle Tests aus: C:SSG_RUN_ALL_TESTS
  test_kreisel.lsp         # LISP-Testrunner: C:TEST_KREISEL
  test_kreisel.py          # pytest-Validierung der Kreisel-Ergebnisse
  test_foerderer.lsp       # LISP-Testrunner: C:TEST_FOERDERER
  test_omniflo.lsp         # LISP-Testrunner: C:TEST_OMNIFLO
  test_omniflo.py          # pytest-Validierung der Omniflo-Ergebnisse
  test_omniflo_strecke.lsp # LISP-Testrunner: C:TEST_OMNIFLO_STRECKE (Streckenzug)
  test_omniflo_strecke.py  # pytest-Validierung des Omniflo-Streckenzugs
  test_export_all.lsp      # LISP-Testrunner: C:TEST_EXPORT_ALL (manuell, nicht in alltests.json)
  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.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_geometrie.py     # pytest: die GEBAUTE Geometrie gegen
                           #   reference/hundm05_attribute.json - Attribute,
                           #   Bausteine, Einfuegepunkt (faengt eine
                           #   vertauschte vfl-acc-*-Reihenfolge)
  test_vf_spec.lsp         # LISP-Unittest: Spec-Uebersetzer (vsp-journal),
                           #   je Glied-Typ + die 5 echten HundM-Ketten
                           #   gegen ihr aufgezeichnetes Journal
  test_vf_spec.py          # pytest: Rundlauf Journal->Spec->Journal, die
                           #   Spec-Testdaten, und die Ergebnisse von
                           #   VF_SPEC_BAU (letztere brauchen BricsCAD)
  test_vf_journal_grammatik.py # pytest ohne CAD: Journal-Dekoder aus
                           #   lib/vf_journal_export.py gegen handgebaute
                           #   Journale (Linie-VF, gewinkelter Erstkoerper)
  test_vf_headless_statisch.py # pytest ohne CAD: statischer Waechter, dass
                           #   im VF-Bau-Pfad kein alert/get*/ssget neu
                           #   auftaucht und der Headless-Riegel an allen
                           #   vfl-in-*-Wrappern sitzt
  test_unit.lsp / .scr     # LISP-Unit-Tests: C:TEST_UNIT (ueber --lisp)
  testdata/
    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)
    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)
    export_all_results.json    # nur nach manuellem TEST_EXPORT_ALL-Lauf
    <basis>_export.csv         # EXPORTCSV je Quelldatei, nur nach TEST_EXPORT_ALL
    <basis>_sivas.csv          # EXPORTSIVAS je Quelldatei, nur nach TEST_EXPORT_ALL
  reference/               # Abgenommene Referenzdaten (in Git): *_results.json,
    ...                    # *_tests_export.csv / *_tests_sivas.csv, *_tests.dxf,
    ...                    # omniflo_export.csv, export_raw.json

Referenz-Namenskonvention (vereinheitlicht)

Eine Referenz traegt IMMER denselben Dateinamen wie das zugehoerige Ist-Ergebnis in output/. Der einzige Unterschied ist das Verzeichnis:

output/<datei>      # Ist-Ergebnis (bei jedem Lauf neu, nicht in Git)
reference/<datei>   # abgenommener Soll-Stand (identischer Name, in Git)

Damit gilt durchgehend: Referenz festlegen = Datei aus output/ nach reference/ kopieren - kein Umbenennen, kein Suffix. Beispiele:

Ist-Ergebnis in output/ Referenz in reference/
kreisel_tests.dxf kreisel_tests.dxf
kreisel_tests_export.csv kreisel_tests_export.csv
foerderer_results.json foerderer_results.json
omniflo_export.csv omniflo_export.csv
copy tests\output\kreisel_tests.dxf tests\reference\kreisel_tests.dxf

Frueher gab es zwei Ausreisser, die bewusst auf dieses Schema gezogen wurden (nicht mehr verwenden):

  • reference/kreisel_ref.dxf (bzw. der Tippfehler Kresel_ref.dxf) -> jetzt reference/kreisel_tests.dxf
  • reference/omniflo_export_reference.csv -> jetzt reference/omniflo_export.csv

Die pytest-Fixtures und -Checks suchen ausschliesslich nach dem vereinheitlichten Namen; fehlt die Referenz, wird der Vergleich mit einem Hinweis uebersprungen (kein Fehler).

Workflow

1. LISP-Tests in BricsCAD ausfuehren

BricsCAD starten (ueber bin\start_briscad.bat) und Module laden:

(load (strcat (getenv "DXFMAKRO") "/Lisp/ssg_load.lsp"))

Dann alle Tests auf einmal ausfuehren:

;; Alle Tests ausfuehren (empfohlen)
(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp"))
SSG_RUN_ALL_TESTS

SSG_RUN_ALL_TESTS liest die Testliste aus tests/alltests.json und fuehrt fuer jedes Modul aus:

  1. Zeichnung leeren (ERASE ALL + PURGE x5) — saubere Basis fuer jeden Test
  2. test_<name>.lsp laden und TEST_<NAME> ausfuehren
  3. JSON-Export aufrufen: <name>:export-results — immer, unabhaengig von save
  4. Zeichnung speichern — nur wenn "save" in alltests.json gesetzt ist ("dxf" oder "dwg")
  5. Bei Fehler: weiter mit naechstem Test (vl-catch-all-apply)

Einzeltest (z.B. nur Kreisel pruefen):

;; Test-Datei laden und Befehl ausfuehren
(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp"))
TEST_KREISEL
;; JSON-Export manuell aufrufen:
(kreisel:export-results (strcat (getenv "DXFMAKRO") "/tests/output"))

Unterschied SSG_RUN_ALL_TESTS vs. Einzeltest:

SSG_RUN_ALL_TESTS Einzeltest
Testliste aus alltests.json manuell gewaehlter Test
Zeichnung leeren automatisch vor jedem Test nein (Zeichnung bleibt)
JSON-Export automatisch nach jedem Test manuell aufrufen
DXF/DWG speichern gemaess save in alltests.json manuell (SAVEAS / DXFOUT)
Fehlerisolation ja (Fehler unterbricht nicht weitere Tests) nein

2. Python-Validierung ausfuehren

bin\run_tests.bat --check

Oder fuer ein einzelnes Testmodul:

bin\run_tests.bat --check test_kreisel.py

Die Python-Tests pruefen:

  • Attribut-Werte gegen die Testdefinitionen (JSON)
  • DXF-Geometrie mit ezdxf (Bloecke, Positionen, Radien)
  • Referenz-Vergleich gegen abgenommene Referenz-DXF
  • CSV-Export ueber lib/export_csv.py (Omniflo: Spalten, Merkmale, Sum-Zeile, Datentypen)

Tests, deren Eingabedaten fehlen (z.B. kein BricsCAD-Lauf), werden automatisch uebersprungen (pytest.skip).

3. Referenz erstellen/aktualisieren

Nach einem erfolgreichen, geprueften Testlauf die betreffende(n) Ergebnisdatei(en) manuell aus output/ unter DEMSELBEN Dateinamen nach reference/ kopieren (nur bei bestandenen Tests, siehe Abschnitt "Referenz-Namenskonvention"). Es gibt keinen eigenen Batch-Schalter dafuer.

copy tests\output\<datei> tests\reference\<datei>

Beispiel (Kreisel-DXF und Omniflo-CSV als Referenz festlegen):

copy tests\output\kreisel_tests.dxf tests\reference\kreisel_tests.dxf
copy tests\output\omniflo_export.csv tests\reference\omniflo_export.csv

Zum Vergleich der aktuellen CSVs gegen die Referenz (mit Diff-Tool meld):

bin\run_tests.bat --diff_references

Neuen Test erstellen

Schritt 1: Testdaten definieren (JSON)

Neue Testfaelle in testdata/<modul>_tests.json eintragen. Das Format ist ein flaches JSON-Array (kompatibel mit omni:load-json).

Kreisel-Beispiel (kreisel_tests.json):

[
  {
    "id": "KR_Insert_Neu",
    "function": "insert",
    "x": 5000, "y": 5000, "z": 2000,
    "abstand": 5000,
    "rotation": 0.0,
    "typ": "STANDARD",
    "expect_block_prefix": "KREISEL_",
    "expect_hoehe": "2000",
    "expect_kreiselart": "STANDARD"
  }
]

Felder:

  • id — Eindeutiger Name des Testfalls
  • function — "insert" oder "connect"
  • Eingabeparameter (x, y, z, abstand, rotation, typ fuer Insert; start_x/y/z, end_x/y/z, typ fuer Connect)
  • expect_* — Erwartete Ergebniswerte (fuer Python-Validierung)

Omniflo-Beispiel (omniflo_tests.json):

[
  {
    "sivasnr": "834372001",
    "type": "bogen",
    "description": "Bogen 90 Grad R200",
    "hoehe": 2000,
    "drehung": 0
  }
]

Foerderer-Beispiel (foerderer_tests.json):

[
  { "y_abstand": 800 },
  {
    "test_id": "VF_Auf_2000_Rechts",
    "richtung": "Auf",
    "deltaL": 7000,
    "deltaH": 2000,
    "seite": "rechts",
    "winkel": 27,
    "gf_verteilung": "vorne"
  }
]

Felder:

  • test_id — Eindeutiger Name des Testfalls
  • richtung — "Auf" oder "Ab"
  • deltaL, deltaH — Horizontal-/Hoehendistanz in mm
  • seite (optional, Default "links") — "links" oder "rechts", steuert AS-/ES-Seite (SEITE_AS/SEITE_ES-Attribut)
  • winkel (optional) — erzwingt einen bestimmten Kandidatenwinkel statt des automatisch gewaehlten kleinsten gueltigen Winkels; muss ein gueltiger Wert aus der bogen_winkel-Kandidatenliste sein, sonst wird der Testfall NICHT_GEBAUT
  • gf_verteilung (optional, Default "gleichmaessig") — "gleichmaessig" (50/50 auf Einlauf/Auslauf), "vorne" (alles am Einlauf) oder "hinten" (alles am Auslauf)

Schritt 2: LISP-Testrunner erweitern (falls neues Modul)

Fuer ein neues Modul eine neue .lsp-Datei erstellen nach dem Muster:

;; test_<modul>.lsp
;; Konvention: Basisname <modul> bestimmt Befehl, DXF und Export-Funktion

;; Export-Funktion (wird von test_run_all.lsp aufgerufen)
(defun <modul>:export-results (tests-out-dir / out-json f first)
  (if (null *<modul>-test-results*)
    (princ "\n  Keine <modul>-Ergebnisse vorhanden.")
    (progn
      (vl-mkdir tests-out-dir)
      (setq out-json (strcat tests-out-dir "/<modul>_results.json"))
      ;; ... JSON-Array schreiben ...
    )
  )
)

(defun c:TEST_<MODUL> (/ ...)
  (ssg-start "TEST_<MODUL>" '(("OSMODE") ("ATTREQ") ("ATTDIA")))
  (setvar "OSMODE" 0)
  (setvar "ATTREQ" 0)
  (setvar "ATTDIA" 0)

  ;; 1. Testdaten laden
  (setq testfaelle (omni:load-json
    (strcat (getenv "DXFMAKRO") "/tests/testdata/<modul>_tests.json")))

  ;; 2. Testfaelle ausfuehren, Ergebnisse sammeln
  (foreach eintrag testfaelle
    ;; ... Script-Funktion aufrufen, Ergebnis pruefen ...
  )

  ;; 3. Ergebnisse in globaler Variable speichern
  (setq *<modul>-test-results* (reverse results-list))

  (ssg-end)
)

Dann den Basisnamen in tests/alltests.json eintragen:

{ "name": "<modul>", "save": "dxf" }

"save" waehlen je nach Bedarf ("dxf", "dwg" oder null). test_run_all.lsp leitet alle Namen aus dem "name"-Feld ab — nichts in test_run_all.lsp aendern.

Namenskonventionen (werden von test_run_all.lsp automatisch abgeleitet)

Aus dem Basisnamen <name> in alltests.json entstehen durch Konvention:

Element Schema Beispiel fuer "name": "kreisel"
LISP-Datei tests/test_<name>.lsp tests/test_kreisel.lsp
BricsCAD-Befehl TEST_<NAME> (Grossbuchstaben) TEST_KREISEL
Export-Funktion <name>:export-results kreisel:export-results
JSON-Ergebnis output/<name>_results.json output/kreisel_results.json
DXF/DWG-Datei output/<name>_tests.dxf/.dwg output/kreisel_tests.dxf
Referenz (Soll) reference/<gleicher Name wie output> reference/kreisel_tests.dxf

Die Export-Funktion muss exakt so heissen: <name>:export-results (Namespace-Notation mit Doppelpunkt). Sie nimmt genau einen Parameter entgegen: den Pfad zum Output-Verzeichnis.

Wichtig:

  • omni:load-json erwartet ein flaches JSON-Array mit flachen Objekten (kein Nesting)
  • omni:val liest Werte aus den geladenen Eintraegen
  • Script-Funktionen (z.B. kreisel-insert-script) muessen ohne User-Interaktion funktionieren

Schritt 3: Python-Validierung erstellen

Neue test_<modul>.py erstellen:

import pytest

class TestModulAttributes:
    """Prueft Attribute gegen Testdefinitionen."""

    def test_all_executed(self, <modul>_results):
        """Alle Testfaelle muessen ausgefuehrt worden sein."""
        # ...

    def test_attribute_values(self, <modul>_testdata, <modul>_results):
        """Attributwerte muessen mit Erwartungen uebereinstimmen."""
        # ...

class TestModulGeometry:
    """Prueft DXF-Geometrie mit ezdxf."""

    def test_blocks_exist(self, <modul>_dxf):
        """Bloecke muessen im Modelspace vorhanden sein."""
        # ...

class TestModulReference:
    """Vergleicht gegen Referenz."""

    def test_matches_reference(self, <modul>_dxf, <modul>_ref_dxf):
        # ...

Schritt 4: Fixtures in conftest.py ergaenzen

Neue Fixtures fuer Testdaten, Ergebnisse und DXF in conftest.py hinzufuegen:

@pytest.fixture
def <modul>_testdata():
    path = os.path.join(_testdata_dir(), "<modul>_tests.json")
    return _load_json(path)

@pytest.fixture
def <modul>_results():
    path = os.path.join(_output_dir(), "<modul>_results.json")
    if not os.path.exists(path):
        pytest.skip("<modul>_results.json nicht vorhanden")
    return _load_json(path)

@pytest.fixture
def <modul>_dxf():
    path = os.path.join(_output_dir(), "<modul>_tests.dxf")
    if not os.path.exists(path):
        pytest.skip("<modul>_tests.dxf nicht vorhanden")
    return ezdxf.readfile(path)

@pytest.fixture
def <modul>_ref_dxf():
    # Konvention: Referenz = GLEICHER Dateiname wie das Ist-Ergebnis in output/.
    path = os.path.join(_reference_dir(), "<modul>_tests.dxf")
    if not os.path.exists(path):
        pytest.skip("reference/<modul>_tests.dxf nicht vorhanden - Referenz noch "
                    "nicht festgelegt (output/<modul>_tests.dxf nach reference/ kopieren)")
    return ezdxf.readfile(path)

Testklassen

test_kreisel.py

Klasse Prueft
TestKreiselAttributes Attribute (KREISELART, HOEHE), Block-Prefixe, Status
TestKreiselGeometry DXF-Bloecke, Positionen, Kreis-Radien (400mm)
TestKreiselReference Block-Anzahl und -Typen gegen Referenz-DXF

test_omniflo.py

Klasse Prueft
TestOmnifloExportUnit Mock-Export ueber lib/export_csv.py: Merkmale, Datentypen, Sum-Zeile, Vollstaendigkeit (ohne BricsCAD)
TestOmnifloResults omniflo_results.json aus BricsCAD: Testfaelle vollstaendig, Status OK, Hoehe
TestOmnifloReferenceCSV CSV-Export aus lib/export_csv.py gegen abgenommene Referenz-CSV

test_omniflo_strecke.py

Streckenzug 4 Geraden -> 180-Grad-Kurve (4x 45-Grad-Bogen) -> 4 Geraden. Die Geometrie wird ohne BricsCAD direkt aus omniflo_strecke_tests.json geprueft.

Klasse Prueft
TestStreckeStruktur 4+4+4 = 12 Elemente, Reihenfolge der Gruppen, Typen
TestStreckeKatalog Bogen-SivasNummern existieren in omniflo_boegen.json (Radius/Winkel)
TestStreckeGeometrie Lueckenlose Verkettung, Kurvensumme 180 Grad, gemeinsamer Bogen-Mittelpunkt
TestStreckeResults omniflo_strecke_results.json aus BricsCAD: Elemente vollstaendig, Status OK

Umgebungsvariablen

Werden von 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