From f594b29a7a208cadfd3214e51c731ac3044d97fe Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 21:37:51 -0400 Subject: [PATCH] test: deterministic CI-flake fixes (FOCAS alarm resubscribe, Modbus dead-port connect, S7 array poll) Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- .../FocasAlarmProjectionTests.cs | 4 +++- .../ModbusSocketLifecycleTests.cs | 2 +- .../S7PollEngineMigrationTests.cs | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasAlarmProjectionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasAlarmProjectionTests.cs index e06eacd1..9006936d 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasAlarmProjectionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasAlarmProjectionTests.cs @@ -250,7 +250,9 @@ public sealed class FocasAlarmProjectionTests { var (drv, factory) = NewDriver(Tag(ServoPath, "ALARM:Servo"), Tag(AnyPath, "ALARM:*")); await InitAndSubscribeForTicksAsync(drv, factory, ServoPath, AnyPath); - var events = Capture(drv); + // Transitions only: each resubscribe starts a real loop whose (deliberately failing) read can annotate a + // quality-only BadNoCommunication event before PauseAsync lands - that raced the exact-list asserts (CI run 911). + var events = Capture(drv, transitionsOnly: true); Tick(drv, Servo401()); events.Count.ShouldBe(2); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusSocketLifecycleTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusSocketLifecycleTests.cs index 210c9657..3c0a4120 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusSocketLifecycleTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusSocketLifecycleTests.cs @@ -25,7 +25,7 @@ public sealed class ModbusSocketLifecycleTests [Fact] public async Task Connect_to_dead_port_surfaces_socket_failure() { - var life = new ModbusSocketLifecycle("127.0.0.1", 1, TimeSpan.FromMilliseconds(300), + var life = new ModbusSocketLifecycle("127.0.0.1", 1, TimeSpan.FromSeconds(30), // loopback RST is immediate; a tight timeout races it under load autoReconnect: false); await Should.ThrowAsync( life.ConnectAsync(TestContext.Current.CancellationToken)); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs index 368229d5..16510fc9 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs @@ -21,6 +21,9 @@ public sealed class S7PollEngineMigrationTests private sealed class ConstantArrayFactory : IS7PlcFactory { public bool ReadThrows { get; set; } + private int _reads; + public int Reads => Volatile.Read(ref _reads); + public void CountRead() => Interlocked.Increment(ref _reads); public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout) => new ConstantArrayPlc(this); } @@ -39,6 +42,7 @@ public sealed class S7PollEngineMigrationTests public Task ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken ct) { + owner.CountRead(); if (owner.ReadThrows) throw new PlcException(ErrorCode.ConnectionError, "poll failure"); // Fresh array instance each call, identical contents: BE words 1, 2, 3. var block = new byte[count]; @@ -81,7 +85,16 @@ public sealed class S7PollEngineMigrationTests drv.OnDataChange += (_, e) => events.Enqueue(e); var h = await drv.SubscribeAsync(["Arr"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); - await Task.Delay(500, TestContext.Current.CancellationToken); // several poll cycles + // Wait for the initial push (generous deadline: a loaded CI box can starve the first poll), then + // for several MORE reads to complete as the positive control that the absence below is meaningful. + var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(30); + while (events.IsEmpty && DateTime.UtcNow < deadline) + await Task.Delay(10, TestContext.Current.CancellationToken); + events.Count.ShouldBeGreaterThan(0, "the initial-data push never landed"); + var readsAtPush = factory.Reads; + while (factory.Reads < readsAtPush + 4 && DateTime.UtcNow < deadline) + await Task.Delay(10, TestContext.Current.CancellationToken); + factory.Reads.ShouldBeGreaterThanOrEqualTo(readsAtPush + 4, "later poll cycles never ran"); await drv.UnsubscribeAsync(h, TestContext.Current.CancellationToken); events.Count.ShouldBe(1); // only the initial-data push