using System.Buffers.Binary; using System.Text; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class CipTemplateObjectDecoderTests { /// /// Construct a template buffer in libplctag's real layout (see ). /// info here is the member's CIP type word; arraySize > 0 marks an array member. /// private static byte[] BuildTemplate( string udtName, uint instanceSize, params (string name, ushort info, ushort arraySize, uint offset)[] members) => TemplateBufferBuilder.Build(udtName, instanceSize, [.. members.Select(m => (m.name, m.info, m.arraySize, m.offset))]); /// Verifies that a simple UDT with two members decodes correctly. [Fact] public void Simple_two_member_UDT_decodes_correctly() { var bytes = BuildTemplate("MotorUdt", instanceSize: 8, ("Speed", info: 0xC4, arraySize: 0, offset: 0), // DINT at offset 0 ("Enabled", info: 0xC1, arraySize: 0, offset: 4)); // BOOL at offset 4 var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.TypeName.ShouldBe("MotorUdt"); shape.TotalSize.ShouldBe(8); shape.Members.Count.ShouldBe(2); shape.Members[0].Name.ShouldBe("Speed"); shape.Members[0].DataType.ShouldBe(AbCipDataType.DInt); shape.Members[0].Offset.ShouldBe(0); shape.Members[0].ArrayLength.ShouldBe(1); shape.Members[1].Name.ShouldBe("Enabled"); shape.Members[1].DataType.ShouldBe(AbCipDataType.Bool); shape.Members[1].Offset.ShouldBe(4); } /// Verifies that the struct member flag is decoded as Structure type. [Fact] public void Struct_member_flag_surfaces_Structure_type() { var bytes = BuildTemplate("ContainerUdt", instanceSize: 32, ("InnerStruct", info: 0x8042, arraySize: 0, offset: 0)); // struct flag + template-id 0x42 var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure); } /// /// A struct member's lower 12 bits carry the nested UDT's template instance id (same /// encoding as the Symbol Object); the decoder captures it as NestedTemplateId so the /// nested shape can be fetched. A scalar member carries no nested id. Uses the FULL 12-bit /// mask (0x123 here exceeds a byte) to prove the id is not byte-truncated. /// [Fact] public void Struct_member_captures_nested_template_id_scalar_member_does_not() { var bytes = BuildTemplate("ContainerUdt", instanceSize: 8, ("Inner", info: (ushort)(0x8000 | 0x123), arraySize: 0, offset: 0), // struct + nested id 0x123 ("Count", info: 0xC4, arraySize: 0, offset: 4)); // scalar DINT, no nested id var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Count.ShouldBe(2); shape.Members[0].Name.ShouldBe("Inner"); shape.Members[0].DataType.ShouldBe(AbCipDataType.Structure); shape.Members[0].NestedTemplateId.ShouldBe(0x123u); // full 12-bit id, not byte-truncated shape.Members[1].Name.ShouldBe("Count"); shape.Members[1].DataType.ShouldBe(AbCipDataType.DInt); shape.Members[1].NestedTemplateId.ShouldBeNull(); } /// Verifies that array members carry correct non-one array lengths. [Fact] public void Array_member_carries_non_one_ArrayLength() { var bytes = BuildTemplate("ArrayUdt", instanceSize: 40, ("Values", info: 0xC4, arraySize: 10, offset: 0)); var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Single().ArrayLength.ShouldBe(10); } /// Verifies that multiple atomic types preserve their offsets and type information. [Fact] public void Multiple_atomic_types_preserve_offsets_and_types() { var bytes = BuildTemplate("MixedUdt", instanceSize: 24, ("A", 0xC1, 0, 0), // BOOL ("B", 0xC2, 0, 1), // SINT ("C", 0xC3, 0, 2), // INT ("D", 0xC4, 0, 4), // DINT ("E", 0xCA, 0, 8), // REAL ("F", 0xCB, 0, 16)); // LREAL var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Count.ShouldBe(6); shape.Members.Select(m => m.DataType).ShouldBe( [AbCipDataType.Bool, AbCipDataType.SInt, AbCipDataType.Int, AbCipDataType.DInt, AbCipDataType.Real, AbCipDataType.LReal]); shape.Members.Select(m => m.Offset).ShouldBe([0, 1, 2, 4, 8, 16]); } /// Verifies that unknown atomic type codes fall back to Structure type. [Fact] public void Unknown_atomic_type_code_falls_back_to_Structure() { var bytes = BuildTemplate("WeirdUdt", instanceSize: 4, ("Unknown", info: 0xFF, 0, 0)); var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure); } /// Verifies that zero member count returns null. [Fact] public void Zero_member_count_returns_null() { var buf = new byte[14]; BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(6), 8); // instance size BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(10), 0); // member count CipTemplateObjectDecoder.Decode(buf).ShouldBeNull(); } /// Verifies that a short buffer returns null. [Fact] public void Short_buffer_returns_null() { CipTemplateObjectDecoder.Decode([0x01, 0x00]).ShouldBeNull(); // only 2 bytes — less than header } /// Verifies that missing member names surface a placeholder. [Fact] public void Missing_member_name_surfaces_placeholder() { // Header says 3 members but strings list has only UDT name + 2 member names. var full = TemplateBufferBuilder.Build("MyUdt", 12, ("A", 0xC4, 0, 0), ("B", 0xC4, 0, 4), ("C", 0xC4, 0, 8)); // Drop the trailing "C\0" so only two member names remain. var combined = full[..^2]; var shape = CipTemplateObjectDecoder.Decode(combined); shape.ShouldNotBeNull(); shape.Members.Count.ShouldBe(3); shape.Members[2].Name.ShouldBe(""); } /// Verifies the template string section splits into the type name + NUL-terminated member names. /// The raw string section. /// Member names to read after the type name. /// The expected type name followed by member names. [Theory] [InlineData("STRING;nEAIA\0LEN\0DATA\0", 2, new[] { "STRING", "LEN", "DATA" })] [InlineData("Udt;n\0A\0B\0\0\u0001junk", 2, new[] { "Udt", "A", "B" })] // trailing bytes ignored [InlineData("Udt\0A\0", 1, new[] { "Udt", "A" })] // no ';' tail [InlineData("Udt;nEA\0A\0", 3, new[] { "Udt", "A" })] // fewer names than members [InlineData("", 1, new string[] { })] public void ParseTemplateStrings_handles_shapes(string input, int memberCount, string[] expected) { var bytes = Encoding.ASCII.GetBytes(input); var result = CipTemplateObjectDecoder.ParseTemplateStrings(bytes, memberCount); result.ShouldBe(expected); } }