Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/CipSymbolObjectDecoderTests.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

202 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
/// <summary>Verifies that a single DInt entry decodes correctly.</summary>
[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();
}
/// <summary>Verifies that all known atomic type codes map to correct data types.</summary>
/// <param name="typeCode">The CIP type code to map.</param>
/// <param name="expected">The expected AbCipDataType result.</param>
[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);
}
/// <summary>Verifies that unknown type codes return null for opaque handling.</summary>
[Fact]
public void Unknown_type_code_returns_null_so_caller_treats_as_opaque()
{
CipSymbolObjectDecoder.MapTypeCode(0xFF).ShouldBeNull();
}
/// <summary>Verifies that struct flag overrides type code.</summary>
[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);
}
/// <summary>Verifies that system flag surfaces as IsSystemTag true.</summary>
[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);
}
/// <summary>Verifies that program scope names split correctly into prefix and name.</summary>
[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");
}
/// <summary>Verifies that multiple entries decode in wire order with proper padding.</summary>
[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);
}
/// <summary>Verifies that truncated buffers stop decoding gracefully.</summary>
[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
}
/// <summary>Verifies that empty buffers yield no tags.</summary>
[Fact]
public void Empty_buffer_yields_no_tags()
{
CipSymbolObjectDecoder.Decode([]).ShouldBeEmpty();
}
/// <summary>Verifies that SplitProgramScope handles all valid name shapes.</summary>
/// <param name="input">The name string to split.</param>
/// <param name="expectedScope">The expected program scope prefix, if any.</param>
/// <param name="expectedName">The expected name after splitting.</param>
[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);
}
}