review(Driver.AbCip): fix declared UDT array members read as scalar (Medium)

Re-review at 7286d320. AbCip-016 (Medium): two cooperating defects made a declared array
member (e.g. REAL[4]) read one scalar/null — fan-out dropped ElementCount/IsArray, and
UdtMemberLayout.TryBuild ignored array members (mis-placing later members). Fix: thread
array shape through fan-out + opt whole-UDT grouping out when any member is an array + TDD.
AbCip-017 (severity-read StatusCode, Low) deferred.
This commit is contained in:
Joseph Doherty
2026-06-19 11:34:34 -04:00
parent db72dd1dca
commit a914b73d57
5 changed files with 181 additions and 4 deletions
@@ -156,6 +156,37 @@ public sealed class AbCipArrayTests
snapshots.Single().Value.ShouldNotBeOfType<int[]>();
}
/// <summary>
/// Driver.AbCip-016 — a DECLARED UDT member that is a 1-D array (<c>Setpoints : REAL[4]</c>)
/// must READ as a typed CLR array, matching the array node it discovers as. Before the fix
/// the member fan-out in <c>InitializeAsync</c> dropped the member's <c>ElementCount</c> /
/// <c>IsArray</c>, so the fanned-out runtime definition defaulted to scalar and the read
/// returned a single element (or null) instead of the array — a declared-type-vs-runtime-value
/// mismatch.
/// </summary>
[Fact]
public async Task Declared_udt_array_member_reads_as_typed_array()
{
var (drv, factory) = NewDriver(
new AbCipTagDefinition("Motor", "ab://10.0.0.5/1,0", "Motor", AbCipDataType.Structure,
Members:
[
new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4),
new AbCipStructureMember("Speed", AbCipDataType.DInt),
]));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new float[] { 1.5f, 2.5f, 3.5f, 4.5f });
var snapshots = await drv.ReadAsync(["Motor.Setpoints"], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
var value = snapshots.Single().Value.ShouldBeOfType<float[]>();
value.ShouldBe([1.5f, 2.5f, 3.5f, 4.5f]);
// The fanned-out member runtime must thread the member's element count to libplctag.
factory.Tags["Motor.Setpoints"].CreationParams.ElementCount.ShouldBe(4);
factory.Tags["Motor.Setpoints"].CreationParams.IsArray.ShouldBeTrue();
}
// ---- Resolver: arrayLength threading ----
/// <summary>The equipment-tag resolver threads arrayLength into the def's ElementCount.</summary>
@@ -74,4 +74,29 @@ public sealed class AbCipUdtMemberLayoutTests
{
AbCipUdtMemberLayout.TryBuild(Array.Empty<AbCipStructureMember>()).ShouldBeNull();
}
/// <summary>
/// Driver.AbCip-016 — declaration-only layout returns null when any member is a 1-D array.
/// The whole-UDT grouped read path decodes one scalar per member at its offset
/// (<c>DecodeValueAt</c>) and cannot return an array, and the scalar-size cursor advance
/// would mis-place every member after the array. Opting the whole group out sends array
/// members through the per-tag read path, which reads them as typed arrays.
/// </summary>
[Fact]
public void Returns_Null_When_A_Member_Is_An_Array()
{
// Explicit IsArray flag (even a 1-element array).
AbCipUdtMemberLayout.TryBuild(new[]
{
new AbCipStructureMember("A", AbCipDataType.DInt),
new AbCipStructureMember("Buf", AbCipDataType.Real, IsArray: true, ElementCount: 1),
}).ShouldBeNull();
// Legacy ElementCount > 1 with the flag unset.
AbCipUdtMemberLayout.TryBuild(new[]
{
new AbCipStructureMember("A", AbCipDataType.DInt),
new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4),
}).ShouldBeNull();
}
}