459cbb444e
I1: the U11 Bits[n] -> Bits[n/32] rewrite was unconditional. The native runtime now keeps the original element path until the first reply settles the layout: 0xD3 confirms DWORD addressing (bit n%32); any other reply type (e.g. a 0xC1 per-element BOOL array) or a refusal of the DWORD index falls back to Bits[n] and re-reads, and a write settles the layout by a read first, so it can never decode or write element n/32 of an element-addressed array. I2: libplctag ErrorNoMatch/ErrorBadDevice no longer fold into NotFound (they now read as BadConnection: comms, Degraded, LastSuccessfulRead preserved). ErrorNotImplemented - and the native runtime's own pre-send refusals - raise AbCipLocalRefusalException: BadNotSupported, no health, LastSuccessfulRead NOT advanced, no backoff record, not reachable for the probe or Test Connect. M1: the bit RMW's parent read/write status words follow the health contract (refusal: per-tag Bad + Warning; comms: evict the parent + Degraded). M2: device Unsupported read refusals return Refused (advance LastSuccessfulRead); device write refusals call RecordWriteAnswered (not the pre-send native type gate). M3: a good write clears a Degraded a write comms fault recorded (a read-side Degraded is still left for the read path). M4: a superseded init throws AbCipInitializationSupersededException (not an OCE); ShutdownAsync stamps Unknown only if no newer init published meanwhile. M5: the sync-Dispose timing assert is 2 s against a 4 s probe read; the unencodable-array test asserts health. Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
109 lines
4.8 KiB
C#
109 lines
4.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Task 22 review r1: a runtime that refuses an operation with <see cref="AbStatus.Unsupported"/> (the native
|
|
/// transport does this before the wire, e.g. a write it cannot type — an <see cref="AbCipLocalRefusalException"/>
|
|
/// since #737 I2) reaches the client as BadNotSupported —
|
|
/// NOT as a comms fault. The handle is kept (one create, never disposed) and driver health is untouched.
|
|
/// </summary>
|
|
[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 AbCipLocalRefusalException("refused read"))
|
|
: base.ReadAsync(cancellationToken);
|
|
|
|
public override Task WriteAsync(CancellationToken cancellationToken) =>
|
|
refuseWrite
|
|
? Task.FromException(new AbCipLocalRefusalException("refused write"))
|
|
: base.WriteAsync(cancellationToken);
|
|
}
|
|
|
|
private static async Task<(AbCipDriver Driver, FakeAbCipTagFactory Factory, Func<int> 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);
|
|
}
|
|
|
|
/// <summary>Control: any OTHER transport failure is still a comms fault (evicted, Degraded).</summary>
|
|
[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"));
|
|
}
|
|
}
|