Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

265 lines
11 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 ----
/// <summary>Verifies FocasHostAddress.TryParse correctly parses valid addresses.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="host">The expected host after parsing.</param>
/// <param name="port">The expected port after parsing.</param>
[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);
}
/// <summary>Verifies FocasHostAddress.TryParse rejects invalid addresses.</summary>
/// <param name="input">The input address string to test for rejection.</param>
[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();
}
/// <summary>Verifies FocasHostAddress.ToString strips the default port.</summary>
[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 ----
/// <summary>Verifies FocasAddress.TryParse correctly parses PMC address forms.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="kind">The expected FocasAreaKind after parsing.</param>
/// <param name="letter">The expected PMC letter after parsing.</param>
/// <param name="num">The expected number after parsing.</param>
/// <param name="bit">The expected bit index after parsing.</param>
[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);
}
/// <summary>Verifies FocasAddress.TryParse correctly parses parameter address forms.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="kind">The expected FocasAreaKind after parsing.</param>
/// <param name="num">The expected number after parsing.</param>
/// <param name="bit">The expected bit index after parsing.</param>
[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);
}
/// <summary>Verifies FocasAddress.TryParse correctly parses macro address forms.</summary>
/// <param name="input">The input address string to parse.</param>
/// <param name="kind">The expected FocasAreaKind after parsing.</param>
/// <param name="num">The expected number after parsing.</param>
[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();
}
/// <summary>Verifies FocasAddress.TryParse rejects invalid address forms.</summary>
/// <param name="input">The input address string to test for rejection.</param>
[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();
}
/// <summary>Verifies FocasAddress.Canonical roundtrips correctly.</summary>
/// <param name="input">The input address string to roundtrip.</param>
[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 ----
/// <summary>Verifies data type mapping covers all atomic FOCAS types.</summary>
[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 ----
/// <summary>Verifies status mapper covers all known FOCAS return codes.</summary>
/// <param name="ret">The FOCAS return code to map.</param>
/// <param name="expected">The expected mapped status code.</param>
[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 ----
/// <summary>Verifies FocasDriver initializes with correct type and ID.</summary>
[Fact]
public void DriverType_is_FOCAS()
{
var drv = new FocasDriver(new FocasDriverOptions(), "drv-1");
drv.DriverType.ShouldBe("FOCAS");
drv.DriverInstanceId.ShouldBe("drv-1");
}
/// <summary>Verifies InitializeAsync parses device addresses correctly.</summary>
[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");
}
/// <summary>Verifies InitializeAsync faults on malformed addresses.</summary>
[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);
}
/// <summary>Verifies ShutdownAsync clears all devices.</summary>
[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 ----
/// <summary>Verifies UnimplementedFocasClientFactory throws on Create.</summary>
[Fact]
public void Unimplemented_factory_throws_on_Create_with_config_pointer()
{
var factory = new UnimplementedFocasClientFactory();
var ex = Should.Throw<NotSupportedException>(() => factory.Create());
ex.Message.ShouldContain("wire");
ex.Message.ShouldContain("docs/drivers/FOCAS.md");
}
}