feat(address-space): server-side array writes gated per driver capability (#610)
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
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #610 — pins <see cref="DriverArrayWriteSupport"/>'s Modbus row against the REAL write path. For every
|
||||
/// region × element type, the server advertises an array node writable exactly when the driver accepts a
|
||||
/// whole-array write for it. A driver change that gains or loses an array encoder fails here until the table
|
||||
/// follows — so the server can never advertise a write this driver refuses (nor hide one it accepts).
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ModbusArrayWriteSupportParityTests
|
||||
{
|
||||
private static IEnumerable<(ModbusRegion Region, ModbusDataType DataType)> Shapes() =>
|
||||
from region in Enum.GetValues<ModbusRegion>()
|
||||
from dt in Enum.GetValues<ModbusDataType>()
|
||||
select (region, dt);
|
||||
|
||||
public static TheoryData<ModbusRegion, ModbusDataType> AllShapes()
|
||||
{
|
||||
var data = new TheoryData<ModbusRegion, ModbusDataType>();
|
||||
foreach (var (region, dt) in Shapes()) data.Add(region, dt);
|
||||
return data;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllShapes))]
|
||||
public async Task Table_agrees_with_the_driver_write_path(ModbusRegion region, ModbusDataType dataType)
|
||||
{
|
||||
var json = $$"""{"region":"{{region}}","address":10,"dataType":"{{dataType}}","stringLength":4,"isArray":true,"arrayLength":2}""";
|
||||
var fake = new ModbusDriverTests.FakeTransport();
|
||||
var drv = new ModbusDriver(
|
||||
new ModbusDriverOptions { Host = "fake", RawTags = [new RawTagEntry("cell/modbus/arr", json, false)] },
|
||||
"modbus-array-parity", _ => fake);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// A two-element value every element type can convert (Convert.ToBoolean / ToInt16 / ToSingle … of 1).
|
||||
var result = await drv.WriteAsync([new WriteRequest("cell/modbus/arr", new object[] { 1, 1 })], CancellationToken.None);
|
||||
var driverAccepts = result.Single().StatusCode == 0u;
|
||||
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", json).ShouldBe(
|
||||
driverAccepts, $"region={region} dataType={dataType}: driver status 0x{result.Single().StatusCode:X8}");
|
||||
}
|
||||
|
||||
/// <summary>Positive control: the theory above is not vacuously "everything refused".</summary>
|
||||
[Fact]
|
||||
public void The_matrix_contains_both_answers()
|
||||
{
|
||||
var answers = Shapes()
|
||||
.Select(s => DriverArrayWriteSupport.Supports(
|
||||
DriverTypeNames.Modbus, "Int16",
|
||||
$$"""{"region":"{{s.Region}}","address":10,"dataType":"{{s.DataType}}"}"""))
|
||||
.ToHashSet();
|
||||
answers.ShouldBe(new[] { true, false }, ignoreOrder: true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user