Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/Browsing/DiscoveryDriverBrowserFakes.cs
T
Joseph Doherty b815446940 refactor(drivers): delete GetMemoryFootprint / FlushOptionalCachesAsync (#525)
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
2026-07-31 09:07:11 -04:00

115 lines
4.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}