test: converge historian and runtime fixtures on SDK 2.0

This commit is contained in:
Joseph Doherty
2026-11-10 11:46:47 -05:00
parent 780f363f16
commit a76fca09f6
5 changed files with 42 additions and 37 deletions
@@ -114,7 +114,7 @@ public sealed class GatewayBoundingValueTests
{
var bound = await HistoryBounds.StartBoundAsync(Source(Aveva()), "T", At(-30), reverse: false, 65536, Now, Ct);
bound!.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadBoundNotFound);
bound!.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadBoundNotFound.Code);
bound.SourceTimestampUtc.ShouldBe(At(-30));
bound.Value.ShouldBeNull();
}
@@ -134,7 +134,7 @@ public sealed class GatewayBoundingValueTests
after.Value.ShouldBe(5d);
var missing = await HistoryBounds.EndBoundAsync(source, "T", HistoryBounds.For(At(0), At(101)), At(100), 65536, Now, Ct);
missing.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadBoundNotFound);
missing.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadBoundNotFound.Code);
missing.SourceTimestampUtc.ShouldBe(At(101));
}
@@ -156,7 +156,7 @@ public sealed class GatewayBoundingValueTests
var calls = fake.ReadRawCallCount;
var beyond = Now.AddDays(3);
var missing = await HistoryBounds.EndBoundAsync(source, "T", HistoryBounds.For(At(0), beyond), At(100), 65536, Now, Ct);
missing.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadBoundNotFound);
missing.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadBoundNotFound.Code);
missing.SourceTimestampUtc.ShouldBe(beyond);
fake.ReadRawCallCount.ShouldBe(calls, "a bound beyond the horizon is not found without a read");
source.GetHealthSnapshot().TotalFailures.ShouldBe(0);
@@ -106,7 +106,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
try
{
var first = ReadRaw(nm, nodeId, At(0), At(3600), 5000, null);
first.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
first.Error.StatusCode.ShouldBe(StatusCodes.Good);
first.Values.Count.ShouldBe(FirstPage);
first.Cp.ShouldNotBeNull();
// The budget the client asked for (+ lookahead + allowance, refused naming 1000), the ceiling re-read that cannot
@@ -114,7 +114,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
fake.RawCalls.Select(c => c.MaxValues).ShouldBe([5000 + 1 + RawAggregateReader.PreWindowAllowance, Ceiling, Ceiling]);
var (rest, _, status) = DrainFrom(nm, nodeId, At(0), At(3600), 5000, first.Cp);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
first.Values.Concat(rest).ShouldBe(PerSecond(3600).Select(s => s.Item2));
Refusals(fake).ShouldBe(1);
}
@@ -140,7 +140,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
? DrainFrom(nm, nodeId, At(3600), At(-1), n, null)
: DrainFrom(nm, nodeId, At(0), At(3600), n, null);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
var expected = stored.Select(s => s.Item2);
values.ShouldBe(reverse ? expected.Reverse() : expected);
pages[0].ShouldBe(FirstPage);
@@ -217,10 +217,10 @@ public sealed class GatewayCeilingPagingTests : IDisposable
var (values, pages, status) = DrainFrom(nm, nodeId, At(0), At(5000), n, null, maxPages: 60);
var expected = stored.Select(s => s.Item2).ToList();
if (status.Code == StatusCodes.Good) values.ShouldBe(expected);
if (status == StatusCodes.Good) values.ShouldBe(expected);
else
{
status.Code.ShouldBe(StatusCodes.BadTooManyOperations);
status.ShouldBe(StatusCodes.BadTooManyOperations);
values.ShouldBe(expected.Take(values.Count)); // an exact prefix
size.ShouldBeGreaterThan(L - 2, "a cluster that fits the over-fetch is always served");
}
@@ -254,7 +254,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
ds.MaxRawValuesPerRead.ShouldBe(0);
var (rest, pages, status) = DrainFrom(nm, nodeId, At(0), At(5000), 0, first.Cp, maxPages: 20);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
first.Values.Concat(rest).ShouldBe(stored.Select(s => s.Item2));
pages.Count.ShouldBeLessThan(20);
}
@@ -278,7 +278,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
fake.MaxValuesCeiling = 500; // the operator lowered it
fake.RawCalls.Clear();
var (rest, pages, status) = DrainFrom(nm, nodeId, At(0), At(3600), 5000, p2.Cp, maxPages: 20);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
got.AddRange(rest);
got.ShouldBe(stored.Select(s => s.Item2));
pages.ShouldAllBe(p => p <= 500 - RawAggregateReader.PreWindowAllowance);
@@ -302,7 +302,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
var start = At(10).AddMilliseconds(500);
var end = At(3000).AddMilliseconds(500);
var (values, pages, status) = DrainFrom(nm, nodeId, start, end, 5000, null, returnBounds: true);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
values.ShouldBe(Enumerable.Range(10, 2992).Select(i => (double)i));
pages.ShouldAllBe(p => p <= L);
Refusals(fake).ShouldBe(0);
@@ -325,7 +325,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
try
{
var r = ReadRaw(nm, nodeId, DateTime.MinValue, T0.AddMilliseconds(200_000), n, null);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
r.Error.StatusCode.ShouldBe(StatusCodes.Good);
r.Cp.ShouldNotBeNull();
var page = ceiling - RawAggregateReader.PreWindowAllowance - 1;
r.Values.ShouldBe(Enumerable.Range(200_000 - r.Values.Count, r.Values.Count).Reverse().Select(i => (double)i));
@@ -360,7 +360,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
try
{
var (values, pages, status) = DrainFrom(nm, nodeId, DateTime.UnixEpoch, DateTime.UnixEpoch.AddDays(1), 0, null);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
values.ShouldBe(stored.Select(s => s.Item2));
pages.Count.ShouldBeGreaterThan(1);
}
@@ -377,7 +377,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
{
ReadRaw(nm, nodeId, DateTime.UnixEpoch, DateTime.UnixEpoch.AddSeconds(5), 5000, null); // learns the ceiling
var r = ReadRaw(nm, nodeId, DateTime.MinValue, DateTime.UnixEpoch.AddDays(30), 10, null);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
r.Error.StatusCode.ShouldBe(StatusCodes.Good);
r.Values.ShouldBe(Enumerable.Range(2991, 10).Reverse().Select(i => (double)i)); // the LATEST ten, newest first
}
finally { await host.DisposeAsync(); }
@@ -425,7 +425,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
fake.RawCalls.Clear();
var (values, _, status) = DrainFrom(nm, nodeId, At(0), x.AddDays(1), n, null);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
values.ShouldBe(stored.Select(s => s.Item2));
Refusals(fake).ShouldBe(0);
EveryRequestFits(fake);
@@ -476,7 +476,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
try
{
var r = ReadRaw(nm, nodeId, At(0), At(10), 10, null);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
r.Error.StatusCode.ShouldBe(StatusCodes.BadNoCommunication);
}
finally { await host.DisposeAsync(); }
}
@@ -528,7 +528,7 @@ public sealed class GatewayCeilingPagingTests : IDisposable
private static (IReadOnlyList<double> Values, ServiceResult Error, byte[]? Cp) ReadRaw(
OtOpcUaNodeManager nm, NodeId nodeId, DateTime start, DateTime end, uint max, byte[]? inboundCp, bool returnBounds = false)
{
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
var details = new ReadRawModifiedDetails
{
ReturnBounds = returnBounds,
@@ -537,16 +537,19 @@ public sealed class GatewayCeilingPagingTests : IDisposable
EndTime = end,
NumValuesPerNode = max,
};
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = nodeId, ContinuationPoint = inboundCp } };
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = nodeId, ContinuationPoint = inboundCp is null ? default : ByteString.From(inboundCp) } };
var results = new List<SdkHistoryReadResult> { null! };
var errors = new List<ServiceResult> { null! };
nm.HistoryRead(context, details, TimestampsToReturn.Both, releaseContinuationPoints: false, nodesToRead, results, errors);
var values = new List<double>();
if (results[0]?.HistoryData is { } ho && !ExtensionObject.IsNull(ho))
foreach (var dv in ((HistoryData)ExtensionObject.ToEncodeable(ho)).DataValues)
values.Add(Convert.ToDouble(dv.Value));
return (values, errors[0], results[0]?.ContinuationPoint);
if (results[0]?.HistoryData is { IsNull: false } historyData)
{
var data = (HistoryData)ExtensionObject.ToEncodeable(historyData)!;
foreach (var dv in data.DataValues)
values.Add(Convert.ToDouble(SdkValueBoundary.ToClr(dv.WrappedValue)));
}
return (values, errors[0], results[0]?.ContinuationPoint is { IsNull: false } cp ? cp.ToArray() : null);
}
public void Dispose()
@@ -205,7 +205,7 @@ public sealed class GatewayRawCapTests : IDisposable
? DrainFrom(nm, nodeId, At(3600), At(-1), n, null)
: DrainFrom(nm, nodeId, At(0), At(3600), n, null);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
var expected = Enumerable.Range(0, 3600).Select(i => (double)i);
values.ShouldBe(reverse ? expected.Reverse() : expected);
pages.Count.ShouldBeGreaterThan(1, "a year-sized window at N = 0 is paged, not one shot");
@@ -224,7 +224,7 @@ public sealed class GatewayRawCapTests : IDisposable
try
{
var (values, pages, status) = DrainFrom(nm, nodeId, At(10).AddMilliseconds(500), At(3000).AddMilliseconds(500), 0, null, returnBounds: true);
status.Code.ShouldBe(StatusCodes.Good);
status.ShouldBe(StatusCodes.Good);
values.ShouldBe(Enumerable.Range(10, 2992).Select(i => (double)i)); // start bound +10, data +11..+3000, end bound +3001
pages.ShouldAllBe(p => p <= Cap);
fake.RawCalls.ShouldAllBe(c => c.MaxValues > 0 && c.MaxValues <= Cap + RawAggregateReader.PreWindowAllowance);
@@ -277,21 +277,24 @@ public sealed class GatewayRawCapTests : IDisposable
private static (IReadOnlyList<double> Values, ServiceResult Error, byte[]? Cp) ReadRaw(
OtOpcUaNodeManager nm, NodeId nodeId, DateTime start, DateTime end, uint max, byte[]? inboundCp, bool returnBounds = false)
{
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
var details = new ReadRawModifiedDetails
{
ReturnBounds = returnBounds, IsReadModified = false, StartTime = start, EndTime = end, NumValuesPerNode = max,
};
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = nodeId, ContinuationPoint = inboundCp } };
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = nodeId, ContinuationPoint = inboundCp is null ? default : ByteString.From(inboundCp) } };
var results = new List<SdkHistoryReadResult> { null! };
var errors = new List<ServiceResult> { null! };
nm.HistoryRead(context, details, TimestampsToReturn.Both, releaseContinuationPoints: false, nodesToRead, results, errors);
var values = new List<double>();
if (results[0]?.HistoryData is { } ho && !ExtensionObject.IsNull(ho))
foreach (var dv in ((HistoryData)ExtensionObject.ToEncodeable(ho)).DataValues)
values.Add(Convert.ToDouble(dv.Value));
return (values, errors[0], results[0]?.ContinuationPoint);
if (results[0]?.HistoryData is { IsNull: false } historyData)
{
var data = (HistoryData)ExtensionObject.ToEncodeable(historyData)!;
foreach (var dv in data.DataValues)
values.Add(Convert.ToDouble(SdkValueBoundary.ToClr(dv.WrappedValue)));
}
return (values, errors[0], results[0]?.ContinuationPoint is { IsNull: false } cp ? cp.ToArray() : null);
}
public void Dispose()
@@ -28,7 +28,6 @@ using LocalizedText = Opc.Ua.LocalizedText;
using Proto = ZB.MOM.WW.MxGateway.Contracts.Proto;
using UaNodeId = Opc.Ua.NodeId;
using Variant = Opc.Ua.Variant;
using VariantCollection = Opc.Ua.VariantCollection;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
@@ -505,9 +504,9 @@ public sealed class NativeAlarmRestatementEndToEndTests : RuntimeActorTestBase
}
/// <summary>An upstream A&amp;C condition event, in the driver's select-clause order.</summary>
private static VariantCollection ConditionFields(bool active, bool acked) =>
private static Variant[] ConditionFields(bool active, bool acked) =>
[
new Variant(new byte[] { 1 }), // EventId
new Variant(Opc.Ua.ByteString.From(new byte[] { 1 })), // EventId
new Variant(new UaNodeId("TripAlarmType", 3)), // EventType
new Variant(UaNodeId.Parse(UpstreamSource)), // SourceNode
new Variant(new LocalizedText("tripped")), // Message
@@ -35,7 +35,7 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
port => new OpcUaApplicationHost(
BuildOptions("PrimaryLeader", pkiRoot, port),
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
() => new StandardServer(Opc.Ua.DefaultTelemetry.Create(_ => { })), Ct);
await using var host = started.Host;
var server = started.Server;
@@ -58,7 +58,7 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
CorrelationId: CorrelationId.NewId()));
AwaitAssertion(() =>
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)240));
server.CurrentInstance.ServerObject.ServiceLevel.ShouldNotBeNull().Value.ShouldBe((byte)240));
}
finally
{
@@ -77,7 +77,7 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
port => new OpcUaApplicationHost(
BuildOptions("Secondary", pkiRoot, port),
NullLogger<OpcUaApplicationHost>.Instance),
() => new StandardServer(), Ct);
() => new StandardServer(Opc.Ua.DefaultTelemetry.Create(_ => { })), Ct);
await using var host = started.Host;
var server = started.Server;
@@ -100,7 +100,7 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
CorrelationId: CorrelationId.NewId()));
AwaitAssertion(() =>
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)100));
server.CurrentInstance.ServerObject.ServiceLevel.ShouldNotBeNull().Value.ShouldBe((byte)100));
}
finally
{