using System.Collections.Concurrent; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests; /// /// In-memory transport whose every send is answered by a test-supplied handler; falls back to the /// register/coil bank fake () when the handler returns null. /// Counts sends per unit and dispose calls; the connect can be blocked on a gate. /// internal sealed class ScriptedTransport : IModbusTransport { private readonly ConcurrentDictionary _sendsByUnit = new(); private int _disposeCount; /// The bank answering un-scripted sends. public ModbusDriverTests.FakeTransport Bank { get; } = new(); /// Per-send handler; return null to fall through to . public Func?>? Handler { get; set; } /// When set, waits for it before completing. public TaskCompletionSource? ConnectGate { get; set; } /// Signalled when is entered. public TaskCompletionSource ConnectEntered { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public int DisposeCount => Volatile.Read(ref _disposeCount); public int SendsTo(byte unit) => _sendsByUnit.TryGetValue(unit, out var n) ? n : 0; public int TotalSends => _sendsByUnit.Values.Sum(); public async Task ConnectAsync(CancellationToken ct) { ConnectEntered.TrySetResult(); if (ConnectGate is { } gate) await gate.Task.WaitAsync(ct); } public Task SendAsync(byte unitId, byte[] pdu, CancellationToken ct) { _sendsByUnit.AddOrUpdate(unitId, 1, (_, n) => n + 1); var scripted = Handler?.Invoke(unitId, pdu, ct); return scripted ?? Bank.SendAsync(unitId, pdu, ct); } public ValueTask DisposeAsync() { Interlocked.Increment(ref _disposeCount); return ValueTask.CompletedTask; } }