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