Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATReadWriteTests.cs
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00

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);
}
}