using libplctag; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; [Trait("Category", "Unit")] public sealed class AbLegacyHostAndStatusTests { /// Verifies that HostAddress parses valid URI strings correctly. /// The raw URI string to parse. /// The expected gateway host. /// The expected port number. /// The expected CIP path component. [Theory] [InlineData("ab://10.0.0.5/1,0", "10.0.0.5", 44818, "1,0")] [InlineData("ab://10.0.0.5/", "10.0.0.5", 44818, "")] [InlineData("ab://10.0.0.5:2222/1,0", "10.0.0.5", 2222, "1,0")] [InlineData("ab://plc-slc.factory/1,2", "plc-slc.factory", 44818, "1,2")] public void HostAddress_parses_valid(string input, string gateway, int port, string path) { var parsed = AbLegacyHostAddress.TryParse(input); parsed.ShouldNotBeNull(); parsed.Gateway.ShouldBe(gateway); parsed.Port.ShouldBe(port); parsed.CipPath.ShouldBe(path); } /// Verifies that HostAddress rejects invalid URI strings. /// The invalid or null URI string to test. [Theory] [InlineData(null)] [InlineData("http://10.0.0.5/1,0")] [InlineData("ab://10.0.0.5")] [InlineData("ab:///1,0")] [InlineData("ab://10.0.0.5:0/1,0")] public void HostAddress_rejects_invalid(string? input) { AbLegacyHostAddress.TryParse(input).ShouldBeNull(); } /// Verifies that HostAddress.ToString produces canonical URI format. [Fact] public void HostAddress_ToString_canonicalises() { new AbLegacyHostAddress("10.0.0.5", 44818, "1,0").ToString().ShouldBe("ab://10.0.0.5/1,0"); new AbLegacyHostAddress("10.0.0.5", 2222, "1,0").ToString().ShouldBe("ab://10.0.0.5:2222/1,0"); } /// Verifies that AbStatus members map to OPC UA status codes correctly. /// The AbStatus value to map. /// The expected OPC UA status code. [Theory] [InlineData(AbStatus.Ok, AbLegacyStatusMapper.Good)] [InlineData(AbStatus.Pending, AbLegacyStatusMapper.GoodMoreData)] [InlineData(AbStatus.Timeout, AbLegacyStatusMapper.BadTimeout)] [InlineData(AbStatus.NotFound, AbLegacyStatusMapper.BadNodeIdUnknown)] [InlineData(AbStatus.NotAllowed, AbLegacyStatusMapper.BadNotWritable)] [InlineData(AbStatus.OutOfBounds, AbLegacyStatusMapper.BadOutOfRange)] [InlineData(AbStatus.TooLarge, AbLegacyStatusMapper.BadOutOfRange)] [InlineData(AbStatus.TooSmall, AbLegacyStatusMapper.BadOutOfRange)] [InlineData(AbStatus.Unsupported, AbLegacyStatusMapper.BadNotSupported)] [InlineData(AbStatus.TypeMismatch, AbLegacyStatusMapper.BadTypeMismatch)] [InlineData(AbStatus.DeviceStateConflict, AbLegacyStatusMapper.BadDeviceFailure)] [InlineData(AbStatus.BadConnection, AbLegacyStatusMapper.BadCommunicationError)] [InlineData(AbStatus.BadReply, AbLegacyStatusMapper.BadCommunicationError)] [InlineData(AbStatus.Partial, AbLegacyStatusMapper.BadCommunicationError)] [InlineData(AbStatus.Abort, AbLegacyStatusMapper.BadCommunicationError)] public void AbStatus_maps_to_opcua_codes(AbStatus status, uint expected) { AbLegacyStatusMapper.MapStatus(status).ShouldBe(expected); // The int overload must agree — it is the seam IAbLegacyTagRuntime.GetStatus() drives. AbLegacyStatusMapper.MapStatus((int)status).ShouldBe(expected); } /// /// The regression oracle: the OPC UA code the pre-seam MapLibplctagStatus(Status) returned, /// copied verbatim from the deleted switch. For every libplctag member the new /// translation + must reproduce it. /// private static uint OldMapLibplctagStatus(Status status) => status switch { Status.Ok => AbLegacyStatusMapper.Good, Status.Pending => AbLegacyStatusMapper.GoodMoreData, Status.ErrorTimeout => AbLegacyStatusMapper.BadTimeout, Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => AbLegacyStatusMapper.BadNodeIdUnknown, Status.ErrorNotAllowed => AbLegacyStatusMapper.BadNotWritable, Status.ErrorOutOfBounds or Status.ErrorTooLarge or Status.ErrorTooSmall => AbLegacyStatusMapper.BadOutOfRange, Status.ErrorUnsupported or Status.ErrorNotImplemented => AbLegacyStatusMapper.BadNotSupported, _ => AbLegacyStatusMapper.BadCommunicationError, }; /// Every libplctag Status member maps to the same OPC UA code as before the seam. [Fact] public void Translation_plus_mapper_matches_the_old_libplctag_switch_for_every_member() { foreach (var s in Enum.GetValues()) { AbLegacyStatusMapper.MapStatus(LibplctagLegacyStatusTranslation.ToAbStatus(s)) .ShouldBe(OldMapLibplctagStatus(s), $"libplctag Status.{s}"); } } /// Verifies that timeout errors are distinguished from generic communication errors. [Fact] public void MapStatus_distinguishes_timeout_from_generic_comms_error() { AbLegacyStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbLegacyStatusMapper.BadTimeout); AbLegacyStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown); } }