Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusDeadbandRecoveryTests.cs
T
Joseph Doherty ae20519e9d fix(modbus): deadband baseline on (value, status) so the Good recovery publishes
H2: a Bad/null publish drops the deadband baseline and any status change
publishes, so a device that returns holding the identical value is no longer
swallowed by |new - old| < deadband (the #527 value-only-dedup trap).
Integration case drives a real drop/restore through a loopback relay.

Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:05:25 -04:00

54 lines
2.4 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
/// <summary>
/// Review 07 H2: the deadband baseline is keyed on (value, status). A Good-after-Bad recovery carrying
/// the IDENTICAL value must publish — the value-only dedup left the node Bad until the value drifted.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ModbusDeadbandRecoveryTests
{
private const uint Good = 0u;
private const uint Uncertain = 0x40000000u;
private const uint BadComm = 0x80050000u;
private static async Task<ModbusDriver> NewDriverAsync()
{
var tag = new ModbusTagDefinition("Temp", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16, Deadband: 5.0);
var opts = new ModbusDriverOptions
{
Host = "f", RawTags = ModbusRawTags.Entries([tag]), Probe = new ModbusProbeOptions { Enabled = false },
};
var drv = new ModbusDriver(opts, "m1", _ => new ModbusDriverTests.FakeTransport());
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
return drv;
}
private static DataValueSnapshot Snap(object? v, uint status) => new(v, status, DateTime.UtcNow, DateTime.UtcNow);
[Fact]
public async Task Good_after_Bad_with_the_same_value_publishes()
{
await using var drv = await NewDriverAsync();
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue(); // baseline
drv.ShouldPublish("Temp", Snap(null, BadComm)).ShouldBeTrue(); // comms drop
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue("the recovery publish must not be swallowed");
// Positive control: the deadband is still active after the recovery re-seeded the baseline.
drv.ShouldPublish("Temp", Snap((short)11, Good)).ShouldBeFalse();
drv.ShouldPublish("Temp", Snap((short)16, Good)).ShouldBeTrue();
}
[Fact]
public async Task A_status_change_with_the_same_value_publishes()
{
await using var drv = await NewDriverAsync();
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue();
drv.ShouldPublish("Temp", Snap((short)10, Uncertain)).ShouldBeTrue("a quality change must publish");
drv.ShouldPublish("Temp", Snap((short)12, Uncertain)).ShouldBeFalse("within deadband at the same quality");
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue("back to Good must publish");
}
}