127 lines
5.2 KiB
C#
127 lines
5.2 KiB
C#
using Opc.Ua;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Helpers;
|
|
|
|
public class VariantBoundaryTests
|
|
{
|
|
public static IEnumerable<object?[]> Payloads()
|
|
{
|
|
foreach (var value in new object?[] { null, true, (sbyte)-1, (byte)1, (short)-2, (ushort)2,
|
|
-3, 3u, -4L, 4UL, 1.25f, 2.5, "", "text", Guid.Parse("de305d54-75b4-431b-adb2-eb6b9e546014"),
|
|
new DateTime(2026, 11, 10, 0, 0, 0, DateTimeKind.Utc), Array.Empty<byte>(), new byte[] { 1, 2 },
|
|
Array.Empty<int>(), new[] { 1 }, new[] { 1, 2 }, Array.Empty<string>(), new[] { "", "text" } })
|
|
yield return [value];
|
|
foreach (var value in new object[] { true, (sbyte)-1, (byte)1, (short)-2, (ushort)2,
|
|
-3, 3u, -4L, 4UL, 1.25f, 2.5, "text", Guid.Empty,
|
|
new DateTime(2026, 11, 10, 0, 0, 0, DateTimeKind.Utc) })
|
|
{
|
|
foreach (var count in new[] { 0, 1, 3 })
|
|
{
|
|
var array = Array.CreateInstance(value.GetType(), count);
|
|
for (var i = 0; i < count; i++) array.SetValue(value, i);
|
|
yield return [array];
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(Payloads))]
|
|
public void RoundTrip_PreservesClrTypeAndValue(object? expected)
|
|
{
|
|
var actual = ValueConverter.ToClrObject(ValueConverter.ToVariant(expected));
|
|
if (expected == null) actual.ShouldBeNull();
|
|
else
|
|
{
|
|
actual.ShouldNotBeNull();
|
|
actual.GetType().ShouldBe(expected.GetType());
|
|
actual.ShouldBe(expected);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RetainedArrays_AreDetachedFromCaller()
|
|
{
|
|
var bytes = new byte[] { 1, 2 };
|
|
var numbers = new[] { 3, 4 };
|
|
var byteValue = ValueConverter.ToVariant(bytes);
|
|
var numericValue = ValueConverter.ToVariant(numbers);
|
|
bytes[0] = 99;
|
|
numbers[0] = 99;
|
|
ValueConverter.ToClrObject(byteValue).ShouldBe(new byte[] { 1, 2 });
|
|
ValueConverter.ToClrObject(numericValue).ShouldBe(new[] { 3, 4 });
|
|
}
|
|
|
|
[Fact]
|
|
public void ByteString_NullAndEmptyRemainDistinct()
|
|
{
|
|
ValueConverter.ToClrObject(Variant.From(default(ByteString))).ShouldBeNull();
|
|
ValueConverter.ToClrObject(Variant.From(ByteString.Empty)).ShouldBe(Array.Empty<byte>());
|
|
ValueConverter.ToClrObject(Variant.Null).ShouldBeNull();
|
|
ValueConverter.ToClrObject(Variant.From(0)).ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void NullWrapperInputs_RemainNullAfterRetention()
|
|
{
|
|
ValueConverter.ToClrObject(ValueConverter.ToVariant(default(ByteString))).ShouldBeNull();
|
|
ValueConverter.ToClrObject(ValueConverter.ToVariant(default(ArrayOf<int>))).ShouldBeNull();
|
|
ValueConverter.ToVariant(default(ArrayOf<byte>)).TypeInfo.ValueRank.ShouldBe(ValueRanks.OneDimension);
|
|
ValueConverter.ToClrObject(ValueConverter.ToVariant(Variant.From(default(ByteString)))).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadOnlyWrappers_AreDetachedFromMutableBackingBuffers()
|
|
{
|
|
var bytes = new byte[] { 1, 2 };
|
|
var numbers = new[] { 3, 4 };
|
|
var retainedBytes = ValueConverter.ToVariant(new ByteString(bytes));
|
|
var retainedNumbers = ValueConverter.ToVariant(new ArrayOf<int>(numbers));
|
|
bytes[0] = 99;
|
|
numbers[0] = 99;
|
|
ValueConverter.ToClrObject(retainedBytes).ShouldBe(new byte[] { 1, 2 });
|
|
ValueConverter.ToClrObject(retainedNumbers).ShouldBe(new[] { 3, 4 });
|
|
}
|
|
|
|
[Fact]
|
|
public void RetainedByteStringArray_CopiesElementBuffersAndPreservesNullEmpty()
|
|
{
|
|
var bytes = new byte[] { 1, 2 };
|
|
var value = ValueConverter.ToVariant(new ByteString[] { default, ByteString.Empty, new(bytes) });
|
|
bytes[0] = 99;
|
|
var actual = ValueConverter.ToClrObject(value).ShouldBeOfType<byte[][]>();
|
|
actual[0].ShouldBeNull();
|
|
actual[1].ShouldBeEmpty();
|
|
actual[2].ShouldBe(new byte[] { 1, 2 });
|
|
}
|
|
|
|
[Fact]
|
|
public void ByteArrayAndByteString_KeepWireRank()
|
|
{
|
|
ValueConverter.ToVariant(new byte[] { 1 }).TypeInfo.BuiltInType.ShouldBe(BuiltInType.ByteString);
|
|
ValueConverter.ToVariant(new ArrayOf<byte>(new byte[] { 1 })).TypeInfo.BuiltInType.ShouldBe(BuiltInType.Byte);
|
|
ValueConverter.ToVariant(new ArrayOf<byte>(new byte[] { 1 })).TypeInfo.ValueRank.ShouldBe(ValueRanks.OneDimension);
|
|
}
|
|
|
|
[Fact]
|
|
public void NodeIds_KeepOpaqueBytesAndLiteralStringIdentifiers()
|
|
{
|
|
var opaque = NodeId.Parse("ns=2;b=AQID");
|
|
ValueConverter.ToClrObject(ValueConverter.ToVariant(opaque)).ShouldBe(opaque);
|
|
var literal = new NodeId("ns=2;s=literal", 4);
|
|
var result = ValueConverter.ToClrObject(ValueConverter.ToVariant(literal)).ShouldBeOfType<NodeId>();
|
|
result.NamespaceIndex.ShouldBe((ushort)4);
|
|
result.TryGetValue(out string? identifier).ShouldBeTrue();
|
|
identifier.ShouldBe("ns=2;s=literal");
|
|
}
|
|
|
|
[Fact]
|
|
public void UnsupportedShape_IsRejected()
|
|
{
|
|
Should.Throw<ServiceResultException>(() => ValueConverter.ToVariant(new int[2, 2]));
|
|
Should.Throw<ServiceResultException>(() => ValueConverter.ToVariant(new object()));
|
|
}
|
|
}
|