using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.MxGateway.Contracts.Proto; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// Round-trip tests for . Each scenario constructs a /// gateway-style , decodes, and asserts the boxed CLR value /// matches the expected type and value. /// public sealed class MxValueDecoderTests { /// Verifies that decoding null returns null. [Fact] public void Decode_Null_ReturnsNull() { MxValueDecoder.Decode(null).ShouldBeNull(); } /// Verifies that decoding IsNull flag returns null. [Fact] public void Decode_IsNullFlag_ReturnsNull() { var v = new MxValue { IsNull = true }; MxValueDecoder.Decode(v).ShouldBeNull(); } /// Verifies that bool values decode correctly. [Fact] public void Decode_Bool() => MxValueDecoder.Decode(new MxValue { BoolValue = true }).ShouldBe(true); /// Verifies that Int32 values decode correctly. [Fact] public void Decode_Int32() => MxValueDecoder.Decode(new MxValue { Int32Value = -42 }).ShouldBe(-42); /// Verifies that Int64 values decode correctly. [Fact] public void Decode_Int64() => MxValueDecoder.Decode(new MxValue { Int64Value = 123456789012L }).ShouldBe(123456789012L); /// Verifies that float values decode correctly. [Fact] public void Decode_Float() => MxValueDecoder.Decode(new MxValue { FloatValue = 3.14f }).ShouldBe(3.14f); /// Verifies that double values decode correctly. [Fact] public void Decode_Double() => MxValueDecoder.Decode(new MxValue { DoubleValue = 2.71828 }).ShouldBe(2.71828); /// Verifies that string values decode correctly. [Fact] public void Decode_String() => MxValueDecoder.Decode(new MxValue { StringValue = "hello" }).ShouldBe("hello"); /// Verifies that timestamp values decode to UTC datetime. [Fact] public void Decode_Timestamp_ReturnsUtcDateTime() { var when = new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc); var v = new MxValue { TimestampValue = Timestamp.FromDateTime(when) }; MxValueDecoder.Decode(v).ShouldBe(when); } /// Verifies that bool arrays decode correctly. [Fact] public void Decode_BoolArray() { var v = new MxValue { ArrayValue = new MxArray { BoolValues = new BoolArray(), }, }; v.ArrayValue.BoolValues.Values.AddRange(new[] { true, false, true }); var decoded = MxValueDecoder.Decode(v); decoded.ShouldBe(new[] { true, false, true }); } /// Verifies that double arrays decode correctly. [Fact] public void Decode_DoubleArray() { var v = new MxValue { ArrayValue = new MxArray { DoubleValues = new DoubleArray() }, }; v.ArrayValue.DoubleValues.Values.AddRange(new[] { 1.0, 2.0, 3.5 }); var decoded = MxValueDecoder.Decode(v); decoded.ShouldBe(new[] { 1.0, 2.0, 3.5 }); } /// Verifies that string arrays decode correctly. [Fact] public void Decode_StringArray() { var v = new MxValue { ArrayValue = new MxArray { StringValues = new StringArray() }, }; v.ArrayValue.StringValues.Values.AddRange(new[] { "a", "b" }); var decoded = MxValueDecoder.Decode(v); decoded.ShouldBe(new[] { "a", "b" }); } /// Verifies that raw byte values decode correctly. [Fact] public void Decode_RawValue_ReturnsBytes() { var bytes = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; var v = new MxValue { RawValue = ByteString.CopyFrom(bytes) }; var decoded = (byte[])MxValueDecoder.Decode(v)!; decoded.ShouldBe(bytes); } // --------------------------------------------------------------------------------- // Sparse arrays (MxGateway 0.2.0). // // The gateway contract makes these write-only — it expands a sparse write into a full // MxArray and "the worker never receives or produces it" — so inbound is a contract // violation rather than an expected path. These pin the behaviour if one ever arrives: // the correct dense array, never a silent null and never a throw. The fill semantics // mirror the gateway's own SparseArrayExpander, so a value that round-trips through a // write and back reads as the same CLR array. // --------------------------------------------------------------------------------- private static MxValue Sparse(MxDataType elementType, uint totalLength, params (uint Index, MxValue Value)[] elements) { var sparse = new MxSparseArray { ElementDataType = elementType, TotalLength = totalLength }; foreach (var (index, value) in elements) { sparse.Elements.Add(new MxSparseElement { Index = index, Value = value }); } return new MxValue { SparseArrayValue = sparse }; } /// Verifies unmentioned indices are reset to the element default, not dropped. [Fact] public void Decode_SparseInt32Array_FillsUnmentionedIndicesWithTheDefault() { var v = Sparse(MxDataType.Integer, 5, (1, new MxValue { Int32Value = 42 }), (4, new MxValue { Int32Value = -7 })); MxValueDecoder.Decode(v).ShouldBe(new[] { 0, 42, 0, 0, -7 }); } /// /// Verifies an Integer sparse array widens to long[] when any element carries an int64. /// The gateway picks its Int32/Int64 sub-array the same way — a sparse payload has no CLR /// array to infer the width from — so decoding narrow here would change the published /// element type across a write round-trip. /// [Fact] public void Decode_SparseIntegerArray_WidensToInt64WhenAnyElementIs64Bit() { var v = Sparse(MxDataType.Integer, 3, (0, new MxValue { Int32Value = 1 }), (2, new MxValue { Int64Value = 8_000_000_000L })); MxValueDecoder.Decode(v).ShouldBe(new[] { 1L, 0L, 8_000_000_000L }); } /// Verifies bool / float / double / string sparse arrays fill with their own defaults. [Fact] public void Decode_SparseArrays_UseThePerTypeDefaultForUnmentionedIndices() { MxValueDecoder.Decode(Sparse(MxDataType.Boolean, 3, (1, new MxValue { BoolValue = true }))) .ShouldBe(new[] { false, true, false }); MxValueDecoder.Decode(Sparse(MxDataType.Float, 2, (0, new MxValue { FloatValue = 1.5f }))) .ShouldBe(new[] { 1.5f, 0f }); MxValueDecoder.Decode(Sparse(MxDataType.Double, 2, (1, new MxValue { DoubleValue = 2.5d }))) .ShouldBe(new[] { 0d, 2.5d }); // string defaults to empty, NOT null — matching the expander, so a consumer never // has to null-check individual elements of an array it was handed whole. MxValueDecoder.Decode(Sparse(MxDataType.String, 3, (2, new MxValue { StringValue = "z" }))) .ShouldBe(new[] { string.Empty, string.Empty, "z" }); } /// /// Verifies a Time sparse array defaults to the Unix epoch — the expander fills /// Timestamp{0,0}, and default(DateTime) would decode a different instant. /// [Fact] public void Decode_SparseTimeArray_DefaultsToTheUnixEpochNotDefaultDateTime() { var when = new DateTime(2026, 8, 12, 3, 4, 5, DateTimeKind.Utc); var v = Sparse(MxDataType.Time, 2, (1, new MxValue { TimestampValue = Timestamp.FromDateTime(when) })); var decoded = (DateTime[])MxValueDecoder.Decode(v)!; decoded[0].ShouldBe(DateTime.UnixEpoch); decoded[0].ShouldNotBe(default); decoded[1].ShouldBe(when); } /// /// Verifies a malformed sparse payload degrades instead of throwing. The decoder runs /// inside the pump's dispatch loop, where an escaping exception kills dispatch for the /// life of the pump — so an out-of-range index must be skipped, not raised. /// [Fact] public void Decode_SparseArray_WithAnOutOfRangeIndex_SkipsItRatherThanThrowing() { var v = Sparse(MxDataType.Integer, 2, (0, new MxValue { Int32Value = 5 }), (99, new MxValue { Int32Value = 6 })); object? decoded = null; Should.NotThrow(() => decoded = MxValueDecoder.Decode(v)); decoded.ShouldBe(new[] { 5, 0 }); } /// Verifies an unsupported element type and a zero length report as unrepresentable. [Fact] public void TryDecode_SparseArray_WithAnUnsupportedShape_ReportsUnrepresentable() { // A struct element type is not a scalar the expander supports either — there is no // array to build, so this must NOT masquerade as a successfully decoded null. MxValueDecoder.TryDecode(Sparse(MxDataType.QualifiedStruct, 2), out var structDecoded).ShouldBeFalse(); structDecoded.ShouldBeNull(); MxValueDecoder.TryDecode(Sparse(MxDataType.Integer, 0), out _).ShouldBeFalse(); } // --------------------------------------------------------------------------------- // Representable-vs-null. Decode() alone collapses both to null, which is how a value // kind this driver cannot represent would be published as a good null and lost. // --------------------------------------------------------------------------------- /// Verifies a genuine null decodes successfully rather than reporting failure. [Fact] public void TryDecode_DistinguishesAGenuineNullFromAnUnrepresentableValue() { MxValueDecoder.TryDecode(null, out var fromNullRef).ShouldBeTrue(); fromNullRef.ShouldBeNull(); MxValueDecoder.TryDecode(new MxValue { IsNull = true }, out var fromIsNull).ShouldBeTrue(); fromIsNull.ShouldBeNull(); // An MxValue with no kind set at all is not a null value — it is a payload this // driver cannot interpret, and the two must not report identically. MxValueDecoder.TryDecode(new MxValue(), out var fromEmpty).ShouldBeFalse(); fromEmpty.ShouldBeNull(); } /// Verifies an array whose element oneof is unset reports as unrepresentable. [Fact] public void TryDecode_ArrayWithNoElementsOneofSet_ReportsUnrepresentable() { MxValueDecoder.TryDecode(new MxValue { ArrayValue = new MxArray() }, out var decoded).ShouldBeFalse(); decoded.ShouldBeNull(); } }