test: preserve server SDK 2.0 value security and history contracts
This commit is contained in:
@@ -46,7 +46,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var src = DateTime.UtcNow.AddSeconds(-5);
|
||||
var srv = DateTime.UtcNow;
|
||||
fake.RawResult = new HistorianRead(
|
||||
new[] { new DataValueSnapshot(42.5f, StatusCodes.Good, src, srv) }, null);
|
||||
new[] { new DataValueSnapshot(42.5f, StatusCodes.Good.Code, src, srv) }, null);
|
||||
|
||||
var start = DateTime.UtcNow.AddHours(-1);
|
||||
var end = DateTime.UtcNow;
|
||||
@@ -68,13 +68,13 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
fake.LastEnd.ShouldBe(end);
|
||||
fake.LastMaxValues.ShouldBe(101u, "the page plus one lookahead row (#692) — it tells the server whether more follows");
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
var data = (HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
var data = (HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!;
|
||||
data.DataValues.Count.ShouldBe(1);
|
||||
var dv = data.DataValues[0];
|
||||
dv.Value.ShouldBe(42.5f);
|
||||
dv.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
SdkValueBoundary.ToClr(dv.WrappedValue)!.ShouldBe(42.5f);
|
||||
dv.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
dv.SourceTimestamp.ShouldBe(src);
|
||||
dv.ServerTimestamp.ShouldBe(srv);
|
||||
|
||||
@@ -140,10 +140,10 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
|
||||
var (results, errors) = InvokeHistoryRead(server, nm, details, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.GoodNoData);
|
||||
var data = (HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData);
|
||||
data.DataValues.ShouldBeEmpty();
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.GoodNoData.Code);
|
||||
var data = (HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!;
|
||||
data.DataValues.Count.ShouldBe(0);
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -174,7 +174,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, details, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
fake.LastCall.ShouldBeNull(); // source never reached
|
||||
|
||||
await host.DisposeAsync();
|
||||
@@ -208,14 +208,14 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
|
||||
var (results, errors) = InvokeHistoryRead(server, nm, details, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
fake.LastCall.ShouldBeNull(); // the raw read is never served in place of a modified one
|
||||
|
||||
// The Null default (no historian configured) answers the same.
|
||||
nm.HistorianDataSource = NullHistorianDataSource.Instance;
|
||||
InvokeHistoryRead(server, nm, details, nodeId).Item2[0].StatusCode.Code
|
||||
.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -239,7 +239,8 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
StartTime = DateTime.UtcNow.AddHours(-1),
|
||||
EndTime = DateTime.UtcNow,
|
||||
ProcessingInterval = 10_000.0, // ms
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_Average },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_Average },
|
||||
};
|
||||
|
||||
InvokeHistoryRead(server, nm, details, nodeId);
|
||||
@@ -272,12 +273,13 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
EndTime = DateTime.UtcNow,
|
||||
ProcessingInterval = 10_000.0,
|
||||
// StandardDeviationSample is a real OPC UA aggregate we do not serve.
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_StandardDeviationSample },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_StandardDeviationSample },
|
||||
};
|
||||
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, details, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadAggregateNotSupported);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadAggregateNotSupported.Code);
|
||||
fake.LastCall.ShouldBeNull();
|
||||
|
||||
await host.DisposeAsync();
|
||||
@@ -310,12 +312,13 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
StartTime = DateTime.UtcNow.AddHours(-1),
|
||||
EndTime = DateTime.UtcNow,
|
||||
ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { aggregate },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { aggregate },
|
||||
};
|
||||
|
||||
// Average is mapped by the server but not in this backend's set ⇒ refused, no call.
|
||||
InvokeHistoryRead(server, nm, Details(ObjectIds.AggregateFunction_Average), nodeId).Item2[0].StatusCode.Code
|
||||
.ShouldBe(StatusCodes.BadAggregateNotSupported);
|
||||
.ShouldBe(StatusCodes.BadAggregateNotSupported.Code);
|
||||
fake.LastCall.ShouldBeNull();
|
||||
|
||||
// Positive control: a member of the set reaches the backend.
|
||||
@@ -326,7 +329,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
// No historian: the Null default serves no aggregate.
|
||||
nm.HistorianDataSource = NullHistorianDataSource.Instance;
|
||||
InvokeHistoryRead(server, nm, Details(ObjectIds.AggregateFunction_TimeAverage), nodeId).Item2[0].StatusCode.Code
|
||||
.ShouldBe(StatusCodes.BadAggregateNotSupported);
|
||||
.ShouldBe(StatusCodes.BadAggregateNotSupported.Code);
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -354,13 +357,14 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
StartTime = DateTime.UtcNow.AddHours(-1),
|
||||
EndTime = DateTime.UtcNow,
|
||||
ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_StandardDeviationPopulation },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_StandardDeviationPopulation },
|
||||
}, nodeId);
|
||||
StatusCode.IsBad(errors[0].StatusCode).ShouldBeFalse($"got {errors[0].StatusCode}");
|
||||
fake.LastCall.ShouldBe("Processed");
|
||||
fake.LastAggregate.ShouldBe(HistoryAggregateType.StandardDeviationPopulation);
|
||||
|
||||
var diagnostics = (CustomNodeManager2)nm.Server.DiagnosticsNodeManager;
|
||||
var diagnostics = (AsyncCustomNodeManager)nm.Server.DiagnosticsNodeManager;
|
||||
var folder = diagnostics.FindPredefinedNode<NodeState>(ObjectIds.HistoryServerCapabilities_AggregateFunctions)!;
|
||||
AggregateFunctionMap.ByNodeId.Keys.Count(id => folder.ReferenceExists(ReferenceTypeIds.Organizes, false, id))
|
||||
.ShouldBe(AggregateFunctionMap.ByNodeId.Count);
|
||||
@@ -397,7 +401,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, new ReadProcessedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_TimeAverage },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_TimeAverage },
|
||||
AggregateConfiguration = new AggregateConfiguration
|
||||
{
|
||||
UseServerCapabilitiesDefaults = false, TreatUncertainAsBad = treatUncertainAsBad,
|
||||
@@ -405,7 +409,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
},
|
||||
}, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadAggregateConfigurationRejected);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadAggregateConfigurationRejected.Code);
|
||||
fake.LastCall.ShouldBeNull();
|
||||
|
||||
await host.DisposeAsync();
|
||||
@@ -432,7 +436,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, new ReadProcessedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_TimeAverage },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_TimeAverage },
|
||||
AggregateConfiguration = new AggregateConfiguration
|
||||
{
|
||||
UseServerCapabilitiesDefaults = useServerDefaults, TreatUncertainAsBad = treatUncertainAsBad,
|
||||
@@ -455,7 +459,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
[Fact]
|
||||
public void The_server_aggregate_defaults_deliberately_diverge_from_the_SDK_on_TreatUncertainAsBad()
|
||||
{
|
||||
var sdk = new Opc.Ua.Server.AggregateManager(null!).GetDefaultConfiguration(null);
|
||||
var sdk = new Opc.Ua.Server.AggregateManager(null!).GetDefaultConfiguration(NodeId.Null);
|
||||
var ours = ServerAggregateConfiguration.Defaults;
|
||||
ours.TreatUncertainAsBad.ShouldBeFalse();
|
||||
ours.PercentDataBad.ShouldBe((byte)100);
|
||||
@@ -476,7 +480,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
public async Task The_booted_SDK_AggregateManager_default_is_our_TreatUncertainAsBad_false()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var cfg = server.CurrentInstance.AggregateManager.GetDefaultConfiguration(null);
|
||||
var cfg = server.CurrentInstance.AggregateManager.GetDefaultConfiguration(NodeId.Null);
|
||||
cfg.TreatUncertainAsBad.ShouldBeFalse();
|
||||
cfg.PercentDataBad.ShouldBe(ServerAggregateConfiguration.PercentDataBad);
|
||||
cfg.PercentDataGood.ShouldBe(ServerAggregateConfiguration.PercentDataGood);
|
||||
@@ -500,7 +504,8 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
InvokeHistoryRead(server, nm, new ReadProcessedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_Interpolative },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_Interpolative },
|
||||
}, nodeId);
|
||||
|
||||
fake.LastCall.ShouldBe("Processed");
|
||||
@@ -528,7 +533,8 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
InvokeHistoryRead(server, nm, new ReadProcessedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { aggregateId },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { aggregateId },
|
||||
}, nodeId);
|
||||
|
||||
fake.LastCall.ShouldBe("Processed");
|
||||
@@ -551,10 +557,11 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, new ReadProcessedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_PercentGood },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_PercentGood },
|
||||
}, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadAggregateNotSupported);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadAggregateNotSupported.Code);
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -573,10 +580,11 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, new ReadProcessedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, ProcessingInterval = 10_000.0,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_Total },
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_Total },
|
||||
}, nodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadTooManyOperations);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadTooManyOperations.Code);
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -597,7 +605,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var t2 = DateTime.UtcNow.AddMinutes(-1);
|
||||
var details = new ReadAtTimeDetails
|
||||
{
|
||||
ReqTimes = new DateTimeCollection { t1, t2 },
|
||||
ReqTimes = new List<DateTimeUtc> { t1, t2 },
|
||||
};
|
||||
|
||||
InvokeHistoryRead(server, nm, details, nodeId);
|
||||
@@ -638,7 +646,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var nodeIds = paths.Select((p, i) => nm.TryGetVariable(p, realms[i])!.NodeId).ToArray();
|
||||
|
||||
var (_, errors) = InvokeHistoryRead(server, nm,
|
||||
new ReadAtTimeDetails { ReqTimes = new DateTimeCollection { DateTime.UtcNow.AddMinutes(-1) } }, nodeIds);
|
||||
new ReadAtTimeDetails { ReqTimes = new List<DateTimeUtc> { DateTime.UtcNow.AddMinutes(-1) } }, nodeIds);
|
||||
|
||||
errors.ShouldAllBe(e => StatusCode.IsGood(e.StatusCode));
|
||||
fake.Hints["R.Flag"].ShouldBe(TypeCode.Boolean);
|
||||
@@ -707,7 +715,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
// Per-tagname fake: A.PV returns one sample; B.PV throws.
|
||||
var perTagFake = new PerTagnameHistorianDataSource(tagname =>
|
||||
tagname == "A.PV"
|
||||
? Task.FromResult(new HistorianRead(new[] { new DataValueSnapshot(1.0f, StatusCodes.Good, src, srv) }, null))
|
||||
? Task.FromResult(new HistorianRead(new[] { new DataValueSnapshot(1.0f, StatusCodes.Good.Code, src, srv) }, null))
|
||||
: throw new InvalidOperationException("backend boom for B.PV"));
|
||||
nm.HistorianDataSource = perTagFake;
|
||||
|
||||
@@ -725,20 +733,20 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var (results, errors) = InvokeHistoryRead(server, nm, details, goodNodeId, badNodeId);
|
||||
|
||||
// Good node (index 0): should have a Good status + one sample.
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
var data = (HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
var data = (HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!;
|
||||
data.DataValues.Count.ShouldBe(1);
|
||||
data.DataValues[0].Value.ShouldBe(1.0f);
|
||||
SdkValueBoundary.ToClr(data.DataValues[0].WrappedValue)!.ShouldBe(1.0f);
|
||||
|
||||
// Bad node (index 1): errors slot must be Bad; results slot must NOT carry any HistoryData
|
||||
// (no data was projected into it). The base may pre-seed results[i] to a default-constructed
|
||||
// SdkHistoryReadResult whose StatusCode is 0 (Good) — that's fine for the result field, but the
|
||||
// distinguishing signal is the error slot AND the absence of HistoryData from the good node.
|
||||
StatusCode.IsBad(errors[1].StatusCode).ShouldBeTrue();
|
||||
errors[1].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
errors[1].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
// HistoryData must be null/empty — the good node's sample MUST NOT have leaked into slot 1.
|
||||
(results[1].HistoryData == null || ExtensionObject.IsNull(results[1].HistoryData)).ShouldBeTrue();
|
||||
results[1].HistoryData.IsNull.ShouldBeTrue();
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -770,13 +778,47 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var (results, errors) = InvokeHistoryRead(server, nm, details, nodeId);
|
||||
|
||||
StatusCode.IsBad(errors[0].StatusCode).ShouldBeTrue();
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
// The result slot was never filled with a GoodNoData success.
|
||||
(results[0].StatusCode.Code == StatusCodes.GoodNoData).ShouldBeFalse();
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Canceling_the_SDK_request_cancels_an_inflight_backend_read()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
await using var ownedHost = host;
|
||||
var nm = server.NodeManager!;
|
||||
nm.HistoryReadDeadline = TimeSpan.Zero;
|
||||
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource
|
||||
{
|
||||
RawRead = async ct =>
|
||||
{
|
||||
entered.SetResult();
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, ct);
|
||||
return Empty();
|
||||
},
|
||||
};
|
||||
nm.EnsureVariable("cancel", null, "Cancel", "Double", false, AddressSpaceRealm.Raw, "cancel.tag");
|
||||
var node = nm.TryGetVariable("cancel", AddressSpaceRealm.Raw)!.NodeId;
|
||||
using var lifetime = new RequestLifetime();
|
||||
using var context = new OperationContext(new RequestHeader(), null, RequestType.HistoryRead, lifetime);
|
||||
var results = new List<SdkHistoryReadResult> { null! };
|
||||
var errors = new List<ServiceResult> { null! };
|
||||
var read = Task.Run(() => nm.HistoryRead(context, new ReadRawModifiedDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow, NumValuesPerNode = 10,
|
||||
}, TimestampsToReturn.Both, false, [new HistoryReadValueId { NodeId = node }], results, errors), Ct);
|
||||
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), Ct);
|
||||
lifetime.TryCancel(StatusCodes.BadRequestCancelledByClient).ShouldBeTrue();
|
||||
await read.WaitAsync(TimeSpan.FromSeconds(5), Ct);
|
||||
StatusCode.IsBad(errors[0].StatusCode).ShouldBeTrue();
|
||||
results[0].ContinuationPoint.IsEmpty.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Invoke the node manager's public HistoryRead with a single node, returning the filled
|
||||
/// results + errors. Uses a session-less <see cref="OperationContext"/> (the
|
||||
/// (RequestHeader, SecureChannelContext, RequestType, IUserIdentity) ctor) — HistoryRead's
|
||||
@@ -791,7 +833,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
OtOpcUaSdkServer server, OtOpcUaNodeManager nm, HistoryReadDetails details, params NodeId[] nodeIds)
|
||||
{
|
||||
var context = new OperationContext(
|
||||
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
|
||||
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
|
||||
|
||||
var nodesToRead = nodeIds.Select(id => new HistoryReadValueId { NodeId = id }).ToList();
|
||||
var results = Enumerable.Repeat<SdkHistoryReadResult>(null!, nodeIds.Length).ToList();
|
||||
@@ -825,13 +867,14 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
HistoryReadDetails details = kind switch
|
||||
{
|
||||
"Raw" => new ReadRawModifiedDetails { IsReadModified = false, ReturnBounds = false, StartTime = t, EndTime = t.AddSeconds(10) },
|
||||
"AtTime" => new ReadAtTimeDetails { ReqTimes = new DateTimeCollection { t } },
|
||||
"AtTime" => new ReadAtTimeDetails { ReqTimes = new List<DateTimeUtc> { t } },
|
||||
_ => new ReadProcessedDetails { StartTime = t, EndTime = t.AddSeconds(10), ProcessingInterval = 1000,
|
||||
AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_Average } }
|
||||
AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_Average } }
|
||||
};
|
||||
var (results, errors) = InvokeHistoryRead(server, nm, details, nm.TryGetVariable("eq/comm")!.NodeId);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication.Code);
|
||||
results[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication.Code);
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -850,20 +893,20 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
{
|
||||
var nm = server.NodeManager!;
|
||||
var t = DateTime.UtcNow.AddMinutes(-1);
|
||||
var value = new HistorianRead([new DataValueSnapshot(1d, StatusCodes.Good, t, t)], null);
|
||||
var value = new HistorianRead([new DataValueSnapshot(1d, StatusCodes.Good.Code, t, t)], null);
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource { RawResult = value, AtTimeResult = value };
|
||||
nm.EnsureVariable("eq/typed", null, "Typed", dataType, false, historianTagname: "T", realm: AddressSpaceRealm.Uns);
|
||||
HistoryReadDetails details = kind == "Raw"
|
||||
? new ReadRawModifiedDetails { IsReadModified = false, ReturnBounds = false, StartTime = t, EndTime = t.AddSeconds(10) }
|
||||
: new ReadAtTimeDetails { ReqTimes = new DateTimeCollection { t } };
|
||||
: new ReadAtTimeDetails { ReqTimes = new List<DateTimeUtc> { t } };
|
||||
var (results, errors) = InvokeHistoryRead(server, nm, details, nm.TryGetVariable("eq/typed")!.NodeId);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
var actual = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)).DataValues.Single();
|
||||
if (dataType == "Int32") actual.Value.ShouldBeOfType<int>().ShouldBe(1);
|
||||
if (dataType == "Boolean") actual.Value.ShouldBeOfType<bool>().ShouldBeTrue();
|
||||
if (dataType == "Float") actual.Value.ShouldBeOfType<float>().ShouldBe(1f);
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
var actual = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!).DataValues.ToArray()!.Single();
|
||||
if (dataType == "Int32") SdkValueBoundary.ToClr(actual.WrappedValue)!.ShouldBeOfType<int>().ShouldBe(1);
|
||||
if (dataType == "Boolean") SdkValueBoundary.ToClr(actual.WrappedValue)!.ShouldBeOfType<bool>().ShouldBeTrue();
|
||||
if (dataType == "Float") SdkValueBoundary.ToClr(actual.WrappedValue)!.ShouldBeOfType<float>().ShouldBe(1f);
|
||||
actual.SourceTimestamp.ShouldBe(t);
|
||||
actual.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
actual.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -879,12 +922,12 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
var t = DateTime.UtcNow.AddMinutes(-1);
|
||||
nm.EnsureVariable("eq/mismatch", null, "Mismatch", type, false, historianTagname: "T", realm: AddressSpaceRealm.Uns);
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource { RawResult = new HistorianRead([new(input, StatusCodes.Good, t, t)], null) };
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource { RawResult = new HistorianRead([new(input, StatusCodes.Good.Code, t, t)], null) };
|
||||
var (results, _) = InvokeHistoryRead(server, nm, new ReadRawModifiedDetails { IsReadModified = false, ReturnBounds = false,
|
||||
StartTime = t, EndTime = t.AddSeconds(10) }, nm.TryGetVariable("eq/mismatch")!.NodeId);
|
||||
var value = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)).DataValues.Single();
|
||||
value.Value.ShouldBeNull();
|
||||
value.StatusCode.Code.ShouldBe(StatusCodes.BadTypeMismatch);
|
||||
var value = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!).DataValues.ToArray()!.Single();
|
||||
SdkValueBoundary.ToClr(value.WrappedValue)!.ShouldBeNull();
|
||||
value.StatusCode.Code.ShouldBe(StatusCodes.BadTypeMismatch.Code);
|
||||
value.SourceTimestamp.ShouldBe(t);
|
||||
value.ServerTimestamp.ShouldBe(t);
|
||||
}
|
||||
@@ -902,17 +945,17 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var serverTime = t.AddSeconds(1);
|
||||
nm.EnsureVariable("eq/array", null, "Array", "Int32", false, historianTagname: "T", realm: AddressSpaceRealm.Raw, isArray: true, arrayLength: 2);
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource { RawResult = new HistorianRead([
|
||||
new(new int[] { 1, 2 }, StatusCodes.Uncertain, t, serverTime),
|
||||
new(null, StatusCodes.BadNoCommunication, t.AddSeconds(1), serverTime)], null) };
|
||||
new(new int[] { 1, 2 }, StatusCodes.Uncertain.Code, t, serverTime),
|
||||
new(null, StatusCodes.BadNoCommunication.Code, t.AddSeconds(1), serverTime)], null) };
|
||||
var (results, _) = InvokeHistoryRead(server, nm, new ReadRawModifiedDetails { IsReadModified = false, ReturnBounds = false,
|
||||
StartTime = t, EndTime = t.AddSeconds(10) }, nm.TryGetVariable("eq/array", AddressSpaceRealm.Raw)!.NodeId);
|
||||
var values = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)).DataValues;
|
||||
values[0].Value.ShouldBeOfType<int[]>().ShouldBe([1, 2]);
|
||||
values[0].StatusCode.Code.ShouldBe(StatusCodes.Uncertain);
|
||||
var values = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!).DataValues;
|
||||
SdkValueBoundary.ToClr(values[0].WrappedValue)!.ShouldBeOfType<int[]>().ShouldBe([1, 2]);
|
||||
values[0].StatusCode.Code.ShouldBe(StatusCodes.Uncertain.Code);
|
||||
values[0].SourceTimestamp.ShouldBe(t);
|
||||
values[0].ServerTimestamp.ShouldBe(serverTime);
|
||||
values[1].Value.ShouldBeNull();
|
||||
values[1].StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
|
||||
SdkValueBoundary.ToClr(values[1].WrappedValue)!.ShouldBeNull();
|
||||
values[1].StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication.Code);
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -926,11 +969,12 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
var t = DateTime.UtcNow.AddMinutes(-1);
|
||||
nm.EnsureVariable("eq/count", null, "Count", "Boolean", false, historianTagname: "T", realm: AddressSpaceRealm.Uns);
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource { ProcessedResult = new HistorianRead([new(3d, StatusCodes.Good, t, t)], null) };
|
||||
nm.HistorianDataSource = new RecordingHistorianDataSource { ProcessedResult = new HistorianRead([new(3d, StatusCodes.Good.Code, t, t)], null) };
|
||||
var (results, _) = InvokeHistoryRead(server, nm, new ReadProcessedDetails { StartTime = t, EndTime = t.AddSeconds(10),
|
||||
ProcessingInterval = 1000, AggregateType = new NodeIdCollection { ObjectIds.AggregateFunction_Count } }, nm.TryGetVariable("eq/count")!.NodeId);
|
||||
var value = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)).DataValues.Single();
|
||||
value.Value.ShouldBeOfType<double>().ShouldBe(3d);
|
||||
ProcessingInterval = 1000, AggregateConfiguration = new AggregateConfiguration { UseServerCapabilitiesDefaults = true },
|
||||
AggregateType = new List<NodeId> { ObjectIds.AggregateFunction_Count } }, nm.TryGetVariable("eq/count")!.NodeId);
|
||||
var value = ((HistoryData)ExtensionObject.ToEncodeable(results[0].HistoryData)!).DataValues.ToArray()!.Single();
|
||||
SdkValueBoundary.ToClr(value.WrappedValue)!.ShouldBeOfType<double>().ShouldBe(3d);
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -941,6 +985,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
/// a configured result (or throws when <see cref="ThrowOnRead"/> is set).</summary>
|
||||
private sealed class RecordingHistorianDataSource : IHistorianDataSource
|
||||
{
|
||||
public Func<CancellationToken, Task<HistorianRead>>? RawRead { get; init; }
|
||||
public bool ThrowOnRead { get; init; }
|
||||
public Exception? ReadException { get; init; }
|
||||
public bool AggregateNotSupported { get; init; }
|
||||
@@ -971,7 +1016,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
LastStart = startUtc;
|
||||
LastEnd = endUtc;
|
||||
LastMaxValues = maxValuesPerNode;
|
||||
return Task.FromResult(RawResult);
|
||||
return RawRead?.Invoke(cancellationToken) ?? Task.FromResult(RawResult);
|
||||
}
|
||||
|
||||
public Task<HistorianRead> ReadProcessedAsync(
|
||||
|
||||
Reference in New Issue
Block a user