fix(ablegacy): an Unsupported the device replied with is an answered refusal, not a local one
Review I2: Driver.Ab.Transport raises statuses that came off the wire in an Execute PCCC reply (envelope CIP status, PCCC STS) as AbDeviceReplyException. AbLegacy's IsLocalRefusal now excludes it, and the new IsAnsweredRefusal (used by the read, write, probe and indirect-index paths) counts a wire Unsupported - CIP 0x08/0x1C/0x20 on the envelope, STS 0x10 - as answered: per-tag BadNotSupported, Healthy, LastSuccessfulRead advanced. Before, they landed in the 'refused before the wire' neutral class. Pre-wire refusals are unchanged (still neutral). A socket test covers all four codes (red before); transport tests pin the marker and a pre-wire builder refusal as the positive control. The AbCip Forward Open BadConnection path is untouched. Refs #747 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+17
-1
@@ -148,7 +148,23 @@ public sealed class PcccServicesTests
|
||||
|
||||
[Fact]
|
||||
public void ParseReply_cip_error_status_throws() =>
|
||||
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply("", 0x08), 1)).Status.ShouldBe(AbStatus.Unsupported);
|
||||
Should.Throw<AbDeviceReplyException>(() => PcccServices.ParseReply(Reply("", 0x08), 1)).Status.ShouldBe(AbStatus.Unsupported);
|
||||
|
||||
/// <summary>Review I2: a status the device REPLIED with is marked as such (envelope CIP status and PCCC STS).</summary>
|
||||
[Fact]
|
||||
public void ParseReply_marks_device_statuses_as_device_replies()
|
||||
{
|
||||
Should.Throw<AbDeviceReplyException>(() => PcccServices.ParseReply(Reply("", 0x20), 1)).Status.ShouldBe(AbStatus.Unsupported);
|
||||
Should.Throw<AbDeviceReplyException>(() => PcccServices.ParseReply(Reply(Id + "4F10 9987"), 0x8799)).Status.ShouldBe(AbStatus.Unsupported);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: a refusal raised by a builder before anything is sent is NOT a device reply.</summary>
|
||||
[Fact]
|
||||
public void A_pre_wire_builder_refusal_is_not_a_device_reply()
|
||||
{
|
||||
var ex = Should.Throw<AbTransportException>(() => PcccServices.SlcTypedRead(1, A("N7:0"), 999));
|
||||
ex.ShouldNotBeOfType<AbDeviceReplyException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseReply_wrong_command_byte_is_BadReply() =>
|
||||
|
||||
+40
@@ -20,6 +20,16 @@ public sealed class NativePcccBridgeStatusHealthTests
|
||||
{
|
||||
private static readonly byte[] FixedId = [0x07, 0x3D, 0xF3, 0x45, 0x43, 0x50, 0x21];
|
||||
|
||||
/// <summary>A fake whose Execute PCCC envelope itself is refused with CIP general status <paramref name="general"/>.</summary>
|
||||
private static FakeEipServer CipEnvelopeServer(byte general)
|
||||
{
|
||||
var server = new FakeEipServer { ZeroReplyContext = true };
|
||||
server.Handler = req => FakeEipServer.Command(req) == Ab.Transport.Eip.EncapCommand.SendRRData
|
||||
? FakeEipServer.RRReply(req, [0xCB, 0x00, general, 0x00])
|
||||
: null;
|
||||
return server;
|
||||
}
|
||||
|
||||
/// <summary>A PCCC fake that answers every request with one fixed STS.</summary>
|
||||
private static FakeEipServer StsServer(byte sts)
|
||||
{
|
||||
@@ -87,5 +97,35 @@ public sealed class NativePcccBridgeStatusHealthTests
|
||||
await driver.ShutdownAsync(ct);
|
||||
server.HandlerErrors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review I2: a refusal that came OFF THE WIRE — the device (or the bridge's Execute PCCC object) answered with
|
||||
/// CIP 0x08 / 0x1C / 0x20 on the envelope, or PCCC STS 0x10 — is an answered refusal: per-tag BadNotSupported,
|
||||
/// Healthy, <c>LastSuccessfulRead</c> advanced. It must not land in the "refused before the wire" neutral class
|
||||
/// (which advances nothing). The local-refusal counterpart stays neutral (NativeAbLegacyRefusedWriteTests).
|
||||
/// </summary>
|
||||
/// <param name="general">The CIP general status of the envelope reply, or 0 for a PCCC STS 0x10 reply.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)0x08)]
|
||||
[InlineData((byte)0x1C)]
|
||||
[InlineData((byte)0x20)]
|
||||
[InlineData((byte)0x00)]
|
||||
public async Task A_wire_Unsupported_reply_is_an_answered_refusal(byte general)
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var server = general == 0 ? StsServer(0x10) : CipEnvelopeServer(general);
|
||||
var driver = await DriverAsync(server);
|
||||
var before = driver.GetHealth().LastSuccessfulRead;
|
||||
|
||||
var result = (await driver.ReadAsync(["T"], ct)).Single();
|
||||
|
||||
result.StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
||||
var health = driver.GetHealth();
|
||||
health.State.ShouldBe(DriverState.Healthy);
|
||||
health.LastSuccessfulRead.ShouldNotBeNull();
|
||||
health.LastSuccessfulRead.ShouldNotBe(before);
|
||||
await driver.ShutdownAsync(ct);
|
||||
server.HandlerErrors.ShouldBeEmpty();
|
||||
}
|
||||
#pragma warning restore OTOPCUA0001
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user