698703744f
Task 9: DiscoverTreeAsync UntilStable settle loop (FOCAS) — re-capture on a 1s interval until
the node set is non-empty and stable across two passes, bounded by open-timeout; on
timeout with a prior non-empty capture, return it (best-effort) instead of failing.
Task 11: BrowserSessionService resolves bespoke-first, falls back to IUniversalDriverBrowser
when no bespoke browser matches and CanBrowse is true; new CanBrowse on the service.
16 unit tests green (4 settle + 12 service).
88 lines
3.4 KiB
C#
88 lines
3.4 KiB
C#
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<DiscoveryDriverBrowser>.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<OperationCanceledException>(
|
|
() => 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);
|
|
}
|
|
}
|