Files
lmxopcua/scripts/validation/aveva-history-801/diff_runs.py
T
Joseph Doherty f607ee7502 test(historian): #801 real-AVEVA history gate over the SDK wire (env-gated)
AvevaGatewayHistoryLiveGateTests boots the server in-process, wires the production
Gateway backend (HistorianBackendRegistry -> GatewayHistorianDataSource) to a real
HistorianGateway and drives HistoryRead over a real SDK session: raw (all, paged,
reverse, bounds), modified, the ten served aggregates and seven refused ones, AtTime,
events (all, 1-event pages, reverse), a missing tag, HistoryServerCapabilities and
node metadata, and a HistoryUpdate that must be refused (sandbox-bound node only).
Every wire answer is compared with the backend's direct answer; the run is dumped as
JSON for cross-SDK and SQL comparison. Skips unless HISTGW_GATEWAY_ENDPOINT and
HISTGW_GATEWAY_APIKEY are set; read-only by construction.

scripts/validation/aveva-history-801: run/compare instructions, the SDK 1.5.378
baseline port patch (API spelling only), and the diff / SQL / aggregate / histgw-cli
comparison scripts.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 10:02:21 -05:00

29 lines
1.7 KiB
Python

import json, sys, re
a = json.load(open(sys.argv[1])); b = json.load(open(sys.argv[2]))
def key(c): return (c['Name'], c['Tag'], c['Window'], c['Path'])
A = {key(c): c for c in a['Cases']}; B = {key(c): c for c in b['Cases']}
print(f"A={a['Label']} sdk={a['SdkServerAssembly']} cases={len(A)} failures={len(a['Failures'])}")
print(f"B={b['Label']} sdk={b['SdkServerAssembly']} cases={len(B)} failures={len(b['Failures'])}")
only = set(A) ^ set(B)
if only: print("cases only in one run:", sorted(only, key=str))
diffs = 0; rows = 0; events = 0; same = 0
def norm_note(n): return re.sub(r'elapsedMs=\d+', 'elapsedMs=*', n or '')
for k in sorted(set(A) & set(B), key=str):
x, y = A[k], B[k]; d = []
if x['Status'] != y['Status']: d.append(f"status {x['Status']} vs {y['Status']}")
if norm_note(x['Note']) != norm_note(y['Note']): d.append(f"note {x['Note']} vs {y['Note']}")
if (x['Attributes'] or {}) != (y['Attributes'] or {}): d.append(f"attrs {x['Attributes']} vs {y['Attributes']}")
xr, yr = x['Rows'] or [], y['Rows'] or []
rows += len(xr)
if len(xr) != len(yr): d.append(f"rows {len(xr)} vs {len(yr)}")
else:
for i, (p, q) in enumerate(zip(xr, yr)):
if (p['V'], p['Status'], p['Src'], p['Srv'] is None) != (q['V'], q['Status'], q['Src'], q['Srv'] is None):
d.append(f"row[{i}] {p} vs {q}"); break
xe, ye = x['Events'] or [], y['Events'] or []
events += len(xe)
if xe != ye: d.append(f"events {len(xe)} vs {len(ye)}")
if d: diffs += 1; print("DIFF", k, "; ".join(d)[:500])
else: same += 1
print(f"compared {same+diffs} cases: identical={same} differing={diffs}; rows compared={rows}, events compared={events} (ServerTimestamp compared for presence only: it is the read time)")