Files
sps_skel/tests/test_scl_context.py
T
Samer AyadiandClaude Opus 5.5 4895ebf5ed SCL aus parametrisierten Templates: Golden-Files, Render-Kontext, Pilot 1Sep1Swi
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>
2026-10-09 18:06:18 +02:00

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")