88 lines
4.1 KiB
C#
88 lines
4.1 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// PR abcip-4.2 — end-to-end coverage for write-deadband / write-on-change suppression
|
|
/// against a running <c>ab_server</c>. Drives a 5-write jittery sequence with
|
|
/// <c>WriteDeadband=1.0</c> and asserts the driver's <c>AbCip.WritesSuppressed</c>
|
|
/// diagnostics counter reflects the expected number of suppressions. Wire-level write
|
|
/// count isn't directly observable in <c>ab_server</c> (no admin shim for "tell me how
|
|
/// many CIP writes you got"), so the suppression evidence is the driver's own counter
|
|
/// plus the final read confirming the last passed-through value reached the PLC.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Unit coverage in <see cref="ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.AbCipWriteDeadbandTests"/>
|
|
/// proves the suppression math against an in-process fake. This test exercises the full
|
|
/// libplctag stack so a regression in how the driver wires its coalescer to the real wire
|
|
/// path shows up here.
|
|
/// </remarks>
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Requires", "AbServer")]
|
|
public sealed class AbCipWriteDeadbandTests
|
|
{
|
|
[AbServerFact]
|
|
public async Task Jittery_setpoints_within_deadband_dont_reach_the_wire()
|
|
{
|
|
var profile = KnownProfiles.ControlLogix;
|
|
var fixture = new AbServerFixture(profile);
|
|
await fixture.InitializeAsync();
|
|
try
|
|
{
|
|
var deviceUri = $"ab://127.0.0.1:{fixture.Port}/1,0";
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)],
|
|
// ab_server seeds TestDINT — drive integer setpoints with a 1.0 deadband so
|
|
// values that differ by 0 are suppressed. Real-world deadbanding usually
|
|
// targets REAL setpoints; integer here is fine because the suppression rule
|
|
// looks at the boxed numeric value, not the on-wire encoding.
|
|
Tags = [new AbCipTagDefinition("Setpoint", deviceUri, "TestDINT",
|
|
AbCipDataType.DInt, WriteDeadband: 1.0)],
|
|
Timeout = TimeSpan.FromSeconds(5),
|
|
}, "drv-write-deadband-smoke");
|
|
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
|
|
// Five-write jittery sequence: 100, 100, 100, 102, 102.
|
|
// - 100 (first): passes (no prior).
|
|
// - 100, 100: suppressed (|0| < 1.0).
|
|
// - 102: passes (|2| ≥ 1.0).
|
|
// - 102: suppressed (|0| < 1.0).
|
|
// Expected: 2 wire writes, 3 suppressions.
|
|
var inputs = new[] { 100, 100, 100, 102, 102 };
|
|
foreach (var v in inputs)
|
|
{
|
|
var results = await drv.WriteAsync(
|
|
[new WriteRequest("Setpoint", v)],
|
|
TestContext.Current.CancellationToken);
|
|
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good,
|
|
"every write — suppressed or not — must surface as Good to the OPC UA client");
|
|
}
|
|
|
|
drv.WriteCoalescer.TotalWritesSuppressed.ShouldBe(3);
|
|
drv.WriteCoalescer.TotalWritesPassedThrough.ShouldBe(2);
|
|
|
|
// Final readback proves the last passed-through value (102) made it to the PLC.
|
|
var readback = await drv.ReadAsync(["Setpoint"], TestContext.Current.CancellationToken);
|
|
readback.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
Convert.ToInt32(readback.Single().Value).ShouldBe(102);
|
|
|
|
// Diagnostics counters are also reflected through GetHealth — the path the
|
|
// driver-diagnostics RPC + Admin UI consume.
|
|
var diag = drv.GetHealth().DiagnosticsOrEmpty;
|
|
diag["AbCip.WritesSuppressed"].ShouldBe(3);
|
|
diag["AbCip.WritesPassedThrough"].ShouldBe(2);
|
|
|
|
await drv.ShutdownAsync(TestContext.Current.CancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
await fixture.DisposeAsync();
|
|
}
|
|
}
|
|
}
|