using libplctag; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// /// Driver-timeout-hardening parity with AbCip: a read/write that fails with a non-zero libplctag /// status or a transport exception must EVICT the cached runtime so the next call re-creates a /// fresh handle — rather than returning the same failure forever until the probe loop happens to /// recycle it. Previously AbLegacy evicted on neither path (unlike AbCip). /// [Trait("Category", "Unit")] public sealed class AbLegacyEvictOnFailureTests { private const string Device = "ab://10.0.0.5/1,0"; // Probe disabled — the probe loop also calls _tagFactory.Create, which would perturb the // creation count these tests assert on. private static (AbLegacyDriver drv, FakeAbLegacyTagFactory factory) NewDriver(params AbLegacyTagDefinition[] tags) { var factory = new FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(Device)], Tags = tags, Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); return (drv, factory); } /// A non-zero libplctag status on read evicts the runtime; the next read creates a fresh handle and recovers. [Fact] public async Task Read_nonzero_status_evicts_runtime_so_next_read_creates_fresh_handle() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int)); var callCount = 0; factory.Customise = p => { callCount++; return callCount == 1 ? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection } : new FakeAbLegacyTag(p) { Status = 0, Value = 42 }; }; await drv.InitializeAsync("{}", CancellationToken.None); // First read — bad status → runtime evicted, no reuse. var first = await drv.ReadAsync(["Counter"], CancellationToken.None); first.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good); // Second read — fresh handle, succeeds. var second = await drv.ReadAsync(["Counter"], CancellationToken.None); second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); second.Single().Value.ShouldBe(42); callCount.ShouldBe(2); // one failed handle + one fresh handle } /// A transport exception on read evicts the runtime; the next read creates a fresh handle and recovers. [Fact] public async Task Read_transport_exception_evicts_runtime_so_next_read_creates_fresh_handle() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int)); var callCount = 0; factory.Customise = p => { callCount++; return callCount == 1 ? new FakeAbLegacyTag(p) { ThrowOnRead = true, Exception = new System.Net.Sockets.SocketException() } : new FakeAbLegacyTag(p) { Status = 0, Value = 7 }; }; await drv.InitializeAsync("{}", CancellationToken.None); var first = await drv.ReadAsync(["Counter"], CancellationToken.None); first.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError); var second = await drv.ReadAsync(["Counter"], CancellationToken.None); second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); second.Single().Value.ShouldBe(7); callCount.ShouldBe(2); } /// A non-zero libplctag status on write evicts the runtime; the next write creates a fresh handle and recovers. [Fact] public async Task Write_nonzero_status_evicts_runtime_so_next_write_creates_fresh_handle() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int)); var callCount = 0; factory.Customise = p => { callCount++; return callCount == 1 ? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection } : new FakeAbLegacyTag(p) { Status = 0 }; }; await drv.InitializeAsync("{}", CancellationToken.None); var first = await drv.WriteAsync([new WriteRequest("Counter", (short)5)], CancellationToken.None); first.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good); var second = await drv.WriteAsync([new WriteRequest("Counter", (short)5)], CancellationToken.None); second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); callCount.ShouldBe(2); } }