fc94b87634
P11: AbCipUdtReadPlanner called AbCipUdtMemberLayout.TryBuild(parent.Members) for every member reference on every poll - O(members^2) per parent per tick, a dictionary allocation each time. GetOrBuild caches the layout (including a "not groupable" verdict) in a ConditionalWeakTable keyed by the member list's identity, which is immutable for the life of the tag table; a re-init's new lists replace the entries and the old ones are collected with them. Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
125 lines
5.1 KiB
C#
125 lines
5.1 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipUdtMemberLayoutTests
|
|
{
|
|
/// <summary>Verifies that packed atomic types get natural alignment offsets.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that signed integer types are packed without padding.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that layout returns null when a member is a Bool type.</summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>Verifies that layout returns null when a member is a String or Structure type.</summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>Verifies that layout returns null when the member list is empty.</summary>
|
|
[Fact]
|
|
public void Returns_Null_On_Empty_Members()
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>Review 07 P11: the layout is computed once per member list, not once per member per poll.</summary>
|
|
[Fact]
|
|
public void GetOrBuild_computes_a_layout_once_per_member_list()
|
|
{
|
|
IReadOnlyList<AbCipStructureMember> members =
|
|
[
|
|
new AbCipStructureMember("A", AbCipDataType.DInt),
|
|
new AbCipStructureMember("B", AbCipDataType.Real),
|
|
];
|
|
var first = AbCipUdtMemberLayout.GetOrBuild(members);
|
|
first.ShouldNotBeNull();
|
|
first["B"].ShouldBe(4);
|
|
AbCipUdtMemberLayout.GetOrBuild(members).ShouldBeSameAs(first);
|
|
|
|
// Positive control: a different (equal-content) list is its own entry, and a not-groupable verdict caches too.
|
|
IReadOnlyList<AbCipStructureMember> copy = [.. members];
|
|
AbCipUdtMemberLayout.GetOrBuild(copy).ShouldNotBeSameAs(first);
|
|
IReadOnlyList<AbCipStructureMember> withBool = [new AbCipStructureMember("F", AbCipDataType.Bool)];
|
|
AbCipUdtMemberLayout.GetOrBuild(withBool).ShouldBeNull();
|
|
AbCipUdtMemberLayout.GetOrBuild(withBool).ShouldBeNull();
|
|
}
|
|
}
|