Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/AbCipEmulateMultiPacketReadTests.cs

101 lines
5.2 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;
/// <summary>
/// PR abcip-3.3 — golden-box-tier MultiPacket read-strategy test against Logix Emulate.
/// Exercises the sparse-UDT case the strategy is designed for: a 50-member UDT instance
/// where the OPC UA client subscribed to 5 members. Asserts the driver routes the read
/// through the MultiPacket planner (<see cref="AbCipDriver.DeviceState.MultiPacketGroupsExecuted"/>
/// counter increments) and returns Good StatusCodes for every member.
/// </summary>
/// <remarks>
/// <para><b>Required Emulate project state</b> (see <c>LogixProject/README.md</c> for
/// the L5X export that seeds this; ship the project once Emulate is on the integration
/// host):</para>
/// <list type="bullet">
/// <item>UDT <c>Tank_50</c> with 50 DINT members <c>M0</c>..<c>M49</c> — a deliberately
/// oversized UDT so a 5-member subscription is sparse enough for the
/// <see cref="AbCipDeviceOptions.MultiPacketSparsityThreshold"/> default of 0.25 to
/// pick MultiPacket.</item>
/// <item>Controller-scope tag <c>Tank1 : Tank_50</c> with each <c>M{i}</c> seeded to
/// <c>i * 10</c> so each subscribed member returns a distinct value.</item>
/// </list>
/// <para>Runs only when <c>AB_SERVER_PROFILE=emulate</c>. With the default ab_server the
/// test skips cleanly — ab_server lacks UDT / Multi-Service-Packet emulation depth so a
/// wire-level pass against it would be vacuous regardless. Note: the libplctag .NET
/// wrapper (1.5.x) does not expose explicit Multi-Service-Packet bundling, so the
/// driver's MultiPacket runtime today issues N member reads sequentially. The planner-tier
/// dispatch is what's under test here — the wire-level bundling lands when the upstream
/// wrapper exposes the 0x0A service primitive (see
/// <c>docs/drivers/AbCip-Performance.md</c> §"Read strategy").</para>
/// </remarks>
[Collection("AbServerEmulate")]
[Trait("Category", "Integration")]
[Trait("Tier", "Emulate")]
public sealed class AbCipEmulateMultiPacketReadTests
{
[AbServerFact]
public async Task Sparse_5_of_50_member_subscription_dispatches_through_MultiPacket()
{
AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);
var endpoint = Environment.GetEnvironmentVariable("AB_SERVER_ENDPOINT")
?? throw new InvalidOperationException(
"AB_SERVER_ENDPOINT must be set to the Logix Emulate instance " +
"(e.g. '10.0.0.42:44818') when AB_SERVER_PROFILE=emulate.");
// Build a 50-member declared UDT — the planner needs the full member set to compute
// the subscribed-fraction in the Auto heuristic and to place MultiPacket member offsets.
var members = new AbCipStructureMember[50];
for (var i = 0; i < 50; i++)
members[i] = new AbCipStructureMember($"M{i}", AbCipDataType.DInt);
var options = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(
HostAddress: $"ab://{endpoint}/1,0",
PlcFamily: AbCipPlcFamily.ControlLogix,
ReadStrategy: ReadStrategy.MultiPacket)],
Tags = [
new AbCipTagDefinition(
Name: "Tank1",
DeviceHostAddress: $"ab://{endpoint}/1,0",
TagPath: "Tank1",
DataType: AbCipDataType.Structure,
Members: members),
],
Timeout = TimeSpan.FromSeconds(5),
};
await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-multipacket-smoke");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
// Sparse pick: 5 of 50 = 0.10 < default threshold 0.25 → MultiPacket planner. Force
// the strategy explicitly above so the test isn't sensitive to threshold drift.
var refs = new[] { "Tank1.M0", "Tank1.M3", "Tank1.M7", "Tank1.M22", "Tank1.M49" };
var snapshots = await drv.ReadAsync(refs, TestContext.Current.CancellationToken);
snapshots.Count.ShouldBe(5);
foreach (var s in snapshots) s.StatusCode.ShouldBe(AbCipStatusMapper.Good);
// Plan-stats counter assertion — the device-level counter increments once per parent
// UDT routed through the MultiPacket path. Sibling counter for WholeUdt must stay zero.
var deviceState = drv.GetDeviceState($"ab://{endpoint}/1,0");
deviceState.ShouldNotBeNull();
deviceState!.MultiPacketGroupsExecuted.ShouldBeGreaterThan(0);
deviceState.WholeUdtGroupsExecuted.ShouldBe(0);
// Sanity-check the seeded values land at the right indices: M{i} == i * 10 in the
// emulate fixture's startup routine.
Convert.ToInt32(snapshots[0].Value).ShouldBe(0);
Convert.ToInt32(snapshots[1].Value).ShouldBe(30);
Convert.ToInt32(snapshots[2].Value).ShouldBe(70);
Convert.ToInt32(snapshots[3].Value).ShouldBe(220);
Convert.ToInt32(snapshots[4].Value).ShouldBe(490);
}
}