131 lines
5.5 KiB
C#
131 lines
5.5 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
/// <summary>
|
|
/// Task #194 — ReadAsync integration tests for the whole-UDT grouping path. The fake
|
|
/// runtime records ReadCount + surfaces member values by byte offset so we can assert
|
|
/// both "one read per parent UDT" and "each member decoded at the correct offset."
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipDriverWholeUdtReadTests
|
|
{
|
|
private const string Device = "ab://10.0.0.5/1,0";
|
|
|
|
private static (AbCipDriver drv, FakeAbCipTagFactory factory) NewDriver(params AbCipTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeAbCipTagFactory();
|
|
var opts = new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions(Device)],
|
|
Tags = tags,
|
|
};
|
|
return (new AbCipDriver(opts, "drv-1", factory), factory);
|
|
}
|
|
|
|
private static AbCipTagDefinition MotorUdt() => new(
|
|
"Motor", Device, "Motor", AbCipDataType.Structure, Members:
|
|
[
|
|
new AbCipStructureMember("Speed", AbCipDataType.DInt), // offset 0
|
|
new AbCipStructureMember("Torque", AbCipDataType.Real), // offset 4
|
|
]);
|
|
|
|
[Fact]
|
|
public async Task Two_members_of_same_udt_trigger_one_parent_read()
|
|
{
|
|
var (drv, factory) = NewDriver(MotorUdt());
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
|
|
|
|
snapshots.Count.ShouldBe(2);
|
|
snapshots[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
snapshots[1].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
|
|
// Factory should have created ONE runtime (for the parent "Motor") + issued ONE read.
|
|
// Without the optimization two runtimes (one per member) + two reads would appear.
|
|
factory.Tags.Count.ShouldBe(1);
|
|
factory.Tags.ShouldContainKey("Motor");
|
|
factory.Tags["Motor"].ReadCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Each_member_decodes_at_its_own_offset()
|
|
{
|
|
var (drv, factory) = NewDriver(MotorUdt());
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// Arrange the offset-keyed values before the read fires — the planner places
|
|
// Speed at offset 0 (DInt) and Torque at offset 4 (Real).
|
|
// The fake records CreationParams so we fetch it up front by the parent name.
|
|
var snapshotsTask = drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
|
|
// The factory creates the runtime inside ReadAsync; we need to set the offset map
|
|
// AFTER creation. Easier path: create the runtime on demand by reading once then
|
|
// re-arming. Instead: seed via a pre-read by constructing the fake in the factory's
|
|
// customise hook.
|
|
var snapshots = await snapshotsTask;
|
|
|
|
// First run establishes the runtime + gives the fake a chance to hold its reference.
|
|
factory.Tags["Motor"].ValuesByOffset[0] = 1234; // Speed
|
|
factory.Tags["Motor"].ValuesByOffset[4] = 9.5f; // Torque
|
|
|
|
snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
|
|
snapshots[0].Value.ShouldBe(1234);
|
|
snapshots[1].Value.ShouldBe(9.5f);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Parent_read_failure_stamps_every_grouped_member_Bad()
|
|
{
|
|
var (drv, factory) = NewDriver(MotorUdt());
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// Prime runtime existence via a first (successful) read so we can flip it to error.
|
|
await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
|
|
factory.Tags["Motor"].Status = -3; // libplctag BadTimeout — mapped in AbCipStatusMapper
|
|
|
|
var snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
|
|
|
|
snapshots.Count.ShouldBe(2);
|
|
snapshots[0].StatusCode.ShouldNotBe(AbCipStatusMapper.Good);
|
|
snapshots[0].Value.ShouldBeNull();
|
|
snapshots[1].StatusCode.ShouldNotBe(AbCipStatusMapper.Good);
|
|
snapshots[1].Value.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mixed_batch_groups_udt_and_falls_back_atomics()
|
|
{
|
|
var plain = new AbCipTagDefinition("PlainDint", Device, "PlainDint", AbCipDataType.DInt);
|
|
var (drv, factory) = NewDriver(MotorUdt(), plain);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(
|
|
["Motor.Speed", "PlainDint", "Motor.Torque"], CancellationToken.None);
|
|
|
|
snapshots.Count.ShouldBe(3);
|
|
// Motor parent ran one read, PlainDint ran its own read = 2 runtimes, 2 reads total.
|
|
factory.Tags.Count.ShouldBe(2);
|
|
factory.Tags.ShouldContainKey("Motor");
|
|
factory.Tags.ShouldContainKey("PlainDint");
|
|
factory.Tags["Motor"].ReadCount.ShouldBe(1);
|
|
factory.Tags["PlainDint"].ReadCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Single_member_of_Udt_uses_per_tag_read_path()
|
|
{
|
|
// One member of a UDT doesn't benefit from grouping — the planner demotes to
|
|
// fallback so the member-level runtime (distinct from the parent runtime) is used,
|
|
// matching pre-#194 behavior.
|
|
var (drv, factory) = NewDriver(MotorUdt());
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.ReadAsync(["Motor.Speed"], CancellationToken.None);
|
|
|
|
factory.Tags.ShouldContainKey("Motor.Speed");
|
|
factory.Tags.ShouldNotContainKey("Motor");
|
|
}
|
|
}
|