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(); 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(() => SymbolObjectServices.ParseListingReply(new CipReply(0x55, 0x04, [], ReadOnlyMemory.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(() => SymbolObjectServices.ParseListingReply(new CipReply(0x55, 0x06, [], ReadOnlyMemory.Empty))) .Status.ShouldBe(AbStatus.BadReply); }