Schritte 1-3 aus doc/SCL_Templates.md (neu: Konzept, Wertklassen je Zeile, Namensregeln, Tests, Migrationsplan, Vertrag fuer den CSV->SCL-Workflow). - Golden-Files (tests/test_scl_skeleton_golden.py, tests/fixtures/scl/): komplette FB_Main-Ausgabe von mubea und HundM05 byte-genau festgeschrieben, Zeitstempel maskiert; gewollte Aenderung per SKEL_UPDATE_GOLDEN=1 - lib/scl_context.py (neu): alles, was scl_skeleton aus der TRO-JSON ableitet (Reihenfolge, Separator-/Weichen-/Scanner-Bloecke, Staubereiche, Ziellisten, Namen) als pydantic-Kontext SclFile/SclTro; Namen als SclName (Wert + todo-Merker) als Andockstelle fuer die Namensregeln (Schritt 4). scl_skeleton schreibt nur noch Text; Ausgabe byte-identisch (Golden-Files, zusaetzlich dematic/showroom mit und ohne --start alt/neu verglichen) - lib/scl_templates/ (neu, Jinja2): _macros.scl.j2 + Pilot FB_ILS_MTRO_1Sep1Swi.scl.j2 nach der realen Aufrufstelle TRO107 (PriorityManagerDir2, stInSenJam<n>, xInCarouselRun1, xInRelease TRUE, Separatorzeiten 100/50 ms). Ortsgebundene Zeilen als Vorschlag umgesetzt, jede offene Stelle mit TODO(E-Planung) - Bestaetigung durch SPS steht aus - scl_skeleton: --templates [DIR] (opt-in, ohne Schalter Ausgabe unveraendert), --strict (Exit 1 ohne SCL, wenn ein FB-Typ kein Template hat); Kopf und Zusammenfassung nennen Template-/generische Typen; Templatefehler (StrictUndefined) -> Exit 1 ohne SCL - tests/test_scl_templates.py: Rundlauf Template mit Referenzwerten == Referenz-REGION (normalisiert); jede Platzhalterzeile traegt den TODO-Marker - requirements.txt: jinja2 >= 3.1 (bestehende .venv: pip install -r requirements.txt) - Doku: SCL_Templates.md, Python_Scripts.md §5b, Commandlines.md §2.5, CLAUDE.md Befunde: xInCarouselRun1 laesst sich nicht aus der TRO-JSON ableiten (Kanten- art "umlauf" kommt praktisch nicht vor) -> immer als TODO; nTo1Destinations ist in der Referenz 3 bei 2 Abgaengen (zaehlt evtl. Ziele) -> "pruefen". Mit --templates nur die 1Sep1Swi-REGIONs (+ Kopf) anders als generisch; 32 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests fuer lib/scl_context.py - TroDocument -> Render-Kontext.
|
|
|
|
Die komplette Ausgabe sichern die Golden-Files (test_scl_skeleton_golden.py);
|
|
hier stehen die Ableitungen einzeln, auf die sich Templates (Schritt 3) und
|
|
Namensregeln (Schritt 4) aus doc/SCL_Templates.md verlassen.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from model import Analysis, DerivedTro, TroDocument
|
|
from scl_context import SclName, build_context, flow_order
|
|
from scl_skeleton import load_document
|
|
|
|
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
|
|
|
|
|
|
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",
|
|
)
|
|
data.update(extra)
|
|
return DerivedTro(**data)
|
|
|
|
|
|
@pytest.fixture
|
|
def kontext():
|
|
"""SYS1 -> TRO01 (1 Weiche) -> TRO02 -> SYS2 und TRO01 -> TRO03 (Vario)."""
|
|
analysis = Analysis(tros=[
|
|
_tro("TRO01", "1Sep1Swi", predecessors=["SYS1"], successors=["TRO02", "TRO03"]),
|
|
_tro("TRO02", "1Sep", separators=["0041"], predecessors=["TRO01"],
|
|
successors=["SYS2"]),
|
|
_tro("TRO03", "Vario", separators=[], predecessors=["TRO01"]),
|
|
])
|
|
document = TroDocument.from_analysis(analysis, source="test.csv")
|
|
return build_context(document, "test_tro.json", created="2026-01-01 00:00")
|
|
|
|
|
|
def _by_id(context, tro_id):
|
|
return next(t for t in context.tros if t.id == tro_id)
|
|
|
|
|
|
def test_reihenfolge_und_startgrund(kontext):
|
|
assert [t.id for t in kontext.tros] == ["TRO01", "TRO02", "TRO03"]
|
|
assert kontext.start_reason == "Materialeingang ueber SYS1"
|
|
assert kontext.created == "2026-01-01 00:00"
|
|
|
|
|
|
def test_nummernteil_wie_geschrieben():
|
|
"""{nr} fuer die Namensregeln: fuehrende Nullen bleiben stehen."""
|
|
analysis = Analysis(tros=[_tro("TRO07", "1Sep"), _tro("TRO107", "1Sep"),
|
|
_tro("XY", "1Sep")])
|
|
ctx = build_context(TroDocument.from_analysis(analysis, source="t"), "t")
|
|
assert {t.id: t.nr for t in ctx.tros} == {"TRO07": "07", "TRO107": "107", "XY": ""}
|
|
|
|
|
|
def test_staubereiche_nummerierung(kontext):
|
|
"""Eingaenge ab 1, Ausgaenge ab 2 - wie in den Referenz-Aufrufstellen."""
|
|
tro = _by_id(kontext, "TRO01")
|
|
assert [(j.n, j.neighbour) for j in tro.jam_entries] == [
|
|
(1, "SYS1 (Station, kein TRO)")]
|
|
assert [(j.n, j.neighbour) for j in tro.jam_exits] == [(2, "TRO02"), (3, "TRO03")]
|
|
|
|
|
|
def test_eine_weiche_bedient_alle_abgaenge(kontext):
|
|
tro = _by_id(kontext, "TRO01")
|
|
assert len(tro.switches) == 1
|
|
weiche = tro.switches[0]
|
|
assert weiche.to1_destinations == 2
|
|
assert weiche.direction_target == "#TRO01.nInDirection"
|
|
assert [(e.via, e.targets) for e in tro.switch_exits] == [
|
|
("TRO02", ["SYS2", "TRO02"]), ("TRO03", ["TRO03"])]
|
|
|
|
|
|
def test_mehrere_weichen_keine_geratene_aufteilung():
|
|
"""2Sep2Swi (mubea): nTo1Destinations bleibt offen, Richtung je Weiche."""
|
|
ctx = build_context(load_document(FIXTURES / "mubea_tro.json"), "mubea_tro.json")
|
|
tro = next(t for t in ctx.tros if t.type == "2Sep2Swi")
|
|
assert [w.to1_destinations for w in tro.switches] == [None, None]
|
|
assert [w.direction_target for w in tro.switches] == [
|
|
f"#{tro.id}.nInDirection1", f"#{tro.id}.nInDirection2"]
|
|
|
|
|
|
def test_separatoren_aus_dem_layout(kontext):
|
|
"""Mehr Separatoren laut Typ als im Layout -> '?' statt Fehler."""
|
|
assert [s.layout for s in _by_id(kontext, "TRO01").separators] == ["0040"]
|
|
assert [s.layout for s in _by_id(kontext, "TRO03").separators] == ["?"]
|
|
|
|
|
|
def test_vario_nur_beim_vario_typ(kontext):
|
|
vario = _by_id(kontext, "TRO03")
|
|
assert vario.has_vario
|
|
assert list(vario.vario_sensors) == [
|
|
"stSenCarrInPos", "stSenFinger", "stSenLastPos", "stSenJam"]
|
|
assert _by_id(kontext, "TRO02").vario_sensors == {}
|
|
|
|
|
|
def test_heute_sind_alle_namen_offen(kontext):
|
|
"""Bis zu den Namensregeln (Schritt 4) ist jeder Name ein TODO-Platzhalter."""
|
|
namen: list[SclName] = []
|
|
for tro in kontext.tros:
|
|
namen += [tro.main_tro, tro.safety, tro.motor_protection, tro.vario_motor,
|
|
tro.machine_state, tro.priority_udt, tro.prio_dir2, tro.carousel_run]
|
|
namen += [j.sensor for j in tro.jam_exits]
|
|
assert all(j.sensor is None for j in tro.jam_entries)
|
|
namen += [n for s in tro.separators for n in (s.no, s.sensor, s.stopper)]
|
|
namen += [n for w in tro.switches for n in (w.no, w.valve, w.jam_db)]
|
|
namen += [j.db for j in tro.jam_entries + tro.jam_exits]
|
|
namen += list(tro.vario_sensors.values())
|
|
assert namen and all(n.todo for n in namen)
|
|
|
|
|
|
def test_flow_order_kreislauf_ohne_eingang():
|
|
analysis = Analysis(tros=[
|
|
_tro("TRO02", "1Sep", predecessors=["TRO01"], successors=["TRO01"]),
|
|
_tro("TRO01", "1Sep", predecessors=["TRO02"], successors=["TRO02"]),
|
|
])
|
|
geordnet, grund = flow_order(analysis.tros)
|
|
assert [t.tro_id for t in geordnet] == ["TRO01", "TRO02"]
|
|
assert grund.startswith("Kreislauf ohne Eingang")
|