Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Logix/SymbolObjectServicesTests.cs
T
Joseph Doherty be421a4cb0 fix(abcip): native browse paging/pacing/template lease (#572); differential equal-refusal + budget isolation (#575)
#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
2026-10-07 22:59:42 -04:00

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