[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>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#!/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)
|
||||
)
|
||||
Reference in New Issue
Block a user