using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using ZB.MOM.WW.OtOpcUa.Commons.Browsing; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Browsing; public class DiscoveryDriverBrowserSettleTests { private static readonly TimeSpan ShortShutdown = TimeSpan.FromMilliseconds(200); private static DiscoveryDriverBrowser Browser(IDriverFactory factory, TimeSpan openTimeout) => new(factory, NullLogger.Instance, openTimeout, ShortShutdown); private static void StreamN(IAddressSpaceBuilder builder, int n) { for (var i = 0; i < n; i++) builder.Variable($"T{i}", $"T{i}", new DriverAttributeInfo($"Dev.T{i}", DriverDataType.Float64, false, null, SecurityClassification.Operate, false)); } [Fact] public async Task UntilStable_RerunsUntilNonEmptyAndStable() { // Pass 1 streams nothing (FixedTree cache still filling); passes 2+ stream 3 nodes. var driver = new FakeDiscoveryDriver { RediscoverPolicy = DiscoveryRediscoverPolicy.UntilStable, OnDiscover = (b, idx) => { if (idx >= 2) StreamN(b, 3); return Task.CompletedTask; }, }; var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(30)); var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None); var root = await session.RootAsync(CancellationToken.None); root.Count.ShouldBe(3); driver.DiscoverCalls.ShouldBeGreaterThanOrEqualTo(3); // empty -> 3 -> 3-stable } [Fact] public async Task UntilStable_NeverStable_ReturnsLastNonEmptyAtTimeout() { // Node count grows every pass — never stabilises; on open-timeout return the last capture. var driver = new FakeDiscoveryDriver { RediscoverPolicy = DiscoveryRediscoverPolicy.UntilStable, OnDiscover = (b, idx) => { StreamN(b, idx); return Task.CompletedTask; }, }; var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(3)); var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None); var root = await session.RootAsync(CancellationToken.None); root.ShouldNotBeEmpty(); // last non-empty capture, not an exception } [Fact] public async Task UntilStable_NeverAnyNodes_FailsAtOpenTimeout() { // Every pass empty — no non-empty capture to fall back to, so the timeout surfaces. var driver = new FakeDiscoveryDriver { RediscoverPolicy = DiscoveryRediscoverPolicy.UntilStable, OnDiscover = (_, _) => Task.CompletedTask, }; var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(2)); await Should.ThrowAsync( () => browser.OpenAsync("Fake", "{}", CancellationToken.None)); } [Fact] public async Task OncePolicy_SingleDiscoverPass() { var driver = new FakeDiscoveryDriver { RediscoverPolicy = DiscoveryRediscoverPolicy.Once, OnDiscover = (b, _) => { StreamN(b, 3); return Task.CompletedTask; }, }; var browser = Browser(FakeDriverFactory.Of(() => driver), TimeSpan.FromSeconds(30)); await browser.OpenAsync("Fake", "{}", CancellationToken.None); driver.DiscoverCalls.ShouldBe(1); } }