Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasShutdownJoinTests.cs
T

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);
}
}