Files
sps_skel/tests/test_scl_anlage.py
T
s.ayadi 39b445a7c4 scl_skeleton: Anlagen-JSON als Eingabe (Stufe 4, Namen)
--json erkennt <csv>_anlage.json am Schema. scl_context.HwSource setzt
Namen aus bindings (nur "zugeordnet") und conveyors ein: Separator-,
Lagerlinien-, Weichen- und Vario-Antriebsnamen, #fbCarousel<NNNN> am
Kreisel, Foerderernummer im DB_JamArea-Platzhalter (<k> bleibt offen).
Eingesetzte Namen werden gegen <projekt>_hw.json geprueft (--hw,
--strict bricht ab). --eplan bleibt fuer die TRO-JSON; Templates und
Golden-Files unveraendert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:53:52 +02:00

184 lines
7.6 KiB
Python

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