@@ -0,0 +1,216 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PR 8 — per-tag deadband / change filter tests. The driver layers the deadband on top of
|
||||
/// <see cref="PollGroupEngine"/>'s built-in change-detection: the engine fires its callback
|
||||
/// whenever value or status differs from its own last snapshot; the driver then applies the
|
||||
/// per-tag absolute / percent / type-aware filter before raising <c>OnDataChange</c>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyDeadbandTests
|
||||
{
|
||||
private const uint Good = 0x00000000u;
|
||||
private const uint BadCommunication = 0x80050000u;
|
||||
|
||||
private static async Task<AbLegacyDriver> BuildDriverAsync(params AbLegacyTagDefinition[] tags)
|
||||
{
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [], // ShouldPublish only inspects the tag-by-name map; no device is needed.
|
||||
Tags = tags,
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false, ProbeAddress = null },
|
||||
}, "drv-deadband", new FakeAbLegacyTagFactory());
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
return drv;
|
||||
}
|
||||
|
||||
private static DataValueSnapshot Snap(object? v, uint status = Good) =>
|
||||
new(v, status, DateTime.UtcNow, DateTime.UtcNow);
|
||||
|
||||
[Fact]
|
||||
public async Task Absolute_deadband_suppresses_changes_below_threshold()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
AbsoluteDeadband: 1.0);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
// Walk the published flag for each sample — the asserted sequence is [10.0, 11.5].
|
||||
drv.ShouldPublish("t", Snap(10.0)).ShouldBeTrue(); // first-seen
|
||||
drv.ShouldPublish("t", Snap(10.5)).ShouldBeFalse(); // 0.5 < 1.0
|
||||
drv.ShouldPublish("t", Snap(11.5)).ShouldBeTrue(); // 1.5 from last published 10.0
|
||||
drv.ShouldPublish("t", Snap(11.6)).ShouldBeFalse(); // 0.1 from last published 11.5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Percent_deadband_suppresses_changes_below_threshold()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
PercentDeadband: 10.0);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap(100.0)).ShouldBeTrue(); // first-seen
|
||||
drv.ShouldPublish("t", Snap(105.0)).ShouldBeFalse(); // 5% < 10%
|
||||
drv.ShouldPublish("t", Snap(115.0)).ShouldBeTrue(); // |115-100|=15 >= 10% of 100
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Percent_deadband_with_zero_prev_always_publishes_on_change()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
PercentDeadband: 50.0);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap(0.0)).ShouldBeTrue(); // first-seen
|
||||
drv.ShouldPublish("t", Snap(0.001)).ShouldBeTrue(); // prev=0 short-circuit
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Boolean_publishes_every_transition_and_suppresses_no_op()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "B3:0/0", AbLegacyDataType.Bit,
|
||||
AbsoluteDeadband: 1.0);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
// Edge sequence false -> true -> false -> true => 4 publishes.
|
||||
drv.ShouldPublish("t", Snap(false)).ShouldBeTrue();
|
||||
drv.ShouldPublish("t", Snap(true)).ShouldBeTrue();
|
||||
drv.ShouldPublish("t", Snap(false)).ShouldBeTrue();
|
||||
drv.ShouldPublish("t", Snap(true)).ShouldBeTrue();
|
||||
|
||||
// No-op (true -> true) suppressed.
|
||||
drv.ShouldPublish("t", Snap(true)).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Status_change_always_publishes_even_within_deadband()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
AbsoluteDeadband: 100.0); // ridiculously large — would normally suppress everything
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap(10.0, Good)).ShouldBeTrue(); // first-seen
|
||||
drv.ShouldPublish("t", Snap(10.5, Good)).ShouldBeFalse(); // within deadband
|
||||
drv.ShouldPublish("t", Snap(10.5, BadCommunication)).ShouldBeTrue(); // status flip
|
||||
drv.ShouldPublish("t", Snap(10.5, Good)).ShouldBeTrue(); // status flip back
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task String_publishes_on_value_change_and_suppresses_no_op()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "ST20:0", AbLegacyDataType.String,
|
||||
AbsoluteDeadband: 1.0); // deadband meaningless on strings — should be ignored
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap("alpha")).ShouldBeTrue(); // first-seen
|
||||
drv.ShouldPublish("t", Snap("alpha")).ShouldBeFalse(); // identical
|
||||
drv.ShouldPublish("t", Snap("beta")).ShouldBeTrue(); // change
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Both_deadbands_set_either_triggers_publish()
|
||||
{
|
||||
// 1.0 absolute OR 50% percent — picking deltas where one passes, the other does not.
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
AbsoluteDeadband: 1.0, PercentDeadband: 50.0);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap(2.0)).ShouldBeTrue(); // first-seen
|
||||
// |3.0 - 2.0| = 1.0 -> absolute passes (>= 1.0); percent test 50% of 2.0 = 1.0 -> 1.0 >= 1.0 also passes.
|
||||
drv.ShouldPublish("t", Snap(3.0)).ShouldBeTrue();
|
||||
// After last-publish=3.0: small delta of 0.6, absolute fails (0.6 < 1.0), percent fails (0.6 < 1.5).
|
||||
drv.ShouldPublish("t", Snap(3.6)).ShouldBeFalse();
|
||||
// |5.0 - 3.0| = 2.0; absolute passes (>= 1.0).
|
||||
drv.ShouldPublish("t", Snap(5.0)).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task First_seen_always_publishes_even_with_huge_deadband()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
AbsoluteDeadband: 1e9);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap(1.0)).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_clears_last_published_cache()
|
||||
{
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float,
|
||||
AbsoluteDeadband: 1.0);
|
||||
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [],
|
||||
Tags = [tag],
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false, ProbeAddress = null },
|
||||
}, "drv-reinit", new FakeAbLegacyTagFactory());
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
drv.ShouldPublish("t", Snap(10.0)).ShouldBeTrue();
|
||||
drv.ShouldPublish("t", Snap(10.2)).ShouldBeFalse();
|
||||
|
||||
await drv.ReinitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// After reinit the cache is empty — the very next sample is treated as first-seen and
|
||||
// publishes regardless of how close it is to the pre-reinit last-published value.
|
||||
drv.ShouldPublish("t", Snap(10.2)).ShouldBeTrue();
|
||||
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tag_without_deadband_publishes_every_sample_reaching_dispatcher()
|
||||
{
|
||||
// No deadband fields set — the driver passes through whatever the engine gives it.
|
||||
// (PollGroupEngine itself filters by value-equality, so any sample reaching ShouldPublish
|
||||
// is already a real change.)
|
||||
var tag = new AbLegacyTagDefinition("t", "ab://h/", "F8:0", AbLegacyDataType.Float);
|
||||
await using var drv = await BuildDriverAsync(tag);
|
||||
|
||||
drv.ShouldPublish("t", Snap(1.0)).ShouldBeTrue();
|
||||
drv.ShouldPublish("t", Snap(1.001)).ShouldBeTrue();
|
||||
drv.ShouldPublish("t", Snap(1.002)).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Dto_round_trip_preserves_deadband_fields()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"Devices": [
|
||||
{ "HostAddress": "ab://10.0.0.5/1,0", "PlcFamily": "Slc500" }
|
||||
],
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "T1",
|
||||
"DeviceHostAddress": "ab://10.0.0.5/1,0",
|
||||
"Address": "F8:0",
|
||||
"DataType": "Float",
|
||||
"AbsoluteDeadband": 0.5,
|
||||
"PercentDeadband": 5.0
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var drv = AbLegacyDriverFactoryExtensions.CreateInstance("drv-json", json);
|
||||
try
|
||||
{
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
// ShouldPublish exercises the deadband — confirms the DTO values landed on the
|
||||
// AbLegacyTagDefinition.
|
||||
drv.ShouldPublish("T1", Snap(100.0)).ShouldBeTrue(); // first-seen
|
||||
drv.ShouldPublish("T1", Snap(100.4)).ShouldBeFalse(); // 0.4 < 0.5 absolute
|
||||
drv.ShouldPublish("T1", Snap(101.0)).ShouldBeTrue(); // 1.0 >= 0.5 absolute
|
||||
}
|
||||
finally
|
||||
{
|
||||
await drv.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user