merge: Task 3 — AbLegacy seam decoupled from libplctag types

This commit is contained in:
Joseph Doherty
2026-10-07 15:46:31 -04:00
11 changed files with 216 additions and 115 deletions
@@ -112,7 +112,7 @@ public sealed class AbLegacyArrayTests
var (drv, factory) = NewDriver(
new AbLegacyTagDefinition("Levels", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 5));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)libplctag.Status.ErrorTimeout };
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbStatus.Timeout };
var snapshots = await drv.ReadAsync(["Levels"], CancellationToken.None);
@@ -229,7 +229,7 @@ public sealed class AbLegacyBitRmwTests
public async Task Bit_write_surfaces_device_rejection_status()
{
// Arrange: the parent tag reads OK (Status = 0) but write returns a timeout error.
const int errorTimeout = (int)libplctag.Status.ErrorTimeout;
const int errorTimeout = (int)ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbStatus.Timeout;
var factory = new FakeAbLegacyTagFactory
{
Customise = p => new FakeAbLegacyTag(p)
@@ -252,6 +252,6 @@ public sealed class AbLegacyBitRmwTests
// Assert: the write rejection must NOT be silently swallowed as Good.
var statusCode = results.Single().StatusCode;
statusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
statusCode.ShouldBe(AbLegacyStatusMapper.MapLibplctagStatus(errorTimeout));
statusCode.ShouldBe(AbLegacyStatusMapper.MapStatus(errorTimeout));
}
}
@@ -1,4 +1,4 @@
using libplctag;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
@@ -42,7 +42,7 @@ public sealed class AbLegacyEvictOnFailureTests
{
callCount++;
return callCount == 1
? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
? new FakeAbLegacyTag(p) { Status = (int)AbStatus.BadConnection }
: new FakeAbLegacyTag(p) { Status = 0, Value = 42 };
};
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -94,7 +94,7 @@ public sealed class AbLegacyEvictOnFailureTests
{
callCount++;
return callCount == 1
? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
? new FakeAbLegacyTag(p) { Status = (int)AbStatus.BadConnection }
: new FakeAbLegacyTag(p) { Status = 0 };
};
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -1,6 +1,7 @@
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;
@@ -65,42 +66,75 @@ public sealed class AbLegacyHostAndStatusTests
AbLegacyStatusMapper.MapPcccStatus(sts).ShouldBe(expected);
}
/// <summary>Verifies that libplctag Status enum members are mapped to OPC UA status codes correctly.</summary>
/// <param name="status">The libplctag Status enum value to map.</param>
/// <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>
// Driver.AbLegacy-010 — tests use the libplctag.NET Status enum members (what
// (int)Tag.GetStatus() actually returns) rather than the unverified magic integers
// that predated this fix (-5/-7/-14/-16/-17 matched neither native PLCTAG_ERR_*
// constants nor the .NET wrapper enum ordinals reliably).
[Theory]
[InlineData(Status.Ok, AbLegacyStatusMapper.Good)]
[InlineData(Status.Pending, AbLegacyStatusMapper.GoodMoreData)]
[InlineData(Status.ErrorTimeout, AbLegacyStatusMapper.BadTimeout)]
[InlineData(Status.ErrorNotFound, AbLegacyStatusMapper.BadNodeIdUnknown)]
[InlineData(Status.ErrorNoMatch, AbLegacyStatusMapper.BadNodeIdUnknown)]
[InlineData(Status.ErrorNotAllowed, AbLegacyStatusMapper.BadNotWritable)]
[InlineData(Status.ErrorOutOfBounds, AbLegacyStatusMapper.BadOutOfRange)]
[InlineData(Status.ErrorTooLarge, AbLegacyStatusMapper.BadOutOfRange)]
[InlineData(Status.ErrorBadConnection, AbLegacyStatusMapper.BadCommunicationError)]
[InlineData(Status.ErrorBadGateway, AbLegacyStatusMapper.BadCommunicationError)]
[InlineData(Status.ErrorUnsupported, AbLegacyStatusMapper.BadNotSupported)]
[InlineData(Status.ErrorNoMem, AbLegacyStatusMapper.BadCommunicationError)] // unmapped → generic comms
public void LibplctagStatus_maps_real_enum_members(Status status, uint expected)
[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.MapLibplctagStatus(status).ShouldBe(expected);
AbLegacyStatusMapper.MapStatus(status).ShouldBe(expected);
// The int overload must agree — it is the seam IAbLegacyTagRuntime.GetStatus() drives.
AbLegacyStatusMapper.MapLibplctagStatus((int)status).ShouldBe(expected);
AbLegacyStatusMapper.MapStatus((int)status).ShouldBe(expected);
}
/// <summary>MapException reads the status off an AbTransportException, else falls back to comms error.</summary>
[Fact]
public void MapException_uses_transport_status()
{
AbLegacyStatusMapper.MapException(new AbTransportException(AbStatus.Timeout, "t"))
.ShouldBe(AbLegacyStatusMapper.BadTimeout);
AbLegacyStatusMapper.MapException(new InvalidOperationException("x"))
.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
}
/// <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 MapLibplctagStatus_distinguishes_timeout_from_generic_comms_error()
public void MapStatus_distinguishes_timeout_from_generic_comms_error()
{
// Regression for Driver.AbLegacy-010: timeout must not fall through to
// BadCommunicationError the way the old magic-integer switch did.
AbLegacyStatusMapper.MapLibplctagStatus((int)Status.ErrorTimeout)
.ShouldBe(AbLegacyStatusMapper.BadTimeout);
AbLegacyStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound)
.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
AbLegacyStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbLegacyStatusMapper.BadTimeout);
AbLegacyStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
}
}
@@ -1,4 +1,4 @@
using libplctag;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
@@ -70,12 +70,12 @@ public sealed class AbLegacyReadWriteTests
[Fact]
public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
{
// Use the real libplctag.Status enum value rather than a raw integer so the test
// Use the real AbStatus enum value rather than a raw integer so the test
// stays correct if the wrapper renumbers its ordinals (Driver.AbLegacy-010).
var (drv, factory) = NewDriver(
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)Status.ErrorNotFound };
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)AbStatus.NotFound };
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
@@ -0,0 +1,36 @@
using libplctag;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
public sealed class LibplctagLegacyStatusTranslationTests
{
[Theory]
[InlineData(Status.Ok, AbStatus.Ok)]
[InlineData(Status.Pending, AbStatus.Pending)]
[InlineData(Status.ErrorTimeout, AbStatus.Timeout)]
[InlineData(Status.ErrorNotFound, AbStatus.NotFound)]
[InlineData(Status.ErrorNoMatch, AbStatus.NotFound)]
[InlineData(Status.ErrorBadDevice, AbStatus.NotFound)]
[InlineData(Status.ErrorNotAllowed, AbStatus.NotAllowed)]
[InlineData(Status.ErrorOutOfBounds, AbStatus.OutOfBounds)]
[InlineData(Status.ErrorTooLarge, AbStatus.TooLarge)]
[InlineData(Status.ErrorTooSmall, AbStatus.TooSmall)]
[InlineData(Status.ErrorUnsupported, AbStatus.Unsupported)]
[InlineData(Status.ErrorNotImplemented, AbStatus.Unsupported)]
[InlineData(Status.ErrorPartial, AbStatus.Partial)]
[InlineData(Status.ErrorAbort, AbStatus.Abort)]
[InlineData(Status.ErrorBadReply, AbStatus.BadReply)]
[InlineData(Status.ErrorBadGateway, AbStatus.BadConnection)]
[InlineData(Status.ErrorWinsock, AbStatus.BadConnection)]
public void Translates(Status from, AbStatus to) => LibplctagLegacyStatusTranslation.ToAbStatus(from).ShouldBe(to);
[Fact]
public void Exception_message_is_recovered_as_the_enum_name()
{
LibplctagLegacyStatusTranslation.ToAbStatus(new LibPlcTagException("ErrorNotFound")).ShouldBe(AbStatus.NotFound);
LibplctagLegacyStatusTranslation.ToAbStatus(new LibPlcTagException("gibberish")).ShouldBe(AbStatus.BadConnection);
}
}