refactor(abcip): confine libplctag to its adapters behind AbStatus/AbTransportException
This commit is contained in:
@@ -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,
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
@@ -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 <paramref name="tagName"/> from the device's cache and dispose
|
||||
/// it so the next read/write call re-creates and re-initializes a fresh handle.
|
||||
/// Called from <see cref="ReadSingleAsync"/>, <see cref="ReadGroupAsync"/>, and
|
||||
/// <see cref="WriteAsync"/> after a non-zero libplctag status or transport exception —
|
||||
/// <see cref="WriteAsync"/> after a non-zero transport status or transport exception —
|
||||
/// mirroring the probe loop's recreate-on-failure behaviour.
|
||||
/// </summary>
|
||||
private static void EvictRuntime(DeviceState device, string tagName)
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// Two-phase Test-Connect probe for the <see cref="AbCipDriverOptions"/>-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 <see cref="Tag"/>
|
||||
/// 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 <c>Ok = false</c> with a "handshake failed" message instead of
|
||||
/// a false-positive green tick.
|
||||
/// <para>
|
||||
/// Status-code discrimination: libplctag statuses that indicate the controller answered
|
||||
/// Status-code discrimination: transport statuses (<c>AbStatus</c>) that indicate the controller answered
|
||||
/// the CIP session but could not resolve the probe tag
|
||||
/// (<see cref="Status.ErrorNotFound"/>, <see cref="Status.ErrorNoMatch"/>,
|
||||
/// <see cref="Status.ErrorBadDevice"/>) are treated as "reachable — Ok = true" because
|
||||
/// (<c>ErrorNotFound</c>, <c>ErrorNoMatch</c>,
|
||||
/// <c>ErrorBadDevice</c>) are treated as "reachable — Ok = true" because
|
||||
/// a non-existent tag name proves CIP connectivity. Statuses that indicate a transport or
|
||||
/// session failure (<see cref="Status.ErrorTimeout"/>, <see cref="Status.ErrorBadConnection"/>,
|
||||
/// <see cref="Status.ErrorBadGateway"/>, <see cref="Status.ErrorWinsock"/>,
|
||||
/// <see cref="Status.ErrorOpen"/>, <see cref="Status.ErrorClose"/>,
|
||||
/// <see cref="Status.ErrorRead"/>, <see cref="Status.ErrorWrite"/>,
|
||||
/// <see cref="Status.ErrorBadReply"/>, <see cref="Status.ErrorRemoteErr"/>,
|
||||
/// <see cref="Status.ErrorPartial"/>, <see cref="Status.ErrorAbort"/>) are treated as
|
||||
/// session failure (<c>ErrorTimeout</c>, <c>ErrorBadConnection</c>,
|
||||
/// <c>ErrorBadGateway</c>, <c>ErrorWinsock</c>,
|
||||
/// <c>ErrorOpen</c>, <c>ErrorClose</c>,
|
||||
/// <c>ErrorRead</c>, <c>ErrorWrite</c>,
|
||||
/// <c>ErrorBadReply</c>, <c>ErrorRemoteErr</c>,
|
||||
/// <c>ErrorPartial</c>, <c>ErrorAbort</c>) are treated as
|
||||
/// "handshake failed — Ok = false". All other error statuses default to
|
||||
/// "handshake failed" (conservative).
|
||||
/// </para>
|
||||
@@ -42,6 +42,12 @@ public sealed class AbCipDriverProbe : IDriverProbe
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
private readonly IAbCipTagFactory _tagFactory;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AbCipDriverProbe"/> class.</summary>
|
||||
/// <param name="tagFactory">Factory for the probe's tag runtime; defaults to the libplctag-backed one.</param>
|
||||
public AbCipDriverProbe(IAbCipTagFactory? tagFactory = null) => _tagFactory = tagFactory ?? new LibplctagTagFactory();
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> for libplctag statuses that indicate the CIP controller answered
|
||||
/// Returns <c>true</c> 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 <c>true</c> for <see cref="Status.Ok"/> (tag was found).
|
||||
/// reachability. Returns <c>true</c> for <c>Ok</c> (tag was found).
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether a <see cref="LibPlcTagException"/> is a "controller answered CIP but the
|
||||
/// Decides whether an <see cref="AbTransportException"/> is a "controller answered CIP but the
|
||||
/// probe tag did not resolve" condition (reachable) or a session / transport failure
|
||||
/// (unreachable), by recovering the <see cref="Status"/> the wrapper threw on and deferring
|
||||
/// to <see cref="IsReachableStatus"/> — one classification, not two that can drift.
|
||||
/// (unreachable), deferring to <see cref="IsReachableStatus"/> — one classification, not two.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception libplctag threw out of <c>InitializeAsync</c>.</param>
|
||||
/// <param name="ex">The exception thrown out of <c>InitializeAsync</c>.</param>
|
||||
/// <returns><c>true</c> when the controller answered; <c>false</c> for a transport/session failure.</returns>
|
||||
/// <remarks>
|
||||
/// <b>The status arrives as the managed enum NAME.</b> <c>NativeTagWrapper.ThrowIfStatusNotOk</c>
|
||||
/// constructs the exception with <c>Status.ToString()</c>, so the message is exactly
|
||||
/// <c>"ErrorNotFound"</c> — never the native C spelling <c>"STATUS_NOT_FOUND"</c> 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.
|
||||
/// </remarks>
|
||||
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)
|
||||
|
||||
@@ -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).</item>
|
||||
/// <item>0x16 object does not exist — <c>BadNodeIdUnknown</c>.</item>
|
||||
/// <item>0x1E embedded service error — unwrap to the extended status when possible.</item>
|
||||
/// <item>libplctag.NET <see cref="Status"/> errors — mapped per-member by
|
||||
/// <see cref="MapLibplctagStatus(Status)"/>: timeout, not-found, not-allowed, and
|
||||
/// <item>Transport <see cref="AbStatus"/> errors — mapped per-member by
|
||||
/// <see cref="MapStatus(AbStatus)"/>: timeout, not-found, not-allowed, and
|
||||
/// out-of-bounds get their specific OPC UA codes; the remaining transport errors
|
||||
/// fold into <c>BadCommunicationError</c>.</item>
|
||||
/// </list>
|
||||
@@ -64,66 +64,42 @@ public static class AbCipStatusMapper
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here
|
||||
/// is <c>(int)Tag.GetStatus()</c> — i.e. the underlying value of the libplctag.NET
|
||||
/// <see cref="Status"/> enum, NOT a raw native <c>PLCTAG_ERR_*</c> constant. The wrapper
|
||||
/// renumbers the native codes into a contiguous enum, so this method switches on the
|
||||
/// <see cref="Status"/> members directly to stay correct if the wrapper renumbers again.
|
||||
/// <see cref="Status.Ok"/> is success; <see cref="Status.Pending"/> 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
|
||||
/// <see cref="IAbCipTagRuntime.GetStatus"/>, i.e. the underlying value of <see cref="AbStatus"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">The libplctag status code as an integer.</param>
|
||||
/// <param name="status">The <see cref="AbStatus"/> as an integer.</param>
|
||||
/// <returns>The mapped OPC UA StatusCode value.</returns>
|
||||
public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
|
||||
public static uint MapStatus(int status) => MapStatus((AbStatus)status);
|
||||
|
||||
/// <summary>
|
||||
/// Recover the libplctag <see cref="Status"/> a <see cref="LibPlcTagException"/> was thrown on.
|
||||
/// The wrapper builds the exception from <c>Status.ToString()</c>, so the message is the enum
|
||||
/// name (<c>"ErrorNotFound"</c>).
|
||||
/// Map an <see cref="AbStatus"/> to an OPC UA StatusCode. Timeout, not-found, not-allowed and
|
||||
/// out-of-bounds get their specific codes; the remaining transport errors fold into
|
||||
/// <see cref="BadCommunicationError"/>.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <param name="status">The recovered status.</param>
|
||||
/// <returns><c>true</c> when the message names a defined <see cref="Status"/>.</returns>
|
||||
public static bool TryGetStatus(LibPlcTagException ex, out Status status)
|
||||
/// <param name="status">The transport status.</param>
|
||||
/// <returns>The mapped OPC UA StatusCode value.</returns>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Status for an exception thrown while creating / reading a tag: a libplctag exception maps
|
||||
/// through its <see cref="Status"/> (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 <see cref="AbStatus"/> (so a tag the controller does not have reads
|
||||
/// <see cref="BadNodeIdUnknown"/>, not a comms error); anything else is
|
||||
/// <see cref="BadCommunicationError"/>.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <returns>The OPC UA status code.</returns>
|
||||
public static uint MapException(Exception ex) =>
|
||||
ex is LibPlcTagException lpe && TryGetStatus(lpe, out var status)
|
||||
? MapLibplctagStatus(status)
|
||||
: BadCommunicationError;
|
||||
|
||||
/// <summary>
|
||||
/// Map a libplctag.NET <see cref="Status"/> enum value to an OPC UA StatusCode. This is
|
||||
/// the strongly-typed core of the mapper; the <c>int</c> overload exists only for the
|
||||
/// <see cref="IAbCipTagRuntime.GetStatus"/> seam, which returns the boxed-as-int value.
|
||||
/// </summary>
|
||||
/// <param name="status">The libplctag Status enum value.</param>
|
||||
/// <returns>The mapped OPC UA StatusCode value.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ public interface IAbCipTagRuntime : IDisposable
|
||||
Task WriteAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Raw libplctag status code — mapped to an OPC UA StatusCode via
|
||||
/// <see cref="AbCipStatusMapper.MapLibplctagStatus(int)"/>. Zero on success, negative on error.
|
||||
/// Raw transport status code (an <c>AbStatus</c> value) — mapped to an OPC UA StatusCode via
|
||||
/// <see cref="AbCipStatusMapper.MapStatus(int)"/>. Zero on success, negative on error.
|
||||
/// </summary>
|
||||
/// <returns>The raw libplctag status code (zero on success, negative on error).</returns>
|
||||
int GetStatus();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using libplctag;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>The ONLY place libplctag's <see cref="Status"/> meets <see cref="AbStatus"/>.</summary>
|
||||
internal static class LibplctagStatusTranslation
|
||||
{
|
||||
/// <summary>Translates a libplctag status into the driver-owned <see cref="AbStatus"/>.</summary>
|
||||
/// <param name="s">The libplctag status.</param>
|
||||
/// <returns>The equivalent transport-neutral status.</returns>
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Recovers the status a <see cref="LibPlcTagException"/> was thrown on. libplctag builds the
|
||||
/// exception from <c>Status.ToString()</c>, so the message is the enum name.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <returns>The translated status, or <see cref="AbStatus.BadConnection"/> when the message names no status.</returns>
|
||||
public static AbStatus ToAbStatus(LibPlcTagException ex) =>
|
||||
Enum.TryParse<Status>((ex.Message ?? string.Empty).Trim(), ignoreCase: true, out var s) && Enum.IsDefined(s)
|
||||
? ToAbStatus(s)
|
||||
: AbStatus.BadConnection;
|
||||
|
||||
/// <summary>Runs <paramref name="op"/>, rethrowing any <see cref="LibPlcTagException"/> as <see cref="AbTransportException"/>.</summary>
|
||||
/// <param name="op">The libplctag operation.</param>
|
||||
/// <returns>A task completing when the operation does.</returns>
|
||||
public static async Task Wrap(Func<Task> op)
|
||||
{
|
||||
try { await op().ConfigureAwait(false); }
|
||||
catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); }
|
||||
}
|
||||
|
||||
/// <summary>Runs <paramref name="op"/>, rethrowing any <see cref="LibPlcTagException"/> as <see cref="AbTransportException"/>.</summary>
|
||||
/// <typeparam name="T">The result type.</typeparam>
|
||||
/// <param name="op">The libplctag operation.</param>
|
||||
/// <returns>The operation result.</returns>
|
||||
public static async Task<T> Wrap<T>(Func<Task<T>> op)
|
||||
{
|
||||
try { return await op().ConfigureAwait(false); }
|
||||
catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
||||
public Task ReadAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
|
||||
public Task WriteAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() => (int)_tag.GetStatus();
|
||||
public int GetStatus() => (int)LibplctagStatusTranslation.ToAbStatus(_tag.GetStatus());
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetSize() => _tag.GetSize();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Builds the (uninitialized) libplctag tag for the <c>@udt/{id}</c> pseudo.</summary>
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// The exception path classifies by the SAME rule as the status path. libplctag.NET builds
|
||||
/// <see cref="LibPlcTagException"/> with <c>Status.ToString()</c>, 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.
|
||||
/// </summary>
|
||||
/// <param name="status">The status libplctag threw on.</param>
|
||||
/// <param name="status">The status the transport threw on.</param>
|
||||
/// <param name="expectedReachable">Whether the controller should be reported reachable.</param>
|
||||
[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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero libplctag status is mapped via AbCipStatusMapper.</summary>
|
||||
/// <summary>Verifies that non-zero transport status is mapped via AbCipStatusMapper.</summary>
|
||||
[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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-zero libplctag status after write maps correctly.</summary>
|
||||
/// <summary>Verifies that non-zero transport status after write maps correctly.</summary>
|
||||
[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);
|
||||
|
||||
@@ -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<AbCipDriver>();
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
/// <summary>Verifies that libplctag Status enum values are mapped to OPC UA status codes.</summary>
|
||||
/// <param name="status">The libplctag Status enum value to map.</param>
|
||||
/// <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(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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timeout is distinguished from generic communication error.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <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/...
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 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 <c>LibPlcTagException("ErrorNotFound")</c>, the driver counted that as a failed
|
||||
/// create threw <c>AbTransportException(AbStatus.NotFound)</c>, 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 <c>BadCommunicationError</c> (throttled). The bad
|
||||
/// tag itself also read <c>BadCommunicationError</c> instead of <c>BadNodeIdUnknown</c>.
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>Pins the libplctag <see cref="Status"/> to <see cref="AbStatus"/> translation table.</summary>
|
||||
public sealed class LibplctagStatusTranslationTests
|
||||
{
|
||||
/// <summary>Each libplctag status lands in the documented bucket.</summary>
|
||||
/// <param name="from">The libplctag status.</param>
|
||||
/// <param name="to">The expected transport status.</param>
|
||||
[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);
|
||||
|
||||
/// <summary>The exception message is recovered as the enum name.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Wrap rethrows as a transport exception keeping the original as InnerException.</summary>
|
||||
[Fact]
|
||||
public async Task Wrap_rethrows_as_AbTransportException_with_inner()
|
||||
{
|
||||
var inner = new LibPlcTagException("ErrorTimeout");
|
||||
|
||||
var ex = await Should.ThrowAsync<AbTransportException>(
|
||||
() => LibplctagStatusTranslation.Wrap(() => throw inner));
|
||||
|
||||
ex.Status.ShouldBe(AbStatus.Timeout);
|
||||
ex.InnerException.ShouldBeSameAs(inner);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user