#!/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}'