Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUdtReadPlannerTests.cs
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00

124 lines
5.0 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
[Trait("Category", "Unit")]
public sealed class AbCipUdtReadPlannerTests
{
private const string Device = "ab://10.0.0.1/1,0";
[Fact]
public void Groups_Two_Members_Of_The_Same_Udt_Parent()
{
var tags = BuildUdtTagMap(out var _);
var plan = AbCipUdtReadPlanner.Build(new[] { "Motor.Speed", "Motor.Torque" }, tags);
plan.Groups.Count.ShouldBe(1);
plan.Groups[0].ParentName.ShouldBe("Motor");
plan.Groups[0].Members.Count.ShouldBe(2);
plan.Fallbacks.Count.ShouldBe(0);
}
[Fact]
public void Single_Member_Reference_Falls_Back_To_Per_Tag_Path()
{
// Reading just one member of a UDT gains nothing from grouping — one whole-UDT read
// vs one member read is equivalent cost but more client-side work. Planner demotes.
var tags = BuildUdtTagMap(out var _);
var plan = AbCipUdtReadPlanner.Build(new[] { "Motor.Speed" }, tags);
plan.Groups.ShouldBeEmpty();
plan.Fallbacks.Count.ShouldBe(1);
plan.Fallbacks[0].Reference.ShouldBe("Motor.Speed");
}
[Fact]
public void Unknown_References_Fall_Back_Without_Affecting_Groups()
{
var tags = BuildUdtTagMap(out var _);
var plan = AbCipUdtReadPlanner.Build(
new[] { "Motor.Speed", "Motor.Torque", "DoesNotExist", "Motor.NonMember" }, tags);
plan.Groups.Count.ShouldBe(1);
plan.Groups[0].Members.Count.ShouldBe(2);
plan.Fallbacks.Count.ShouldBe(2);
plan.Fallbacks.ShouldContain(f => f.Reference == "DoesNotExist");
plan.Fallbacks.ShouldContain(f => f.Reference == "Motor.NonMember");
}
[Fact]
public void Atomic_Top_Level_Tag_Falls_Back_Untouched()
{
var tags = BuildUdtTagMap(out var _);
tags = new Dictionary<string, AbCipTagDefinition>(tags, StringComparer.OrdinalIgnoreCase)
{
["PlainDint"] = new("PlainDint", Device, "PlainDint", AbCipDataType.DInt),
};
var plan = AbCipUdtReadPlanner.Build(new[] { "Motor.Speed", "Motor.Torque", "PlainDint" }, tags);
plan.Groups.Count.ShouldBe(1);
plan.Fallbacks.Count.ShouldBe(1);
plan.Fallbacks[0].Reference.ShouldBe("PlainDint");
}
[Fact]
public void Udt_With_Bool_Member_Does_Not_Group()
{
// Any BOOL in the declared members disqualifies the group — offset rules for BOOL
// can't be determined from declaration alone (Logix packs them into a hidden host
// byte). Fallback path reads each member individually.
var members = new[]
{
new AbCipStructureMember("Run", AbCipDataType.Bool),
new AbCipStructureMember("Speed", AbCipDataType.DInt),
};
var parent = new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure,
Members: members);
var tags = new Dictionary<string, AbCipTagDefinition>(StringComparer.OrdinalIgnoreCase)
{
["Motor"] = parent,
["Motor.Run"] = new("Motor.Run", Device, "Motor.Run", AbCipDataType.Bool),
["Motor.Speed"] = new("Motor.Speed", Device, "Motor.Speed", AbCipDataType.DInt),
};
var plan = AbCipUdtReadPlanner.Build(new[] { "Motor.Run", "Motor.Speed" }, tags);
plan.Groups.ShouldBeEmpty();
plan.Fallbacks.Count.ShouldBe(2);
}
[Fact]
public void Original_Indices_Preserved_For_Out_Of_Order_Batches()
{
var tags = BuildUdtTagMap(out var _);
var plan = AbCipUdtReadPlanner.Build(
new[] { "Other", "Motor.Speed", "DoesNotExist", "Motor.Torque" }, tags);
// Motor.Speed was at index 1, Motor.Torque at 3 — must survive through the plan so
// ReadAsync can write decoded values back at the right output slot.
plan.Groups.ShouldHaveSingleItem();
var group = plan.Groups[0];
group.Members.ShouldContain(m => m.OriginalIndex == 1 && m.Definition.Name == "Motor.Speed");
group.Members.ShouldContain(m => m.OriginalIndex == 3 && m.Definition.Name == "Motor.Torque");
plan.Fallbacks.ShouldContain(f => f.OriginalIndex == 0 && f.Reference == "Other");
plan.Fallbacks.ShouldContain(f => f.OriginalIndex == 2 && f.Reference == "DoesNotExist");
}
private static Dictionary<string, AbCipTagDefinition> BuildUdtTagMap(out AbCipTagDefinition parent)
{
var members = new[]
{
new AbCipStructureMember("Speed", AbCipDataType.DInt),
new AbCipStructureMember("Torque", AbCipDataType.Real),
};
parent = new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure, Members: members);
return new Dictionary<string, AbCipTagDefinition>(StringComparer.OrdinalIgnoreCase)
{
["Motor"] = parent,
["Motor.Speed"] = new("Motor.Speed", Device, "Motor.Speed", AbCipDataType.DInt),
["Motor.Torque"] = new("Motor.Torque", Device, "Motor.Torque", AbCipDataType.Real),
};
}
}