using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// bounds the gateway CloseSession round-trip by /// the caller's token (the SDK dispose closes under its 30 s call timeout) and leaves the session /// re-openable. /// public sealed class GalaxyMxSessionCloseTests { /// A close against a never-answering gateway returns when the token fires, without throwing. [Fact] public async Task CloseAsync_returns_at_the_token_deadline_against_a_never_answering_gateway() { var session = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest")) { OpenAndRegisterOverrideForTests = _ => Task.CompletedTask, CloseOverrideForTests = ct => Task.Delay(Timeout.Infinite, ct), }; await session.ConnectAsync(null!, CancellationToken.None); session.IsConnected.ShouldBeTrue(); using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); var sw = System.Diagnostics.Stopwatch.StartNew(); await session.CloseAsync(cts.Token).WaitAsync(TimeSpan.FromSeconds(10)); sw.Stop(); sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5)); session.IsConnected.ShouldBeFalse(); // Still usable: a close is not a dispose. await session.ConnectAsync(null!, CancellationToken.None); session.IsConnected.ShouldBeTrue(); } /// Positive control: a close that answers completes and reports the token it was handed. [Fact] public async Task CloseAsync_completes_when_the_gateway_answers() { CancellationToken seen = default; var session = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest")) { OpenAndRegisterOverrideForTests = _ => Task.CompletedTask, CloseOverrideForTests = ct => { seen = ct; return Task.CompletedTask; }, }; await session.ConnectAsync(null!, CancellationToken.None); using var cts = new CancellationTokenSource(); await session.CloseAsync(cts.Token); seen.ShouldBe(cts.Token, "the caller's token must reach the gateway round-trip"); session.IsConnected.ShouldBeFalse(); } /// /// The driver's teardown is CloseAsync (bounded) then DisposeAsync (final). The dispose must /// not issue a second gateway close; a fresh connect after a close must close again (positive /// control: the guard keys on "was connected", not on "ever closed"). /// [Fact] public async Task CloseAsync_then_DisposeAsync_closes_the_gateway_session_once() { var closes = 0; var session = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest")) { OpenAndRegisterOverrideForTests = _ => Task.CompletedTask, CloseOverrideForTests = _ => { Interlocked.Increment(ref closes); return Task.CompletedTask; }, }; await session.ConnectAsync(null!, CancellationToken.None); await session.CloseAsync(CancellationToken.None); await session.DisposeAsync(); closes.ShouldBe(1, "dispose after a close must not close the gateway session again"); var reopened = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest")) { OpenAndRegisterOverrideForTests = _ => Task.CompletedTask, CloseOverrideForTests = _ => { Interlocked.Increment(ref closes); return Task.CompletedTask; }, }; await reopened.ConnectAsync(null!, CancellationToken.None); await reopened.CloseAsync(CancellationToken.None); await reopened.ConnectAsync(null!, CancellationToken.None); await reopened.DisposeAsync(); closes.ShouldBe(3, "each live session closes exactly once"); } }