Files
sps_skel/tests/test_scl_templates.py
T
Samer AyadiandClaude Opus 5.5 41df841317 SCL-Templates fuer alle Typen mit Referenz-Aufrufstelle (Schritt 5)
- neue Templates: FB_ILS_MTRO_1Sep (TRO100), _Vario (TRO115),
  _Vario_workStation (TRO421), _1Sep2Swi (TRO267), _1Sep_SSCC (TRO105),
  _PinStore_Auto (TRO208); zusammen mit dem Pilot 1Sep1Swi 7 von 11 Typen.
  2Sep1Swi, EmptyCarrBuffer, LoadingBoom, 2Sep2Swi bleiben generisch
  (keine reale Aufrufstelle)
- scl_context: Felder nur fuer die Templates - Cognex-Verdrahtung je
  Scanner, part_sensor, carriers/data je Staubereich, jam_in/jam_out (dieselben
  Objekte wie erster Ein-/Ausgang), error_quit, automatic, is_border,
  correction_station, Bloecke sscc/work_station/store (PinStore: eine Linie je
  Speicherbahn aus dem Layout statt der 19 aus dem Katalog)
- _macros: cognex_in/cognex_out/cognex, correction, todo_mehr
- tests/test_scl_templates.py: Rundlauf je Template gegen seine Referenz
  (PinStore: von der Referenz abgekuerzte Teile ausgenommen); Normalisierung
  vereinheitlicht ":="/"=>"-Abstaende
- Golden-Files *_templates neu geschrieben (gewollt: mehr TROs aus Templates);
  generische Golden-Files unveraendert
- Doku: SCL_Templates.md (§6 Entscheidungen je Template, §11 Schritt 5, §13
  offene Fragen 9-12), Python_Scripts.md, CLAUDE.md, README.md

Zeilenentscheidungen sind Vorschlaege, noch mit den SPS-Kollegen zu
bestaetigen. Geprueft: 39 Tests gruen, jede Rundlauf-Pruefung erkennt eine
geaenderte Zeile; tro_flow --json + scl_skeleton --templates auf allen 8 CSVs
ohne Fehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:13:55 +02:00

353 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
# "a:=b" und "a := b" sind dasselbe SCL (Referenzen schreiben beides)
zeile = re.sub(r"\s*(:=|=>)\s*", r" \1 ", zeile)
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 _jam(jam, db: str) -> None:
"""Staubereich mit bekanntem DB (Traeger und Daten wie ueblich darin)."""
jam.db = _name(db)
jam.carriers = _name(f"{db}.arCarrier")
jam.data = _name(f"{db}.stData")
def _scanner(sc, interface: str, tag: str, hw_results: str) -> None:
sc.interface = _name(interface)
sc.tag = _name(tag)
sc.hw_results = _name(hw_results)
def _kontext(tro_id: str, type_name: str, vorgaenger: list[str],
nachfolger: list[str], separators: list[str] | None = None,
hosts: list[str] | None = None) -> SclTro:
"""Kontext eines TRO mit der Topologie der Referenzstelle."""
tro = DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id,
hosts=hosts or [], separators=separators or ["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_1sep_wie_tro100(templates):
"""FB_ILS_MTRO_1Sep gegen TRO 100 (=A01+UH01-KF00 FB_Main)."""
t = _kontext("TRO100", "1Sep", ["TRO099"], ["TRO101"])
t.region_title = "TRO 100 (1Sep)"
sep = t.separators[0]
sep.no = _name("cSep100.1")
sep.sensor = _name("cInBG3242")
sep.stopper = _name("MB3242")
t.jam_exits[0].sensor = _name("cInBG3243")
t.main_tro = _name("cMainTro100")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.carousel_run = _name("#fbCarousel0101.xOutConveyorFullSpeed")
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
_jam(t.jam_entries[0], '"DB_JamArea0100".stJam0100_1')
_jam(t.jam_exits[0], '"DB_JamArea0101".stJam0101_1')
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep.scl"))
def test_vario_wie_tro115(templates):
"""FB_ILS_MTRO_Vario gegen TRO 115."""
t = _kontext("TRO115", "Vario", ["TRO114"], ["TRO116"])
t.region_title = "TRO 115 (Vario)"
sep = t.separators[0]
sep.no = _name("cSep115.1")
sep.sensor = _name("cInBG3025")
sep.stopper = _name("MB3025")
for name, wert in (("stSenCarrInPos", "cInBG3026"), ("stSenFinger", "cInBG3027"),
("stSenLastPos", "cInBG3000"), ("stSenJam", "cInBG3001")):
t.vario_sensors[name] = _name(wert)
t.is_border = _name("TRUE")
t.main_tro = _name("cMainTro115")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.motor_protection = _name('"FC0145"')
t.error_quit = _name('"SF0002"')
t.automatic = _name('#t_Automatik_OF1 AND "SF0120"')
_jam(t.jam_in, '"DB_JamArea4104".stJam4104_1')
_jam(t.jam_out, '"DB_JamArea4103".stJam4103_1')
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
t.vario_motor = _name("MA0145")
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_Vario.scl"))
def test_vario_workstation_wie_tro421(templates):
"""FB_ILS_MTRO_Vario_workStation gegen die Aufrufstelle TRO 421 (UH04)."""
t = _kontext("TRO421", "Vario_workStation", ["TRO420"], ["TRO422"])
t.region_title = "TRO 421 (Vario)"
sep = t.separators[0]
sep.no = _name("cSep421.1")
sep.sensor = _name("cInBG3326")
sep.stopper = _name("MB3326")
for name, wert in (("stSenFinger", "cInBG3315"), ("stSenLastPos", "cInBG3317"),
("stSenCarrInPos", "cInBG3324"), ("stSenJam", "cInBG3320")):
t.vario_sensors[name] = _name(wert)
t.work_station.loaded_error_sensor = _name("cInBG3325")
t.work_station.station_sensor = _name("cInBG3314")
t.work_station.station_release = _name('#"rTrig_Workstation 1".CLK')
t.main_tro = _name("cMainTro421")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMain.Q')
t.automatic = _name("#t_automatik")
t.release = '"DB_TRO_Test".TRO421'
t.motor_protection = _name('"FC0062"')
_jam(t.jam_in, '"DB_JamArea4004".stJam4004_2')
_jam(t.jam_out, '"DB_JamArea4005".stJam4005_1')
t.machine_state = _name("#stInOutControlUnitCabinet")
t.vario_motor = _name("MA0062")
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_Vario_workStation_CallSite.scl"))
def test_1sep2swi_wie_tro267(templates):
"""FB_ILS_MTRO_1Sep2Swi gegen TRO 267 (Scanner BX2100, 3 Abgaenge)."""
t = _kontext("TRO267", "1Sep2Swi", ["TRO266"], ["TRO268", "TRO269", "TRO270"])
t.region_title = "TRO 267 (1Sep2Switch)+Scanner(BX2100)"
sep = t.separators[0]
sep.no = _name("cSep267.1")
sep.sensor = _name("cInBG4440")
sep.part_sensor = _name("cInBG4465")
sep.stopper = _name("MB4440")
for jam, sensor in zip(t.jam_exits, ("cInBG4444", "cInBG4455", "cInBG4454")):
jam.sensor = _name(sensor)
for weiche, ventil in zip(t.switches, ("MB4444", "MB4455")):
weiche.valve = _name(ventil)
weiche.to1_destinations = 3 # Referenz: 3 je Weiche
t.switches[0].no = _name("cSwi267.1")
_scanner(t.scanners[0], "cScannerBX2100", "bx2100",
"=A01+UC021-BX2100~Ergebnisse_–_64_Bytes_1")
t.main_tro = _name("cMainTro267")
t.safety = _name('"cTodoTRUE"')
t.release = '"DB_TRO_Test".TRO267'
t.carousel_run = _name("#fbCarousel1110.xOutConveyorFullSpeed")
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
_jam(t.jam_entries[0], '"DB_JamArea1110".stJam1110_4')
for jam, nr in zip(t.jam_exits[:2], ("5001", "5000")): # Uebergaben
jam.carriers = _name(f"#arCarrierHandoverJam{nr}")
jam.data = _name(f"#stJamDataHandoverJam{nr}")
_jam(t.jam_exits[2], '"DB_JamArea1110".stJam1110_5')
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep2Swi.scl"))
def test_1sep_sscc_wie_tro105(templates):
"""FB_ILS_MTRO_1Sep_SSCC gegen TRO 105 (Scanner BX1104, SSCC-Scanner BX1107)."""
t = _kontext("TRO105", "1Sep_SSCC", ["TRO104"], ["TRO106"])
t.region_title = "TRO 105 (1Sep)+Scanner"
sep = t.separators[0]
sep.no = _name("cSep105.1")
sep.sensor = _name("cInBG3280")
sep.stopper = _name("MB3280")
t.jam_exits[0].sensor = _name("cInBG3281")
_scanner(t.scanners[0], "cScannerBX1104", "bx1104",
"BX1104~Ergebnisse_–_64_Bytes_1")
_scanner(t.sscc.scanner, "#stCognexIntfTro105", "bx1107",
"BX1107~Ergebnisse_–_64_Bytes_1")
t.sscc.end_sensor = _name("cInBG3281")
t.sscc.hmi = _name("SSCC1107")
t.main_tro = _name("cMainTro105")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.carousel_run = _name("TRUE")
t.release = ('"FB_CommunicationWCSBL_DB".fbTeleHandler.xOutConnectionActive '
'OR "IBN".xFahrenOhneTcpPartner')
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
_jam(t.jam_entries[0], '"DB_JamArea0104".stJam0104_3')
_jam(t.jam_exits[0], '"DB_JamArea0106".stJam0106_2')
t.correction_station = _name("0")
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep_SSCC.scl"))
def test_pinstore_auto_wie_tro208(templates):
"""FB_ILS_MTRO_PinStore_Auto gegen BLOCK 1.1.1 (TRO 208).
Die Referenz hat 19 Linien, schreibt aber nur die Linien 1-3 aus (bei
"Separator Store Out" nur Linie 1, "weitere analog"). Der Kontext hat
deshalb 19 Linien; was die Referenz abkuerzt, wird vor dem Vergleich aus
der Template-Ausgabe genommen.
"""
linien = [f"{n:04d}" for n in range(1, 20)]
t = _kontext("TRO208", "PinStore_Auto", ["TRO207"], ["TRO209"],
separators=linien, hosts=linien)
t.region_title = "BLOCK 1.1.1 (TRO 208)"
st = t.store
st.encoder = _name("#fbEncoderGF2200")
st.dest = _name("cDest11100")
st.conv_stations = _name("cHooksSorterMaxPin111")
st.stations_max = _name("cStationsMax111")
st.threshold = _name("cNoPositionsBlock1")
st.reference_sensor = _name("cInBG3001")
st.station = _name("111")
st.quit_local = _name('"SF0121" OR "SF0117"')
st.test_target = _name('"IBN".nTestTarget111')
st.run_in = _name("#fbSortConv1101.xOutConveyorFullSpeed")
st.run_out = _name("#fbExitConv1103.xOutConveyorFullSpeed")
st.db = _name('"DB_Storage1.1.1"')
st.removal = _name('"DB_HandshakeStorage".Str_StorageRemoval.'
'arrudt_StorageRemovalHandshakeTROsLvl1[1]')
st.conv_db = _name('"DB_ConveyorStations".stConveyorStations111')
st.pin_sensor = _name("cInBG3000")
st.pin_stopper = _name("MB3000")
for linie, (laenge, sensor, stau, stopper) in zip(st.lines, (
("4", "cInBG3270", "cInBG3030", "MB3270"),
('"cStorage111ExitJam2"', "cInBG3271", "cInBG3031", "MB3271"),
('"cStorage111ExitJam3"', "cInBG3272", "cInBG3032", "MB3272"))):
linie.jam_length = _name(laenge)
linie.sensor = _name(sensor)
linie.jam_sensor = _name(stau)
linie.stopper = _name(stopper)
_scanner(t.scanners[0], "cScannerBX2104", "bx2104",
"=A01+UH02-BX2104~Ergebnisse_–_64_Bytes_1")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF2.Q')
t.release = '"DB_TRO_Test".TRO208'
t.machine_state = _name("#stInOutControlUnitCabinetOF2")
_jam(t.jam_in, '"DB_JamArea1101".stJam1101_2')
_jam(t.jam_out, '"DB_JamArea1103".stJam1103_1')
t.correction_station = _name("1")
ist, abgekuerzt = [], False
for zeile in _normalisiert(templates.render(t)):
treffer = re.fullmatch(r"REGION Sep Store (\d+)", zeile)
if treffer:
abgekuerzt = int(treffer.group(1)) > 3
if abgekuerzt: # REGION Sep Store 4..19
abgekuerzt = zeile != "END_REGION"
continue
if re.search(r"arInJamSensor\[(\d+)\]", zeile) and \
int(re.search(r"\[(\d+)\]", zeile).group(1)) > 3:
continue
if re.search(r"arOutSeparatorStore\[(\d+)\]", zeile) and \
int(re.search(r"\[(\d+)\]", zeile).group(1)) > 1:
continue
ist.append(zeile)
_vergleich(ist, _referenz_region("FB_ILS_MTRO_PinStore_Auto.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)
kopf = next(z for z in text.splitlines() if z.startswith("// Aus Template :"))
for fb in {t.fb_block for t in document.tros if templates.has(t.fb_block)}:
assert fb in kopf
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)