using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests; /// /// Gitea #579 / #593: soak of ConnectionLease.ReleaseInBackground against a real ab_server. /// The offline NativeAbCipLeaseReleaseTests pin the ordering against a fake CIP target for 25 cycles; /// this drives the same release-then-immediately-recreate loop over a real socket and the simulator's own /// connection bookkeeping, where a Forward Open racing the predecessor's still-in-flight Forward Close would /// be refused or would leak a connection slot. /// /// /// Skips when ab_server is unreachable (). Cycle count: /// AB_LEASE_SOAK_CYCLES (default 100). Read-only against the target (one DINT read per cycle), so it /// needs no write opt-in. /// [Trait("Category", "Integration")] [Trait("Requires", "AbServer")] public sealed class NativeLeaseReleaseSoakTests { private static readonly TimeSpan Guard = TimeSpan.FromSeconds(60); private static int Cycles => int.TryParse(Environment.GetEnvironmentVariable("AB_LEASE_SOAK_CYCLES"), out var n) && n > 0 ? n : 100; /// /// Each cycle: create a native runtime, initialise, read, then Dispose (the background release) and /// re-create at once. Every read must be Good, exactly one Forward Open and one Forward Close per cycle /// must hit the wire (a re-create that overlapped the close, or a close that never ran, shows as a count /// mismatch), and the pool must end with no live entry. /// [AbServerFact] public async Task Release_in_background_then_immediate_recreate_never_overlaps_or_leaks_against_ab_server() { var fixture = new AbServerFixture(KnownProfiles.ControlLogix); var pool = new DeviceSessionPool(); var factory = new NativeAbCipTagFactory(pool); var cycles = Cycles; var teardowns = new List(cycles); var createParams = new AbCipTagCreateParams( Gateway: fixture.Host, Port: fixture.Port, CipPath: "1,0", LibplctagPlcAttribute: "controllogix", TagName: "TestDINT", Timeout: TimeSpan.FromSeconds(5), AllowPacking: false, ElementCount: 1, IsArray: false, AuthoredType: AbCipDataType.DInt, UseConnectedMessaging: true); using var budget = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); // ~1 s per cycle against ab_server (close + re-open + read), so the budget scales with the // cycle count; a fixed budget made AB_LEASE_SOAK_CYCLES above ~100 time out, not fail. budget.CancelAfter(TimeSpan.FromSeconds(60 + 3 * cycles)); for (var i = 0; i < cycles; i++) { var runtime = (NativeAbCipTagRuntime)factory.Create(createParams); await runtime.InitializeAsync(budget.Token); await runtime.ReadAsync(budget.Token); runtime.GetStatus().ShouldBe(0, $"cycle {i}: read status"); runtime.DecodeValue(AbCipDataType.DInt, null).ShouldNotBeNull($"cycle {i}: decoded value"); runtime.Dispose(); // background release; the next Create must queue behind its close teardowns.Add(runtime.TeardownTask ?? Task.CompletedTask); } await Task.WhenAll(teardowns).WaitAsync(Guard, TestContext.Current.CancellationToken); pool.EntryCount.ShouldBe(0, "every lease was released, so no pooled connection may remain"); pool.Diagnostics.ForwardOpens.ShouldBe(cycles, "one Forward Open per cycle: a re-create must wait for its predecessor's close"); pool.Diagnostics.ForwardClosesSent.ShouldBe(cycles, "one Forward Close per cycle: none dropped, none doubled"); } }