Merge #570/#579: native AbCip per-tag config errors, no mistype churn, STRING 82, LWORD type checks; non-blocking lease release (AbCip + AbLegacy)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe # Conflicts: # src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/Cip/CipPath.cs
This commit is contained in:
+54
@@ -630,4 +630,58 @@ public sealed class NativeAbLegacyTagRuntimeTests
|
||||
runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(4321);
|
||||
server.HandlerErrors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #579 offline soak: N create → read → release cycles over a pooled connection, each release
|
||||
/// fire-and-forget (<see cref="ConnectionLease.ReleaseInBackground"/>, the shape the native AbCip runtime
|
||||
/// now shares) and each create immediately after it. Every TCP connection opened is closed, one per
|
||||
/// cycle, and the pool ends empty. A fast-running monotonic clock lets the 1 s open throttle pass each
|
||||
/// cycle without the test taking N seconds; the throttle is still exercised.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Repeated_create_and_release_cycles_leak_no_connection()
|
||||
{
|
||||
const int cycles = 25;
|
||||
byte[] fixedId = [0x07, 0x3D, 0xF3, 0x45, 0x43, 0x50, 0x21];
|
||||
await using var server = new FakeEipServer { ZeroReplyContext = true };
|
||||
server.Handler = req =>
|
||||
{
|
||||
if (FakeEipServer.Command(req) != Ab.Transport.Eip.EncapCommand.SendRRData) return null;
|
||||
var cip = FakeEipServer.RequestCip(req);
|
||||
byte[] reply = [0xCB, 0x00, 0x00, 0x00, .. fixedId, 0x4F, 0x00, cip[15], cip[16], 0xE1, 0x10];
|
||||
return FakeEipServer.RRReply(req, reply);
|
||||
};
|
||||
var pool = new DeviceSessionPool(new FastForwardClock());
|
||||
var p = new AbLegacyTagCreateParams("127.0.0.1", server.Port, "1,0", "slc500", "N7:0", TimeSpan.FromSeconds(5));
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
for (var i = 0; i < cycles; i++)
|
||||
{
|
||||
var runtime = new NativeAbLegacyTagRuntime(p, pool);
|
||||
await runtime.InitializeAsync(ct);
|
||||
await runtime.ReadAsync(ct);
|
||||
runtime.GetStatus().ShouldBe(0);
|
||||
runtime.DecodeValue(AbLegacyDataType.Int, null).ShouldBe(4321);
|
||||
runtime.Dispose(); // not awaited — the next cycle starts at once
|
||||
}
|
||||
|
||||
var giveUp = DateTime.UtcNow + TimeSpan.FromSeconds(15);
|
||||
while ((server.OpenConnections != 0 || pool.EntryCount != 0) && DateTime.UtcNow < giveUp)
|
||||
await Task.Delay(5, ct);
|
||||
server.OpenConnections.ShouldBe(0);
|
||||
server.Connections.ShouldBe(cycles);
|
||||
pool.EntryCount.ShouldBe(0);
|
||||
server.HandlerErrors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Real timers; a monotonic clock that jumps 10 s per read, so the open throttle never waits.</summary>
|
||||
private sealed class FastForwardClock : TimeProvider
|
||||
{
|
||||
private long _skew;
|
||||
|
||||
public override long GetTimestamp() =>
|
||||
System.GetTimestamp() + Interlocked.Add(ref _skew, 10 * System.TimestampFrequency);
|
||||
|
||||
public override long TimestampFrequency => System.TimestampFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user