test(abcip): native end-to-end proof a NotFound tag causes no lease churn
L4 evidence over the real native runtime and a fake CIP target: a renamed tag that is the device's only lease holder is created once and Forward-Opened once across five polls, with no Forward Close, every poll BadNodeIdUnknown and the driver Healthy. Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -2,6 +2,7 @@ using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
@@ -178,4 +179,50 @@ public sealed class AbCipDeviceRefusalTests
|
||||
factory.Tags["ReadOnlyTag"].Disposed.ShouldBeFalse();
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// L4 end to end over the REAL native runtime + a private pool: a renamed tag (CIP 0x04) that is the device's
|
||||
/// ONLY lease holder used to be evicted every poll — the last release, so Forward Close, then a throttled Forward
|
||||
/// Open on the next poll. One runtime and one Forward Open across N polls and no Forward Close proves no churn.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_native_NotFound_tag_causes_no_runtime_or_connection_churn()
|
||||
{
|
||||
await using var target = new FakeCipTarget();
|
||||
target.ConnectedHandler = req => Task.FromResult<byte[]?>(
|
||||
req[0] == 0x52 ? [0xD2, 0x00, 0x04, 0x00] : target.ServiceReply(req));
|
||||
var host = $"ab://127.0.0.1:{target.Port}/1,0";
|
||||
var creates = 0;
|
||||
var native = new NativeAbCipTagFactory(new DeviceSessionPool());
|
||||
var factory = new CountingFactory(p => { Interlocked.Increment(ref creates); return native.Create(p); });
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(host, AbCipPlcFamily.ControlLogix)],
|
||||
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Renamed", host, "Renamed", AbCipDataType.DInt)),
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
}, "abcip-l4-native", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
try
|
||||
{
|
||||
for (var n = 0; n < 5; n++)
|
||||
(await drv.ReadAsync(["Renamed"], CancellationToken.None))[0].StatusCode
|
||||
.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
|
||||
|
||||
Volatile.Read(ref creates).ShouldBe(1);
|
||||
target.ForwardOpens.ShouldBe(1);
|
||||
target.ForwardCloses.ShouldBe(0);
|
||||
target.ConnectedRequests.ShouldBeGreaterThanOrEqualTo(5); // positive control: every poll hit the wire
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await drv.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CountingFactory(Func<AbCipTagCreateParams, IAbCipTagRuntime> create) : IAbCipTagFactory
|
||||
{
|
||||
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) => create(createParams);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user