Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ScriptedTransport.cs
T
Joseph Doherty 2ef3cd0df2 feat(modbus): block-read coalescing on by default; coils/DI/BitInRegister/String fold
U2: MaxReadGap defaults to 8 and 0 now means adjacent-only rather than
off; a new DisableReadCoalescing switch (factory DTO + AdminUI form) turns it
off entirely. The planner also merges Coils/DiscreteInputs (FC01/FC02, gap in
bits), String scalars and BitInRegister tags - sixteen bit tags on one status
word are one FC03 instead of sixteen. Auto-prohibition and its re-probe /
bisection use the region's own function code (a coil range was re-probed
with FC04).

Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:24:38 -04:00

60 lines
2.5 KiB
C#

using System.Collections.Concurrent;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
/// <summary>
/// In-memory transport whose every send is answered by a test-supplied handler; falls back to the
/// register/coil bank fake (<see cref="ModbusDriverTests.FakeTransport"/>) when the handler returns null.
/// Counts sends per unit and dispose calls; the connect can be blocked on a gate.
/// </summary>
internal sealed class ScriptedTransport : IModbusTransport
{
private readonly ConcurrentDictionary<byte, int> _sendsByUnit = new();
private int _disposeCount;
/// <summary>The bank answering un-scripted sends.</summary>
public ModbusDriverTests.FakeTransport Bank { get; } = new();
/// <summary>Per-send handler; return null to fall through to <see cref="Bank"/>.</summary>
public Func<byte, byte[], CancellationToken, Task<byte[]>?>? Handler { get; set; }
/// <summary>When set, <see cref="ConnectAsync"/> waits for it before completing.</summary>
public TaskCompletionSource? ConnectGate { get; set; }
/// <summary>Signalled when <see cref="ConnectAsync"/> is entered.</summary>
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();
/// <summary>Every PDU sent, in order.</summary>
public ConcurrentQueue<(byte Unit, byte[] Pdu)> Sent { get; } = new();
/// <summary>The (function code, start, quantity) of every sent request with that function code.</summary>
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<byte[]> 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;
}
}