1ac17b95e2
- validate_anlage.py (+ bin-Wrapper): Pruefung vor der SCL fuer beide Wege (Struktur, Zuordnungen, Aktualitaet der JSON, Korrekturen ohne Ziel); scl_skeleton ruft sie auf (Warnung, --strict bricht ab) - DerivedTro.key: stabiler Schluessel (ID der ID-Datei); hw_overrides.ini, <csv>_jam_names.ini und tro_overrides.ini greifen ueber ID oder Schluessel, eine Umbenennung in TRO_EDIT laesst sie bestehen; JAM_AREA traegt KEY - --read-tros liest die CONNECTION_ARROW-Bloecke mit -> [<csv>.edit_connections]; tro_flow.apply_connection_edits wendet sie nach link_tros an - tro_annotate --anlage zeichnet Marker/Verbindungen aus der Anlagen-JSON - INI-Korrekturen auch mit BOM lesbar (utf-8-sig) - Ablauf CSV/DXF+TIA -> Anlagen-JSON -> (BricsCAD) -> SCL in doc/Python_Scripts.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
233 lines
10 KiB
Python
233 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests fuer den Ablauf Anlagen-JSON -> (BricsCAD) -> SCL.
|
|
|
|
- stabiler Schluessel (DerivedTro.key): Korrekturen ueberleben eine Umbenennung
|
|
- Verbindungen aus BricsCAD (edit_connections, tro_flow.apply_connection_edits,
|
|
tro_annotate._read_connections)
|
|
- Pruefung vor der SCL (validate_anlage)
|
|
- Zeichnung aus der JSON (tro_annotate.analysis_from_document)
|
|
|
|
Fixture: Testanlage test_system (wie test_tro_edits.py).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from material_flow import build_graph, read_elements
|
|
from model import AnlageDocument, Binding, TroDocument
|
|
from scl_context import jam_override, jam_stable_key
|
|
from tro_annotate import analysis_from_document, format_slots, read_tros, tro_slots
|
|
from tro_flow import analyse_tros
|
|
from tro_overrides import ConnectionEdit, TroEdit, load_overrides, save_edits
|
|
from validate_anlage import FEHLER, validate
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "test_system_export.csv"
|
|
CSV = FIXTURE.name
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def layout():
|
|
elements = read_elements(FIXTURE, [])
|
|
return elements, build_graph(elements, CSV, [])
|
|
|
|
|
|
def _analyse(layout, edits=None, connection_edits=None):
|
|
elements, graph = layout
|
|
return analyse_tros(graph, elements, edits=edits, connection_edits=connection_edits)
|
|
|
|
|
|
def _tro_kante(analysis):
|
|
"""Eine abgeleitete Kante zwischen zwei TROs (keine Station)."""
|
|
ids = {t.tro_id for t in analysis.tros}
|
|
return next((s, d) for s, d in analysis.edges if s in ids and d in ids)
|
|
|
|
|
|
def _markers(analysis):
|
|
return [{"ID": t.tro_id, "KEY": analysis.edit_key(t.tro_id), "TYPE": t.type_name,
|
|
"UID": f"{n:04d}", "SLOTS": format_slots(*tro_slots(t)),
|
|
"_BLOCK": f"TRO_SYM_{t.type_name}"}
|
|
for n, t in enumerate(analysis.tros, start=100)]
|
|
|
|
|
|
def _arrows(analysis, markers):
|
|
uid = {m["ID"]: m["UID"] for m in markers}
|
|
ids = {t.tro_id for t in analysis.tros}
|
|
return [{"FROM_TRO": s, "TO_TRO": d, "FROM_UID": uid[s], "TO_UID": uid[d],
|
|
"KIND": "flow", "_BLOCK": "CONNECTION_ARROW"}
|
|
for s, d in analysis.edges if s in ids and d in ids]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stabiler Schluessel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_umbenannter_tro_traegt_seinen_schluessel(layout):
|
|
raw = _analyse(layout)
|
|
key = raw.tros[0].tro_id
|
|
doc = TroDocument.from_analysis(_analyse(layout, [TroEdit(key=key, name="Neu_1")]), CSV)
|
|
tro = doc.by_id("Neu_1")
|
|
assert tro.key == key and tro.stable_key == key
|
|
assert doc.resolve(key) is tro and doc.resolve("Neu_1") is tro
|
|
# Staubereichs-Korrektur unter dem alten Schluessel gilt weiter
|
|
area = next(a for a in doc.jam_areas_or_derived() if a.tro == "Neu_1")
|
|
assert jam_stable_key(area, doc) == f"{key}.{area.entry}"
|
|
assert jam_override(area, doc, {f"{key}.{area.entry}": ("0042", 3)}) == ("0042", 3)
|
|
# unveraenderte TROs: Schluessel = ID
|
|
assert all(t.key == t.tro_id for t in doc.tros if t.tro_id != "Neu_1")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Verbindungen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_connection_edit_hin_und_zurueck(tmp_path):
|
|
c = ConnectionEdit.parse("+ TRO01 -> TRO05 | weiche")
|
|
assert (c.add, c.src, c.dst, c.kind) == (True, "TRO01", "TRO05", "weiche")
|
|
assert ConnectionEdit.parse(c.render()) == c
|
|
assert not ConnectionEdit.parse("- TRO01 -> TRO05").add
|
|
with pytest.raises(ValueError):
|
|
ConnectionEdit.parse("TRO01 -> TRO05")
|
|
save_edits(CSV, [TroEdit(key="TRO02", name="X")], tmp_path,
|
|
[c, ConnectionEdit(add=False, src="TRO03", dst="TRO04")])
|
|
o = load_overrides(CSV, tmp_path)
|
|
assert o.connection_edits == [c, ConnectionEdit(add=False, src="TRO03", dst="TRO04")]
|
|
assert [e.key for e in o.edits] == ["TRO02"]
|
|
save_edits(CSV, [], tmp_path, [])
|
|
assert "edit_connections" not in (tmp_path / "tro_overrides.ini").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_verbindungen_aendern_vorgaenger_und_nachfolger(layout):
|
|
raw = _analyse(layout)
|
|
s, d = _tro_kante(raw)
|
|
andere = next(t.tro_id for t in raw.tros
|
|
if t.tro_id not in (s, d) and (t.tro_id, d) not in raw.edges)
|
|
analysis = _analyse(layout, connection_edits=[
|
|
ConnectionEdit(add=False, src=s, dst=d),
|
|
ConnectionEdit(add=True, src=andere, dst=d),
|
|
ConnectionEdit(add=True, src="GIBTSNICHT", dst=d)])
|
|
assert (s, d) not in analysis.edges and (andere, d) in analysis.edges
|
|
assert analysis.edge_kinds[(andere, d)] == "transfer"
|
|
ziel = analysis.by_id(d)
|
|
assert andere in ziel.predecessors and s not in ziel.predecessors
|
|
assert d not in analysis.by_id(s).successors
|
|
assert any("GIBTSNICHT gibt es nicht" in f for f in analysis.findings)
|
|
# die Staubereiche folgen den Vorgaengern
|
|
doc = TroDocument.from_analysis(analysis, CSV)
|
|
eingang = next(a for a in doc.jam_areas if a.tro == d and a.entry == 1)
|
|
assert andere in eingang.predecessors
|
|
|
|
|
|
def test_verbindung_ueber_schluessel_nach_umbenennung(layout):
|
|
raw = _analyse(layout)
|
|
s, d = _tro_kante(raw)
|
|
analysis = _analyse(layout, [TroEdit(key=s, name="Quelle_1")],
|
|
[ConnectionEdit(add=False, src=s, dst=d)])
|
|
assert ("Quelle_1", d) not in analysis.edges
|
|
|
|
|
|
def test_pfeile_der_zeichnung_werden_zu_verbindungskorrekturen(layout, tmp_path):
|
|
elements, _ = layout
|
|
raw = _analyse(layout)
|
|
markers = _markers(raw)
|
|
arrows = _arrows(raw, markers)
|
|
# unveraendert: keine Korrektur
|
|
_, meldungen = read_tros(markers, raw, elements, CSV, tmp_path, arrows=arrows)
|
|
assert load_overrides(CSV, tmp_path).connection_edits == []
|
|
|
|
s, d = _tro_kante(raw)
|
|
andere = next(t.tro_id for t in raw.tros
|
|
if t.tro_id not in (s, d) and (t.tro_id, d) not in raw.edges)
|
|
arrows = [a for a in arrows if (a["FROM_TRO"], a["TO_TRO"]) != (s, d)]
|
|
arrows.append({"FROM_TRO": andere, "TO_TRO": d, "FROM_UID": "", "TO_UID": "",
|
|
"KIND": "flow", "_BLOCK": "CONNECTION_ARROW"})
|
|
# ein Marker umbenannt, seine Pfeile tragen noch den alten Namen -> ueber UID
|
|
quelle = next(m for m in markers if m["ID"] == s)
|
|
quelle["ID"] = "Quelle_1"
|
|
read_tros(markers, raw, elements, CSV, tmp_path, arrows=arrows)
|
|
o = load_overrides(CSV, tmp_path)
|
|
assert o.connection_edits == [ConnectionEdit(add=False, src=s, dst=d),
|
|
ConnectionEdit(add=True, src=andere, dst=d)]
|
|
assert [(e.key, e.name) for e in o.edits] == [(s, "Quelle_1")]
|
|
|
|
# Rundlauf: angewandt, neu gezeichnet, wieder gelesen -> dasselbe
|
|
edited = _analyse(layout, o.edits, o.connection_edits)
|
|
m2 = _markers(edited)
|
|
again, _ = read_tros(m2, raw, elements, CSV, tmp_path, write=False,
|
|
arrows=_arrows(edited, m2), base=_analyse(layout, o.edits))
|
|
assert [(e.key, e.name) for e in again] == [(s, "Quelle_1")]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pruefung
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _anlage_dateien(layout, tmp_path, bindings=None):
|
|
doc = TroDocument.from_analysis(_analyse(layout), CSV)
|
|
tro_file = tmp_path / "x_tro.json"
|
|
tro_file.write_text(doc.to_json(), encoding="utf-8")
|
|
anlage = AnlageDocument(layout=doc, layout_file=tro_file.name, bindings=bindings or [])
|
|
anlage_file = tmp_path / "x_anlage.json"
|
|
anlage_file.write_text(anlage.model_dump_json(by_alias=True, indent=2), encoding="utf-8")
|
|
return tro_file, anlage_file, doc
|
|
|
|
|
|
def test_pruefung_frische_anlage_ist_bereit(layout, tmp_path):
|
|
tro_file, anlage_file, _ = _anlage_dateien(layout, tmp_path)
|
|
os.utime(tro_file, (1_000_000, 1_000_000))
|
|
p = validate(anlage_file, cfg_dir=tmp_path)
|
|
assert p.anzahl_fehler == 0, [str(b) for b in p.befunde]
|
|
assert p.bereit()
|
|
|
|
|
|
def test_pruefung_findet_fehler(layout, tmp_path):
|
|
tro = TroDocument.from_analysis(_analyse(layout), CSV).tros[0]
|
|
bindings = [Binding(tro="TRO99", slot="separator1", role="sensor", bmk="BG1"),
|
|
Binding(tro=tro.tro_id, slot="separator1", role="sensor", bmk="BG2",
|
|
status="widerspruch", note="BMK nicht im TIA-Import"),
|
|
Binding(tro=tro.tro_id, slot="separator1", role="stopper", status="offen")]
|
|
tro_file, anlage_file, _ = _anlage_dateien(layout, tmp_path, bindings)
|
|
# TRO-JSON juenger als die Anlagen-JSON -> veraltet
|
|
os.utime(anlage_file, (1_000_000, 1_000_000))
|
|
(tmp_path / "x_jam_names.ini").write_text("[jam]\nTRO99.1 = 0042_1\n", encoding="utf-8")
|
|
p = validate(anlage_file, cfg_dir=tmp_path)
|
|
texte = [b.text for b in p.befunde if b.stufe == FEHLER]
|
|
assert any("TRO99 steht nicht im Layout" in t for t in texte)
|
|
assert any("BMK nicht im TIA-Import" in t for t in texte)
|
|
assert any("aelter als TRO-JSON" in t for t in texte)
|
|
assert any("stopper 1x" in b.text for b in p.befunde)
|
|
assert not p.bereit()
|
|
# die Staubereichsdatei heisst nach der CSV, nicht nach der JSON
|
|
assert not any("TRO99.1" in b.text for b in p.befunde)
|
|
|
|
|
|
def test_pruefung_unbekannte_verbindung(layout, tmp_path):
|
|
doc = TroDocument.from_analysis(_analyse(layout), CSV)
|
|
doc.connections[0].dst = "TRO99"
|
|
f = tmp_path / "x_tro.json"
|
|
f.write_text(doc.to_json(), encoding="utf-8")
|
|
p = validate(f, cfg_dir=tmp_path)
|
|
assert any("TRO99 unbekannt" in b.text for b in p.befunde if b.stufe == FEHLER)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Zeichnung aus der JSON
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_zeichnung_zeigt_die_json(layout):
|
|
raw = _analyse(layout)
|
|
s = raw.tros[0].tro_id
|
|
doc = TroDocument.from_analysis(_analyse(layout, [TroEdit(key=s, name="Neu_1")]), CSV)
|
|
a = analysis_from_document(doc)
|
|
assert [t.tro_id for t in a.tros] == [t.tro_id for t in doc.tros]
|
|
assert set(a.edges) == {(c.src, c.dst) for c in doc.connections}
|
|
assert a.edit_key("Neu_1") == s
|
|
assert [b.tro_id for b in a.boundaries] == [b.tro_id for b in doc.boundaries]
|