217 lines
9.2 KiB
C#
217 lines
9.2 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|