test: preserve server SDK 2.0 value security and history contracts

This commit is contained in:
Joseph Doherty
2026-11-10 10:37:52 -05:00
parent ac5e9d84f0
commit 9cddc9306b
45 changed files with 1215 additions and 641 deletions
@@ -29,7 +29,7 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
private static DataValueSnapshot[] EightValues() =>
Enumerable.Range(1, 8).Select(i => Sample(i, T0.AddSeconds(i))).ToArray();
private static DataValueSnapshot Sample(double v, DateTime t) => new(v, StatusCodes.Good, t, t);
private static DataValueSnapshot Sample(double v, DateTime t) => new(v, StatusCodes.Good.Code, t, t);
private static (DateTime Start, DateTime End) Window(Direction direction) => direction switch
{
@@ -193,7 +193,7 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
{
source.ResetCounts();
var (values, error, next) = await ReadAsync(source, start, end, 1, bounds: false, cp);
error.StatusCode.Code.ShouldBe(StatusCodes.Good);
error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
values.Count.ShouldBe(1);
all.AddRange(values);
readsPerPage.Add(source.Reads);
@@ -202,7 +202,7 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
}
while (cp is not null);
all.Select(x => (double)x.Value).ShouldBe([0d, 1d, 2d, 3d, 4d, 5d, 6d]);
all.Select(x => (double)SdkValueBoundary.ToClr(x.WrappedValue)!).ShouldBe([0d, 1d, 2d, 3d, 4d, 5d, 6d]);
// Page 1 [a], page 2 [t1]: one read each. Page 3 [t2]: the read stalls on the cluster, then the over-fetch (2).
// Pages 4 [t3], 5 [t4], 6 [t5]: the flagged cursor over-fetches directly (1 each). Page 7 [b]: one read.
readsPerPage.ShouldBe([1, 1, 2, 1, 1, 1, 1]);
@@ -227,12 +227,12 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
var (start, end) = direction == Direction.Forward ? (T0.AddSeconds(5), T0.AddSeconds(7)) : (T0.AddSeconds(7), T0.AddSeconds(5));
var (first, _, cp) = await ReadAsync(source, start, end, 1, bounds: true, cp: null);
first.Single().StatusCode.Code.ShouldBe(StatusCodes.BadBoundNotFound);
first.Single().StatusCode.Code.ShouldBe(StatusCodes.BadBoundNotFound.Code);
cp.ShouldNotBeNull();
source.ResetCounts();
var (second, _, cp2) = await ReadAsync(source, start, end, 1, bounds: true, cp: cp);
second.Single().Value.ShouldBe(direction == Direction.Forward ? 9d : 3d);
SdkValueBoundary.ToClr(second.Single().WrappedValue)!.ShouldBe(direction == Direction.Forward ? 9d : 3d);
cp2.ShouldBeNull();
source.Reads.ShouldBe(0, "the end bound was found by the first page and carried");
}
@@ -253,7 +253,7 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
fx.Nm.HistorianDataSource = source;
// Reverse (0.1s ← 30s]: no sample at or after 30s ⇒ the start bound is a placeholder; the data is below.
var (values, _, cp) = Read(T0.AddSeconds(30), T0.AddMilliseconds(100), 1, bounds: true, cp: null);
values.Single().StatusCode.Code.ShouldBe(StatusCodes.BadBoundNotFound);
values.Single().StatusCode.Code.ShouldBe(StatusCodes.BadBoundNotFound.Code);
cp.ShouldNotBeNull();
store.Saved.Last().WalkHint.WidthTicks.ShouldBeGreaterThan(0);
}
@@ -272,7 +272,7 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
[Fact]
public async Task A_stored_row_with_the_bound_not_found_status_is_data_not_a_placeholder()
{
var stored = new DataValueSnapshot(null, StatusCodes.BadBoundNotFound, T0.AddSeconds(6), T0.AddSeconds(6));
var stored = new DataValueSnapshot(null, StatusCodes.BadBoundNotFound.Code, T0.AddSeconds(6), T0.AddSeconds(6));
HistoryBounds.IsPlaceholder(stored).ShouldBeFalse();
HistoryBounds.IsPlaceholder(HistoryBounds.NotFound(T0)).ShouldBeTrue();
@@ -285,12 +285,12 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
// --- helpers --------------------------------------------------------------------------------
private static string Render(IEnumerable<DataValue> values) =>
string.Join(" | ", values.Select(v => $"{v.SourceTimestamp:HH:mm:ss.fffffff}={v.Value ?? "null"}/{v.StatusCode.Code:X8}"));
string.Join(" | ", values.Select(v => $"{v.SourceTimestamp:HH:mm:ss.fffffff}={SdkValueBoundary.ToClr(v.WrappedValue)! ?? "null"}/{v.StatusCode.Code:X8}"));
private async Task<List<DataValue>> SinglePageAsync(CountingSource source, DateTime start, DateTime end, bool bounds)
{
var (values, error, cp) = await ReadAsync(source, start, end, 10_000, bounds, cp: null);
error.StatusCode.Code.ShouldBe(StatusCodes.Good);
error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
cp.ShouldBeNull();
return values.ToList();
}
@@ -303,7 +303,7 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
do
{
var (values, error, next) = await ReadAsync(source, start, end, max, bounds, cp);
error.StatusCode.Code.ShouldBe(StatusCodes.Good);
error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
pages.Add(values);
cp = next;
pages.Count.ShouldBeLessThan(80, "paging must terminate");
@@ -328,20 +328,20 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
private (IReadOnlyList<DataValue> Values, ServiceResult Error, byte[]? Cp) Read(
DateTime start, DateTime end, uint max, bool bounds, byte[]? cp)
{
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
{
StartTime = start, EndTime = end, NumValuesPerNode = max, ReturnBounds = bounds, IsReadModified = false,
};
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = fx.NodeId, ContinuationPoint = cp } };
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = fx.NodeId, ContinuationPoint = ByteString.From(cp) } };
var results = new List<SdkHistoryReadResult> { null! };
var errors = new List<ServiceResult> { null! };
fx.Nm.HistoryRead(context, details, TimestampsToReturn.Both, releaseContinuationPoints: false, nodesToRead, results, errors);
var values = new List<DataValue>();
if (results[0]?.HistoryData is { } ho && !ExtensionObject.IsNull(ho))
values.AddRange(((HistoryData)ExtensionObject.ToEncodeable(ho)).DataValues);
return (values, errors[0], results[0]?.ContinuationPoint);
if (results[0]?.HistoryData is { } ho && !ho.IsNull)
values.AddRange(((HistoryData)ExtensionObject.ToEncodeable(ho)!).DataValues);
return (values, errors[0], results[0]?.ContinuationPoint.ToNullableArray());
}
private static CountingSource Source(Shape shape, IReadOnlyList<DataValueSnapshot> series) => shape switch