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
26 lines
1.9 KiB
Python
26 lines
1.9 KiB
Python
# Independent recomputation of the simple per-interval aggregates from the wire raw rows (Good samples only, [t, t+60s)).
|
|
import json, sys, math
|
|
from datetime import datetime, timedelta
|
|
rep = json.load(open(sys.argv[1])); C = rep['Cases']
|
|
def ts(s): return datetime.fromisoformat(s.replace('Z', '+00:00'))
|
|
def get(name, tag, win): return next(c for c in C if c['Name'] == name and c['Tag'] == tag and c['Window'] == win and c['Path'] == 'wire')
|
|
checked = bad = 0
|
|
for raw in [c for c in C if c['Name'] == 'raw.all' and c['Path'] == 'wire' and not c['Tag'].startswith('OtOpcUa.')]:
|
|
tag, win = raw['Tag'], raw['Window']
|
|
samples = [(ts(r['Src']), float(r['V'])) for r in raw['Rows'] if r['V'] is not None and r['Status'] & 0xC0000000 == 0]
|
|
nongood = [ts(r['Src']) for r in raw['Rows'] if r['V'] is None or r['Status'] & 0xC0000000 != 0]
|
|
for agg in ['Count', 'Minimum', 'Maximum', 'Average', 'Range', 'Start', 'End', 'Delta']:
|
|
p = get(f'processed.{agg}', tag, win)
|
|
for row in p['Rows']:
|
|
w0 = ts(win.split('/')[0]); k = int((ts(row['Src']) - w0).total_seconds() // 60)
|
|
t0 = w0 + timedelta(seconds=60 * k); t1 = t0 + timedelta(seconds=60) # Start/End carry the sample's own time
|
|
xs = [v for (t, v) in samples if t0 <= t < t1]
|
|
if not xs or any(t0 <= t < t1 for t in nongood): continue # empty / outage intervals: status semantics are compared against SDK1, not recomputed here
|
|
exp = {'Count': len(xs), 'Minimum': min(xs), 'Maximum': max(xs), 'Average': sum(xs) / len(xs),
|
|
'Range': max(xs) - min(xs), 'Start': xs[0], 'End': xs[-1], 'Delta': xs[-1] - xs[0]}[agg]
|
|
got = float(str(row['V']).split(':')[-1])
|
|
checked += 1
|
|
if not math.isclose(got, exp, rel_tol=1e-12, abs_tol=1e-9):
|
|
bad += 1; print('MISMATCH', tag, win[:19], agg, row, 'expected', exp)
|
|
print(f'{checked} non-empty interval values recomputed from raw, {bad} mismatching')
|