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 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), new byte[] { 1, 2 }, new[] { 1 }, new[] { 1, 2 }, 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()); 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))).ShouldBeNull(); ValueConverter.ToVariant(default(ArrayOf)).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(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(); 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(new byte[] { 1 })).TypeInfo.BuiltInType.ShouldBe(BuiltInType.Byte); ValueConverter.ToVariant(new ArrayOf(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(); result.NamespaceIndex.ShouldBe((ushort)4); result.TryGetValue(out string? identifier).ShouldBeTrue(); identifier.ShouldBe("ns=2;s=literal"); } [Fact] public void UnsupportedShape_IsRejected() { Should.Throw(() => ValueConverter.ToVariant(new int[2, 2])); Should.Throw(() => ValueConverter.ToVariant(new object())); } }