using System.Collections.Concurrent; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class AbCipHostProbeTests { [Fact] public async Task GetHostStatuses_returns_one_entry_per_device() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions("ab://10.0.0.5/1,0"), new AbCipDeviceOptions("ab://10.0.0.6/1,0"), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); var statuses = drv.GetHostStatuses(); statuses.Count.ShouldBe(2); statuses.Select(s => s.HostName).ShouldBe(["ab://10.0.0.5/1,0", "ab://10.0.0.6/1,0"], ignoreOrder: true); statuses.ShouldAllBe(s => s.State == HostState.Unknown); } [Fact] public async Task Probe_with_successful_read_transitions_to_Running() { var factory = new FakeAbCipTagFactory { Customise = p => new FakeAbCipTag(p) { Status = 0 } }; var transitions = new ConcurrentQueue(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Probe = new AbCipProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(100), Timeout = TimeSpan.FromMilliseconds(50), ProbeTagPath = "@raw_cpu_type", }, }, "drv-1", factory); drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e); await drv.InitializeAsync("{}", CancellationToken.None); await WaitForAsync(() => transitions.Any(t => t.NewState == HostState.Running), TimeSpan.FromSeconds(2)); transitions.Select(t => t.NewState).ShouldContain(HostState.Running); drv.GetHostStatuses().Single().State.ShouldBe(HostState.Running); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Probe_with_read_failure_transitions_to_Stopped() { var factory = new FakeAbCipTagFactory { Customise = p => new FakeAbCipTag(p) { ThrowOnRead = true }, }; var transitions = new ConcurrentQueue(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Probe = new AbCipProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(100), Timeout = TimeSpan.FromMilliseconds(50), ProbeTagPath = "@raw_cpu_type", }, }, "drv-1", factory); drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e); await drv.InitializeAsync("{}", CancellationToken.None); await WaitForAsync(() => transitions.Any(t => t.NewState == HostState.Stopped), TimeSpan.FromSeconds(2)); drv.GetHostStatuses().Single().State.ShouldBe(HostState.Stopped); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Probe_disabled_when_Enabled_is_false() { var factory = new FakeAbCipTagFactory(); var transitions = new ConcurrentQueue(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Probe = new AbCipProbeOptions { Enabled = false, ProbeTagPath = "@raw_cpu_type" }, }, "drv-1", factory); drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e); await drv.InitializeAsync("{}", CancellationToken.None); await Task.Delay(300); transitions.ShouldBeEmpty(); drv.GetHostStatuses().Single().State.ShouldBe(HostState.Unknown); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Probe_skipped_when_ProbeTagPath_is_null() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Probe = new AbCipProbeOptions { Enabled = true, ProbeTagPath = null }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); await Task.Delay(200); drv.GetHostStatuses().Single().State.ShouldBe(HostState.Unknown); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Probe_loops_across_multiple_devices_independently() { var factory = new FakeAbCipTagFactory { // Device A returns ok, Device B throws on read. Customise = p => p.Gateway == "10.0.0.5" ? new FakeAbCipTag(p) : new FakeAbCipTag(p) { ThrowOnRead = true }, }; var transitions = new ConcurrentQueue(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions("ab://10.0.0.5/1,0"), new AbCipDeviceOptions("ab://10.0.0.6/1,0"), ], Probe = new AbCipProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(100), Timeout = TimeSpan.FromMilliseconds(50), ProbeTagPath = "@raw_cpu_type", }, }, "drv-1", factory); drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e); await drv.InitializeAsync("{}", CancellationToken.None); await WaitForAsync(() => transitions.Count >= 2, TimeSpan.FromSeconds(3)); transitions.ShouldContain(t => t.HostName == "ab://10.0.0.5/1,0" && t.NewState == HostState.Running); transitions.ShouldContain(t => t.HostName == "ab://10.0.0.6/1,0" && t.NewState == HostState.Stopped); await drv.ShutdownAsync(CancellationToken.None); } // ---- IPerCallHostResolver ---- [Fact] public async Task ResolveHost_returns_declared_device_for_known_tag() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions("ab://10.0.0.5/1,0"), new AbCipDeviceOptions("ab://10.0.0.6/1,0"), ], Tags = [ new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt), new AbCipTagDefinition("B", "ab://10.0.0.6/1,0", "B", AbCipDataType.DInt), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.ResolveHost("A").ShouldBe("ab://10.0.0.5/1,0"); drv.ResolveHost("B").ShouldBe("ab://10.0.0.6/1,0"); } [Fact] public async Task ResolveHost_falls_back_to_first_device_for_unknown_reference() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.ResolveHost("does-not-exist").ShouldBe("ab://10.0.0.5/1,0"); } [Fact] public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices() { var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.ResolveHost("anything").ShouldBe("drv-1"); } [Fact] public async Task ResolveHost_for_UDT_member_walks_to_synthesised_definition() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.7/1,0")], Tags = [ new AbCipTagDefinition("Motor1", "ab://10.0.0.7/1,0", "Motor1", AbCipDataType.Structure, Members: [new AbCipStructureMember("Speed", AbCipDataType.DInt)]), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.ResolveHost("Motor1.Speed").ShouldBe("ab://10.0.0.7/1,0"); } private static async Task WaitForAsync(Func condition, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (!condition() && DateTime.UtcNow < deadline) await Task.Delay(20); } }