Files
ScadaBridge/docker/verify-mes-test-machine.sh
T
Joseph Doherty 0af0672597 deploy(vd03): fe89363b + migrations 56/57; in-memory MES/Delmia/alarm-API test machines
vd03 (DEV) upgraded 2b9a7c3c -> fe89363b via the deployment-only winrm channel: COPY_ONLY backup,
55->57 delta script (AddAlarmMesReportable, AddSiteArtifactRevision), ~53 s window, snapshots
identical to baseline, artifacts deployed. Register row 126 now covers the QA pair only.

docker/seed-mes-test-machine.sh (idempotent) builds a standalone TestMesMachine base template -
Left/Right TestMESReceiver + TestDelmiaReceiver (prod attribute names, STATIC), computed alarms
with IsMesReportable, CvdReactor-compatible MesMoveIn/MesMoveOut/ProcessRecipeDownload/AlarmStatus
scripts ported to SetAsync+WaitAsync, and Galaxy-side simulators with reject/no-response knobs -
plus instances SBTEST01/02 and the SimpleAlarmStatusRequest (was a stub) / AlarmStatusRequest
inbound methods in the legacy /mes/(simple)alarmstatus shapes, resolving machines through BTDB.
BTDB rows SBTEST01=990001 / SBTEST02=990002 added on vd03.
docker/verify-mes-test-machine.sh drives the real inbound methods via flow-debug: PASS 25/25.
2026-09-28 10:10:17 -04:00

126 lines
8.3 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Live gate for docker/seed-mes-test-machine.sh — drives the REAL inbound methods (MesMoveIn, MesMoveOut,
# DelmiaRecipeDownload, SimpleAlarmStatusRequest, AlarmStatusRequest) through `flow-debug run-method`
# (the same inbound executor an HTTP call reaches, minus the API-key check — no key needed) against the
# SBTEST01 / SBTEST02 test machines, and trips/clears the computed alarms by writing their static source
# attributes. Needs the BTDB rows SBTEST01=990001 / SBTEST02=990002 (vd03 only).
#
# SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 bash docker/verify-mes-test-machine.sh
#
# Leaves both machines as it found them: alarm sources reset to their defaults, no batch moved in.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
export SB_CLI="$PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge"
export SB_URL="${SCADABRIDGE_URL:-http://localhost:9000}"
export SB_USER="${SCADABRIDGE_USERNAME:-multi-role}" SB_PASS="${SCADABRIDGE_PASSWORD:-password}"
python3 - <<'PY'
import json, os, subprocess, sys
CLI, URL = os.environ["SB_CLI"], os.environ["SB_URL"]
AUTH = ["--url", URL, "--format=json", "--username", os.environ["SB_USER"], "--password", os.environ["SB_PASS"]]
def cli(*args, timeout=180):
r = subprocess.run([CLI, *args, *AUTH], capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise RuntimeError(f"{' '.join(args[:3])}: {r.stderr.strip() or r.stdout.strip()}")
return json.loads(r.stdout)
methods = {m["name"]: m["id"] for m in cli("api-method", "list")}
instances = {i["uniqueName"]: i["id"] for i in cli("instance", "list")}
def call(method, params):
"""Run an inbound method via the Flow Debugger and return its response body (dict)."""
d = cli("flow-debug", "run-method", "--method-id", str(methods[method]), "--params", json.dumps(params),
"--timeout-seconds", "90", timeout=150)
ev = next(e for e in d["events"] if e["action"] == "ApiInbound.InboundRequest")
det = json.loads(ev["detailsJson"])
if det.get("status") != "Delivered":
raise RuntimeError(f"{method} not delivered: {det.get('status')} {det.get('errorMessage')}")
return json.loads(det["responseSummary"])
def write(inst, attr, value):
subprocess.run([CLI, "flow-debug", "run-value-change", "--instance-id", str(instances[inst]), "--attribute", attr,
"--value", str(value), "--wait-seconds", "1", *AUTH], capture_output=True, text=True, timeout=90)
passed = failed = 0
def check(label, cond, detail=""):
global passed, failed
if cond: passed += 1; print(f" PASS {label}")
else: failed += 1; print(f" FAIL {label} {detail}")
WO = [{"WorkOrderNumber": "WO-1001", "PartNumber": "PN-2001"}]
def movein(sap, wo=WO): return call("MesMoveIn", {"SAPID": sap, "ContainerNumber": "CN-1", "JobSequenceNumber": "10", "NumberWorkOrders": len(wo), "WorkOrders": wo, "OperatorName": "sb-test"})
def moveout(sap, wo=WO): return call("MesMoveOut", {"SAPID": sap, "ContainerNumber": "CN-1", "NumberWorkOrders": len(wo), "WorkOrders": wo, "OperatorName": "sb-test"})
def names(r): return sorted(a["Name"] for a in r.get("Alarms") or [])
print("A. DelmiaRecipeDownload (routes by MachineCode)")
r = call("DelmiaRecipeDownload", {"MachineCode": "SBTEST01", "DownloadPath": r"\\share\recipes\r1.xml", "WorkOrderNumber": "WO-1001", "PartNumber": "PN-2001", "JobStepNumber": "20", "Username": "sb-test"})
check("A1 accepted", r.get("Result") is True and "accepted" in r.get("ResultText", ""), r)
r = call("DelmiaRecipeDownload", {"MachineCode": "SBTEST01", "DownloadPath": r"\\share\recipes\r1.bad", "WorkOrderNumber": "WO-1001"})
check("A2 .bad path rejected", r.get("Result") is False and "reject" in r.get("ResultText", "").lower(), r)
print("B. MesMoveIn / MesMoveOut (SAPID -> BTDB -> SBTEST01)")
r = movein("990001_A")
batch = r.get("BatchID", 0)
check("B1 MoveIn left succeeds with a batch id", r.get("WasSuccessful") is True and batch >= 5000, r)
r = moveout("990001_A")
check("B2 MoveOut left returns the same batch", r.get("WasSuccessful") is True and r.get("BatchID") == batch, r)
check("B3 MoveOut Run returns 16 cycle rows", len(r.get("Data") or []) == 16 and r["Data"][0].get("ValueName") == "TargetRuntime", r.get("Data"))
r = moveout("990001_A")
check("B4 second MoveOut rejected (nothing moved in)", r.get("WasSuccessful") is False and "No batch" in r.get("ErrorText", ""), r)
r = movein("990001_A", [{"WorkOrderNumber": "WO-FAIL-1", "PartNumber": "PN-2001"}])
check("B5 FAIL work order rejected", r.get("WasSuccessful") is False and "FAIL" in r.get("ErrorText", ""), r)
r = movein("990001")
check("B6 missing side suffix", r.get("WasSuccessful") is False and "suffix" in r.get("ErrorText", ""), r)
r = movein("990001_B_LT"); lt = r.get("BatchID", 0)
check("B7 leak-test MoveIn right", r.get("WasSuccessful") is True and lt > 0, r)
r = moveout("990001_B_LT")
check("B8 leak-test MoveOut returns 6 LT rows", r.get("WasSuccessful") is True and len(r.get("Data") or []) == 6 and r["Data"][0]["ValueName"] == "LTType", r)
r = movein("999999_A")
check("B9 unknown SAPID", r.get("WasSuccessful") is False and "No machine found" in r.get("ErrorText", ""), r)
print("C. Alarm APIs")
r = call("SimpleAlarmStatusRequest", {"SAPID": "990001"})
check("C1 no alarms active -> success, empty", r.get("WasSuccessful") is True and names(r) == [], r)
for inst in ("SBTEST01", "SBTEST02"):
write(inst, "VacuumPressure", 12); write(inst, "DoorOpen", "true"); write(inst, "SensorStatus", -1)
r = call("SimpleAlarmStatusRequest", {"SAPID": "990001"})
check("C2 SBTEST01 simple = flagged only (HighVacuumFault)", names(r) == ["HighVacuumFault"], names(r))
a = (r.get("Alarms") or [{}])[0]
check("C3 row shape", a.get("HierarchicalName") == "SBTEST01.HighVacuumFault" and a.get("IsFlaggedForMES") is True
and a.get("Severity") == 800 and a.get("StatusCode") == "Triggered.Acked" and a.get("TriggeredDT"), a)
r = call("SimpleAlarmStatusRequest", {"SAPID": "990002"})
check("C4 SBTEST02 simple includes the overridden SensorFault", names(r) == ["HighVacuumFault", "SensorFault"], names(r))
r = call("AlarmStatusRequest", {"MachineFilter": {"Code": "SBTEST01"}})
check("C5 full, no filter = all 3 active", names(r) == ["DoorOpenAlarm", "HighVacuumFault", "SensorFault"], names(r))
flags = {x["Name"]: x["IsFlaggedForMES"] for x in r.get("Alarms") or []}
check("C6 IsFlaggedForMES per alarm", flags == {"DoorOpenAlarm": False, "HighVacuumFault": True, "SensorFault": False}, flags)
r = call("AlarmStatusRequest", {"MachineFilter": {"SAPID": "990001_A"}, "AlarmFilter": {"MinSeverity": 500}})
check("C7 SAPID w/ suffix + MinSeverity 500", names(r) == ["HighVacuumFault", "SensorFault"], names(r))
r = call("AlarmStatusRequest", {"MachineFilter": {"ZTag": "SBTEST01"}, "AlarmFilter": {"NameFilter": "door"}})
check("C8 ZTag + NameFilter", names(r) == ["DoorOpenAlarm"], names(r))
r = call("AlarmStatusRequest", {"MachineFilter": {"MachineID": 204}, "AlarmFilter": {"FlaggedOnly": True, "MaxSeverity": 750}})
check("C9 MachineID + FlaggedOnly + MaxSeverity", names(r) == ["SensorFault"], names(r))
r = call("AlarmStatusRequest", {"MachineFilter": {"Code": "SBTEST01"}, "AlarmFilter": {"IncludeAcked": False}})
check("C10 IncludeAcked=false drops computed (always-acked) alarms", r.get("WasSuccessful") is True and names(r) == [], names(r))
r = call("AlarmStatusRequest", {"MachineFilter": {"SAPID": "123"}})
check("C11 unknown SAPID error text", r.get("WasSuccessful") is False and "Failed to find machine with SAPID '123'" in (r.get("ErrorText") or ""), r)
r = call("AlarmStatusRequest", {})
check("C12 empty filter error", r.get("WasSuccessful") is False and "machine filter" in (r.get("ErrorText") or ""), r)
r = call("SimpleAlarmStatusRequest", {"SAPID": "103559"})
check("C13 machine without AlarmStatus script -> not supported", r.get("WasSuccessful") is False and "not supported" in (r.get("ErrorText") or ""), r)
print("D. Reset alarm sources")
for inst in ("SBTEST01", "SBTEST02"):
write(inst, "VacuumPressure", 0.5); write(inst, "DoorOpen", "false"); write(inst, "SensorStatus", 0)
r = call("AlarmStatusRequest", {"MachineFilter": {"Code": "SBTEST02"}})
check("D1 cleared", r.get("WasSuccessful") is True and names(r) == [], names(r))
print(f"\n{'PASS' if failed == 0 else 'FAIL'} {passed}/{passed + failed}")
sys.exit(1 if failed else 0)
PY