fix(opcua): raw open-ended/end-only reads per Part 11; extreme-bound conformance (#669)
This commit is contained in:
@@ -40,6 +40,22 @@ public abstract class RawReadConformance : ConformanceBase
|
||||
Values(await B.CreateDataSource().ReadRawAsync("A", At(-1), At(100), uint.MaxValue, Ct)).Length.ShouldBe(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #669: the node manager now pages an open-ended read (Part 11: only a StartTime / only an EndTime) with
|
||||
/// <c>DateTime.MaxValue</c> / <c>DateTime.MinValue</c> as the missing bound, so every backend must bind both
|
||||
/// extremes without overflow, ±infinity or a cast error.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Extreme_bounds_MaxValue_and_MinValue_are_safe()
|
||||
{
|
||||
await ConformanceData.SeedAAsync(B);
|
||||
var max = new DateTime(DateTime.MaxValue.Ticks, DateTimeKind.Utc);
|
||||
var min = new DateTime(0, DateTimeKind.Utc);
|
||||
Values(await B.CreateDataSource().ReadRawAsync("A", At(10), max, 2, Ct)).ShouldBe([20d, 30d]);
|
||||
Values(await B.CreateDataSource().ReadRawAsync("A", At(100), min, 2, Ct)).ShouldBe([50d, 40d]);
|
||||
Values(await B.CreateDataSource().ReadRawAsync("A", At(10), max, 0, Ct)).ShouldBe([20d, 30d, 40d, 50d]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reverse_read_is_descending_over_end_exclusive_start_inclusive()
|
||||
{
|
||||
|
||||
@@ -73,6 +73,65 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>Part 11: only a StartTime (EndTime unset = MinValue) with a cap pages FORWARD to an open upper bound —
|
||||
/// it must not be mistaken for a reverse read, and the stored cursor must not carry MinValue as its end.</summary>
|
||||
[Fact]
|
||||
public async Task Raw_open_ended_forward_read_pages_ascending_exactly_once()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.HistoryContinuationStore = new InMemoryHistoryContinuationStore();
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(MakeSeries(count: 250, stepSeconds: 1));
|
||||
nm.EnsureVariable("eq-1/open", parentFolderNodeId: null, displayName: "Open", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.Open", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/open")!.NodeId;
|
||||
|
||||
var collected = new List<double>();
|
||||
byte[]? cp = null;
|
||||
var pages = 0;
|
||||
do
|
||||
{
|
||||
var (values, error, next) = ReadRaw(nm, nodeId, Epoch, DateTime.MinValue, max: 100, inboundCp: cp);
|
||||
error.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
collected.AddRange(values);
|
||||
cp = next;
|
||||
(++pages).ShouldBeLessThan(20);
|
||||
}
|
||||
while (cp is not null);
|
||||
|
||||
collected.ShouldBe(Enumerable.Range(0, 250).Select(i => (double)i));
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>Part 11: only an EndTime (StartTime unset = MinValue) with a cap reads BACKWARD from the EndTime.</summary>
|
||||
[Fact]
|
||||
public async Task Raw_end_only_read_pages_descending_from_the_end_time()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.HistoryContinuationStore = new InMemoryHistoryContinuationStore();
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(MakeSeries(count: 250, stepSeconds: 1));
|
||||
nm.EnsureVariable("eq-1/endonly", parentFolderNodeId: null, displayName: "EndOnly", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.EndOnly", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/endonly")!.NodeId;
|
||||
|
||||
var collected = new List<double>();
|
||||
byte[]? cp = null;
|
||||
var pages = 0;
|
||||
do
|
||||
{
|
||||
var (values, error, next) = ReadRaw(nm, nodeId, DateTime.MinValue, Epoch.AddHours(1), max: 100, inboundCp: cp);
|
||||
error.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
collected.AddRange(values);
|
||||
cp = next;
|
||||
(++pages).ShouldBeLessThan(20);
|
||||
}
|
||||
while (cp is not null);
|
||||
|
||||
collected.ShouldBe(Enumerable.Range(0, 250).Reverse().Select(i => (double)i));
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>An exactly-full series whose total is a multiple of the cap pages to a trailing
|
||||
/// EMPTY page (GoodNoData, no CP) — the server can't know the prior page was the last until it reads
|
||||
/// past the end.</summary>
|
||||
|
||||
Reference in New Issue
Block a user