Merge #610: server-side array writes gated per driver capability (DriverArrayWriteSupport); ArrayWritable on the surgical whitelist + UNS Backing; shape/IndexRange refusal; ValueRank>1 out of scope
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe # Conflicts: # docs/drivers/S7.md
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #610 — pins <see cref="DriverArrayWriteSupport"/>'s S7 row against the REAL array encoder
|
||||
/// (<see cref="S7Driver.EncodeArrayBlock"/>). For every <see cref="S7DataType"/>, the server advertises an
|
||||
/// array node writable exactly when the encoder accepts the element type, so a driver change that adds or
|
||||
/// drops an array element type fails here until the table follows.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class S7ArrayWriteSupportParityTests
|
||||
{
|
||||
public static TheoryData<S7DataType> AllTypes() => new(Enum.GetValues<S7DataType>());
|
||||
|
||||
// An address whose access size matches the element type, so only the element type can make the encoder refuse.
|
||||
private static string AddressFor(S7DataType dt) => dt switch
|
||||
{
|
||||
S7DataType.Bool => "DB1.DBX0.0",
|
||||
S7DataType.Byte => "DB1.DBB0",
|
||||
S7DataType.Int16 or S7DataType.UInt16 => "DB1.DBW0",
|
||||
_ => "DB1.DBD0",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllTypes))]
|
||||
public void Table_agrees_with_EncodeArrayBlock(S7DataType dataType)
|
||||
{
|
||||
var address = AddressFor(dataType);
|
||||
var tag = new S7TagDefinition("Arr", address, dataType, Writable: true, ArrayCount: 2);
|
||||
|
||||
bool encoderAccepts;
|
||||
try
|
||||
{
|
||||
S7Driver.EncodeArrayBlock(tag, S7AddressParser.Parse(address), new object[] { 1, 1 });
|
||||
encoderAccepts = true;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
encoderAccepts = false;
|
||||
}
|
||||
|
||||
DriverArrayWriteSupport.Supports(
|
||||
DriverTypeNames.S7, "Int16", $$"""{"address":"{{address}}","dataType":"{{dataType}}","isArray":true,"arrayLength":2}""")
|
||||
.ShouldBe(encoderAccepts, dataType.ToString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user