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);
}
}