using System.Collections.Concurrent; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors; /// /// The two OPC UA servers of a redundant pair as the ScadaBridge#138 tests see them: a /// ServiceLevel per endpoint (changeable at any time, null = unreadable), whether each endpoint /// accepts a session, and a record of what the actor did to them. /// internal sealed class ServiceLevelRig { private readonly ConcurrentDictionary> _levels = new(); private readonly ConcurrentDictionary _unreachable = new(); /// Every endpoint a session was opened on, in order — live sessions and probes alike. public ConcurrentQueue Connects { get; } = new(); /// Every tag subscribed, as "endpoint|tag" — only a live session receives these. public ConcurrentQueue Subscribes { get; } = new(); /// Number of ServiceLevel reads, per endpoint. public ConcurrentDictionary Reads { get; } = new(); /// Sets a fixed ServiceLevel for . public void SetLevel(string endpoint, byte? level) => _levels[endpoint] = () => level; /// Sets a ServiceLevel source evaluated on every read of . public void SetLevel(string endpoint, Func level) => _levels[endpoint] = level; /// Makes refuse (or accept again) new sessions. public void SetReachable(string endpoint, bool reachable) => _unreachable[endpoint] = !reachable; public bool IsReachable(string endpoint) => !_unreachable.GetValueOrDefault(endpoint); public byte? Read(string endpoint) { Reads.AddOrUpdate(endpoint, 1, (_, n) => n + 1); return _levels.TryGetValue(endpoint, out var level) ? level() : null; } public int ConnectCount(string endpoint) => Connects.Count(e => e == endpoint); public int ReadCount(string endpoint) => Reads.GetValueOrDefault(endpoint); } /// /// An with the capability, bound to /// one endpoint of a . Subscriptions and reads always succeed. /// internal sealed class FakeServiceLevelConnection : IDataConnection, IServiceLevelReadable { private readonly ServiceLevelRig _rig; private volatile bool _connected; public FakeServiceLevelConnection(ServiceLevelRig rig, string endpoint) { _rig = rig; Endpoint = endpoint; } public string Endpoint { get; } public ConnectionHealth Status => _connected ? ConnectionHealth.Connected : ConnectionHealth.Disconnected; public event Action? Disconnected; public Task ConnectAsync(IDictionary connectionDetails, CancellationToken cancellationToken = default) { _rig.Connects.Enqueue(Endpoint); if (!_rig.IsReachable(Endpoint)) return Task.FromException(new InvalidOperationException($"{Endpoint} refused the session")); _connected = true; return Task.CompletedTask; } public Task DisconnectAsync(CancellationToken cancellationToken = default) { _connected = false; return Task.CompletedTask; } public Task ReadServiceLevelAsync(CancellationToken cancellationToken = default) => Task.FromResult(_connected ? _rig.Read(Endpoint) : null); public Task SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default) { _rig.Subscribes.Enqueue($"{Endpoint}|{tagPath}"); return Task.FromResult($"{Endpoint}:{tagPath}"); } public Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default) => Task.CompletedTask; public Task ReadAsync(string tagPath, CancellationToken cancellationToken = default) => Task.FromResult(new ReadResult(true, new TagValue(1, QualityCode.Good, DateTimeOffset.UtcNow), null)); public Task> ReadBatchAsync(IEnumerable tagPaths, CancellationToken cancellationToken = default) => Task.FromResult>(tagPaths.ToDictionary( t => t, _ => new ReadResult(true, new TagValue(1, QualityCode.Good, DateTimeOffset.UtcNow), null))); public Task WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default) => Task.FromResult(new WriteResult(true, null)); public Task> WriteBatchAsync(IDictionary values, CancellationToken cancellationToken = default) => Task.FromResult>(values.ToDictionary(v => v.Key, _ => new WriteResult(true, null))); public void RaiseDisconnected() { _connected = false; Disconnected?.Invoke(); } public ValueTask DisposeAsync() { _connected = false; return ValueTask.CompletedTask; } }