From 34e455f412652f07f2b652e52fdfa3e4fcda1555 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 14:48:35 -0400 Subject: [PATCH] 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 --- .../AbLegacyDeadDeviceIsolationTests.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyDeadDeviceIsolationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyDeadDeviceIsolationTests.cs index 2e5be188..64eb19fb 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyDeadDeviceIsolationTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyDeadDeviceIsolationTests.cs @@ -47,19 +47,23 @@ public sealed class AbLegacyDeadDeviceIsolationTests(AbLegacyServerFixture sim) await drv.InitializeAsync("{}", ct); string[] refs = ["Live", .. dead.Select(d => d.Name)]; - var sw = Stopwatch.StartNew(); - var first = await drv.ReadAsync(refs, ct); - sw.Stop(); + // 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); + } - first[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "the live device must be served in the same poll"); - first.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good); - sw.Elapsed.ShouldBeLessThan(timeout * 3, "a dead device may cost at most two timeouts per backoff window"); - - sw.Restart(); - var second = await drv.ReadAsync(refs, ct); - sw.Stop(); - second[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good); - sw.Elapsed.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled"); + 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); } }