dfc62cef14
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
89 lines
4.8 KiB
C#
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);
|
|
}
|
|
}
|