test: deterministic CI-flake fixes (FOCAS alarm resubscribe, Modbus dead-port connect, S7 array poll)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 21:37:51 -04:00
parent e7dd742541
commit f594b29a7a
3 changed files with 18 additions and 3 deletions
@@ -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);
@@ -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<System.Net.Sockets.SocketException>(
life.ConnectAsync(TestContext.Current.CancellationToken));
@@ -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<byte[]> 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