39b445a7c4
--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>
184 lines
7.6 KiB
Python
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()
|
|
|