fix(twincat): probe recycle honors cancellation — no uncancellable gate wait (05/STAB-12 compounded part); B6 complete (TwinCAT suite 189 green)
This commit is contained in:
@@ -328,6 +328,42 @@ public sealed class TwinCATReconnectReplayTests
|
||||
factory.Clients[1].Notifications.ShouldBeEmpty("the fresh handle must be disposed — no orphan ADS notification");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// STAB-12 (compounded part): <c>RecycleClientAsync</c> must honor cancellation — it must
|
||||
/// take the probe token into <c>ConnectGate.WaitAsync(ct)</c> rather than
|
||||
/// <c>CancellationToken.None</c>, so a shutdown mid-recycle (or a connect wedged under the
|
||||
/// gate) can't hang the probe loop uncancellably. Verified white-box: with the gate held and
|
||||
/// a cancelled token, the call must throw promptly rather than block forever.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RecycleClient_HonorsCancellation()
|
||||
{
|
||||
var (drv, _) = NewNativeDriver(
|
||||
tags: new TwinCATTagDefinition("X", Host, "MAIN.X", TwinCATDataType.DInt));
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
// Reach the single configured device + wedge its ConnectGate so a recycle must wait on it.
|
||||
var devicesField = typeof(TwinCATDriver).GetField("_devices",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
var devicesObj = devicesField.GetValue(drv)!;
|
||||
var values = (System.Collections.IEnumerable)devicesObj.GetType().GetProperty("Values")!.GetValue(devicesObj)!;
|
||||
var device = (TwinCATDriver.DeviceState)values.Cast<object>().First();
|
||||
await device.ConnectGate.WaitAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
var recycle = typeof(TwinCATDriver).GetMethod("RecycleClientAsync",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await cts.CancelAsync();
|
||||
var task = (Task)recycle.Invoke(drv, [device, cts.Token])!;
|
||||
|
||||
// Must throw promptly (cancelled token), NOT hang on the held gate. The 2 s bound turns an
|
||||
// uncancellable-wait regression into a failure rather than a hung suite.
|
||||
await Should.ThrowAsync<OperationCanceledException>(async () => await task.WaitAsync(TimeSpan.FromSeconds(2)));
|
||||
|
||||
device.ConnectGate.Release();
|
||||
}
|
||||
|
||||
private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
|
||||
Reference in New Issue
Block a user