7e8941fbdc
- NodeManagerHistoryReadRawCapFuzzTests: 10k seeds (env OTOPCUA_PAGING_FUZZ_ITERATIONS)
through a real SDK node manager against wire-faithful fakes of SqlServer / QuestDB /
Gateway that enforce the backend half of the cap; random series with ties, random
windows (empty, on/past a tie, between samples, forward/reverse/end-only, bounds),
N in {0,1,2,small,huge}, cap in {off,3..1000}, tie budget {3,8,65536}. Pages must
concatenate to the cap-off one-shot answer (checked against the series), terminate
within values + 2 (+ one empty step per oversized boundary cluster), never exceed
the cap, never ask the backend for 0 / over-cap; refusals only past the budget.
- HistoryPaging.AdvertiseMaxReturnDataValues writes HistoryServerCapabilities/
MaxReturnDataValues (i=11273), read back through the SDK master node manager.
- MinMaxRawValuesPerRead raised to 3: at 2 a resumed page has no room (value + its
boundary tie + lookahead) and every value cost an empty page + CP (seed 15).
- RawPageLimit rows pin the default-cap arithmetic (9 999 first page, 9 998 resumed).
Fuzz findings recorded as follow-ups: a bounded read whose data fits one page
returns cap + 1 (end bound fitted against N, not the cap; node manager, F2);
the Gateway walk-back's cost on a sparse series under a tiny tie budget is
pre-existing (seed 320: 82 reads uncapped, 93 capped).
Refs #736
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
59 lines
2.5 KiB
C#
59 lines
2.5 KiB
C#
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
/// <summary>
|
|
/// R08:1.1 (#736): the server raw cap (<c>ServerHistorian:MaxRawValuesPerRead</c>) is advertised as
|
|
/// <c>HistoryServerCapabilities/MaxReturnDataValues</c> (i=11273) through <see cref="HistoryPaging.AdvertiseMaxReturnDataValues"/>,
|
|
/// read back the way a client would — through the SDK master node manager — over a real server.
|
|
/// </summary>
|
|
public sealed class HistoryServerCapabilitiesAdvertisementTests(BoundsServerFixture fx) : IClassFixture<BoundsServerFixture>
|
|
{
|
|
private async Task<uint> ReadAdvertisedAsync()
|
|
{
|
|
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.Read, identity: null);
|
|
var nodesToRead = new ReadValueIdCollection
|
|
{
|
|
new ReadValueId { NodeId = VariableIds.HistoryServerCapabilities_MaxReturnDataValues, AttributeId = Attributes.Value },
|
|
};
|
|
var (values, _) = await fx.Nm.Server.NodeManager.ReadAsync(context, 0, TimestampsToReturn.Neither, nodesToRead, TestContext.Current.CancellationToken);
|
|
values.Count.ShouldBe(1);
|
|
StatusCode.IsGood(values[0].StatusCode).ShouldBeTrue(values[0].StatusCode.ToString());
|
|
return (uint)values[0].Value;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_cap_is_advertised_as_MaxReturnDataValues_and_a_re_sync_overwrites_it()
|
|
{
|
|
await fx.Gate.WaitAsync(TestContext.Current.CancellationToken);
|
|
try
|
|
{
|
|
HistoryPaging.AdvertiseMaxReturnDataValues(fx.Nm.Server, 10_000).ShouldBeTrue();
|
|
(await ReadAdvertisedAsync()).ShouldBe(10_000u);
|
|
|
|
HistoryPaging.AdvertiseMaxReturnDataValues(fx.Nm.Server, 2_500).ShouldBeTrue();
|
|
(await ReadAdvertisedAsync()).ShouldBe(2_500u);
|
|
|
|
// Unbounded (the opt-out) advertises Part 11's "no limit".
|
|
HistoryPaging.AdvertiseMaxReturnDataValues(fx.Nm.Server, 0).ShouldBeTrue();
|
|
(await ReadAdvertisedAsync()).ShouldBe(0u);
|
|
HistoryPaging.AdvertiseMaxReturnDataValues(fx.Nm.Server, -5).ShouldBeTrue();
|
|
(await ReadAdvertisedAsync()).ShouldBe(0u);
|
|
}
|
|
finally
|
|
{
|
|
HistoryPaging.AdvertiseMaxReturnDataValues(fx.Nm.Server, 0);
|
|
fx.Gate.Release();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void No_server_is_a_quiet_false()
|
|
{
|
|
HistoryPaging.AdvertiseMaxReturnDataValues(null, 10_000).ShouldBeFalse();
|
|
}
|
|
}
|