fix(ablegacy): gate array read on isArray:true; 1-element arrays + assumption comments (review C-2/I-3)
This commit is contained in:
@@ -138,18 +138,24 @@ public sealed class AbLegacyArrayTests
|
||||
factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>An ArrayLength of 1 behaves like a scalar (single value, ElementCount 1).</summary>
|
||||
/// <summary>
|
||||
/// A 1-element array (ArrayLength: 1) reads as a 1-element typed array, NOT a scalar.
|
||||
/// The foundation materialises a <c>[1]</c> OPC UA array node for <c>isArray:true,
|
||||
/// arrayLength:1</c>, so the driver read must return a single-element array to match
|
||||
/// (review I-3). ElementCount stays 1.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ArrayLength_of_one_behaves_like_scalar()
|
||||
public async Task ArrayLength_of_one_reads_a_one_element_array()
|
||||
{
|
||||
var (drv, factory) = NewDriver(
|
||||
new AbLegacyTagDefinition("Counter", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 1));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Value = 7 };
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 7 } };
|
||||
|
||||
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
|
||||
snapshots.Single().Value.ShouldBe(7);
|
||||
var arr = snapshots.Single().Value.ShouldBeOfType<short[]>();
|
||||
arr.ShouldBe(new short[] { 7 });
|
||||
factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
@@ -175,6 +181,24 @@ public sealed class AbLegacyArrayTests
|
||||
captured[0].ArrayDim.ShouldBe(8u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// I-3: a 1-element array tag (ArrayLength: 1) discovers as IsArray=true / ArrayDim=1 —
|
||||
/// a <c>[1]</c> OPC UA array node, not a scalar.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Discovery_surfaces_one_element_array_as_IsArray_with_dim_one()
|
||||
{
|
||||
var captured = new List<DriverAttributeInfo>();
|
||||
var (drv, _) = NewDriver(
|
||||
new AbLegacyTagDefinition("Single", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 1));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
await drv.DiscoverAsync(new RecordingBuilder(captured), CancellationToken.None);
|
||||
|
||||
captured[0].IsArray.ShouldBeTrue();
|
||||
captured[0].ArrayDim.ShouldBe(1u);
|
||||
}
|
||||
|
||||
/// <summary>Scalar tag discovery keeps IsArray false / ArrayDim null (regression guard).</summary>
|
||||
[Fact]
|
||||
public async Task Discovery_keeps_scalar_tag_non_array()
|
||||
@@ -216,11 +240,11 @@ public sealed class AbLegacyArrayTests
|
||||
def!.ArrayLength.ShouldBe(10);
|
||||
}
|
||||
|
||||
/// <summary>The parser caps <c>arrayLength</c> at the PCCC 256-word file maximum.</summary>
|
||||
/// <summary>The parser caps <c>arrayLength</c> at the PCCC 256-word file maximum (isArray:true).</summary>
|
||||
[Fact]
|
||||
public void EquipmentTagParser_caps_arrayLength_at_256()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","arrayLength":99999}""";
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":99999}""";
|
||||
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBe(256);
|
||||
}
|
||||
@@ -234,15 +258,50 @@ public sealed class AbLegacyArrayTests
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C-2 regression: an <c>arrayLength</c> WITHOUT <c>isArray:true</c> is a SCALAR. The
|
||||
/// canonical contract gates an array on <c>isArray:true</c>, so a stale length behind a
|
||||
/// cleared / absent isArray must leave ArrayLength null and never produce an orphan array.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EquipmentTagParser_arrayLength_without_isArray_is_scalar()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","arrayLength":8}""";
|
||||
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>C-2 regression: <c>isArray:false</c> with a positive arrayLength is a SCALAR.</summary>
|
||||
[Fact]
|
||||
public void EquipmentTagParser_isArray_false_with_arrayLength_is_scalar()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
|
||||
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// I-3: <c>isArray:true, arrayLength:1</c> is a valid 1-element array — ArrayLength is 1
|
||||
/// (not null), so the foundation materialises a <c>[1]</c> node and the driver reads it
|
||||
/// as a 1-element array.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EquipmentTagParser_isArray_true_arrayLength_one_is_one_element_array()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":1}""";
|
||||
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end: an AbLegacy driver with NO authored tags reads an equipment-tag ref whose
|
||||
/// TagConfig carries <c>arrayLength</c> — the resolver threads the count and the read
|
||||
/// surfaces a typed array, capping the libplctag element count at 256.
|
||||
/// TagConfig carries <c>isArray:true</c> + <c>arrayLength</c> — the resolver threads the
|
||||
/// count and the read surfaces a typed array, capping the libplctag element count at 256.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Driver_reads_equipment_array_ref_as_typed_array()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","arrayLength":3}""";
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":3}""";
|
||||
var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 11, 22, 33 } } };
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
@@ -260,6 +319,32 @@ public sealed class AbLegacyArrayTests
|
||||
factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C-2 end-to-end: an equipment-tag ref carrying <c>isArray:false, arrayLength:8</c> reads
|
||||
/// a single SCALAR value (the parser drops the orphan length), so the read never decodes a
|
||||
/// phantom 8-element array against a scalar node. ElementCount stays 1.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Driver_reads_isArray_false_with_arrayLength_as_scalar()
|
||||
{
|
||||
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
|
||||
var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 99 } };
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
||||
Tags = [],
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, "ablegacy-eq-scalar", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
r[0].Value.ShouldBe(99);
|
||||
r[0].Value.ShouldNotBeOfType<short[]>();
|
||||
factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
private sealed class RecordingBuilder(List<DriverAttributeInfo> captured) : IAddressSpaceBuilder
|
||||
{
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
|
||||
|
||||
Reference in New Issue
Block a user