257 lines
9.8 KiB
C#
257 lines
9.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbLegacyReadWriteTests
|
|
{
|
|
private static (AbLegacyDriver drv, FakeAbLegacyTagFactory factory) NewDriver(params AbLegacyTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory();
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = tags,
|
|
}, "drv-1", factory);
|
|
return (drv, factory);
|
|
}
|
|
|
|
// ---- Read ----
|
|
|
|
[Fact]
|
|
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
|
{
|
|
var (drv, _) = NewDriver();
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["missing"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_N_file_read_returns_Good_value()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("Counter", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p) { Value = 42 };
|
|
|
|
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
|
|
|
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
snapshots.Single().Value.ShouldBe(42);
|
|
factory.Tags["N7:0"].InitializeCount.ShouldBe(1);
|
|
factory.Tags["N7:0"].ReadCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repeat_read_reuses_runtime()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 };
|
|
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
|
|
factory.Tags["N7:0"].InitializeCount.ShouldBe(1);
|
|
factory.Tags["N7:0"].ReadCount.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p) { Status = -14 };
|
|
|
|
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_exception_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnRead = true };
|
|
|
|
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batched_reads_preserve_order()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
|
|
new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "F8:0", AbLegacyDataType.Float),
|
|
new AbLegacyTagDefinition("C", "ab://10.0.0.5/1,0", "ST9:0", AbLegacyDataType.String));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => p.TagName switch
|
|
{
|
|
"N7:0" => new FakeAbLegacyTag(p) { Value = 1 },
|
|
"F8:0" => new FakeAbLegacyTag(p) { Value = 3.14f },
|
|
_ => new FakeAbLegacyTag(p) { Value = "hello" },
|
|
};
|
|
|
|
var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None);
|
|
|
|
snapshots.Count.ShouldBe(3);
|
|
snapshots[0].Value.ShouldBe(1);
|
|
snapshots[1].Value.ShouldBe(3.14f);
|
|
snapshots[2].Value.ShouldBe("hello");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_TagCreateParams_composed_from_device_and_profile()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:5", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
|
|
var p = factory.Tags["N7:5"].CreationParams;
|
|
p.Gateway.ShouldBe("10.0.0.5");
|
|
p.Port.ShouldBe(44818);
|
|
p.CipPath.ShouldBe("1,0");
|
|
p.LibplctagPlcAttribute.ShouldBe("slc500");
|
|
p.TagName.ShouldBe("N7:5");
|
|
}
|
|
|
|
// ---- Write ----
|
|
|
|
[Fact]
|
|
public async Task Non_writable_tag_rejects_with_BadNotWritable()
|
|
{
|
|
var (drv, _) = NewDriver(
|
|
new AbLegacyTagDefinition("RO", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, Writable: false));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("RO", 1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_N_file_write_encodes_and_flushes()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("X", 123)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
factory.Tags["N7:0"].Value.ShouldBe(123);
|
|
factory.Tags["N7:0"].WriteCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_within_word_write_rejected_as_BadNotSupported()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory { Customise = p => new RmwThrowingFake(p) };
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)],
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Bit3", true)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_exception_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnWrite = true };
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("X", 1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batch_write_preserves_order_across_outcomes()
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory();
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
|
|
new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int, Writable: false),
|
|
],
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[
|
|
new WriteRequest("A", 1),
|
|
new WriteRequest("B", 2),
|
|
new WriteRequest("Unknown", 3),
|
|
], CancellationToken.None);
|
|
|
|
results.Count.ShouldBe(3);
|
|
results[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
results[1].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable);
|
|
results[2].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancellation_propagates()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p)
|
|
{
|
|
ThrowOnRead = true,
|
|
Exception = new OperationCanceledException(),
|
|
};
|
|
|
|
await Should.ThrowAsync<OperationCanceledException>(
|
|
() => drv.ReadAsync(["X"], CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ShutdownAsync_disposes_runtimes()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 };
|
|
|
|
await drv.ReadAsync(["A"], CancellationToken.None);
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
|
|
factory.Tags["N7:0"].Disposed.ShouldBeTrue();
|
|
}
|
|
|
|
private sealed class RmwThrowingFake(AbLegacyTagCreateParams p) : FakeAbLegacyTag(p)
|
|
{
|
|
public override void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value)
|
|
{
|
|
if (type == AbLegacyDataType.Bit && bitIndex is not null)
|
|
throw new NotSupportedException("bit-within-word RMW deferred");
|
|
Value = value;
|
|
}
|
|
}
|
|
}
|