# -*- 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)