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; /// /// #739 P10: a bad tag warns ONCE (not once per poll — 50 typo'd symbols on a 500 ms poll used to be 100 /// Warnings/s forever), re-arms when it reads Good, and a device refusal leaves driver health alone while a /// comms-class status degrades. /// [Trait("Category", "Unit")] public sealed class TwinCATReadWarningTests { private const string Host = "ads://5.23.91.23.1.1:851"; private static (TwinCATDriver drv, FakeTwinCATClient fake, TwinCATCapturingLogger log) NewDriver() { var fake = new FakeTwinCATClient(); var log = new TwinCATCapturingLogger(); var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions(Host)], RawTags = TwinCATRawTags.Entries( [ new TwinCATTagDefinition("Typo", Host, "MAIN.Typo", TwinCATDataType.DInt), new TwinCATTagDefinition("Other", Host, "MAIN.Other", TwinCATDataType.DInt), ]), Probe = new TwinCATProbeOptions { Enabled = false }, }, "drv-1", new FakeTwinCATClientFactory { Customise = () => fake }, log); return (drv, fake, log); } private static int WarningsFor(TwinCATCapturingLogger log, string reference) => log.Warnings.Count(w => w.Contains($"'{reference}'", StringComparison.Ordinal)); /// Repeated bad reads of one tag log one Warning; a second bad tag gets its own (positive control). [Fact] public async Task A_bad_tag_warns_once_across_polls_and_rearms_after_Good() { var (drv, fake, log) = NewDriver(); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); fake.ReadStatuses["MAIN.Typo"] = TwinCATStatusMapper.BadNodeIdUnknown; for (var i = 0; i < 5; i++) (await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken)) .Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown); WarningsFor(log, "Typo").ShouldBe(1); fake.ReadStatuses["MAIN.Other"] = TwinCATStatusMapper.BadNodeIdUnknown; _ = await drv.ReadAsync(["Other"], TestContext.Current.CancellationToken); WarningsFor(log, "Other").ShouldBe(1); // Fixed, then broken again: a new episode warns again. fake.ReadStatuses.Remove("MAIN.Typo"); _ = await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken); fake.ReadStatuses["MAIN.Typo"] = TwinCATStatusMapper.BadNodeIdUnknown; _ = await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken); _ = await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken); WarningsFor(log, "Typo").ShouldBe(2); } /// A re-initialise re-arms the warning. [Fact] public async Task A_reinitialise_rearms_the_warning() { var (drv, fake, log) = NewDriver(); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); fake.ReadStatuses["MAIN.Typo"] = TwinCATStatusMapper.BadNodeIdUnknown; _ = await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); _ = await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken); WarningsFor(log, "Typo").ShouldBe(2); } /// /// A status proving the device answered (symbol not found) keeps the driver Healthy and advances the /// stamp; a comms-class status (timeout) degrades with the stamp preserved. /// [Fact] public async Task A_refusal_status_does_not_degrade_but_a_comms_status_does() { var (drv, fake, _) = NewDriver(); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); var initStamp = drv.GetHealth().LastSuccessfulRead!.Value; fake.ReadStatuses["MAIN.Typo"] = TwinCATStatusMapper.BadNodeIdUnknown; var deadline = DateTime.UtcNow.AddSeconds(2); while (DateTime.UtcNow <= initStamp && DateTime.UtcNow < deadline) await Task.Delay(5, TestContext.Current.CancellationToken); _ = await drv.ReadAsync(["Typo"], TestContext.Current.CancellationToken); var afterRefusal = drv.GetHealth(); afterRefusal.State.ShouldBe(DriverState.Healthy); afterRefusal.LastSuccessfulRead!.Value.ShouldBeGreaterThan(initStamp); fake.ReadStatuses["MAIN.Other"] = TwinCATStatusMapper.BadTimeout; _ = await drv.ReadAsync(["Other"], TestContext.Current.CancellationToken); var afterTimeout = drv.GetHealth(); afterTimeout.State.ShouldBe(DriverState.Degraded); afterTimeout.LastSuccessfulRead.ShouldBe(afterRefusal.LastSuccessfulRead); } }