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
212 lines
13 KiB
Diff
212 lines
13 KiB
Diff
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AvevaGatewayHistoryLiveGateTests.cs
|
|
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/AvevaGatewayHistoryLiveGateTests.cs
|
|
@@ -15,7 +15,7 @@
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
|
|
|
|
/// <summary>
|
|
-/// Gitea #801 (SDK 2 live gate, "real AVEVA history"): the booted OtOpcUa server, with the <b>production</b>
|
|
+/// Gitea #801 BASELINE PORT (SDK 1.5.378.182, master efb9197a) (SDK 2 live gate, "real AVEVA history"): the booted OtOpcUa server, with the <b>production</b>
|
|
/// Gateway backend (<see cref="HistorianBackendRegistry"/> → <c>GatewayHistorianDataSource</c>) pointed at a REAL
|
|
/// HistorianGateway in front of the real AVEVA Historian, serves HistoryRead over a real SDK client session.
|
|
/// Every wire answer is compared with the same backend's direct <see cref="IHistorianDataSource"/> answer
|
|
@@ -283,11 +283,11 @@
|
|
{
|
|
var t = DateTime.UtcNow.AddHours(-1);
|
|
var insert = new UpdateDataDetails { NodeId = Node(sandboxNode), PerformInsertReplace = PerformUpdateType.Insert };
|
|
- insert.UpdateValues += new DataValue(Variant.From(801.0), StatusCodes.Good, t, t);
|
|
+ insert.UpdateValues.Add(new DataValue(new Variant(801.0), StatusCodes.Good, t, t));
|
|
var del = new DeleteRawModifiedDetails { NodeId = Node(sandboxNode), IsDeleteModified = false, StartTime = t, EndTime = t.AddSeconds(1) };
|
|
foreach (var (name, details) in new (string, IEncodeable)[] { ("insert", insert), ("deleteRaw", del) })
|
|
{
|
|
- var response = await session.HistoryUpdateAsync(null, new ExtensionObject[] { new(details) }, ct);
|
|
+ var response = await session.HistoryUpdateAsync(null, new ExtensionObjectCollection { new ExtensionObject(details) }, ct);
|
|
var result = response.Results.ToList().ShouldHaveSingleItem();
|
|
var status = Name(result.StatusCode);
|
|
report.Add(new Case($"historyUpdate.{name}", sandboxTag, null, "wire", status,
|
|
@@ -316,7 +316,7 @@
|
|
{
|
|
StartTime = start, EndTime = end, NumValuesPerNode = max, IsReadModified = false, ReturnBounds = returnBounds,
|
|
};
|
|
- var (status, rows, cp) = await HistoryReadAsync(session, details, node, default, ct);
|
|
+ var (status, rows, cp) = await HistoryReadAsync(session, details, node, null, ct);
|
|
await ReleaseAsync(session, details, node, cp, ct);
|
|
return new WireRead(status, rows, 1);
|
|
}
|
|
@@ -330,7 +330,7 @@
|
|
};
|
|
var all = new List<Row>();
|
|
var pages = 0;
|
|
- ByteString cp = default;
|
|
+ byte[]? cp = null;
|
|
string status;
|
|
do
|
|
{
|
|
@@ -339,14 +339,14 @@
|
|
pages++;
|
|
if (status.StartsWith("Bad", StringComparison.Ordinal)) break;
|
|
}
|
|
- while (!cp.IsEmpty && pages < 10_000);
|
|
+ while (cp is { Length: > 0 } && pages < 10_000);
|
|
return new WireRead(status, all, pages);
|
|
}
|
|
|
|
private static async Task<string> ReadModifiedAsync(ISession session, NodeId node, DateTime start, DateTime end, CancellationToken ct)
|
|
{
|
|
var details = new ReadRawModifiedDetails { StartTime = start, EndTime = end, NumValuesPerNode = 0, IsReadModified = true, ReturnBounds = false };
|
|
- var (status, _, cp) = await HistoryReadAsync(session, details, node, default, ct);
|
|
+ var (status, _, cp) = await HistoryReadAsync(session, details, node, null, ct);
|
|
await ReleaseAsync(session, details, node, cp, ct);
|
|
return status;
|
|
}
|
|
@@ -357,12 +357,12 @@
|
|
var details = new ReadProcessedDetails
|
|
{
|
|
StartTime = start, EndTime = end, ProcessingInterval = IntervalMs,
|
|
- AggregateType = new List<NodeId> { aggregate },
|
|
+ AggregateType = new NodeIdCollection { aggregate },
|
|
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
|
};
|
|
var all = new List<Row>();
|
|
var pages = 0;
|
|
- ByteString cp = default;
|
|
+ byte[]? cp = null;
|
|
string status;
|
|
do
|
|
{
|
|
@@ -371,36 +371,36 @@
|
|
pages++;
|
|
if (status.StartsWith("Bad", StringComparison.Ordinal)) break;
|
|
}
|
|
- while (!cp.IsEmpty && pages < 1000);
|
|
+ while (cp is { Length: > 0 } && pages < 1000);
|
|
return new WireRead(status, all, pages);
|
|
}
|
|
|
|
private static async Task<WireRead> ReadAtTimeAsync(ISession session, NodeId node, IReadOnlyList<DateTime> times, CancellationToken ct)
|
|
{
|
|
- var details = new ReadAtTimeDetails { ReqTimes = times.Select(t => (DateTimeUtc)t).ToList(), UseSimpleBounds = true };
|
|
- var (status, rows, cp) = await HistoryReadAsync(session, details, node, default, ct);
|
|
+ var details = new ReadAtTimeDetails { ReqTimes = new DateTimeCollection(times), UseSimpleBounds = true };
|
|
+ var (status, rows, cp) = await HistoryReadAsync(session, details, node, null, ct);
|
|
await ReleaseAsync(session, details, node, cp, ct);
|
|
return new WireRead(status, rows, 1);
|
|
}
|
|
|
|
- private static async Task<(string Status, List<Row> Rows, ByteString Cp)> HistoryReadAsync(
|
|
- ISession session, IEncodeable details, NodeId node, ByteString cp, CancellationToken ct)
|
|
+ private static async Task<(string Status, List<Row> Rows, byte[]? Cp)> HistoryReadAsync(
|
|
+ ISession session, IEncodeable details, NodeId node, byte[]? cp, CancellationToken ct)
|
|
{
|
|
var response = await session.HistoryReadAsync(null, new ExtensionObject(details), TimestampsToReturn.Both, false,
|
|
- new HistoryReadValueId[] { new() { NodeId = node, ContinuationPoint = cp } }, ct);
|
|
+ new HistoryReadValueIdCollection { new HistoryReadValueId { NodeId = node, ContinuationPoint = cp } }, ct);
|
|
var result = response.Results.ToList().ShouldHaveSingleItem();
|
|
var rows = new List<Row>();
|
|
- if (!result.HistoryData.IsNull && ExtensionObject.ToEncodeable(result.HistoryData) is HistoryData data)
|
|
+ if (result.HistoryData?.Body is HistoryData data)
|
|
rows.AddRange(data.DataValues.ToList().Select(dv => new Row(
|
|
- Fmt(dv.ClrValue()), dv.StatusCode.Code, Ts(dv.SourceTimestamp), Ts(dv.ServerTimestamp))));
|
|
+ Fmt(dv.Value), dv.StatusCode.Code, Ts(dv.SourceTimestamp), Ts(dv.ServerTimestamp))));
|
|
return (Name(result.StatusCode), rows, result.ContinuationPoint);
|
|
}
|
|
|
|
- private static async Task ReleaseAsync(ISession session, IEncodeable details, NodeId node, ByteString cp, CancellationToken ct)
|
|
+ private static async Task ReleaseAsync(ISession session, IEncodeable details, NodeId node, byte[]? cp, CancellationToken ct)
|
|
{
|
|
- if (cp.IsEmpty) return;
|
|
+ if (cp is not { Length: > 0 }) return;
|
|
await session.HistoryReadAsync(null, new ExtensionObject(details), TimestampsToReturn.Both, true,
|
|
- new HistoryReadValueId[] { new() { NodeId = node, ContinuationPoint = cp } }, ct);
|
|
+ new HistoryReadValueIdCollection { new HistoryReadValueId { NodeId = node, ContinuationPoint = cp } }, ct);
|
|
}
|
|
|
|
private static async Task<EventRead> ReadEventsAsync(ISession session, NodeId node, DateTime start, DateTime end, uint page,
|
|
@@ -408,33 +408,33 @@
|
|
{
|
|
var filter = new EventFilter();
|
|
foreach (var field in new[] { "EventId", "Time", "ReceiveTime", "SourceName", "Message", "Severity" })
|
|
- filter.SelectClauses += new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName(field)) { AttributeId = Attributes.Value };
|
|
+ filter.SelectClauses.Add(new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName(field)) { AttributeId = Attributes.Value });
|
|
var details = new ReadEventDetails { StartTime = start, EndTime = end, NumValuesPerNode = page, Filter = filter };
|
|
var rows = new List<EventRow>();
|
|
var pages = 0;
|
|
- ByteString cp = default;
|
|
+ byte[]? cp = null;
|
|
string status;
|
|
do
|
|
{
|
|
var response = await session.HistoryReadAsync(null, new ExtensionObject(details), TimestampsToReturn.Both, false,
|
|
- new HistoryReadValueId[] { new() { NodeId = node, ContinuationPoint = cp } }, ct);
|
|
+ new HistoryReadValueIdCollection { new HistoryReadValueId { NodeId = node, ContinuationPoint = cp } }, ct);
|
|
var result = response.Results.ToList().ShouldHaveSingleItem();
|
|
status = Name(result.StatusCode);
|
|
cp = result.ContinuationPoint;
|
|
pages++;
|
|
- if (!result.HistoryData.IsNull && ExtensionObject.ToEncodeable(result.HistoryData) is HistoryEvent he)
|
|
+ if (result.HistoryData?.Body is HistoryEvent he)
|
|
foreach (var e in he.Events.ToList())
|
|
{
|
|
var f = e.EventFields.ToList();
|
|
rows.Add(new EventRow(
|
|
- f[0].ClrValue() is byte[] id ? System.Text.Encoding.UTF8.GetString(id) : Fmt(f[0].ClrValue()),
|
|
- f[1].ClrValue() as DateTime? ?? default, f[2].ClrValue() as DateTime? ?? default,
|
|
- f[3].ClrValue() as string, f[4].ClrValue() is LocalizedText lt ? lt.Text : Fmt(f[4].ClrValue()),
|
|
- Convert.ToUInt16(f[5].ClrValue() ?? 0, CultureInfo.InvariantCulture)));
|
|
+ f[0].Value is byte[] id ? System.Text.Encoding.UTF8.GetString(id) : Fmt(f[0].Value),
|
|
+ f[1].Value as DateTime? ?? default, f[2].Value as DateTime? ?? default,
|
|
+ f[3].Value as string, f[4].Value is LocalizedText lt ? lt.Text : Fmt(f[4].Value),
|
|
+ Convert.ToUInt16(f[5].Value ?? 0, CultureInfo.InvariantCulture)));
|
|
}
|
|
if (status.StartsWith("Bad", StringComparison.Ordinal)) break;
|
|
}
|
|
- while (!cp.IsEmpty && pages < 10_000);
|
|
+ while (cp is { Length: > 0 } && pages < 10_000);
|
|
return new EventRead(status, rows, pages);
|
|
}
|
|
|
|
@@ -446,9 +446,9 @@
|
|
("UserAccessLevel", Attributes.UserAccessLevel), ("DataType", Attributes.DataType),
|
|
};
|
|
var response = await session.ReadAsync(null, 0, TimestampsToReturn.Neither,
|
|
- ids.Select(a => new ReadValueId { NodeId = node, AttributeId = a.Id }).ToArray(), ct);
|
|
+ new ReadValueIdCollection(ids.Select(a => new ReadValueId { NodeId = node, AttributeId = a.Id })), ct);
|
|
var results = response.Results.ToList();
|
|
- return ids.Select((a, i) => (a.Name, Value: Fmt(results[i].ClrValue())))
|
|
+ return ids.Select((a, i) => (a.Name, Value: Fmt(results[i].Value)))
|
|
.ToDictionary(x => x.Name, x => x.Value);
|
|
}
|
|
|
|
@@ -460,9 +460,9 @@
|
|
if (vars.Count > 0)
|
|
{
|
|
var response = await session.ReadAsync(null, 0, TimestampsToReturn.Neither,
|
|
- vars.Select(r => new ReadValueId { NodeId = ExpandedNodeId.ToNodeId(r.NodeId, session.NamespaceUris), AttributeId = Attributes.Value }).ToArray(), ct);
|
|
+ new ReadValueIdCollection(vars.Select(r => new ReadValueId { NodeId = ExpandedNodeId.ToNodeId(r.NodeId, session.NamespaceUris), AttributeId = Attributes.Value })), ct);
|
|
var results = response.Results.ToList();
|
|
- for (var i = 0; i < vars.Count; i++) caps[vars[i].BrowseName.Name!] = Fmt(results[i].ClrValue());
|
|
+ for (var i = 0; i < vars.Count; i++) caps[vars[i].BrowseName.Name!] = Fmt(results[i].Value);
|
|
}
|
|
var aggs = await BrowseAsync(session, ObjectIds.HistoryServerCapabilities_AggregateFunctions, ReferenceTypeIds.Organizes, ct);
|
|
caps["AggregateFunctions"] = string.Join(",", aggs.Select(r => r.BrowseName.Name).OrderBy(n => n, StringComparer.Ordinal));
|
|
@@ -472,9 +472,9 @@
|
|
private static async Task<List<ReferenceDescription>> BrowseAsync(ISession session, NodeId node, NodeId referenceType, CancellationToken ct)
|
|
{
|
|
var response = await session.BrowseAsync(null, null, 0,
|
|
- new BrowseDescription[]
|
|
+ new BrowseDescriptionCollection
|
|
{
|
|
- new()
|
|
+ new BrowseDescription
|
|
{
|
|
NodeId = node, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = referenceType,
|
|
IncludeSubtypes = true, NodeClassMask = 0, ResultMask = (uint)BrowseResultMask.All,
|
|
@@ -483,7 +483,7 @@
|
|
return response.Results[0].References.ToList();
|
|
}
|
|
|
|
- private static DateTime? Ts(DateTimeUtc t) => (DateTime)t == DateTime.MinValue ? null : DateTime.SpecifyKind((DateTime)t, DateTimeKind.Utc);
|
|
+ private static DateTime? Ts(DateTime t) => t == DateTime.MinValue ? null : DateTime.SpecifyKind(t, DateTimeKind.Utc);
|
|
|
|
private static string Name(StatusCode code) =>
|
|
code.SymbolicId is { Length: > 0 } s ? s : $"0x{code.Code:X8}";
|