122 lines
5.2 KiB
C#
122 lines
5.2 KiB
C#
using Shouldly;
|
|
using ZB.MOM.WW.OtOpcUa.Tests;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
/// <summary>
|
|
/// #630 (same pattern) — <c>InitializeAsync</c> on a live driver overwrote <c>_devices[host]</c> and
|
|
/// orphaned the previous state's probe loop + handles. It now releases them first.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipReinitializeReleasesStateTests(ITestOutputHelper output)
|
|
{
|
|
/// <summary>Initialize twice: the first state's probe loop has stopped and its probe handle is disposed.</summary>
|
|
[Fact]
|
|
public async Task SecondInitialize_StopsFirstStatesLoop_AndDisposesItsHandles()
|
|
{
|
|
const string host = "ab://10.0.0.5/1,0";
|
|
var factory = new FakeAbCipTagFactory();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions(host)],
|
|
Probe = new AbCipProbeOptions
|
|
{
|
|
Enabled = true,
|
|
Interval = TimeSpan.FromMilliseconds(20),
|
|
Timeout = TimeSpan.FromMilliseconds(200),
|
|
ProbeTagPath = "@raw_cpu_type",
|
|
},
|
|
}, "drv-reinit", factory);
|
|
await FixtureCleanup.RunAsync(async () =>
|
|
{
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var first = drv.GetDeviceState(host)!;
|
|
// Positive control: the first probe loop is running and has initialised its runtime.
|
|
var deadline = DateTime.UtcNow.AddSeconds(5);
|
|
while (!(factory.Tags.TryGetValue("@raw_cpu_type", out var t) && t.InitializeCount > 0))
|
|
{
|
|
if (DateTime.UtcNow > deadline) throw new TimeoutException("probe never started");
|
|
await Task.Delay(10);
|
|
}
|
|
var firstTag = factory.Tags["@raw_cpu_type"];
|
|
var firstLoop = first.ProbeTask;
|
|
_ = firstLoop.ShouldNotBeNull();
|
|
firstLoop!.IsCompleted.ShouldBeFalse();
|
|
firstTag.Disposed.ShouldBeFalse();
|
|
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
firstLoop.IsCompleted.ShouldBeTrue(); // joined
|
|
firstTag.Disposed.ShouldBeTrue();
|
|
drv.GetDeviceState(host).ShouldNotBeSameAs(first);
|
|
}, () => drv.ShutdownAsync(CancellationToken.None),
|
|
failure => output.WriteLine($"AbCip SecondInitialize cleanup failed: {failure}"));
|
|
}
|
|
|
|
/// <summary>Failure after a live first or replacement state must release both owned resources.</summary>
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task Body_failure_releases_live_probe_resources(bool afterReplacement)
|
|
{
|
|
const string host = "ab://10.0.0.5/1,0";
|
|
var factory = new FakeAbCipTagFactory();
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions(host)],
|
|
Probe = new AbCipProbeOptions
|
|
{
|
|
Enabled = true,
|
|
Interval = TimeSpan.FromMilliseconds(20),
|
|
Timeout = TimeSpan.FromMilliseconds(200),
|
|
ProbeTagPath = "@raw_cpu_type",
|
|
},
|
|
}, "drv-reinit", factory);
|
|
Task? firstLoop = null, secondLoop = null;
|
|
FakeAbCipTag? firstTag = null, secondTag = null;
|
|
var sentinel = new InvalidOperationException("body sentinel");
|
|
var actual = await Should.ThrowAsync<InvalidOperationException>(() => FixtureCleanup.RunAsync(async () =>
|
|
{
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
await WaitForProbeAsync(() => factory.Tags.TryGetValue("@raw_cpu_type", out var tag) && tag.InitializeCount > 0);
|
|
firstTag = factory.Tags["@raw_cpu_type"];
|
|
firstLoop = drv.GetDeviceState(host)!.ProbeTask;
|
|
_ = firstLoop.ShouldNotBeNull();
|
|
firstLoop!.IsCompleted.ShouldBeFalse();
|
|
firstTag.Disposed.ShouldBeFalse();
|
|
if (afterReplacement)
|
|
{
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
await WaitForProbeAsync(() => factory.Tags.TryGetValue("@raw_cpu_type", out var tag) && !ReferenceEquals(tag, firstTag) && tag.InitializeCount > 0);
|
|
secondTag = factory.Tags["@raw_cpu_type"];
|
|
secondLoop = drv.GetDeviceState(host)!.ProbeTask;
|
|
_ = secondLoop.ShouldNotBeNull();
|
|
secondLoop!.IsCompleted.ShouldBeFalse();
|
|
}
|
|
throw sentinel;
|
|
}, () => drv.ShutdownAsync(CancellationToken.None),
|
|
failure => output.WriteLine($"AbCip failure fixture cleanup failed: {failure}")));
|
|
actual.ShouldBeSameAs(sentinel);
|
|
firstLoop!.IsCompleted.ShouldBeTrue();
|
|
firstTag!.Disposed.ShouldBeTrue();
|
|
if (afterReplacement)
|
|
{
|
|
secondLoop!.IsCompleted.ShouldBeTrue();
|
|
secondTag!.Disposed.ShouldBeTrue();
|
|
}
|
|
}
|
|
|
|
private static async Task WaitForProbeAsync(Func<bool> condition)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddSeconds(5);
|
|
while (!condition())
|
|
{
|
|
if (DateTime.UtcNow > deadline) throw new TimeoutException("probe never started");
|
|
await Task.Delay(10);
|
|
}
|
|
}
|
|
|
|
}
|