230 lines
8.5 KiB
C#
230 lines
8.5 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 FocasScaffoldingTests
|
|
{
|
|
// ---- FocasHostAddress ----
|
|
|
|
[Theory]
|
|
[InlineData("focas://10.0.0.5:8193", "10.0.0.5", 8193)]
|
|
[InlineData("focas://10.0.0.5", "10.0.0.5", 8193)] // default port
|
|
[InlineData("focas://cnc-01.factory.internal:8193", "cnc-01.factory.internal", 8193)]
|
|
[InlineData("focas://10.0.0.5:12345", "10.0.0.5", 12345)]
|
|
[InlineData("FOCAS://10.0.0.5:8193", "10.0.0.5", 8193)] // case-insensitive scheme
|
|
public void HostAddress_parses_valid(string input, string host, int port)
|
|
{
|
|
var parsed = FocasHostAddress.TryParse(input);
|
|
parsed.ShouldNotBeNull();
|
|
parsed.Host.ShouldBe(host);
|
|
parsed.Port.ShouldBe(port);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("http://10.0.0.5/")]
|
|
[InlineData("focas:10.0.0.5:8193")] // missing //
|
|
[InlineData("focas://")] // empty body
|
|
[InlineData("focas://10.0.0.5:0")] // port 0
|
|
[InlineData("focas://10.0.0.5:65536")] // port out of range
|
|
[InlineData("focas://10.0.0.5:abc")] // non-numeric port
|
|
public void HostAddress_rejects_invalid(string? input)
|
|
{
|
|
FocasHostAddress.TryParse(input).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void HostAddress_ToString_strips_default_port()
|
|
{
|
|
new FocasHostAddress("10.0.0.5", 8193).ToString().ShouldBe("focas://10.0.0.5");
|
|
new FocasHostAddress("10.0.0.5", 12345).ToString().ShouldBe("focas://10.0.0.5:12345");
|
|
}
|
|
|
|
// ---- FocasAddress ----
|
|
|
|
[Theory]
|
|
[InlineData("X0.0", FocasAreaKind.Pmc, "X", 0, 0)]
|
|
[InlineData("X0", FocasAreaKind.Pmc, "X", 0, null)]
|
|
[InlineData("Y10", FocasAreaKind.Pmc, "Y", 10, null)]
|
|
[InlineData("F20.3", FocasAreaKind.Pmc, "F", 20, 3)]
|
|
[InlineData("G54", FocasAreaKind.Pmc, "G", 54, null)]
|
|
[InlineData("R100", FocasAreaKind.Pmc, "R", 100, null)]
|
|
[InlineData("D200", FocasAreaKind.Pmc, "D", 200, null)]
|
|
[InlineData("C300", FocasAreaKind.Pmc, "C", 300, null)]
|
|
[InlineData("K400", FocasAreaKind.Pmc, "K", 400, null)]
|
|
[InlineData("A500", FocasAreaKind.Pmc, "A", 500, null)]
|
|
[InlineData("E600", FocasAreaKind.Pmc, "E", 600, null)]
|
|
[InlineData("T50.4", FocasAreaKind.Pmc, "T", 50, 4)]
|
|
public void Address_parses_PMC_forms(string input, FocasAreaKind kind, string letter, int num, int? bit)
|
|
{
|
|
var a = FocasAddress.TryParse(input);
|
|
a.ShouldNotBeNull();
|
|
a.Kind.ShouldBe(kind);
|
|
a.PmcLetter.ShouldBe(letter);
|
|
a.Number.ShouldBe(num);
|
|
a.BitIndex.ShouldBe(bit);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("PARAM:1020", FocasAreaKind.Parameter, 1020, null)]
|
|
[InlineData("PARAM:1815/0", FocasAreaKind.Parameter, 1815, 0)]
|
|
[InlineData("PARAM:1815/31", FocasAreaKind.Parameter, 1815, 31)]
|
|
public void Address_parses_parameter_forms(string input, FocasAreaKind kind, int num, int? bit)
|
|
{
|
|
var a = FocasAddress.TryParse(input);
|
|
a.ShouldNotBeNull();
|
|
a.Kind.ShouldBe(kind);
|
|
a.PmcLetter.ShouldBeNull();
|
|
a.Number.ShouldBe(num);
|
|
a.BitIndex.ShouldBe(bit);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("MACRO:100", FocasAreaKind.Macro, 100)]
|
|
[InlineData("MACRO:500", FocasAreaKind.Macro, 500)]
|
|
public void Address_parses_macro_forms(string input, FocasAreaKind kind, int num)
|
|
{
|
|
var a = FocasAddress.TryParse(input);
|
|
a.ShouldNotBeNull();
|
|
a.Kind.ShouldBe(kind);
|
|
a.Number.ShouldBe(num);
|
|
a.BitIndex.ShouldBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("Z0")] // unknown PMC letter
|
|
[InlineData("X")] // missing number
|
|
[InlineData("X-1")] // negative number
|
|
[InlineData("Xabc")] // non-numeric
|
|
[InlineData("X0.8")] // bit out of range (0-7)
|
|
[InlineData("X0.-1")] // negative bit
|
|
[InlineData("PARAM:")] // missing number
|
|
[InlineData("PARAM:1815/32")] // bit out of range (0-31)
|
|
[InlineData("MACRO:abc")] // non-numeric
|
|
public void Address_rejects_invalid_forms(string? input)
|
|
{
|
|
FocasAddress.TryParse(input).ShouldBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("X0.0")]
|
|
[InlineData("R100")]
|
|
[InlineData("F20.3")]
|
|
[InlineData("PARAM:1020")]
|
|
[InlineData("PARAM:1815/0")]
|
|
[InlineData("MACRO:100")]
|
|
public void Address_Canonical_roundtrips(string input)
|
|
{
|
|
var parsed = FocasAddress.TryParse(input);
|
|
parsed.ShouldNotBeNull();
|
|
parsed.Canonical.ShouldBe(input);
|
|
}
|
|
|
|
// ---- FocasDataType ----
|
|
|
|
[Fact]
|
|
public void DataType_mapping_covers_atomic_focas_types()
|
|
{
|
|
FocasDataType.Bit.ToDriverDataType().ShouldBe(DriverDataType.Boolean);
|
|
FocasDataType.Int16.ToDriverDataType().ShouldBe(DriverDataType.Int32);
|
|
FocasDataType.Int32.ToDriverDataType().ShouldBe(DriverDataType.Int32);
|
|
FocasDataType.Float32.ToDriverDataType().ShouldBe(DriverDataType.Float32);
|
|
FocasDataType.Float64.ToDriverDataType().ShouldBe(DriverDataType.Float64);
|
|
FocasDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String);
|
|
}
|
|
|
|
// ---- FocasStatusMapper ----
|
|
|
|
[Theory]
|
|
[InlineData(0, FocasStatusMapper.Good)]
|
|
[InlineData(3, FocasStatusMapper.BadOutOfRange)] // EW_NUMBER
|
|
[InlineData(4, FocasStatusMapper.BadOutOfRange)] // EW_LENGTH
|
|
[InlineData(5, FocasStatusMapper.BadNotWritable)] // EW_PROT
|
|
[InlineData(6, FocasStatusMapper.BadNotSupported)] // EW_NOOPT
|
|
[InlineData(8, FocasStatusMapper.BadNodeIdUnknown)] // EW_DATA
|
|
[InlineData(-1, FocasStatusMapper.BadDeviceFailure)] // EW_BUSY
|
|
[InlineData(-8, FocasStatusMapper.BadInternalError)] // EW_HANDLE
|
|
[InlineData(-16, FocasStatusMapper.BadCommunicationError)] // EW_SOCKET
|
|
[InlineData(999, FocasStatusMapper.BadCommunicationError)] // unknown → generic
|
|
public void StatusMapper_covers_known_focas_returns(int ret, uint expected)
|
|
{
|
|
FocasStatusMapper.MapFocasReturn(ret).ShouldBe(expected);
|
|
}
|
|
|
|
// ---- FocasDriver ----
|
|
|
|
[Fact]
|
|
public void DriverType_is_FOCAS()
|
|
{
|
|
var drv = new FocasDriver(new FocasDriverOptions(), "drv-1");
|
|
drv.DriverType.ShouldBe("FOCAS");
|
|
drv.DriverInstanceId.ShouldBe("drv-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InitializeAsync_parses_device_addresses()
|
|
{
|
|
var drv = new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices =
|
|
[
|
|
new FocasDeviceOptions("focas://10.0.0.5:8193"),
|
|
new FocasDeviceOptions("focas://10.0.0.6:12345", DeviceName: "CNC-2"),
|
|
],
|
|
}, "drv-1");
|
|
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
drv.DeviceCount.ShouldBe(2);
|
|
drv.GetDeviceState("focas://10.0.0.5:8193")!.ParsedAddress.Port.ShouldBe(8193);
|
|
drv.GetDeviceState("focas://10.0.0.6:12345")!.Options.DeviceName.ShouldBe("CNC-2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InitializeAsync_malformed_address_faults()
|
|
{
|
|
var drv = new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions("not-an-address")],
|
|
}, "drv-1");
|
|
|
|
await Should.ThrowAsync<InvalidOperationException>(
|
|
() => drv.InitializeAsync("{}", CancellationToken.None));
|
|
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ShutdownAsync_clears_devices()
|
|
{
|
|
var drv = new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
|
|
Probe = new FocasProbeOptions { Enabled = false },
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
drv.DeviceCount.ShouldBe(0);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
|
|
}
|
|
|
|
// ---- UnimplementedFocasClientFactory ----
|
|
|
|
[Fact]
|
|
public void Default_factory_throws_on_Create_with_deployment_pointer()
|
|
{
|
|
var factory = new UnimplementedFocasClientFactory();
|
|
var ex = Should.Throw<NotSupportedException>(() => factory.Create());
|
|
ex.Message.ShouldContain("Fwlib32.dll");
|
|
ex.Message.ShouldContain("licensed");
|
|
}
|
|
}
|