#!/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/_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 "-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 "-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) )