Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs
T
Joseph Doherty 341417f193 fix(ab): share status map + libplctag orphan release, harden confinement guard, pin width-mismatch + DefaultTransport wiring (#568 #569 #580 #585)
- 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
2026-10-07 22:59:06 -04:00

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);
}
}