using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Symbol Object (@tags) entries captured byte for byte off two real CompactLogix /// controllers on 2026-10-07 (read-only, one listing read each): a 5069-L320ERS2 rev 34.11 press /// and a second CompactLogix. Unlike the ab_server capture in /// — which reports every scalar as a 1-element /// array — a real controller sets the dimension bits only on genuine arrays. /// [Trait("Category", "Unit")] public sealed class CipSymbolObjectRealControllerCaptureTests { // Controller scope, press: WW_InCycle (BOOL), Inf_LookupFwkB (UDT[23], template 3386), // Program:UN01_Press (the program entry itself, type 0x1068). internal const string WwInCycle = "AF010000C10001000000000000000000000000000A0057575F496E4379636C65"; internal const string InfLookupFwkB = "6D0000003AAD60001700000000000000000000000E00496E665F4C6F6F6B757046776B42"; internal const string ProgramUn01 = "7900000068100000000000000000000000000000120050726F6772616D3A554E30315F5072657373"; // Controller scope, second controller: arrSysTime (DINT[7]), arrMessages (STRING[50]). internal const string ArrSysTime = "11000000C42004000700000000000000000000000A0061727253797354696D65"; internal const string ArrMessages = "61000000CEAF58003200000000000000000000000B006172724D65737361676573"; // Program:UN01_Press.@tags: names arrive WITHOUT the Program: prefix. internal const string OutEState = "1A000000C40004000000000000000000000000000A004F75745F655374617465"; internal const string DisableStates = "4C000000C42004002000000000000000000000000E005F44697361626C65537461746573"; private static List Decode(params string[] entries) => CipSymbolObjectDecoder.Decode(Convert.FromHexString(string.Concat(entries))).ToList(); /// Array dimensions in the entry surface as IsArray + ElementCount; scalars stay scalar. [Fact] public void Real_controller_arrays_surface_their_element_count() { var tags = Decode(WwInCycle, InfLookupFwkB, ArrSysTime, ArrMessages); var inCycle = tags.Single(t => t.Name == "WW_InCycle"); inCycle.IsArray.ShouldBeFalse(); inCycle.ElementCount.ShouldBe(1); var sysTime = tags.Single(t => t.Name == "arrSysTime"); sysTime.DataType.ShouldBe(AbCipDataType.DInt); sysTime.IsArray.ShouldBeTrue(); sysTime.ElementCount.ShouldBe(7); var lookup = tags.Single(t => t.Name == "Inf_LookupFwkB"); lookup.DataType.ShouldBe(AbCipDataType.Structure); lookup.TemplateInstanceId.ShouldBe(3386u); lookup.IsArray.ShouldBeTrue(); lookup.ElementCount.ShouldBe(23); var messages = tags.Single(t => t.Name == "arrMessages"); messages.TemplateInstanceId.ShouldBe(0x0FCEu); messages.IsArray.ShouldBeTrue(); messages.ElementCount.ShouldBe(50); } /// A program entry is a system tag whose program name the enumerator can walk. [Fact] public void Program_entry_is_system_and_names_its_program() { var program = Decode(ProgramUn01).ShouldHaveSingleItem(); program.IsSystemTag.ShouldBeTrue(); CipSymbolObjectDecoder.ProgramNameOf(program).ShouldBe("UN01_Press"); CipSymbolObjectDecoder.ProgramNameOf(Decode(WwInCycle).Single()).ShouldBeNull(); } /// A program listing decodes with the program as each tag's scope. [Fact] public void Program_listing_decodes_with_program_scope() { var tags = CipSymbolObjectDecoder.Decode( Convert.FromHexString(OutEState + DisableStates), programScope: "UN01_Press").ToList(); tags.Select(t => t.Name).ShouldBe(["Out_eState", "_DisableStates"]); tags.ShouldAllBe(t => t.ProgramScope == "UN01_Press"); tags[0].DataType.ShouldBe(AbCipDataType.DInt); tags[1].IsArray.ShouldBeTrue(); tags[1].ElementCount.ShouldBe(32); } }