Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTemplateReaderTests.cs
T

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);
}
}