dabd8eb955
MAJOR-1: the SDK skips TypeInfo.IsInstanceOfDataType on any node with an OnWriteValue handler (every writable equipment node), and the node manager's fail-fast treated numeric<->numeric as compatible, so a UInt32[] to a Galaxy Int32 array matched MxValueEncoder's int[] arm by CLR covariance and was written bit-reinterpreted with a Good result. EvaluateEquipmentWriteStructure now matches an ARRAY write's element type exactly (Byte[]/ByteString the only equivalence; Variant[]/object[] refused) -> BadTypeMismatch, nothing dispatched. MxValueEncoder's int[]/long[] arms are exact-type guarded too, and the Galaxy parity test's false "the SDK refuses" premise is corrected (the UInt32/UInt64 exclusion is now the encoder's own answer). Scalar writes keep numeric widening by decision: consumers write CLR-typed values (Double to a Float node) and every driver's scalar encoder range-checks. MINOR: Modbus row honours maxRegistersPerWrite (smallest of DriverConfig / the tag's DeviceConfig, default 123) and FC15's 1968-coil ceiling, and requires region + numeric address; Modbus/S7/Galaxy require isArray + integer arrayLength >= 1 (Galaxy: an unbounded node would let a short write resize the attribute); an array value to a scalar node is refused inline. Docs: Uns.md array-write rules + upgrade note (ReadWrite array tags become client-writable on first boot), Modbus.md, Galaxy.md. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
70 lines
3.1 KiB
C#
70 lines
3.1 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 driver receives the CLR array type of the node's <c>DataType</c> and no
|
|
/// other: NOT because the SDK checks it (it skips its own type check on a node with an <c>OnWriteValue</c>
|
|
/// handler, which every writable equipment node has) but because <c>OtOpcUaNodeManager.EvaluateEquipmentWriteStructure</c>
|
|
/// refuses an array whose element type does not match the node's DataType exactly (#610 review MAJOR-1). 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 used to be a hand-written exclusion here, because CLR array covariance let uint[] /
|
|
// ulong[] match the encoder's int[] / long[] arms and be encoded by reinterpreting their bits. The encoder
|
|
// now refuses them itself, so the table and the encoder agree with no special case.
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dataType, "{\"FullName\":\"Obj.Attr\",\"isArray\":true,\"arrayLength\":2}")
|
|
.ShouldBe(encoderAccepts, dataType);
|
|
}
|
|
|
|
/// <summary>Positive control: the theory above sees both answers, so it is not vacuously all-refused.</summary>
|
|
[Fact]
|
|
public void The_matrix_contains_both_answers() =>
|
|
ClrArrays.Keys
|
|
.Select(dt => DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dt, "{\"isArray\":true,\"arrayLength\":2}"))
|
|
.ToHashSet()
|
|
.ShouldBe(new[] { true, false }, ignoreOrder: true);
|
|
}
|