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 TwinCATDriverTests { [Fact] public void DriverType_is_TwinCAT() { var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1"); drv.DriverType.ShouldBe("TwinCAT"); drv.DriverInstanceId.ShouldBe("drv-1"); } [Fact] public async Task InitializeAsync_parses_device_addresses() { var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [ new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851"), new TwinCATDeviceOptions("ads://10.0.0.1.1.1:852", DeviceName: "Machine2"), ], }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.DeviceCount.ShouldBe(2); drv.GetDeviceState("ads://5.23.91.23.1.1:851")!.ParsedAddress.Port.ShouldBe(851); drv.GetDeviceState("ads://10.0.0.1.1.1:852")!.Options.DeviceName.ShouldBe("Machine2"); } [Fact] public async Task InitializeAsync_malformed_address_faults() { var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("not-an-address")], }, "drv-1"); await Should.ThrowAsync( () => drv.InitializeAsync("{}", CancellationToken.None)); drv.GetHealth().State.ShouldBe(DriverState.Faulted); } [Fact] public async Task ShutdownAsync_clears_devices() { var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Probe = new TwinCATProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); await drv.ShutdownAsync(CancellationToken.None); drv.DeviceCount.ShouldBe(0); drv.GetHealth().State.ShouldBe(DriverState.Unknown); } [Fact] public async Task ReinitializeAsync_cycles_devices() { var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Probe = new TwinCATProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); await drv.ReinitializeAsync("{}", CancellationToken.None); drv.DeviceCount.ShouldBe(1); drv.GetHealth().State.ShouldBe(DriverState.Healthy); } [Fact] public void DataType_mapping_covers_atomic_iec_types() { TwinCATDataType.Bool.ToDriverDataType().ShouldBe(DriverDataType.Boolean); TwinCATDataType.DInt.ToDriverDataType().ShouldBe(DriverDataType.Int32); TwinCATDataType.LInt.ToDriverDataType().ShouldBe(DriverDataType.Int64); TwinCATDataType.ULInt.ToDriverDataType().ShouldBe(DriverDataType.UInt64); TwinCATDataType.Real.ToDriverDataType().ShouldBe(DriverDataType.Float32); TwinCATDataType.LReal.ToDriverDataType().ShouldBe(DriverDataType.Float64); TwinCATDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String); TwinCATDataType.WString.ToDriverDataType().ShouldBe(DriverDataType.String); // IEC durations map to UA Duration; absolute timestamps map to UA DateTime. TwinCATDataType.Time.ToDriverDataType().ShouldBe(DriverDataType.Duration); TwinCATDataType.TimeOfDay.ToDriverDataType().ShouldBe(DriverDataType.Duration); TwinCATDataType.Date.ToDriverDataType().ShouldBe(DriverDataType.DateTime); TwinCATDataType.DateTime.ToDriverDataType().ShouldBe(DriverDataType.DateTime); } [Fact] public void IecTime_post_process_converts_TIME_to_TimeSpan_ms() { var ts = (TimeSpan)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.Time, 12_345u)!; ts.ShouldBe(TimeSpan.FromMilliseconds(12_345)); } [Fact] public void IecTime_post_process_converts_TOD_to_TimeSpan_ms() { var ts = (TimeSpan)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.TimeOfDay, 3_600_000u)!; ts.ShouldBe(TimeSpan.FromHours(1)); } [Fact] public void IecTime_post_process_converts_DT_to_DateTime_utc() { // 1970-01-01 + 1 hour = 1970-01-01 01:00:00 UTC var dt = (DateTime)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.DateTime, 3600u)!; dt.ShouldBe(new DateTime(1970, 1, 1, 1, 0, 0, DateTimeKind.Utc)); dt.Kind.ShouldBe(DateTimeKind.Utc); } [Fact] public void IecTime_post_process_converts_DATE_to_midnight_utc() { // 86400 seconds = exactly 1970-01-02 00:00 UTC var dt = (DateTime)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.Date, 86_400u)!; dt.ShouldBe(new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc)); } [Fact] public void IecTime_post_process_passthrough_for_non_time_types() { AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.DInt, 42).ShouldBe(42); AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.LReal, 3.14).ShouldBe(3.14); } [Fact] public void ConvertForWrite_TIME_accepts_TimeSpan_and_returns_UDINT_ms() { var raw = AdsTwinCATClient.ConvertForWrite(TwinCATDataType.Time, TimeSpan.FromMilliseconds(2_500)); raw.ShouldBeOfType(); ((uint)raw).ShouldBe(2_500u); } [Fact] public void ConvertForWrite_TOD_accepts_double_ms() { var raw = AdsTwinCATClient.ConvertForWrite(TwinCATDataType.TimeOfDay, 60_000.0); ((uint)raw).ShouldBe(60_000u); } [Fact] public void ConvertForWrite_DT_accepts_DateTime_and_returns_UDINT_seconds() { var raw = AdsTwinCATClient.ConvertForWrite( TwinCATDataType.DateTime, new DateTime(1970, 1, 1, 1, 0, 0, DateTimeKind.Utc)); ((uint)raw).ShouldBe(3600u); } [Fact] public void ConvertForWrite_DT_round_trips_via_post_process() { var original = new DateTime(2024, 6, 15, 12, 30, 45, DateTimeKind.Utc); var raw = (uint)AdsTwinCATClient.ConvertForWrite(TwinCATDataType.DateTime, original); var roundTrip = (DateTime)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.DateTime, raw)!; roundTrip.ShouldBe(original); } [Fact] public void ConvertForWrite_DT_rejects_pre_epoch_values() { Should.Throw(() => AdsTwinCATClient.ConvertForWrite( TwinCATDataType.DateTime, new DateTime(1969, 12, 31, 23, 59, 59, DateTimeKind.Utc))); } [Theory] [InlineData(0u, TwinCATStatusMapper.Good)] [InlineData(1798u, TwinCATStatusMapper.BadNodeIdUnknown)] // symbol not found [InlineData(1808u, TwinCATStatusMapper.BadNotWritable)] // access denied [InlineData(1861u, TwinCATStatusMapper.BadTimeout)] // sync timeout [InlineData(1793u, TwinCATStatusMapper.BadOutOfRange)] // invalid index group [InlineData(1794u, TwinCATStatusMapper.BadOutOfRange)] // invalid index offset [InlineData(1792u, TwinCATStatusMapper.BadNotSupported)] // service not supported [InlineData(7u, TwinCATStatusMapper.BadCommunicationError)] // port unreachable [InlineData(99999u, TwinCATStatusMapper.BadCommunicationError)] // unknown → generic comm fail public void StatusMapper_covers_known_ads_error_codes(uint adsError, uint expected) { TwinCATStatusMapper.MapAdsError(adsError).ShouldBe(expected); } }