using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Decodes a captured @tags Symbol Object response, byte for byte as libplctag handed /// it back — the evidence for the entry layout, rather than another statement of it. /// /// /// /// Every other decoder test builds its own input, so the builder and the decoder can share /// a wrong assumption and still agree. They did: both padded each entry to an even length, /// and a controller pads nothing (Gitea #468). /// /// /// This capture came off the AbCip integration fixture — libplctag's ab_server, /// patched to serve the Symbol Object listing (see the fixture's /// Docker/patches/) — and the same server's replies are read correctly by /// libplctag's own list_tags_logix client. That client is the reference /// implementation exercised against real Logix hardware, so its agreement is what makes /// this sample evidence about controllers and not merely about our fixture. /// /// /// Re-capture with plc_tag_create("…&name=@tags") against the fixture and dump /// plc_tag_get_uint8 across plc_tag_get_size if the fixture's tag set changes. /// /// [Trait("Category", "Unit")] public sealed class CipSymbolObjectWireCaptureTests { /// /// The nine tags the controllogix fixture profile serves, in wire order. 281 bytes. /// private const string CapturedTagsReply = "01000000C120010001000000000000000000000009004D6F746F725F52756E" + "02000000C42004000100000000000000000000000B004C696E65315F436F756E74" + "03000000CA2004000100000000000000000000000C0050756D705F315F5370656564" + "04000000C42004001000000000000000000000000900546573744172726179" + "05000000D02058000100000000000000000000000A0054657374537472696E67" + "06000000C220010001000000000000000000000008005465737453494E54" + "07000000C1200100010000000000000000000000080054657374424F4F4C" + "08000000CA2004000100000000000000000000000800546573745245414C" + "09000000C420040001000000000000000000000008005465737444494E54"; private static byte[] Capture() => Convert.FromHexString(CapturedTagsReply); /// The capture is the length the entry layout predicts — no padding anywhere. /// /// Nine entries of 22 header bytes plus their name lengths /// (9 + 11 + 12 + 9 + 10 + 8 + 8 + 8 + 8 = 83) come to 281. An even-padded stream would be /// 285, so the length alone falsifies the old assumption. /// [Fact] public void The_capture_is_packed_with_no_padding_between_entries() { Capture().Length.ShouldBe((9 * 22) + 83); } /// Every tag the fixture serves comes back, in wire order. /// /// The first entry's name is 9 bytes, so the second entry begins at offset 31 — an odd /// offset. Under the old even-alignment step the decoder read Motor_Run and then /// lost all eight tags behind it. /// [Fact] public void Every_captured_symbol_decodes_in_wire_order() { var tags = CipSymbolObjectDecoder.Decode(Capture()).ToList(); tags.Select(t => t.Name).ShouldBe([ "Motor_Run", "Line1_Count", "Pump_1_Speed", "TestArray", "TestString", "TestSINT", "TestBOOL", "TestREAL", "TestDINT", ]); } /// Types survive the array-dimension bits a real symbol type carries. /// /// Each captured symbol type is 0x20xx, not 0x00xx: bits 13-14 hold the array /// dimension count. Only the low 12 bits are the CIP type code, and reading the word whole /// would classify every one of these as an unknown type. /// [Fact] public void Captured_symbol_types_map_through_the_dimension_bits() { var tags = CipSymbolObjectDecoder.Decode(Capture()).ToList(); tags.Single(t => t.Name == "Motor_Run").DataType.ShouldBe(AbCipDataType.Bool); tags.Single(t => t.Name == "Line1_Count").DataType.ShouldBe(AbCipDataType.DInt); tags.Single(t => t.Name == "Pump_1_Speed").DataType.ShouldBe(AbCipDataType.Real); tags.Single(t => t.Name == "TestString").DataType.ShouldBe(AbCipDataType.String); tags.Single(t => t.Name == "TestSINT").DataType.ShouldBe(AbCipDataType.SInt); } /// None of the captured symbols is misread as a system tag or a structure. /// /// The dimension bits sit next to the flags the decoder tests for, so a mask that is one /// bit wide in the wrong place would show up as a plain DINT being reported as a UDT — and /// a UDT is fanned out into member tags, so the error would not stay quiet. /// [Fact] public void No_captured_symbol_is_misread_as_a_system_tag_or_a_structure() { var tags = CipSymbolObjectDecoder.Decode(Capture()).ToList(); tags.ShouldAllBe(t => !t.IsSystemTag); tags.ShouldAllBe(t => t.DataType != AbCipDataType.Structure); tags.ShouldAllBe(t => t.TemplateInstanceId == null); } }