129 lines
5.2 KiB
C#
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));
|
|
}
|
|
}
|