From e91f1d00d0145b8393bb493cb3bba0bd830e5ab5 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Thu, 3 Sep 2026 14:10:56 +0200 Subject: [PATCH] [REFACTOR] Stufe 2: VF-Eingang/-Ausgang getrennt + Regressionsnetz fuer die Geometrie Der CAD-Lauf belegt die Schritte 0-4 jetzt auch EMPIRISCH: results/test_hundm05.dxf entstand nach dem Umbau, das Ergebnis-JSON von 11:18 davor - fuer alle 5 Ketten sind ALLE 37 Attribute identisch, inklusive der reihenfolgeabhaengigen Komma-Listen L_VF_m, L_GF_m, GF_WINKEL und ANTRIEBFAHRTRICHTUNG. TEST_VF_SPEC: 28 PASS / 0 FAIL. results/test_linienzug.dxf zeigt eine vollstaendige Kette (AS+ES, Motor, Umlenkung, 2 Vario-Kurven, 22 Bausteine, HOEHE_VON = HOEHE_BIS = 4500 bei DELTA_H 0). Schritt 5a/5b - vfl-vf-eingang-bauen und vfl-vf-ausgang-bauen: beide Bloecke (GF1 + Einlauf-Separator + Umlenkstation bzw. Motorstation + GF2) enthalten keine Frage und sind zusammenhaengend, der Umzug ist wortwoertlich (Zeilenvergleich: 0 Zeilen entfernt, nur zwei Koepfe, zwei Aufrufe und zwei "frame)" neu). Die Auswahl gf2-eff (ziel-gf2 im Kettenende-Modus, sonst L_GF2-bau) bleibt BEWUSST an der Aufrufstelle - genau dort entsteht sonst still die falsche GF2 hinter dem Motor. Der Separator-Zaehler und die vfl-acc-*-Aufrufe bleiben an derselben Stelle in derselben Reihenfolge. Regressionsnetz, damit die restlichen Schritte nicht blind gemacht werden muessen: - lib/dxf_vf_abbild.py (neu): liest eine Testzeichnung streamend und bildet je Kette Attribute, Bausteinzusammensetzung und Einfuegepunkt ab. Ketten werden ueber den EINFUEGEPUNKT der Spec zugeordnet, nicht ueber den Blocknamen - die VF-Nummer beginnt in jeder neuen Zeichnung wieder bei 1. ID und Bezeichnung sind aus dem Vergleich heraus (pro Zeichnung neu). --referenz vergleicht (Exit 1 bei Abweichung), --schreibe-referenz frischt die Referenz nach einem GEPRUEFTEN Lauf auf. - tests/reference/hundm05_attribute.json (neu): die Referenz, erzeugt aus dem geprueften Lauf - also dem Stand, der attributgleich mit dem Zustand VOR dem Umbau ist. - tests/test_vf_geometrie.py (neu, 8 Tests): vergleicht automatisch, mit einem eigenen Test nur fuer die Komma-Listen (das einzige, was eine vertauschte vfl-acc-*-Reihenfolge sichtbar macht) und einer Gegenprobe, dass eine verfaelschte Referenz auch erkannt wird. Das faengt, was der Ergebnis-Record NICHT sieht: er sagt "executed, kein Prompt, Journal aufgegangen", aber nichts darueber, ob dieselbe Geometrie herauskam. Ablauf ab jetzt pro Schritt: umbauen -> TEST_HUNDM05 (save dxf) -> pytest tests/test_vf_geometrie.py. Offen bleiben 5c, 6, 8, 9, 10 (Schleifenrumpf der VF-Einheit, Daten-Executor, Einheit-Abschluss, Kettenebene je Glied, vfl-spec-ausfuehren). Die vier haengen zusammen: 6 und 10 brauchen den aufgeteilten Schleifenrumpf. Halb umgebaut waere der Code schlechter dran als vor oder nach dem Umbau, darum als ein Schritt mit CAD-Lauf davor und danach. Verifiziert: 117 pytest-Tests gruen, vf_linienzug.lsp lint-sauber. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 1 + Lisp/vf_linienzug.lsp | 51 +++-- doc/TODO-plan-vf-interactive.md | 43 +++- lib/dxf_vf_abbild.py | 286 ++++++++++++++++++++++++ tests/README.md | 4 + tests/reference/hundm05_attribute.json | 297 +++++++++++++++++++++++++ tests/test_vf_geometrie.py | 151 +++++++++++++ 7 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 lib/dxf_vf_abbild.py create mode 100644 tests/reference/hundm05_attribute.json create mode 100644 tests/test_vf_geometrie.py diff --git a/CLAUDE.md b/CLAUDE.md index ddef2fc..5570f8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -167,6 +167,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py ` | | `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ | | `vf_journal_export.py` | Liest die Eingabe-Journale der `VF_n`-Bloecke (XDATA-App `SSG_VF_EDIT`, Marker `"linienzug"`) aus einer Zeichnung und schreibt sie als Frage-Antwort-Protokoll im Schema von `tests/testdata/linienzug_tests.json` (Gegenstueck zu `vfl-entry->string`; kein ezdxf, reine Gruppencode-Lesung). Optional `--csv ` fuer Strecken-ID und Erwartungswerte je Kette. Aufruf: `python vf_journal_export.py [--csv ...] [--kein-startpunkt-fix]` — erzeugt `tests/testdata/hm_recformat.json` aus `data/polylines.dxf` (die Aufzeichnung, aus der `tests/testdata/hundm05.json` als Spec abgeleitet wird) | +| `dxf_vf_abbild.py` | Abbild der `VF_`/`GF_`-Wrapperbloecke einer Testzeichnung: Attribute UND Bausteinzusammensetzung je Kette, Ketten ueber den Einfuegepunkt der Spec zugeordnet (der Blockname taugt nicht - die VF-Nummer beginnt in jeder Zeichnung neu bei 1). Regressionsnetz fuer Umbauten am Bau-Ablauf: `--referenz` vergleicht gegen `tests/reference/hundm05_attribute.json` (Exit 1 bei Abweichung), `--schreibe-referenz` frischt sie nach einem GEPRUEFTEN Lauf auf. Faengt genau das, was der Ergebnis-Record nicht sieht - eine vertauschte `vfl-acc-*`-Reihenfolge (Komma-Listen `L_VF_m`/`L_GF_m`/`GF_WINKEL`, in der Zeichnung unsichtbar, im Sivas-Export falsch) und eine falsche Blockvariante. Liest ASCII-DXF streamend; binaere DXF kann `dxf_scan_components.py`. Genutzt von `tests/test_vf_geometrie.py` | | `vf_spec_export.py` | Uebersetzt ein Eingabe-Journal in eine **Spec** und zurueck (`spec_aus_journal` / `journal_aus_spec`) - Gegenstueck zu `vsp-journal` in `Lisp/vf_spec.lsp`, beide muessen dieselbe Tokenfolge liefern. Der Rundlauf `journal_aus_spec(spec_aus_journal(tok)) == tok` laeuft in `tests/test_vf_spec.py` gegen die 5 echten HundM-Ketten und beweist, dass die Spec das Journal verlustfrei abbildet. Schreibt das flache Spec-JSON (vom LISP-Parser lesbar). Aufruf: `python vf_spec_export.py ` — erzeugt `tests/testdata/hundm05.json` (Spec, Eingabe fuer `TEST_HUNDM05`/`VF_SPEC_BAU`) aus `tests/testdata/hm_recformat.json` (Aufzeichnung) | | `dxf_scan_components.py` | Erkennt ILS-/Omniflo-Komponenten in einer **fremden** Projektzeichnung (ohne unsere Wrapper-Bloecke) und schreibt sie im JSON-Schema aus `tests/testdata/object_data.md` — Grundlage fuer Nachbau-Testfaelle wie `tests/testdata/mubea.json`. Liest binaere DXF im GB-Bereich streamend per mmap (ezdxf-DOM waere zu gross), loest INSERTs inkl. OCS/Extrusionsrichtung rekursiv in Weltkoordinaten auf, paart AN8/SP8 zu Kreiseln und Antrieb/Umlenkung zu Foerderern. Aufruf: `python dxf_scan_components.py [--fenster X0 Y0 X1 Y1]` — ohne `--fenster` liefert eine Anlagenzeichnung leicht ueber 1300 Eintraege, zuviel fuer einen Testlauf | diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 618d9a9..0dd5f0c 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -2704,6 +2704,37 @@ ) ) +;; --- VF-Einheit: Eingang (reiner Bauteil) --- +;; GF1 + Einlauf-Separator + Umlenkstation. Keine Frage, kein Dialog. +;; Rueckgabe: neuer Frame (Ausgang der Umlenkstation, 3-Grad-Basis). +;; Der Separator-Zaehler wird hier erhoeht, weil vfs-vf-entry den Separator +;; mitbaut - die *vfl-acc-*-Aufrufe muessen an derselben Stelle in derselben +;; Reihenfolge bleiben, sonst vertauschen sich die Komma-Listen L_VF_m/L_GF_m +;; im Sivas-Export (in der Zeichnung unsichtbar). +(defun vfl-vf-eingang-bauen (frame hz1 L_GF1-bau) + (setq frame (vfl-frame-3grad (vfs-vf-entry (car frame) L_GF1-bau hz1) hz1)) + (if (> L_GF1-bau 0.1) + (vfl-acc-gf-seg L_GF1-bau (ssg-cfg-or "vario" "gefaelle_winkel" 3))) + (setq *vfl-acc-separator* (1+ *vfl-acc-separator*)) ; Einlauf-Separator (in vfs-vf-entry) + frame) + +;; --- VF-Einheit: Ausgang (reiner Bauteil) --- +;; Motorstation [+ GF2], KEIN Separator - der sitzt erst vor dem ES-Element +;; bzw. optional zwischen zwei Foerderern. +;; gf2-eff kommt FERTIG herein (siehe Aufrufstelle). +(defun vfl-vf-ausgang-bauen (frame letzt-hz gf2-eff) + ;; Motorseite erfassen: derzeit immer "rechts" (nur diese DWG vorhanden; + ;; die "links"-Einzel-DWG wird spaeter ergaenzt). + (setq *vfl-acc-motorseite* (append *vfl-acc-motorseite* (list "rechts"))) + ;; Motorstation braucht 3-Grad-Basis: falls die Kette gerade flach endet + ;; (horizontales Stueck ohne ab_3), hier den ab_3-Uebergang nachholen. + (setq frame (vfl-nach-3grad frame)) + (setq frame (vfl-frame-3grad + (vfs-vf-exit (car frame) gf2-eff letzt-hz nil) letzt-hz)) + (if (> gf2-eff 0.1) + (vfl-acc-gf-seg gf2-eff (ssg-cfg-or "vario" "gefaelle_winkel" 3))) + frame) + ;; Rueckgabe: (frame anzahl-koerper ziel-ende) (defun vfl-vf-einheit (frame hz1 richtung1 winkel1 L_GF1 L_VF1 gf-am-ausgang / p-umlenk vf-count antwort L_GF-eff L_GF1-bau L_GF2-bau @@ -2721,11 +2752,10 @@ (setq L_GF1-bau L_GF-eff L_GF2-bau 0.0)) ; alles am Einlauf ;; --- Eingang: GF1 + Separator + Umlenkstation --- + ;; entry-start/p-umlenk sind der Stand VOR bzw. NACH dem Eingang - die + ;; horizontale Erstkoerper-Rechnung braucht beide (gemessener Fussabdruck). (setq entry-start (car frame)) - (setq frame (vfl-frame-3grad (vfs-vf-entry (car frame) L_GF1-bau hz1) hz1)) - (if (> L_GF1-bau 0.1) - (vfl-acc-gf-seg L_GF1-bau (ssg-cfg-or "vario" "gefaelle_winkel" 3))) - (setq *vfl-acc-separator* (1+ *vfl-acc-separator*)) ; Einlauf-Separator (in vfs-vf-entry) + (setq frame (vfl-vf-eingang-bauen frame hz1 L_GF1-bau)) (setq p-umlenk (car frame)) ;; --- Erstes Koerper-Sub-Segment --- @@ -2908,17 +2938,12 @@ ;; zwischen zwei Foerderern - nicht hier. ;; Motorseite erfassen: derzeit immer "rechts" (nur diese DWG vorhanden; ;; die "links"-Einzel-DWG wird spaeter ergaenzt). - (setq *vfl-acc-motorseite* (append *vfl-acc-motorseite* (list "rechts"))) - ;; Motorstation braucht 3-Grad-Basis: falls die Kette gerade flach endet - ;; (horizontales Stueck ohne ab_3), hier den ab_3-Uebergang nachholen. - (setq frame (vfl-nach-3grad frame)) ;; Auslauf-GF2: im Kettenende-Modus (Option 3) neu berechnet (ziel-gf2), - ;; sonst aus der GF-Verteilungs-Frage (L_GF2-bau). + ;; sonst aus der GF-Verteilungs-Frage (L_GF2-bau). Die Auswahl steht + ;; BEWUSST hier und nicht im Bauteil - sie ist die Stelle, an der sonst + ;; still die falsche GF2 hinter dem Motor entsteht. (setq gf2-eff (if ziel-ende ziel-gf2 L_GF2-bau)) - (setq frame (vfl-frame-3grad - (vfs-vf-exit (car frame) gf2-eff letzt-hz nil) letzt-hz)) - (if (> gf2-eff 0.1) - (vfl-acc-gf-seg gf2-eff (ssg-cfg-or "vario" "gefaelle_winkel" 3))) + (setq frame (vfl-vf-ausgang-bauen frame letzt-hz gf2-eff)) (list frame vf-count ziel-ende es-gewuenscht) ) diff --git a/doc/TODO-plan-vf-interactive.md b/doc/TODO-plan-vf-interactive.md index 05473bc..f27da22 100644 --- a/doc/TODO-plan-vf-interactive.md +++ b/doc/TODO-plan-vf-interactive.md @@ -605,7 +605,48 @@ der falschen Stelle verbraucht wird, kann beide passieren lassen und trotzdem andere Geometrie erzeugen. Sitzt im Daten-Pfad (`vf_spec.lsp`), nicht als `dbgmsg` im Bau-Pfad - dort waere er nur bei offener `.dbg`-Datei sichtbar. -**Offen: 5, 6, 8, 9, 10** (VF-Einheit aufteilen, Daten-Executor, +**Nachtrag (nach dem CAD-Lauf 12:22): Schritte 0-4 sind auch EMPIRISCH +belegt.** `results/test_hundm05.dxf` entstand nach dem Umbau, das +Ergebnis-JSON von 11:18 davor - fuer alle 5 Ketten sind **alle 37 Attribute +identisch**, inklusive der reihenfolgeabhaengigen Komma-Listen `L_VF_m`, +`L_GF_m`, `GF_WINKEL`, `ANTRIEBFAHRTRICHTUNG`. Genau der Vergleich, den +dieser Abschnitt fuer die Geometrie-Schritte verlangt. +`results/test_linienzug.dxf` zeigt eine vollstaendige Kette (AS+ES, Motor, +Umlenkung, 2 Vario-Kurven, 22 Bausteine, `HOEHE_VON` = `HOEHE_BIS` = 4500 bei +`DELTA_H` 0). + +**Zusaetzlich Schritt 5a/5b**: `vfl-vf-eingang-bauen` (GF1 + Einlauf-Separator ++ Umlenkstation) und `vfl-vf-ausgang-bauen` (Motorstation + GF2) sind +herausgezogen - beide Bloecke enthalten keine Frage und sind +zusammenhaengend, der Umzug ist wortwoertlich (Zeilenvergleich: 0 Zeilen +entfernt, nur zwei Koepfe, zwei Aufrufe und zwei `frame)` neu). Die Auswahl +`gf2-eff` (`ziel-gf2` im Kettenende-Modus, sonst `L_GF2-bau`) bleibt +BEWUSST an der Aufrufstelle - genau dort entsteht sonst still die falsche +GF2 hinter dem Motor. + +### Regressionsnetz fuer die restlichen Schritte + +Damit 5c/6/8/9/10 nicht mehr blind gemacht werden muessen, ist der +Attribut-Vergleich jetzt ein Werkzeug plus Test: + +- `lib/dxf_vf_abbild.py` liest die Testzeichnung streamend und bildet je + Kette Attribute, Bausteinzusammensetzung und Einfuegepunkt ab. Ketten + werden ueber den **Einfuegepunkt der Spec** zugeordnet, nicht ueber den + Blocknamen (die VF-Nummer beginnt in jeder Zeichnung neu bei 1). `ID` und + `Bezeichnung` sind bewusst aus dem Vergleich heraus. +- `tests/reference/hundm05_attribute.json` ist die Referenz - erzeugt aus dem + geprueften Lauf, also dem Stand, der attributgleich mit dem Zustand VOR + dem Umbau ist. +- `tests/test_vf_geometrie.py` (8 Tests) vergleicht automatisch, mit einem + eigenen Test nur fuer die Komma-Listen und einer Gegenprobe, dass eine + verfaelschte Referenz auch erkannt wird. + +Ablauf ab jetzt pro Schritt: umbauen -> `TEST_HUNDM05` (save dxf) -> +`pytest tests/test_vf_geometrie.py`. Weicht ein Attribut oder ein Baustein +ab, nennt der Test Kette und Feld. Referenz nur nach geprueftem Lauf +auffrischen (`--schreibe-referenz`). + +**Offen: 5c, 6, 8, 9, 10** (Schleifenrumpf der VF-Einheit, Daten-Executor, Einheit-Abschluss, Kettenebene je Glied-Zweig, `vfl-spec-ausfuehren`). Bewusst NICHT vorab erledigt: das sind die Schritte, bei denen laut Abschnitt "Wo Geometrie still kippen kann" die Reihenfolge selbst Geometrie ist. Der diff --git a/lib/dxf_vf_abbild.py b/lib/dxf_vf_abbild.py new file mode 100644 index 0000000..88a62d6 --- /dev/null +++ b/lib/dxf_vf_abbild.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +dxf_vf_abbild.py - Abbild der VF_/GF_-Wrapperbloecke einer Testzeichnung: +Attribute und Bausteinliste je Kette. + +Zweck: Regressionsnetz fuer Umbauten am Bau-Ablauf. Nach jedem Schritt der +Funktionstrennung (doc/TODO-plan-vf-interactive.md, Stufe 2) muss die +gebaute Geometrie unveraendert sein - "unveraendert" heisst hier: gleiche +Attribute (inklusive der reihenfolgeabhaengigen Komma-Listen L_VF_m/L_GF_m/ +GF_WINKEL, die in der Zeichnung unsichtbar, im Sivas-Export aber falsch +waeren) UND gleiche Bausteinzusammensetzung des Blocks (faengt eine falsche +Blockvariante, die kein Attribut verraet). + +Die Ketten werden ueber ihren Einfuegepunkt der Spec zugeordnet, nicht ueber +den Blocknamen: die laufende VF-Nummer beginnt in jeder neuen Zeichnung +wieder bei 1, der Kettenstart ist dagegen der stabile Anker. + +Liest ASCII-DXF streamend (Gruppencode-Paare; ein ezdxf-DOM waere fuer die +~90 MB Testzeichnungen zu gross). Binaere DXF kann dieses Skript NICHT - +dafuer gibt es lib/dxf_scan_components.py. + +Aufruf: + python dxf_vf_abbild.py [--spec tests/testdata/hundm05.json] + [--json abbild.json] + [--referenz tests/reference/hundm05_attribute.json] + [--schreibe-referenz] + +Ohne --referenz wird das Abbild nur ausgegeben. Mit --referenz wird +verglichen (Exit-Code 1 bei Abweichung); mit --schreibe-referenz wird die +Referenz aus der Zeichnung neu geschrieben - nur nach geprueftem Lauf! +""" + +import argparse +import collections +import io +import json +import os +import sys + +PREFIX = "[dxf_vf_abbild]" + +# Attribute, die von Lauf zu Lauf legitim wechseln: die laufende Nummer und +# der daraus gebildete Name. Sie sagen nichts ueber die Geometrie. +FLUECHTIG = ("ID", "Bezeichnung") + +RUND = 3 + + +def gruppen(pfad): + """Alle (Gruppencode, Wert)-Paare der Datei.""" + with io.open(pfad, "r", encoding="utf-8", errors="replace") as fh: + while True: + code = fh.readline() + if not code: + return + wert = fh.readline() + if not wert: + return + code = code.strip() + if not (code.isdigit() or (code.startswith("-") + and code[1:].isdigit())): + continue + yield int(code), wert.rstrip("\n").rstrip("\r") + + +def lese_bloecke(pfad, praefixe): + """(modelspace_inserts, blockinhalte) aus einer ASCII-DXF. + + modelspace_inserts: INSERTs der ENTITIES-Sektion mit passendem Praefix, + je mit Einfuegepunkt, Handle und den zugehoerigen ATTRIB-Werten. + blockinhalte: {Blockname: [(Baustein, x, y, z, rotation), ...]} aus der + BLOCKS-Sektion. + """ + sektion = None + block_name = None + ent = None + ms = [] + blockinhalt = collections.defaultdict(list) + letzter_insert = None + erwarte_sektion = False + erwarte_block_name = False + + for code, wert in gruppen(pfad): + if code == 0: + if ent is not None: + _entity_ablegen(ent, sektion, block_name, ms, blockinhalt, + praefixe) + if ent["typ"] == "INSERT" and not block_name: + letzter_insert = ent + elif ent["typ"] == "ATTRIB" and letzter_insert is not None: + letzter_insert.setdefault("attribs", {})[ + ent.get("tag", "?")] = ent.get("wert", "") + ent = None + if wert == "SECTION": + erwarte_sektion = True + elif wert == "ENDSEC": + sektion = None + elif wert == "BLOCK": + erwarte_block_name = True + ent = {"typ": "BLOCK"} + elif wert == "ENDBLK": + block_name = None + elif wert in ("INSERT", "ATTRIB"): + ent = {"typ": wert} + continue + + if erwarte_sektion and code == 2: + sektion = wert + erwarte_sektion = False + continue + if ent is None: + continue + if ent["typ"] == "BLOCK" and code == 2 and erwarte_block_name: + block_name = wert + erwarte_block_name = False + ent = None + continue + + if code == 2: + # Bei ATTRIB ist Gruppencode 2 der TAG, bei INSERT der Blockname. + if ent["typ"] == "ATTRIB": + ent["tag"] = wert + else: + ent["name"] = wert + elif code == 1 and ent["typ"] == "ATTRIB": + ent["wert"] = wert + elif code == 10: + ent["x"] = float(wert) + elif code == 20: + ent["y"] = float(wert) + elif code == 30: + ent["z"] = float(wert) + elif code == 50: + ent["rot"] = float(wert) + elif code == 5: + ent["handle"] = wert + + if ent is not None: + _entity_ablegen(ent, sektion, block_name, ms, blockinhalt, praefixe) + return ms, blockinhalt + + +def _entity_ablegen(ent, sektion, block_name, ms, blockinhalt, praefixe): + if ent["typ"] != "INSERT": + return + name = ent.get("name", "") + if block_name: + blockinhalt[block_name].append( + (name, round(ent.get("x", 0.0), RUND), round(ent.get("y", 0.0), RUND), + round(ent.get("z", 0.0), RUND), round(ent.get("rot", 0.0), RUND))) + elif sektion == "ENTITIES" and any(name.startswith(p) for p in praefixe): + ms.append(ent) + + +def spec_startpunkte(spec_datei): + """{spec_id: [x, y, z]} aus einer Spec-Datei (flaches JSON).""" + if not spec_datei or not os.path.isfile(spec_datei): + return {} + with io.open(spec_datei, encoding="utf-8") as fh: + daten = json.load(fh) + return dict((o["spec_id"], o["start_punkt"]) for o in daten + if "spec_id" in o) + + +def abbild(dxf, spec_datei=None, praefixe=("VF_", "GF_")): + ms, blocks = lese_bloecke(dxf, praefixe) + starts = spec_startpunkte(spec_datei) + raus = collections.OrderedDict() + for e in sorted(ms, key=lambda x: x.get("x", 0.0)): + name = e.get("name") + punkt = [e.get("x"), e.get("y"), e.get("z")] + kette = _kette_zuordnen(punkt, starts) or name + inhalt = blocks.get(name, []) + raus[kette] = collections.OrderedDict([ + ("block", name), + ("insert", [round(v, RUND) if v is not None else None + for v in punkt]), + ("bausteine", collections.OrderedDict( + sorted(collections.Counter(b[0] for b in inhalt).items()))), + ("bausteine_gesamt", len(inhalt)), + ("attribs", collections.OrderedDict( + (k, v) for k, v in sorted(e.get("attribs", {}).items()) + if k not in FLUECHTIG)), + ]) + return raus + + +def _kette_zuordnen(punkt, starts, toleranz=1.0): + """Kette ueber den Einfuegepunkt der Spec zuordnen - der Blockname + taugt nicht: die VF-Nummer beginnt in jeder Zeichnung neu bei 1.""" + for sid, p in starts.items(): + if all(abs(a - b) <= toleranz for a, b in zip(punkt[:3], p[:3])): + return sid + return None + + +def vergleiche(ist, soll): + """Liste von Abweichungstexten (leer = gleich).""" + fehler = [] + for kette in sorted(set(ist) | set(soll)): + if kette not in ist: + fehler.append("%s: in der Zeichnung nicht gebaut" % kette) + continue + if kette not in soll: + fehler.append("%s: nicht in der Referenz (neue Kette? Referenz " + "mit --schreibe-referenz auffrischen)" % kette) + continue + a, b = ist[kette], soll[kette] + for feld in ("bausteine", "bausteine_gesamt"): + if a[feld] != b[feld]: + fehler.append("%s: %s ist %s, Referenz %s" + % (kette, feld, a[feld], b[feld])) + for tag in sorted(set(a["attribs"]) | set(b["attribs"])): + wa, wb = a["attribs"].get(tag), b["attribs"].get(tag) + if wa != wb: + fehler.append("%s: Attribut %s ist %r, Referenz %r" + % (kette, tag, wa, wb)) + for achse, (wa, wb) in enumerate(zip(a["insert"], b["insert"])): + if wa is None or wb is None or abs(wa - wb) > 1.0: + fehler.append("%s: Einfuegepunkt Achse %d ist %s, Referenz %s" + % (kette, achse, wa, wb)) + return fehler + + +def main(): + p = argparse.ArgumentParser(description=__doc__.split("\n")[1]) + p.add_argument("dxf", help="Testzeichnung (ASCII-DXF)") + p.add_argument("--spec", help="Spec-JSON zur Ketten-Zuordnung") + p.add_argument("--json", help="Abbild hierhin schreiben") + p.add_argument("--referenz", help="Referenz-Abbild zum Vergleich") + p.add_argument("--schreibe-referenz", action="store_true", + help="Referenz aus dieser Zeichnung neu schreiben") + p.add_argument("--praefix", default="VF_,GF_", + help="Blocknamen-Praefixe, komma-getrennt") + args = p.parse_args() + + if not os.path.isfile(args.dxf): + print("%s FEHLER: %s nicht gefunden" % (PREFIX, args.dxf)) + return 1 + groesse = os.path.getsize(args.dxf) / (1024.0 * 1024.0) + print("%s lese %s (%.1f MB) ..." % (PREFIX, args.dxf, groesse)) + + ist = abbild(args.dxf, args.spec, tuple(args.praefix.split(","))) + print("%s %d Kette(n): %s" % (PREFIX, len(ist), ", ".join(ist))) + + if args.json: + _schreibe(args.json, ist) + print("%s Abbild -> %s" % (PREFIX, args.json)) + + if args.schreibe_referenz: + if not args.referenz: + print("%s FEHLER: --schreibe-referenz braucht --referenz" % PREFIX) + return 1 + _schreibe(args.referenz, ist) + print("%s Referenz neu geschrieben -> %s" % (PREFIX, args.referenz)) + return 0 + + if args.referenz: + if not os.path.isfile(args.referenz): + print("%s FEHLER: Referenz %s fehlt" % (PREFIX, args.referenz)) + return 1 + with io.open(args.referenz, encoding="utf-8") as fh: + soll = json.load(fh) + fehler = vergleiche(ist, soll) + if fehler: + print("%s %d ABWEICHUNG(EN):" % (PREFIX, len(fehler))) + for f in fehler: + print(" - %s" % f) + return 1 + print("%s Geometrie unveraendert (Attribute und Bausteine je Kette " + "gleich der Referenz)" % PREFIX) + return 0 + + +def _schreibe(pfad, daten): + verz = os.path.dirname(os.path.abspath(pfad)) + if verz and not os.path.isdir(verz): + os.makedirs(verz) + with io.open(pfad, "w", encoding="utf-8", newline="\r\n") as fh: + fh.write(json.dumps(daten, indent=2, ensure_ascii=False)) + fh.write("\n") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/README.md b/tests/README.md index eabfd4e..fc310e1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -170,6 +170,10 @@ tests/ # 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 diff --git a/tests/reference/hundm05_attribute.json b/tests/reference/hundm05_attribute.json new file mode 100644 index 0000000..e770653 --- /dev/null +++ b/tests/reference/hundm05_attribute.json @@ -0,0 +1,297 @@ +{ + "VF_hundm05_LZ_01": { + "block": "VF_1", + "insert": [ + 4912.239, + 1230.886, + 2154.0 + ], + "bausteine": { + "AS_Element_90_links_3D": 1, + "ES_Element_90_links_3D": 1, + "Staustrecke_SP_1000_mm_3D": 7, + "Staustrecke_Separator_SP_300_mm_3D": 3, + "Vario_Bogen_ab_3_TEF_rechts_3D": 2, + "Vario_Bogen_auf_3_TEF_rechts_3D": 2, + "Vario_Kurve_links_30_TEF_innen_3D": 1, + "Vario_Kurve_rechts_30_TEF_innen_3D": 1, + "Vario_Kurve_rechts_90_TEF_innen_3D": 2, + "Vario_Motorstation_500mm_rechts_3D": 2, + "Vario_Umlenkstation_500mm_rechts_3D": 2 + }, + "bausteine_gesamt": 24, + "attribs": { + "ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,horizontal,horizontal", + "ANZAHL_GF": "3", + "ANZAHL_SCANNER": "0", + "ANZAHL_SEPARATOR": "3", + "ANZAHL_VF": "4", + "ARTINR": "6220", + "DELTA_H_mm": "463", + "DELTA_L_mm": "1934", + "GERUEST_EINZELMODUL": "1", + "GERUEST_TYP": "Schoenenberger Geruest", + "GF_Bogen_L_30": "0", + "GF_Bogen_L_60": "0", + "GF_Bogen_L_90": "0", + "GF_Bogen_R_30": "0", + "GF_Bogen_R_60": "0", + "GF_Bogen_R_90": "0", + "GF_WINKEL": "3,3,3", + "HOEHE_BIS_mm": "1690", + "HOEHE_VON_mm": "2154", + "L_GF_m": "1.826,0.4,0.4", + "L_VF_m": "1.859,1.434,0.1,1.83", + "MONTAGEHOEHE_m": "1.922", + "MOTORSEITE": "rechts,rechts", + "SEITE_AS": "links", + "SEITE_ES": "links", + "TYP": "Streckengruppe", + "VF_Bogen_A_30": "0", + "VF_Bogen_A_60": "0", + "VF_Bogen_A_90": "0", + "VF_Bogen_I_30": "2", + "VF_Bogen_I_60": "0", + "VF_Bogen_I_90": "2", + "VF_WINKEL": "0,0,0,0", + "WINKEL_AS": "90", + "WINKEL_ES": "90" + } + }, + "VF_hundm05_LZ_02": { + "block": "VF_2", + "insert": [ + 8571.951, + 1276.061, + 2494.931 + ], + "bausteine": { + "AS_Element_90_links_3D": 1, + "ES_Element_90_links_3D": 1, + "Staustrecke_SP_1000_mm_3D": 7, + "Staustrecke_Separator_SP_300_mm_3D": 2, + "Vario_Bogen_ab_21_TEF_rechts_3D": 1, + "Vario_Bogen_ab_3_TEF_rechts_3D": 2, + "Vario_Bogen_auf_21_TEF_rechts_3D": 1, + "Vario_Bogen_auf_3_TEF_rechts_3D": 2, + "Vario_Kurve_rechts_90_TEF_innen_3D": 2, + "Vario_Motorstation_500mm_rechts_3D": 1, + "Vario_Umlenkstation_500mm_rechts_3D": 1 + }, + "bausteine_gesamt": 21, + "attribs": { + "ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal", + "ANZAHL_GF": "3", + "ANZAHL_SCANNER": "0", + "ANZAHL_SEPARATOR": "2", + "ANZAHL_VF": "4", + "ARTINR": "6220", + "DELTA_H_mm": "107", + "DELTA_L_mm": "1808", + "GERUEST_EINZELMODUL": "1", + "GERUEST_TYP": "Schoenenberger Geruest", + "GF_Bogen_L_30": "0", + "GF_Bogen_L_60": "0", + "GF_Bogen_L_90": "0", + "GF_Bogen_R_30": "0", + "GF_Bogen_R_60": "0", + "GF_Bogen_R_90": "0", + "GF_WINKEL": "3,3,3", + "HOEHE_BIS_mm": "2387", + "HOEHE_VON_mm": "2494", + "L_GF_m": "2.727,0.4,0.418", + "L_VF_m": "0.177,0.339,0.548,0.548", + "MONTAGEHOEHE_m": "2.441", + "MOTORSEITE": "rechts", + "SEITE_AS": "links", + "SEITE_ES": "links", + "TYP": "Streckengruppe", + "VF_Bogen_A_30": "0", + "VF_Bogen_A_60": "0", + "VF_Bogen_A_90": "0", + "VF_Bogen_I_30": "0", + "VF_Bogen_I_60": "0", + "VF_Bogen_I_90": "2", + "VF_WINKEL": "0,0,21,0", + "WINKEL_AS": "90", + "WINKEL_ES": "90" + } + }, + "VF_hundm05_LZ_03": { + "block": "VF_3", + "insert": [ + 12916.466, + 1276.061, + 2494.931 + ], + "bausteine": { + "AS_Element_90_links_3D": 1, + "ES_Element_90_links_3D": 1, + "Staustrecke_SP_1000_mm_3D": 7, + "Staustrecke_Separator_SP_300_mm_3D": 2, + "Vario_Bogen_ab_21_TEF_rechts_3D": 1, + "Vario_Bogen_ab_3_TEF_rechts_3D": 2, + "Vario_Bogen_auf_21_TEF_rechts_3D": 1, + "Vario_Bogen_auf_3_TEF_rechts_3D": 2, + "Vario_Kurve_rechts_90_TEF_innen_3D": 2, + "Vario_Motorstation_500mm_rechts_3D": 1, + "Vario_Umlenkstation_500mm_rechts_3D": 1 + }, + "bausteine_gesamt": 21, + "attribs": { + "ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal", + "ANZAHL_GF": "3", + "ANZAHL_SCANNER": "0", + "ANZAHL_SEPARATOR": "2", + "ANZAHL_VF": "4", + "ARTINR": "6220", + "DELTA_H_mm": "107", + "DELTA_L_mm": "1808", + "GERUEST_EINZELMODUL": "1", + "GERUEST_TYP": "Schoenenberger Geruest", + "GF_Bogen_L_30": "0", + "GF_Bogen_L_60": "0", + "GF_Bogen_L_90": "0", + "GF_Bogen_R_30": "0", + "GF_Bogen_R_60": "0", + "GF_Bogen_R_90": "0", + "GF_WINKEL": "3,3,3", + "HOEHE_BIS_mm": "2387", + "HOEHE_VON_mm": "2494", + "L_GF_m": "2.727,0.4,0.418", + "L_VF_m": "0.177,0.339,0.548,0.548", + "MONTAGEHOEHE_m": "2.441", + "MOTORSEITE": "rechts", + "SEITE_AS": "links", + "SEITE_ES": "links", + "TYP": "Streckengruppe", + "VF_Bogen_A_30": "0", + "VF_Bogen_A_60": "0", + "VF_Bogen_A_90": "0", + "VF_Bogen_I_30": "0", + "VF_Bogen_I_60": "0", + "VF_Bogen_I_90": "2", + "VF_WINKEL": "0,0,21,0", + "WINKEL_AS": "90", + "WINKEL_ES": "90" + } + }, + "VF_hundm05_LZ_04": { + "block": "VF_4", + "insert": [ + 17201.778, + 1205.361, + 2154.0 + ], + "bausteine": { + "AS_Element_90_links_3D": 1, + "ES_Element_90_links_3D": 1, + "Staustrecke_SP_1000_mm_3D": 7, + "Staustrecke_Separator_SP_300_mm_3D": 2, + "Vario_Bogen_ab_39_TEF_rechts_3D": 1, + "Vario_Bogen_ab_3_TEF_rechts_3D": 2, + "Vario_Bogen_auf_39_TEF_rechts_3D": 1, + "Vario_Bogen_auf_3_TEF_rechts_3D": 2, + "Vario_Kurve_rechts_90_TEF_innen_3D": 2, + "Vario_Motorstation_500mm_rechts_3D": 1, + "Vario_Umlenkstation_500mm_rechts_3D": 1 + }, + "bausteine_gesamt": 21, + "attribs": { + "ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal", + "ANZAHL_GF": "3", + "ANZAHL_SCANNER": "0", + "ANZAHL_SEPARATOR": "2", + "ANZAHL_VF": "4", + "ARTINR": "6220", + "DELTA_H_mm": "220", + "DELTA_L_mm": "1788", + "GERUEST_EINZELMODUL": "1", + "GERUEST_TYP": "Schoenenberger Geruest", + "GF_Bogen_L_30": "0", + "GF_Bogen_L_60": "0", + "GF_Bogen_L_90": "0", + "GF_Bogen_R_30": "0", + "GF_Bogen_R_60": "0", + "GF_Bogen_R_90": "0", + "GF_WINKEL": "3,3,3", + "HOEHE_BIS_mm": "2374", + "HOEHE_VON_mm": "2154", + "L_GF_m": "2.727,0.4,0.418", + "L_VF_m": "0.177,0.339,0.356,0.548", + "MONTAGEHOEHE_m": "2.264", + "MOTORSEITE": "rechts", + "SEITE_AS": "links", + "SEITE_ES": "links", + "TYP": "Streckengruppe", + "VF_Bogen_A_30": "0", + "VF_Bogen_A_60": "0", + "VF_Bogen_A_90": "0", + "VF_Bogen_I_30": "0", + "VF_Bogen_I_60": "0", + "VF_Bogen_I_90": "2", + "VF_WINKEL": "0,0,39,0", + "WINKEL_AS": "90", + "WINKEL_ES": "90" + } + }, + "VF_hundm05_LZ_05": { + "block": "VF_5", + "insert": [ + 21490.274, + 1195.74, + 2494.931 + ], + "bausteine": { + "AS_Element_90_links_3D": 1, + "ES_Element_90_links_3D": 1, + "Staustrecke_SP_1000_mm_3D": 7, + "Staustrecke_Separator_SP_300_mm_3D": 2, + "Vario_Bogen_ab_21_TEF_rechts_3D": 1, + "Vario_Bogen_ab_3_TEF_rechts_3D": 2, + "Vario_Bogen_auf_21_TEF_rechts_3D": 1, + "Vario_Bogen_auf_3_TEF_rechts_3D": 2, + "Vario_Kurve_rechts_90_TEF_innen_3D": 2, + "Vario_Motorstation_500mm_rechts_3D": 1, + "Vario_Umlenkstation_500mm_rechts_3D": 1 + }, + "bausteine_gesamt": 21, + "attribs": { + "ANTRIEBFAHRTRICHTUNG": "horizontal,horizontal,Auf,horizontal", + "ANZAHL_GF": "3", + "ANZAHL_SCANNER": "0", + "ANZAHL_SEPARATOR": "2", + "ANZAHL_VF": "4", + "ARTINR": "6220", + "DELTA_H_mm": "107", + "DELTA_L_mm": "1808", + "GERUEST_EINZELMODUL": "1", + "GERUEST_TYP": "Schoenenberger Geruest", + "GF_Bogen_L_30": "0", + "GF_Bogen_L_60": "0", + "GF_Bogen_L_90": "0", + "GF_Bogen_R_30": "0", + "GF_Bogen_R_60": "0", + "GF_Bogen_R_90": "0", + "GF_WINKEL": "3,3,3", + "HOEHE_BIS_mm": "2387", + "HOEHE_VON_mm": "2494", + "L_GF_m": "2.727,0.4,0.418", + "L_VF_m": "0.177,0.339,0.548,0.548", + "MONTAGEHOEHE_m": "2.441", + "MOTORSEITE": "rechts", + "SEITE_AS": "links", + "SEITE_ES": "links", + "TYP": "Streckengruppe", + "VF_Bogen_A_30": "0", + "VF_Bogen_A_60": "0", + "VF_Bogen_A_90": "0", + "VF_Bogen_I_30": "0", + "VF_Bogen_I_60": "0", + "VF_Bogen_I_90": "2", + "VF_WINKEL": "0,0,21,0", + "WINKEL_AS": "90", + "WINKEL_ES": "90" + } + } +} diff --git a/tests/test_vf_geometrie.py b/tests/test_vf_geometrie.py new file mode 100644 index 0000000..f62b708 --- /dev/null +++ b/tests/test_vf_geometrie.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_vf_geometrie.py - Regressionsnetz fuer die GEBAUTE Geometrie der +HundM-Ketten. + +Warum zusaetzlich zu tests/test_hundm05.py: der Ergebnis-Record sagt +"executed, kein Prompt, Journal aufgegangen" - er sagt NICHT, ob dieselbe +Geometrie herauskam wie vorher. Genau das ist bei der Funktionstrennung in +Stufe 2 (doc/TODO-plan-vf-interactive.md) das Risiko: die Reihenfolge der +dL-Subtraktionen und der vfl-acc-*-Aufrufe IST Geometrie, und eine falsche +Reihenfolge faellt in der Zeichnung nicht auf - nur im Sivas-Export. + +Verglichen wird gegen tests/reference/hundm05_attribute.json: + + - alle Attribute des VF_n-Blocks, insbesondere die reihenfolgeabhaengigen + Komma-Listen L_VF_m / L_GF_m / GF_WINKEL / ANTRIEBFAHRTRICHTUNG + - die Bausteinzusammensetzung des Blocks (faengt eine falsche + Blockvariante, die kein Attribut verraet) + - der Einfuegepunkt + +Die laufende ID und der Blockname sind bewusst NICHT Teil des Vergleichs: +die VF-Nummer beginnt in jeder neuen Zeichnung wieder bei 1. + +Braucht eine gebaute Zeichnung (TEST_HUNDM05 mit "save": "dxf") und wird +sonst uebersprungen. Referenz auffrischen - nur nach geprueftem Lauf: + + python lib/dxf_vf_abbild.py --spec tests/testdata/hundm05.json \ + --referenz tests/reference/hundm05_attribute.json \ + --schreibe-referenz +""" + +import json +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) + +import dxf_vf_abbild as dva # noqa: E402 + +HIER = os.path.dirname(__file__) +SPEC = os.path.join(HIER, "testdata", "hundm05.json") +REFERENZ = os.path.join(HIER, "reference", "hundm05_attribute.json") + +# SSG_RUN_ALL_TESTS legt die Zeichnung in tests/output ab; ein Lauf mit +# gesetztem DXFM_RESULTS speichert sie nach results/. +DXF_ORTE = [os.path.join(HIER, "output", "hundm05_tests.dxf"), + os.path.join(HIER, "..", "results", "test_hundm05.dxf")] + + +@pytest.fixture(scope="module") +def gebautes_abbild(): + for pfad in DXF_ORTE: + if os.path.isfile(pfad): + return dva.abbild(pfad, SPEC, ("VF_",)) + pytest.skip("keine gebaute HundM05-Zeichnung gefunden (%s) - " + "TEST_HUNDM05 in BricsCAD ausfuehren" + % " bzw. ".join(os.path.basename(p) for p in DXF_ORTE)) + + +@pytest.fixture(scope="module") +def referenz(): + if not os.path.isfile(REFERENZ): + pytest.skip("%s fehlt - mit lib/dxf_vf_abbild.py " + "--schreibe-referenz anlegen" % REFERENZ) + with open(REFERENZ, encoding="utf-8") as fh: + return json.load(fh) + + +def test_referenz_deckt_alle_specs(referenz): + """Jede Kette der Spec muss in der Referenz stehen - sonst laeuft der + Vergleich unbemerkt ins Leere.""" + if not os.path.isfile(SPEC): + pytest.skip("Spec fehlt") + with open(SPEC, encoding="utf-8") as fh: + specs = [o["spec_id"] for o in json.load(fh) if "spec_id" in o] + fehlt = sorted(set(specs) - set(referenz)) + assert not fehlt, ( + "Ketten ohne Referenz: %s. Nach einem geprueften Lauf die Referenz " + "mit lib/dxf_vf_abbild.py --schreibe-referenz auffrischen." % fehlt) + + +def test_alle_ketten_gebaut(gebautes_abbild, referenz): + fehlt = sorted(set(referenz) - set(gebautes_abbild)) + assert not fehlt, "in der Zeichnung nicht gebaut: %s" % fehlt + + +@pytest.mark.parametrize("feld", ["bausteine", "bausteine_gesamt"]) +def test_bausteine_wie_referenz(gebautes_abbild, referenz, feld): + """Die Bausteinliste faengt, was kein Attribut verraet: eine falsche + Blockvariante (z.B. auf_3 statt ab_3, links statt rechts).""" + for kette, soll in sorted(referenz.items()): + if kette not in gebautes_abbild: + continue + ist = gebautes_abbild[kette] + assert ist[feld] == soll[feld], \ + f"{kette}: {feld} ist {ist[feld]}, Referenz {soll[feld]}" + + +def test_attribute_wie_referenz(gebautes_abbild, referenz): + for kette, soll in sorted(referenz.items()): + if kette not in gebautes_abbild: + continue + ist = gebautes_abbild[kette]["attribs"] + for tag in sorted(set(ist) | set(soll["attribs"])): + assert ist.get(tag) == soll["attribs"].get(tag), \ + f'{kette}: Attribut {tag} ist {ist.get(tag)!r}, ' \ + f'Referenz {soll["attribs"].get(tag)!r}' + + +def test_reihenfolge_listen_wie_referenz(gebautes_abbild, referenz): + """Eigener Test fuer die Komma-Listen: sie sind das einzige, was eine + vertauschte vfl-acc-*-Reihenfolge sichtbar macht - in der Zeichnung + unsichtbar, im Sivas-Export falsch.""" + listen = ("L_VF_m", "L_GF_m", "GF_WINKEL", "ANTRIEBFAHRTRICHTUNG") + for kette, soll in sorted(referenz.items()): + if kette not in gebautes_abbild: + continue + ist = gebautes_abbild[kette]["attribs"] + for tag in listen: + if tag not in soll["attribs"]: + continue + assert ist.get(tag) == soll["attribs"][tag], \ + f'{kette}: {tag} ist {ist.get(tag)!r}, ' \ + f'Referenz {soll["attribs"][tag]!r} - Reihenfolge der ' \ + f'vfl-acc-*-Aufrufe geaendert?' + + +def test_einfuegepunkt_wie_referenz(gebautes_abbild, referenz): + for kette, soll in sorted(referenz.items()): + if kette not in gebautes_abbild: + continue + for achse, (a, b) in enumerate( + zip(gebautes_abbild[kette]["insert"], soll["insert"])): + assert abs(a - b) < 1.0, \ + f"{kette}: Einfuegepunkt Achse {achse} ist {a}, Referenz {b}" + + +def test_vergleichsfunktion_meldet_abweichung(gebautes_abbild, referenz): + """Gegenprobe: der Vergleich muss eine verfaelschte Referenz erkennen - + sonst bewiesen die Tests oben nur, dass beide Seiten gleich gelesen + werden.""" + kette = sorted(referenz)[0] + gift = json.loads(json.dumps(referenz)) + gift[kette]["attribs"]["DELTA_L_mm"] = "999999" + assert dva.vergleiche(gebautes_abbild, gift), \ + "verfaelschte Referenz wurde NICHT erkannt" + assert not dva.vergleiche(gebautes_abbild, referenz), \ + "echte Referenz wird als abweichend gemeldet"