From 7e36842556b2941f359f6aad2a2498c932b1a08b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 06:58:25 -0400 Subject: [PATCH] test(abcip): ab_server soak of ConnectionLease.ReleaseInBackground; fixture docs (#579, #593, #594) Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- docs/drivers/AbLegacy-Test-Fixture.md | 5 +- docs/drivers/AbServer-Test-Fixture.md | 2 +- .../NativeLeaseReleaseSoakTests.cs | 77 +++++++++++++++++++ .../Docker/README.md | 2 +- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/NativeLeaseReleaseSoakTests.cs diff --git a/docs/drivers/AbLegacy-Test-Fixture.md b/docs/drivers/AbLegacy-Test-Fixture.md index 2c7f459a..ecf9894d 100644 --- a/docs/drivers/AbLegacy-Test-Fixture.md +++ b/docs/drivers/AbLegacy-Test-Fixture.md @@ -36,8 +36,9 @@ supplies a `FakeAbLegacyTag`. The `AbLegacy.IntegrationTests` suites now run **once per transport** (`TransportMatrix`: `Libplctag` and `Native`). Set `AB_LEGACY_COMPOSE_PROFILE` to the running profile. On the 2026-10-07 run native passed -everything libplctag passed on `slc500`, `micrologix` and `plc5` (the PLC-5 test reads N7 only, so the PLC-5 -refusal paths and status-bit writes are not exercised by it). Native PCCC is **simulator-only**. Write-capable +everything libplctag passed on `slc500`, `micrologix` and `plc5` (the smoke tests read N7 only; the PLC-5 +refusal paths and status-bit write refusals are exercised by `AbLegacyUnsupportedPathTests`, native only, #593 — the +refusals are the driver's own, raised before the wire, so each test carries a positive-control read). Native PCCC is **simulator-only**. Write-capable tests (the SLC500 N7:5 round trip) require `AB_SERVER_ALLOW_WRITES=1`. Golden frames: `scripts/ab-capture/capture.sh`, fixtures under `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Captures/`. Evidence: diff --git a/docs/drivers/AbServer-Test-Fixture.md b/docs/drivers/AbServer-Test-Fixture.md index 74a071e8..7c22622e 100644 --- a/docs/drivers/AbServer-Test-Fixture.md +++ b/docs/drivers/AbServer-Test-Fixture.md @@ -37,7 +37,7 @@ quirk. UDT / alarm / quirk behavior is verified only by unit tests with Every integration test in `AbCip.IntegrationTests` now runs **once per transport** (`TransportMatrix`: `Libplctag` and `Native`). Run one compose profile at a time and name it with `AB_SERVER_COMPOSE_PROFILE` -(all profiles bind `:44818`, so set it to the running profile). On the 2026-10-07 run native passed everything +(all profiles bind `:44818`, so set it to the running profile; **unset, only the default `controllogix` rows run** — ab_server does not self-identify its `--plc` mode, so crossing every family at one container failed the non-matching rows instead of skipping them, #594). `NativeLeaseReleaseSoakTests` is the ab_server soak of the background lease release (`AB_LEASE_SOAK_CYCLES`, default 100; read-only, skips without the fixture, #593). On the 2026-10-07 run native passed everything libplctag passed on all seven profiles; on `micro800` libplctag failed on the empty-path defect described in [AbCip.md](AbCip.md#known-gaps) and native passed. That defect is now fixed: a re-run of the `micro800` smoke test passes on both transports. Evidence: diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/NativeLeaseReleaseSoakTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/NativeLeaseReleaseSoakTests.cs new file mode 100644 index 00000000..0624ebb8 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/NativeLeaseReleaseSoakTests.cs @@ -0,0 +1,77 @@ +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); + budget.CancelAfter(TimeSpan.FromSeconds(120)); + + 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"); + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md index 3aec0909..18b8aef2 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md @@ -54,7 +54,7 @@ families stop the current service + start another. | `AB_LEGACY_ENDPOINT` | *unset* — per-family ports on `10.100.0.35` | `host:port` of a single PCCC device, pinning **every** family to it (real-hardware mode). | | `AB_LEGACY_CIP_PATH` | `1,0` | CIP routing path portion of the `ab://host:port/` URI. ab_server rejects empty paths at the CIP unconnected-send layer; real SLC/MicroLogix/PLC-5 hardware accepts empty (no backplane). Set to empty (`AB_LEGACY_CIP_PATH=`) when pointing at real hardware. | | `AB_SERVER_ALLOW_WRITES` | *unset* | **Write opt-in.** Every write-capable test (today: the SLC500 N7:5 write-then-read round trip) skips unless this is `1`. Set it for simulator runs; **never** set it when an endpoint variable points at production hardware. Write tests also refuse while `AB_DIFF_ENDPOINT` (the AbCip read-only differential) is set. | -| `AB_LEGACY_COMPOSE_PROFILE` | *unset* | Only consulted when `AB_LEGACY_ENDPOINT` pins an endpoint: names which family that one device is, so the SLC500-only write test skips against other hardware. Against the Docker fixture it is unnecessary — each family has its own port, and each test skips itself if its own port is down. | +| `AB_LEGACY_COMPOSE_PROFILE` | *unset* | Only consulted when `AB_LEGACY_ENDPOINT` pins an endpoint: names which family that one device is, so the SLC500-only write test skips against other hardware. When `AB_LEGACY_ENDPOINT` is set and this is not, only the `slc500` rows run (the family of a pinned device cannot be detected). Against the Docker fixture it is unnecessary — each family has its own port, and each test skips itself if its own port is down. | ## Run the integration tests