fix(r2-01): RED connect-timeout regression tests T1+T3 (task 1)

This commit is contained in:
Joseph Doherty
2026-07-13 09:50:07 -04:00
parent fcdcf0e2fb
commit 9dedb7936b
2 changed files with 51 additions and 1 deletions
@@ -10,7 +10,7 @@
{ {
"id": 1, "id": 1,
"subject": "RED: connect-timeout regression tests T1+T3 (read/write degrade instead of throwing OCE) — must FAIL at f6eaa267", "subject": "RED: connect-timeout regression tests T1+T3 (read/write degrade instead of throwing OCE) — must FAIL at f6eaa267",
"status": "pending", "status": "completed",
"blockedBy": [0] "blockedBy": [0]
}, },
{ {
@@ -133,6 +133,56 @@ public sealed class S7DriverReconnectTests
drv.GetHealth().State.ShouldBe(DriverState.Healthy); drv.GetHealth().State.ShouldBe(DriverState.Healthy);
} }
// ---- STAB-14: connect-timeout must degrade, not escape as OCE ----
/// <summary>
/// T1 (STAB-14 regression). When the reopen HANGS and is cut by the internal
/// <c>CancelAfter(_options.Timeout)</c> (the unreachable-host outage — SYNs dropped, not
/// refused), the read batch must degrade to a communication error with Degraded health,
/// NOT throw the timeout-born <see cref="TaskCanceledException"/> up to the caller.
/// Before the fix the OCE escaped <see cref="S7Driver.ReadAsync"/>.
/// </summary>
[Fact]
public async Task Read_degrades_batch_on_connect_timeout_instead_of_throwing()
{
var factory = new FakeS7PlcFactory();
using var drv = new S7Driver(Options(), "s7-read-timeout", factory);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
// Drop connection #1 so the next read reopens; every subsequent open hangs until the
// internal connect-timeout CTS fires.
factory.Created[0].NextReadThrows = new PlcException(ErrorCode.ConnectionError, "dropped");
await drv.ReadAsync(["W0"], TestContext.Current.CancellationToken); // marks dead
factory.OpenHangsUntilCancelledCount = int.MaxValue;
// Must NOT throw — the connect timeout degrades the batch.
var timedOut = await drv.ReadAsync(["W0"], TestContext.Current.CancellationToken);
timedOut[0].StatusCode.ShouldNotBe(0u);
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
}
/// <summary>
/// T3 (STAB-14 regression). The same connect-timeout degrade through
/// <see cref="S7Driver.WriteAsync"/> — <c>EnsureConnectedAsync</c> is called before the
/// per-item loop, so a hanging reopen must degrade the whole write batch, not throw OCE.
/// </summary>
[Fact]
public async Task Write_degrades_batch_on_connect_timeout_instead_of_throwing()
{
var factory = new FakeS7PlcFactory();
using var drv = new S7Driver(Options(), "s7-write-timeout", factory);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
factory.Created[0].NextReadThrows = new PlcException(ErrorCode.ConnectionError, "dropped");
await drv.ReadAsync(["W0"], TestContext.Current.CancellationToken); // marks dead
factory.OpenHangsUntilCancelledCount = int.MaxValue;
var timedOut = await drv.WriteAsync(
[new WriteRequest("W0", (ushort)1)], TestContext.Current.CancellationToken);
timedOut[0].StatusCode.ShouldNotBe(0u);
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
}
// ---- fakes ---- // ---- fakes ----
private sealed class FakeS7PlcFactory : IS7PlcFactory private sealed class FakeS7PlcFactory : IS7PlcFactory