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(); /// Every PDU sent, in order. public ConcurrentQueue<(byte Unit, byte[] Pdu)> Sent { get; } = new(); /// The (function code, start, quantity) of every sent request with that function code. public List<(byte Fc, ushort Start, ushort Qty)> Requests(byte fc) => [.. Sent.Where(s => s.Pdu[0] == fc).Select(s => (s.Pdu[0], (ushort)((s.Pdu[1] << 8) | s.Pdu[2]), (ushort)((s.Pdu[3] << 8) | s.Pdu[4])))]; 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); Sent.Enqueue((unitId, pdu)); var scripted = Handler?.Invoke(unitId, pdu, ct); return scripted ?? Bank.SendAsync(unitId, pdu, ct); } public ValueTask DisposeAsync() { Interlocked.Increment(ref _disposeCount); return ValueTask.CompletedTask; } }