Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/CipSymbolObjectDecoderTests.cs
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00

187 lines
6.6 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;
}
[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);
}
}