Read path scaffold + the byte→uint quality mapping table that the parity matrix (PR 5.x) pins. PR 4.4 supplies the production GW-backed reader; this PR ships the abstraction and the supporting infrastructure so 4.4 just plugs the implementation in. Files: - Runtime/StatusCodeMap.cs — explicit OPC DA quality byte → OPC UA StatusCode uint mapping. Extends the legacy Galaxy.Host HistorianQualityMapper with named constants (Good / GoodLocalOverride, Uncertain + 4 substatuses, Bad + 7 substatuses, BadInternalError) and an MxStatusProxy → uint helper that honors success flag → detail byte → detected_by transport-error fallback. Unknown bytes fall back to category bucket with a once-per-session diagnostic log so field captures can extend the table. - Runtime/MxValueDecoder.cs — gateway MxValue → boxed CLR value for the seven Galaxy data types (Boolean, Int32, Int64, Float32, Float64, String, DateTime) plus their array variants. Honors MxValue.IsNull and RawValue passthrough. - Runtime/IGalaxyDataReader.cs — driver-side seam for one-shot reads. PR 4.4 ships the production wrapper around MxGatewaySession.SubscribeBulk + StreamEvents + UnsubscribeBulk; this PR exposes the contract so GalaxyDriver.ReadAsync wires through it. - Runtime/GalaxyMxSession.cs — wrapper around MxGatewaySession that owns the Register handle. ConnectAsync opens session + Register; AttachForTests lets tests bypass real gw construction. PR 4.3/4.4/4.5 add write, subscribe, and reconnect surfaces. GalaxyDriver: - Implements IReadable. ReadAsync routes through the injected IGalaxyDataReader (test seam) when present; production path throws NotSupportedException pointing at PR 4.4 — protects deployments running this PR from silent wrong reads while signaling that the legacy-host backend (Galaxy:Backend=legacy-host) handles reads in the meantime. - Internal ctor extended with optional dataReader parameter (default null, preserves PR 4.0/4.1 callers). Tests: 42 new — exhaustive byte→uint table for StatusCodeMap (15 known codes + category-bucket fallback for unknowns + MxStatusProxy precedence rules + OPC UA top-byte invariants), every MxValue oneof case for the decoder (bool/int32/int64/float/double/string/timestamp/3 array variants/ raw bytes/null), GalaxyDriver IReadable wiring (route-through, empty- request, no-reader-throws, post-dispose-throws, status-code preservation). 62 Galaxy tests total pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
3.2 KiB
C#
108 lines
3.2 KiB
C#
using Google.Protobuf;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using MxGateway.Contracts.Proto;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
|
|
|
/// <summary>
|
|
/// Round-trip tests for <see cref="MxValueDecoder"/>. Each scenario constructs a
|
|
/// gateway-style <see cref="MxValue"/>, decodes, and asserts the boxed CLR value
|
|
/// matches the expected type and value.
|
|
/// </summary>
|
|
public sealed class MxValueDecoderTests
|
|
{
|
|
[Fact]
|
|
public void Decode_Null_ReturnsNull()
|
|
{
|
|
MxValueDecoder.Decode(null).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_IsNullFlag_ReturnsNull()
|
|
{
|
|
var v = new MxValue { IsNull = true };
|
|
MxValueDecoder.Decode(v).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_Bool() => MxValueDecoder.Decode(new MxValue { BoolValue = true }).ShouldBe(true);
|
|
|
|
[Fact]
|
|
public void Decode_Int32() => MxValueDecoder.Decode(new MxValue { Int32Value = -42 }).ShouldBe(-42);
|
|
|
|
[Fact]
|
|
public void Decode_Int64() => MxValueDecoder.Decode(new MxValue { Int64Value = 123456789012L }).ShouldBe(123456789012L);
|
|
|
|
[Fact]
|
|
public void Decode_Float() => MxValueDecoder.Decode(new MxValue { FloatValue = 3.14f }).ShouldBe(3.14f);
|
|
|
|
[Fact]
|
|
public void Decode_Double() => MxValueDecoder.Decode(new MxValue { DoubleValue = 2.71828 }).ShouldBe(2.71828);
|
|
|
|
[Fact]
|
|
public void Decode_String() => MxValueDecoder.Decode(new MxValue { StringValue = "hello" }).ShouldBe("hello");
|
|
|
|
[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);
|
|
}
|
|
|
|
[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 });
|
|
}
|
|
|
|
[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 });
|
|
}
|
|
|
|
[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" });
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|