using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// 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. /// [Trait("Category", "Unit")] public sealed class FocasShutdownJoinTests { /// A client whose connect parks until released and deliberately ignores the cancellation token. 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); } } /// Shutdown blocks on a probe loop parked in connect, then disposes the client that connect produced. [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); } }