124 lines
5.0 KiB
C#
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),
|
|
};
|
|
}
|
|
}
|