using System.Collections.Concurrent; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors; /// /// In-memory batch-capable for the WP2.1b seam tests. It /// records every batch call (so "one round trip per chunk, not per tag" is assertable), /// can fail individual tags or a whole batch, and can hang its bulk read so the seed /// deadline is observable. /// public sealed class FakeBatchDataConnection : IDataConnection, IBatchSubscribableConnection, IAlarmSubscribableConnection { private int _nextId; /// Tag lists handed to , one entry per call. public readonly ConcurrentQueue> SubscribeBatches = new(); /// Id lists handed to , one entry per call. public readonly ConcurrentQueue> UnsubscribeBatches = new(); /// Tag lists handed to , one entry per call. public readonly ConcurrentQueue> ReadBatches = new(); /// Count of SINGLE-tag subscribe calls; must stay 0 on a batch-capable adapter. public int SingleSubscribeCalls; /// Count of SINGLE-tag read calls; must stay 0 on a batch-capable adapter. public int SingleReadCalls; /// Wall-clock instant of each call. public readonly ConcurrentQueue SubscribeBatchTimes = new(); /// Tags reported as failed rows (per-tag resolution failure). public readonly HashSet FailingTags = new(StringComparer.Ordinal); /// When set, every batch subscribe throws this — a batch-level fault. public Func? BatchSubscribeThrows; /// When true, bulk reads never return until the caller's token cancels. public bool HangReads; /// /// When set, records the call and then parks until this /// task completes, before producing its result rows. Lets a test hold a subscribe batch /// in flight while it drives other messages into the actor (e.g. an unsubscribe that /// races the completion). /// public Task? SubscribeGate; /// Value returned for every readable tag. public object? SeedValue = 42; /// Callback the last batch subscribe registered; drives value pushes in tests. public SubscriptionCallback? ValueCallback; /// Callback the last alarm subscribe registered. public AlarmTransitionCallback? AlarmCallback; /// public ConnectionHealth Status { get; private set; } = ConnectionHealth.Disconnected; /// public event Action? Disconnected; /// Raises as a real adapter would on a transport fault. public void RaiseDisconnected() => Disconnected?.Invoke(); /// public Task ConnectAsync(IDictionary connectionDetails, CancellationToken cancellationToken = default) { Status = ConnectionHealth.Connected; return Task.CompletedTask; } /// public Task DisconnectAsync(CancellationToken cancellationToken = default) { Status = ConnectionHealth.Disconnected; return Task.CompletedTask; } /// public async Task> SubscribeBatchAsync( IReadOnlyList tagPaths, SubscriptionCallback callback, CancellationToken cancellationToken = default) { SubscribeBatches.Enqueue(tagPaths.ToList()); SubscribeBatchTimes.Enqueue(DateTimeOffset.UtcNow); ValueCallback = callback; if (BatchSubscribeThrows is { } factory) throw factory(); if (SubscribeGate is { } gate) await gate; IReadOnlyList rows = tagPaths .Select(t => FailingTags.Contains(t) ? new TagSubscribeResult(t, false, null, "node not found") : new TagSubscribeResult(t, true, $"sub-{Interlocked.Increment(ref _nextId)}", null)) .ToList(); return rows; } /// public Task UnsubscribeBatchAsync(IReadOnlyList subscriptionIds, CancellationToken cancellationToken = default) { UnsubscribeBatches.Enqueue(subscriptionIds.ToList()); return Task.CompletedTask; } /// public Task SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default) { Interlocked.Increment(ref SingleSubscribeCalls); ValueCallback = callback; return Task.FromResult($"sub-{Interlocked.Increment(ref _nextId)}"); } /// public Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default) => Task.CompletedTask; /// public async Task ReadAsync(string tagPath, CancellationToken cancellationToken = default) { Interlocked.Increment(ref SingleReadCalls); if (HangReads) await Task.Delay(Timeout.Infinite, cancellationToken); return new ReadResult(true, new TagValue(SeedValue, QualityCode.Good, DateTimeOffset.UtcNow), null); } /// public async Task> ReadBatchAsync( IEnumerable tagPaths, CancellationToken cancellationToken = default) { var tags = tagPaths.ToList(); ReadBatches.Enqueue(tags); if (HangReads) await Task.Delay(Timeout.Infinite, cancellationToken); return tags.ToDictionary( t => t, t => new ReadResult(true, new TagValue(SeedValue, 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(kv => kv.Key, _ => new WriteResult(true, null))); /// public Task WriteBatchAndWaitAsync( IDictionary values, string flagPath, object? flagValue, string responsePath, object? responseValue, TimeSpan timeout, CancellationToken cancellationToken = default) => Task.FromResult(true); /// public Task SubscribeAlarmsAsync( string sourceReference, string? conditionFilter, AlarmTransitionCallback callback, CancellationToken cancellationToken = default) { AlarmCallback = callback; return Task.FromResult($"alarm-{Interlocked.Increment(ref _nextId)}"); } /// public Task UnsubscribeAlarmsAsync(string subscriptionId, CancellationToken cancellationToken = default) => Task.CompletedTask; /// public ValueTask DisposeAsync() => ValueTask.CompletedTask; }