f607ee7502
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
42 lines
2.7 KiB
Python
42 lines
2.7 KiB
Python
# Compares the gate's raw.all wire rows with direct read-only SQL on the AVEVA Runtime DB (History, Full, UTC).
|
|
import json, sys, os, subprocess, math
|
|
from datetime import datetime, timezone
|
|
rep = json.load(open(sys.argv[1]))
|
|
def sql(q):
|
|
env = dict(os.environ, SQLCMDPASSWORD=os.environ['HISTORIAN_SQL_PASSWORD'])
|
|
out = subprocess.run(['sqlcmd', '-S', 'wonder-sql-vd03.zmr.zimmer.com,1433', '-U', os.environ['HISTORIAN_SQL_USER'], '-C',
|
|
'-d', 'Runtime', '-h', '-1', '-W', '-s', '|', '-Q', 'SET NOCOUNT ON; ' + q],
|
|
env=env, capture_output=True, text=True, timeout=120)
|
|
if out.returncode: raise SystemExit(out.stderr + out.stdout)
|
|
return [l.split('|') for l in out.stdout.splitlines() if l.strip()]
|
|
def iso(s): # '2026-11-10 11:30:01.6200000' -> datetime
|
|
return datetime.strptime(s[:23], '%Y-%m-%d %H:%M:%S.%f').replace(tzinfo=timezone.utc)
|
|
def wts(s): return datetime.fromisoformat(s.replace('Z', '+00:00'))
|
|
total = bad = 0
|
|
for c in rep['Cases']:
|
|
if c['Name'] != 'raw.all' or c['Path'] != 'wire' or c['Tag'].startswith('OtOpcUa.'): continue
|
|
start, end = c['Window'].split('/')
|
|
s0, e0 = wts(start), wts(end)
|
|
rows = sql(f"SELECT CONVERT(varchar(27), DateTime, 121), CONVERT(varchar(40), Value, 3), Quality, QualityDetail, OPCQuality "
|
|
f"FROM dbo.History WHERE TagName = '{c['Tag']}' AND wwRetrievalMode = 'Full' AND wwTimeZone = 'UTC' "
|
|
f"AND DateTime >= '{s0:%Y-%m-%d %H:%M:%S}' AND DateTime < '{e0:%Y-%m-%d %H:%M:%S}' ORDER BY DateTime")
|
|
# AVEVA's synthetic boundary row (Quality 133) at the query start is not a stored sample (#687): OtOpcUa drops it.
|
|
stored = [r for r in rows if not (r[2] == '133' and iso(r[0]) == s0)]
|
|
wire = c['Rows']
|
|
mism = []
|
|
if len(stored) != len(wire): mism.append(f"count sql={len(stored)} wire={len(wire)}")
|
|
for i, (r, w) in enumerate(zip(stored, wire)):
|
|
t_ok = abs((iso(r[0]) - wts(w['Src'])).total_seconds()) < 0.0005
|
|
sv = None if r[1] == 'NULL' else float(r[1])
|
|
wv = None if w['V'] is None else float(w['V'])
|
|
v_ok = (sv is None and wv is None) or (sv is not None and wv is not None and sv == wv)
|
|
good = r[4] == '192'
|
|
q_ok = (w['Status'] & 0xC0000000) == (0 if good else 0x80000000)
|
|
if not (t_ok and v_ok and q_ok): mism.append(f"[{i}] sql={r} wire={w}"); break
|
|
total += 1; bad += bool(mism)
|
|
q = {}
|
|
for r in stored: q[(r[2], r[3], r[4])] = q.get((r[2], r[3], r[4]), 0) + 1
|
|
print(f"{c['Tag']:<30} {start[:19]} sql_stored={len(stored)} (boundary rows dropped={len(rows)-len(stored)}) wire={len(wire)} "
|
|
f"quality(Q,QD,OPC)={q} -> {'MATCH' if not mism else 'MISMATCH ' + '; '.join(mism)}")
|
|
print(f"{total} tag-windows compared, {bad} mismatching")
|