18f8eb5d2f
Array nodes were forced read-only for every driver. A new single authority, Core.Abstractions DriverArrayWriteSupport, says which (driver, element type) pairs encode a whole-array write: Modbus (Coils / numeric HoldingRegisters), S7 (Byte/Int16/UInt16/Int32/UInt32/Float32), OpcUaClient (pass-through) and Galaxy (Boolean/Int32/Int64/Float/Double/String/DateTime). Everything else stays read-only, so the server never advertises a write the driver refuses. - Composer derives RawTagPlan.ArrayWritable + UnsReferenceVariable .BackingArrayWritable at compose time (both seams, byte-parity); the applier's one writability rule is Writable && (!IsArray || ArrayWritable) for materialise and surgical paths, raw and UNS alike. - ArrayWritable is on the classifier's surgical raw-tag whitelist and mirrored on the UNS record (UnsReferenceBackingParityTests updated). - Node manager refuses a wrong-length / non-array value on an array node (BadTypeMismatch) and any IndexRange write (BadWriteNotSupported) inline, before dispatch; WriteOperate/ACL gates and the revert are unchanged. - Per-driver parity tests pin the table against the real Modbus write path, S7 EncodeArrayBlock and Galaxy MxValueEncoder (UInt32/UInt64 excluded: array covariance reinterprets their bits). - Over-the-wire proof in OpcUaServer.IntegrationTests (ArrayWriteWireTests). - ValueRank > 1 documented as out of scope by decision (docs/Uns.md). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
64 lines
2.7 KiB
C#
64 lines
2.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
|
|
|
/// <summary>
|
|
/// Gitea #610 — pins <see cref="DriverArrayWriteSupport"/>'s Galaxy row against the REAL write encoder
|
|
/// (<see cref="MxValueEncoder"/>). The OPC UA server hands the driver the CLR array type of the node's
|
|
/// <c>DataType</c> (the SDK refuses a mistyped array before the write handler runs), so for every
|
|
/// <c>Tag.DataType</c> the server can materialise, the node is advertised writable exactly when the encoder
|
|
/// accepts that array type.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class GalaxyArrayWriteSupportParityTests
|
|
{
|
|
// Tag.DataType name → the CLR array the OPC UA stack delivers for a 1-D write of that type.
|
|
private static readonly Dictionary<string, Array> ClrArrays = new()
|
|
{
|
|
["Boolean"] = new[] { true, false },
|
|
["SByte"] = new sbyte[] { 1, 2 },
|
|
["Byte"] = new byte[] { 1, 2 },
|
|
["Int16"] = new short[] { 1, 2 },
|
|
["UInt16"] = new ushort[] { 1, 2 },
|
|
["Int32"] = new[] { 1, 2 },
|
|
["UInt32"] = new uint[] { 1, 2 },
|
|
["Int64"] = new long[] { 1, 2 },
|
|
["UInt64"] = new ulong[] { 1, 2 },
|
|
["Float"] = new[] { 1f, 2f },
|
|
["Double"] = new[] { 1d, 2d },
|
|
["String"] = new[] { "a", "b" },
|
|
["DateTime"] = new[] { DateTime.UtcNow, DateTime.UtcNow },
|
|
};
|
|
|
|
public static TheoryData<string> DataTypes() => new(ClrArrays.Keys);
|
|
|
|
[Theory]
|
|
[MemberData(nameof(DataTypes))]
|
|
public void Table_agrees_with_MxValueEncoder(string dataType)
|
|
{
|
|
bool encoderAccepts;
|
|
try
|
|
{
|
|
MxValueEncoder.Encode(ClrArrays[dataType]);
|
|
encoderAccepts = true;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
encoderAccepts = false;
|
|
}
|
|
|
|
// UInt32 / UInt64 are a deliberate exclusion: CLR array covariance lets uint[] / ulong[] match the encoder's
|
|
// int[] / long[] arms, so it "accepts" them by REINTERPRETING the bits (uint.MaxValue → -1) — unlike the
|
|
// scalar arms, which range-check. A Galaxy attribute is never unsigned (DataTypeMap), so the table refuses
|
|
// rather than advertise a write that silently corrupts large values.
|
|
var expected = encoderAccepts && dataType is not ("UInt32" or "UInt64");
|
|
if (dataType is "UInt32" or "UInt64") encoderAccepts.ShouldBeTrue("the covariance hazard this exclusion documents");
|
|
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dataType, "{\"FullName\":\"Obj.Attr\"}")
|
|
.ShouldBe(expected, dataType);
|
|
}
|
|
}
|