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);
}
}