be421a4cb0
#572: listing continues at max(instance in page)+1 (no ascending-order assumption, no-progress guard kept); browsePageDelayMs (default 5 ms) paces pages; DiscoverAsync reads templates through one batch reader (IAbCipTemplateReaderFactory.CreateBatch) holding one lease. #575: differential harness reports a same-status template refusal as an equal refusal; offline test pins the open-attempt budget without relying on the abort ending the phase. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
86 lines
3.7 KiB
C#
86 lines
3.7 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Logix;
|
|
|
|
public sealed class SymbolObjectServicesTests
|
|
{
|
|
private static readonly string[] ExpectedNames =
|
|
["Motor_Run", "Line1_Count", "Pump_1_Speed", "TestArray", "TestString", "TestSINT", "TestBOOL", "TestREAL", "TestDINT"];
|
|
|
|
[Fact]
|
|
public void Request_matches_capture()
|
|
{
|
|
// libplctag requests attributes 2,7,8,1 in that order -- same as the brief; no change needed.
|
|
var cap = CaptureFile.CipPayload(CaptureFile.Load("logix-tags-listing.hex").First(f => f.FromClient));
|
|
SymbolObjectServices.GetInstanceAttributeList(null, 0).ShouldBe(cap);
|
|
}
|
|
|
|
[Fact]
|
|
public void Request_with_program_scope_and_start_instance() =>
|
|
Convert.ToHexString(SymbolObjectServices.GetInstanceAttributeList("Main", 0x0102)).ShouldBe(
|
|
"550A" + "910C" + Convert.ToHexString(Encoding.ASCII.GetBytes("Program:Main"))
|
|
+ "206B2500" + "0201" + "0400" + "0200" + "0700" + "0800" + "0100");
|
|
|
|
[Fact]
|
|
public void Captured_reply_entries_have_the_decoder_layout()
|
|
{
|
|
var reply = CipReply.Parse(CaptureFile.CipPayload(CaptureFile.Load("logix-tags-listing.hex").First(f => !f.FromClient)));
|
|
var (entries, last, more) = SymbolObjectServices.ParseListingReply(reply);
|
|
more.ShouldBeFalse();
|
|
last.ShouldBe(9u);
|
|
|
|
var names = new List<string>();
|
|
var pos = 0;
|
|
uint expectedInstance = 1;
|
|
while (pos < entries.Length)
|
|
{
|
|
BinaryPrimitives.ReadUInt32LittleEndian(entries.AsSpan(pos)).ShouldBe(expectedInstance++);
|
|
var nameLen = BinaryPrimitives.ReadUInt16LittleEndian(entries.AsSpan(pos + 20));
|
|
names.Add(Encoding.ASCII.GetString(entries, pos + 22, nameLen));
|
|
pos += 22 + nameLen;
|
|
}
|
|
names.ShouldBe(ExpectedNames);
|
|
}
|
|
|
|
[Fact]
|
|
public void More_flag_follows_status_0x06()
|
|
{
|
|
var entry = Convert.FromHexString("05000000" + "C400" + "0400" + "00000000" + "00000000" + "00000000" + "0100" + "41");
|
|
var reply = new CipReply(0x55, 0x06, [], entry);
|
|
var (entries, last, more) = SymbolObjectServices.ParseListingReply(reply);
|
|
more.ShouldBeTrue(); last.ShouldBe(5u); entries.ShouldBe(entry);
|
|
}
|
|
|
|
[Fact]
|
|
public void MaxInstance_is_the_highest_in_the_page_whatever_its_order()
|
|
{
|
|
static string Entry(uint i) => BitConverter.ToString(BitConverter.GetBytes(i)).Replace("-", "")
|
|
+ "C400" + "0400" + "00000000" + "00000000" + "00000000" + "0100" + "41";
|
|
var entries = Convert.FromHexString(Entry(9) + Entry(3) + Entry(5));
|
|
var (_, max, more) = SymbolObjectServices.ParseListingReply(new CipReply(0x55, 0x06, [], entries));
|
|
more.ShouldBeTrue();
|
|
max.ShouldBe(9u);
|
|
}
|
|
|
|
[Fact]
|
|
public void Failure_status_throws() =>
|
|
Should.Throw<AbTransportException>(() =>
|
|
SymbolObjectServices.ParseListingReply(new CipReply(0x55, 0x04, [], ReadOnlyMemory<byte>.Empty)));
|
|
|
|
[Fact]
|
|
public void Start_instance_above_16_bits_uses_32_bit_segment() =>
|
|
Convert.ToHexString(SymbolObjectServices.GetInstanceAttributeList(null, 0x12345)).ShouldBe(
|
|
"5504" + "206B" + "2600" + "45230100" + "0400" + "0200" + "0700" + "0800" + "0100");
|
|
|
|
[Fact]
|
|
public void More_with_no_entries_throws() =>
|
|
Should.Throw<AbTransportException>(() =>
|
|
SymbolObjectServices.ParseListingReply(new CipReply(0x55, 0x06, [], ReadOnlyMemory<byte>.Empty)))
|
|
.Status.ShouldBe(AbStatus.BadReply);
|
|
}
|