Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Browse/GatewayGalaxyHierarchySourcePagingTests.cs
T
Joseph Doherty 6dedf5158f feat(galaxy): wire DiscoverPageSize and ProbeTimeoutSeconds; concurrent first-touch write lanes
Review 2026-10-21 G-5, G-6, G-8 ("wire them all").

- G-5: GatewayGalaxyHierarchySource now pages the DiscoverHierarchy RPC
  itself (DiscoverHierarchyRawAsync) at Repository.DiscoverPageSize. The
  mxaccessgw client's typed DiscoverHierarchyAsync overloads hard-code a
  5000-row page and DiscoverHierarchyOptions carries no page size, so the
  authored knob reached nothing; the raw request takes page_size on the
  wire, so it is honoured here (non-positive → 5000). The loop keeps the
  client's repeated-page-token guard.
- G-6: GalaxyDriverProbe reads ProbeTimeoutSeconds from the config and
  runs under min(caller budget, configured cap) — the knob can shorten a
  probe, never extend it past the host's budget. The TCP preflight now
  shares that deadline instead of hanging on the caller's token alone.
- G-8: GatewayGalaxyDataWriter runs a batch as one lane per distinct tag,
  concurrently and bounded at 8 (MaxConcurrentWriteLanes); same-tag
  writes keep their authored order inside one lane and results land at
  their request's index. Acks stay serial by design.
- GalaxyDriverForm: help text now says what the two knobs actually do.

Refs #735

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 06:41:47 -04:00

93 lines
3.6 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Browse;
/// <summary>
/// Review 2026-10-21 G-5: <c>Repository.DiscoverPageSize</c> was authored and parsed but reached
/// nothing — the client's typed <c>DiscoverHierarchyAsync</c> hard-codes 5000. The source now pages
/// the raw RPC itself at the configured size.
/// </summary>
public sealed class GatewayGalaxyHierarchySourcePagingTests
{
private static GalaxyObject Obj(int id) => new() { GobjectId = id, TagName = $"Obj{id}" };
/// <summary>Every request carries the configured page size and the page tokens chain until the last page.</summary>
[Fact]
public async Task Pages_at_the_configured_size_and_chains_tokens()
{
var requests = new List<DiscoverHierarchyRequest>();
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
{
requests.Add(req);
var reply = new DiscoverHierarchyReply();
switch (req.PageToken)
{
case "":
reply.Objects.Add([Obj(1), Obj(2)]);
reply.NextPageToken = "p2";
break;
case "p2":
reply.Objects.Add([Obj(3), Obj(4)]);
reply.NextPageToken = "p3";
break;
default:
reply.Objects.Add(Obj(5));
reply.NextPageToken = "";
break;
}
return Task.FromResult(reply);
}
var objects = await GatewayGalaxyHierarchySource.PageAsync(Discover, pageSize: 2, CancellationToken.None);
objects.Select(o => o.GobjectId).ShouldBe([1, 2, 3, 4, 5]);
requests.Count.ShouldBe(3);
requests.ShouldAllBe(r => r.PageSize == 2);
requests.Select(r => r.PageToken).ShouldBe(["", "p2", "p3"]);
}
/// <summary>A different configured size is what goes on the wire (positive control for the knob).</summary>
[Fact]
public async Task A_different_page_size_is_sent_on_the_wire()
{
int? sent = null;
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
{
sent = req.PageSize;
return Task.FromResult(new DiscoverHierarchyReply());
}
await GatewayGalaxyHierarchySource.PageAsync(Discover, pageSize: 250, CancellationToken.None);
sent.ShouldBe(250);
}
/// <summary>A non-positive configured size falls back to the client library's 5000 default.</summary>
[Theory]
[InlineData(0)]
[InlineData(-7)]
public void Non_positive_page_size_falls_back_to_the_default(int configured)
=> GatewayGalaxyHierarchySource.EffectivePageSize(configured).ShouldBe(GatewayGalaxyHierarchySource.DefaultPageSize);
/// <summary>A repeated page token is refused rather than walked forever.</summary>
[Fact]
public async Task A_repeated_page_token_is_refused()
{
var calls = 0;
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
{
calls++;
var reply = new DiscoverHierarchyReply { NextPageToken = "same" };
reply.Objects.Add(Obj(calls));
return Task.FromResult(reply);
}
var ex = await Should.ThrowAsync<InvalidOperationException>(
() => GatewayGalaxyHierarchySource.PageAsync(Discover, pageSize: 10, CancellationToken.None));
ex.Message.ShouldContain("repeated page token");
calls.ShouldBe(2);
}
}