Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyDeadDeviceIsolationTests.cs
T
Joseph Doherty 34e455f412 test(ablegacy): dead-device gate tolerates a coalesced single-unit pass
Live run against ab_server (slc500/micrologix/plc5 x libplctag/native): with
coalescing on, the blackholed device's 20 tags are one block, i.e. one comms
fault per poll, so its backoff window opens on the second poll rather than the
first. The gate now asserts the actual contract: at most two timeouts across
three polls, the live device Good on every poll, and the third poll fast.

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 14:48:35 -04:00

70 lines
3.6 KiB
C#

using System.Diagnostics;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
/// <summary>
/// Review 07 P7 (High), wire level: one driver serving a live <c>ab_server</c> device AND a dead device (a
/// blackholed address, so every connect waits out the full timeout). Before the fix the dead device's 20 tags
/// cost 20 x Timeout per poll and the live device was read once per cycle; now the live device reads Good in
/// the same call, the dead device costs at most two timeouts, and the next poll does not touch it at all.
/// Read-only.
/// </summary>
[Collection(AbLegacyServerCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Simulator", "ab_server-PCCC")]
public sealed class AbLegacyDeadDeviceIsolationTests(AbLegacyServerFixture sim)
{
// TEST-NET-1 (RFC 5737): never routed, so a connect blackholes until the driver's timeout.
private const string DeadUri = "ab://192.0.2.1:44818/1,0";
/// <summary>The live device is served promptly while a blackholed sibling is throttled.</summary>
/// <param name="profile">The simulator profile.</param>
/// <param name="transport">The transport stack the driver runs on.</param>
[AbLegacyTheory]
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
public async Task Dead_device_does_not_stall_the_live_one(AbLegacyServerProfile profile, AbLegacyTransport transport)
{
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
var liveUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}";
var timeout = TimeSpan.FromSeconds(2);
var dead = Enumerable.Range(0, 20)
.Select(i => new AbLegacyTagDefinition($"Dead{i}", DeadUri, $"N7:{i}", AbLegacyDataType.Int))
.ToArray();
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions(liveUri, profile.Family), new AbLegacyDeviceOptions(DeadUri, profile.Family)],
RawTags = AbLegacyRawTags.Entries(
[new AbLegacyTagDefinition("Live", liveUri, "N7:0", AbLegacyDataType.Int), .. dead]),
Timeout = timeout,
Probe = new AbLegacyProbeOptions { Enabled = false },
Transport = transport,
}, driverInstanceId: $"ablegacy-p7-{profile.Family}-{transport}");
var ct = TestContext.Current.CancellationToken;
await drv.InitializeAsync("{}", ct);
string[] refs = ["Live", .. dead.Select(d => d.Name)];
// Two consecutive comms faults mark the device down. With coalescing on, the dead device's 20 tags are ONE
// block (one fault per poll), so the window can open on the second poll rather than the first — either way
// at most two timeouts are paid before the device stops being dialled.
var total = TimeSpan.Zero;
TimeSpan last = default;
for (var poll = 0; poll < 3; poll++)
{
var sw = Stopwatch.StartNew();
var r = await drv.ReadAsync(refs, ct);
sw.Stop();
total += last = sw.Elapsed;
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, $"poll {poll}: the live device must be served in the same poll");
r.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good);
}
total.ShouldBeLessThan(timeout * 2.5, "a dead device may cost at most two timeouts per backoff window");
last.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled");
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
}
}