review(Driver.AbCip): thread ElementCount/IsArray through factory tag DTOs

Cross-module fix from the review sweep. -018 (Medium): AbCipTagDto/AbCipMemberDto dropped
elementCount/isArray, so driver-config-authored array tags read as a single scalar. Added the
two optional JSON fields (additive; missing -> scalar as before) threaded into the tag def +
TDD.
This commit is contained in:
Joseph Doherty
2026-06-19 12:29:40 -04:00
parent 40749d3f67
commit 3e789dcafc
3 changed files with 190 additions and 3 deletions
@@ -0,0 +1,119 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
/// <summary>
/// Regression coverage for Driver.AbCip-018 — the driver-config factory path
/// (<see cref="AbCipDriverFactoryExtensions.ParseOptions"/>) dropped the array shape of a
/// pre-declared tag. A <c>tags[]</c> entry with <c>"isArray": true, "elementCount": 4</c>
/// deserialised into a scalar <see cref="AbCipTagDefinition"/> (because <c>AbCipTagDto</c> /
/// <c>AbCipMemberDto</c> had no <c>ElementCount</c> / <c>IsArray</c> fields), so the tag read
/// as a single scalar despite the explicit array declaration. These tests assert the array
/// shape now survives factory deserialization for both top-level tags and UDT members, and
/// that omitting the fields keeps the legacy scalar default (additive change — no break).
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipFactoryArrayTagTests
{
/// <summary>A driver-config tag declaring isArray/elementCount produces an array definition.</summary>
[Fact]
public void ParseOptions_threads_isArray_and_elementCount_into_tag_definition()
{
const string json = """
{
"Tags": [ {
"Name": "Setpoints",
"DeviceHostAddress": "ab://10.0.0.5/1,0",
"TagPath": "Setpoints",
"DataType": "Real",
"isArray": true,
"elementCount": 4
} ]
}
""";
var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
var tag = opts.Tags.Single();
tag.IsArray.ShouldBeTrue();
tag.ElementCount.ShouldBe(4);
}
/// <summary>A driver-config UDT member declaring isArray/elementCount produces an array member.</summary>
[Fact]
public void ParseOptions_threads_isArray_and_elementCount_into_structure_member()
{
const string json = """
{
"Tags": [ {
"Name": "Motor",
"DeviceHostAddress": "ab://10.0.0.5/1,0",
"TagPath": "Motor",
"DataType": "Structure",
"Members": [ {
"Name": "Setpoints",
"DataType": "Real",
"isArray": true,
"elementCount": 4
} ]
} ]
}
""";
var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
var member = opts.Tags.Single().Members!.Single();
member.IsArray.ShouldBeTrue();
member.ElementCount.ShouldBe(4);
}
/// <summary>A driver-config tag without array fields stays scalar (additive change — no break).</summary>
[Fact]
public void ParseOptions_defaults_to_scalar_when_array_fields_absent()
{
const string json = """
{
"Tags": [ {
"Name": "Speed",
"DeviceHostAddress": "ab://10.0.0.5/1,0",
"TagPath": "Speed",
"DataType": "DInt"
} ]
}
""";
var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
var tag = opts.Tags.Single();
tag.IsArray.ShouldBeFalse();
tag.ElementCount.ShouldBe(1);
}
/// <summary>A non-positive elementCount falls back to the scalar count of 1 (positive-value guard).</summary>
[Fact]
public void ParseOptions_guards_against_non_positive_elementCount()
{
const string json = """
{
"Tags": [ {
"Name": "Speed",
"DeviceHostAddress": "ab://10.0.0.5/1,0",
"TagPath": "Speed",
"DataType": "DInt",
"isArray": true,
"elementCount": 0
} ]
}
""";
var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
var tag = opts.Tags.Single();
// elementCount <= 0 is degenerate — count clamps to 1; the explicit IsArray flag still
// carries (a 1-element array is valid), mirroring the equipment-tag parser's contract.
tag.ElementCount.ShouldBe(1);
tag.IsArray.ShouldBeTrue();
}
}