b815446940
Both `IDriver` members had no production call site. Their only consumer was `MemoryTracking`, deleted with the driver-tier recycle machinery in #522, and `GetMemoryFootprint`'s doc-comment claimed "polled every 30s by Core" — which had not been true since v3, even before #522 removed the poller. Owner decision on #525 was (a) delete rather than (b) build the process-wide watchdog that would consume them. The driver-side implementations are in git history and were mostly reusable; what was always missing is the trigger, and a member nobody calls is how the misleading doc-comment survived this long. Removed from `IDriver`, all 12 driver implementations, and every test stub — plus the support code that existed only to feed them, which is the part a member-only deletion would have left behind as dead weight: - MTConnect's three per-entry byte-weight constants, its cached `ProbeModelDataItemCount`, and `CountDataItems` — a full device-tree walk run on every cache fill to populate a number with no remaining reader. - OpcUaClient's `_discoveredNodeCount` field and the discovery-pass write to it. - MQTT's `ApproxBytesPerAuthoredTag`. Two behavioural notes, neither a change in what the software does: - `AbCipTemplateCache` now has no production reset path. Its only caller was AB CIP's flush, which itself had no caller — so nothing changes except how visible the fact is. Recorded on the type, along with the correction that its comment's claim about a device `ReinitializeAsync` also flushing it was never true. A UDT redefined on a live controller keeps its stale shape until the driver instance is rebuilt. - MTConnect's `/probe` cache is still dropped, by `InvalidateProbeModel()` on an Agent restart. That is now `internal` rather than `private` so the tests which used the flush purely to empty the cache drive the real path instead — they now cover a case that actually occurs in production. Several unrelated doc comments cited `FlushOptionalCachesAsync` as the canonical CAS example and are repointed to it. Tests: the ones that existed only to assert on the removed members are deleted (3 OpcUaClient, 3 Galaxy, 2 MQTT, 1 MTConnect, 1 AbCip). Worth noting one was vacuous — `FlushOptionalCachesAsync_resets_footprint_counter` asserted a footprint of 0 before the flush and 0 after with no discovery in between, so it would have passed against a flush that did nothing at all. Full suite green except 7 failures verified identical on master before and after (4 AbLegacy integration with the compose profile down, 3 Host.IntegrationTests), checked by name rather than assumed. Docs corrected where they described this as live: `DriverLifecycle.md` (which carried the #525 placeholder), `drivers/AbCip.md`, `drivers/AbLegacy.md`, and `v2/driver-stability.md` — whose "polls this every 30 s" + per-driver `DriverConfig` memory budget were never built as written and are now banner-ed as design intent rather than description. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
115 lines
4.8 KiB
C#
115 lines
4.8 KiB
C#
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);
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
/// <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());
|
||
}
|