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; /// /// 05/STAB-9 — the poll engine's onError sink must be wired to the driver health /// surface: a poll-loop reader failure degrades health while preserving /// , and must never downgrade a pre-existing /// . /// [Trait("Category", "Unit")] public sealed class FocasPollErrorHealthTests { /// A poll reader failure degrades health, preserving the last successful read timestamp. [Fact] public async Task PollReaderFailure_DegradesHealth_PreservesLastSuccessfulRead() { var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], Probe = new FocasProbeOptions { Enabled = false }, }, "focas-1", new FakeFocasClientFactory()); await drv.InitializeAsync("{}", CancellationToken.None); var before = drv.GetHealth(); before.State.ShouldBe(DriverState.Healthy); before.LastSuccessfulRead.ShouldNotBeNull(); drv.HandlePollError(new InvalidOperationException("poll boom")); var after = drv.GetHealth(); after.State.ShouldBe(DriverState.Degraded); after.LastSuccessfulRead.ShouldBe(before.LastSuccessfulRead); after.LastError.ShouldNotBeNull(); after.LastError.ShouldContain("poll boom"); } /// A poll reader failure never downgrades a pre-existing Faulted state. [Fact] public async Task PollReaderFailure_NeverDowngradesFaulted() { var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], // DeviceName "…:9999" is not in Devices → InitializeAsync fails fast → Faulted. RawTags = [FocasRawTags.Entry(new FocasTagDefinition("X", "focas://10.0.0.5:9999", "R100", FocasDataType.Byte))], Probe = new FocasProbeOptions { Enabled = false }, }, "focas-1", new FakeFocasClientFactory()); await Should.ThrowAsync(() => drv.InitializeAsync("{}", CancellationToken.None)); drv.GetHealth().State.ShouldBe(DriverState.Faulted); drv.HandlePollError(new InvalidOperationException("poll boom")); drv.GetHealth().State.ShouldBe(DriverState.Faulted); } /// /// #740 review round 2 m3 — a poll failure carries the generation its batch STARTED under, so a batch that /// began before a re-initialise cannot degrade the new generation. Positive control: a failure stamped with /// the live generation (or unstamped) does degrade. /// [Fact] public async Task PollReaderFailure_from_a_previous_generation_does_not_degrade_the_new_one() { var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], Probe = new FocasProbeOptions { Enabled = false }, }, "focas-1", new FakeFocasClientFactory()); await drv.InitializeAsync("{}", CancellationToken.None); var oldGeneration = drv.LifecycleGeneration; await drv.InitializeAsync("{}", CancellationToken.None); var stale = new InvalidOperationException("old batch boom"); stale.Data[FocasDriver.BatchGenerationKey] = oldGeneration; drv.HandlePollError(stale); drv.GetHealth().State.ShouldBe(DriverState.Healthy); var live = new InvalidOperationException("live batch boom"); live.Data[FocasDriver.BatchGenerationKey] = drv.LifecycleGeneration; drv.HandlePollError(live); drv.GetHealth().State.ShouldBe(DriverState.Degraded); } }