using libplctag; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class AbCipStatusMapperTests { /// Verifies that CIP general status codes are mapped to OPC UA status codes. /// The raw CIP general status byte. /// The expected OPC UA status code. [Theory] [InlineData((byte)0x00, AbCipStatusMapper.Good)] [InlineData((byte)0x04, AbCipStatusMapper.BadNodeIdUnknown)] [InlineData((byte)0x05, AbCipStatusMapper.BadNodeIdUnknown)] [InlineData((byte)0x06, AbCipStatusMapper.GoodMoreData)] [InlineData((byte)0x08, AbCipStatusMapper.BadNotSupported)] [InlineData((byte)0x0A, AbCipStatusMapper.BadOutOfRange)] [InlineData((byte)0x13, AbCipStatusMapper.BadOutOfRange)] [InlineData((byte)0x0B, AbCipStatusMapper.Good)] [InlineData((byte)0x0E, AbCipStatusMapper.BadNotWritable)] [InlineData((byte)0x10, AbCipStatusMapper.BadDeviceFailure)] [InlineData((byte)0x16, AbCipStatusMapper.BadNodeIdUnknown)] [InlineData((byte)0xFF, AbCipStatusMapper.BadInternalError)] public void MapCipGeneralStatus_maps_known_codes(byte status, uint expected) { AbCipStatusMapper.MapCipGeneralStatus(status).ShouldBe(expected); } /// Verifies that transport status values are mapped to OPC UA status codes. /// The transport status to map. /// The expected OPC UA status code. [Theory] [InlineData(AbStatus.Ok, AbCipStatusMapper.Good)] [InlineData(AbStatus.Pending, AbCipStatusMapper.GoodMoreData)] [InlineData(AbStatus.Timeout, AbCipStatusMapper.BadTimeout)] [InlineData(AbStatus.NotFound, AbCipStatusMapper.BadNodeIdUnknown)] [InlineData(AbStatus.NotAllowed, AbCipStatusMapper.BadNotWritable)] [InlineData(AbStatus.OutOfBounds, AbCipStatusMapper.BadOutOfRange)] [InlineData(AbStatus.TooLarge, AbCipStatusMapper.BadOutOfRange)] [InlineData(AbStatus.TooSmall, AbCipStatusMapper.BadOutOfRange)] [InlineData(AbStatus.Unsupported, AbCipStatusMapper.BadNotSupported)] [InlineData(AbStatus.TypeMismatch, AbCipStatusMapper.BadTypeMismatch)] [InlineData(AbStatus.DeviceStateConflict, AbCipStatusMapper.BadDeviceFailure)] [InlineData(AbStatus.BadConnection, AbCipStatusMapper.BadCommunicationError)] [InlineData(AbStatus.BadReply, AbCipStatusMapper.BadCommunicationError)] [InlineData(AbStatus.Partial, AbCipStatusMapper.BadCommunicationError)] [InlineData(AbStatus.Abort, AbCipStatusMapper.BadCommunicationError)] public void MapStatus_maps_every_AbStatus(AbStatus status, uint expected) { AbCipStatusMapper.MapStatus(status).ShouldBe(expected); // The int overload must agree - it is the seam IAbCipTagRuntime.GetStatus() drives. AbCipStatusMapper.MapStatus((int)status).ShouldBe(expected); } /// Verifies that timeout is distinguished from generic communication error. [Fact] public void MapStatus_distinguishes_timeout_from_generic_comms_error() { AbCipStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbCipStatusMapper.BadTimeout); AbCipStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbCipStatusMapper.BadNodeIdUnknown); AbCipStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldNotBe(AbCipStatusMapper.BadCommunicationError); } /// Verifies MapException maps transport exceptions by status and everything else to comms error. [Fact] public void MapException_uses_the_transport_status() { AbCipStatusMapper.MapException(new AbTransportException(AbStatus.NotFound, "x")) .ShouldBe(AbCipStatusMapper.BadNodeIdUnknown); AbCipStatusMapper.MapException(new InvalidOperationException("x")) .ShouldBe(AbCipStatusMapper.BadCommunicationError); } /// /// Regression oracle: the OPC UA code the pre-AbStatus MapLibplctagStatus returned for every /// libplctag member (table copied from the deleted switch) must equal /// MapStatus(ToAbStatus(member)). /// /// A libplctag status. [Theory] [MemberData(nameof(AllLibplctagStatuses))] public void Translation_plus_MapStatus_matches_the_old_libplctag_mapping(Status status) { AbCipStatusMapper.MapStatus(LibplctagStatusTranslation.ToAbStatus(status)).ShouldBe(OldMapLibplctagStatus(status)); } /// Every defined libplctag member. /// One row per member. public static IEnumerable AllLibplctagStatuses() => Enum.GetValues().Select(s => new object[] { s }); private static uint OldMapLibplctagStatus(Status status) => status switch { Status.Ok => AbCipStatusMapper.Good, Status.Pending => AbCipStatusMapper.GoodMoreData, Status.ErrorTimeout => AbCipStatusMapper.BadTimeout, Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => AbCipStatusMapper.BadNodeIdUnknown, Status.ErrorNotAllowed => AbCipStatusMapper.BadNotWritable, Status.ErrorOutOfBounds or Status.ErrorTooLarge or Status.ErrorTooSmall => AbCipStatusMapper.BadOutOfRange, Status.ErrorUnsupported or Status.ErrorNotImplemented => AbCipStatusMapper.BadNotSupported, _ => AbCipStatusMapper.BadCommunicationError, // incl. BadConnection/BadGateway/BadReply/Partial/Abort/... }; /// A decode past the buffer end surfaces as BadOutOfRange once the adapter wraps it. [Fact] public void Wrapped_decode_OutOfBounds_maps_to_BadOutOfRange() { var ex = Should.Throw(() => LibplctagStatusTranslation.WrapSync(() => throw new LibPlcTagException("ErrorOutOfBounds"))); AbCipStatusMapper.MapException(ex).ShouldBe(AbCipStatusMapper.BadOutOfRange); } }