7628402893
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
85 lines
3.8 KiB
C#
85 lines
3.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
/// <summary>
|
|
/// STAB-6 / #602 — the probe loop was fire-and-forget, so shutdown cancelled and disposed while the
|
|
/// loop could still be parked in a probe-runtime call that ignores cancellation, leaving its
|
|
/// libplctag handle alive past teardown. Shutdown now joins the loop (bounded) first.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbLegacyShutdownJoinTests
|
|
{
|
|
private sealed class ParkedInitTag(
|
|
AbLegacyTagCreateParams p, TaskCompletionSource entered, TaskCompletionSource release) : FakeAbLegacyTag(p)
|
|
{
|
|
public override async Task InitializeAsync(CancellationToken ct)
|
|
{
|
|
entered.TrySetResult();
|
|
await release.Task.ConfigureAwait(false); // deliberately ignores ct
|
|
}
|
|
}
|
|
|
|
/// <summary>Shutdown blocks on a probe loop parked in runtime init, then the loop disposes its probe runtime.</summary>
|
|
[Fact]
|
|
public async Task Shutdown_WaitsForProbeLoopParkedInInit_ThenProbeRuntimeIsDisposed()
|
|
{
|
|
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new ParkedInitTag(p, entered, release),
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(50), ProbeAddress = "S:0" },
|
|
}, "drv-join", 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 runtime init");
|
|
|
|
// Positive control: releasing the parked call lets shutdown finish, with the probe runtime released.
|
|
release.SetResult();
|
|
await shutdown.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
factory.Tags["S:0"].Disposed.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>The synchronous <c>Dispose()</c> path joins the parked loop too (it used to cancel and dispose under it).</summary>
|
|
[Fact]
|
|
public async Task Dispose_WaitsForProbeLoopParkedInInit_ThenProbeRuntimeIsDisposed()
|
|
{
|
|
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var factory = new FakeAbLegacyTagFactory
|
|
{
|
|
Customise = p => new ParkedInitTag(p, entered, release),
|
|
};
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)],
|
|
Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(50), ProbeAddress = "S:0" },
|
|
}, "drv-join-dispose", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
var dispose = Task.Run(drv.Dispose);
|
|
|
|
await Task.Delay(300);
|
|
dispose.IsCompleted.ShouldBeFalse("Dispose must join the probe loop parked in runtime init");
|
|
|
|
// Positive control: releasing the parked call lets Dispose finish, with the probe runtime released.
|
|
release.SetResult();
|
|
await dispose.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
factory.Tags["S:0"].Disposed.ShouldBeTrue();
|
|
}
|
|
}
|