7e36842556
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
78 lines
3.7 KiB
C#
78 lines
3.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Gitea #579 / #593: soak of <c>ConnectionLease.ReleaseInBackground</c> against a real <c>ab_server</c>.
|
|
/// The offline <c>NativeAbCipLeaseReleaseTests</c> 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Skips when ab_server is unreachable (<see cref="AbServerFactAttribute"/>). Cycle count:
|
|
/// <c>AB_LEASE_SOAK_CYCLES</c> (default 100). Read-only against the target (one DINT read per cycle), so it
|
|
/// needs no write opt-in.
|
|
/// </remarks>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// Each cycle: create a native runtime, initialise, read, then <c>Dispose</c> (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.
|
|
/// </summary>
|
|
[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<Task>(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");
|
|
}
|
|
}
|