- model: Slot (separator/switch/scanner/drive + Rollen), JamArea (J<Separator>, Eingang eines TRO mit allen Vorgaengern), derive_slots, derive_jam_areas; Schema sps_skel-tro-v4, v3-Dateien werden weiter gelesen - scl_context: ein JamEntr je eigenem Staubereich statt je Vorgaenger (wie UH01-UH05), Ausgaenge nennen den Staubereich des Nachfolgers - Golden Files neu erzeugt, Tests fuer Slots/Staubereiche, Doku nachgezogen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
201 lines
7.6 KiB
Python
201 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests fuer lib/model.py - die TRO-JSON zwischen tro_flow und scl_skeleton."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from model import SCHEMA, Analysis, Connection, DerivedTro, TroCandidate, TroDocument
|
|
from scl_skeleton import render
|
|
|
|
|
|
def _tro(tro_id: str, type_name: str, **extra) -> DerivedTro:
|
|
data = dict(
|
|
tro_id=tro_id,
|
|
type_name=type_name,
|
|
label=f"Label {tro_id}",
|
|
hosts=["0001"],
|
|
separators=["0040"],
|
|
scanners=[],
|
|
reason="Test",
|
|
confidence="mittel",
|
|
)
|
|
data.update(extra)
|
|
return DerivedTro(**data)
|
|
|
|
|
|
@pytest.fixture
|
|
def document() -> TroDocument:
|
|
"""Kleine Anlage: SYS1 -> TRO01 (Weiche) -> TRO02 / TRO03 -> SYS2."""
|
|
analysis = Analysis(
|
|
tros=[
|
|
_tro("TRO01", "1Sep1Swi", predecessors=["SYS1"], successors=["TRO02", "TRO03"],
|
|
position=(1030.0, 4700.0, 1500.0)),
|
|
_tro("TRO02", "1Sep", separators=["0041"], predecessors=["TRO01"],
|
|
successors=["SYS2"]),
|
|
_tro("TRO03", "Vario", separators=["0042"], predecessors=["TRO01"]),
|
|
],
|
|
boundaries=[
|
|
_tro("SYS1", "Materialeingang", separators=[], successors=["TRO01"]),
|
|
_tro("SYS2", "Materialausgang", separators=[], predecessors=["TRO02"]),
|
|
],
|
|
edges=[("SYS1", "TRO01"), ("TRO01", "TRO02"), ("TRO01", "TRO03"), ("TRO02", "SYS2")],
|
|
edge_kinds={("TRO01", "TRO02"): "weiche"},
|
|
findings=["ein Befund"],
|
|
)
|
|
return TroDocument.from_analysis(analysis, source="test.csv", note="Test")
|
|
|
|
|
|
def test_round_trip_is_lossless(document):
|
|
text = document.to_json()
|
|
again = TroDocument.from_json(text)
|
|
assert again.to_json() == text
|
|
assert again.by_id("TRO01").successors == ["TRO02", "TRO03"]
|
|
|
|
|
|
def test_json_uses_tro_json_keys(document):
|
|
data = json.loads(document.to_json())
|
|
assert data["schema"] == SCHEMA
|
|
first = data["tros"][0]
|
|
assert first["id"] == "TRO01"
|
|
assert first["type"] == "1Sep1Swi"
|
|
assert first["fbType"] == "FB_ILS_MTRO_1Sep1Swi"
|
|
assert first["items"] == {"Separator": 1, "Switch": 1}
|
|
assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0}
|
|
assert data["connections"][1] == {"from": "TRO01", "to": "TRO02", "kind": "weiche"}
|
|
assert data["candidates"] == []
|
|
|
|
|
|
def test_candidate_round_trip():
|
|
candidate = TroCandidate(name="Qualitaetskontrolle", type_name="1Sep1Swi",
|
|
position=(-1026.0, 5250.05, 1478.5), belongs_to="TRO09")
|
|
data = json.loads(candidate.model_dump_json(by_alias=True))
|
|
assert data["type"] == "1Sep1Swi"
|
|
assert data["position"] == {"x": -1026.0, "y": 5250.05, "z": 1478.5}
|
|
assert data["belongsTo"] == "TRO09"
|
|
assert TroCandidate.model_validate(data) == candidate
|
|
assert candidate.describe_position() == "-1026, 5250"
|
|
|
|
|
|
def test_items_and_fb_block_follow_the_type():
|
|
tro = _tro("TRO01", "1Sep")
|
|
assert tro.items == {"Separator": 1}
|
|
tro.type_name = "1Sep2Swi"
|
|
assert tro.items == {"Separator": 1, "Switch": 2, "Scanner": 1}
|
|
assert tro.fb_block == "FB_ILS_MTRO_1Sep2Swi"
|
|
|
|
|
|
def test_unknown_type_has_no_catalog_data():
|
|
station = _tro("SYS1", "Materialeingang")
|
|
assert station.definition() is None
|
|
assert station.items == {}
|
|
assert station.fb_block == ""
|
|
|
|
|
|
def test_catalog_fields_cannot_be_assigned():
|
|
tro = _tro("TRO01", "1Sep")
|
|
with pytest.raises((AttributeError, ValidationError)):
|
|
tro.fb_block = "FB_Anders"
|
|
|
|
|
|
def test_unknown_key_is_rejected(document):
|
|
data = json.loads(document.to_json())
|
|
data["tros"][0]["separatorz"] = data["tros"][0].pop("separators")
|
|
with pytest.raises(ValidationError, match="separatorz"):
|
|
TroDocument.model_validate(data)
|
|
|
|
|
|
def test_items_not_matching_catalog_are_rejected(document):
|
|
data = json.loads(document.to_json())
|
|
data["tros"][1]["items"] = {"Separator": 2}
|
|
with pytest.raises(ValidationError, match="passt nicht zum Typ"):
|
|
TroDocument.model_validate(data)
|
|
|
|
|
|
def test_fb_type_not_matching_catalog_is_rejected(document):
|
|
data = json.loads(document.to_json())
|
|
data["tros"][1]["fbType"] = "FB_ILS_MTRO_Vario"
|
|
with pytest.raises(ValidationError, match="fbType"):
|
|
TroDocument.model_validate(data)
|
|
|
|
|
|
def test_connection_accepts_alias_and_field_name():
|
|
assert Connection(**{"from": "A", "to": "B"}) == Connection(src="A", dst="B")
|
|
|
|
|
|
def test_scl_skeleton_renders_from_document(document):
|
|
scl = render(document, "test_tro.json")
|
|
assert 'TRO01 : "FB_ILS_MTRO_1Sep1Swi";' in scl
|
|
assert 'TRO03 : "FB_ILS_MTRO_Vario";' in scl
|
|
# Weiche: FC_Direction und eine Zielliste je Abgang
|
|
assert '"FC_Direction"' in scl
|
|
assert "// ueber TRO02: SYS2, TRO02" in scl
|
|
# Vario-TRO bekommt den Vario-Parameterblock
|
|
assert "#TRO03.stInVario.Settings" in scl
|
|
# Systemgrenzen sind keine Instanzen
|
|
assert "SYS1 :" not in scl
|
|
assert "Anfang bei TRO01" in scl
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Schema v4: Slots und Staubereiche
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_slots_aus_katalog_und_layout(document):
|
|
weiche = document.by_id("TRO01")
|
|
assert [(s.slot, s.layout_id) for s in weiche.slots] == [
|
|
("separator1", "0040"), ("switch1", "")]
|
|
assert weiche.slots[0].roles == ["sensor", "stopper", "jamSensor?", "partSensor?"]
|
|
vario = document.by_id("TRO03")
|
|
assert [s.slot for s in vario.slots] == ["separator1", "drive"]
|
|
assert vario.slots[1].layout_id == "0001" # Host = angetriebene Strecke
|
|
# Systemgrenzen haben keine Slots
|
|
assert document.by_id("SYS1").slots == []
|
|
|
|
|
|
def test_ein_staubereich_je_tro_eingang_nach_separator(document):
|
|
areas = {a.jam_id: a for a in document.jam_areas}
|
|
assert sorted(areas) == ["J0040", "J0041", "J0042"]
|
|
assert areas["J0040"].tro == "TRO01"
|
|
assert areas["J0040"].predecessors == ["SYS1"]
|
|
assert areas["J0040"].position == (1030.0, 4700.0, 1500.0)
|
|
assert areas["J0041"].predecessors == ["TRO01"]
|
|
|
|
|
|
def test_zusammenfuehrung_ein_staubereich_mehrere_vorgaenger():
|
|
analysis = Analysis(tros=[
|
|
_tro("TRO01", "1Sep", separators=["0001"], successors=["TRO03"]),
|
|
_tro("TRO02", "1Sep", separators=["0002"], successors=["TRO03"]),
|
|
_tro("TRO03", "1Sep", separators=["0003"], predecessors=["TRO01", "TRO02"]),
|
|
])
|
|
doc = TroDocument.from_analysis(analysis, source="t")
|
|
eingang = [a for a in doc.jam_areas if a.tro == "TRO03"]
|
|
assert [(a.jam_id, a.predecessors) for a in eingang] == [("J0003", ["TRO01", "TRO02"])]
|
|
|
|
|
|
def test_zwei_separatoren_zwei_eingaenge_aufteilung_offen():
|
|
analysis = Analysis(tros=[
|
|
_tro("TRO01", "2Sep2Swi", separators=["0005", "0036"], predecessors=["TRO09"]),
|
|
])
|
|
areas = TroDocument.from_analysis(analysis, source="t").jam_areas
|
|
assert [(a.jam_id, a.entry, a.predecessors) for a in areas] == [
|
|
("J0005", 1, ["TRO09"]), ("J0036", 2, [])]
|
|
assert all("offen" in a.note for a in areas)
|
|
|
|
|
|
def test_v4_round_trip_und_v3_ohne_jam_areas(document):
|
|
data = json.loads(document.to_json())
|
|
assert data["schema"] == "sps_skel-tro-v4"
|
|
assert data["jamAreas"][0]["id"] == "J0040"
|
|
assert data["tros"][0]["slots"][0]["layoutId"] == "0040"
|
|
assert TroDocument.from_json(document.to_json()) == document
|
|
# v3-Datei: keine jamAreas/slots -> beim Rendern abgeleitet
|
|
del data["jamAreas"]
|
|
for tro in data["tros"]:
|
|
del tro["slots"]
|
|
data["schema"] = "sps_skel-tro-v3"
|
|
alt = TroDocument.from_json(json.dumps(data))
|
|
assert alt.jam_areas == []
|
|
assert [a.jam_id for a in alt.jam_areas_or_derived()] == ["J0040", "J0041", "J0042"]
|