using System.Collections.Concurrent; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// Opt-in fake the can register /// so a deployed driver actually reaches Connected (Healthy) in-process — no Docker sim and /// no real backend. Materialises a for the "Modbus" driver /// type (any other type returns null, mirroring ). /// /// /// The DI wiring resolves from the container; the production /// AddOtOpcUaRuntime() seeds via TryAddSingleton, so a /// harness that wants live drivers registers this fake first (its registration wins). Every /// initialize/connect succeeds, so the wrapping DriverInstanceActor walks /// Connecting → Connected → Healthy and (on ReconnectDriver) drives /// ForceReconnect → Reconnecting → re-initialize → Connected without any fault injection. /// Each created driver is recorded in so a test can reach in and flip its /// health to just before issuing the reconnect command. /// public sealed class FakeReconnectDriverFactory : IDriverFactory { /// The single driver type this fake factory materialises. public const string FakeDriverType = "Modbus"; /// /// Drivers this factory has created, keyed by driverInstanceId, so a test can retrieve the /// live instance (e.g. to call before /// dispatching a reconnect command). Concurrent because the factory is invoked on the spawning /// actor thread while the test reads on its own thread. /// public ConcurrentDictionary Created { get; } = new(); /// /// Returns a when is /// ; otherwise null (the host logs + skips the row). Records the /// created driver in keyed by . /// /// The driver type name from the deployed DriverInstance row. /// The stable driver-instance identifier. /// The driver configuration as a JSON string (ignored by the fake). /// A new , or null for an unsupported type. public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) { if (driverType != FakeDriverType) return null; var driver = new FakeReconnectDriver(driverInstanceId, driverType); Created[driverInstanceId] = driver; return driver; } /// Gets the driver-type names this factory can materialise. public IReadOnlyCollection SupportedTypes { get; } = new[] { FakeDriverType }; } /// /// A minimal in-process whose connect/initialize path always succeeds, so the /// wrapping DriverInstanceActor reaches Connected and publishes a /// snapshot. Reads/writes/subscribes are benign no-ops; this exists /// only to let a deployed driver walk the Healthy ↔ Reconnecting FSM in-process for E2E tests. /// /// The driver's reported health is controllable: a test calls /// to make return (simulating a lost /// connection — the realistic trigger for an operator Reconnect). The DriverInstanceActor's /// ForceReconnect handler POLLS right after entering its Reconnecting /// state, so that snapshot surfaces on the driver-health topic. /// The subsequent retry calls , which clears the flag back to /// , so the next snapshot returns to Healthy. /// public sealed class FakeReconnectDriver : IDriver { /// /// Initializes a new . /// /// The stable logical driver-instance identifier. /// The driver type name. public FakeReconnectDriver(string driverInstanceId, string driverType) { DriverInstanceId = driverInstanceId; DriverType = driverType; } /// Gets the stable logical ID of this driver instance. public string DriverInstanceId { get; } /// Gets the driver type name (e.g. "Modbus"). public string DriverType { get; } /// /// When true, reports . /// volatile because the actor polls from its own thread while the /// test flips this from another via . /// private volatile bool _reconnecting; /// Timestamp of the most recent successful initialize; surfaced as the last successful read. private DateTime _lastSuccess = DateTime.UtcNow; /// Backing field for ; mutated via . private int _initializeCount; /// /// Number of times has been invoked. Read by the test to prove a /// reconnect genuinely re-initialised the driver through the full cluster path (≥ 2 means the /// initial connect plus at least one reconnect retry). The actor's retry path runs on a thread-pool /// thread, so the increment is and the read is . /// public int InitializeCount => Volatile.Read(ref _initializeCount); /// /// Marks the driver as having lost its connection so the next poll reports /// . The test calls this immediately before dispatching the /// reconnect command, simulating the realistic operator-Reconnect trigger. /// public void ReportReconnecting() => _reconnecting = true; /// /// Connect/initialize path — always succeeds (returns a completed task), so the actor self-Tells /// InitializeSucceeded and becomes Connected. This is the method that makes connect /// succeed; the FSM's reconnect path re-invokes it and it succeeds again. Increments /// and clears the reconnecting flag (initialize succeeded → healthy /// again). /// /// The driver configuration JSON (ignored). /// Cancellation token for the operation. /// A completed task — initialization always succeeds. public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { Interlocked.Increment(ref _initializeCount); _lastSuccess = DateTime.UtcNow; _reconnecting = false; return Task.CompletedTask; } /// Applies a config change in place — a no-op that always succeeds. /// The driver configuration JSON (ignored). /// Cancellation token for the operation. /// A completed task — reinitialization always succeeds. public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask; /// Stops the driver — a no-op that always succeeds. /// Cancellation token for the operation. /// A completed task. public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// /// Returns a snapshot when the test has flagged a lost /// connection via ; otherwise a /// snapshot. The actor polls this on every observable state change, so the published state tracks /// this flag. /// /// A reflecting the controllable connection state. public DriverHealth GetHealth() => _reconnecting ? new DriverHealth(DriverState.Reconnecting, _lastSuccess, null) : new DriverHealth(DriverState.Healthy, _lastSuccess, null); /// Returns a zero memory footprint (the fake holds no driver-attributable caches). /// Always 0. public long GetMemoryFootprint() => 0; /// Flushes optional caches — a no-op (the fake holds none). /// Cancellation token for the operation. /// A completed task. public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; }