using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class AbCipArrayReadPlannerTests { private const string Device = "ab://10.0.0.5/1,0"; private static AbCipTagCreateParams BaseParams(string tagName) => new( Gateway: "10.0.0.5", Port: 44818, CipPath: "1,0", LibplctagPlcAttribute: "controllogix", TagName: tagName, Timeout: TimeSpan.FromSeconds(5)); [Fact] public void TryBuild_emits_single_tag_create_with_element_count() { var def = new AbCipTagDefinition("DataSlice", Device, "Data[0..15]", AbCipDataType.DInt); var parsed = AbCipTagPath.TryParse(def.TagPath)!; var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..15]")); plan.ShouldNotBeNull(); plan.ElementType.ShouldBe(AbCipDataType.DInt); plan.Stride.ShouldBe(4); plan.Slice.Count.ShouldBe(16); plan.CreateParams.ElementCount.ShouldBe(16); // Anchored at the slice start; libplctag reads N consecutive elements from there. plan.CreateParams.TagName.ShouldBe("Data[0]"); } [Fact] public void TryBuild_returns_null_when_path_has_no_slice() { var def = new AbCipTagDefinition("Plain", Device, "Data[3]", AbCipDataType.DInt); var parsed = AbCipTagPath.TryParse(def.TagPath)!; AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[3]")).ShouldBeNull(); } [Theory] [InlineData(AbCipDataType.Bool)] [InlineData(AbCipDataType.String)] [InlineData(AbCipDataType.Structure)] public void TryBuild_returns_null_for_unsupported_element_types(AbCipDataType type) { var def = new AbCipTagDefinition("Slice", Device, "Data[0..3]", type); var parsed = AbCipTagPath.TryParse(def.TagPath)!; AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..3]")).ShouldBeNull(); } [Theory] [InlineData(AbCipDataType.SInt, 1)] [InlineData(AbCipDataType.Int, 2)] [InlineData(AbCipDataType.DInt, 4)] [InlineData(AbCipDataType.Real, 4)] [InlineData(AbCipDataType.LInt, 8)] [InlineData(AbCipDataType.LReal, 8)] public void TryBuild_uses_natural_stride_per_element_type(AbCipDataType type, int expectedStride) { var def = new AbCipTagDefinition("Slice", Device, "Data[0..3]", type); var parsed = AbCipTagPath.TryParse(def.TagPath)!; var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..3]"))!; plan.Stride.ShouldBe(expectedStride); } [Fact] public void Decode_walks_buffer_at_element_stride() { var def = new AbCipTagDefinition("DataSlice", Device, "Data[0..3]", AbCipDataType.DInt); var parsed = AbCipTagPath.TryParse(def.TagPath)!; var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..3]"))!; var fake = new FakeAbCipTag(plan.CreateParams); // Stride == 4 for DInt, so offsets 0/4/8/12 hold the four element values. fake.ValuesByOffset[0] = 100; fake.ValuesByOffset[4] = 200; fake.ValuesByOffset[8] = 300; fake.ValuesByOffset[12] = 400; var decoded = AbCipArrayReadPlanner.Decode(plan, fake); decoded.Length.ShouldBe(4); decoded.ShouldBe(new object?[] { 100, 200, 300, 400 }); } [Fact] public void Decode_preserves_slice_count_for_real_arrays() { var def = new AbCipTagDefinition("FloatSlice", Device, "Floats[2..5]", AbCipDataType.Real); var parsed = AbCipTagPath.TryParse(def.TagPath)!; var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Floats[2]"))!; var fake = new FakeAbCipTag(plan.CreateParams); fake.ValuesByOffset[0] = 1.5f; fake.ValuesByOffset[4] = 2.5f; fake.ValuesByOffset[8] = 3.5f; fake.ValuesByOffset[12] = 4.5f; var decoded = AbCipArrayReadPlanner.Decode(plan, fake); decoded.ShouldBe(new object?[] { 1.5f, 2.5f, 3.5f, 4.5f }); } }