using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Decodes @udt/<id> template buffers captured byte for byte off a real controller — /// a CompactLogix 5069-L320ERS2 rev 34.11, read through libplctag 1.5.2's Tag.GetBuffer() /// on 2026-10-07 (read-only; one template read per id). /// /// /// /// Every pre-existing template test built its own input in the layout the decoder assumed /// (u16 member count first, a 12-byte header, [info][array][offset] blocks, names /// each ending ;\0). A real buffer agrees with none of that, so the decoder returned /// null for every UDT a controller has and discovered UDTs never fanned out. The /// same builder-shares-the-parser's-assumption trap as #468. /// /// /// What libplctag actually hands back for @udt/id (cross-checked against its own /// list_tags_logix example): a 14-byte header — u16 template id, u32 member-definition /// size in 32-bit words, u32 instance size in bytes, u16 member count, u16 struct handle — /// then one 8-byte block per member: u16 info (array length, or the bit number of a /// BOOL), u16 type (bit 15 = struct, bit 13 = array, low 12 bits = CIP type code or /// the nested template id), u32 byte offset. Then the strings: the type name ending at /// ; (followed by an encoded tail up to a NUL), then each member name NUL-terminated. /// /// [Trait("Category", "Unit")] public sealed class CipTemplateObjectWireCaptureTests { /// The built-in STRING template (id 0x0FCE): LEN DINT + DATA SINT[82], 88 bytes. internal const string StringTemplate = "CE0F10000000580000000200CE0F0000C400000000005200C22004000000" + "535452494E473B6E45414941004C454E004441544100000001000200"; /// /// Rockwell library UDT raC_UDT_LookupMember_STR0082 (id 3386): DINT, a nested /// STRING member (struct type 0x8FCE), SINT, BOOL. Trailing bytes after the last name are /// part of the capture and must be ignored. /// internal const string LookupMemberTemplate = "3A0D1F000000600000000400B3810000C400000000000000CE8F040000000000C2005C0000000000C1005C000000" + "7261435F5544545F4C6F6F6B75704D656D6265725F535452303038323B6E45414141454245430043" + "6F64650044657363004461746154797065004C6F636B00008F92361DA1314013"; private static AbCipUdtShape DecodeHex(string hex) { var shape = CipTemplateObjectDecoder.Decode(Convert.FromHexString(hex)); shape.ShouldNotBeNull(); return shape; } /// The STRING template decodes to its two members with the real array length. [Fact] public void Captured_STRING_template_decodes_LEN_and_DATA() { var shape = DecodeHex(StringTemplate); shape.TypeName.ShouldBe("STRING"); shape.TotalSize.ShouldBe(88); shape.Members.Select(m => m.Name).ShouldBe(["LEN", "DATA"]); shape.Members[0].DataType.ShouldBe(AbCipDataType.DInt); shape.Members[0].Offset.ShouldBe(0); shape.Members[0].ArrayLength.ShouldBe(1); shape.Members[1].DataType.ShouldBe(AbCipDataType.SInt); shape.Members[1].Offset.ShouldBe(4); shape.Members[1].ArrayLength.ShouldBe(82); } /// A nested struct member carries its template id; trailing bytes are ignored. [Fact] public void Captured_library_UDT_decodes_nested_STRING_member_and_ignores_trailing_bytes() { var shape = DecodeHex(LookupMemberTemplate); shape.TypeName.ShouldBe("raC_UDT_LookupMember_STR0082"); shape.TotalSize.ShouldBe(96); shape.Members.Select(m => m.Name).ShouldBe(["Code", "Desc", "DataType", "Lock"]); shape.Members[0].DataType.ShouldBe(AbCipDataType.DInt); shape.Members[1].DataType.ShouldBe(AbCipDataType.Structure); shape.Members[1].NestedTemplateId.ShouldBe(0x0FCEu); shape.Members[1].Offset.ShouldBe(4); shape.Members[2].DataType.ShouldBe(AbCipDataType.SInt); shape.Members[2].Offset.ShouldBe(92); shape.Members[3].DataType.ShouldBe(AbCipDataType.Bool); shape.Members.ShouldAllBe(m => m.ArrayLength == 1); } /// A shape is recognised as a Logix string family (LEN DINT + DATA SINT[n]). [Fact] public void Captured_STRING_template_is_a_string_family_shape_and_a_plain_UDT_is_not() { DecodeHex(StringTemplate).IsStringFamily.ShouldBeTrue(); DecodeHex(LookupMemberTemplate).IsStringFamily.ShouldBeFalse(); } }