Stabile TRO-IDs: ID-Datei je Anlage, Wiederfinden ueber die Lage

- lib/tro_ids.py: cfg/<csv>_tro_ids.ini lesen/schreiben, TROs ueber die
  Lage ihrer Separatoren wiederfinden, neue IDs vergeben, Weggefallene melden
- tro_flow: Nummerierung entlang des Materialflusses, Schalter --ids,
  --renumber, --id-tolerance; Bericht der IDs
- tro_annotate: liest dieselbe ID-Datei (nur lesend)
- Tests tests/test_tro_ids.py, Doku Python_Scripts.md, Konzept (Stufe 1 erledigt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-10-11 20:31:18 +02:00
co-authored by Claude Opus 5.5
parent ef57a2218e
commit dd2d98f2ee
7 changed files with 712 additions and 29 deletions
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/tro_ids.py: ID-Datei lesen/schreiben, TROs ueber die Lage
wiederfinden (doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs)."""
from __future__ import annotations
from dataclasses import dataclass, field
import pytest
from tro_ids import (
IdEntry,
TroIds,
assign_ids,
insert_entries,
load_ids,
other_sections,
render_ids,
render_new_entries,
)
@dataclass
class FakeTro:
separators: list[str]
type_name: str = "1Sep"
label: str = ""
tro_id: str = ""
hosts: list[str] = field(default_factory=list)
ANCHORS = {
"0001": (0.0, 0.0),
"0002": (1000.0, 0.0),
"0003": (2000.0, 0.0),
"0004": (2000.0, 500.0),
}
def _tros(*seps: list[str]) -> list[FakeTro]:
return [FakeTro(separators=list(s)) for s in seps]
def test_ohne_eintraege_wird_durchnummeriert():
tros = _tros(["0001"], ["0002"], ["0003"])
result = assign_ids(tros, ANCHORS, [])
assert result.renumbered
assert [t.tro_id for t in tros] == ["TRO01", "TRO02", "TRO03"]
def test_wiederfinden_ueber_lage_auch_umbenannt():
# Neuer Export: andere TeileIds, gleiche Lage, andere Reihenfolge
anchors = {"0101": (2000.0, 0.0), "0102": (0.0, 0.0), "0103": (1000.0, 30.0)}
tros = _tros(["0101"], ["0102"], ["0103"])
entries = [IdEntry("TRO01", 0.0, 0.0), IdEntry("K0102_1", 1000.0, 0.0),
IdEntry("TRO03", 2000.0, 0.0)]
result = assign_ids(tros, anchors, entries)
assert [t.tro_id for t in tros] == ["TRO03", "TRO01", "K0102_1"]
assert not result.new and not result.missing
def test_neue_tros_naechste_freie_nummer_weggefallene_nicht_wiederverwendet():
tros = _tros(["0001"], ["0003"])
entries = [IdEntry("TRO01", 0.0, 0.0), IdEntry("TRO02", 1000.0, 0.0)]
result = assign_ids(tros, ANCHORS, entries)
assert [t.tro_id for t in tros] == ["TRO01", "TRO03"]
assert result.new == ["TRO03"]
assert [e.tro_id for e in result.missing] == ["TRO02"]
def test_ausserhalb_toleranz_gilt_als_neu():
tros = _tros(["0001"])
entries = [IdEntry("TRO01", 150.0, 0.0)]
result = assign_ids(tros, ANCHORS, entries, tolerance=100.0)
assert tros[0].tro_id == "TRO02"
assert result.missing[0].tro_id == "TRO01"
assert assign_ids(tros, ANCHORS, entries, tolerance=200.0).kept[0][0] == "TRO01"
def test_zusammengelegte_tros_naechster_eintrag_gewinnt():
# zwei Separatoren, frueher zwei TROs, jetzt ein 2Sep-TRO
tros = _tros(["0003", "0004"])
entries = [IdEntry("TRO05", 2000.0, 0.0), IdEntry("TRO06", 2000.0, 510.0)]
result = assign_ids(tros, ANCHORS, entries)
assert tros[0].tro_id == "TRO05"
assert result.merged_into == {"TRO06": "TRO05"}
def test_renumber_ignoriert_eintraege():
tros = _tros(["0001"], ["0002"])
result = assign_ids(tros, ANCHORS, [IdEntry("X", 1000.0, 0.0)], renumber=True)
assert result.renumbered
assert [t.tro_id for t in tros] == ["TRO01", "TRO02"]
def test_datei_rundlauf_und_anhaengen(tmp_path):
path = tmp_path / "anlage_tro_ids.ini"
tros = _tros(["0001"], ["0002"])
assign_ids(tros, ANCHORS, [])
path.write_text(render_ids(tros, ANCHORS, "anlage.csv")
+ "\n[separator]\n; spaeter\n", encoding="utf-8")
entries = load_ids(path)
assert [(e.tro_id, e.x, e.y) for e in entries] == [
("TRO01", 0.0, 0.0), ("TRO02", 1000.0, 0.0)]
tros = _tros(["0001"], ["0002"], ["0003"])
result = assign_ids(tros, ANCHORS, entries)
insert_entries(path, render_new_entries(tros, ANCHORS, result.new, "anlage.csv"))
assert [e.tro_id for e in load_ids(path)] == ["TRO01", "TRO02", "TRO03"]
# neuer Eintrag steht in [tro], nicht im folgenden Abschnitt
assert other_sections(path) == "[separator]\n; spaeter"
assert TroIds.load(path).exists
@pytest.mark.parametrize("inhalt, meldung", [
("[tro]\nTRO01 = 1, 2\nTRO01 = 3, 4\n", "already exists"),
("[tro]\nSYS1 = 1, 2\n", "reserviert"),
("[tro]\nTRO-1 = 1, 2\n", "SCL-Bezeichner"),
("[tro]\nTRO01 = 1\n", "erwartet"),
("[andere]\n", "fehlt"),
])
def test_fehlerhafte_datei(tmp_path, inhalt, meldung):
path = tmp_path / "x.ini"
path.write_text(inhalt, encoding="utf-8")
with pytest.raises(ValueError, match=meldung):
load_ids(path)