diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs index 8afb0a38..df97d4a9 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -1,4 +1,4 @@ -using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.OtOpcUa.Commons.Observability; @@ -789,12 +789,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // mirrors the probe loop's recreate-on-failure behaviour. EvictRuntime(device, def.Name); results[fb.OriginalIndex] = new DataValueSnapshot(null, - AbCipStatusMapper.MapLibplctagStatus(status), null, now); + AbCipStatusMapper.MapStatus(status), null, now); _logger.LogWarning( - "AbCip read returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " + + "AbCip read returned non-zero transport status {Status} for tag {Tag} on device {Device}; " + "evicting cached runtime so next call re-creates it", status, reference, def.DeviceHostAddress); - return ReadOutcome.Degraded($"libplctag status {status} reading {reference}"); + return ReadOutcome.Degraded($"transport status {status} reading {reference}"); } var tagPath = AbCipTagPath.TryParse(def.TagPath); @@ -850,7 +850,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// /// Perform one whole-UDT read on the parent tag, then decode each /// grouped member from the runtime's buffer at its computed byte offset. A per-group - /// failure (parent read raised, non-zero libplctag status, or missing device) stamps + /// failure (parent read raised, non-zero transport status, or missing device) stamps /// the mapped fault across every grouped member only — sibling groups + the /// per-tag fallback list are unaffected. /// @@ -881,13 +881,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, if (status != 0) { EvictRuntime(device, parent.Name); - var mapped = AbCipStatusMapper.MapLibplctagStatus(status); + var mapped = AbCipStatusMapper.MapStatus(status); StampGroupStatus(group, results, now, mapped); _logger.LogWarning( - "AbCip whole-UDT read returned non-zero libplctag status {LibplctagStatus} for parent {Parent} " + + "AbCip whole-UDT read returned non-zero transport status {Status} for parent {Parent} " + "on device {Device}; {MemberCount} member values stamped with mapped status", status, group.ParentName, parent.DeviceHostAddress, group.Members.Count); - return ReadOutcome.Degraded($"libplctag status {status} reading UDT {group.ParentName}"); + return ReadOutcome.Degraded($"transport status {status} reading UDT {group.ParentName}"); } foreach (var member in group.Members) @@ -992,9 +992,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, if (status != 0) { EvictRuntime(device, def.Name); - results[i] = new WriteResult(AbCipStatusMapper.MapLibplctagStatus(status)); + results[i] = new WriteResult(AbCipStatusMapper.MapStatus(status)); _logger.LogWarning( - "AbCip write returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " + + "AbCip write returned non-zero transport status {Status} for tag {Tag} on device {Device}; " + "evicting cached runtime so next call re-creates it", status, w.FullReference, def.DeviceHostAddress); } @@ -1076,7 +1076,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, var parentRuntime = await EnsureParentRuntimeAsync(device, parentName, ct).ConfigureAwait(false); await parentRuntime.ReadAsync(ct).ConfigureAwait(false); var readStatus = parentRuntime.GetStatus(); - if (readStatus != 0) return AbCipStatusMapper.MapLibplctagStatus(readStatus); + if (readStatus != 0) return AbCipStatusMapper.MapStatus(readStatus); var current = Convert.ToInt32(parentRuntime.DecodeValue(AbCipDataType.DInt, bitIndex: null) ?? 0); var updated = Convert.ToBoolean(value) @@ -1088,7 +1088,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, var writeStatus = parentRuntime.GetStatus(); return writeStatus == 0 ? AbCipStatusMapper.Good - : AbCipStatusMapper.MapLibplctagStatus(writeStatus); + : AbCipStatusMapper.MapStatus(writeStatus); } finally { @@ -1183,10 +1183,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // not answer. "Tag not found" means the controller replied over a working session; it // is a config error on this one tag, and counting it as a failed Forward-Open throttled // every sibling tag on the device (seen live: one typo blacked out the whole device). - if (ex is LibPlcTagException lpe - && AbCipStatusMapper.TryGetStatus(lpe, out var status) - && status != Status.Ok - && AbCipDriverProbe.IsReachableStatus(status)) + if (ex is AbTransportException ate + && ate.Status != AbStatus.Ok + && AbCipDriverProbe.IsReachableStatus(ate.Status)) device.Backoff.RecordSuccess(); else device.Backoff.RecordFailure(DateTime.UtcNow); @@ -1211,7 +1210,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// Evict the runtime for from the device's cache and dispose /// it so the next read/write call re-creates and re-initializes a fresh handle. /// Called from , , and - /// after a non-zero libplctag status or transport exception — + /// after a non-zero transport status or transport exception — /// mirroring the probe loop's recreate-on-failure behaviour. /// private static void EvictRuntime(DeviceState device, string tagName) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs index ccfa04f4..6973cd04 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.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.AbCip.PlcFamilies; @@ -11,24 +11,24 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Two-phase Test-Connect probe for the -shaped driver config. /// Phase 1: bare TCP connect to the first device's gateway host + EtherNet/IP port to -/// quickly reject unreachable targets. Phase 2: initialises a libplctag +/// quickly reject unreachable targets. Phase 2: initialises a tag runtime /// against the same device to open a real EIP session and perform a CIP Forward Open — /// confirming the remote endpoint actually speaks EtherNet/IP CIP, not just accepts TCP. /// A device that accepts the TCP connection but is not a CIP controller (wrong CIP path, /// non-CIP 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 (AbStatus) that indicate the controller answered /// the CIP session but could not resolve the probe tag -/// (, , -/// ) are treated as "reachable — Ok = true" because +/// (ErrorNotFound, ErrorNoMatch, +/// ErrorBadDevice) are treated as "reachable — Ok = true" because /// a non-existent tag name proves CIP connectivity. Statuses that indicate a transport or -/// session failure (, , -/// , , -/// , , -/// , , -/// , , -/// , ) are treated as +/// session failure (ErrorTimeout, ErrorBadConnection, +/// ErrorBadGateway, ErrorWinsock, +/// ErrorOpen, ErrorClose, +/// ErrorRead, ErrorWrite, +/// ErrorBadReply, ErrorRemoteErr, +/// ErrorPartial, ErrorAbort) are treated as /// "handshake failed — Ok = false". All other error statuses default to /// "handshake failed" (conservative). /// @@ -42,6 +42,12 @@ public sealed class AbCipDriverProbe : IDriverProbe Converters = { new JsonStringEnumConverter() }, }; + private readonly IAbCipTagFactory _tagFactory; + + /// Initializes a new instance of the class. + /// Factory for the probe's tag runtime; defaults to the libplctag-backed one. + public AbCipDriverProbe(IAbCipTagFactory? tagFactory = null) => _tagFactory = tagFactory ?? new LibplctagTagFactory(); + /// public string DriverType => "AbCip"; @@ -124,7 +130,7 @@ public sealed class AbCipDriverProbe : IDriverProbe AllowPacking: firstDevice?.AllowPacking ?? profile.SupportsRequestPacking, ConnectionSize: firstDevice?.ConnectionSize ?? profile.DefaultConnectionSize); - var rt = new LibplctagTagRuntime(p); + var rt = _tagFactory.Create(p); try { await rt.InitializeAsync(cts.Token); @@ -132,10 +138,10 @@ public sealed class AbCipDriverProbe : 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 ? "CIP session OK" : "CIP session OK (controller reachable; probe tag not found)"; return new(true, msg, sw.Elapsed); @@ -148,9 +154,9 @@ public sealed class AbCipDriverProbe : 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. + // AbTransportException carries the Status; classify as reachable vs transport failure. if (IsReachableException(ex)) { sw.Stop(); @@ -170,42 +176,23 @@ public sealed class AbCipDriverProbe : IDriverProbe } /// - /// Returns true for libplctag statuses that indicate the CIP controller answered + /// Returns true for transport statuses that indicate the CIP controller answered /// but the probe tag name was not found or did not match — sufficient to confirm CIP - /// reachability. Returns true for (tag was found). + /// reachability. Returns true for Ok (tag was found). /// - internal static bool IsReachableStatus(Status s) => s is - Status.Ok or - Status.ErrorNotFound or - Status.ErrorNoMatch or - Status.ErrorBadDevice; + internal static bool IsReachableStatus(AbStatus s) => s is AbStatus.Ok or AbStatus.NotFound; /// - /// Decides whether a is a "controller answered CIP but the + /// Decides whether an is a "controller answered CIP but the /// probe tag did not resolve" condition (reachable) or a session / transport failure - /// (unreachable), by recovering the the wrapper threw on and deferring - /// to — one classification, not two that can drift. + /// (unreachable), deferring to — one classification, not two. /// - /// The exception libplctag threw out of InitializeAsync. + /// The exception thrown out of InitializeAsync. /// true when the controller answered; false for a transport/session failure. - /// - /// The status arrives as the managed enum NAME. NativeTagWrapper.ThrowIfStatusNotOk - /// constructs the exception with Status.ToString(), so the message is exactly - /// "ErrorNotFound" — never the native C spelling "STATUS_NOT_FOUND" this method - /// used to look for. Those substrings could not match, so the reachable classification was - /// unreachable on this path — and this IS the path a probe takes whenever its tag does not - /// resolve, which is every Test Connect against a device with no authored tags. The native - /// spellings are kept as a secondary match in case a future wrapper reports them. - /// - internal static bool IsReachableException(LibPlcTagException ex) + internal static bool IsReachableException(AbTransportException ex) { - var msg = ex.Message ?? string.Empty; - if (AbCipStatusMapper.TryGetStatus(ex, out var status)) - return IsReachableStatus(status); - - return msg.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase) - || msg.Contains("NO_MATCH", StringComparison.OrdinalIgnoreCase) - || msg.Contains("BAD_DEVICE", StringComparison.OrdinalIgnoreCase); + ArgumentNullException.ThrowIfNull(ex); + return IsReachableStatus(ex.Status); } private static (AbCipHostAddress? parsed, AbCipDeviceOptions? device) ExtractTarget(AbCipDriverOptions opts) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs index bbe48a20..7b026d30 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs @@ -1,4 +1,4 @@ -using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; @@ -26,8 +26,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// writes during download / test-mode transitions). /// 0x16 object does not exist — BadNodeIdUnknown. /// 0x1E embedded service error — unwrap to the extended status when possible. -/// libplctag.NET errors — mapped per-member by -/// : timeout, not-found, not-allowed, and +/// Transport errors — mapped per-member by +/// : timeout, not-found, not-allowed, and /// out-of-bounds get their specific OPC UA codes; the remaining transport errors /// fold into BadCommunicationError. /// @@ -64,66 +64,42 @@ public static class AbCipStatusMapper }; /// - /// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here - /// is (int)Tag.GetStatus() — i.e. the underlying value of the libplctag.NET - /// enum, NOT a raw native PLCTAG_ERR_* constant. The wrapper - /// renumbers the native codes into a contiguous enum, so this method switches on the - /// members directly to stay correct if the wrapper renumbers again. - /// is success; is an in-flight - /// operation; every other (negative) member is an error. + /// Map a transport status code to an OPC UA StatusCode. The integer is + /// , i.e. the underlying value of . /// - /// The libplctag status code as an integer. + /// The as an integer. /// The mapped OPC UA StatusCode value. - public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status); + public static uint MapStatus(int status) => MapStatus((AbStatus)status); /// - /// Recover the libplctag a was thrown on. - /// The wrapper builds the exception from Status.ToString(), so the message is the enum - /// name ("ErrorNotFound"). + /// Map an to an OPC UA StatusCode. Timeout, not-found, not-allowed and + /// out-of-bounds get their specific codes; the remaining transport errors fold into + /// . /// - /// The exception. - /// The recovered status. - /// true when the message names a defined . - public static bool TryGetStatus(LibPlcTagException ex, out Status status) + /// The transport status. + /// The mapped OPC UA StatusCode value. + public static uint MapStatus(AbStatus status) => status switch { - ArgumentNullException.ThrowIfNull(ex); - return Enum.TryParse((ex.Message ?? string.Empty).Trim(), ignoreCase: true, out status) - && Enum.IsDefined(status); - } + 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 + }; /// - /// Status for an exception thrown while creating / reading a tag: a libplctag exception maps - /// through its (so a tag the controller does not have reads + /// Status for an exception thrown while creating / reading a tag: a transport exception maps + /// through its (so a tag the controller does not have reads /// , not a comms error); anything else is /// . /// /// The exception. /// The OPC UA status code. public static uint MapException(Exception ex) => - ex is LibPlcTagException lpe && TryGetStatus(lpe, out var status) - ? MapLibplctagStatus(status) - : BadCommunicationError; - - /// - /// Map a libplctag.NET enum value to an OPC UA StatusCode. This is - /// the strongly-typed core of the mapper; the int overload exists only for the - /// seam, which returns the boxed-as-int value. - /// - /// The libplctag Status enum value. - /// The mapped OPC UA StatusCode value. - public static uint MapLibplctagStatus(Status 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, - }; + ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs index d3cbafa8..61b6a016 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs @@ -25,8 +25,8 @@ public interface IAbCipTagRuntime : IDisposable Task WriteAsync(CancellationToken cancellationToken); /// - /// Raw libplctag status code — mapped to an OPC UA StatusCode via - /// . Zero on success, negative on error. + /// Raw transport status code (an AbStatus value) — mapped to an OPC UA StatusCode via + /// . Zero on success, negative on error. /// /// The raw libplctag status code (zero on success, negative on error). int GetStatus(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs new file mode 100644 index 00000000..4e938c5b --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs @@ -0,0 +1,58 @@ +using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; + +/// The ONLY place libplctag's meets . +internal static class LibplctagStatusTranslation +{ + /// Translates a libplctag status into the driver-owned . + /// The libplctag status. + /// The equivalent transport-neutral status. + 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, + }; + + /// + /// Recovers the status a was thrown on. libplctag builds the + /// exception from Status.ToString(), so the message is the enum name. + /// + /// The exception. + /// The translated status, or when the message names no status. + public static AbStatus ToAbStatus(LibPlcTagException ex) => + Enum.TryParse((ex.Message ?? string.Empty).Trim(), ignoreCase: true, out var s) && Enum.IsDefined(s) + ? ToAbStatus(s) + : AbStatus.BadConnection; + + /// Runs , rethrowing any as . + /// The libplctag operation. + /// A task completing when the operation does. + public static async Task Wrap(Func op) + { + try { await op().ConfigureAwait(false); } + catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); } + } + + /// Runs , rethrowing any as . + /// The result type. + /// The libplctag operation. + /// The operation result. + public static async Task Wrap(Func> op) + { + try { return 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.AbCip/LibplctagTagEnumerator.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs index e2858385..f9626b00 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs @@ -77,9 +77,12 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator AbCipTagCreateParams deviceParams, string listingName, CancellationToken cancellationToken) { using var tag = BuildSymbolListTag(deviceParams, listingName); - await tag.InitializeAsync(cancellationToken).ConfigureAwait(false); - await tag.ReadAsync(cancellationToken).ConfigureAwait(false); - return tag.GetBuffer(); + return await LibplctagStatusTranslation.Wrap(async () => + { + await tag.InitializeAsync(cancellationToken).ConfigureAwait(false); + await tag.ReadAsync(cancellationToken).ConfigureAwait(false); + return tag.GetBuffer(); + }).ConfigureAwait(false); } /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs index b503dc03..62d8a914 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs @@ -1,4 +1,5 @@ using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; @@ -35,16 +36,19 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime } /// - public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken); + public Task InitializeAsync(CancellationToken cancellationToken) => + LibplctagStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken)); /// - public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken); + public Task ReadAsync(CancellationToken cancellationToken) => + LibplctagStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken)); /// - public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken); + public Task WriteAsync(CancellationToken cancellationToken) => + LibplctagStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken)); /// - public int GetStatus() => (int)_tag.GetStatus(); + public int GetStatus() => (int)LibplctagStatusTranslation.ToAbStatus(_tag.GetStatus()); /// public int GetSize() => _tag.GetSize(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs index 5af05c01..2fc4d94f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs @@ -20,9 +20,13 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader { _tag?.Dispose(); _tag = BuildTemplateTag(deviceParams, templateInstanceId); - await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false); - await _tag.ReadAsync(cancellationToken).ConfigureAwait(false); - return _tag.GetBuffer(); + var tag = _tag; + return await LibplctagStatusTranslation.Wrap(async () => + { + await tag.InitializeAsync(cancellationToken).ConfigureAwait(false); + await tag.ReadAsync(cancellationToken).ConfigureAwait(false); + return tag.GetBuffer(); + }).ConfigureAwait(false); } /// Builds the (uninitialized) libplctag tag for the @udt/{id} pseudo. diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs index dbc39685..f1fabd0f 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.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; @@ -323,7 +323,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests // First tag creation → returns error; second → returns success. callCount++; return callCount == 1 - ? new FakeAbCipTag(p) { Status = (int)libplctag.Status.ErrorBadConnection } + ? new FakeAbCipTag(p) { Status = (int)AbStatus.BadConnection } : new FakeAbCipTag(p) { Status = 0, Value = 42 }; }; var drv = new AbCipDriver(new AbCipDriverOptions diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs index ba406b9f..5707aea4 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs @@ -1,6 +1,6 @@ using System.Net; using System.Net.Sockets; -using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Shouldly; using Xunit; @@ -151,27 +151,22 @@ public sealed class AbCipDriverProbeTests // ------------------------------------------------------------------------- /// - /// The exception path classifies by the SAME rule as the status path. libplctag.NET builds - /// with Status.ToString(), so its message is - /// "ErrorNotFound" — this method used to look for the native C spelling "STATUS_NOT_FOUND", - /// which that message can never contain, so a controller that answered "tag does not exist" - /// was reported as a failed handshake. That is the answer every Test Connect gets on a device - /// with no authored tags, i.e. every device at the moment an operator first presses the button. + /// The exception path classifies by the SAME rule as the status path: a controller that answered + /// "tag does not exist" is reachable (the answer every Test Connect gets on a device with no + /// authored tags); a session or transport failure is not. /// - /// The status libplctag threw on. + /// The status the transport threw on. /// Whether the controller should be reported reachable. [Theory] - [InlineData(Status.ErrorNotFound, true)] - [InlineData(Status.ErrorNoMatch, true)] - [InlineData(Status.ErrorBadDevice, true)] - [InlineData(Status.ErrorTimeout, false)] - [InlineData(Status.ErrorBadConnection, false)] - [InlineData(Status.ErrorBadGateway, false)] - [InlineData(Status.ErrorBadParam, false)] - public void ThrownStatus_IsClassifiedTheSameWayAsAReturnedStatus(Status status, bool expectedReachable) + [InlineData(AbStatus.NotFound, true)] + [InlineData(AbStatus.Timeout, false)] + [InlineData(AbStatus.BadConnection, false)] + [InlineData(AbStatus.BadReply, false)] + [InlineData(AbStatus.Partial, false)] + [InlineData(AbStatus.Abort, false)] + public void ThrownStatus_IsClassifiedTheSameWayAsAReturnedStatus(AbStatus status, bool expectedReachable) { - // Exactly how NativeTagWrapper.ThrowIfStatusNotOk constructs it. - var ex = new LibPlcTagException(status.ToString()); + var ex = new AbTransportException(status, status.ToString()); AbCipDriverProbe.IsReachableException(ex).ShouldBe(expectedReachable); AbCipDriverProbe.IsReachableStatus(status).ShouldBe(expectedReachable); @@ -185,5 +180,5 @@ public sealed class AbCipDriverProbeTests [Fact] public void AnUnrecognisedExceptionMessageIsNotTreatedAsReachable() => AbCipDriverProbe.IsReachableException( - new LibPlcTagException("something the wrapper has never said")).ShouldBeFalse(); + new AbTransportException(AbStatus.BadConnection, "something the wrapper has never said")).ShouldBeFalse(); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs index 735a9c35..fa46b20d 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.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; @@ -87,14 +87,14 @@ public sealed class AbCipDriverReadTests factory.Tags["Motor1.Speed"].ReadCount.ShouldBe(3); } - /// Verifies that non-zero libplctag status is mapped via AbCipStatusMapper. + /// Verifies that non-zero transport status is mapped via AbCipStatusMapper. [Fact] - public async Task NonZero_libplctag_status_maps_via_AbCipStatusMapper() + public async Task NonZero_transport_status_maps_via_AbCipStatusMapper() { var (drv, factory) = NewDriver( new AbCipTagDefinition("Ghost", "ab://10.0.0.5/1,0", "Missing.Tag", AbCipDataType.DInt)); await drv.InitializeAsync("{}", CancellationToken.None); - factory.Customise = p => new FakeAbCipTag(p) { Status = (int)Status.ErrorNotFound }; + factory.Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.NotFound }; var snapshots = await drv.ReadAsync(["Ghost"], CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs index b5403ca9..f339b396 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs @@ -1,3 +1,4 @@ +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Shouldly; using Xunit; @@ -89,7 +90,7 @@ public sealed class AbCipDriverWholeUdtReadTests // Prime runtime existence via a first (successful) read so we can flip it to error. await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None); - factory.Tags["Motor"].Status = -3; // libplctag BadTimeout — mapped in AbCipStatusMapper + factory.Tags["Motor"].Status = (int)AbStatus.Timeout; // mapped in AbCipStatusMapper var snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs index 2417399b..b70fe900 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.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; @@ -84,14 +84,14 @@ public sealed class AbCipDriverWriteTests results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good); } - /// Verifies that non-zero libplctag status after write maps correctly. + /// Verifies that non-zero transport status after write maps correctly. [Fact] - public async Task Non_zero_libplctag_status_after_write_maps_via_AbCipStatusMapper() + public async Task Non_zero_transport_status_after_write_maps_via_AbCipStatusMapper() { var (drv, factory) = NewDriver( new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.DInt)); await drv.InitializeAsync("{}", CancellationToken.None); - factory.Customise = p => new FakeAbCipTag(p) { Status = (int)Status.ErrorTimeout }; + factory.Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.Timeout }; var results = await drv.WriteAsync( [new WriteRequest("Broken", 1)], CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs index 11123518..1704614b 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs @@ -1,3 +1,4 @@ +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Microsoft.Extensions.Logging; using Shouldly; using Xunit; @@ -88,7 +89,7 @@ public sealed class AbCipLoggingTests var logger = new CapturingLogger(); var factory = new FakeAbCipTagFactory { - Customise = p => new FakeAbCipTag(p) { Status = (int)libplctag.Status.ErrorBadConnection }, + Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.BadConnection }, }; var drv = new AbCipDriver(new AbCipDriverOptions { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs index dc15dcc7..e8cf140b 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs @@ -1,4 +1,5 @@ using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; @@ -29,41 +30,78 @@ public sealed class AbCipStatusMapperTests AbCipStatusMapper.MapCipGeneralStatus(status).ShouldBe(expected); } - // Driver.AbCip-002 — the integers here are the underlying values of the libplctag.NET - // Status enum (what (int)Tag.GetStatus() actually returns), NOT raw native PLCTAG_ERR_* - // constants. The libplctag.NET wrapper renumbers the native codes into a contiguous enum. - /// Verifies that libplctag Status enum values are mapped to OPC UA status codes. - /// The libplctag Status enum value to map. + /// Verifies that transport status values are mapped to OPC UA status codes. + /// The transport status to map. /// The expected OPC UA status code. [Theory] - [InlineData(Status.Ok, AbCipStatusMapper.Good)] - [InlineData(Status.Pending, AbCipStatusMapper.GoodMoreData)] - [InlineData(Status.ErrorTimeout, AbCipStatusMapper.BadTimeout)] - [InlineData(Status.ErrorNotFound, AbCipStatusMapper.BadNodeIdUnknown)] - [InlineData(Status.ErrorNotAllowed, AbCipStatusMapper.BadNotWritable)] - [InlineData(Status.ErrorOutOfBounds, AbCipStatusMapper.BadOutOfRange)] - [InlineData(Status.ErrorBadConnection, AbCipStatusMapper.BadCommunicationError)] - [InlineData(Status.ErrorBadGateway, AbCipStatusMapper.BadCommunicationError)] - [InlineData(Status.ErrorUnsupported, AbCipStatusMapper.BadNotSupported)] - [InlineData(Status.ErrorNoMem, AbCipStatusMapper.BadCommunicationError)] // unmapped → generic comms - public void MapLibplctagStatus_maps_real_enum_members(Status status, uint expected) + [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.MapLibplctagStatus(status).ShouldBe(expected); - // The int overload must agree — it is the seam IAbCipTagRuntime.GetStatus() drives. - AbCipStatusMapper.MapLibplctagStatus((int)status).ShouldBe(expected); + AbCipStatusMapper.MapStatus(status).ShouldBe(expected); + // The int overload must agree - it is the seam IAbCipTagRuntime.GetStatus() drives. + AbCipStatusMapper.MapStatus((int)status).ShouldBe(expected); } /// Verifies that timeout is distinguished from generic communication error. [Fact] - public void MapLibplctagStatus_distinguishes_timeout_from_generic_comms_error() + public void MapStatus_distinguishes_timeout_from_generic_comms_error() { - // Regression for Driver.AbCip-002: a real timeout used to fall through to - // BadCommunicationError because the code matched the wrong integer (-5). - AbCipStatusMapper.MapLibplctagStatus((int)Status.ErrorTimeout) - .ShouldBe(AbCipStatusMapper.BadTimeout); - AbCipStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound) - .ShouldBe(AbCipStatusMapper.BadNodeIdUnknown); - AbCipStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound) - .ShouldNotBe(AbCipStatusMapper.BadCommunicationError); + AbCipStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbCipStatusMapper.BadTimeout); + AbCipStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbCipStatusMapper.BadNodeIdUnknown); + AbCipStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldNotBe(AbCipStatusMapper.BadCommunicationError); } + + /// Verifies MapException maps transport exceptions by status and everything else to comms error. + [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); + } + + /// + /// Regression oracle: the OPC UA code the pre-AbStatus MapLibplctagStatus returned for every + /// libplctag member (table copied from the deleted switch) must equal + /// MapStatus(ToAbStatus(member)). + /// + /// A libplctag status. + [Theory] + [MemberData(nameof(AllLibplctagStatuses))] + public void Translation_plus_MapStatus_matches_the_old_libplctag_mapping(Status status) + { + AbCipStatusMapper.MapStatus(LibplctagStatusTranslation.ToAbStatus(status)).ShouldBe(OldMapLibplctagStatus(status)); + } + + /// Every defined libplctag member. + /// One row per member. + public static IEnumerable AllLibplctagStatuses() => + Enum.GetValues().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/... + }; } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUnknownTagReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUnknownTagReadTests.cs index 15643010..e5aa3c51 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUnknownTagReadTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUnknownTagReadTests.cs @@ -1,4 +1,4 @@ -using libplctag; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// A tag the controller does not have is a configuration error, not a lost connection. Seen live /// against a production CompactLogix: with one misspelled tag first in the read list, libplctag's -/// create threw LibPlcTagException("ErrorNotFound"), the driver counted that as a failed +/// create threw AbTransportException(AbStatus.NotFound), the driver counted that as a failed /// Forward-Open, opened the device's connect-backoff window, and every other not-yet-connected tag /// on the device — all of them real — came back BadCommunicationError (throttled). The bad /// tag itself also read BadCommunicationError instead of BadNodeIdUnknown. @@ -43,7 +43,7 @@ public sealed class AbCipUnknownTagReadTests [Fact] public async Task Unknown_tag_does_not_throttle_sibling_tags_and_reads_BadNodeIdUnknown() { - var (drv, _) = Build(name => name == "NoSuchTag_xyz" ? new LibPlcTagException("ErrorNotFound") : null); + var (drv, _) = Build(name => name == "NoSuchTag_xyz" ? new AbTransportException(AbStatus.NotFound, "NotFound") : null); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); var r = await drv.ReadAsync(["Typo", "Real1", "Real2"], TestContext.Current.CancellationToken); @@ -61,7 +61,7 @@ public sealed class AbCipUnknownTagReadTests [Fact] public async Task A_connection_failure_still_opens_the_backoff_window() { - var (drv, factory) = Build(name => name == "NoSuchTag_xyz" ? new LibPlcTagException("ErrorTimeout") : null); + var (drv, factory) = Build(name => name == "NoSuchTag_xyz" ? new AbTransportException(AbStatus.Timeout, "Timeout") : null); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); var r = await drv.ReadAsync(["Typo", "Real1"], TestContext.Current.CancellationToken); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs new file mode 100644 index 00000000..67751521 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs @@ -0,0 +1,54 @@ +using libplctag; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; + +/// Pins the libplctag to translation table. +public sealed class LibplctagStatusTranslationTests +{ + /// Each libplctag status lands in the documented bucket. + /// The libplctag status. + /// The expected transport status. + [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) => LibplctagStatusTranslation.ToAbStatus(from).ShouldBe(to); + + /// The exception message is recovered as the enum name. + [Fact] + public void Exception_message_is_recovered_as_the_enum_name() + { + LibplctagStatusTranslation.ToAbStatus(new LibPlcTagException("ErrorNotFound")).ShouldBe(AbStatus.NotFound); + LibplctagStatusTranslation.ToAbStatus(new LibPlcTagException("gibberish")).ShouldBe(AbStatus.BadConnection); + } + + /// Wrap rethrows as a transport exception keeping the original as InnerException. + [Fact] + public async Task Wrap_rethrows_as_AbTransportException_with_inner() + { + var inner = new LibPlcTagException("ErrorTimeout"); + + var ex = await Should.ThrowAsync( + () => LibplctagStatusTranslation.Wrap(() => throw inner)); + + ex.Status.ShouldBe(AbStatus.Timeout); + ex.InnerException.ShouldBeSameAs(inner); + } +}