[FEAT] VF-Linienzug ohne GUI/Konsole baubar (Stufe 0) + hundm05-Testfall
Ziel: eine VF_n-Kette soll aus einem Stapel Eingabedaten gebaut werden koennen - ohne Dialog, ohne Konsolenfrage. Fahrplan und Begruendungen in doc/TODO-plan-vf-interactive.md. Alle Aenderungen sind No-Ops, solange *ssg-gui-aus* und *vfl-headless* nil sind. Produktion (Lisp/vf_linienzug.lsp, Lisp/vf_core.lsp): - vfl-journal-reset im Dispatcher VOR die Modus-cond gezogen. Bisher nur im Modus-1-Zweig: ein frischer Modus-2-Lauf erbte das Journal des Vorlaufs und schrieb es in die XDATA, ein spaeterer Doppelklick spielte fremde Eingaben vor. - Lokale Variable "member" in vf-linienzug-modus2 umbenannt. Sie verdeckte im selben Scope das Builtin member, das weiter unten gebraucht wird - jedes Kletterer-Segment waere in "bad function" gelaufen. - Neu vfl-meldung: sammelt den Text nach *vfl-meldungen* + dbgmsg und zeigt ihn nur bei erlaubter GUI modal, sonst per princ. Die 12 Bau-Pfad-alerts darauf umgestellt; ein Alert blockierte sonst jeden Batch-Lauf, und sein Text ist die einzige Auskunft, WELCHE Sektion abgewiesen wurde. Die reinen Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben alert. - Neu *vfl-headless* (+ vfl-headless-p/-abbruch/-notausgang/-ort, Diagnose *vfl-headless-fehler*, optionaler Antwort-Hook *vfl-headless-antwort-fn*): eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf Live-Eingabe. Eingebaut in vfl-in-value, vfl-in-value-p, vfl-in-selection und vfl-in-abstand. - vfl-journal-reset loescht Meldungen und Diagnose mit (gehoeren zum Lauf); vfl-view-refresh ueberspringt headless _PLAN/_ZOOM. Testfall HundM05 (5 echte Ketten aus data/polylines.dxf): - tests/testdata/hundm05.json neu erzeugt aus den XDATA-Journalen der VF_n-Bloecke (lib/vf_journal_export.py) - flach, weil ssg-load-json zeilenweise liest. Die drei kopierten Ketten bekommen ihren echten Einfuegepunkt, nicht das veraltete HOEHE_VON-Attribut. - tests/test_hundm05.lsp arbeitet jetzt per Journal-Replay statt mit Eingabe-Mocks: ein echtes Journal fuehrt die geerbte Fahrtrichtung nicht mit (vfl-in-abstand journalisiert hz nur beim ersten Segment), ein Mock kann sie also nicht kennen. Schaltet *vfl-headless* ein und schreibt prompts, headless_fehler und meldungen ins Ergebnis-JSON. - Kettenschleife fangt je Kette: ein Fehler NACH dem Bau nimmt nicht mehr die restlichen Ketten mit. - entprev gibt es in AutoLISP nicht (nur entnext/entlast) - die Suche nach dem fertigen Block laeuft vorwaerts ab dem Zeichnungsstand vor dem Bau. Dieselbe Falle in tests/test_mubea.lsp mitbehoben; sie schlug dort nie zu, weil entlast immer sofort traf. Absicherung ohne CAD: - tests/test_vf_headless_statisch.py: eingechecktes Inventar aller alert/get*/ssget/new_dialog-Fundstellen je Funktion (ein neues getreal in einer Bau-Funktion faellt auf, auch wenn sein Zweig im Test nie erreicht wird), Praesenz des Riegels in allen vier Wrappern, Diagnose-Reset und die Reset-Reihenfolge im Dispatcher. Dazu ein Waechter gegen erfundene AutoLISP-Funktionen (entprev u.a.) - diese Fehlerklasse kostet sonst jedes Mal einen CAD-Lauf. - tests/test_hundm05.py prueft zusaetzlich prompts == 0, keine Headless-Abbrueche und keine Bau-Meldungen. tests/alltests.json: hundm05-Zeile laedt VarioFoerderer (nicht KreiselInsert) und bleibt bis zu einem gruenen CAD-Lauf abgeschaltet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+9
-2
@@ -102,7 +102,7 @@ Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module.
|
||||
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
|
||||
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
|
||||
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
|
||||
{ "name": "hundm05", "save": "dxf", "module": "KreiselInsert", "disabled": true }
|
||||
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
|
||||
]
|
||||
```
|
||||
|
||||
@@ -160,7 +160,14 @@ tests/
|
||||
test_foerderer.py # pytest-Validierung Foerderer (Status/Mathematik/CSV/Sivas)
|
||||
test_gefaellestrecke.py # pytest-Validierung Gefaellestrecke
|
||||
test_mubea.py # pytest-Validierung Mubea-Gesamtmodell
|
||||
test_hundm05.py # pytest-Validierung HundM05-Abschnitt
|
||||
test_hundm05.py # pytest-Validierung HundM05 (5 Linienzuege)
|
||||
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
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@
|
||||
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
|
||||
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
|
||||
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
|
||||
{ "name": "hundm05", "save": "dxf", "module": "KreiselInsert", "disabled": true }
|
||||
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
|
||||
]
|
||||
|
||||
+534
-468
File diff suppressed because it is too large
Load Diff
+299
-179
@@ -1,66 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_hundm05.py - Validiert den HundM-Kreisel-Abschnitt.
|
||||
test_hundm05.py - Validiert die 5 VarioFoerderer-Linienzuege der Anlage HundM.
|
||||
|
||||
Prueft die von TEST_HUNDM05 (test_hundm05.lsp) erzeugten Ergebnisse:
|
||||
1. Alle Eintraege aus hundm05.json wurden verarbeitet (results.json)
|
||||
2. Statuswerte (executed / placeholder fuer Sensoren / unsupported Omniflo)
|
||||
3. Kreisel-Attribute (HOEHE, KREISELART, Block-Praefix)
|
||||
4. Vario: VF_-Block, L_GF gleichmaessig verteilt (kein L_GF1 in den Daten)
|
||||
5. DXF-Geometrie: Block-Anzahlen je Typ (ezdxf)
|
||||
Zwei Ebenen:
|
||||
1. Die Testdaten selbst (tests/testdata/hundm05.json): Struktur der
|
||||
Eingabe-Journale und Lesbarkeit durch den LISP-Parser.
|
||||
2. Die von TEST_HUNDM05 (test_hundm05.lsp) erzeugten Ergebnisse
|
||||
(output/hundm05_results.json + output/hundm05_tests.dxf): jede Kette
|
||||
gebaut, kein Journal-Wert offen, Attribute passen zu den Eingaben.
|
||||
|
||||
Die Testdaten stammen aus lib/dxf_scan_components.py (Fenster
|
||||
216000 144000 228000 156000 der Zeichnung results/ST500592_05.dxf) und nicht
|
||||
aus Handarbeit - anders als bei mubea.json.
|
||||
Die Testdaten stammen aus lib/vf_journal_export.py (Journale der VF_n-Bloecke
|
||||
aus data/polylines.dxf, XDATA-App SSG_VF_EDIT) und nicht aus Handarbeit -
|
||||
Schema siehe tests/testdata/object_data.md Abschnitt 8.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
EINGABE_TYPEN = ("point_abs", "point_rel", "real", "int", "string", "step")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Klassifikation der hundm05.json-Eintraege
|
||||
# Hilfsfunktionen: flaches JSON in Ketten gruppieren
|
||||
# ============================================================
|
||||
# Reihenfolge wie im Dispatcher von test_hundm05.lsp: Omniflo VOR Kreisel,
|
||||
# weil Omniflo-Eintraege sowohl "id" als auch "type" tragen.
|
||||
# Gleiche Regel wie hundm05:gruppiere in test_hundm05.lsp: ein Objekt mit
|
||||
# "test_id" ist ein Kopf und beginnt eine Kette, die folgenden Objekte mit
|
||||
# "typ" sind ihre Eingaben.
|
||||
|
||||
def _is_sensor(item):
|
||||
return "block" in item
|
||||
def _ketten(testdata):
|
||||
ketten = []
|
||||
for obj in testdata:
|
||||
if "test_id" in obj:
|
||||
ketten.append((obj, []))
|
||||
elif "typ" in obj and ketten:
|
||||
ketten[-1][1].append(obj)
|
||||
return ketten
|
||||
|
||||
|
||||
def _is_omniflo(item):
|
||||
return "type" in item and "block" not in item
|
||||
def _journal_laenge(eingaben):
|
||||
"""Anzahl der Journal-Eintraege, die der Treiber daraus baut: ein
|
||||
point_rel mit "hz" wird zu ZWEI Eintraegen (DL + REAL)."""
|
||||
return sum(2 if (e["typ"] == "point_rel" and "hz" in e) else 1
|
||||
for e in eingaben)
|
||||
|
||||
|
||||
def _is_kreisel(item):
|
||||
return "id" in item and "type" not in item and "block" not in item
|
||||
|
||||
|
||||
def _hat_prefix(item, prefix):
|
||||
return str(item.get("test_id", "")).startswith(prefix)
|
||||
|
||||
|
||||
def _is_vario(item):
|
||||
return _hat_prefix(item, "VF_")
|
||||
|
||||
|
||||
def _is_gefaelle(item):
|
||||
return _hat_prefix(item, "GF_")
|
||||
|
||||
|
||||
def _is_gerade(item):
|
||||
return _hat_prefix(item, "GERADE_")
|
||||
|
||||
|
||||
def _expected_counts(testdata):
|
||||
return {
|
||||
"kreisel": sum(1 for t in testdata if _is_kreisel(t)),
|
||||
"vario": sum(1 for t in testdata if _is_vario(t)),
|
||||
"gefaellestrecke": sum(1 for t in testdata if _is_gefaelle(t)),
|
||||
"gerade": sum(1 for t in testdata if _is_gerade(t)),
|
||||
"sensor": sum(1 for t in testdata if _is_sensor(t)),
|
||||
"omniflo": sum(1 for t in testdata if _is_omniflo(t)),
|
||||
}
|
||||
def _pfad(*teile):
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), *teile)
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -69,34 +58,106 @@ def _expected_counts(testdata):
|
||||
|
||||
class TestHundM05Testdaten:
|
||||
|
||||
def test_jeder_eintrag_klassifizierbar(self, hundm05_testdata):
|
||||
"""Kein Eintrag darf durch alle Zweige des Dispatchers fallen."""
|
||||
for i, t in enumerate(hundm05_testdata):
|
||||
erkannt = (_is_sensor(t) or _is_omniflo(t) or _is_kreisel(t)
|
||||
or _is_vario(t) or _is_gefaelle(t) or _is_gerade(t))
|
||||
assert erkannt, \
|
||||
f"Eintrag {i} ist keinem Typ zuzuordnen: {sorted(t)}"
|
||||
def test_fuenf_ketten(self, hundm05_testdata):
|
||||
"""Die Anlage hat fuenf am Kreisel haengende Linienzuege."""
|
||||
ketten = _ketten(hundm05_testdata)
|
||||
assert len(ketten) == 5, \
|
||||
f"{len(ketten)} Ketten in hundm05.json, erwartet 5"
|
||||
|
||||
def test_kreisel_vorhanden(self, hundm05_testdata):
|
||||
"""Der Abschnitt wurde wegen des Kreisels gewaehlt - er muss drin sein."""
|
||||
kreisel = [t for t in hundm05_testdata if _is_kreisel(t)]
|
||||
assert kreisel, "kein Kreisel in hundm05.json"
|
||||
def test_jedes_objekt_zuordenbar(self, hundm05_testdata):
|
||||
"""Kein Objekt darf zwischen Kopf und Eingabe fallen - sonst
|
||||
verschluckt hundm05:gruppiere es stillschweigend."""
|
||||
for i, obj in enumerate(hundm05_testdata):
|
||||
assert ("test_id" in obj) or ("typ" in obj), \
|
||||
f"Objekt {i} ist weder Kopf noch Eingabe: {sorted(obj)}"
|
||||
|
||||
def test_vario_hat_hoehendifferenz(self, hundm05_testdata):
|
||||
"""VarioFoerderer brauchen deltaH > 0 - der Winkel-Solver findet sonst
|
||||
keinen Bogenwinkel. Waagrechte Paare gehoeren als 'gerade' ins JSON
|
||||
(siehe GERADE_MAX_DELTA_H in lib/dxf_scan_components.py)."""
|
||||
for t in hundm05_testdata:
|
||||
if _is_vario(t):
|
||||
assert float(t.get("deltaH", 0)) > 0, \
|
||||
f'{t["test_id"]}: deltaH={t.get("deltaH")} - gehoert als ' \
|
||||
f'"gerade" klassifiziert'
|
||||
def test_kopf_felder(self, hundm05_testdata):
|
||||
for kopf, _ in _ketten(hundm05_testdata):
|
||||
assert kopf["test_id"].startswith("VF_"), kopf["test_id"]
|
||||
# "typ" wuerde den Kopf zu einer Eingabe machen (siehe
|
||||
# hundm05:gruppiere) - der Anlagetyp heisst darum "anlagetyp".
|
||||
assert "typ" not in kopf, \
|
||||
f'{kopf["test_id"]}: Kopf darf kein Feld "typ" tragen'
|
||||
assert kopf["anlagetyp"] == "linienzug", kopf["test_id"]
|
||||
assert kopf["modus"] == 1, kopf["test_id"]
|
||||
assert kopf["block"].startswith("VF_"), kopf["block"]
|
||||
assert len(kopf["startpunkt_mm"]) == 3, kopf["test_id"]
|
||||
|
||||
def test_gerade_hat_laenge(self, hundm05_testdata):
|
||||
for t in hundm05_testdata:
|
||||
if _is_gerade(t):
|
||||
assert float(t.get("laenge", 0)) > 0, \
|
||||
f'{t["test_id"]}: laenge={t.get("laenge")}'
|
||||
def test_anzahl_eingaben_stimmt(self, hundm05_testdata):
|
||||
"""anzahl_eingaben ist die Selbstauskunft des Kopfes - weicht sie ab,
|
||||
fehlen Eingaben (z.B. durch eine Handaenderung)."""
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
assert kopf["anzahl_eingaben"] == len(eingaben), \
|
||||
f'{kopf["test_id"]}: anzahl_eingaben=' \
|
||||
f'{kopf["anzahl_eingaben"]}, gezaehlt {len(eingaben)}'
|
||||
|
||||
def test_eingabe_typen_bekannt(self, hundm05_testdata):
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
for i, e in enumerate(eingaben):
|
||||
assert e["typ"] in EINGABE_TYPEN, \
|
||||
f'{kopf["test_id"]} Eingabe {i}: unbekannter Typ ' \
|
||||
f'{e["typ"]}'
|
||||
assert e.get("kommentar"), \
|
||||
f'{kopf["test_id"]} Eingabe {i}: kein Kommentar'
|
||||
|
||||
def test_kette_beginnt_mit_startpunkt(self, hundm05_testdata):
|
||||
"""Erste Eingabe = absoluter Startpunkt, zweite = Starthoehe. Fehlt
|
||||
der Punkt, wuerde das Replay ihn interaktiv erfragen und die Kette an
|
||||
der falschen Stelle aufbauen (siehe hundm05:build-linienzug)."""
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
assert eingaben[0]["typ"] == "point_abs", kopf["test_id"]
|
||||
assert eingaben[1]["typ"] == "real", kopf["test_id"]
|
||||
punkt = eingaben[0]["wert"]
|
||||
assert len(punkt) == 3, kopf["test_id"]
|
||||
assert punkt == kopf["startpunkt_mm"], \
|
||||
f'{kopf["test_id"]}: point_abs {punkt} != startpunkt_mm ' \
|
||||
f'{kopf["startpunkt_mm"]}'
|
||||
assert eingaben[1]["wert"] == punkt[2], \
|
||||
f'{kopf["test_id"]}: Starthoehe {eingaben[1]["wert"]} != ' \
|
||||
f'Z des Startpunkts {punkt[2]}'
|
||||
|
||||
def test_hz_nur_beim_ersten_segment(self, hundm05_testdata):
|
||||
"""vfl-in-abstand journalisiert die Fahrtrichtung NUR beim ersten
|
||||
Segment der Kette; jedes weitere erbt sie vom Vorgaenger. Ein "hz" an
|
||||
spaeterer Stelle waere ein Wert zu viel in der Replay-Queue und
|
||||
verschiebt alle folgenden Antworten (Desync)."""
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
rel = [e for e in eingaben if e["typ"] == "point_rel"]
|
||||
assert rel, f'{kopf["test_id"]}: kein Segment'
|
||||
assert "hz" in rel[0], \
|
||||
f'{kopf["test_id"]}: erstes Segment ohne hz'
|
||||
for i, e in enumerate(rel[1:], start=2):
|
||||
assert "hz" not in e, \
|
||||
f'{kopf["test_id"]}: Segment {i} traegt hz - nur das ' \
|
||||
f'erste Segment darf eines haben'
|
||||
|
||||
def test_segmentlaengen_positiv(self, hundm05_testdata):
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
for e in eingaben:
|
||||
if e["typ"] == "point_rel":
|
||||
assert e["dL"] > 0, f'{kopf["test_id"]}: dL={e["dL"]}'
|
||||
|
||||
def test_menue_antworten_sind_strings(self, hundm05_testdata):
|
||||
"""Menue-Antworten muessen Strings bleiben ("1"/"2"/"90"/"30"): der
|
||||
Produktionscode vergleicht mit (= antwort "1"), eine Zahl wuerde
|
||||
stumm in den Default-Zweig laufen."""
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
for i, e in enumerate(eingaben):
|
||||
if e["typ"] in ("string", "step"):
|
||||
assert isinstance(e["wert"], str), \
|
||||
f'{kopf["test_id"]} Eingabe {i}: {e["wert"]!r} ist ' \
|
||||
f'kein String'
|
||||
|
||||
def test_glieder_vorhanden(self, hundm05_testdata):
|
||||
"""Jede Kette hat Glied-Marker (STEP) - sie sind der Anker fuer den
|
||||
Sektions-/Glied-Edit und die Kurzbeschreibung."""
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
glieder = [e["wert"] for e in eingaben if e["typ"] == "step"]
|
||||
assert glieder, f'{kopf["test_id"]}: kein STEP-Marker'
|
||||
for g in glieder:
|
||||
assert g in ("Linie-GF", "Linie-VF", "Linie", "GF-Bogen",
|
||||
"Horizontal-VF", "Vario-Kurve", "ES"), \
|
||||
f'{kopf["test_id"]}: unbekanntes Glied {g}'
|
||||
|
||||
def test_arrays_einzeilig(self):
|
||||
"""Jedes Array muss komplett in EINER Zeile stehen.
|
||||
@@ -104,14 +165,11 @@ class TestHundM05Testdaten:
|
||||
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei zeilenweise,
|
||||
und ssg-cfg-parse-array braucht das ganze Array in einem String. Ein
|
||||
von json.dump(indent=2) umgebrochenes Array wird zu nil - der Treiber
|
||||
baut das Element dann nicht an seiner Position. Das ist genau der
|
||||
baut die Kette dann nicht an ihrer Position. Das ist genau der
|
||||
Fehler, der die Anlage einmal komplett am Ursprung aufgebaut hat, und
|
||||
er ist in der Zeichnung nicht als Datenfehler zu erkennen.
|
||||
"""
|
||||
import os
|
||||
pfad = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"testdata", "hundm05.json")
|
||||
with open(pfad, encoding="utf-8") as fh:
|
||||
with open(_pfad("testdata", "hundm05.json"), encoding="utf-8") as fh:
|
||||
for nr, zeile in enumerate(fh, start=1):
|
||||
offen = zeile.count("[") - zeile.count("]")
|
||||
# Die aeusserste Klammer des Dokuments darf allein stehen.
|
||||
@@ -121,6 +179,33 @@ class TestHundM05Testdaten:
|
||||
f"Zeile {nr}: Array ueber mehrere Zeilen umgebrochen - " \
|
||||
f"ssg-cfg-parse-array kann das nicht lesen: {zeile.strip()!r}"
|
||||
|
||||
def test_flach_fuer_lisp_parser(self, hundm05_testdata):
|
||||
"""Die Datei muss FLACH bleiben.
|
||||
|
||||
ssg-load-json / ssg-parse-json-array behandelt jedes "{" als
|
||||
Objektanfang und jedes "}"/"}," als Objektende - es kennt keine
|
||||
Verschachtelung. Ein Objekt in einem Objekt (oder ein Array von
|
||||
Objekten wie "eingaben": [...]) wuerde das umgebende Objekt
|
||||
verschlucken. Dieser Test zaehlt die Objekte so, wie der LISP-Parser
|
||||
sie zaehlen wuerde, und vergleicht mit dem echten JSON.
|
||||
"""
|
||||
objekte = 0
|
||||
with open(_pfad("testdata", "hundm05.json"), encoding="utf-8") as fh:
|
||||
for zeile in fh:
|
||||
gestrippt = zeile.strip()
|
||||
if gestrippt == "{":
|
||||
objekte += 1
|
||||
elif gestrippt in ("}", "},"):
|
||||
continue
|
||||
elif gestrippt.startswith('"'):
|
||||
# Ein Wert, der ein Objekt oder eine Objektliste eroeffnet,
|
||||
# macht die Datei fuer den LISP-Parser unlesbar.
|
||||
assert not gestrippt.endswith(("{", "[")), \
|
||||
f"verschachtelter Wert: {gestrippt!r}"
|
||||
assert objekte == len(hundm05_testdata), \
|
||||
f"LISP-Parser saehe {objekte} Objekte, json.load sieht " \
|
||||
f"{len(hundm05_testdata)}"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Ergebnis-Pruefung (results.json vs. testdata)
|
||||
@@ -128,100 +213,147 @@ class TestHundM05Testdaten:
|
||||
|
||||
class TestHundM05Results:
|
||||
|
||||
def test_alle_verarbeitet(self, hundm05_testdata, hundm05_results):
|
||||
"""Je Kind muss die erwartete Anzahl an Ergebnissen vorliegen."""
|
||||
expected = _expected_counts(hundm05_testdata)
|
||||
got = {}
|
||||
def test_je_kette_ein_ergebnis(self, hundm05_testdata, hundm05_results):
|
||||
soll = [k["test_id"] for k, _ in _ketten(hundm05_testdata)]
|
||||
ist = [r["test_id"] for r in hundm05_results]
|
||||
assert ist == soll, f"Ergebnisse {ist}, erwartet {soll}"
|
||||
for r in hundm05_results:
|
||||
got[r["kind"]] = got.get(r["kind"], 0) + 1
|
||||
for kind, n in expected.items():
|
||||
assert got.get(kind, 0) == n, \
|
||||
f'{kind}: erwartet {n}, verarbeitet {got.get(kind, 0)}'
|
||||
assert r["kind"] == "linienzug", f'{r["test_id"]}: {r["kind"]}'
|
||||
|
||||
def test_status_ok(self, hundm05_results):
|
||||
"""Sensoren duerfen 'placeholder', Omniflo ist 'unsupported'
|
||||
(omni:insert-dxf ist nicht scriptbar, siehe object_data.md 4.1),
|
||||
alles andere muss 'executed' sein."""
|
||||
def test_status_executed(self, hundm05_results):
|
||||
""""desync" (Journal-Werte nicht verbraucht) und "failed" (Abbruch)
|
||||
sind beide echte Fehler - siehe hundm05:build-linienzug."""
|
||||
for r in hundm05_results:
|
||||
if r["kind"] == "sensor":
|
||||
erlaubt = ("executed", "placeholder")
|
||||
elif r["kind"] == "omniflo":
|
||||
erlaubt = ("unsupported",)
|
||||
else:
|
||||
erlaubt = ("executed",)
|
||||
assert r["status"] in erlaubt, \
|
||||
f'{r["test_id"]} ({r["kind"]}): Status={r["status"]}, ' \
|
||||
f'erlaubt {erlaubt}'
|
||||
assert r["status"] == "executed", \
|
||||
f'{r["test_id"]}: Status={r["status"]}'
|
||||
|
||||
def test_kreisel_attribute(self, hundm05_testdata, hundm05_results):
|
||||
"""Kreisel: KREISELART und HOEHE stimmen mit der Vorgabe, Blockname
|
||||
traegt das Praefix KREISEL_. HOEHE wird von kreisel-connect-script aus
|
||||
'hoehe' bzw. dem Z des Startpunkts gebildet (rtos ... 2 0, also
|
||||
ganzzahlig)."""
|
||||
by_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for t in hundm05_testdata:
|
||||
if not _is_kreisel(t):
|
||||
continue
|
||||
r = by_id.get(t["id"])
|
||||
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
|
||||
assert r["block_name"].startswith("KREISEL_"), \
|
||||
f'{t["id"]}: Block "{r["block_name"]}" ohne Praefix KREISEL_'
|
||||
assert r["actual_attributes"].get("KREISELART") == \
|
||||
t.get("typ", "STANDARD"), \
|
||||
f'{t["id"]}: KREISELART ist=' \
|
||||
f'{r["actual_attributes"].get("KREISELART")}'
|
||||
soll = t.get("hoehe", t.get("start_z", 0.0))
|
||||
assert r["actual_attributes"].get("HOEHE") == str(int(round(soll))), \
|
||||
f'{t["id"]}: HOEHE ist={r["actual_attributes"].get("HOEHE")}, ' \
|
||||
f'erwartet={int(round(soll))}'
|
||||
def test_journal_vollstaendig_verbraucht(self, hundm05_testdata,
|
||||
hundm05_results):
|
||||
"""Alle aufgezeichneten Werte muessen im Bau abgerufen worden sein.
|
||||
Bleibt etwas offen, ist der Ablauf von der Aufzeichnung abgewichen und
|
||||
die Geometrie kann der Vorlage nicht entsprechen."""
|
||||
soll = {k["test_id"]: _journal_laenge(e)
|
||||
for k, e in _ketten(hundm05_testdata)}
|
||||
for r in hundm05_results:
|
||||
assert "eingaben_offen" in r, (
|
||||
f'{r["test_id"]}: kein eingaben_offen im Ergebnis - die Kette '
|
||||
f'ist vor der Auswertung abgebrochen (status='
|
||||
f'{r.get("status")!r}, {r.get("fehler_text")})')
|
||||
assert r["eingaben_offen"] == 0, \
|
||||
f'{r["test_id"]}: {r["eingaben_offen"]} Journal-Werte offen'
|
||||
assert r["eingaben_gesamt"] == soll[r["test_id"]], \
|
||||
f'{r["test_id"]}: {r["eingaben_gesamt"]} Journal-Eintraege ' \
|
||||
f'abgespielt, erwartet {soll[r["test_id"]]}'
|
||||
|
||||
def test_kreisel_abstand(self, hundm05_testdata, hundm05_results):
|
||||
"""Bei 'connect' berechnet kreisel-connect-script den Abstand als
|
||||
Distanz(Start,Ende) - Kreiseldurchmesser (800mm). Der Block muss also
|
||||
auf dem Startpunkt sitzen."""
|
||||
by_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for t in hundm05_testdata:
|
||||
if not _is_kreisel(t) or t.get("function") != "connect":
|
||||
continue
|
||||
r = by_id.get(t["id"])
|
||||
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
|
||||
ip = r["insert_point"]
|
||||
assert abs(ip[0] - t["start_x"]) < 1.0, \
|
||||
f'{t["id"]}: Einfuegepunkt X {ip[0]} != start_x {t["start_x"]}'
|
||||
assert abs(ip[1] - t["start_y"]) < 1.0, \
|
||||
f'{t["id"]}: Einfuegepunkt Y {ip[1]} != start_y {t["start_y"]}'
|
||||
def test_keine_live_eingabe(self, hundm05_results):
|
||||
"""Kein Prompt, kein Dialog: der Treiber schaltet *vfl-headless* ein
|
||||
(harter Abbruch statt stillem Rueckfall auf Live-Eingabe, siehe
|
||||
vfl-headless-abbruch in Lisp/vf_linienzug.lsp) UND ersetzt zusaetzlich
|
||||
getpoint/getstring/getint/getreal durch zaehlende Stubs. Beide Wege
|
||||
muessen leer bleiben - "prompts" ist der positive Beweis, dass keine
|
||||
Eingabe ausserhalb der vfl-in-*-Wrapper erreicht wurde."""
|
||||
for r in hundm05_results:
|
||||
if "prompts" in r:
|
||||
assert r["prompts"] == 0, \
|
||||
f'{r["test_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
|
||||
assert not r.get("headless_fehler"), \
|
||||
f'{r["test_id"]}: Headless-Abbruch bei {r["headless_fehler"]}'
|
||||
|
||||
def test_vario_block_und_split(self, hundm05_testdata, hundm05_results):
|
||||
"""Vario: VF_-Block, und ohne L_GF1-Vorgabe wird L_GF gleichmaessig auf
|
||||
Vor- und Nachlauf verteilt (Default nach object_data.md 3.1)."""
|
||||
by_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for t in hundm05_testdata:
|
||||
if not _is_vario(t):
|
||||
continue
|
||||
r = by_id.get(t["test_id"])
|
||||
assert r, f'Vario {t["test_id"]} fehlt im Ergebnis'
|
||||
def test_keine_geometrie_meldungen(self, hundm05_results):
|
||||
"""*vfl-meldungen* sammelt die abgewiesenen Sektionen (frueher
|
||||
blockierende alert-Fenster, jetzt vfl-meldung). Eine Meldung heisst:
|
||||
ein Glied liess sich geometrisch nicht bauen - der Nachbau weicht dann
|
||||
von der Vorlage ab, auch wenn das Journal aufgeht."""
|
||||
for r in hundm05_results:
|
||||
assert not r.get("meldungen"), \
|
||||
f'{r["test_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
|
||||
|
||||
def test_vf_block_erzeugt(self, hundm05_results):
|
||||
for r in hundm05_results:
|
||||
assert r["block_name"].startswith("VF_"), \
|
||||
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
|
||||
attrs = r["actual_attributes"]
|
||||
if "L_GF1" in t or "L_GF_m" not in attrs:
|
||||
continue
|
||||
teile = attrs["L_GF_m"].split(",")
|
||||
assert len(teile) == 2, \
|
||||
f'{t["test_id"]}: L_GF_m Format "{attrs["L_GF_m"]}"'
|
||||
vorne, hinten = float(teile[0]), float(teile[1])
|
||||
assert abs(vorne - hinten) < 0.002, \
|
||||
f'{t["test_id"]}: L_GF nicht gleichmaessig verteilt ' \
|
||||
f'({vorne} / {hinten})'
|
||||
f'{r["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
|
||||
assert r["block_handle"], f'{r["test_id"]}: kein Handle'
|
||||
|
||||
def test_gefaelle_block(self, hundm05_testdata, hundm05_results):
|
||||
by_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for t in hundm05_testdata:
|
||||
if not _is_gefaelle(t):
|
||||
def test_dimension_einheitlich(self, hundm05_results):
|
||||
"""Alle Ergebnisse eines Laufs stammen aus EINER Bau-Dimension.
|
||||
|
||||
Die Eingabedaten sind dimensionsfrei; ob _2D- oder _3D-Blockvarianten
|
||||
verwendet werden, loest ssg-ils-dim-aktuell auf. Ein Lauf, in dem beide
|
||||
vorkommen, waere ein halb umgeschalteter Zustand (z.B. weil ein
|
||||
vorheriger Test *ssg-ils-dim* nicht zurueckgesetzt hat).
|
||||
"""
|
||||
dims = {r.get("dimension") for r in hundm05_results}
|
||||
assert len(dims) == 1, f"gemischte Bau-Dimensionen im Lauf: {dims}"
|
||||
dim = dims.pop()
|
||||
assert dim in ("2D", "3D"), \
|
||||
f'Bau-Dimension "{dim}" ist weder 2D noch 3D'
|
||||
|
||||
def test_startpunkt_getroffen(self, hundm05_testdata, hundm05_results):
|
||||
"""Der VF_n-Block wird mit dem Kettenstartpunkt als Basispunkt
|
||||
gewickelt (ssg-block-wrap-welt in vfl-block-erstellen) - sein
|
||||
Einfuegepunkt muss also der Startpunkt aus den Eingaben sein."""
|
||||
soll = {k["test_id"]: k["startpunkt_mm"]
|
||||
for k, _ in _ketten(hundm05_testdata)}
|
||||
for r in hundm05_results:
|
||||
p = soll[r["test_id"]]
|
||||
ist = r["insert_point"]
|
||||
for achse, i in (("X", 0), ("Y", 1), ("Z", 2)):
|
||||
assert abs(ist[i] - p[i]) < 1.0, \
|
||||
f'{r["test_id"]}: Einfuegepunkt {achse} {ist[i]} != ' \
|
||||
f'{p[i]}'
|
||||
|
||||
def test_as_es_attribute_aus_eingaben(self, hundm05_testdata,
|
||||
hundm05_results):
|
||||
"""WINKEL_AS/SEITE_AS und WINKEL_ES/SEITE_ES muessen die Antworten aus
|
||||
dem Journal wiedergeben (AS steht in der Praeambel, ES im ES-Glied)."""
|
||||
seiten = {"1": "links", "2": "rechts"}
|
||||
nach_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
r = nach_id[kopf["test_id"]]
|
||||
attrs = r["actual_attributes"]
|
||||
# Praeambel: [0] Punkt, [1] Hoehe, [2] AS setzen, [3] Winkel,
|
||||
# [4] Seite
|
||||
if eingaben[2]["wert"] == "1":
|
||||
assert attrs.get("WINKEL_AS") == eingaben[3]["wert"], \
|
||||
f'{kopf["test_id"]}: WINKEL_AS=' \
|
||||
f'{attrs.get("WINKEL_AS")}, erwartet {eingaben[3]["wert"]}'
|
||||
assert attrs.get("SEITE_AS") == seiten[eingaben[4]["wert"]], \
|
||||
f'{kopf["test_id"]}: SEITE_AS={attrs.get("SEITE_AS")}'
|
||||
# ES: die beiden Strings direkt nach dem STEP-Marker "ES"
|
||||
for i, e in enumerate(eingaben):
|
||||
if e["typ"] == "step" and e["wert"] == "ES":
|
||||
assert attrs.get("WINKEL_ES") == eingaben[i + 1]["wert"], \
|
||||
f'{kopf["test_id"]}: WINKEL_ES=' \
|
||||
f'{attrs.get("WINKEL_ES")}'
|
||||
assert attrs.get("SEITE_ES") == \
|
||||
seiten[eingaben[i + 2]["wert"]], \
|
||||
f'{kopf["test_id"]}: SEITE_ES={attrs.get("SEITE_ES")}'
|
||||
break
|
||||
|
||||
def test_vario_kurven_gezaehlt(self, hundm05_testdata, hundm05_results):
|
||||
"""Jede Vario-Kurve im Journal muss in den VF_Bogen_*-Attributen
|
||||
auftauchen (vfl-insert-vario-kurve-block zaehlt sie mit)."""
|
||||
nach_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
soll = sum(1 for e in eingaben
|
||||
if e["typ"] == "step" and e["wert"] == "Vario-Kurve")
|
||||
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
|
||||
ist = sum(int(attrs.get(f"VF_Bogen_{lage}_{w}", 0) or 0)
|
||||
for lage in ("A", "I") for w in (90, 60, 30))
|
||||
assert ist == soll, \
|
||||
f'{kopf["test_id"]}: {ist} Vario-Kurven in den Attributen, ' \
|
||||
f'{soll} im Journal'
|
||||
|
||||
def test_hoehe_von_ist_starthoehe(self, hundm05_testdata,
|
||||
hundm05_results):
|
||||
nach_id = {r["test_id"]: r for r in hundm05_results}
|
||||
for kopf, eingaben in _ketten(hundm05_testdata):
|
||||
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
|
||||
if "HOEHE_VON_mm" not in attrs:
|
||||
continue
|
||||
r = by_id.get(t["test_id"])
|
||||
assert r, f'Gefaellestrecke {t["test_id"]} fehlt im Ergebnis'
|
||||
assert r["block_name"].startswith("GF_"), \
|
||||
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein GF_-Block'
|
||||
assert abs(int(attrs["HOEHE_VON_mm"]) - eingaben[1]["wert"]) <= 1, \
|
||||
f'{kopf["test_id"]}: HOEHE_VON_mm={attrs["HOEHE_VON_mm"]}, ' \
|
||||
f'Starthoehe {eingaben[1]["wert"]}'
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -230,24 +362,12 @@ class TestHundM05Results:
|
||||
|
||||
class TestHundM05Geometry:
|
||||
|
||||
def _counts(self, dxf):
|
||||
c = {}
|
||||
for e in dxf.modelspace():
|
||||
if e.dxftype() == "INSERT":
|
||||
c[e.dxf.name] = c.get(e.dxf.name, 0) + 1
|
||||
return c
|
||||
|
||||
def test_block_anzahlen(self, hundm05_dxf, hundm05_testdata):
|
||||
"""Anzahl der KREISEL_/VF_/GF_-Bloecke >= Vorgabe aus hundm05.json."""
|
||||
c = self._counts(hundm05_dxf)
|
||||
exp = _expected_counts(hundm05_testdata)
|
||||
n_kr = sum(v for n, v in c.items() if n.startswith("KREISEL_"))
|
||||
n_vf = sum(v for n, v in c.items() if n.startswith("VF_"))
|
||||
n_gf = sum(v for n, v in c.items() if n.startswith("GF_"))
|
||||
assert n_kr >= exp["kreisel"], f"KREISEL_: {n_kr} < {exp['kreisel']}"
|
||||
assert n_vf >= exp["vario"], f"VF_: {n_vf} < {exp['vario']}"
|
||||
assert n_gf >= exp["gefaellestrecke"], \
|
||||
f"GF_: {n_gf} < {exp['gefaellestrecke']}"
|
||||
def test_block_anzahl(self, hundm05_dxf, hundm05_testdata):
|
||||
"""Je Kette muss ein VF_-Block im Modelspace stehen."""
|
||||
n_vf = sum(1 for e in hundm05_dxf.modelspace()
|
||||
if e.dxftype() == "INSERT" and e.dxf.name.startswith("VF_"))
|
||||
soll = len(_ketten(hundm05_testdata))
|
||||
assert n_vf >= soll, f"VF_: {n_vf} < {soll}"
|
||||
|
||||
def test_handles_im_modelspace(self, hundm05_dxf, hundm05_results):
|
||||
"""Jedes gebaute Block-Handle (executed) liegt im Modelspace."""
|
||||
|
||||
+43
-12
@@ -69,18 +69,49 @@
|
||||
)
|
||||
|
||||
|
||||
;; --- Letzten INSERT mit Blocknamen-Praefix (rueckwaerts) suchen ---
|
||||
(defun mubea:last-insert-prefix (pref / ent found ed bn)
|
||||
(setq ent (entlast))
|
||||
(while (and ent (null found))
|
||||
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
|
||||
(if (and (= (cdr (assoc 0 ed)) "INSERT") bn
|
||||
(>= (strlen bn) (strlen pref))
|
||||
(= (substr bn 1 (strlen pref)) pref))
|
||||
(setq found ent))
|
||||
(setq ent (entprev ent)))
|
||||
found
|
||||
)
|
||||
;; --- Letzten INSERT mit Blocknamen-Praefix suchen ---
|
||||
;; AutoLISP kennt KEIN entprev (nur entnext und entlast) - rueckwaerts durch
|
||||
;; die Zeichnung laufen geht also nicht. Gesucht wird darum VORWAERTS, wobei
|
||||
;; der letzte Treffer gewinnt. Ein blosses (entlast) genuegt nicht: nach dem
|
||||
;; Bau koennen weitere Objekte entstehen (z.B. Beschriftungstexte), entlast
|
||||
;; ist dann nicht der gesuchte Block - genau daran scheiterte der Testlauf.
|
||||
;; ab-ent = Stand der Zeichnung VOR dem Bau. Damit prueft die Suche nur die
|
||||
;; neu entstandenen Objekte: schnell, und sie kann nicht versehentlich den
|
||||
;; Block eines frueheren Baus liefern (siehe Kommentar bei -ab).
|
||||
(defun mubea:insert-prefix-p (ent pref / ed bn)
|
||||
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
|
||||
(and ed (= (cdr (assoc 0 ed)) "INSERT") bn
|
||||
(>= (strlen bn) (strlen pref))
|
||||
(= (substr bn 1 (strlen pref)) pref)))
|
||||
|
||||
;; Vorwaertslauf ab ab-ent (nil = ganze Zeichnung); der LETZTE Treffer
|
||||
;; gewinnt. Der entget-Test faengt ein inzwischen ungueltiges ab-ent ab
|
||||
;; (entget liefert dann nil, entnext wuerde einen Fehler werfen).
|
||||
(defun mubea:suche-vorwaerts (pref ab-ent / ent found)
|
||||
(setq ent (if (and ab-ent (entget ab-ent)) (entnext ab-ent) (entnext)))
|
||||
(while ent
|
||||
(if (mubea:insert-prefix-p ent pref) (setq found ent))
|
||||
(setq ent (entnext ent)))
|
||||
found)
|
||||
|
||||
(defun mubea:last-insert-prefix-ab (pref ab-ent / ent)
|
||||
(if ab-ent
|
||||
;; MIT Marker nur die neu entstandenen Objekte pruefen - und BEWUSST
|
||||
;; ohne entlast-Kurzschluss: hat dieser Bau keinen Block erzeugt, waere
|
||||
;; entlast der Block eines FRUEHEREN Baus und das Ergebnis wuerde der
|
||||
;; falschen Kette zugeordnet. Kein Treffer = kein Block, das ist die
|
||||
;; richtige Antwort.
|
||||
(mubea:suche-vorwaerts pref ab-ent)
|
||||
(progn
|
||||
;; Ohne Marker: erst entlast (der haeufige Fall, das gerade
|
||||
;; eingefuegte Objekt), sonst die ganze Zeichnung.
|
||||
(setq ent (entlast))
|
||||
(if (and ent (mubea:insert-prefix-p ent pref))
|
||||
ent
|
||||
(mubea:suche-vorwaerts pref nil)))))
|
||||
|
||||
(defun mubea:last-insert-prefix (pref)
|
||||
(mubea:last-insert-prefix-ab pref nil))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_vf_headless_statisch.py - Statischer Waechter fuer den nicht-interaktiven
|
||||
Betrieb des VF-Linienzugs. Laeuft ohne BricsCAD und ohne Zeichnung.
|
||||
|
||||
Warum statisch: die dynamischen Beweise (Ergebnis-Record mit "prompts": 0,
|
||||
"headless_fehler": null in tests/output/hundm05_results.json) sagen nur, dass
|
||||
der EINE getestete Ablauf sauber lief. Sie schlagen nicht an, wenn jemand
|
||||
einem selten erreichten Zweig ein neues (alert ...) oder ein rohes
|
||||
(getreal ...) hinzufuegt. Genau das haelt dieser Test fest: die Fundstellen
|
||||
sind eingecheckt, jede neue muss hier bewusst eingetragen werden.
|
||||
|
||||
Hintergrund und Fahrplan: doc/TODO-plan-vf-interactive.md,
|
||||
Mechanik: doc/variofoerderer.md Abschnitt 8b.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import io
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
LISP = os.path.join(os.path.dirname(__file__), "..", "Lisp",
|
||||
"vf_linienzug.lsp")
|
||||
CORE = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_core.lsp")
|
||||
|
||||
# Gesuchte Aufrufe: alles, was den Anwender fragt oder ihm ein Fenster zeigt.
|
||||
GESUCHT = ("alert", "getstring", "getint", "getreal", "getpoint", "getkword",
|
||||
"ssget", "entsel", "new_dialog")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Minimaler LISP-Scanner
|
||||
# ============================================================
|
||||
# Kein voller Parser: er braucht nur die Klammertiefe (fuer die Grenzen der
|
||||
# Top-Level-Formen) und muss Strings sowie Kommentare ueberspringen, damit ein
|
||||
# "alert" im Kommentar oder in einem i18n-Text nicht mitgezaehlt wird.
|
||||
|
||||
def funktions_inventar(quelltext, gesucht=GESUCHT):
|
||||
"""{Funktionsname: Counter({Aufruf: Anzahl})} fuer alle Top-Level-defuns.
|
||||
Code ausserhalb eines defun laeuft unter dem Namen "<top-level>".
|
||||
gesucht = die Aufrufnamen, die gezaehlt werden."""
|
||||
inventar = collections.defaultdict(collections.Counter)
|
||||
tiefe = 0
|
||||
aktuell = "<top-level>"
|
||||
i = 0
|
||||
n = len(quelltext)
|
||||
while i < n:
|
||||
c = quelltext[i]
|
||||
if c == ";": # Kommentar bis Zeilenende
|
||||
while i < n and quelltext[i] != "\n":
|
||||
i += 1
|
||||
continue
|
||||
if c == '"': # String ueberspringen
|
||||
i += 1
|
||||
while i < n:
|
||||
if quelltext[i] == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if quelltext[i] == '"':
|
||||
i += 1
|
||||
break
|
||||
i += 1
|
||||
continue
|
||||
if c == "(":
|
||||
wort = _wort_nach(quelltext, i + 1)
|
||||
if tiefe == 0:
|
||||
aktuell = ("<top-level>" if wort != "defun"
|
||||
else _wort_nach(quelltext,
|
||||
i + 1 + len("defun") + 1))
|
||||
if wort in gesucht:
|
||||
inventar[aktuell][wort] += 1
|
||||
tiefe += 1
|
||||
i += 1
|
||||
continue
|
||||
if c == ")":
|
||||
tiefe -= 1
|
||||
if tiefe <= 0:
|
||||
tiefe = 0
|
||||
aktuell = "<top-level>"
|
||||
i += 1
|
||||
continue
|
||||
i += 1
|
||||
return inventar
|
||||
|
||||
|
||||
def _wort_nach(text, pos):
|
||||
"""Erstes Symbol ab pos (Leerraum davor wird uebersprungen)."""
|
||||
while pos < len(text) and text[pos] in " \t\r\n":
|
||||
pos += 1
|
||||
ende = pos
|
||||
while ende < len(text) and text[ende] not in " \t\r\n()\";'":
|
||||
ende += 1
|
||||
return text[pos:ende]
|
||||
|
||||
|
||||
def _funktionskoerper(quelltext, name):
|
||||
"""Quelltext einer Top-Level-Funktion (Rohtext, genuegt fuer eine
|
||||
Namenssuche)."""
|
||||
start = quelltext.find("(defun %s " % name)
|
||||
if start < 0:
|
||||
start = quelltext.find("(defun %s(" % name)
|
||||
if start < 0:
|
||||
return None
|
||||
tiefe, i = 0, start
|
||||
while i < len(quelltext):
|
||||
if quelltext[i] == "(":
|
||||
tiefe += 1
|
||||
elif quelltext[i] == ")":
|
||||
tiefe -= 1
|
||||
if tiefe == 0:
|
||||
return quelltext[start:i + 1]
|
||||
i += 1
|
||||
return quelltext[start:]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def quelltext():
|
||||
if not os.path.isfile(LISP):
|
||||
pytest.skip("Lisp/vf_linienzug.lsp nicht gefunden")
|
||||
return io.open(LISP, encoding="cp1252").read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def inventar(quelltext):
|
||||
return funktions_inventar(quelltext)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 1. alert - nur noch in reinen Interaktiv-Pfaden
|
||||
# ============================================================
|
||||
# Alle Bau-Pfad-Alerts sind zu vfl-meldung geworden (P1 im Fahrplan): der Text
|
||||
# wird gesammelt statt modal gezeigt, damit ein Batch-Lauf nicht blockiert und
|
||||
# die Auskunft "welche Sektion war nicht baubar" nicht verloren geht.
|
||||
# Stehen bleiben darf, was nur bei echter Benutzerbedienung entstehen kann -
|
||||
# plus vfl-meldung selbst, das den Alert bei erlaubter GUI weiterhin zeigt.
|
||||
ALERT_ERLAUBT = {
|
||||
"vfl-meldung": 1, # zeigt den Text nur bei (ssg-gui-p)
|
||||
"vfl-glied-gf-bogen-dialog": 1, # dcl/vf_linienzug_wizard.dcl fehlt
|
||||
"vfl-glied-as-es-dialog": 1, # dito
|
||||
"vfl-glied-vario-dialog": 1, # dito
|
||||
"vfl-dlg-modus": 1, # dcl/vfl_edit.dcl fehlt
|
||||
"vfl-dlg-position": 1, # dito
|
||||
"vfl-edit-glied": 1, # "Glied nicht einzeln editierbar"
|
||||
"vfl-edit-ent": 1, # "kein Journal am Block"
|
||||
}
|
||||
|
||||
|
||||
def test_alert_nur_in_interaktiv_pfaden(inventar):
|
||||
ist = {name: z["alert"] for name, z in inventar.items() if z["alert"]}
|
||||
neu = sorted(k for k in ist if k not in ALERT_ERLAUBT)
|
||||
assert not neu, (
|
||||
"Neue (alert ...)-Aufrufe in %s. Im Bau-Pfad statt alert vfl-meldung "
|
||||
"verwenden (sammelt den Text und zeigt ihn nur bei erlaubter GUI). "
|
||||
"Ist der Aufruf wirklich rein interaktiv, hier eintragen." % neu)
|
||||
assert ist == ALERT_ERLAUBT, (
|
||||
"Alert-Inventar veraendert: ist %s, erwartet %s" % (ist,
|
||||
ALERT_ERLAUBT))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2. Rohe Eingaben nur in den Wrappern und in Interaktiv-Befehlen
|
||||
# ============================================================
|
||||
# Vollstaendiges Soll-Inventar. Ein neues getreal/getpoint/ssget in einer
|
||||
# Bau-Funktion faellt damit auf, auch wenn sein Zweig im Test nie erreicht
|
||||
# wird. Kommentar je Eintrag = warum die Stelle in Ordnung ist.
|
||||
EINGABE_ERLAUBT = {
|
||||
# --- die Journal-Wrapper selbst: hier laeuft der Replay durch ---
|
||||
"vfl-getpoint": {"getpoint": 2},
|
||||
"vfl-in-string": {"getstring": 1},
|
||||
"vfl-in-int": {"getint": 1},
|
||||
"vfl-in-real": {"getreal": 1},
|
||||
"vfl-in-selection": {"ssget": 1},
|
||||
"vfl-winkel-live": {"getint": 1}, # livefn fuer vfl-in-value
|
||||
# --- Wizard-Schicht (DCL); bei ssg-gui-aus greift sie nicht ---
|
||||
"vflw-zahl": {"getreal": 1}, # Fallback ohne Dialog
|
||||
"vflw-zahl-impl": {"new_dialog": 1},
|
||||
"vflw-wahl-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-as-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-gefaelle-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-horizontal-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-punkt-hoehe-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-variokurve-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-winkel-seite-impl": {"new_dialog": 1},
|
||||
"vflw-gruppe-ziel-hoehe-impl": {"new_dialog": 1},
|
||||
"vflw-seg-bogen-impl": {"new_dialog": 1},
|
||||
"vflw-seg-linie-m2-impl": {"new_dialog": 1},
|
||||
"vflw-seg-linie-m3-impl": {"new_dialog": 1},
|
||||
# --- Edit-Dialoge: nur per Doppelklick erreichbar ---
|
||||
"vfl-dlg-modus": {"new_dialog": 1, "alert": 1},
|
||||
"vfl-dlg-position": {"new_dialog": 1, "alert": 1},
|
||||
"vfl-glied-as-es-dialog": {"new_dialog": 1, "alert": 1},
|
||||
"vfl-glied-gf-bogen-dialog": {"new_dialog": 1, "alert": 1},
|
||||
"vfl-glied-vario-dialog": {"new_dialog": 1, "alert": 1},
|
||||
"vfl-edit-ent": {"alert": 1},
|
||||
"vfl-edit-glied": {"alert": 1},
|
||||
"vfl-meldung": {"alert": 1},
|
||||
# --- reine Benutzer-Befehle (kein Bau-Pfad, nicht journalisiert) ---
|
||||
"vfl-praeambel-erfragen": {"getpoint": 1, "getreal": 1, "getkword": 2},
|
||||
"c:VF_SEKTION_RESTORE": {"ssget": 1},
|
||||
"c:VFL_KS_DIAG": {"getstring": 1},
|
||||
"c:Vario_Kette_Merge": {"entsel": 1},
|
||||
# --- Zeichnung durchsuchen, keine Benutzerfrage ---
|
||||
"vfl-kette-sammle-alle": {"ssget": 1},
|
||||
}
|
||||
|
||||
|
||||
def test_eingabe_fundstellen_unveraendert(inventar):
|
||||
ist = {name: dict(z) for name, z in inventar.items() if z}
|
||||
neu = sorted(k for k in ist if k not in EINGABE_ERLAUBT)
|
||||
assert not neu, (
|
||||
"Neue Eingabe-/Dialog-Aufrufe in %s. Im Bau-Pfad gehoeren Eingaben "
|
||||
"durch die vfl-in-*-Wrapper (Journal + Replay + Headless-Riegel), "
|
||||
"nicht direkt an get*/ssget. Ist die Stelle rein interaktiv, hier "
|
||||
"eintragen." % neu)
|
||||
weg = sorted(k for k in EINGABE_ERLAUBT if k not in ist)
|
||||
assert not weg, ("Erwartete Fundstellen verschwunden (umbenannt?): %s"
|
||||
% weg)
|
||||
abweichung = {k: (ist[k], EINGABE_ERLAUBT[k]) for k in ist
|
||||
if ist[k] != EINGABE_ERLAUBT[k]}
|
||||
assert not abweichung, ("Anzahl der Aufrufe geaendert (ist, erwartet): %s"
|
||||
% abweichung)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3. Der Headless-Riegel sitzt an allen Eingabe-Engstellen
|
||||
# ============================================================
|
||||
# Jede interaktive Eingabe des Linienzugs laeuft durch einen dieser Wrapper.
|
||||
# Fehlt dort die Headless-Pruefung, faellt eine erschoepfte Replay-Queue
|
||||
# wieder still auf Live-Eingabe zurueck - der Fehler, den P2 behoben hat.
|
||||
RIEGEL_ERWARTET = {
|
||||
"vfl-in-value": ["vfl-headless-p", "vfl-headless-notausgang"],
|
||||
"vfl-in-value-p": ["vfl-headless-p", "vfl-headless-notausgang"],
|
||||
"vfl-in-selection": ["vfl-headless-p", "vfl-headless-abbruch"],
|
||||
"vfl-in-abstand": ["vfl-headless-p", "vfl-headless-abbruch"],
|
||||
"vfl-view-refresh": ["vfl-headless-p"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(RIEGEL_ERWARTET))
|
||||
def test_headless_riegel_vorhanden(quelltext, name):
|
||||
koerper = _funktionskoerper(quelltext, name)
|
||||
assert koerper, "Funktion %s nicht gefunden" % name
|
||||
for gesucht in RIEGEL_ERWARTET[name]:
|
||||
assert gesucht in koerper, (
|
||||
"%s ruft %s nicht - ohne den Riegel fragt der Wrapper bei leerer "
|
||||
"Replay-Queue wieder live (siehe doc/variofoerderer.md 8b)"
|
||||
% (name, gesucht))
|
||||
|
||||
|
||||
def test_headless_symbole_definiert(quelltext):
|
||||
for gesucht in ("(defun vfl-headless-p ", "(defun vfl-headless-abbruch ",
|
||||
"(defun vfl-headless-notausgang ",
|
||||
"(defun vfl-headless-ort ", "(defun vfl-meldung ",
|
||||
"*vfl-headless-antwort-fn*", "*vfl-meldungen*",
|
||||
"*vfl-headless-fehler*"):
|
||||
assert gesucht in quelltext, "%s fehlt" % gesucht
|
||||
|
||||
|
||||
def test_journal_reset_loescht_diagnose(quelltext):
|
||||
"""Meldungen und Headless-Diagnose gehoeren zum LAUF: bleiben sie stehen,
|
||||
traegt der naechste Bau die Befunde des vorherigen im Ergebnis."""
|
||||
koerper = _funktionskoerper(quelltext, "vfl-journal-reset")
|
||||
assert koerper
|
||||
assert "*vfl-meldungen*" in koerper
|
||||
assert "*vfl-headless-fehler*" in koerper
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4. Funktionen, die es in AutoLISP nicht gibt
|
||||
# ============================================================
|
||||
# Ein Tippfehler oder eine erfundene Funktion faellt erst auf, wenn der Zweig
|
||||
# tatsaechlich durchlaufen wird - im Testlauf hat genau das (entprev ...)
|
||||
# gekostet: AutoLISP kennt nur entnext und entlast, ein Rueckwaertslauf durch
|
||||
# die Zeichnung ist nicht moeglich. Solche Faelle sind hier billig zu fangen.
|
||||
# Erweitern, sobald wieder eine erfundene Funktion auffliegt.
|
||||
NICHT_EXISTENT = ("entprev", "entfirst", "ssprev", "vl-string-replace")
|
||||
|
||||
LISP_DIRS = ("Lisp", "tests")
|
||||
|
||||
|
||||
def test_keine_nicht_existierenden_funktionen():
|
||||
wurzel = os.path.join(os.path.dirname(__file__), "..")
|
||||
treffer = []
|
||||
for verz in LISP_DIRS:
|
||||
pfad = os.path.join(wurzel, verz)
|
||||
if not os.path.isdir(pfad):
|
||||
continue
|
||||
for name in sorted(os.listdir(pfad)):
|
||||
if not name.lower().endswith(".lsp"):
|
||||
continue
|
||||
text = io.open(os.path.join(pfad, name), encoding="cp1252").read()
|
||||
# Gleicher Scanner, andere Trefferliste - er ueberspringt
|
||||
# Kommentare und Strings, ein erklaerender Kommentartext wie
|
||||
# "kennt kein entprev" schlaegt also nicht an.
|
||||
for wo, zaehler in funktions_inventar(text,
|
||||
NICHT_EXISTENT).items():
|
||||
for fn in sorted(zaehler):
|
||||
treffer.append("%s/%s: (%s in %s)" % (verz, name, fn, wo))
|
||||
assert not treffer, (
|
||||
"Aufrufe nicht existierender AutoLISP-Funktionen: %s" % treffer)
|
||||
|
||||
|
||||
def test_dispatcher_setzt_journal_zurueck():
|
||||
"""P0: der Reset muss VOR der Modus-Verzweigung stehen, nicht nur im
|
||||
Modus-1-Zweig - sonst erbt ein frischer Modus-2-Lauf das Journal des
|
||||
Vorlaufs und schreibt es in die XDATA."""
|
||||
if not os.path.isfile(CORE):
|
||||
pytest.skip("Lisp/vf_core.lsp nicht gefunden")
|
||||
text = io.open(CORE, encoding="cp1252").read()
|
||||
cond = text.find("(cond ((= wahl 2) (vf-linienzug-modus2))")
|
||||
assert cond >= 0, "Dispatcher-cond nicht gefunden"
|
||||
reset = text.rfind("(vfl-journal-reset)", 0, cond)
|
||||
assert reset >= 0, "vfl-journal-reset steht nicht vor der Modus-cond"
|
||||
zweig = text[cond:text.find("(exit)", cond)]
|
||||
assert "vfl-journal-reset" not in zweig, (
|
||||
"Reset steht (wieder) IN der cond - dann trifft er nur einen Modus")
|
||||
Vendored
+1367
-448
File diff suppressed because it is too large
Load Diff
Vendored
+94
-2
@@ -401,6 +401,36 @@ JSON (Prompt-Protokoll, Auszug):
|
||||
]
|
||||
```
|
||||
|
||||
Dasselbe Protokoll steht produktionsseitig als **XDATA am fertigen
|
||||
`VF_n`-Block** (App `SSG_VF_EDIT`, Marker `"linienzug"`, Serialisierung
|
||||
`vfl-entry->string`): jede Eingabe wird beim Bau journalisiert und ist damit
|
||||
wiederabspielbar (`vfl-journal-replay-start` + `vf-linienzug-modus`, genutzt
|
||||
vom Doppelklick-Editieren). Ein Journal aus einer echten Zeichnung laesst
|
||||
sich darum 1:1 in dieses Schema zuruecklesen — genau das tut
|
||||
`lib/vf_journal_export.py` (siehe Abschnitt 8, `hundm05.json`). Zwei
|
||||
Unterschiede zum handgeschriebenen `linienzug_tests.json`:
|
||||
|
||||
- Zusaetzlicher Eintragstyp `step` (Glied-Marker aus `vfl-journal-mark`,
|
||||
Label = gewaehlter Segmenttyp). Beim Replay uebersprungen
|
||||
(`vfl-replay-pop`), fuer den Sektions-/Glied-Edit aber der Anker.
|
||||
- `hz` steht nur beim **ersten** Segment der Kette in einem `point_rel`
|
||||
(`vfl-in-abstand` journalisiert die gesnappte Richtung nur bei freier
|
||||
Richtungswahl); jedes weitere Segment erbt die Richtung des Vorgaengers
|
||||
und traegt nur `dL`. Winkel von AS/ES stehen als **Wert** (`"90"`/`"30"`),
|
||||
Seiten/Optionen als **Menue-Index** (`"1"`/`"2"`).
|
||||
|
||||
**Headless-Riegel (Stufe 0, siehe `doc/TODO-plan-vf-interactive.md`)**: der
|
||||
Replay allein garantierte noch keinen prompt-freien Lauf - lief die Queue
|
||||
mitten im Bau leer (Datenquelle passt nicht zum Ablauf), fielen die
|
||||
`vfl-in-*`-Wrapper **stillschweigend** auf Live-Eingabe zurueck und der Lauf
|
||||
blieb an einem `getpoint` haengen. Mit `*vfl-headless*` = T ist das jetzt ein
|
||||
harter Abbruch mit Fundstelle (`vfl-headless-abbruch` -> `*vfl-headless-fehler*`
|
||||
nennt Art der Eingabe, Glied-Nummer und Eingabe-Nummer). Die Meldungen der
|
||||
geometrisch abgewiesenen Sektionen laufen ueber `vfl-meldung` statt `alert` und
|
||||
stehen in `*vfl-meldungen*`, blockieren also nichts mehr. `tests/test_hundm05.lsp`
|
||||
schaltet beides ein; die Diagnose landet im Ergebnis-JSON
|
||||
(`headless_fehler`, `meldungen`, `prompts`).
|
||||
|
||||
### 3.4 Vario_Kette_Merge — kein Neubau
|
||||
|
||||
`c:Vario_Kette_Merge`: benoetigt nur **einen** Start-Entity-Pick (`entsel`),
|
||||
@@ -572,9 +602,9 @@ unbaubar. `lib/dxf_scan_components.py` trennt solche Paare darum ueber
|
||||
gibt den **Ausgangspunkt** zurueck, nicht die Entity — ein Treiber holt den
|
||||
Block ueber `(entlast)`. Bei fehlender Blockdatei ruft sie `(exit)` auf und
|
||||
wuerde einen Testlauf abbrechen, darum vorher mit `ssg-ils-block-laden`
|
||||
pruefen (so macht es `hundm05:build-gerade` in `tests/test_hundm05.lsp`).
|
||||
pruefen.
|
||||
|
||||
Vollstaendiges JSON (wie in `hundm05.json`):
|
||||
Vollstaendiges JSON:
|
||||
```json
|
||||
{
|
||||
"test_id": "GERADE_HundM05_08",
|
||||
@@ -585,6 +615,9 @@ Vollstaendiges JSON (wie in `hundm05.json`):
|
||||
"endpunkt": [220499.01, 154926.53, 0.0]
|
||||
}
|
||||
```
|
||||
Einen Treiber dafuer gibt es derzeit nicht mehr: `tests/test_hundm05.lsp`
|
||||
spielt seit dem Umbau auf die Linienzug-Journale nur noch Ketten ab (siehe
|
||||
Abschnitt 8); der fruehere `hundm05:build-gerade` steht in der Git-Historie.
|
||||
|
||||
---
|
||||
|
||||
@@ -606,3 +639,62 @@ Vollstaendiges JSON (wie in `hundm05.json`):
|
||||
| Omniflo TEF/Zubehoer | `omni:insert-dxf` (fester Sivasnr) | Position/Hoehe/Rotation ja, Punkt-Eingabe interaktiv |
|
||||
| Omniflo TV_*/APBW_* | - | **nein** (nicht implementiert, DUMMY) |
|
||||
| Separator/Scanner | `ils-insert-sensor` | **nein** (Schleife mit `getpoint`/`pause`) |
|
||||
|
||||
## 8. HundM05 — die 5 Linienzuege am Kreisel (`hundm05.json`)
|
||||
|
||||
`tests/testdata/hundm05.json` enthaelt die **Eingabe-Journale der 5
|
||||
VarioFoerderer-Linienzuege** aus `data/polylines.dxf` (die Anlage HundM, ein
|
||||
Kreisel mit fuenf daran haengenden Ketten). Quelle ist nicht die gezeichnete
|
||||
Geometrie, sondern die XDATA `SSG_VF_EDIT` (Marker `"linienzug"`) an den
|
||||
`VF_n`-Bloecken — also genau die Werte, mit denen die Ketten gebaut wurden.
|
||||
Schema je Eintrag: Kopffelder (`test_id`, `block`, `dxf_handle`,
|
||||
`startpunkt_mm`, `rotation_grad`, `sivas_id`, `beschreibung`,
|
||||
`erwartung_hinweis`, `quelle`) plus die Prompt-Sequenz in `eingaben` — Format
|
||||
wie `linienzug_tests.json`, siehe Abschnitt 3.3 (inkl. `step`-Marker).
|
||||
|
||||
| test_id | Block | Sivas | Startpunkt (mm) | Kette |
|
||||
|---|---|---|---|---|
|
||||
| VF_hundm05_LZ_01 | VF_87 | 0025 | 4912 / 1231 / 2154 | der komplizierte links: 2x Horizontal-VF, 4 Vario-Kurven (90/90/30/30) |
|
||||
| VF_hundm05_LZ_02 | VF_46 | 0019 | 8572 / 1276 / 2495 | Standardkette |
|
||||
| VF_hundm05_LZ_03 | VF_46 | 0018 | 12916 / 1276 / 2495 | Standardkette |
|
||||
| VF_hundm05_LZ_04 | VF_46 | 0020 | 17202 / 1205 / 2154 | Standardkette (die zuerst gebaute) |
|
||||
| VF_hundm05_LZ_05 | VF_46 | 0008 | 21490 / 1196 / 2495 | Standardkette |
|
||||
|
||||
Die vier `VF_46`-Bloecke sind **Kopien EINER** gebauten Kette: sie referenzieren
|
||||
dieselbe Blockdefinition und tragen darum Wort fuer Wort dasselbe Journal,
|
||||
inklusive des Startpunkts der zuerst gebauten Kette (17201.78/1205.36/2100 mit
|
||||
Starthoehe 2154). Im JSON ist dieser eine absolute Eintrag je Instanz auf ihren
|
||||
echten Blockeinfuegepunkt gezogen (alle weiteren Eintraege sind relativ), damit
|
||||
jeder Testfall fuer sich abspielbar ist. Achtung bei den Kopien: ihre
|
||||
Block-Attribute (`HOEHE_VON_mm` = 2154, so auch im Sivas-Export) stammen aus
|
||||
dem Original und sind nach dem Verschieben veraltet — die drei Kopien liegen
|
||||
tatsaechlich 340.93 mm hoeher (belegt durch ihre Separator-Z-Werte im
|
||||
Sivas-Export: 2575.43 statt 2234.50).
|
||||
|
||||
Neu erzeugen:
|
||||
|
||||
```
|
||||
python lib/vf_journal_export.py data/polylines.dxf \
|
||||
tests/testdata/hundm05.json --csv results/HundM_export.csv
|
||||
```
|
||||
|
||||
Die frueheren Inhalte (Kreisel/Gefaellestrecke/Sensor-Eintraege aus dem
|
||||
Fenster 215000/148000..250000/161000 von `results/ST500592_05.dxf`, erzeugt mit
|
||||
`lib/dxf_scan_components.py`) stehen in der Git-Historie dieser Datei.
|
||||
|
||||
**Aufbau flach, nicht verschachtelt**: `ssg-load-json`/`ssg-parse-json-array`
|
||||
(`Lisp/ssg_core.lsp`) liest die Datei ZEILENWEISE und behandelt jedes `{` als
|
||||
Objektanfang — Verschachtelung (z.B. `"eingaben": [ {...} ]`) wuerde die
|
||||
Kopf-Objekte verschlucken. Darum steht je Kette ein Kopf-Objekt (traegt
|
||||
`test_id`) und danach ihre Eingaben (tragen `typ`), wie in
|
||||
`linienzug_tests.json`. Der Anlagetyp im Kopf heisst `anlagetyp` und NICHT
|
||||
`typ`, damit Kopf und Eingabe unterscheidbar bleiben.
|
||||
|
||||
Abgespielt wird das per `tests/test_hundm05.lsp` (`TEST_HUNDM05`,
|
||||
`TEST_HUNDM05_2D`/`_3D`): der Treiber gruppiert die Objekte
|
||||
(`hundm05:gruppiere`), baut daraus das Vorwaerts-Journal
|
||||
(`hundm05:eintrag->journal`) und spielt es je Kette ueber
|
||||
`vfl-journal-replay-start` + `vf-linienzug-modus` ab — denselben Weg nimmt die
|
||||
Produktion beim Editieren/Konvertieren (`vfl-konvertiere-ent`). Bewusst KEINE
|
||||
Eingabe-Mocks wie in `test_linienzug.lsp`: ein Mock muesste zu jedem Segment
|
||||
die Fahrtrichtung kennen, die im Journal nur beim ersten Segment steht.
|
||||
|
||||
Reference in New Issue
Block a user