Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Fakes/FakeCipChannel.cs
T

129 lines
5.2 KiB
C#

using System.Buffers.Binary;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
/// <summary>
/// Scriptable <see cref="ICipChannel"/>: settable <see cref="MaxRequestBytes"/>, records every request
/// and the token it was sent with, and answers via <see cref="Respond"/>. The default responder
/// answers any service with an empty success reply and splits a Multiple Service Packet into
/// per-service replies via <see cref="RespondPerService"/>. Reused by Tasks 12 and 13.
/// </summary>
internal sealed class FakeCipChannel : ICipChannel
{
private readonly SemaphoreSlim _entered = new(0);
public FakeCipChannel(int maxRequestBytes = 504)
{
MaxRequestBytes = maxRequestBytes;
Respond = DefaultRespond;
}
private int _maxRequestBytes;
public int MaxRequestBytes
{
get => MaxRequestBytesThrows is { } ex ? throw ex : _maxRequestBytes;
set => _maxRequestBytes = value;
}
/// <summary>When set, reading <see cref="MaxRequestBytes"/> throws it (simulates a broken channel).</summary>
public Exception? MaxRequestBytesThrows { get; set; }
/// <summary>Every request, in send order.</summary>
public List<byte[]> Sent { get; } = [];
/// <summary>The token each request was sent with, parallel to <see cref="Sent"/>.</summary>
public List<CancellationToken> Tokens { get; } = [];
/// <summary>Answers one request with raw CIP reply bytes.</summary>
public Func<byte[], Task<byte[]>> Respond { get; set; }
/// <summary>Answers one embedded (non-MSP) service; used by the default <see cref="Respond"/>.</summary>
public Func<byte[], byte[]> RespondPerService { get; set; } = OkReply;
/// <summary>When set, every send blocks on it (after being recorded) until the test completes it.</summary>
public TaskCompletionSource? Gate { get; set; }
public int SentCount { get { lock (Sent) return Sent.Count; } }
public async Task<ReadOnlyMemory<byte>> SendAsync(ReadOnlyMemory<byte> cipRequest, CancellationToken cancellationToken)
{
var req = cipRequest.ToArray();
lock (Sent)
{
Sent.Add(req);
Tokens.Add(cancellationToken);
}
_entered.Release();
if (Gate is { } g) await g.Task.WaitAsync(cancellationToken);
return await Respond(req);
}
/// <summary>Completes once the channel has received <paramref name="count"/> requests in total.</summary>
public async Task WaitForSendsAsync(int count)
{
while (SentCount < count)
{
if (!await _entered.WaitAsync(TimeSpan.FromSeconds(10)))
throw new TimeoutException($"Expected {count} sends, saw {SentCount}.");
}
}
/// <summary>A success reply for <paramref name="request"/> with optional data.</summary>
public static byte[] OkReply(byte[] request) => Reply(request[0], 0x00, []);
public static byte[] Reply(byte requestService, byte generalStatus, byte[] data, params ushort[] extended)
{
var r = new byte[4 + 2 * extended.Length + data.Length];
r[0] = (byte)(requestService | 0x80);
r[2] = generalStatus;
r[3] = (byte)extended.Length;
for (var i = 0; i < extended.Length; i++) BinaryPrimitives.WriteUInt16LittleEndian(r.AsSpan(4 + 2 * i), extended[i]);
data.CopyTo(r, 4 + 2 * extended.Length);
return r;
}
/// <summary>Splits an MSP request into its embedded requests.</summary>
public static List<byte[]> SplitMsp(byte[] msp)
{
const int countAt = 6; // 0A 02 20 02 24 01
var data = msp.AsSpan(countAt);
int n = BinaryPrimitives.ReadUInt16LittleEndian(data);
var offsets = new int[n];
for (var i = 0; i < n; i++) offsets[i] = BinaryPrimitives.ReadUInt16LittleEndian(data[(2 + 2 * i)..]);
var result = new List<byte[]>(n);
for (var i = 0; i < n; i++)
{
var end = i + 1 < n ? offsets[i + 1] : data.Length;
result.Add(data[offsets[i]..end].ToArray());
}
return result;
}
/// <summary>An MSP reply (service 0x8A) carrying the given embedded replies.</summary>
public static byte[] MspReply(IReadOnlyList<byte[]> replies, byte generalStatus = 0x00)
{
var body = new List<byte>();
var header = new byte[2 + 2 * replies.Count];
BinaryPrimitives.WriteUInt16LittleEndian(header, (ushort)replies.Count);
var offset = header.Length;
for (var i = 0; i < replies.Count; i++)
{
BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(2 + 2 * i), (ushort)offset);
offset += replies[i].Length;
}
body.AddRange(header);
foreach (var r in replies) body.AddRange(r);
return Reply(0x0A, generalStatus, [.. body]);
}
/// <summary>The default responder: per-service replies, split and re-packed for an MSP.</summary>
public Task<byte[]> DefaultRespond(byte[] request)
{
if (request[0] != 0x0A) return Task.FromResult(RespondPerService(request));
var replies = SplitMsp(request).Select(RespondPerService).ToList();
var anyFailed = replies.Any(r => r[2] != 0);
return Task.FromResult(MspReply(replies, anyFailed ? (byte)0x1E : (byte)0x00));
}
}