Files
sps_skel/tests/test_scl_templates.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

134 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
"""Rundlauf der FB-Templates gegen die realen Aufrufstellen (doc/SCL_Templates.md §8.1).
Je Template: Kontext mit den ECHTEN Werten der Referenz-Aufrufstelle bauen
(TRO-Nummer, Sensoren, Ventile, Staubereiche ...), Template rendern und mit dem
REGION-Block der Referenz in doc/TRO_Katalog/scl_templates/ vergleichen.
Verglichen wird normalisiert: Kommentare und Leerzeilen weg, Leerraum
zusammengefasst. Stimmt das, hat die Parametrisierung nichts verloren -
gleiche Zeilen, gleiche Reihenfolge, gleiche Werte.
Wird eine Referenzdatei durch eine neuere Aufrufstelle ersetzt, zeigt dieser
Test genau die Zeilen, denen das Template folgen muss.
"""
import re
from pathlib import Path
import pytest
from model import Analysis, DerivedTro, TroDocument
from scl_context import SclName, SclTro, build_context
from scl_skeleton import TEMPLATE_DIR, SclTemplates, load_document, render
ROOT = Path(__file__).resolve().parent.parent
REFERENZ = ROOT / "doc" / "TRO_Katalog" / "scl_templates"
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
def _name(wert: str) -> SclName:
"""Ein bekannter Name aus der Referenz - also nicht mehr offen."""
return SclName(value=wert, todo=False)
def _normalisiert(text: str) -> list[str]:
zeilen = []
for zeile in text.splitlines():
zeile = re.sub(r"//.*$", "", zeile) # Kommentar
zeile = re.sub(r"\s+", " ", zeile).strip() # Leerraum
if zeile:
zeilen.append(zeile)
return zeilen
def _referenz_region(datei: str) -> list[str]:
"""Vom ersten 'REGION' bis zum letzten 'END_REGION' der Referenzdatei."""
zeilen = (REFERENZ / datei).read_text(encoding="utf-8-sig").splitlines()
anfang = next(i for i, z in enumerate(zeilen) if z.startswith("REGION "))
ende = max(i for i, z in enumerate(zeilen) if z.strip() == "END_REGION")
return _normalisiert("\n".join(zeilen[anfang:ende + 1]))
def _kontext(tro_id: str, type_name: str, vorgaenger: list[str],
nachfolger: list[str]) -> SclTro:
"""Kontext eines TRO mit der Topologie der Referenzstelle."""
tro = DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id, hosts=[],
separators=["0001"], scanners=[], reason="Referenz",
predecessors=vorgaenger, successors=nachfolger)
document = TroDocument.from_analysis(Analysis(tros=[tro]), source="referenz")
return build_context(document, "referenz").tros[0]
def _vergleich(ist: list[str], soll: list[str]) -> None:
for nr, (a, b) in enumerate(zip(ist, soll), start=1):
assert a == b, f"Zeile {nr}:\n Referenz: {b}\n Template: {a}"
assert len(ist) == len(soll), (
f"{len(ist)} statt {len(soll)} Zeilen; zuviel/fehlend: "
f"{ist[len(soll):] or soll[len(ist):]}")
@pytest.fixture(scope="module")
def templates() -> SclTemplates:
return SclTemplates(TEMPLATE_DIR)
def test_1sep1swi_wie_tro107(templates):
"""FB_ILS_MTRO_1Sep1Swi gegen TRO 107 (=A01+UH01-KF00 FB_Main)."""
t = _kontext("TRO107", "1Sep1Swi", ["TRO106"], ["TRO108", "TRO110"])
t.region_title = "TRO 107(1Sep1Swi)"
sep = t.separators[0]
sep.no = _name("cSep107.1")
sep.sensor = _name("cInBG3306")
sep.stopper = _name("MB3306")
t.jam_exits[0].sensor = _name("cInBG3305")
t.jam_exits[1].sensor = _name("cInBG3330")
weiche = t.switches[0]
weiche.no = _name("cSwi107.1")
weiche.valve = _name("MB3304")
weiche.to1_destinations = 3 # Referenz: 3 Ziele bei 2 Abgaengen (§13)
weiche.jam_db = _name('"DB_JamArea0102".stJam0102_1')
t.prio_dir2 = _name("TRUE")
t.priority_udt = _name("udt_TRO107")
t.main_tro = _name("cMainTro107")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.carousel_run = _name("#fbCarousel0102.xOutConveyorFullSpeed")
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
t.jam_entries[0].db = _name('"DB_JamArea0102".stJam0102_1')
t.jam_exits[0].db = _name('"DB_JamArea0103".stJam0103_1')
t.jam_exits[1].db = _name('"DB_JamArea0102".stJam0102_2')
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep1Swi.scl"))
def test_template_ohne_bekannte_namen_hat_nur_markierte_luecken(templates):
"""Mit den heutigen Platzhaltern: jede offene Stelle traegt den TODO-Marker."""
t = _kontext("TRO07", "1Sep1Swi", ["SYS1"], ["TRO08", "TRO09"])
text = templates.render(t)
for zeile in text.splitlines():
if "____" in zeile.split("//")[0]:
assert "TODO(E-Planung)" in zeile, zeile
@pytest.mark.parametrize("name", ["mubea", "HundM05"])
def test_jedes_template_rendert_fuer_echte_anlagen(name, templates):
"""StrictUndefined: ein Tippfehler im Template faellt hier auf, nicht erst beim Kunden."""
document = load_document(FIXTURES / f"{name}_tro.json")
text = render(document, f"{name}_tro.json", templates=templates)
regionen = [z for z in text.splitlines() if z.startswith("REGION ")]
assert len(regionen) == len(document.tros)
assert "// Aus Template : FB_ILS_MTRO_1Sep1Swi" in text
def test_strikter_undefined_bricht_ab(tmp_path):
"""Unbekannte Variable im Template -> Fehler statt leerer SCL-Stelle."""
(tmp_path / "FB_X.scl.j2").write_text("REGION {{ t.gibt_es_nicht }}\n", encoding="utf-8")
t = _kontext("TRO01", "1Sep", [], [])
t.fb = "FB_X"
with pytest.raises(Exception):
SclTemplates(tmp_path).render(t)