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
88 lines
4.6 KiB
C#
88 lines
4.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #610 — the per-driver array-write capability table. The table's agreement with each listed driver's
|
|
/// real encoder is pinned in that driver's own test project (<c>*ArrayWriteSupportParityTests</c>); these
|
|
/// tests pin the table's own reading of TagConfig (defaults, numeric enums, malformed blobs).
|
|
/// </summary>
|
|
public sealed class DriverArrayWriteSupportTests
|
|
{
|
|
[Theory]
|
|
[InlineData("{\"region\":\"Coils\",\"address\":0,\"dataType\":\"Bool\"}", true)]
|
|
[InlineData("{\"region\":\"Coils\",\"address\":0}", true)] // FC15 ignores dataType
|
|
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":0,\"dataType\":\"Float32\"}", true)]
|
|
[InlineData("{\"region\":\"holdingregisters\",\"address\":0,\"dataType\":\"bcd16\"}", true)] // case-insensitive, like the driver
|
|
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":0}", true)] // dataType ⇒ Int16
|
|
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":0,\"dataType\":\"BitInRegister\"}", false)]
|
|
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":0,\"dataType\":\"String\"}", false)]
|
|
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":0,\"dataType\":\"Bool\"}", false)]
|
|
[InlineData("{\"region\":\"InputRegisters\",\"address\":0,\"dataType\":\"Int16\"}", false)] // read-only region
|
|
[InlineData("{\"region\":\"DiscreteInputs\",\"address\":0}", false)]
|
|
[InlineData("{\"region\":0,\"address\":0,\"dataType\":\"Int16\"}", true)] // numeric ⇒ absent ⇒ HoldingRegisters (driver reads it that way)
|
|
[InlineData("not json", false)]
|
|
[InlineData("", false)]
|
|
public void Modbus_rows_follow_region_and_element_type(string tagConfig, bool expected) =>
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", tagConfig).ShouldBe(expected);
|
|
|
|
[Theory]
|
|
[InlineData("Byte", true)]
|
|
[InlineData("Int16", true)]
|
|
[InlineData("UInt16", true)]
|
|
[InlineData("Int32", true)]
|
|
[InlineData("UInt32", true)]
|
|
[InlineData("Float32", true)]
|
|
[InlineData("Bool", false)]
|
|
[InlineData("Int64", false)]
|
|
[InlineData("UInt64", false)]
|
|
[InlineData("Float64", false)]
|
|
[InlineData("String", false)]
|
|
[InlineData("DateTime", false)]
|
|
public void S7_rows_follow_the_element_type(string s7Type, bool expected) =>
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.S7, "Int16", $"{{\"address\":\"DB1.DBW0\",\"dataType\":\"{s7Type}\"}}")
|
|
.ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void S7_without_a_dataType_key_is_not_an_S7_tag() =>
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.S7, "Int16", "{\"address\":\"DB1.DBW0\"}").ShouldBeFalse();
|
|
|
|
[Theory]
|
|
[InlineData("Boolean", true)]
|
|
[InlineData("Int32", true)]
|
|
[InlineData("Int64", true)]
|
|
[InlineData("Float", true)]
|
|
[InlineData("Double", true)]
|
|
[InlineData("String", true)]
|
|
[InlineData("DateTime", true)]
|
|
[InlineData("Int16", false)]
|
|
[InlineData("UInt32", false)]
|
|
[InlineData("Byte", false)]
|
|
public void Galaxy_rows_follow_the_opc_ua_data_type(string dataType, bool expected) =>
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dataType, "{\"FullName\":\"Obj.Attr\"}").ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void OpcUaClient_forwards_every_array() =>
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.OpcUaClient, "UInt16", "{\"nodeId\":\"ns=2;s=x\"}").ShouldBeTrue();
|
|
|
|
/// <summary>Every driver type NOT in the table — and an unknown or absent one — stays read-only.</summary>
|
|
[Fact]
|
|
public void Every_other_driver_type_is_not_array_writable()
|
|
{
|
|
var listed = new[] { DriverTypeNames.Modbus, DriverTypeNames.S7, DriverTypeNames.OpcUaClient, DriverTypeNames.Galaxy };
|
|
var others = DriverTypeNames.All.Except(listed).ToList();
|
|
others.ShouldNotBeEmpty();
|
|
const string blob = "{\"region\":\"Coils\",\"dataType\":\"Int32\"}";
|
|
// Positive control: the same blob IS array-writable under the listed drivers that read it.
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int32", blob).ShouldBeTrue();
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.S7, "Int32", blob).ShouldBeTrue();
|
|
foreach (var driverType in others.Append("NoSuchDriver").Append(null))
|
|
{
|
|
// A blob every listed driver would accept, so only the driver type can make the answer false.
|
|
DriverArrayWriteSupport.Supports(driverType, "Int32", "{\"region\":\"Coils\",\"dataType\":\"Int32\"}")
|
|
.ShouldBeFalse(driverType ?? "<null>");
|
|
}
|
|
}
|
|
}
|