using Microsoft.Extensions.Logging; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using CapturingLogger = ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.AbCipLoggingTests.CapturingLogger; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Review 07 H10: a typo'd (or download-deleted) Probe.ProbeTagPath used to record a Forward-Open /// FAILURE every probe tick, so the device's backoff window never closed and every cold / evicted data tag read /// BadCommunicationError forever — a permanent device blackout that a restart did not cure. The controller /// ANSWERED, so the probe now counts it as reachable: backoff success, host Running, one config Warning. /// [Trait("Category", "Unit")] public sealed class AbCipProbeRefusalTests { private const string Device = "ab://10.0.0.5/1,0"; private static (AbCipDriver Driver, CapturingLogger Logger) Build(Exception probeFault) { var logger = new CapturingLogger(); var factory = new FakeAbCipTagFactory { Customise = p => p.TagName == "ProbeTagTypo" ? new FakeAbCipTag(p) { ThrowOnInitialize = true, Exception = probeFault } : new FakeAbCipTag(p) { Value = 7 }, }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(Device)], RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Motor1.Speed", AbCipDataType.DInt)), Probe = new AbCipProbeOptions { Enabled = true, ProbeTagPath = "ProbeTagTypo", Interval = TimeSpan.FromMilliseconds(10), Timeout = TimeSpan.FromMilliseconds(100), }, }, "abcip-probe-refusal", factory, logger: logger); return (drv, logger); } private static async Task WaitUntilAsync(Func condition) { var deadline = DateTime.UtcNow.AddSeconds(5); while (!condition()) { if (DateTime.UtcNow > deadline) throw new TimeoutException("condition never met"); await Task.Delay(5); } } /// A NotFound probe tag leaves the device reachable: data tags still connect, host is Running, warned once. [Fact] public async Task A_missing_probe_tag_does_not_black_out_the_device() { var (drv, logger) = Build(new AbTransportException(AbStatus.NotFound, "NotFound")); await drv.InitializeAsync("{}", CancellationToken.None); await WaitUntilAsync(() => drv.GetHostStatuses().Single().State == HostState.Running); await Task.Delay(100); // many probe ticks var r = await drv.ReadAsync(["Speed"], CancellationToken.None); r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good); // a cold create, NOT throttled r[0].Value.ShouldBe(7); drv.GetHostStatuses().Single().State.ShouldBe(HostState.Running); logger.Entries.Count(e => e.Level == LogLevel.Warning && e.Message.Contains("ProbeTagTypo")).ShouldBe(1); await drv.DisposeAsync(); } /// Positive control: a probe that genuinely cannot connect still stops the host and throttles cold creates. [Fact] public async Task An_unreachable_device_still_stops_the_host_and_throttles() { var (drv, _) = Build(new AbTransportException(AbStatus.Timeout, "Timeout")); await drv.InitializeAsync("{}", CancellationToken.None); await WaitUntilAsync(() => drv.GetHostStatuses().Single().State == HostState.Stopped); var r = await drv.ReadAsync(["Speed"], CancellationToken.None); r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError); // throttled by the open window await drv.DisposeAsync(); } }