3b48e05e9a
Live read-only check against production CompactLogix 5069-L320ERS2 rev 34.11 presses (Z-35162 / 10.209.70.160): every @udt/<id> template the controller returned decoded to null, so controller-discovered UDTs never fanned out. The decoder assumed a 12-byte header led by the member count and [info][array][offset] blocks with ';'-terminated names; libplctag actually returns a 14-byte header (u16 id, u32 def words, u32 instance size, u16 member count, u16 handle), [info][type][offset] blocks, and NUL-terminated names after a 'Name;<tail>' type name. All three test builders restated the old assumption, so the unit suite agreed with itself (the #468 trap again); they now share a TemplateBufferBuilder pinned by real captures (CipTemplateObjectWireCaptureTests). Discovery follow-ons that the working decoder exposed: - STRING (and user string types, LEN DINT + DATA SINT[n]) browse as one String variable, top-level and nested, not as a LEN/DATA folder. - Controller-generated members (ZZZZZZZZZZ* BOOL hosts, __* AOI backing) are not browsed. - A UDT array is not fanned out into element-free Tag.Member paths.
180 lines
7.7 KiB
C#
180 lines
7.7 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Construct a template buffer in libplctag's real layout (see <see cref="TemplateBufferBuilder"/>).
|
|
/// <c>info</c> here is the member's CIP type word; <c>arraySize</c> > 0 marks an array member.
|
|
/// </summary>
|
|
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))]);
|
|
|
|
/// <summary>Verifies that a simple UDT with two members decodes correctly.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that the struct member flag is decoded as Structure type.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <c>NestedTemplateId</c> 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.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>Verifies that array members carry correct non-one array lengths.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that multiple atomic types preserve their offsets and type information.</summary>
|
|
[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]);
|
|
}
|
|
|
|
/// <summary>Verifies that unknown atomic type codes fall back to Structure type.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that zero member count returns null.</summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>Verifies that a short buffer returns null.</summary>
|
|
[Fact]
|
|
public void Short_buffer_returns_null()
|
|
{
|
|
CipTemplateObjectDecoder.Decode([0x01, 0x00]).ShouldBeNull(); // only 2 bytes — less than header
|
|
}
|
|
|
|
/// <summary>Verifies that missing member names surface a placeholder.</summary>
|
|
[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("<member_2>");
|
|
}
|
|
|
|
/// <summary>Verifies the template string section splits into the type name + NUL-terminated member names.</summary>
|
|
/// <param name="input">The raw string section.</param>
|
|
/// <param name="memberCount">Member names to read after the type name.</param>
|
|
/// <param name="expected">The expected type name followed by member names.</param>
|
|
[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);
|
|
}
|
|
}
|