From 1affb4b613810c3382e7d93aae1a5b8451f8e4de Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 15:39:01 -0400 Subject: [PATCH] refactor(ablegacy): confine libplctag to its adapter behind AbStatus/AbTransportException --- .../AbLegacyDriver.cs | 12 +-- .../AbLegacyDriverProbe.cs | 63 +++++-------- .../AbLegacyStatusMapper.cs | 55 ++++++------ .../LibplctagLegacyStatusTranslation.cs | 45 ++++++++++ .../LibplctagLegacyTagRuntime.cs | 12 ++- .../AbLegacyArrayTests.cs | 2 +- .../AbLegacyBitRmwTests.cs | 4 +- .../AbLegacyEvictOnFailureTests.cs | 6 +- .../AbLegacyHostAndStatusTests.cs | 90 +++++++++++++------ .../AbLegacyReadWriteTests.cs | 6 +- .../LibplctagLegacyStatusTranslationTests.cs | 36 ++++++++ 11 files changed, 216 insertions(+), 115 deletions(-) create mode 100644 src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs index 1befff77..c4d5d5e6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs @@ -315,14 +315,14 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // Evict the stale handle so the next call re-creates it (mirrors AbCip). EvictRuntime(device, def.Name); results[i] = new DataValueSnapshot(null, - AbLegacyStatusMapper.MapLibplctagStatus(status), null, now); + AbLegacyStatusMapper.MapStatus(status), null, now); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, - $"libplctag status {status} reading {reference}"); + $"transport status {status} reading {reference}"); if (!device.FirstNonZeroStatusLogged) { device.FirstNonZeroStatusLogged = true; _logger.LogWarning( - "AbLegacy non-zero libplctag status. Driver={DriverInstanceId} Device={DeviceHostAddress} Reference={Reference} Status={Status}", + "AbLegacy non-zero transport status. Driver={DriverInstanceId} Device={DeviceHostAddress} Reference={Reference} Status={Status}", _driverInstanceId, def.DeviceHostAddress, reference, status); } continue; @@ -416,7 +416,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover { // Evict the stale handle so the next call re-creates it (mirrors AbCip). EvictRuntime(device, def.Name); - results[i] = new WriteResult(AbLegacyStatusMapper.MapLibplctagStatus(status)); + results[i] = new WriteResult(AbLegacyStatusMapper.MapStatus(status)); } else { @@ -654,7 +654,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover var parentRuntime = await EnsureParentRuntimeAsync(device, parentName, ct).ConfigureAwait(false); await parentRuntime.ReadAsync(ct).ConfigureAwait(false); var readStatus = parentRuntime.GetStatus(); - if (readStatus != 0) return AbLegacyStatusMapper.MapLibplctagStatus(readStatus); + if (readStatus != 0) return AbLegacyStatusMapper.MapStatus(readStatus); // Mask the decoded word to its native width: DecodeValue for a 16-bit Int returns a // sign-extended int, so a word with bit 15 set decodes negative. Masking pins the @@ -673,7 +673,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover var writeStatus = parentRuntime.GetStatus(); return writeStatus == 0 ? AbLegacyStatusMapper.Good - : AbLegacyStatusMapper.MapLibplctagStatus(writeStatus); + : AbLegacyStatusMapper.MapStatus(writeStatus); } finally { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs index fadeab3a..62bfe1fa 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using System.Net.Sockets; using System.Text.Json; using System.Text.Json.Serialization; -using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; @@ -18,23 +18,25 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// non-PCCC server) returns Ok = false with a "handshake failed" message instead of /// a false-positive green tick. /// -/// Status-code discrimination: libplctag statuses that indicate the controller answered +/// Status-code discrimination: transport statuses that indicate the controller answered /// the PCCC session but could not resolve the probe tag -/// (, , -/// ) are treated as "reachable — Ok = true" because +/// () are treated as "reachable — Ok = true" because /// a non-existent tag name proves PCCC connectivity. Statuses that indicate a transport or -/// session failure (, , -/// , , -/// , , -/// , , -/// , , -/// , ) are treated as +/// session failure (, , +/// , , ) are treated as /// "handshake failed — Ok = false". All other error statuses default to /// "handshake failed" (conservative). /// /// public sealed class AbLegacyDriverProbe : IDriverProbe { + private readonly IAbLegacyTagFactory _tagFactory; + + /// Initializes a new instance of the class. + /// The tag factory used for the PCCC handshake; defaults to the libplctag-backed one. + public AbLegacyDriverProbe(IAbLegacyTagFactory? tagFactory = null) => + _tagFactory = tagFactory ?? new LibplctagLegacyTagFactory(); + private static readonly JsonSerializerOptions _opts = new() { PropertyNameCaseInsensitive = true, @@ -104,7 +106,7 @@ public sealed class AbLegacyDriverProbe : IDriverProbe TagName: tagName, Timeout: timeout); - var rt = new LibplctagLegacyTagRuntime(p); + var rt = _tagFactory.Create(p); try { await rt.InitializeAsync(cts.Token); @@ -112,10 +114,10 @@ public sealed class AbLegacyDriverProbe : IDriverProbe // InitializeAsync completed without throwing — either the tag was found (Ok) or // libplctag returned a non-exception status. Check via GetStatus(). - var statusCode = (Status)rt.GetStatus(); + var statusCode = (AbStatus)rt.GetStatus(); if (IsReachableStatus(statusCode)) { - var msg = statusCode == Status.Ok + var msg = statusCode == AbStatus.Ok ? "PCCC session OK" : "PCCC session OK (controller reachable; probe tag not found)"; return new(true, msg, sw.Elapsed); @@ -128,10 +130,10 @@ public sealed class AbLegacyDriverProbe : IDriverProbe { return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null); } - catch (LibPlcTagException ex) + catch (AbTransportException ex) { - // LibPlcTagException carries the Status; classify as reachable vs transport failure. - if (IsReachableException(ex)) + // AbTransportException carries the Status; classify as reachable vs transport failure. + if (IsReachableStatus(ex.Status)) { sw.Stop(); return new(true, "PCCC session OK (controller reachable; probe tag not found)", sw.Elapsed); @@ -150,30 +152,13 @@ public sealed class AbLegacyDriverProbe : IDriverProbe } /// - /// Returns true for libplctag statuses that indicate the PCCC controller answered - /// but the probe tag name was not found or did not match — sufficient to confirm PCCC - /// reachability. Returns true for (tag was found). + /// Returns true for statuses that indicate the PCCC controller answered but the + /// probe tag was not found — sufficient to confirm PCCC reachability. Also true for + /// (tag was found). /// - private static bool IsReachableStatus(Status s) => s is - Status.Ok or - Status.ErrorNotFound or - Status.ErrorNoMatch or - Status.ErrorBadDevice; - - /// - /// Inspects a message to decide whether the error is a - /// "controller answered PCCC but tag not found" condition (reachable) versus a session / - /// transport failure (unreachable). Falls back to checking the exception message for - /// known tag-not-found phrases used by the libplctag native library. - /// - private static bool IsReachableException(LibPlcTagException ex) - { - // libplctag wraps the Status in the message as "STATUS_NOT_FOUND", "STATUS_NO_MATCH", etc. - var msg = ex.Message ?? string.Empty; - return msg.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase) - || msg.Contains("NO_MATCH", StringComparison.OrdinalIgnoreCase) - || msg.Contains("BAD_DEVICE", StringComparison.OrdinalIgnoreCase); - } + /// The transport status. + /// Whether the status proves the controller answered. + internal static bool IsReachableStatus(AbStatus s) => s is AbStatus.Ok or AbStatus.NotFound; private static (AbLegacyHostAddress? parsed, AbLegacyDeviceOptions? device) ExtractTarget(AbLegacyDriverOptions opts) { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs index 9402a82e..d5746476 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs @@ -1,9 +1,9 @@ -using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// -/// Maps libplctag status codes + PCCC STS/EXT_STS bytes to OPC UA StatusCodes. Mirrors the +/// Maps transport status codes + PCCC STS/EXT_STS bytes to OPC UA StatusCodes. Mirrors the /// AbCip mapper — PCCC errors roughly align with CIP general-status in shape but with a /// different byte vocabulary (PCCC STS nibble-low + EXT_STS on code 0x0F). /// @@ -22,44 +22,41 @@ public static class AbLegacyStatusMapper public const uint BadTypeMismatch = 0x80740000u; /// - /// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here - /// is (int)Tag.GetStatus() — the underlying value of the libplctag.NET - /// enum. Delegates to the strongly-typed overload so the mapping - /// stays correct regardless of how the wrapper renumbers native PLCTAG_ERR_* constants - /// in future releases. + /// Map the integer form of an (what + /// returns) to an OPC UA StatusCode. /// - /// The integer status code from libplctag. + /// The integer transport status. /// The corresponding OPC UA status code. - public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status); + public static uint MapStatus(int status) => MapStatus((AbStatus)status); - /// - /// Map a libplctag.NET enum value to an OPC UA StatusCode. This is - /// the canonical core; the int overload exists only for the - /// seam which boxes the enum as an int. - /// - /// The libplctag Status enum value. + /// Map a transport-neutral to an OPC UA StatusCode. + /// The transport status. /// The corresponding OPC UA status code. - public static uint MapLibplctagStatus(Status status) => status switch + public static uint MapStatus(AbStatus status) => status switch { - Status.Ok => Good, - Status.Pending => GoodMoreData, - Status.ErrorTimeout => BadTimeout, - Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => BadNodeIdUnknown, - Status.ErrorNotAllowed => BadNotWritable, - Status.ErrorOutOfBounds or Status.ErrorTooLarge or Status.ErrorTooSmall => BadOutOfRange, - Status.ErrorUnsupported or Status.ErrorNotImplemented => BadNotSupported, - Status.ErrorBadConnection or Status.ErrorBadGateway or Status.ErrorBadReply - or Status.ErrorWinsock or Status.ErrorOpen or Status.ErrorClose - or Status.ErrorRead or Status.ErrorWrite or Status.ErrorRemoteErr - or Status.ErrorPartial or Status.ErrorAbort => BadCommunicationError, - _ => BadCommunicationError, + AbStatus.Ok => Good, + AbStatus.Pending => GoodMoreData, + AbStatus.Timeout => BadTimeout, + AbStatus.NotFound => BadNodeIdUnknown, + AbStatus.NotAllowed => BadNotWritable, + AbStatus.OutOfBounds or AbStatus.TooLarge or AbStatus.TooSmall => BadOutOfRange, + AbStatus.Unsupported => BadNotSupported, + AbStatus.TypeMismatch => BadTypeMismatch, + AbStatus.DeviceStateConflict => BadDeviceFailure, + _ => BadCommunicationError, // BadConnection, BadReply, Partial, Abort }; + /// Map an exception thrown by a transport to an OPC UA StatusCode. + /// The exception. + /// The mapped status for an , else BadCommunicationError. + public static uint MapException(Exception ex) => + ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError; + /// /// Map a PCCC STS (status) byte. Common codes per AB PCCC reference: /// 0x00 = success, 0x10 = illegal command, 0x20 = bad address, 0x30 = protected, /// 0x40 = programmer busy, 0x50 = file locked, 0xF0 = extended status follows. - /// libplctag surfaces only its own enum rather than exposing + /// the transports surface only an rather than exposing /// the raw STS byte, so this method is not wired into the current read/write path. /// It is retained as the reference mapping for future PCCC-STS inspection. /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs new file mode 100644 index 00000000..3d762e84 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs @@ -0,0 +1,45 @@ +using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; + +/// The ONLY place libplctag's meets in AbLegacy. +internal static class LibplctagLegacyStatusTranslation +{ + /// Translates a libplctag status into the transport-neutral . + /// The libplctag status. + /// The corresponding . + public static AbStatus ToAbStatus(Status s) => s switch + { + Status.Ok => AbStatus.Ok, + Status.Pending => AbStatus.Pending, + Status.ErrorTimeout => AbStatus.Timeout, + Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => AbStatus.NotFound, + Status.ErrorNotAllowed => AbStatus.NotAllowed, + Status.ErrorOutOfBounds => AbStatus.OutOfBounds, + Status.ErrorTooLarge => AbStatus.TooLarge, + Status.ErrorTooSmall => AbStatus.TooSmall, + Status.ErrorUnsupported or Status.ErrorNotImplemented => AbStatus.Unsupported, + Status.ErrorPartial => AbStatus.Partial, + Status.ErrorAbort => AbStatus.Abort, + Status.ErrorBadReply => AbStatus.BadReply, + _ => AbStatus.BadConnection, + }; + + /// libplctag builds the exception from Status.ToString(), so the message is the enum name. + /// The libplctag exception. + /// The corresponding . + public static AbStatus ToAbStatus(LibPlcTagException ex) => + Enum.TryParse((ex.Message ?? "").Trim(), ignoreCase: true, out var s) && Enum.IsDefined(s) + ? ToAbStatus(s) + : AbStatus.BadConnection; + + /// Runs , rethrowing libplctag failures as . + /// The libplctag operation. + /// A task completing with the operation. + public static async Task Wrap(Func op) + { + try { await op().ConfigureAwait(false); } + catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); } + } +} diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs index fb71ce32..c15605f6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs @@ -1,4 +1,5 @@ using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; @@ -31,16 +32,19 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime } /// - public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken); + public Task InitializeAsync(CancellationToken cancellationToken) => + LibplctagLegacyStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken)); /// - public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken); + public Task ReadAsync(CancellationToken cancellationToken) => + LibplctagLegacyStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken)); /// - public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken); + public Task WriteAsync(CancellationToken cancellationToken) => + LibplctagLegacyStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken)); /// - public int GetStatus() => (int)_tag.GetStatus(); + public int GetStatus() => (int)LibplctagLegacyStatusTranslation.ToAbStatus(_tag.GetStatus()); /// public object? DecodeValue(AbLegacyDataType type, int? bitIndex) => type switch diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs index b01bfbee..9c5e428d 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs @@ -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); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs index 53824c40..3229fb53 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs @@ -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)); } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs index 428c1144..aebeb1a1 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs @@ -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); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs index 1af52555..07c5abdb 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs @@ -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); } - /// Verifies that libplctag Status enum members are mapped to OPC UA status codes correctly. - /// The libplctag Status enum value to map. + /// Verifies that AbStatus members map to OPC UA status codes correctly. + /// The AbStatus value to map. /// The expected OPC UA status code. - // 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); + } + + /// MapException reads the status off an AbTransportException, else falls back to comms error. + [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); + } + + /// + /// 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 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); } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs index 45d36552..e047a267 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs @@ -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); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs new file mode 100644 index 00000000..9d396696 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs @@ -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); + } +}