231 lines
8.8 KiB
C#
231 lines
8.8 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.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipDriverWriteTests
|
|
{
|
|
private static (AbCipDriver drv, FakeAbCipTagFactory factory) NewDriver(params AbCipTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeAbCipTagFactory();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = tags,
|
|
}, "drv-1", factory);
|
|
return (drv, factory);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
|
|
{
|
|
var (drv, _) = NewDriver();
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("does-not-exist", 1)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Non_writable_tag_maps_to_BadNotWritable()
|
|
{
|
|
var (drv, _) = NewDriver(
|
|
new AbCipTagDefinition("ReadOnly", "ab://10.0.0.5/1,0", "RO", AbCipDataType.DInt, Writable: false));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("ReadOnly", 7)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNotWritable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_DInt_write_encodes_and_flushes()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Speed", 4200)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
factory.Tags["Motor1.Speed"].Value.ShouldBe(4200);
|
|
factory.Tags["Motor1.Speed"].WriteCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_in_dint_write_returns_BadNotSupported()
|
|
{
|
|
var factory = new FakeAbCipTagFactory { Customise = p => new ThrowingBoolBitFake(p) };
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Flags.3", AbCipDataType.Bool)],
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Flag3", true)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNotSupported);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Non_zero_libplctag_status_after_write_maps_via_AbCipStatusMapper()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new FakeAbCipTag(p) { Status = -5 /* timeout */ };
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Broken", 1)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadTimeout);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Type_mismatch_surfaces_BadTypeMismatch()
|
|
{
|
|
var (drv, _) = NewDriver(
|
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// Force a FormatException inside Convert.ToInt32 via a runtime that forwards to real Convert.
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new RealConvertFake(p),
|
|
};
|
|
var drv2 = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt)],
|
|
}, "drv-2", factory);
|
|
await drv2.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv2.WriteAsync(
|
|
[new WriteRequest("Speed", "not-a-number")], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overflow_surfaces_BadOutOfRange()
|
|
{
|
|
var factory = new FakeAbCipTagFactory { Customise = p => new RealConvertFake(p) };
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags = [new AbCipTagDefinition("Narrow", "ab://10.0.0.5/1,0", "N", AbCipDataType.Int)],
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Narrow", 1_000_000)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Exception_during_write_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new ThrowOnWriteFake(p);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Broken", 1)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batch_preserves_order_across_success_and_failure()
|
|
{
|
|
var factory = new FakeAbCipTagFactory();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
|
Tags =
|
|
[
|
|
new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt),
|
|
new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "B", AbCipDataType.DInt, Writable: false),
|
|
new AbCipTagDefinition("C", "ab://10.0.0.5/1,0", "C", AbCipDataType.DInt),
|
|
],
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[
|
|
new WriteRequest("A", 1),
|
|
new WriteRequest("B", 2),
|
|
new WriteRequest("UnknownTag", 3),
|
|
new WriteRequest("C", 4),
|
|
], CancellationToken.None);
|
|
|
|
results.Count.ShouldBe(4);
|
|
results[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
results[1].StatusCode.ShouldBe(AbCipStatusMapper.BadNotWritable);
|
|
results[2].StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
|
|
results[3].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancellation_propagates_from_write()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbCipTagDefinition("Slow", "ab://10.0.0.5/1,0", "Slow", AbCipDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = p => new CancelOnWriteFake(p);
|
|
|
|
await Should.ThrowAsync<OperationCanceledException>(
|
|
() => drv.WriteAsync([new WriteRequest("Slow", 1)], CancellationToken.None));
|
|
}
|
|
|
|
// ---- test-fake variants that exercise the real type / error handling ----
|
|
|
|
private sealed class RealConvertFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
|
|
{
|
|
public override void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
|
|
{
|
|
switch (type)
|
|
{
|
|
case AbCipDataType.Int: _ = Convert.ToInt16(value); break;
|
|
case AbCipDataType.DInt: _ = Convert.ToInt32(value); break;
|
|
default: _ = Convert.ToInt32(value); break;
|
|
}
|
|
Value = value;
|
|
}
|
|
}
|
|
|
|
private sealed class ThrowingBoolBitFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
|
|
{
|
|
public override void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
|
|
{
|
|
if (type == AbCipDataType.Bool && bitIndex is not null)
|
|
throw new NotSupportedException("bit-in-DINT deferred");
|
|
Value = value;
|
|
}
|
|
}
|
|
|
|
private sealed class ThrowOnWriteFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
|
|
{
|
|
public override Task WriteAsync(CancellationToken ct) =>
|
|
Task.FromException(new InvalidOperationException("wire dropped"));
|
|
}
|
|
|
|
private sealed class CancelOnWriteFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
|
|
{
|
|
public override Task WriteAsync(CancellationToken ct) =>
|
|
Task.FromException(new OperationCanceledException());
|
|
}
|
|
}
|