fa339a5565
Task 8: IUniversalDriverBrowser + DiscoveryDriverBrowser open path — construct(+PatchForBrowse)
-> connect -> one-shot capture -> bounded (10s) shutdown; CanBrowse gate (catch-throwing
TryCreate, defensive teardown of the throwaway instance); deep-merge browse patch.
Commons gains Microsoft.Extensions.Logging.Abstractions (ILogger doesn't flow transitively)
+ InternalsVisibleTo(Commons.Tests). OTOPCUA0001 suppressed on the deliberate one-shot
browse-capture DiscoverAsync (not a runtime dispatch path). 12 unit tests green.
146 lines
5.7 KiB
C#
146 lines
5.7 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 DiscoveryDriverBrowserTests
|
|
{
|
|
private static readonly TimeSpan ShortOpen = TimeSpan.FromSeconds(5);
|
|
private static readonly TimeSpan ShortShutdown = TimeSpan.FromMilliseconds(200);
|
|
|
|
private static DiscoveryDriverBrowser Browser(IDriverFactory factory) =>
|
|
new(factory, NullLogger<DiscoveryDriverBrowser>.Instance, ShortOpen, ShortShutdown);
|
|
|
|
[Fact]
|
|
public async Task OpenAsync_RunsInitializeDiscoverShutdown_InOrder()
|
|
{
|
|
var driver = new FakeDiscoveryDriver();
|
|
var browser = Browser(FakeDriverFactory.Of(() => driver));
|
|
|
|
var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None);
|
|
|
|
session.ShouldNotBeNull();
|
|
driver.Trace.ShouldBe(new[] { "init", "discover", "shutdown" });
|
|
driver.InitializeCalls.ShouldBe(1);
|
|
driver.ShutdownCalls.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAsync_ShutdownRuns_EvenWhenDiscoverThrows()
|
|
{
|
|
var driver = new FakeDiscoveryDriver
|
|
{
|
|
OnDiscover = (_, _) => throw new InvalidOperationException("boom-discovery"),
|
|
};
|
|
var browser = Browser(FakeDriverFactory.Of(() => driver));
|
|
|
|
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
|
() => browser.OpenAsync("Fake", "{}", CancellationToken.None));
|
|
ex.Message.ShouldBe("boom-discovery"); // discovery error surfaces, not a shutdown error
|
|
driver.ShutdownCalls.ShouldBe(1); // shutdown still ran
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAsync_UnknownType_ThrowsCleanMessage()
|
|
{
|
|
var browser = Browser(new FakeDriverFactory((_, _, _) => null));
|
|
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
|
() => browser.OpenAsync("Ghost", "{}", CancellationToken.None));
|
|
ex.Message.ShouldContain("Ghost");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAsync_NotDiscoveryCapable_Throws()
|
|
{
|
|
var browser = Browser(new FakeDriverFactory((_, _, _) => new FakeNonDiscoveryDriver()));
|
|
await Should.ThrowAsync<InvalidOperationException>(
|
|
() => browser.OpenAsync("Flat", "{}", CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAsync_FlagFalse_Throws()
|
|
{
|
|
var browser = Browser(FakeDriverFactory.Of(() => new FakeDiscoveryDriver { SupportsOnlineDiscovery = false }));
|
|
await Should.ThrowAsync<InvalidOperationException>(
|
|
() => browser.OpenAsync("Fake", "{}", CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public void PatchForBrowse_MergesAbCipPatch()
|
|
{
|
|
var patched = DiscoveryDriverBrowser.PatchForBrowse("AbCip", """{ "Host": "1.2.3.4" }""");
|
|
var obj = System.Text.Json.Nodes.JsonNode.Parse(patched)!.AsObject();
|
|
((string?)obj["Host"]).ShouldBe("1.2.3.4");
|
|
((bool?)obj["EnableControllerBrowse"]).ShouldBe(true);
|
|
|
|
// Unknown type: unchanged.
|
|
DiscoveryDriverBrowser.PatchForBrowse("Modbus", """{ "X": 1 }""").ShouldBe("""{ "X": 1 }""");
|
|
}
|
|
|
|
[Fact]
|
|
public void PatchForBrowse_DeepMergesFocasFixedTree()
|
|
{
|
|
var patched = DiscoveryDriverBrowser.PatchForBrowse("FOCAS", """{ "FixedTree": { "Series": "30i" } }""");
|
|
var fixedTree = System.Text.Json.Nodes.JsonNode.Parse(patched)!["FixedTree"]!.AsObject();
|
|
((string?)fixedTree["Series"]).ShouldBe("30i"); // kept
|
|
((bool?)fixedTree["Enabled"]).ShouldBe(true); // added
|
|
}
|
|
|
|
[Fact]
|
|
public void CanBrowse_TrueForDiscoveryCapable()
|
|
{
|
|
var browser = Browser(FakeDriverFactory.Of(() => new FakeDiscoveryDriver()));
|
|
browser.CanBrowse("Fake", "{}").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void CanBrowse_FalseWhenTryCreateReturnsNull()
|
|
{
|
|
var browser = Browser(new FakeDriverFactory((_, _, _) => null));
|
|
browser.CanBrowse("Ghost", "{}").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void CanBrowse_FalseWhenTryCreateThrows_NoExceptionEscapes()
|
|
{
|
|
var browser = Browser(new FakeDriverFactory((_, _, _) => throw new System.Text.Json.JsonException("bad json")));
|
|
Should.NotThrow(() => browser.CanBrowse("Fake", "{ half-typed").ShouldBeFalse());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanBrowse_ShutsDownThrowawayInstance()
|
|
{
|
|
var driver = new FakeDiscoveryDriver { SupportsOnlineDiscovery = false };
|
|
var factory = FakeDriverFactory.Of(() => driver);
|
|
var browser = Browser(factory);
|
|
|
|
browser.CanBrowse("Fake", "{}").ShouldBeFalse();
|
|
// Teardown is fire-and-forget — await its completion.
|
|
await driver.ShutdownDone.Task.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
|
|
driver.ShutdownCalls.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BoundedShutdown_AbandonsWedgedDriver()
|
|
{
|
|
// Shutdown ignores its token and would never complete; the browser must abandon it
|
|
// and still return the successfully-captured session within the shutdown bound.
|
|
var driver = new FakeDiscoveryDriver
|
|
{
|
|
OnShutdown = _ => Task.Delay(Timeout.Infinite, CancellationToken.None),
|
|
};
|
|
var browser = Browser(FakeDriverFactory.Of(() => driver));
|
|
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
var session = await browser.OpenAsync("Fake", "{}", CancellationToken.None);
|
|
sw.Stop();
|
|
|
|
session.ShouldNotBeNull(); // discovery succeeded → usable session
|
|
driver.ShutdownCalls.ShouldBe(1); // shutdown was attempted
|
|
sw.Elapsed.ShouldBeLessThan(ShortOpen); // returned on the shutdown bound, not the open timeout
|
|
}
|
|
}
|