341417f193
- Ab.Transport: AbOpcUaStatusMap is the single AbStatus->OPC UA table; AbCip/AbLegacy mappers delegate. - Remove dead MapCipGeneralStatus / MapPcccStatus / AbLegacy MapException (+ tests). - LibplctagOrphanRelease is one source file (Ab.LibplctagShared) compiled into both drivers. - LibplctagConfinementGuardTests: tokenising comment/literal stripper, block comments, csproj PackageReference guard. - LibplctagStatusTranslationTests: real LibPlcTagException round trip, WrapSync/Wrap OCE pass-through, adapter routes decode via WrapSync. - AbCipTypeMismatchTests: libplctag width-mismatch path (BadTypeMismatch, handle kept, no Degraded, warn once). - Host: AddAbCipDriverDefaults extension + Program.cs source pin for Drivers:AbCip:DefaultTransport. - docs/drivers/AbCip.md: layout rows + benign Degraded-after-outage note. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
114 lines
5.6 KiB
C#
114 lines
5.6 KiB
C#
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
|
|
{
|
|
/// <summary>Verifies that HostAddress parses valid URI strings correctly.</summary>
|
|
/// <param name="input">The raw URI string to parse.</param>
|
|
/// <param name="gateway">The expected gateway host.</param>
|
|
/// <param name="port">The expected port number.</param>
|
|
/// <param name="path">The expected CIP path component.</param>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that HostAddress rejects invalid URI strings.</summary>
|
|
/// <param name="input">The invalid or null URI string to test.</param>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>Verifies that HostAddress.ToString produces canonical URI format.</summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>Verifies that AbStatus members map to OPC UA status codes correctly.</summary>
|
|
/// <param name="status">The AbStatus value to map.</param>
|
|
/// <param name="expected">The expected OPC UA status code.</param>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The regression oracle: the OPC UA code the pre-seam <c>MapLibplctagStatus(Status)</c> returned,
|
|
/// copied verbatim from the deleted switch. For every libplctag member the new
|
|
/// translation + <see cref="AbLegacyStatusMapper.MapStatus(AbStatus)"/> must reproduce it.
|
|
/// </summary>
|
|
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,
|
|
};
|
|
|
|
/// <summary>Every libplctag Status member maps to the same OPC UA code as before the seam.</summary>
|
|
[Fact]
|
|
public void Translation_plus_mapper_matches_the_old_libplctag_switch_for_every_member()
|
|
{
|
|
foreach (var s in Enum.GetValues<Status>())
|
|
{
|
|
AbLegacyStatusMapper.MapStatus(LibplctagLegacyStatusTranslation.ToAbStatus(s))
|
|
.ShouldBe(OldMapLibplctagStatus(s), $"libplctag Status.{s}");
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that timeout errors are distinguished from generic communication errors.</summary>
|
|
[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);
|
|
}
|
|
}
|