using System.Buffers.Binary;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
///
/// Scriptable : settable , records every request
/// and the token it was sent with, and answers via . The default responder
/// answers any service with an empty success reply and splits a Multiple Service Packet into
/// per-service replies via . Reused by Tasks 12 and 13.
///
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;
}
/// When set, reading throws it (simulates a broken channel).
public Exception? MaxRequestBytesThrows { get; set; }
/// Every request, in send order.
public List Sent { get; } = [];
/// The token each request was sent with, parallel to .
public List Tokens { get; } = [];
/// Answers one request with raw CIP reply bytes.
public Func> Respond { get; set; }
/// Answers one embedded (non-MSP) service; used by the default .
public Func RespondPerService { get; set; } = OkReply;
/// When set, every send blocks on it (after being recorded) until the test completes it.
public TaskCompletionSource? Gate { get; set; }
public int SentCount { get { lock (Sent) return Sent.Count; } }
public async Task> SendAsync(ReadOnlyMemory 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);
}
/// Completes once the channel has received requests in total.
public async Task WaitForSendsAsync(int count)
{
while (SentCount < count)
{
if (!await _entered.WaitAsync(TimeSpan.FromSeconds(10)))
throw new TimeoutException($"Expected {count} sends, saw {SentCount}.");
}
}
/// A success reply for with optional data.
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;
}
/// Splits an MSP request into its embedded requests.
public static List 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(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;
}
/// An MSP reply (service 0x8A) carrying the given embedded replies.
public static byte[] MspReply(IReadOnlyList replies, byte generalStatus = 0x00)
{
var body = new List();
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]);
}
/// The default responder: per-service replies, split and re-packed for an MSP.
public Task 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));
}
}