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>
134 lines
5.4 KiB
Python
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)
|