Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.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

97 lines
4.9 KiB
C#

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
{
/// <summary>Verifies that transport status values are mapped to OPC UA status codes.</summary>
/// <param name="status">The transport status to map.</param>
/// <param name="expected">The expected OPC UA status code.</param>
[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);
}
/// <summary>Verifies that timeout is distinguished from generic communication error.</summary>
[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);
}
/// <summary>Verifies MapException maps transport exceptions by status and everything else to comms error.</summary>
[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);
}
/// <summary>
/// Regression oracle: the OPC UA code the pre-AbStatus <c>MapLibplctagStatus</c> returned for every
/// libplctag member (table copied from the deleted switch) must equal
/// <c>MapStatus(ToAbStatus(member))</c>.
/// </summary>
/// <param name="status">A libplctag status.</param>
[Theory]
[MemberData(nameof(AllLibplctagStatuses))]
public void Translation_plus_MapStatus_matches_the_old_libplctag_mapping(Status status)
{
AbCipStatusMapper.MapStatus(LibplctagStatusTranslation.ToAbStatus(status)).ShouldBe(OldMapLibplctagStatus(status));
}
/// <summary>Every defined libplctag <see cref="Status"/> member.</summary>
/// <returns>One row per member.</returns>
public static IEnumerable<object[]> AllLibplctagStatuses() =>
Enum.GetValues<Status>().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/...
};
/// <summary>A decode past the buffer end surfaces as BadOutOfRange once the adapter wraps it.</summary>
[Fact]
public void Wrapped_decode_OutOfBounds_maps_to_BadOutOfRange()
{
var ex = Should.Throw<AbTransportException>(() =>
LibplctagStatusTranslation.WrapSync<int>(() => throw new LibPlcTagException("ErrorOutOfBounds")));
AbCipStatusMapper.MapException(ex).ShouldBe(AbCipStatusMapper.BadOutOfRange);
}
}