Files
dxfmakros/tests/test_export_ids.py
s.ayadi 21684101f9 [FIX] Eindeutige IDs und korrekte ZUORDNUNG der verpackten Separatoren
Beim CSV-Export von Mubea trugen Gefaellestrecke/VF/Kreisel und die aus
ihnen erzeugten Separator-Kopien dieselbe ID (26 Dubletten 0001-0026), und
die ZUORDNUNG der Separatoren zeigte auf den falschen Carrier.

ssg-id-check-all: neue Phase 0 ermittelt das globale Maximum der bereits
vergebenen IDs, BEVOR die erste neue ID vergeben wird (ausgelagert nach
ssg-id-max-in-ss, das auch ssg-id-max nutzt). Bisher wurde max-id erst
waehrend Phase 1 mitgezogen (Start 0); da (ssget "X" ...) in BricsCAD die
zuletzt erzeugten Entities zuerst liefert, standen die noch ID-losen
Separator-Kopien ganz vorne und bekamen 0001, 0002, ... - genau die IDs der
weiter hinten liegenden Wrapper-Bloecke. Phase 2 konnte das nicht heilen,
weil frisch vergebene IDs nicht in id-map landen.

ssg-collect-nested-inserts: fuehrt Position, Z-Drehung und Skalierung jetzt
ueber alle Verschachtelungsebenen mit (Records statt nackter Entity-Namen).
Vorher gab die Rekursion Entities tieferer Ebenen mit ihrer ROH-Position aus
der Zwischen-Blockdefinition zurueck - zwei Separatoren an derselben lokalen
Stelle in zwei verschieden platzierten Zwischenbloecken landeten dadurch auf
exakt derselben Weltposition.

csv:sep-proxies-erzeugen: die Kopie steht jetzt exakt auf Position/Drehung/
Skalierung ihres verpackten Vorbilds; der kosmetische Versatz von 500 mm ist
weg (er kippte Separatoren am Kettenende in die Boundingbox des Nachbar-
Carriers). Der vla-InsertBlock-Aufruf ist gekapselt, damit ein Sonderfall
nur diese eine Kopie ausfallen laesst statt den ganzen Export abzubrechen.

csv:sep-proxies-zuordnung-setzen (neu, laeuft NACH ssg-id-check-all, da ein
frisch gebauter Wrapper vorher keine ID hat): fuer einen VERPACKTEN Separator
ist der Carrier bekannt - es ist der Wrapper, in dem er steckt. ZUORDNUNG
wird daher auf die Wrapper-ID gesetzt (Separator in VF 0010 -> ZUORDNUNG
0010) und ueber *cs-sep-fix-by-handle* festgenagelt: cs-zuordnung-lauf
uebernimmt sie unveraendert statt sie geometrisch neu zu raten, und
csv:block-to-json reicht sie als "zuordnung_fix" an export_csv.py durch.
Ohne das ueberschrieb compute_sensor_zuordnung (Prioritaet GF > Foerderer >
Kreiselhaelfte) den bekannten Wert - in der Mubea-Zeichnung landeten 26 in
VF/GF verpackte Separatoren so bei einer Kreiselhaelfte.

export_csv.py: respektiert "zuordnung_fix" und ersetzt eine vorhandene
ZUORDNUNG nicht mehr durch "nicht zugeordnet" - nur ein echter Treffer
ueberschreibt, wie im Docstring von compute_sensor_zuordnung beschrieben.

tests/test_export_ids.py (neu): prueft je tests/output/*_export.csv, dass
jede TeileId hoechstens einmal vorkommt und jede Sensor-Zuordnung auf eine
existierende TeileId zeigt. Gegen die fehlerhafte Mubea-CSV schlaegt der
Test mit allen 26 Dubletten an.

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

161 lines
5.9 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_export_ids.py - Validiert IDs und Sensor-Zuordnung im CSV-Export.
Prueft jede von TEST_EXPORT_ALL (test_export_all.lsp) geschriebene
Datei tests/output/<basis>_export.csv:
1. Jede TeileId kommt HOECHSTENS EINMAL vor. Eine ID ist in der
Zeichnung eindeutig - Gefaellestrecke/VF/Kreisel und der aus
ihnen erzeugte Separator duerfen sich keine ID teilen.
(Regression: ssg-id-check-all zog max-id frueher erst waehrend
des Durchlaufs mit; die temporaeren Separator-Kopien aus
csv:sep-proxies-erzeugen stehen in der ssget-Reihenfolge ganz
vorne und bekamen dadurch 0001, 0002, ... - also genau die IDs
der weiter hinten liegenden Wrapper-Bloecke.)
2. Die Zuordnung jedes Separators/Scanners zeigt auf eine TeileId,
die es in derselben Datei auch gibt (bzw. auf eine Kreisel-
haelfte "<TeileId>-L"/"-R"). Eine Zuordnung auf eine nicht
existierende ID ist ein verwaister Verweis.
Beide Pruefungen brauchen keine Zeichnung und kein ezdxf - sie lesen
nur die fertige CSV. Ohne Export-CSV in tests/output wird der Test
uebersprungen (TEST_EXPORT_ALL in BricsCAD ausfuehren).
"""
import csv
import glob
import json
import os
import pytest
# TeileArten, die einen Sensor beschreiben (siehe SENSOR_TEILEARTEN in
# lib/export_neighbors.py).
SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
# Zuordnungs-Wert, den lib/export_neighbors.py setzt, wenn die Bounding-Box
# des Sensors keinen Carrier trifft - kein verwaister Verweis, sondern die
# ausdrueckliche Aussage "keiner".
ZUORDNUNG_KEINE = "nicht zugeordnet"
# Platzhalter-TeileId fuer Bloecke ohne ID-Attribut (aeltere Exporte). Nicht
# als ID zu werten und daher von der Eindeutigkeitspruefung ausgenommen.
TEILEID_LEER = {"", "0000"}
def _output_dir():
return os.getenv("DXFM_TESTOUT",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "output"))
def _read_export_csv(path):
"""Liest eine Export-CSV als Liste von Dicts (Spaltenname -> Wert).
AutoLISP/Python schreiben die Datei je nach Lauf in UTF-8 oder in der
ANSI-Codepage des Systems - wie conftest._load_json erst UTF-8 versuchen,
dann cp1252.
"""
for encoding in ("utf-8-sig", "cp1252"):
try:
with open(path, "r", encoding=encoding, newline="") as f:
return list(csv.DictReader(f, delimiter=";"))
except UnicodeDecodeError:
continue
pytest.fail(f"{os.path.basename(path)}: weder als UTF-8 noch als cp1252 lesbar")
def _export_csv_dateien():
return sorted(glob.glob(os.path.join(_output_dir(), "*_export.csv")))
def _merkmale(row):
"""Merkmale-Spalte (JSON) einer CSV-Zeile als Dict, {} wenn unlesbar."""
rohtext = (row.get("Merkmale") or "").strip()
if not rohtext:
return {}
try:
wert = json.loads(rohtext)
except ValueError:
return {}
return wert if isinstance(wert, dict) else {}
@pytest.fixture(params=_export_csv_dateien(),
ids=lambda p: os.path.basename(p))
def export_csv(request):
"""Je gefundene tests/output/*_export.csv ein eigener Testlauf."""
rows = _read_export_csv(request.param)
if not rows:
pytest.skip(f"{os.path.basename(request.param)} enthaelt keine Datenzeilen")
if "TeileId" not in rows[0]:
pytest.skip(f"{os.path.basename(request.param)} hat kein TeileId-Feld "
"(Export aus einer aelteren Spaltenversion)")
return os.path.basename(request.param), rows
if not _export_csv_dateien():
pytest.skip("Keine tests/output/*_export.csv vorhanden - "
"TEST_EXPORT_ALL in BricsCAD ausfuehren",
allow_module_level=True)
# ============================================================
# 1. Eindeutigkeit der TeileId
# ============================================================
def test_teileid_eindeutig(export_csv):
"""Keine TeileId darf zweimal vorkommen (Platzhalter ausgenommen)."""
name, rows = export_csv
belegung = {}
for row in rows:
teileid = (row.get("TeileId") or "").strip()
if teileid in TEILEID_LEER:
continue
belegung.setdefault(teileid, []).append(
f'{(row.get("TeileArt") or "").strip()} "{(row.get("Bezeichnung") or "").strip()}"')
doppelt = {tid: wer for tid, wer in belegung.items() if len(wer) > 1}
assert not doppelt, (
f"{name}: {len(doppelt)} TeileId(s) mehrfach vergeben - eine ID ist je "
"Zeichnungsobjekt eindeutig:\n" +
"\n".join(f" {tid}: " + ", ".join(wer) for tid, wer in sorted(doppelt.items()))
)
# ============================================================
# 2. Zuordnung der Sensoren zeigt auf eine existierende TeileId
# ============================================================
def test_sensor_zuordnung_existiert(export_csv):
"""Jede Sensor-Zuordnung verweist auf eine TeileId derselben Datei."""
name, rows = export_csv
bekannte_ids = {(row.get("TeileId") or "").strip() for row in rows}
bekannte_ids -= TEILEID_LEER
verwaist = []
for row in rows:
if (row.get("TeileArt") or "").strip() not in SENSOR_TEILEARTEN:
continue
zuordnung = str(_merkmale(row).get("Zuordnung", "")).strip()
if not zuordnung or zuordnung == ZUORDNUNG_KEINE:
continue
# Kreiselhaelften werden als "<TeileId>-L"/"-R" notiert (siehe
# compute_sensor_zuordnung in lib/export_neighbors.py).
carrier_id = zuordnung[:-2] if zuordnung[-2:] in ("-L", "-R") else zuordnung
if carrier_id not in bekannte_ids:
verwaist.append(
f' {(row.get("TeileArt") or "").strip()} '
f'{(row.get("TeileId") or "").strip()} '
f'("{(row.get("Bezeichnung") or "").strip()}") -> {zuordnung}')
assert not verwaist, (
f"{name}: {len(verwaist)} Sensor(en) mit Zuordnung auf eine nicht "
"vorhandene TeileId:\n" + "\n".join(verwaist)
)