fix(r2-01): extend reconnect fakes with token-honouring hang + probe-fault modes (task 0)
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
"id": 0,
|
"id": 0,
|
||||||
"subject": "Extend reconnect-test fakes with token-honouring hang + probe-fault modes (FakeS7Plc/FakeS7PlcFactory)",
|
"subject": "Extend reconnect-test fakes with token-honouring hang + probe-fault modes (FakeS7Plc/FakeS7PlcFactory)",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -143,6 +143,21 @@ public sealed class S7DriverReconnectTests
|
|||||||
/// <summary>When set, the NEXT <see cref="Create"/>d connection throws this on OpenAsync (once).</summary>
|
/// <summary>When set, the NEXT <see cref="Create"/>d connection throws this on OpenAsync (once).</summary>
|
||||||
public Exception? NextOpenThrows { get; set; }
|
public Exception? NextOpenThrows { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of subsequently-created connections whose <see cref="FakeS7Plc.OpenAsync"/>
|
||||||
|
/// hangs until its token is cancelled (simulating the unreachable-but-not-refusing host
|
||||||
|
/// — pulled cable / firewall DROP — that STAB-14 needs and <c>docker restart</c> can't
|
||||||
|
/// produce). Each <see cref="Create"/> decrements it while positive.
|
||||||
|
/// </summary>
|
||||||
|
public int OpenHangsUntilCancelledCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When set, copied onto every created connection's persistent
|
||||||
|
/// <see cref="FakeS7Plc.ReadStatusThrows"/> so a probe fails on every tick. Clear it
|
||||||
|
/// after init to arm connection #1 only.
|
||||||
|
/// </summary>
|
||||||
|
public Exception? ReadStatusThrowsForNewConnections { get; set; }
|
||||||
|
|
||||||
public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout)
|
public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout)
|
||||||
{
|
{
|
||||||
var plc = new FakeS7Plc();
|
var plc = new FakeS7Plc();
|
||||||
@@ -151,6 +166,13 @@ public sealed class S7DriverReconnectTests
|
|||||||
plc.OpenThrowsOnce = ex;
|
plc.OpenThrowsOnce = ex;
|
||||||
NextOpenThrows = null;
|
NextOpenThrows = null;
|
||||||
}
|
}
|
||||||
|
if (OpenHangsUntilCancelledCount > 0)
|
||||||
|
{
|
||||||
|
plc.OpenHangsUntilCancelled = true;
|
||||||
|
OpenHangsUntilCancelledCount--;
|
||||||
|
}
|
||||||
|
if (ReadStatusThrowsForNewConnections is { } rex)
|
||||||
|
plc.ReadStatusThrows = rex;
|
||||||
Created.Add(plc);
|
Created.Add(plc);
|
||||||
return plc;
|
return plc;
|
||||||
}
|
}
|
||||||
@@ -164,45 +186,76 @@ public sealed class S7DriverReconnectTests
|
|||||||
/// <summary>Thrown once by <see cref="OpenAsync"/>, then cleared.</summary>
|
/// <summary>Thrown once by <see cref="OpenAsync"/>, then cleared.</summary>
|
||||||
public Exception? OpenThrowsOnce { get; set; }
|
public Exception? OpenThrowsOnce { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <see cref="OpenAsync"/> hangs until its token is cancelled, then throws
|
||||||
|
/// <see cref="TaskCanceledException"/> — the connect-timeout shape (an unreachable host
|
||||||
|
/// whose SYNs are dropped) the live rig can't cheaply produce (STAB-14).
|
||||||
|
/// </summary>
|
||||||
|
public bool OpenHangsUntilCancelled { get; set; }
|
||||||
|
|
||||||
/// <summary>Thrown once by the next <see cref="ReadAsync"/>, then cleared.</summary>
|
/// <summary>Thrown once by the next <see cref="ReadAsync"/>, then cleared.</summary>
|
||||||
public Exception? NextReadThrows { get; set; }
|
public Exception? NextReadThrows { get; set; }
|
||||||
|
|
||||||
|
/// <summary>When true, <see cref="ReadAsync"/> hangs until its token is cancelled (STAB-15 cancel-mid-I/O).</summary>
|
||||||
|
public bool ReadHangsUntilCancelled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>When true, <see cref="WriteAsync"/> hangs until its token is cancelled (STAB-15 cancel-mid-I/O).</summary>
|
||||||
|
public bool WriteHangsUntilCancelled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Thrown by EVERY <see cref="ReadStatusAsync"/> (persistent, not one-shot) — arms a probe fault (STAB-15b).</summary>
|
||||||
|
public Exception? ReadStatusThrows { get; set; }
|
||||||
|
|
||||||
|
/// <summary>When true, <see cref="ReadStatusAsync"/> hangs until its token is cancelled (probe-timeout, STAB-15b).</summary>
|
||||||
|
public bool ReadStatusHangsUntilCancelled { get; set; }
|
||||||
|
|
||||||
/// <summary>Boxed value returned by a good read (UInt16 tag → ushort).</summary>
|
/// <summary>Boxed value returned by a good read (UInt16 tag → ushort).</summary>
|
||||||
public object? ReadValue { get; set; } = (ushort)42;
|
public object? ReadValue { get; set; } = (ushort)42;
|
||||||
|
|
||||||
public Task OpenAsync(CancellationToken cancellationToken)
|
public async Task OpenAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (OpenHangsUntilCancelled)
|
||||||
|
await Task.Delay(System.Threading.Timeout.Infinite, cancellationToken).ConfigureAwait(false);
|
||||||
if (OpenThrowsOnce is { } ex)
|
if (OpenThrowsOnce is { } ex)
|
||||||
{
|
{
|
||||||
OpenThrowsOnce = null;
|
OpenThrowsOnce = null;
|
||||||
return Task.FromException(ex);
|
throw ex;
|
||||||
}
|
}
|
||||||
IsConnected = true;
|
IsConnected = true;
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close() => IsConnected = false;
|
public void Close() => IsConnected = false;
|
||||||
|
|
||||||
public Task<object?> ReadAsync(string address, CancellationToken cancellationToken)
|
public async Task<object?> ReadAsync(string address, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (ReadHangsUntilCancelled)
|
||||||
|
await Task.Delay(System.Threading.Timeout.Infinite, cancellationToken).ConfigureAwait(false);
|
||||||
if (NextReadThrows is { } ex)
|
if (NextReadThrows is { } ex)
|
||||||
{
|
{
|
||||||
NextReadThrows = null;
|
NextReadThrows = null;
|
||||||
return Task.FromException<object?>(ex);
|
throw ex;
|
||||||
}
|
}
|
||||||
return Task.FromResult(ReadValue);
|
return ReadValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken) =>
|
public Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken) =>
|
||||||
throw new NotSupportedException("FakeS7Plc: block reads not needed for the reconnect tests");
|
throw new NotSupportedException("FakeS7Plc: block reads not needed for the reconnect tests");
|
||||||
|
|
||||||
public Task WriteAsync(string address, object value, CancellationToken cancellationToken) =>
|
public async Task WriteAsync(string address, object value, CancellationToken cancellationToken)
|
||||||
Task.CompletedTask;
|
{
|
||||||
|
if (WriteHangsUntilCancelled)
|
||||||
|
await Task.Delay(System.Threading.Timeout.Infinite, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken) =>
|
public Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken) =>
|
||||||
Task.CompletedTask;
|
Task.CompletedTask;
|
||||||
|
|
||||||
public Task ReadStatusAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public async Task ReadStatusAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (ReadStatusHangsUntilCancelled)
|
||||||
|
await Task.Delay(System.Threading.Timeout.Infinite, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (ReadStatusThrows is { } ex)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user