# -*- coding: utf-8 -*- """Tests fuer scl_skeleton mit der Anlagen-JSON (Stufe 4). Die Anlagen-JSON (merge_hw_layout) traegt das Layout und die Zuordnungen der Hardwareplanung je TRO-ID + Slot + Rolle. scl_context.HwSource setzt daraus die Namen ein; was offen oder widerspruechlich ist, bleibt TODO(E-Planung). Die Zuordnungen sind hier von Hand gebaut - wie merge_hw_layout sie findet, pruefen die Tests in test_merge_hw_layout.py. """ import scl_skeleton from hw_import import HardwareDocument, HwDevice from model import ( AnlageDocument, Analysis, Binding, ConveyorBinding, DerivedTro, LayoutPart, TroDocument, ) from scl_context import TODO, build_context def _tro(tro_id, type_name, seps, hosts, **extra): return DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id, hosts=list(hosts), separators=list(seps), scanners=[], reason="Test", **extra) def _part(layout_id, kind, name): return LayoutPart(layout_id=layout_id, kind=kind, name=name, position=(0.0, 0.0, 0.0)) def _bind(tro, slot, role, bmk="", constant="", status="zugeordnet"): return Binding(tro=tro, slot=slot, role=role, bmk=bmk, constant=constant, status=status) def anlage() -> AnlageDocument: """SYS1 -> TRO01 (1Sep1Swi, Kreisel3) -> TRO02 (1Sep, Kreisel3) / TRO03 (Vario, VF_1).""" tros = [ _tro("TRO01", "1Sep1Swi", ["0042"], ["0010-R"], predecessors=["SYS1"], successors=["TRO02", "TRO03"]), _tro("TRO02", "1Sep", ["0043"], ["0010-L"], predecessors=["TRO01"], successors=["SYS2"]), _tro("TRO03", "Vario", ["0062"], ["0015"], predecessors=["TRO01"]), ] parts = [_part("0010", "Kreisel", "Kreisel3"), _part("0015", "Strecke", "VF_1")] layout = TroDocument.from_analysis(Analysis(tros=tros), source="anlage.csv", parts=parts) return AnlageDocument( layout_file="anlage_tro.json", hw_file="Anlage_hw.json", layout=layout, bindings=[ _bind("TRO01", "separator1", "sensor", "BG0532", "cInBG0532"), _bind("TRO01", "separator1", "stopper", "MB0533", "cOutMB0533"), _bind("TRO01", "switch1", "valve", "MB0540", "cOutMB0540"), # Widerspruch: Name ist da, aber nicht verlaesslich -> bleibt offen _bind("TRO02", "separator1", "sensor", "BG0600", "cInBG0600", status="widerspruch"), _bind("TRO02", "separator1", "stopper", status="offen"), _bind("TRO03", "drive", "senJam", "BG0900", "cInBG0900"), ], conveyors=[ ConveyorBinding(eplan_name="K0102", layout_id="0010", layout_name="Kreisel3", motor="MA0202", protection="FC0202"), ConveyorBinding(eplan_name="K0106", layout_id="0015", layout_name="VF_1", motor="MA0210", protection="FC0210"), ], ) def _by_id(context, tro_id): return next(t for t in context.tros if t.id == tro_id) def _kontext(doc: AnlageDocument): return build_context(doc.layout, "anlage.json", created="2026-01-01 00:00", anlage=doc) def test_separator_und_weiche_aus_den_zuordnungen(): tro = _by_id(_kontext(anlage()), "TRO01") sep = tro.separators[0] assert (sep.sensor.value, sep.sensor.todo) == ("cInBG0532", False) assert (sep.stopper.value, sep.stopper.todo) == ("MB0533", False) assert (tro.switches[0].valve.value, tro.switches[0].valve.todo) == ("MB0540", False) # Rolle ohne Zuordnung bleibt offen assert sep.part_sensor.todo def test_offen_und_widerspruch_bleiben_todo(): sep = _by_id(_kontext(anlage()), "TRO02").separators[0] assert (sep.sensor.value, sep.sensor.todo) == ("cInBG____", True) assert (sep.stopper.value, sep.stopper.todo) == ("MB____", True) def test_kreisel_freigabe_und_staubereich_aus_der_foerderernummer(): ctx = _kontext(anlage()) tro01 = _by_id(ctx, "TRO01") assert tro01.carousel_run.value == "#fbCarousel0102.xOutConveyorFullSpeed" assert not tro01.carousel_run.todo # Staubereich: Foerderer bekannt, laufende Nummer nicht -> weiter offen assert tro01.jam_entries[0].db.value == '"DB_JamArea0102".stJam0102_____' assert tro01.jam_entries[0].db.todo # Ausgang zu TRO03 liegt auf VF_1 (K0106) assert tro01.jam_exits[1].db.value == '"DB_JamArea0106".stJam0106_____' # TRO03 sitzt auf einer Strecke, nicht am Kreisel -> Freigabe bleibt offen assert _by_id(ctx, "TRO03").carousel_run.todo def test_vario_antrieb_ueber_den_foerderer(): tro = _by_id(_kontext(anlage()), "TRO03") assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA0210", False) assert (tro.motor_protection.value, tro.motor_protection.todo) == ('"FC0210"', False) assert tro.vario_sensors["stSenJam"].value == "cInBG0900" assert tro.vario_sensors["stSenFinger"].todo def test_ohne_hardwareplanung_alles_offen(): doc = anlage() ctx = build_context(doc.layout, "anlage_tro.json") tro = _by_id(ctx, "TRO01") assert tro.separators[0].sensor.todo and tro.carousel_run.todo assert ctx.hw_source == "" def _hw(*ohne: str) -> HardwareDocument: namen = [("BG0532", "cInBG0532"), ("MB0533", "cOutMB0533"), ("MB0540", "cOutMB0540"), ("BG0900", "cInBG0900"), ("MA0210", ""), ("FC0210", "")] return HardwareDocument(devices=[HwDevice(bmk=b, constant=c) for b, c in namen if b not in ohne]) def _lauf(tmp_path, monkeypatch, *weitere: str, hw: HardwareDocument | None = None): (tmp_path / "anlage_anlage.json").write_text(anlage().to_json(), encoding="utf-8") (tmp_path / "Anlage_hw.json").write_text((hw or _hw()).to_json(), encoding="utf-8") monkeypatch.setenv("SKEL_RESULTS", str(tmp_path)) rc = scl_skeleton.main(["--json", "anlage_anlage.json", *weitere]) return rc, tmp_path / "anlage_FB_Main.scl" def test_scl_skeleton_liest_die_anlagen_json(tmp_path, monkeypatch, capsys): rc, scl = _lauf(tmp_path, monkeypatch, "--templates") assert rc == 0 text = scl.read_text(encoding="utf-8") assert "// Hardwareplanung: Anlage_hw.json + anlage_tro.json" in text assert '#TRO01.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG0532"];\n' in text assert 'xOutStopper1 => "MB0533",\n' in text assert "xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed," in text assert "0 unbekannte Namen" in capsys.readouterr().out def test_weniger_offene_stellen_als_mit_der_tro_json(tmp_path, monkeypatch): rc, scl = _lauf(tmp_path, monkeypatch, "--templates") mit = scl.read_text(encoding="utf-8").count(TODO) (tmp_path / "anlage_tro.json").write_text(anlage().layout.to_json(), encoding="utf-8") assert scl_skeleton.main(["--json", "anlage_tro.json", "--templates", "--out", "ohne.scl"]) == 0 ohne = (tmp_path / "ohne.scl").read_text(encoding="utf-8").count(TODO) assert rc == 0 and mit < ohne def test_eplan_und_anlagen_json_schliessen_sich_aus(tmp_path, monkeypatch): rc, scl = _lauf(tmp_path, monkeypatch, "--eplan", "x_eplan.json") assert rc == 1 assert not scl.exists() def test_unbekannter_name_warnt_und_bricht_mit_strict_ab(tmp_path, monkeypatch, capsys): rc, scl = _lauf(tmp_path, monkeypatch, hw=_hw("MB0540")) assert rc == 0 and scl.exists() assert "WARNUNG: 1 eingesetzte Namen fehlen in Anlage_hw.json: MB0540" in \ capsys.readouterr().err scl.unlink() rc, scl = _lauf(tmp_path, monkeypatch, "--strict", hw=_hw("MB0540")) assert rc == 1 assert not scl.exists()