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);
/// Every adapter that opened a session, so a test can kill the live one (ScadaBridge#141).
public ConcurrentQueue Sessions { get; } = new();
///
/// The server at goes away: it refuses new sessions and every live
/// session on it raises Disconnected — what a stopped OtOpcUa node looks like to the client.
///
public void Stop(string endpoint)
{
SetReachable(endpoint, false);
foreach (var session in Sessions.Where(s => s.Endpoint == endpoint && s.IsConnected))
session.RaiseDisconnected();
}
/// The endpoints sessions were opened on after the first connects.
public string[] ConnectsAfter(int skip) => Connects.Skip(skip).ToArray();
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;
_rig.Sessions.Enqueue(this);
return Task.CompletedTask;
}
public bool IsConnected => _connected;
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;
}
}