Write path online. GalaxyDriver implements IWritable; routes by SecurityClassification — SecuredWrite / VerifiedWrite tags go through MxCommandKind.WriteSecured, everything else through MxGatewaySession. WriteAsync. Per-tag classifications are captured during ITagDiscovery via a SecurityCapturingBuilder wrapper that intercepts Variable() calls without the discoverer needing to know about the driver's internal state. Files: - Runtime/MxValueEncoder.cs — boxed CLR value → MxValue. Covers seven Galaxy scalar types (bool/int8-32/uint8-32 → Int32, int64/uint64 → Int64, float, double, string, DateTime/DateTimeOffset → Timestamp) and 1-D array variants. Inverse of MxValueDecoder; round-trip pinned by tests. DateTime.Local converts to UTC; unsupported types throw ArgumentException. - Runtime/IGalaxyDataWriter.cs — driver-side seam. Tests inject a fake to capture routing decisions; production path uses GatewayGalaxyDataWriter. - Runtime/GatewayGalaxyDataWriter.cs — production. Lazy-AddItem caches itemHandles, encodes value, routes Write vs WriteSecured, translates MxCommandReply (ProtocolStatus → BadCommunicationError; first MxStatusProxy in statuses[] via StatusCodeMap.FromMxStatus). Per-tag exception isolation: one bad write doesn't fail the batch. - GalaxyDriver: now implements IWritable. Discovery wraps the supplied IAddressSpaceBuilder in SecurityCapturingBuilder which records each attribute's SecurityClass into _securityByFullRef before delegating. WriteAsync resolves classification per tag (FreeAccess default for unknown tags — matches the legacy backend), routes through the injected writer. Throws NotSupportedException with PR 4.4 pointer when no writer is wired (production path requires GalaxyMxSession.Connect from PR 4.4). Tests (32 new, 94 Galaxy total): - MxValueEncoder: every scalar type, narrowing checks (sbyte/short/byte/ ushort fit Int32; uint within Int32 range; ulong within Int64), DateTime.Local → UTC conversion, array variants for bool/double/string/ DateTime, Dimensions populated, unsupported-type throws ArgumentException, encoder/decoder round-trip pin. - GalaxyDriverWriteTests: WriteAsync routes through fake writer with values intact; theory exercises every SecurityClassification value through the discovery-then-write path; unknown-tag defaults to FreeAccess; empty- request short-circuit; no-writer fail-loud; post-dispose throws. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
4.0 KiB
C#
127 lines
4.0 KiB
C#
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>
|
|
/// Tests for <see cref="MxValueEncoder"/>. Pinning each scalar + array case here
|
|
/// guards against accidental drift in the IWritable wire format.
|
|
/// </summary>
|
|
public sealed class MxValueEncoderTests
|
|
{
|
|
[Fact]
|
|
public void Encode_Null_SetsIsNullFlag()
|
|
{
|
|
var v = MxValueEncoder.Encode(null);
|
|
v.IsNull.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_Bool() => MxValueEncoder.Encode(true).BoolValue.ShouldBe(true);
|
|
|
|
[Theory]
|
|
[InlineData((sbyte)-5, -5)]
|
|
[InlineData((short)-1000, -1000)]
|
|
[InlineData((byte)42, 42)]
|
|
[InlineData((ushort)42_000, 42_000)]
|
|
public void Encode_NarrowSignedAndUnsigned_FitsInInt32(object input, int expected)
|
|
{
|
|
var v = MxValueEncoder.Encode(input);
|
|
v.KindCase.ShouldBe(MxValue.KindOneofCase.Int32Value);
|
|
v.Int32Value.ShouldBe(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_Int32_RoundTrip() => MxValueEncoder.Encode(int.MinValue).Int32Value.ShouldBe(int.MinValue);
|
|
|
|
[Fact]
|
|
public void Encode_Int64_RoundTrip()
|
|
{
|
|
var v = MxValueEncoder.Encode(long.MaxValue);
|
|
v.KindCase.ShouldBe(MxValue.KindOneofCase.Int64Value);
|
|
v.Int64Value.ShouldBe(long.MaxValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_UInt32_FitsInInt32() => MxValueEncoder.Encode((uint)int.MaxValue).Int32Value.ShouldBe(int.MaxValue);
|
|
|
|
[Fact]
|
|
public void Encode_Float() => MxValueEncoder.Encode(3.14f).FloatValue.ShouldBe(3.14f);
|
|
|
|
[Fact]
|
|
public void Encode_Double() => MxValueEncoder.Encode(2.71828).DoubleValue.ShouldBe(2.71828);
|
|
|
|
[Fact]
|
|
public void Encode_String() => MxValueEncoder.Encode("hello").StringValue.ShouldBe("hello");
|
|
|
|
[Fact]
|
|
public void Encode_DateTimeUtc()
|
|
{
|
|
var when = new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc);
|
|
var v = MxValueEncoder.Encode(when);
|
|
v.TimestampValue.ShouldNotBeNull();
|
|
v.TimestampValue.ToDateTime().ShouldBe(when);
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_DateTimeLocal_ConvertsToUtc()
|
|
{
|
|
var local = new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Local);
|
|
var v = MxValueEncoder.Encode(local);
|
|
v.TimestampValue.ToDateTime().ShouldBe(local.ToUniversalTime());
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_BoolArray()
|
|
{
|
|
var v = MxValueEncoder.Encode(new[] { true, false, true });
|
|
v.ArrayValue.BoolValues.Values.ToArray().ShouldBe(new[] { true, false, true });
|
|
v.ArrayValue.Dimensions[0].ShouldBe(3u);
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_DoubleArray()
|
|
{
|
|
var v = MxValueEncoder.Encode(new[] { 1.0, 2.0, 3.5 });
|
|
v.ArrayValue.DoubleValues.Values.ToArray().ShouldBe(new[] { 1.0, 2.0, 3.5 });
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_StringArray()
|
|
{
|
|
var v = MxValueEncoder.Encode(new[] { "a", "b" });
|
|
v.ArrayValue.StringValues.Values.ToArray().ShouldBe(new[] { "a", "b" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_DateTimeArray_ConvertsAllToUtc()
|
|
{
|
|
var inputs = new[] { new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc) };
|
|
var v = MxValueEncoder.Encode(inputs);
|
|
v.ArrayValue.TimestampValues.Values[0].ToDateTime().ShouldBe(inputs[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Encode_UnsupportedType_Throws()
|
|
{
|
|
Should.Throw<ArgumentException>(() => MxValueEncoder.Encode(new { Foo = 1 }));
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundTrip_AllScalarTypes_DecodeMatchesOriginal()
|
|
{
|
|
// The encoder + decoder must be inverses for every scalar a Galaxy driver might
|
|
// hand to a write. This pin-test catches accidental drift in either direction.
|
|
object[] inputs = [true, 42, 12345L, 3.14f, 2.71828, "x"];
|
|
foreach (var input in inputs)
|
|
{
|
|
var encoded = MxValueEncoder.Encode(input);
|
|
var decoded = MxValueDecoder.Decode(encoded);
|
|
decoded.ShouldBe(input);
|
|
}
|
|
}
|
|
}
|