132 lines
6.4 KiB
C#
132 lines
6.4 KiB
C#
using System.Buffers.Binary;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
/// <summary>
|
|
/// Task 13: the native template reader over <see cref="FakeCipChannel"/>, serving the real
|
|
/// controller captures (rev 34.11) as Read Template reply data. The native buffer must decode
|
|
/// identically to the captured libplctag buffer.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class NativeAbCipTemplateReaderTests
|
|
{
|
|
private static readonly AbCipTagCreateParams Params = new(
|
|
Gateway: "10.0.0.5", Port: 44818, CipPath: "1,0", LibplctagPlcAttribute: "controllogix",
|
|
TagName: "@udt", Timeout: TimeSpan.FromSeconds(2));
|
|
|
|
/// <summary>Builds a channel serving <paramref name="captured"/> (header + raw) the way a controller would.</summary>
|
|
private static FakeCipChannel ServeTemplate(byte[] captured, int maxRequestBytes, int? statusOverrideAtFirst = null)
|
|
{
|
|
var id = BinaryPrimitives.ReadUInt16LittleEndian(captured);
|
|
var defWords = BinaryPrimitives.ReadUInt32LittleEndian(captured.AsSpan(2));
|
|
var instBytes = BinaryPrimitives.ReadUInt32LittleEndian(captured.AsSpan(6));
|
|
var members = BinaryPrimitives.ReadUInt16LittleEndian(captured.AsSpan(10));
|
|
var handle = BinaryPrimitives.ReadUInt16LittleEndian(captured.AsSpan(12));
|
|
var raw = captured.AsSpan(14).ToArray();
|
|
var total = (int)(defWords * 4 - 23);
|
|
|
|
var ch = new FakeCipChannel(maxRequestBytes);
|
|
ch.RespondPerService = req =>
|
|
{
|
|
if (req[0] == 0x03)
|
|
{
|
|
var d = new byte[30];
|
|
var w = d.AsSpan();
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w, 4);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w[2..], 4); BinaryPrimitives.WriteUInt16LittleEndian(w[4..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(w[6..], defWords);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w[10..], 5); BinaryPrimitives.WriteUInt16LittleEndian(w[12..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(w[14..], instBytes);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w[18..], 2); BinaryPrimitives.WriteUInt16LittleEndian(w[20..], 0);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w[22..], members);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w[24..], 1); BinaryPrimitives.WriteUInt16LittleEndian(w[26..], 0);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(w[28..], handle);
|
|
return FakeCipChannel.Reply(0x03, 0, d);
|
|
}
|
|
req[0].ShouldBe((byte)0x4C);
|
|
var offset = (int)BinaryPrimitives.ReadUInt32LittleEndian(req.AsSpan(8));
|
|
var count = BinaryPrimitives.ReadUInt16LittleEndian(req.AsSpan(12));
|
|
count.ShouldBeLessThanOrEqualTo((ushort)(maxRequestBytes - 16));
|
|
var n = Math.Min(count, total - offset);
|
|
var done = offset + n >= total;
|
|
return FakeCipChannel.Reply(0x4C, done ? (byte)0x00 : (byte)0x06, raw[offset..(offset + n)]);
|
|
};
|
|
return ch;
|
|
}
|
|
|
|
private static async Task<byte[]> ReadNativeAsync(FakeCipChannel ch, ushort id)
|
|
{
|
|
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
|
var released = 0;
|
|
using var reader = new NativeAbCipTemplateReader(_ =>
|
|
(sched, ch, () => { released++; return ValueTask.CompletedTask; }));
|
|
var buf = await reader.ReadAsync(Params, id, TestContext.Current.CancellationToken);
|
|
released.ShouldBe(1);
|
|
return buf;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(CipTemplateObjectWireCaptureTests.StringTemplate, 504)]
|
|
[InlineData(CipTemplateObjectWireCaptureTests.StringTemplate, 40)]
|
|
[InlineData(CipTemplateObjectWireCaptureTests.LookupMemberTemplate, 504)]
|
|
[InlineData(CipTemplateObjectWireCaptureTests.LookupMemberTemplate, 40)]
|
|
public async Task Native_buffer_decodes_identically_to_the_captured_libplctag_buffer(string hex, int maxRequest)
|
|
{
|
|
var captured = Convert.FromHexString(hex);
|
|
var ch = ServeTemplate(captured, maxRequest);
|
|
var id = BinaryPrimitives.ReadUInt16LittleEndian(captured);
|
|
|
|
var native = await ReadNativeAsync(ch, id);
|
|
|
|
var expected = CipTemplateObjectDecoder.Decode(captured);
|
|
var actual = CipTemplateObjectDecoder.Decode(native);
|
|
expected.ShouldNotBeNull();
|
|
actual.ShouldNotBeNull();
|
|
actual.TypeName.ShouldBe(expected.TypeName);
|
|
actual.TotalSize.ShouldBe(expected.TotalSize);
|
|
actual.Members.Count.ShouldBe(expected.Members.Count);
|
|
for (var i = 0; i < expected.Members.Count; i++)
|
|
{
|
|
actual.Members[i].Name.ShouldBe(expected.Members[i].Name);
|
|
actual.Members[i].DataType.ShouldBe(expected.Members[i].DataType);
|
|
actual.Members[i].Offset.ShouldBe(expected.Members[i].Offset);
|
|
actual.Members[i].ArrayLength.ShouldBe(expected.Members[i].ArrayLength);
|
|
}
|
|
// The 14-byte header is byte for byte the libplctag one.
|
|
native.AsSpan(0, 14).SequenceEqual(captured.AsSpan(0, 14)).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_small_request_limit_forces_several_chunks_each_within_the_limit()
|
|
{
|
|
var captured = Convert.FromHexString(CipTemplateObjectWireCaptureTests.LookupMemberTemplate);
|
|
var ch = ServeTemplate(captured, 40);
|
|
|
|
await ReadNativeAsync(ch, 3386);
|
|
|
|
ch.Sent.Count(r => r[0] == 0x4C).ShouldBeGreaterThan(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_failing_read_template_status_throws_and_still_releases_the_lease()
|
|
{
|
|
var captured = Convert.FromHexString(CipTemplateObjectWireCaptureTests.StringTemplate);
|
|
var ch = ServeTemplate(captured, 504);
|
|
var inner = ch.RespondPerService;
|
|
ch.RespondPerService = req => req[0] == 0x4C ? FakeCipChannel.Reply(0x4C, 0x05, []) : inner(req);
|
|
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
|
var released = 0;
|
|
using var reader = new NativeAbCipTemplateReader(_ =>
|
|
(sched, ch, () => { released++; return ValueTask.CompletedTask; }));
|
|
|
|
await Should.ThrowAsync<AbTransportException>(() =>
|
|
reader.ReadAsync(Params, 0x0FCE, TestContext.Current.CancellationToken));
|
|
released.ShouldBe(1);
|
|
}
|
|
}
|