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>
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user