merge: Task 3 — AbLegacy seam decoupled from libplctag types
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 <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 that indicate the controller answered
|
||||
/// the PCCC 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
|
||||
/// (<see cref="AbStatus.NotFound"/>) are treated as "reachable — Ok = true" because
|
||||
/// a non-existent tag name proves PCCC 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 (<see cref="AbStatus.Timeout"/>, <see cref="AbStatus.BadConnection"/>,
|
||||
/// <see cref="AbStatus.BadReply"/>, <see cref="AbStatus.Partial"/>, <see cref="AbStatus.Abort"/>) are treated as
|
||||
/// "handshake failed — Ok = false". All other error statuses default to
|
||||
/// "handshake failed" (conservative).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class AbLegacyDriverProbe : IDriverProbe
|
||||
{
|
||||
private readonly IAbLegacyTagFactory _tagFactory;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AbLegacyDriverProbe"/> class.</summary>
|
||||
/// <param name="tagFactory">The tag factory used for the PCCC handshake; defaults to the libplctag-backed one.</param>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> 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 <c>true</c> for <see cref="Status.Ok"/> (tag was found).
|
||||
/// Returns <c>true</c> for statuses that indicate the PCCC controller answered but the
|
||||
/// probe tag was not found — sufficient to confirm PCCC reachability. Also <c>true</c> for
|
||||
/// <see cref="AbStatus.Ok"/> (tag was found).
|
||||
/// </summary>
|
||||
private static bool IsReachableStatus(Status s) => s is
|
||||
Status.Ok or
|
||||
Status.ErrorNotFound or
|
||||
Status.ErrorNoMatch or
|
||||
Status.ErrorBadDevice;
|
||||
|
||||
/// <summary>
|
||||
/// Inspects a <see cref="LibPlcTagException"/> 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
/// <param name="s">The transport status.</param>
|
||||
/// <returns>Whether the status proves the controller answered.</returns>
|
||||
internal static bool IsReachableStatus(AbStatus s) => s is AbStatus.Ok or AbStatus.NotFound;
|
||||
|
||||
private static (AbLegacyHostAddress? parsed, AbLegacyDeviceOptions? device) ExtractTarget(AbLegacyDriverOptions opts)
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using libplctag;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
@@ -22,44 +22,41 @@ public static class AbLegacyStatusMapper
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
|
||||
/// <summary>
|
||||
/// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here
|
||||
/// is <c>(int)Tag.GetStatus()</c> — the underlying value of the libplctag.NET
|
||||
/// <see cref="Status"/> 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 <see cref="AbStatus"/> (what <see cref="IAbLegacyTagRuntime.GetStatus"/>
|
||||
/// returns) to an OPC UA StatusCode.
|
||||
/// </summary>
|
||||
/// <param name="status">The integer status code from libplctag.</param>
|
||||
/// <param name="status">The integer transport status.</param>
|
||||
/// <returns>The corresponding OPC UA status code.</returns>
|
||||
public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
|
||||
public static uint MapStatus(int status) => MapStatus((AbStatus)status);
|
||||
|
||||
/// <summary>
|
||||
/// Map a libplctag.NET <see cref="Status"/> enum value to an OPC UA StatusCode. This is
|
||||
/// the canonical core; the <c>int</c> overload exists only for the
|
||||
/// <see cref="IAbLegacyTagRuntime.GetStatus"/> seam which boxes the enum as an int.
|
||||
/// </summary>
|
||||
/// <param name="status">The libplctag Status enum value.</param>
|
||||
/// <summary>Map a transport-neutral <see cref="AbStatus"/> to an OPC UA StatusCode.</summary>
|
||||
/// <param name="status">The transport status.</param>
|
||||
/// <returns>The corresponding OPC UA status code.</returns>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>Map an exception thrown by a transport to an OPC UA StatusCode.</summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <returns>The mapped status for an <see cref="AbTransportException"/>, else BadCommunicationError.</returns>
|
||||
public static uint MapException(Exception ex) =>
|
||||
ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Status"/> enum rather than exposing
|
||||
/// the transports surface only an <see cref="AbStatus"/> 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.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using libplctag;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>The ONLY place libplctag's <see cref="Status"/> meets <see cref="AbStatus"/> in AbLegacy.</summary>
|
||||
internal static class LibplctagLegacyStatusTranslation
|
||||
{
|
||||
/// <summary>Translates a libplctag status into the transport-neutral <see cref="AbStatus"/>.</summary>
|
||||
/// <param name="s">The libplctag status.</param>
|
||||
/// <returns>The corresponding <see cref="AbStatus"/>.</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>libplctag builds the exception from <c>Status.ToString()</c>, so the message is the enum name.</summary>
|
||||
/// <param name="ex">The libplctag exception.</param>
|
||||
/// <returns>The corresponding <see cref="AbStatus"/>.</returns>
|
||||
public static AbStatus ToAbStatus(LibPlcTagException ex) =>
|
||||
Enum.TryParse<Status>((ex.Message ?? "").Trim(), ignoreCase: true, out var s) && Enum.IsDefined(s)
|
||||
? ToAbStatus(s)
|
||||
: AbStatus.BadConnection;
|
||||
|
||||
/// <summary>Runs <paramref name="op"/>, rethrowing libplctag failures as <see cref="AbTransportException"/>.</summary>
|
||||
/// <param name="op">The libplctag operation.</param>
|
||||
/// <returns>A task completing with the operation.</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); }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagLegacyStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
||||
public Task ReadAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagLegacyStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
|
||||
public Task WriteAsync(CancellationToken cancellationToken) =>
|
||||
LibplctagLegacyStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() => (int)_tag.GetStatus();
|
||||
public int GetStatus() => (int)LibplctagLegacyStatusTranslation.ToAbStatus(_tag.GetStatus());
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? DecodeValue(AbLegacyDataType type, int? bitIndex) => type switch
|
||||
|
||||
@@ -112,7 +112,7 @@ public sealed class AbLegacyArrayTests
|
||||
var (drv, factory) = NewDriver(
|
||||
new AbLegacyTagDefinition("Levels", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 5));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)libplctag.Status.ErrorTimeout };
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbStatus.Timeout };
|
||||
|
||||
var snapshots = await drv.ReadAsync(["Levels"], CancellationToken.None);
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ public sealed class AbLegacyBitRmwTests
|
||||
public async Task Bit_write_surfaces_device_rejection_status()
|
||||
{
|
||||
// Arrange: the parent tag reads OK (Status = 0) but write returns a timeout error.
|
||||
const int errorTimeout = (int)libplctag.Status.ErrorTimeout;
|
||||
const int errorTimeout = (int)ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbStatus.Timeout;
|
||||
var factory = new FakeAbLegacyTagFactory
|
||||
{
|
||||
Customise = p => new FakeAbLegacyTag(p)
|
||||
@@ -252,6 +252,6 @@ public sealed class AbLegacyBitRmwTests
|
||||
// Assert: the write rejection must NOT be silently swallowed as Good.
|
||||
var statusCode = results.Single().StatusCode;
|
||||
statusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
|
||||
statusCode.ShouldBe(AbLegacyStatusMapper.MapLibplctagStatus(errorTimeout));
|
||||
statusCode.ShouldBe(AbLegacyStatusMapper.MapStatus(errorTimeout));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+62
-28
@@ -1,6 +1,7 @@
|
||||
using libplctag;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
@@ -65,42 +66,75 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
AbLegacyStatusMapper.MapPcccStatus(sts).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that libplctag Status enum members are mapped to OPC UA status codes correctly.</summary>
|
||||
/// <param name="status">The libplctag Status enum value to map.</param>
|
||||
/// <summary>Verifies that AbStatus members map to OPC UA status codes correctly.</summary>
|
||||
/// <param name="status">The AbStatus value to map.</param>
|
||||
/// <param name="expected">The expected OPC UA status code.</param>
|
||||
// Driver.AbLegacy-010 — tests use the libplctag.NET Status enum members (what
|
||||
// (int)Tag.GetStatus() actually returns) rather than the unverified magic integers
|
||||
// that predated this fix (-5/-7/-14/-16/-17 matched neither native PLCTAG_ERR_*
|
||||
// constants nor the .NET wrapper enum ordinals reliably).
|
||||
[Theory]
|
||||
[InlineData(Status.Ok, AbLegacyStatusMapper.Good)]
|
||||
[InlineData(Status.Pending, AbLegacyStatusMapper.GoodMoreData)]
|
||||
[InlineData(Status.ErrorTimeout, AbLegacyStatusMapper.BadTimeout)]
|
||||
[InlineData(Status.ErrorNotFound, AbLegacyStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData(Status.ErrorNoMatch, AbLegacyStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData(Status.ErrorNotAllowed, AbLegacyStatusMapper.BadNotWritable)]
|
||||
[InlineData(Status.ErrorOutOfBounds, AbLegacyStatusMapper.BadOutOfRange)]
|
||||
[InlineData(Status.ErrorTooLarge, AbLegacyStatusMapper.BadOutOfRange)]
|
||||
[InlineData(Status.ErrorBadConnection, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
[InlineData(Status.ErrorBadGateway, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
[InlineData(Status.ErrorUnsupported, AbLegacyStatusMapper.BadNotSupported)]
|
||||
[InlineData(Status.ErrorNoMem, AbLegacyStatusMapper.BadCommunicationError)] // unmapped → generic comms
|
||||
public void LibplctagStatus_maps_real_enum_members(Status status, uint expected)
|
||||
[InlineData(AbStatus.Ok, AbLegacyStatusMapper.Good)]
|
||||
[InlineData(AbStatus.Pending, AbLegacyStatusMapper.GoodMoreData)]
|
||||
[InlineData(AbStatus.Timeout, AbLegacyStatusMapper.BadTimeout)]
|
||||
[InlineData(AbStatus.NotFound, AbLegacyStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData(AbStatus.NotAllowed, AbLegacyStatusMapper.BadNotWritable)]
|
||||
[InlineData(AbStatus.OutOfBounds, AbLegacyStatusMapper.BadOutOfRange)]
|
||||
[InlineData(AbStatus.TooLarge, AbLegacyStatusMapper.BadOutOfRange)]
|
||||
[InlineData(AbStatus.TooSmall, AbLegacyStatusMapper.BadOutOfRange)]
|
||||
[InlineData(AbStatus.Unsupported, AbLegacyStatusMapper.BadNotSupported)]
|
||||
[InlineData(AbStatus.TypeMismatch, AbLegacyStatusMapper.BadTypeMismatch)]
|
||||
[InlineData(AbStatus.DeviceStateConflict, AbLegacyStatusMapper.BadDeviceFailure)]
|
||||
[InlineData(AbStatus.BadConnection, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
[InlineData(AbStatus.BadReply, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
[InlineData(AbStatus.Partial, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
[InlineData(AbStatus.Abort, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
public void AbStatus_maps_to_opcua_codes(AbStatus status, uint expected)
|
||||
{
|
||||
AbLegacyStatusMapper.MapLibplctagStatus(status).ShouldBe(expected);
|
||||
AbLegacyStatusMapper.MapStatus(status).ShouldBe(expected);
|
||||
// The int overload must agree — it is the seam IAbLegacyTagRuntime.GetStatus() drives.
|
||||
AbLegacyStatusMapper.MapLibplctagStatus((int)status).ShouldBe(expected);
|
||||
AbLegacyStatusMapper.MapStatus((int)status).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>MapException reads the status off an AbTransportException, else falls back to comms error.</summary>
|
||||
[Fact]
|
||||
public void MapException_uses_transport_status()
|
||||
{
|
||||
AbLegacyStatusMapper.MapException(new AbTransportException(AbStatus.Timeout, "t"))
|
||||
.ShouldBe(AbLegacyStatusMapper.BadTimeout);
|
||||
AbLegacyStatusMapper.MapException(new InvalidOperationException("x"))
|
||||
.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The regression oracle: the OPC UA code the pre-seam <c>MapLibplctagStatus(Status)</c> returned,
|
||||
/// copied verbatim from the deleted switch. For every libplctag member the new
|
||||
/// translation + <see cref="AbLegacyStatusMapper.MapStatus(AbStatus)"/> must reproduce it.
|
||||
/// </summary>
|
||||
private static uint OldMapLibplctagStatus(Status status) => status switch
|
||||
{
|
||||
Status.Ok => AbLegacyStatusMapper.Good,
|
||||
Status.Pending => AbLegacyStatusMapper.GoodMoreData,
|
||||
Status.ErrorTimeout => AbLegacyStatusMapper.BadTimeout,
|
||||
Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => AbLegacyStatusMapper.BadNodeIdUnknown,
|
||||
Status.ErrorNotAllowed => AbLegacyStatusMapper.BadNotWritable,
|
||||
Status.ErrorOutOfBounds or Status.ErrorTooLarge or Status.ErrorTooSmall => AbLegacyStatusMapper.BadOutOfRange,
|
||||
Status.ErrorUnsupported or Status.ErrorNotImplemented => AbLegacyStatusMapper.BadNotSupported,
|
||||
_ => AbLegacyStatusMapper.BadCommunicationError,
|
||||
};
|
||||
|
||||
/// <summary>Every libplctag Status member maps to the same OPC UA code as before the seam.</summary>
|
||||
[Fact]
|
||||
public void Translation_plus_mapper_matches_the_old_libplctag_switch_for_every_member()
|
||||
{
|
||||
foreach (var s in Enum.GetValues<Status>())
|
||||
{
|
||||
AbLegacyStatusMapper.MapStatus(LibplctagLegacyStatusTranslation.ToAbStatus(s))
|
||||
.ShouldBe(OldMapLibplctagStatus(s), $"libplctag Status.{s}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timeout errors are distinguished from generic communication errors.</summary>
|
||||
[Fact]
|
||||
public void MapLibplctagStatus_distinguishes_timeout_from_generic_comms_error()
|
||||
public void MapStatus_distinguishes_timeout_from_generic_comms_error()
|
||||
{
|
||||
// Regression for Driver.AbLegacy-010: timeout must not fall through to
|
||||
// BadCommunicationError the way the old magic-integer switch did.
|
||||
AbLegacyStatusMapper.MapLibplctagStatus((int)Status.ErrorTimeout)
|
||||
.ShouldBe(AbLegacyStatusMapper.BadTimeout);
|
||||
AbLegacyStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound)
|
||||
.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
AbLegacyStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbLegacyStatusMapper.BadTimeout);
|
||||
AbLegacyStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using libplctag;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -70,12 +70,12 @@ public sealed class AbLegacyReadWriteTests
|
||||
[Fact]
|
||||
public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
|
||||
{
|
||||
// Use the real libplctag.Status enum value rather than a raw integer so the test
|
||||
// Use the real AbStatus enum value rather than a raw integer so the test
|
||||
// stays correct if the wrapper renumbers its ordinals (Driver.AbLegacy-010).
|
||||
var (drv, factory) = NewDriver(
|
||||
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)Status.ErrorNotFound };
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)AbStatus.NotFound };
|
||||
|
||||
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
|
||||
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user