5a5c80ee57
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
58 lines
2.8 KiB
C#
58 lines
2.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
/// <summary>
|
|
/// STAB-23 / #602 — the probe / recycle / fixed-tree loops were fire-and-forget, so shutdown
|
|
/// cancelled and disposed while a loop could still be parked inside a connect that ignores
|
|
/// cancellation, and that connect then completed against a shut-down driver. Shutdown now joins
|
|
/// the loops (bounded) before disposing the client.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasShutdownJoinTests
|
|
{
|
|
/// <summary>A client whose connect parks until released and deliberately ignores the cancellation token.</summary>
|
|
private sealed class ParkedConnectClient(TaskCompletionSource entered, TaskCompletionSource release) : FakeFocasClient
|
|
{
|
|
public override async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken ct)
|
|
{
|
|
entered.TrySetResult();
|
|
await release.Task.ConfigureAwait(false);
|
|
await base.ConnectAsync(address, timeout, CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>Shutdown blocks on a probe loop parked in connect, then disposes the client that connect produced.</summary>
|
|
[Fact]
|
|
public async Task Shutdown_WaitsForLoopParkedInConnect_ThenDisposesItsClient()
|
|
{
|
|
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var factory = new FakeFocasClientFactory { Customise = () => new ParkedConnectClient(entered, release) };
|
|
var options = new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
|
|
RawTags = [FocasRawTags.Entry(new FocasTagDefinition("R", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
|
|
Probe = new FocasProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(50), Timeout = TimeSpan.FromSeconds(10) },
|
|
};
|
|
var drv = new FocasDriver(options, "focas-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
var shutdown = drv.ShutdownAsync(CancellationToken.None);
|
|
|
|
await Task.Delay(300);
|
|
shutdown.IsCompleted.ShouldBeFalse("shutdown must join the probe loop parked in connect");
|
|
|
|
// Positive control: releasing the parked connect lets shutdown finish.
|
|
release.SetResult();
|
|
await shutdown.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
factory.Clients.ShouldNotBeEmpty();
|
|
factory.Clients.ShouldAllBe(c => c.DisposeCount >= 1);
|
|
factory.Clients.ShouldAllBe(c => !c.IsConnected);
|
|
}
|
|
}
|