262 lines
10 KiB
C#
262 lines
10 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasReadWriteTests
|
|
{
|
|
private static (FocasDriver drv, FakeFocasClientFactory factory) NewDriver(params FocasTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeFocasClientFactory();
|
|
var drv = new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
|
|
Tags = tags,
|
|
Probe = new FocasProbeOptions { 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(FocasStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_PMC_read_returns_Good_value()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("Run", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)5 } };
|
|
|
|
var snapshots = await drv.ReadAsync(["Run"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
snapshots.Single().Value.ShouldBe((sbyte)5);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Parameter_read_routes_through_FocasAddress_Parameter_kind()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("Accel", "focas://10.0.0.5:8193", "PARAM:1820", FocasDataType.Int32));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { Values = { ["PARAM:1820"] = 1500 } };
|
|
|
|
var snapshots = await drv.ReadAsync(["Accel"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
snapshots.Single().Value.ShouldBe(1500);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Macro_read_routes_through_FocasAddress_Macro_kind()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("CustomVar", "focas://10.0.0.5:8193", "MACRO:500", FocasDataType.Float64));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { Values = { ["MACRO:500"] = 3.14159 } };
|
|
|
|
var snapshots = await drv.ReadAsync(["CustomVar"], CancellationToken.None);
|
|
snapshots.Single().Value.ShouldBe(3.14159);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Repeat_read_reuses_connection()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)1 } };
|
|
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
|
|
factory.Clients.Count.ShouldBe(1);
|
|
factory.Clients[0].ConnectCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FOCAS_error_status_maps_via_status_mapper()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("Ghost", "focas://10.0.0.5:8193", "R999", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () =>
|
|
{
|
|
var c = new FakeFocasClient();
|
|
c.ReadStatuses["R999"] = FocasStatusMapper.BadNodeIdUnknown;
|
|
return c;
|
|
};
|
|
|
|
var snapshots = await drv.ReadAsync(["Ghost"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_exception_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { ThrowOnRead = true };
|
|
|
|
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Connect_failure_disposes_client_and_surfaces_BadCommunicationError()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { ThrowOnConnect = true };
|
|
|
|
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
|
snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
|
|
factory.Clients[0].DisposeCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batched_reads_preserve_order_across_areas()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("A", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte),
|
|
new FocasTagDefinition("B", "focas://10.0.0.5:8193", "PARAM:1820", FocasDataType.Int32),
|
|
new FocasTagDefinition("C", "focas://10.0.0.5:8193", "MACRO:500", FocasDataType.Float64));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient
|
|
{
|
|
Values =
|
|
{
|
|
["R100"] = (sbyte)5,
|
|
["PARAM:1820"] = 1500,
|
|
["MACRO:500"] = 2.718,
|
|
},
|
|
};
|
|
|
|
var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None);
|
|
snapshots[0].Value.ShouldBe((sbyte)5);
|
|
snapshots[1].Value.ShouldBe(1500);
|
|
snapshots[2].Value.ShouldBe(2.718);
|
|
}
|
|
|
|
// ---- Write ----
|
|
|
|
[Fact]
|
|
public async Task Non_writable_tag_rejected_with_BadNotWritable()
|
|
{
|
|
var (drv, _) = NewDriver(
|
|
new FocasTagDefinition("RO", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte, Writable: false));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("RO", 1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNotWritable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Successful_write_logs_address_type_value()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("Speed", "focas://10.0.0.5:8193", "R100", FocasDataType.Int16));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Speed", (short)1800)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
var write = factory.Clients[0].WriteLog.Single();
|
|
write.addr.Canonical.ShouldBe("R100");
|
|
write.type.ShouldBe(FocasDataType.Int16);
|
|
write.value.ShouldBe((short)1800);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_status_code_maps_via_FocasStatusMapper()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("Protected", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () =>
|
|
{
|
|
var c = new FakeFocasClient();
|
|
c.WriteStatuses["R100"] = FocasStatusMapper.BadNotWritable;
|
|
return c;
|
|
};
|
|
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Protected", (sbyte)1)], CancellationToken.None);
|
|
results.Single().StatusCode.ShouldBe(FocasStatusMapper.BadNotWritable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batch_write_preserves_order_across_outcomes()
|
|
{
|
|
var factory = new FakeFocasClientFactory();
|
|
var drv = new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
|
|
Tags =
|
|
[
|
|
new FocasTagDefinition("A", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte),
|
|
new FocasTagDefinition("B", "focas://10.0.0.5:8193", "R101", FocasDataType.Byte, Writable: false),
|
|
],
|
|
Probe = new FocasProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var results = await drv.WriteAsync(
|
|
[
|
|
new WriteRequest("A", (sbyte)1),
|
|
new WriteRequest("B", (sbyte)2),
|
|
new WriteRequest("Unknown", (sbyte)3),
|
|
], CancellationToken.None);
|
|
|
|
results[0].StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
results[1].StatusCode.ShouldBe(FocasStatusMapper.BadNotWritable);
|
|
results[2].StatusCode.ShouldBe(FocasStatusMapper.BadNodeIdUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancellation_propagates()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient
|
|
{
|
|
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 FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)1 } };
|
|
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
|
|
factory.Clients[0].DisposeCount.ShouldBe(1);
|
|
}
|
|
}
|