feat(browse): Wave-0 Batch D — IUniversalDriverBrowser + DiscoveryDriverBrowser core
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.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Browsing;
|
||||
|
||||
/// <summary>Shared test doubles for the DiscoveryDriverBrowser suites (Tasks 8–10).</summary>
|
||||
internal sealed class FakeDiscoveryDriver : IDriver, ITagDiscovery
|
||||
{
|
||||
public int InitializeCalls;
|
||||
public int DiscoverCalls;
|
||||
public int ShutdownCalls;
|
||||
|
||||
/// <summary>Ordered lifecycle trace ("init"/"discover"/"shutdown").</summary>
|
||||
public List<string> Trace { get; } = [];
|
||||
|
||||
/// <summary>Completes once ShutdownAsync has finished (even the fire-and-forget CanBrowse path).</summary>
|
||||
public TaskCompletionSource ShutdownDone { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public bool SupportsOnlineDiscovery { get; init; } = true;
|
||||
public DiscoveryRediscoverPolicy RediscoverPolicy { get; init; } = DiscoveryRediscoverPolicy.Once;
|
||||
|
||||
/// <summary>Discovery behavior: (builder, 1-based call index) → task. Default streams one leaf.</summary>
|
||||
public Func<IAddressSpaceBuilder, int, Task>? OnDiscover { get; init; }
|
||||
public Func<CancellationToken, Task>? OnInitialize { get; init; }
|
||||
public Func<CancellationToken, Task>? OnShutdown { get; init; }
|
||||
|
||||
public string DriverInstanceId { get; }
|
||||
public string DriverType { get; }
|
||||
public string? LastConfigJson { get; private set; }
|
||||
|
||||
public FakeDiscoveryDriver(string driverType = "Fake", string id = "fake-1")
|
||||
{
|
||||
DriverType = driverType;
|
||||
DriverInstanceId = id;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
LastConfigJson = driverConfigJson;
|
||||
Interlocked.Increment(ref InitializeCalls);
|
||||
lock (Trace) Trace.Add("init");
|
||||
if (OnInitialize is not null) await OnInitialize(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
|
||||
{
|
||||
var idx = Interlocked.Increment(ref DiscoverCalls);
|
||||
lock (Trace) Trace.Add("discover");
|
||||
if (OnDiscover is not null)
|
||||
{
|
||||
await OnDiscover(builder, idx).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
// Default: stream one leaf so the captured tree is non-empty.
|
||||
builder.Variable("Tag1", "Tag1",
|
||||
new DriverAttributeInfo("Dev.Tag1", DriverDataType.Float64, false, null,
|
||||
SecurityClassification.Operate, false));
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref ShutdownCalls);
|
||||
lock (Trace) Trace.Add("shutdown");
|
||||
try
|
||||
{
|
||||
if (OnShutdown is not null) await OnShutdown(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ShutdownDone.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, null, null);
|
||||
public long GetMemoryFootprint() => 0;
|
||||
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>An IDriver that is NOT ITagDiscovery — exercises the not-discovery-capable gate.</summary>
|
||||
internal sealed class FakeNonDiscoveryDriver(string driverType = "Flat", string id = "flat-1") : IDriver
|
||||
{
|
||||
public int ShutdownCalls;
|
||||
public TaskCompletionSource ShutdownDone { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public string DriverInstanceId { get; } = id;
|
||||
public string DriverType { get; } = driverType;
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref ShutdownCalls);
|
||||
ShutdownDone.TrySetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, null, null);
|
||||
public long GetMemoryFootprint() => 0;
|
||||
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Configurable IDriverFactory: delegates construction to a supplied func and records what it made.</summary>
|
||||
internal sealed class FakeDriverFactory(Func<string, string, string, IDriver?> create) : IDriverFactory
|
||||
{
|
||||
public int CreateCalls;
|
||||
public List<IDriver> Created { get; } = [];
|
||||
|
||||
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
Interlocked.Increment(ref CreateCalls);
|
||||
var d = create(driverType, driverInstanceId, driverConfigJson);
|
||||
if (d is not null) lock (Created) Created.Add(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> SupportedTypes { get; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>Factory that always returns a fresh default FakeDiscoveryDriver.</summary>
|
||||
public static FakeDriverFactory Of(Func<FakeDiscoveryDriver> make) =>
|
||||
new((_, _, _) => make());
|
||||
}
|
||||
Reference in New Issue
Block a user