21684101f9
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>
161 lines
5.9 KiB
Python
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)
|
|
)
|