Files
dxfmakros/tests/test_hundm05.py
T
s.ayadi 2dd5382060 [FEAT] Weitere Objekte der Anlage in hundm05.json bauen (Kreisel)
tests/testdata/hundm05.json enthaelt jetzt neben den 5 Spec-Ketten auch
andere Objekte der Anlage - erkennbar am Feld "function", im Format der
jeweiligen Einzel-Testdaten (ein Kreisel also wie in kreisel_tests.json).
Damit beschreibt EINE Datei die ganze Anlage.

- tests/test_hundm05.lsp: hundm05:bau-zusatzobjekte sammelt alle Objekte mit
  "function" und baut sie; hundm05:bau-kreisel ruft kreisel-insert-script
  (wie tests/test_kreisel.lsp) und liefert einen Ergebnis-Record in derselben
  Form wie die Ketten. Eigener ssg-start-Rahmen mit ATTREQ/ATTDIA 0, weil
  vsp-bau-datei seinen schon geschlossen hat und (command "_.INSERT" ...)
  sonst nach Attributwerten fragt. Je Objekt gefangen, damit ein Fehler die
  restlichen nicht mitnimmt.
- Lisp/vf_spec.lsp: "kind" ist jetzt ein Record-Feld (Default "linienzug")
  statt eines Literals in vsp-result-json - so schreibt vsp-results-schreiben
  beide Objektarten in EINE Ergebnisdatei.
- lib/vf_spec_export.py: Objekte der Ziel-Datei, die keine Kette beschreiben,
  werden beim Neuschreiben unveraendert ans Dateiende uebernommen. Ohne das
  waere jeder von Hand ergaenzte Eintrag beim naechsten Generatorlauf weg -
  die Datei ist erzeugt UND handgepflegt. spec_aus_flachen_objekten
  ueberspringt sie jetzt statt zu werfen, genau wie vsp-gruppieren in LISP.
- tests/test_hundm05.py: die Ketten-Tests filtern auf kind "linienzug"
  (Records ohne das Feld stammen aus aelteren Laeufen und waren nur Ketten);
  neue Klasse TestZusatzobjekte prueft Status, expect_block_prefix,
  expect_hoehe (Attribut HOEHE), expect_kreiselart (KREISELART) und den
  Einfuegepunkt - dieselben Erwartungsfelder wie tests/test_kreisel.py.
- tests/test_vf_spec.py: neuer Test, dass die Zusatzobjekte die
  Ketten-Zerlegung nicht beeinflussen - auch nicht, wenn sie mitten zwischen
  den Sektionen stehen (der eingetragene Kreisel stand zunaechst inmitten
  der Sub-Knoten von Kette 5; nach dem Regenerieren steht er am Dateiende).

Verifiziert: 118 pytest-Tests gruen, Spec-Regenerierung uebernimmt das
Zusatzobjekt (58 Objekte), beide .lsp lint-sauber. Die Kreisel-Tests warten
auf den naechsten TEST_HUNDM05-Lauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:49:46 +02:00

251 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_hundm05.py - Validiert die aus der SPEC gebaute Anlage HundM.
Eingabe: tests/testdata/hundm05.json (die 5 Ketten im Spec-Format).
Gebaut von TEST_HUNDM05 bzw. VF_SPEC_BAU in BricsCAD, Ergebnis in
tests/output/hundm05_results.json.
Diese Tests brauchen also einen CAD-Lauf und werden sonst uebersprungen.
Den UEBERSETZER (Spec -> Journal) prueft tests/test_vf_spec.py ohne CAD;
das Aufzeichnungsformat prueft tests/test_hm_recformat.py.
Der Ergebnis-Record je Kette kommt aus Lisp/vf_spec.lsp (vsp-result-json).
"""
import json
import os
import pytest
HIER = os.path.dirname(__file__)
SPEC_JSON = os.path.join(HIER, "testdata", "hundm05.json")
# TEST_HUNDM05 schreibt nach tests/output. Ein Lauf von VF_SPEC_BAU mit
# gesetztem DXFM_VF_SPEC_OUT kann die Datei woanders ablegen - darum beide
# ueblichen Orte pruefen, statt den Test stumm zu ueberspringen.
ERGEBNIS_ORTE = [os.path.join(HIER, "output", "hundm05_results.json"),
os.path.join(HIER, "..", "results", "hundm05_results.json")]
@pytest.fixture(scope="module")
def spec_objekte():
if not os.path.isfile(SPEC_JSON):
pytest.skip("%s fehlt (lib/vf_spec_export.py erzeugt es)" % SPEC_JSON)
with open(SPEC_JSON, encoding="utf-8") as fh:
return json.load(fh)
@pytest.fixture(scope="module")
def alle_ergebnisse():
for pfad in ERGEBNIS_ORTE:
if os.path.isfile(pfad):
with open(pfad, encoding="utf-8") as fh:
return json.load(fh)
pytest.skip("hundm05_results.json fehlt - TEST_HUNDM05 in BricsCAD "
"ausfuehren")
@pytest.fixture(scope="module")
def spec_ergebnisse(alle_ergebnisse):
"""Nur die Ketten. Die Ergebnisdatei enthaelt seit dem Nachtragen
weiterer Anlagenteile mehrere Objektarten, unterschieden ueber "kind"
(Records ohne das Feld stammen aus einem aelteren Lauf und waren
ausschliesslich Ketten)."""
return [r for r in alle_ergebnisse
if r.get("kind", "linienzug") == "linienzug"]
@pytest.fixture(scope="module")
def kreisel_ergebnisse(alle_ergebnisse):
raus = [r for r in alle_ergebnisse if r.get("kind") == "kreisel"]
if not raus:
pytest.skip("keine Kreisel-Ergebnisse (Lauf vor dem Nachtragen "
"weiterer Anlagenteile?)")
return raus
@pytest.fixture(scope="module")
def kreisel_objekte(spec_objekte):
"""Die nachgetragenen Anlagenteile aus den Testdaten - alles mit
"function", das keine Kette beschreibt."""
raus = [o for o in spec_objekte
if o.get("function") and not ("spec_id" in o or "glied" in o
or "sub" in o)]
if not raus:
pytest.skip("keine Zusatzobjekte in tests/testdata/hundm05.json")
return raus
class TestSpecBau:
def test_je_spec_ein_ergebnis(self, spec_objekte, spec_ergebnisse):
soll = [o["spec_id"] for o in spec_objekte if "spec_id" in o]
ist = [r["spec_id"] for r in spec_ergebnisse]
assert ist == soll
def test_status_executed(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["status"] == "executed", \
f'{r["spec_id"]}: Status={r["status"]}, {r.get("fehler_text")}'
def test_keine_spec_fehler(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert not r["spec_fehler"], \
f'{r["spec_id"]}: {r["spec_fehler"]}'
def test_kein_prompt(self, spec_ergebnisse):
"""Der positive Beweis fuer "kein Dialog, keine Konsolenfrage": die
zaehlenden Stubs wurden nie erreicht."""
for r in spec_ergebnisse:
assert r["prompts"] == 0, \
f'{r["spec_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
def test_journal_vollstaendig_verbraucht(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["eingaben_offen"] == 0, \
f'{r["spec_id"]}: {r["eingaben_offen"]} Werte offen'
assert r["eingaben_gesamt"] > 0
def test_glieder_wie_geplant(self, spec_ergebnisse):
"""Der Desync-Detektor, der auch dann greift, wenn die Queue aufgeht:
eine geometrisch abgewiesene Sektion verbraucht nur weniger
Eintraege, die Glied-Folge weicht dann aber ab."""
for r in spec_ergebnisse:
assert r["glieder_ist"] == r["glieder_soll"], \
f'{r["spec_id"]}: gebaut {r["glieder_ist"]}, ' \
f'geplant {r["glieder_soll"]}'
def test_journal_token_gleich(self, spec_ergebnisse):
"""Der schaerfste Desync-Detektor: das aus der Spec erzeugte Journal
und das beim Bau neu aufgezeichnete muessen Token fuer Token gleich
sein. Queue-Rest und Glied-Folge zeigen nur grobe Abweichungen - ein
einzelner Wert, der an der falschen Stelle verbraucht wird, kann
beide passieren lassen und trotzdem andere Geometrie erzeugen."""
for r in spec_ergebnisse:
if "journal_diff" not in r:
pytest.skip("Ergebnis aus einem Lauf vor Stufe 2")
assert not r["journal_diff"], \
f'{r["spec_id"]}: {r["journal_diff"]}'
def test_keine_meldungen(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert not r["meldungen"], \
f'{r["spec_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
def test_vf_block_erzeugt(self, spec_ergebnisse):
for r in spec_ergebnisse:
assert r["block_name"].startswith("VF_"), \
f'{r["spec_id"]}: Block {r["block_name"]!r}'
assert r["block_handle"]
def test_startpunkt_getroffen(self, spec_objekte, spec_ergebnisse):
"""Der Einfuegepunkt des VF_-Blocks ist der Kettenstart aus der
Spec - er ist der einzige absolute Anker der ganzen Kette."""
soll = dict((o["spec_id"], o["start_punkt"]) for o in spec_objekte
if "spec_id" in o)
for r in spec_ergebnisse:
erwartet = soll[r["spec_id"]]
ist = r["insert_point"]
for achse, (e, i) in enumerate(zip(erwartet, ist)):
assert abs(e - i) < 1.0, \
f'{r["spec_id"]}: Achse {achse} ist {i}, erwartet {e}'
def test_gleiche_ergebnisse_wie_journal_pfad(self, spec_ergebnisse):
"""Spec-Pfad == Journal-Pfad: dieselben Ketten, gebaut aus der Spec
statt aus dem aufgezeichneten Journal, muessen dieselben Bloecke mit
denselben Attributen ergeben. Vergleich gegen die Ergebnisse von
TEST_HUNDM05, falls beide Laeufe vorliegen."""
pfad = os.path.join(HIER, "output", "hm_recformat_results.json")
if not os.path.isfile(pfad):
pytest.skip("tests/output/hm_recformat_results.json fehlt "
"(TEST_HM_RECFORMAT nicht gelaufen)")
with open(pfad, encoding="utf-8") as fh:
journal_ergebnisse = json.load(fh)
if len(journal_ergebnisse) != len(spec_ergebnisse):
pytest.skip("unterschiedlich viele Ketten in den beiden Laeufen")
for spec_r, journal_r in zip(spec_ergebnisse, journal_ergebnisse):
if journal_r["status"] != "executed":
continue
for tag, wert in journal_r["actual_attributes"].items():
assert spec_r["actual_attributes"].get(tag) == wert, \
f'{spec_r["spec_id"]}: Attribut {tag} ist ' \
f'{spec_r["actual_attributes"].get(tag)!r}, ' \
f'Journal-Pfad hatte {wert!r}'
# ============================================================
# Weitere Objekte der Anlage (Kreisel)
# ============================================================
# tests/testdata/hundm05.json enthaelt neben den Ketten auch andere Objekte
# der Anlage - Format wie tests/testdata/kreisel_tests.json (Schema in
# tests/testdata/object_data.md Abschnitt 1). Gebaut werden sie von
# hundm05:bau-zusatzobjekte in tests/test_hundm05.lsp.
class TestZusatzobjekte:
def test_je_objekt_ein_ergebnis(self, kreisel_objekte,
kreisel_ergebnisse):
soll = [o["id"] for o in kreisel_objekte]
ist = [r["spec_id"] for r in kreisel_ergebnisse]
assert ist == soll, f"Ergebnisse {ist}, erwartet {soll}"
def test_status_executed(self, kreisel_ergebnisse):
for r in kreisel_ergebnisse:
assert r["status"] == "executed", \
f'{r["spec_id"]}: Status={r["status"]}, {r.get("fehler_text")}'
def test_block_prefix_wie_erwartet(self, kreisel_objekte,
kreisel_ergebnisse):
"""expect_block_prefix aus den Testdaten - bei Kreiseln KREISEL_,
weil die Haelften zu einem Wrapper-Block zusammengefasst werden."""
erwartet = dict((o["id"], o.get("expect_block_prefix"))
for o in kreisel_objekte)
for r in kreisel_ergebnisse:
pref = erwartet.get(r["spec_id"])
if not pref:
continue
assert r["block_name"].startswith(pref), \
f'{r["spec_id"]}: Block {r["block_name"]!r} beginnt nicht ' \
f'mit {pref!r}'
def test_hoehe_wie_erwartet(self, kreisel_objekte, kreisel_ergebnisse):
erwartet = dict((o["id"], o.get("expect_hoehe"))
for o in kreisel_objekte)
for r in kreisel_ergebnisse:
soll = erwartet.get(r["spec_id"])
if soll is None:
continue
ist = r["actual_attributes"].get("HOEHE")
assert ist == str(soll), \
f'{r["spec_id"]}: HOEHE ist {ist!r}, erwartet {soll!r}'
def test_kreiselart_wie_erwartet(self, kreisel_objekte,
kreisel_ergebnisse):
erwartet = dict((o["id"], o.get("expect_kreiselart"))
for o in kreisel_objekte)
for r in kreisel_ergebnisse:
soll = erwartet.get(r["spec_id"])
if soll is None:
continue
ist = r["actual_attributes"].get("KREISELART")
assert ist == str(soll), \
f'{r["spec_id"]}: KREISELART ist {ist!r}, erwartet {soll!r}'
def test_einfuegepunkt_getroffen(self, kreisel_objekte,
kreisel_ergebnisse):
"""Der Einfuegepunkt des Wrapper-Blocks ist der in den Testdaten
angegebene Punkt."""
soll = dict((o["id"], [o.get("x"), o.get("y"), o.get("z")])
for o in kreisel_objekte)
for r in kreisel_ergebnisse:
erwartet = soll.get(r["spec_id"])
if not erwartet or erwartet[0] is None:
continue
for achse, (e, i) in enumerate(zip(erwartet, r["insert_point"])):
if e is None:
continue
assert abs(float(e) - float(i)) < 1.0, \
f'{r["spec_id"]}: Achse {achse} ist {i}, erwartet {e}'