using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class AbCipUdtMemberLayoutTests { /// Verifies that packed atomic types get natural alignment offsets. [Fact] public void Packed_Atomics_Get_Natural_Alignment_Offsets() { // DInt (4 align) + Real (4) + Int (2) + LInt (8 — forces 2-byte pad before it) var members = new[] { new AbCipStructureMember("A", AbCipDataType.DInt), new AbCipStructureMember("B", AbCipDataType.Real), new AbCipStructureMember("C", AbCipDataType.Int), new AbCipStructureMember("D", AbCipDataType.LInt), }; var offsets = AbCipUdtMemberLayout.TryBuild(members); offsets.ShouldNotBeNull(); offsets!["A"].ShouldBe(0); offsets["B"].ShouldBe(4); offsets["C"].ShouldBe(8); // cursor at 10 after Int; LInt needs 8-byte alignment → pad to 16 offsets["D"].ShouldBe(16); } /// Verifies that signed integer types are packed without padding. [Fact] public void SInt_Packed_Without_Padding() { var members = new[] { new AbCipStructureMember("X", AbCipDataType.SInt), new AbCipStructureMember("Y", AbCipDataType.SInt), new AbCipStructureMember("Z", AbCipDataType.SInt), }; var offsets = AbCipUdtMemberLayout.TryBuild(members); offsets!["X"].ShouldBe(0); offsets["Y"].ShouldBe(1); offsets["Z"].ShouldBe(2); } /// Verifies that layout returns null when a member is a Bool type. [Fact] public void Returns_Null_When_Member_Is_Bool() { // BOOL storage in Logix UDTs is packed into a hidden host byte; declaration-only // layout can't place it. Grouping opts out; per-tag read path handles the member. var members = new[] { new AbCipStructureMember("A", AbCipDataType.DInt), new AbCipStructureMember("Flag", AbCipDataType.Bool), }; AbCipUdtMemberLayout.TryBuild(members).ShouldBeNull(); } /// Verifies that layout returns null when a member is a String or Structure type. [Fact] public void Returns_Null_When_Member_Is_String_Or_Structure() { AbCipUdtMemberLayout.TryBuild( new[] { new AbCipStructureMember("Name", AbCipDataType.String) }).ShouldBeNull(); AbCipUdtMemberLayout.TryBuild( new[] { new AbCipStructureMember("Nested", AbCipDataType.Structure) }).ShouldBeNull(); } /// Verifies that layout returns null when the member list is empty. [Fact] public void Returns_Null_On_Empty_Members() { AbCipUdtMemberLayout.TryBuild(Array.Empty()).ShouldBeNull(); } /// /// 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 /// (DecodeValueAt) 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. /// [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(); } }