fix(modbus): gate array read on isArray:true; 1-element arrays (review C-1)
This commit is contained in:
@@ -332,6 +332,81 @@ public sealed class ModbusArrayTests
|
||||
def!.ArrayCount.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- C-1 regression tests: isArray gate ----
|
||||
|
||||
/// <summary>
|
||||
/// C-1 regression: <c>isArray:false</c> with a non-zero <c>arrayLength</c> must produce
|
||||
/// <c>ArrayCount == null</c> (scalar), NOT an array. The foundation materialises a scalar
|
||||
/// node when <c>isArray</c> is false; if the driver returned an array the value-shape
|
||||
/// would mismatch and the OPC UA write would be rejected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parser_IsArrayFalse_With_ArrayLength_Gives_Null_ArrayCount()
|
||||
{
|
||||
// isArray:false, arrayLength:8 → must be scalar (ArrayCount == null).
|
||||
var json = """{"region":"HoldingRegisters","address":0,"dataType":"Int16","byteOrder":"BigEndian","bitIndex":0,"stringLength":0,"isArray":false,"arrayLength":8}""";
|
||||
ModbusEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.ArrayCount.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C-1 regression: <c>isArray:false</c> with <c>arrayLength:8</c> must read as a SCALAR,
|
||||
/// not as an 8-element array.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Equipment_Tag_IsArrayFalse_With_ArrayLength_Reads_As_Scalar()
|
||||
{
|
||||
// isArray:false, arrayLength:8 → scalar read; only register[40] is consumed.
|
||||
var json = """{"region":"HoldingRegisters","address":40,"dataType":"Int16","byteOrder":"BigEndian","bitIndex":0,"stringLength":0,"isArray":false,"arrayLength":8}""";
|
||||
var fake = new ModbusDriverTests.FakeTransport();
|
||||
var opts = new ModbusDriverOptions { Host = "fake", Tags = [] };
|
||||
var drv = new ModbusDriver(opts, "modbus-c1-scalar", _ => fake);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
fake.HoldingRegisters[40] = 9999;
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(0u);
|
||||
// Must be a scalar short, NOT a short[].
|
||||
r[0].Value.ShouldBeOfType<short>();
|
||||
r[0].Value.ShouldBe((short)9999);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <c>isArray:true, arrayLength:1</c> produces <c>ArrayCount == 1</c>
|
||||
/// (a valid 1-element array — the canonical rule says arrayLength >= 1 is valid).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parser_IsArrayTrue_ArrayLength1_Gives_ArrayCount_1()
|
||||
{
|
||||
var json = """{"region":"HoldingRegisters","address":0,"dataType":"Int16","byteOrder":"BigEndian","bitIndex":0,"stringLength":0,"isArray":true,"arrayLength":1}""";
|
||||
ModbusEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.ArrayCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <c>isArray:true, arrayLength:1</c> reads as a 1-ELEMENT array (not a
|
||||
/// scalar). The foundation materialises a <c>[1]</c> OPC UA array node for this tag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Equipment_Tag_IsArrayTrue_ArrayLength1_Reads_As_One_Element_Array()
|
||||
{
|
||||
var json = """{"region":"HoldingRegisters","address":50,"dataType":"Int16","byteOrder":"BigEndian","bitIndex":0,"stringLength":0,"isArray":true,"arrayLength":1}""";
|
||||
var fake = new ModbusDriverTests.FakeTransport();
|
||||
var opts = new ModbusDriverOptions { Host = "fake", Tags = [] };
|
||||
var drv = new ModbusDriver(opts, "modbus-c1-arr1", _ => fake);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
fake.HoldingRegisters[50] = 777;
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(0u);
|
||||
// Must be a short[] of length 1, NOT a scalar short.
|
||||
var arr = r[0].Value.ShouldBeOfType<short[]>();
|
||||
arr.Length.ShouldBe(1);
|
||||
arr[0].ShouldBe((short)777);
|
||||
}
|
||||
|
||||
/// <summary>Recording address space builder for capturing discovered attributes.</summary>
|
||||
/// <param name="captured">List to capture discovered attributes into.</param>
|
||||
private sealed class RecordingBuilder(List<DriverAttributeInfo> captured) : IAddressSpaceBuilder
|
||||
|
||||
Reference in New Issue
Block a user