using Shouldly; using ZB.MOM.WW.OtOpcUa.Tests; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// /// #630 — InitializeAsync on a live driver with no teardown first (the IDriver contract /// allows it) overwrote _devices[host] and orphaned the previous state's /// probe loop + runtime. It now releases them first. /// [Trait("Category", "Unit")] public sealed class AbLegacyReinitializeReleasesStateTests(ITestOutputHelper output) { /// Initialize twice: the first state's probe loop has stopped and its probe runtime is disposed. [Fact] public async Task SecondInitialize_StopsFirstStatesLoop_AndDisposesItsRuntime() { const string host = "ab://10.0.0.5/1,0"; var factory = new FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.Slc500)], Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(20), ProbeAddress = "S:0" }, }, "drv-reinit", factory); await FixtureCleanup.RunAsync(async () => { await drv.InitializeAsync("{}", CancellationToken.None); var first = drv.GetDeviceState(host)!; // Positive control: the first state's probe loop is running and has created + initialised its runtime. var deadline = DateTime.UtcNow.AddSeconds(5); while (!(factory.Tags.TryGetValue("S:0", out var t) && t.InitializeCount > 0)) { if (DateTime.UtcNow > deadline) throw new TimeoutException("probe never started"); await Task.Delay(10); } var firstTag = factory.Tags["S:0"]; 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($"AbLegacy SecondInitialize cleanup failed: {failure}")); } /// Failure after a live first or replacement state must release both owned resources. [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 FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.Slc500)], Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(20), ProbeAddress = "S:0" }, }, "drv-reinit", factory); Task? firstLoop = null, secondLoop = null; FakeAbLegacyTag? firstTag = null, secondTag = null; var sentinel = new InvalidOperationException("body sentinel"); var actual = await Should.ThrowAsync(() => FixtureCleanup.RunAsync(async () => { await drv.InitializeAsync("{}", CancellationToken.None); await WaitForProbeAsync(() => factory.Tags.TryGetValue("S:0", out var tag) && tag.InitializeCount > 0); firstTag = factory.Tags["S:0"]; 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("S:0", out var tag) && !ReferenceEquals(tag, firstTag) && tag.InitializeCount > 0); secondTag = factory.Tags["S:0"]; secondLoop = drv.GetDeviceState(host)!.ProbeTask; _ = secondLoop.ShouldNotBeNull(); secondLoop!.IsCompleted.ShouldBeFalse(); } throw sentinel; }, () => drv.ShutdownAsync(CancellationToken.None), failure => output.WriteLine($"AbLegacy 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 condition) { var deadline = DateTime.UtcNow.AddSeconds(5); while (!condition()) { if (DateTime.UtcNow > deadline) throw new TimeoutException("probe never started"); await Task.Delay(10); } } }