181 lines
6.7 KiB
C#
181 lines
6.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 Object blob — header + member blocks + semicolon-delimited
|
|
/// strings (UDT name first, then member names).
|
|
/// </summary>
|
|
private static byte[] BuildTemplate(
|
|
string udtName,
|
|
uint instanceSize,
|
|
params (string name, ushort info, ushort arraySize, uint offset)[] members)
|
|
{
|
|
var memberCount = (ushort)members.Length;
|
|
var headerSize = 12;
|
|
var memberBlockSize = 8;
|
|
var blocksSize = memberBlockSize * members.Length;
|
|
|
|
var stringsBuf = new MemoryStream();
|
|
void AppendString(string s)
|
|
{
|
|
var bytes = Encoding.ASCII.GetBytes(s + ";\0");
|
|
stringsBuf.Write(bytes, 0, bytes.Length);
|
|
}
|
|
AppendString(udtName);
|
|
foreach (var m in members) AppendString(m.name);
|
|
var strings = stringsBuf.ToArray();
|
|
|
|
var buf = new byte[headerSize + blocksSize + strings.Length];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), memberCount);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(2), 0x1234);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), instanceSize);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(8), 0);
|
|
|
|
for (var i = 0; i < members.Length; i++)
|
|
{
|
|
var o = headerSize + (i * memberBlockSize);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), members[i].info);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o + 2), members[i].arraySize);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), members[i].offset);
|
|
}
|
|
Buffer.BlockCopy(strings, 0, buf, headerSize + blocksSize, strings.Length);
|
|
return buf;
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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]);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[Fact]
|
|
public void Zero_member_count_returns_null()
|
|
{
|
|
var buf = new byte[12];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), 0);
|
|
CipTemplateObjectDecoder.Decode(buf).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Short_buffer_returns_null()
|
|
{
|
|
CipTemplateObjectDecoder.Decode([0x01, 0x00]).ShouldBeNull(); // only 2 bytes — less than header
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_member_name_surfaces_placeholder()
|
|
{
|
|
// Header says 3 members but strings list has only UDT name + 2 member names.
|
|
var memberCount = (ushort)3;
|
|
var buf = new byte[12 + 8 * 3];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), memberCount);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), 12);
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
var o = 12 + i * 8;
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), 0xC4);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), (uint)(i * 4));
|
|
}
|
|
// strings: only UDT + 2 members, missing the third.
|
|
var strings = Encoding.ASCII.GetBytes("MyUdt;\0A;\0B;\0");
|
|
var combined = buf.Concat(strings).ToArray();
|
|
|
|
var shape = CipTemplateObjectDecoder.Decode(combined);
|
|
shape.ShouldNotBeNull();
|
|
shape.Members.Count.ShouldBe(3);
|
|
shape.Members[2].Name.ShouldBe("<member_2>");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Foo;\0Bar;\0", new[] { "Foo", "Bar" })]
|
|
[InlineData("Foo;Bar;", new[] { "Foo", "Bar" })] // no nulls
|
|
[InlineData("Only;\0", new[] { "Only" })]
|
|
[InlineData(";\0", new string[] { })] // empty
|
|
[InlineData("", new string[] { })]
|
|
public void ParseSemicolonTerminatedStrings_handles_shapes(string input, string[] expected)
|
|
{
|
|
var bytes = Encoding.ASCII.GetBytes(input);
|
|
var result = CipTemplateObjectDecoder.ParseSemicolonTerminatedStrings(bytes);
|
|
result.ShouldBe(expected);
|
|
}
|
|
}
|