using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Task 22 review r1: a runtime that refuses an operation with (the native /// transport does this before the wire, e.g. a write it cannot type) reaches the client as BadNotSupported — /// NOT as a comms fault. The handle is kept (one create, never disposed) and driver health is untouched. /// [Trait("Category", "Unit")] public sealed class AbCipLocalRefusalTests { private const string Host = "ab://10.0.0.5/1,0"; private sealed class RefusingTag(AbCipTagCreateParams p, bool refuseRead, bool refuseWrite) : FakeAbCipTag(p) { public override Task ReadAsync(CancellationToken cancellationToken) => refuseRead ? Task.FromException(new AbTransportException(AbStatus.Unsupported, "refused read")) : base.ReadAsync(cancellationToken); public override Task WriteAsync(CancellationToken cancellationToken) => refuseWrite ? Task.FromException(new AbTransportException(AbStatus.Unsupported, "refused write")) : base.WriteAsync(cancellationToken); } private static async Task<(AbCipDriver Driver, FakeAbCipTagFactory Factory, Func Creates)> NewAsync( bool refuseRead, bool refuseWrite, AbCipDataType type = AbCipDataType.DInt) { var creates = 0; var factory = new FakeAbCipTagFactory { Customise = p => { Interlocked.Increment(ref creates); return new RefusingTag(p, refuseRead, refuseWrite) { Value = 1 }; }, }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(Host)], RawTags = AbCipRawTags.From(new AbCipTagDefinition("T", Host, "T", type)), Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); return (drv, factory, () => Volatile.Read(ref creates)); } #pragma warning disable OTOPCUA0001 // the driver's own status mapping is under test, not resilience wrapping [Fact] public async Task A_refused_write_is_BadNotSupported_keeps_the_handle_and_leaves_health_alone() { var (drv, factory, creates) = await NewAsync(refuseRead: false, refuseWrite: true); (await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode.ShouldBe(AbCipStatusMapper.Good); var health = drv.GetHealth(); (await drv.WriteAsync([new WriteRequest("T", 5)], CancellationToken.None)).Single().StatusCode .ShouldBe(AbCipStatusMapper.BadNotSupported); drv.GetHealth().ShouldBe(health); factory.Tags["T"].Disposed.ShouldBeFalse(); (await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode.ShouldBe(AbCipStatusMapper.Good); creates().ShouldBe(1); } [Fact] public async Task A_refused_read_is_BadNotSupported_keeps_the_handle_and_leaves_health_alone() { var (drv, factory, creates) = await NewAsync(refuseRead: true, refuseWrite: false); var health = drv.GetHealth(); for (var n = 0; n < 2; n++) (await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode .ShouldBe(AbCipStatusMapper.BadNotSupported); drv.GetHealth().ShouldBe(health); factory.Tags["T"].Disposed.ShouldBeFalse(); creates().ShouldBe(1); } /// Control: any OTHER transport failure is still a comms fault (evicted, Degraded). [Fact] public async Task A_non_refusal_write_failure_still_evicts_and_degrades() { var factory = new FakeAbCipTagFactory(); factory.Customise = p => new ThrowingWriteTag(p) { Value = 1 }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(Host)], RawTags = AbCipRawTags.From(new AbCipTagDefinition("T", Host, "T", AbCipDataType.DInt)), Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); (await drv.WriteAsync([new WriteRequest("T", 5)], CancellationToken.None)).Single().StatusCode .ShouldBe(AbCipStatusMapper.BadCommunicationError); drv.GetHealth().State.ShouldBe(DriverState.Degraded); factory.Tags["T"].Disposed.ShouldBeTrue(); } #pragma warning restore OTOPCUA0001 private sealed class ThrowingWriteTag(AbCipTagCreateParams p) : FakeAbCipTag(p) { public override Task WriteAsync(CancellationToken cancellationToken) => Task.FromException(new AbTransportException(AbStatus.BadConnection, "socket closed")); } }