187 lines
6.6 KiB
C#
187 lines
6.6 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 CipSymbolObjectDecoderTests
|
||
{
|
||
/// <summary>
|
||
/// Build one Symbol Object entry in the byte layout
|
||
/// <c>instance_id(u32) symbol_type(u16) element_length(u16) array_dims(u32×3) name_len(u16) name[len] pad</c>.
|
||
/// </summary>
|
||
private static byte[] BuildEntry(
|
||
uint instanceId,
|
||
ushort symbolType,
|
||
ushort elementLength,
|
||
(uint, uint, uint) arrayDims,
|
||
string name)
|
||
{
|
||
var nameBytes = Encoding.ASCII.GetBytes(name);
|
||
var nameLen = nameBytes.Length;
|
||
var totalLen = 22 + nameLen;
|
||
if ((totalLen & 1) != 0) totalLen++; // pad to even
|
||
|
||
var buf = new byte[totalLen];
|
||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(0), instanceId);
|
||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4), symbolType);
|
||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(6), elementLength);
|
||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(8), arrayDims.Item1);
|
||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(12), arrayDims.Item2);
|
||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(16), arrayDims.Item3);
|
||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(20), (ushort)nameLen);
|
||
Buffer.BlockCopy(nameBytes, 0, buf, 22, nameLen);
|
||
return buf;
|
||
}
|
||
|
||
private static byte[] Concat(params byte[][] chunks)
|
||
{
|
||
var total = chunks.Sum(c => c.Length);
|
||
var result = new byte[total];
|
||
var pos = 0;
|
||
foreach (var c in chunks)
|
||
{
|
||
Buffer.BlockCopy(c, 0, result, pos, c.Length);
|
||
pos += c.Length;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
[Fact]
|
||
public void Single_DInt_entry_decodes_to_scalar_DInt_tag()
|
||
{
|
||
var bytes = BuildEntry(
|
||
instanceId: 42,
|
||
symbolType: 0xC4,
|
||
elementLength: 4,
|
||
arrayDims: (0, 0, 0),
|
||
name: "Counter");
|
||
|
||
var tags = CipSymbolObjectDecoder.Decode(bytes).ToList();
|
||
|
||
tags.Count.ShouldBe(1);
|
||
tags[0].Name.ShouldBe("Counter");
|
||
tags[0].ProgramScope.ShouldBeNull();
|
||
tags[0].DataType.ShouldBe(AbCipDataType.DInt);
|
||
tags[0].IsSystemTag.ShouldBeFalse();
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData((byte)0xC1, AbCipDataType.Bool)]
|
||
[InlineData((byte)0xC2, AbCipDataType.SInt)]
|
||
[InlineData((byte)0xC3, AbCipDataType.Int)]
|
||
[InlineData((byte)0xC4, AbCipDataType.DInt)]
|
||
[InlineData((byte)0xC5, AbCipDataType.LInt)]
|
||
[InlineData((byte)0xC6, AbCipDataType.USInt)]
|
||
[InlineData((byte)0xC7, AbCipDataType.UInt)]
|
||
[InlineData((byte)0xC8, AbCipDataType.UDInt)]
|
||
[InlineData((byte)0xC9, AbCipDataType.ULInt)]
|
||
[InlineData((byte)0xCA, AbCipDataType.Real)]
|
||
[InlineData((byte)0xCB, AbCipDataType.LReal)]
|
||
[InlineData((byte)0xD0, AbCipDataType.String)]
|
||
public void Every_known_atomic_type_code_maps_to_correct_AbCipDataType(byte typeCode, AbCipDataType expected)
|
||
{
|
||
CipSymbolObjectDecoder.MapTypeCode(typeCode).ShouldBe(expected);
|
||
}
|
||
|
||
[Fact]
|
||
public void Unknown_type_code_returns_null_so_caller_treats_as_opaque()
|
||
{
|
||
CipSymbolObjectDecoder.MapTypeCode(0xFF).ShouldBeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public void Struct_flag_overrides_type_code_and_yields_Structure()
|
||
{
|
||
// 0x8000 (struct) + 0x1234 (template instance id in lower 12 bits; uses 0x234)
|
||
var bytes = BuildEntry(
|
||
instanceId: 5,
|
||
symbolType: 0x8000 | 0x0234,
|
||
elementLength: 16,
|
||
arrayDims: (0, 0, 0),
|
||
name: "Motor1");
|
||
|
||
var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
|
||
tag.DataType.ShouldBe(AbCipDataType.Structure);
|
||
}
|
||
|
||
[Fact]
|
||
public void System_flag_surfaces_as_IsSystemTag_true()
|
||
{
|
||
var bytes = BuildEntry(
|
||
instanceId: 99,
|
||
symbolType: 0x1000 | 0xC4, // system flag + DINT
|
||
elementLength: 4,
|
||
arrayDims: (0, 0, 0),
|
||
name: "__Reserved_1");
|
||
|
||
var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
|
||
tag.IsSystemTag.ShouldBeTrue();
|
||
tag.DataType.ShouldBe(AbCipDataType.DInt);
|
||
}
|
||
|
||
[Fact]
|
||
public void Program_scope_name_splits_prefix_into_ProgramScope()
|
||
{
|
||
var bytes = BuildEntry(
|
||
instanceId: 1,
|
||
symbolType: 0xC4,
|
||
elementLength: 4,
|
||
arrayDims: (0, 0, 0),
|
||
name: "Program:MainProgram.StepIndex");
|
||
|
||
var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
|
||
tag.ProgramScope.ShouldBe("MainProgram");
|
||
tag.Name.ShouldBe("StepIndex");
|
||
}
|
||
|
||
[Fact]
|
||
public void Multiple_entries_decode_in_wire_order_with_even_padding()
|
||
{
|
||
// Name "Abc" is 3 bytes — triggers the even-pad branch between entries.
|
||
var bytes = Concat(
|
||
BuildEntry(1, 0xC4, 4, (0, 0, 0), "Abc"), // DINT named "Abc" (3-byte name, pads to 4)
|
||
BuildEntry(2, 0xCA, 4, (0, 0, 0), "Pi")); // REAL named "Pi"
|
||
|
||
var tags = CipSymbolObjectDecoder.Decode(bytes).ToList();
|
||
tags.Count.ShouldBe(2);
|
||
tags[0].Name.ShouldBe("Abc");
|
||
tags[0].DataType.ShouldBe(AbCipDataType.DInt);
|
||
tags[1].Name.ShouldBe("Pi");
|
||
tags[1].DataType.ShouldBe(AbCipDataType.Real);
|
||
}
|
||
|
||
[Fact]
|
||
public void Truncated_buffer_stops_decoding_gracefully()
|
||
{
|
||
var full = BuildEntry(7, 0xC4, 4, (0, 0, 0), "Counter");
|
||
// Deliberately chop off the last 5 bytes — decoder should bail cleanly, not throw.
|
||
var truncated = full.Take(full.Length - 5).ToArray();
|
||
|
||
CipSymbolObjectDecoder.Decode(truncated).ToList().Count.ShouldBeLessThan(1); // 0 — didn't parse the broken entry
|
||
}
|
||
|
||
[Fact]
|
||
public void Empty_buffer_yields_no_tags()
|
||
{
|
||
CipSymbolObjectDecoder.Decode([]).ShouldBeEmpty();
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("Counter", null, "Counter")]
|
||
[InlineData("Program:MainProgram.Step", "MainProgram", "Step")]
|
||
[InlineData("Program:MyProg.a.b.c", "MyProg", "a.b.c")]
|
||
[InlineData("Program:", null, "Program:")] // malformed — no dot
|
||
[InlineData("Program:OnlyProg", null, "Program:OnlyProg")]
|
||
[InlineData("Motor.Status.Running", null, "Motor.Status.Running")]
|
||
public void SplitProgramScope_handles_every_shape(string input, string? expectedScope, string expectedName)
|
||
{
|
||
var (scope, name) = CipSymbolObjectDecoder.SplitProgramScope(input);
|
||
scope.ShouldBe(expectedScope);
|
||
name.ShouldBe(expectedName);
|
||
}
|
||
}
|