256 lines
10 KiB
C#
256 lines
10 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class TwinCATReadWriteTests
|
|
{
|
|
private static (TwinCATDriver drv, FakeTwinCATClientFactory factory) NewDriver(params TwinCATTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
|
Tags = tags,
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "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(TwinCATStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_DInt_read_returns_Good_value()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 4200 } };
|
|
|
|
var snapshots = await drv.ReadAsync(["Speed"], CancellationToken.None);
|
|
|
|
snapshots.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
|
snapshots.Single().Value.ShouldBe(4200);
|
|
factory.Clients[0].ConnectCount.ShouldBe(1);
|
|
factory.Clients[0].IsConnected.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repeat_read_reuses_connection()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "GVL.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.X"] = 1 } };
|
|
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
|
|
// One client, one connect — subsequent calls reuse the connected client.
|
|
factory.Clients.Count.ShouldBe(1);
|
|
factory.Clients[0].ConnectCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_with_ADS_error_maps_via_status_mapper()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("Ghost", "ads://5.23.91.23.1.1:851", "MAIN.Missing", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () =>
|
|
{
|
|
var c = new FakeTwinCATClient();
|
|
c.ReadStatuses["MAIN.Missing"] = TwinCATStatusMapper.BadNodeIdUnknown;
|
|
return c;
|
|
};
|
|
|
|
var snapshots = await drv.ReadAsync(["Ghost"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_exception_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { ThrowOnRead = true };
|
|
|
|
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Connect_failure_surfaces_BadCommunicationError_and_disposes_client()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { ThrowOnConnect = true };
|
|
|
|
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
|
|
factory.Clients[0].DisposeCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batched_reads_preserve_order()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
|
|
new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.Real),
|
|
new TwinCATTagDefinition("C", "ads://5.23.91.23.1.1:851", "MAIN.C", TwinCATDataType.String));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient
|
|
{
|
|
Values =
|
|
{
|
|
["MAIN.A"] = 1,
|
|
["MAIN.B"] = 3.14f,
|
|
["MAIN.C"] = "hello",
|
|
},
|
|
};
|
|
|
|
var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None);
|
|
snapshots[0].Value.ShouldBe(1);
|
|
snapshots[1].Value.ShouldBe(3.14f);
|
|
snapshots[2].Value.ShouldBe("hello");
|
|
}
|
|
|
|
// ---- Write ----
|
|
|
|
[Fact]
|
|
public async Task Non_writable_tag_rejected_with_BadNotWritable()
|
|
{
|
|
var (drv, _) = NewDriver(
|
|
new TwinCATTagDefinition("RO", "ads://5.23.91.23.1.1:851", "MAIN.RO", TwinCATDataType.DInt, Writable: false));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("RO", 1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNotWritable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_write_logs_symbol_type_value()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Speed", 4200)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
|
var write = factory.Clients[0].WriteLog.Single();
|
|
write.symbol.ShouldBe("MAIN.Speed");
|
|
write.type.ShouldBe(TwinCATDataType.DInt);
|
|
write.value.ShouldBe(4200);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_with_ADS_error_surfaces_mapped_status()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () =>
|
|
{
|
|
var c = new FakeTwinCATClient();
|
|
c.WriteStatuses["MAIN.X"] = TwinCATStatusMapper.BadNotWritable;
|
|
return c;
|
|
};
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("X", 1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNotWritable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_exception_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { ThrowOnWrite = true };
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("X", 1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batch_write_preserves_order_across_outcomes()
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
|
Tags =
|
|
[
|
|
new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
|
|
new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.DInt, Writable: false),
|
|
],
|
|
Probe = new TwinCATProbeOptions { Enabled = 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(TwinCATStatusMapper.Good);
|
|
results[1].StatusCode.ShouldBe(TwinCATStatusMapper.BadNotWritable);
|
|
results[2].StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancellation_propagates()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient
|
|
{
|
|
ThrowOnRead = true,
|
|
Exception = new OperationCanceledException(),
|
|
};
|
|
|
|
await Should.ThrowAsync<OperationCanceledException>(
|
|
() => drv.ReadAsync(["X"], CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ShutdownAsync_disposes_client()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.X"] = 1 } };
|
|
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
|
|
factory.Clients[0].DisposeCount.ShouldBe(1);
|
|
}
|
|
}
|