fix(abcip): explicit IsArray flag so 1-element arrays read as arrays (review I-1)

This commit is contained in:
Joseph Doherty
2026-06-16 22:14:41 -04:00
parent ce5d46be08
commit 94e8c55b5c
6 changed files with 172 additions and 42 deletions
@@ -181,22 +181,111 @@ public sealed class AbCipArrayTests
factory.Tags["Recipe"].CreationParams.ElementCount.ShouldBe(4);
}
/// <summary>The parser threads arrayLength into the transient definition's ElementCount.</summary>
/// <summary>The parser threads arrayLength into the transient definition's ElementCount and sets IsArray.</summary>
[Fact]
public void Parser_threads_arrayLength_into_ElementCount()
{
var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":8}""";
AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
def!.ElementCount.ShouldBe(8);
def.IsArray.ShouldBeTrue();
}
/// <summary>A non-array equipment ref defaults ElementCount to 1 (scalar).</summary>
/// <summary>A non-array equipment ref defaults ElementCount to 1 (scalar) and IsArray false.</summary>
[Fact]
public void Parser_defaults_ElementCount_to_one_when_not_an_array()
{
var json = """{"tagPath":"Recipe","dataType":"DInt"}""";
AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
def!.ElementCount.ShouldBe(1);
def.IsArray.ShouldBeFalse();
}
/// <summary>
/// Review finding I-1 — a 1-element array (<c>isArray:true, arrayLength:1</c>) is a valid
/// 1-element array, NOT a scalar: the parser sets <see cref="AbCipTagDefinition.IsArray"/>
/// true and <see cref="AbCipTagDefinition.ElementCount"/> 1.
/// </summary>
[Fact]
public void Parser_treats_isArray_with_arrayLength_one_as_a_one_element_array()
{
var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":1}""";
AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
def!.IsArray.ShouldBeTrue();
def.ElementCount.ShouldBe(1);
}
/// <summary>
/// Review finding I-1 — <c>isArray:true, arrayLength:1</c> must DISCOVER as a [1] array node
/// (IsArray + ArrayDim 1), matching the foundation's materialisation, not as a scalar.
/// </summary>
[Fact]
public async Task Equipment_ref_isArray_arrayLength_one_discovers_as_one_element_array()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":1}""";
AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
var builder = new RecordingBuilder();
var (drv, _) = NewDriver(def!);
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.DiscoverAsync(builder, CancellationToken.None);
var arr = builder.Variables.Single().Info;
arr.IsArray.ShouldBeTrue();
arr.ArrayDim.ShouldBe(1u);
}
/// <summary>
/// Review finding I-1 — the I-1 case: an <c>isArray:true, arrayLength:1</c> equipment tag
/// reads a 1-ELEMENT typed array, NOT a scalar. On current code (gate <c>ElementCount &gt; 1</c>)
/// this reads a scalar; the explicit IsArray flag fixes it.
/// </summary>
[Fact]
public async Task Equipment_ref_isArray_arrayLength_one_reads_as_one_element_array()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":1}""";
var factory = new FakeAbCipTagFactory();
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [],
};
var drv = new AbCipDriver(opts, "abcip-eq-array1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new int[] { 99 });
var snapshots = await drv.ReadAsync([json], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
var value = snapshots.Single().Value.ShouldBeOfType<int[]>();
value.ShouldBe([99]);
// A 1-element array still threads elem_count 1 to libplctag.
factory.Tags["Recipe"].CreationParams.ElementCount.ShouldBe(1);
}
/// <summary>
/// Regression — a genuinely scalar equipment ref (<c>isArray:false</c>) reads a boxed
/// scalar via <see cref="IAbCipTagRuntime.DecodeValue"/>, never an array.
/// </summary>
[Fact]
public async Task Equipment_ref_isArray_false_reads_as_scalar()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Speed","dataType":"DInt","isArray":false}""";
var factory = new FakeAbCipTagFactory();
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [],
};
var drv = new AbCipDriver(opts, "abcip-eq-scalar", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbCipTag(p) { Value = 4200 };
var snapshots = await drv.ReadAsync([json], CancellationToken.None);
snapshots.Single().Value.ShouldBe(4200);
snapshots.Single().Value.ShouldNotBeOfType<int[]>();
}
// ---- helpers ----