Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/AbStatusCodesTests.cs
T
Joseph Doherty dfc62cef14 fix(ab-transport): map CIP 0x1C/0x20/0xFF-0x2104 as refusals; remap the PCCC STS table
K-F1 (AbCip #737 F1): FromCip maps general 0x1C and 0x20 to Unsupported and
0xFF/0x2104 to OutOfBounds, so a controller refusal is per-tag Bad instead of a
BadReply comms fault. A Forward Open refused with one of them is still
BadConnection. Every CIP code is now listed with its meaning; 0x01/0x07 stay
opaque on purpose.

K-F3 (AbLegacy F8, review 07 r0 I4): FromPccc follows the DF1 STS table:
0x20/0x30/0x90 -> BadConnection (a bridge reporting an unreachable END node),
0x50 and EXT 0x06 -> NotFound, EXT 0x07 -> OutOfBounds, 0x60 -> NotAllowed,
0x70/0xB0 -> DeviceStateConflict. Golden ParseReply tests per STS, and an
end-to-end AbLegacy test: STS 0x30 degrades with LastSuccessfulRead preserved;
the 0x60 positive control stays Healthy and advances it. Both were red before.
Driver edits are doc remarks and the two runtime tests the old table pinned.

Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 16:43:37 -04:00

89 lines
4.8 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests;
public sealed class AbStatusCodesTests
{
[Theory]
[InlineData((byte)0x00, AbStatus.Ok)]
[InlineData((byte)0x04, AbStatus.NotFound)]
[InlineData((byte)0x05, AbStatus.NotFound)]
[InlineData((byte)0x16, AbStatus.NotFound)]
[InlineData((byte)0x06, AbStatus.Partial)]
[InlineData((byte)0x08, AbStatus.Unsupported)]
[InlineData((byte)0x0F, AbStatus.NotAllowed)]
[InlineData((byte)0x10, AbStatus.DeviceStateConflict)]
[InlineData((byte)0x13, AbStatus.TooSmall)]
[InlineData((byte)0x15, AbStatus.TooLarge)]
[InlineData((byte)0x0B, AbStatus.Ok)]
[InlineData((byte)0x0A, AbStatus.OutOfBounds)]
[InlineData((byte)0x0C, AbStatus.DeviceStateConflict)]
[InlineData((byte)0x0E, AbStatus.NotAllowed)]
[InlineData((byte)0x1E, AbStatus.BadReply)]
[InlineData((byte)0x01, AbStatus.BadReply)]
[InlineData((byte)0x07, AbStatus.BadReply)] // connection lost: opaque on purpose (AbLegacy #738)
// Lane K F1 (#737): the controller answered and refused this request — not a garbled reply.
[InlineData((byte)0x1C, AbStatus.Unsupported)] // missing attribute list entry data
[InlineData((byte)0x20, AbStatus.Unsupported)] // invalid parameter
public void Cip_general_status_maps(byte general, AbStatus expected) =>
AbStatusCodes.FromCip(general, ReadOnlySpan<ushort>.Empty).ShouldBe(expected);
[Theory]
[InlineData((ushort)0x2104, AbStatus.OutOfBounds)] // offset beyond end of the tag (Lane K F1)
[InlineData((ushort)0x2105, AbStatus.OutOfBounds)]
[InlineData((ushort)0x2107, AbStatus.TypeMismatch)]
[InlineData((ushort)0x9999, AbStatus.BadReply)]
public void General_status_FF_maps_by_extended_status(ushort ext, AbStatus expected) =>
AbStatusCodes.FromCip(0xFF, [ext]).ShouldBe(expected);
[Fact]
public void General_status_FF_with_no_extended_status_is_BadReply() =>
AbStatusCodes.FromCip(0xFF, ReadOnlySpan<ushort>.Empty).ShouldBe(AbStatus.BadReply);
[Theory]
[InlineData((byte)0x00, null, AbStatus.Ok)]
[InlineData((byte)0x10, null, AbStatus.Unsupported)] // illegal command or format
// Lane K F3 (AbLegacy F8, review 07 r0 I4), per the DF1/PCCC STS table (1770-6.5.16):
[InlineData((byte)0x20, null, AbStatus.BadConnection)] // host has a problem and will not communicate
[InlineData((byte)0x30, null, AbStatus.BadConnection)] // remote node host missing / disconnected / shut down
[InlineData((byte)0x40, null, AbStatus.DeviceStateConflict)] // host hardware fault (the host answered)
[InlineData((byte)0x50, null, AbStatus.NotFound)] // addressing problem or memory protect rungs
[InlineData((byte)0x60, null, AbStatus.NotAllowed)] // function not allowed due to command protection
[InlineData((byte)0x70, null, AbStatus.DeviceStateConflict)] // processor is in Program mode
[InlineData((byte)0x80, null, AbStatus.BadReply)] // compatibility-mode file missing: unclassified
[InlineData((byte)0x90, null, AbStatus.BadConnection)] // remote node cannot buffer command
[InlineData((byte)0xB0, null, AbStatus.DeviceStateConflict)] // remote node problem due to download
[InlineData((byte)0xF0, (byte)0x06, AbStatus.NotFound)] // address does not point to something usable
[InlineData((byte)0xF0, (byte)0x07, AbStatus.OutOfBounds)] // file is wrong size
[InlineData((byte)0xF0, (byte)0x0A, AbStatus.BadReply)]
[InlineData((byte)0xF0, null, AbStatus.BadReply)]
public void Pccc_status_maps(byte sts, byte? ext, AbStatus expected) =>
AbStatusCodes.FromPccc(sts, ext).ShouldBe(expected);
/// <summary>
/// A dead DH+/DH-485 node behind a bridge answers STS 0x30 (and a busy one 0x90). Neither proves the END
/// device answered, so they are the no-reply comms class, never one of the "answered" refusals — that
/// classification made such a device count as reachable forever (no backoff, no comms-loss watchdog).
/// </summary>
/// <param name="sts">The PCCC STS byte.</param>
[Theory]
[InlineData((byte)0x20)]
[InlineData((byte)0x30)]
[InlineData((byte)0x90)]
public void Pccc_no_reply_statuses_are_never_answered_refusals(byte sts)
{
var status = AbStatusCodes.FromPccc(sts, null);
status.ShouldBe(AbStatus.BadConnection);
status.ShouldNotBeOneOf(AbStatus.NotFound, AbStatus.NotAllowed, AbStatus.OutOfBounds,
AbStatus.DeviceStateConflict, AbStatus.TooLarge, AbStatus.TooSmall, AbStatus.TypeMismatch);
}
[Fact]
public void Ok_is_zero_and_every_error_is_negative()
{
((int)AbStatus.Ok).ShouldBe(0);
foreach (var s in Enum.GetValues<AbStatus>().Where(s => s is not AbStatus.Ok and not AbStatus.Pending))
((int)s).ShouldBeLessThan(0);
}
}