178 lines
8.7 KiB
C#
178 lines
8.7 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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_template_read_queued_behind_a_small_read_on_a_packing_scheduler_goes_out_alone()
|
|
{
|
|
var captured = Convert.FromHexString(CipTemplateObjectWireCaptureTests.LookupMemberTemplate);
|
|
var ch = ServeTemplate(captured, 504);
|
|
ch.Gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var served = ch.RespondPerService;
|
|
ch.RespondPerService = r => r[0] == 0x0E ? FakeCipChannel.OkReply(r) : served(r);
|
|
await using var sched = new RequestScheduler(ch, allowPacking: true);
|
|
var ct = TestContext.Current.CancellationToken;
|
|
using var reader = new NativeAbCipTemplateReader(_ => (sched, ch, () => ValueTask.CompletedTask));
|
|
|
|
// A is in flight (gated); B queues behind it; the template read then queues behind B.
|
|
var small = new byte[] { 0x0E, 0x02, 0x20, 0x01, 0x24, 0x01 }; // Get Attribute Single
|
|
var a = sched.SendAsync(small, ct);
|
|
await ch.WaitForSendsAsync(1);
|
|
var b = sched.SendAsync(small, ct);
|
|
var read = reader.ReadAsync(Params, 3386, ct);
|
|
await Task.Delay(200, ct);
|
|
ch.Gate.SetResult();
|
|
await Task.WhenAll(a, b);
|
|
await read;
|
|
|
|
// Packing was live (the attribute query shared an MSP with B), yet no Read Template rode in one.
|
|
ch.Sent.ShouldContain(r => r[0] == 0x0A);
|
|
foreach (var msp in ch.Sent.Where(r => r[0] == 0x0A))
|
|
FakeCipChannel.SplitMsp(msp).ShouldNotContain(e => e[0] == 0x4C);
|
|
ch.Sent.ShouldContain(r => r[0] == 0x4C); // the Read Template requests went out bare
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_template_size_over_the_bound_is_rejected()
|
|
{
|
|
var captured = Convert.FromHexString(CipTemplateObjectWireCaptureTests.StringTemplate);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(captured.AsSpan(2), 0x10000000);
|
|
var ch = ServeTemplate(captured, 504);
|
|
await using var sched = new RequestScheduler(ch, allowPacking: false);
|
|
using var reader = new NativeAbCipTemplateReader(_ => (sched, ch, () => ValueTask.CompletedTask));
|
|
|
|
var ex = await Should.ThrowAsync<AbTransportException>(
|
|
() => reader.ReadAsync(Params, 0x0FCE, TestContext.Current.CancellationToken));
|
|
|
|
ex.Status.ShouldBe(AbStatus.BadReply);
|
|
ch.Sent.ShouldNotContain(r => r[0] == 0x4C);
|
|
}
|
|
}
|