2dd5382060
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>
251 lines
11 KiB
Python
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}'
|