Merge #568/#569/#580/#585: shared AB status map + orphan release, confinement guard tokeniser, adapter + DefaultTransport tests
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+207
-9
@@ -1,3 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -10,33 +11,230 @@ public sealed class LibplctagConfinementGuardTests
|
||||
@"\busing\s+(static\s+)?(\w+\s*=\s*)?libplctag\b|\blibplctag\.[A-Z]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
[Fact]
|
||||
public void Only_the_libplctag_adapters_reference_libplctag()
|
||||
// Directories whose Libplctag*.cs files may reference libplctag. LibplctagShared is the project-less home of
|
||||
// sources compiled into BOTH drivers (#580).
|
||||
private static readonly string[] AllowedSourceDirs =
|
||||
[
|
||||
"ZB.MOM.WW.OtOpcUa.Driver.AbCip",
|
||||
"ZB.MOM.WW.OtOpcUa.Driver.AbLegacy",
|
||||
"ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared",
|
||||
];
|
||||
|
||||
// The only projects that legitimately carry the libplctag package today (#569).
|
||||
private static readonly string[] AllowedPackageProjects =
|
||||
[
|
||||
"ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj",
|
||||
"ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj",
|
||||
];
|
||||
|
||||
private static readonly Regex PackageRef = new(
|
||||
@"<PackageReference\b[^>]*\b(Include|Update)\s*=\s*[""']libplctag(\.[\w.]*)?[""']",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static DirectoryInfo RepoRoot()
|
||||
{
|
||||
var root = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (root is not null && !File.Exists(Path.Combine(root.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) root = root.Parent;
|
||||
root.ShouldNotBeNull("could not find the repo root");
|
||||
return root;
|
||||
}
|
||||
|
||||
private static bool IsBuildOutput(string file) =>
|
||||
file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}") ||
|
||||
file.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}");
|
||||
|
||||
/// <summary>
|
||||
/// Reduces C# source to its code: comments are dropped and string / char literal CONTENT is emptied
|
||||
/// (newlines are kept, so line numbers hold). Strings are tokenised properly rather than split on
|
||||
/// <c>//</c> — a URL literal used to truncate the line and hide a reference after it, and a
|
||||
/// <c>/* */</c> block comment was never stripped (#569).
|
||||
/// </summary>
|
||||
/// <param name="text">The C# source.</param>
|
||||
/// <returns>The source with only code left.</returns>
|
||||
internal static string StripCommentsAndLiterals(string text)
|
||||
{
|
||||
var sb = new StringBuilder(text.Length);
|
||||
var i = 0;
|
||||
while (i < text.Length)
|
||||
{
|
||||
var c = text[i];
|
||||
var next = i + 1 < text.Length ? text[i + 1] : '\0';
|
||||
if (c == '/' && next == '/')
|
||||
{
|
||||
while (i < text.Length && text[i] != '\n') i++;
|
||||
}
|
||||
else if (c == '/' && next == '*')
|
||||
{
|
||||
i += 2;
|
||||
while (i < text.Length && !(text[i] == '*' && i + 1 < text.Length && text[i + 1] == '/'))
|
||||
{
|
||||
if (text[i] == '\n') sb.Append('\n');
|
||||
i++;
|
||||
}
|
||||
i = Math.Min(i + 2, text.Length);
|
||||
}
|
||||
else if (c == '"' && next == '"' && i + 2 < text.Length && text[i + 2] == '"')
|
||||
{
|
||||
// Raw string literal: a run of >= 3 quotes opens it and closes on a run of at least that length.
|
||||
var open = 0;
|
||||
while (i + open < text.Length && text[i + open] == '"') open++;
|
||||
i += open;
|
||||
while (i < text.Length)
|
||||
{
|
||||
if (text[i] == '"')
|
||||
{
|
||||
var run = 0;
|
||||
while (i + run < text.Length && text[i + run] == '"') run++;
|
||||
i += run;
|
||||
if (run >= open) break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (text[i] == '\n') sb.Append('\n');
|
||||
i++;
|
||||
}
|
||||
}
|
||||
sb.Append("\"\"");
|
||||
}
|
||||
else if (c == '"' || (c is '@' or '$' && IsStringPrefix(text, i)))
|
||||
{
|
||||
var verbatim = false;
|
||||
while (text[i] != '"')
|
||||
{
|
||||
if (text[i] == '@') verbatim = true;
|
||||
i++;
|
||||
}
|
||||
i++; // opening quote
|
||||
while (i < text.Length)
|
||||
{
|
||||
if (verbatim && text[i] == '"' && i + 1 < text.Length && text[i + 1] == '"') { i += 2; continue; }
|
||||
if (!verbatim && text[i] == '\\') { i += 2; continue; }
|
||||
if (text[i] == '"') { i++; break; }
|
||||
if (text[i] == '\n') sb.Append('\n');
|
||||
i++;
|
||||
}
|
||||
sb.Append("\"\"");
|
||||
}
|
||||
else if (c == '\'')
|
||||
{
|
||||
i++;
|
||||
while (i < text.Length && text[i] != '\'')
|
||||
{
|
||||
if (text[i] == '\\') i++;
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
sb.Append("''");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// @"..", $"..", $@"..", @$".." — a run of prefix characters followed by a quote.
|
||||
private static bool IsStringPrefix(string text, int i)
|
||||
{
|
||||
var j = i;
|
||||
while (j < text.Length && text[j] is '@' or '$') j++;
|
||||
return j < text.Length && text[j] == '"';
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Only_the_libplctag_adapters_reference_libplctag()
|
||||
{
|
||||
var root = RepoRoot();
|
||||
|
||||
var offenders = new List<string>();
|
||||
foreach (var file in Directory.EnumerateFiles(Path.Combine(root.FullName, "src"), "*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
if (file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}") ||
|
||||
file.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}")) continue;
|
||||
if (IsBuildOutput(file)) continue;
|
||||
var name = Path.GetFileName(file);
|
||||
var dir = Path.GetFileName(Path.GetDirectoryName(file)!);
|
||||
var allowed = name.StartsWith("Libplctag", StringComparison.Ordinal)
|
||||
&& dir is "ZB.MOM.WW.OtOpcUa.Driver.AbCip" or "ZB.MOM.WW.OtOpcUa.Driver.AbLegacy";
|
||||
var allowed = name.StartsWith("Libplctag", StringComparison.Ordinal) && AllowedSourceDirs.Contains(dir);
|
||||
if (allowed) continue;
|
||||
var lines = File.ReadAllLines(file);
|
||||
var raw = File.ReadAllLines(file);
|
||||
var lines = StripCommentsAndLiterals(File.ReadAllText(file)).Split('\n');
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var code = lines[i].Split("//")[0];
|
||||
if (Reference.IsMatch(code)) offenders.Add($"{Path.GetRelativePath(root.FullName, file)}:{i + 1}: {lines[i].Trim()}");
|
||||
if (Reference.IsMatch(lines[i]))
|
||||
offenders.Add($"{Path.GetRelativePath(root.FullName, file)}:{i + 1}: {(i < raw.Length ? raw[i].Trim() : lines[i].Trim())}");
|
||||
}
|
||||
}
|
||||
offenders.ShouldBeEmpty(string.Join(Environment.NewLine, offenders));
|
||||
}
|
||||
|
||||
/// <summary>#569: the libplctag PACKAGE may be referenced only by the two drivers that still carry the adapters.</summary>
|
||||
[Fact]
|
||||
public void Only_the_two_AB_driver_projects_reference_the_libplctag_package()
|
||||
{
|
||||
var root = RepoRoot();
|
||||
var offenders = new List<string>();
|
||||
foreach (var top in new[] { "src", "tests" })
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(Path.Combine(root.FullName, top), "*.csproj", SearchOption.AllDirectories))
|
||||
{
|
||||
if (IsBuildOutput(file)) continue;
|
||||
if (AllowedPackageProjects.Contains(Path.GetFileName(file))) continue;
|
||||
if (PackageRef.IsMatch(File.ReadAllText(file))) offenders.Add(Path.GetRelativePath(root.FullName, file));
|
||||
}
|
||||
}
|
||||
offenders.ShouldBeEmpty("only the AbCip / AbLegacy driver projects may reference the libplctag package:" +
|
||||
Environment.NewLine + string.Join(Environment.NewLine, offenders));
|
||||
}
|
||||
|
||||
/// <summary>The csproj regex matches the shapes a libplctag package reference can take (positive control).</summary>
|
||||
/// <param name="xml">A package reference element.</param>
|
||||
[Theory]
|
||||
[InlineData("<PackageReference Include=\"libplctag\"/>")]
|
||||
[InlineData("<PackageReference Include=\"libplctag\" Version=\"1.5.2\" />")]
|
||||
[InlineData("<PackageReference Update='libplctag'/>")]
|
||||
[InlineData("<PackageReference Include=\"libplctag.NativeImport\"/>")]
|
||||
public void Csproj_package_references_trip_the_guard(string xml) => PackageRef.IsMatch(xml).ShouldBeTrue(xml);
|
||||
|
||||
/// <summary>An unrelated package that merely starts with the same letters does not trip it.</summary>
|
||||
[Fact]
|
||||
public void Unrelated_packages_do_not_trip_the_csproj_guard() =>
|
||||
PackageRef.IsMatch("<PackageReference Include=\"libplctagger\"/>").ShouldBeFalse();
|
||||
|
||||
/// <summary>A reference AFTER a string literal containing <c>//</c> is still seen (#569 item 1).</summary>
|
||||
[Fact]
|
||||
public void A_reference_after_a_url_literal_is_not_hidden()
|
||||
{
|
||||
var code = StripCommentsAndLiterals("var u = \"http://example\"; var t = new libplctag.Tag();");
|
||||
Reference.IsMatch(code).ShouldBeTrue(code);
|
||||
}
|
||||
|
||||
/// <summary>A libplctag mention inside a string literal is text, not a reference.</summary>
|
||||
[Fact]
|
||||
public void A_reference_inside_a_string_literal_is_ignored()
|
||||
{
|
||||
Reference.IsMatch(StripCommentsAndLiterals("var s = \"using libplctag;\";")).ShouldBeFalse();
|
||||
Reference.IsMatch(StripCommentsAndLiterals("var s = @\"a \"\"quoted\"\" libplctag.Tag\";")).ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Block comments are stripped; code after them on the same line is kept (#569 item 2).</summary>
|
||||
[Fact]
|
||||
public void Block_comments_are_stripped_but_code_after_them_is_not()
|
||||
{
|
||||
Reference.IsMatch(StripCommentsAndLiterals("/* using libplctag; */ var x = 1;")).ShouldBeFalse();
|
||||
Reference.IsMatch(StripCommentsAndLiterals("/* a\n using libplctag;\n b */ var x = 1;")).ShouldBeFalse();
|
||||
Reference.IsMatch(StripCommentsAndLiterals("/* c */ var t = new libplctag.Tag();")).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Stripping keeps the line count, so offender line numbers stay accurate.</summary>
|
||||
[Fact]
|
||||
public void Stripping_preserves_line_numbers()
|
||||
{
|
||||
var src = "a\n/* x\ny */\nvar s = @\"p\nq\";\nusing libplctag;\n";
|
||||
var stripped = StripCommentsAndLiterals(src).Split('\n');
|
||||
stripped.Length.ShouldBe(src.Split('\n').Length);
|
||||
Reference.IsMatch(stripped[5]).ShouldBeTrue(string.Join("|", stripped));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prose_mentions_of_libplctag_do_not_trip_the_guard()
|
||||
{
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using System.Globalization;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag.NativeImport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
@@ -9,27 +9,6 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipStatusMapperTests
|
||||
{
|
||||
/// <summary>Verifies that CIP general status codes are mapped to OPC UA status codes.</summary>
|
||||
/// <param name="status">The raw CIP general status byte.</param>
|
||||
/// <param name="expected">The expected OPC UA status code.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)0x00, AbCipStatusMapper.Good)]
|
||||
[InlineData((byte)0x04, AbCipStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData((byte)0x05, AbCipStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData((byte)0x06, AbCipStatusMapper.GoodMoreData)]
|
||||
[InlineData((byte)0x08, AbCipStatusMapper.BadNotSupported)]
|
||||
[InlineData((byte)0x0A, AbCipStatusMapper.BadOutOfRange)]
|
||||
[InlineData((byte)0x13, AbCipStatusMapper.BadOutOfRange)]
|
||||
[InlineData((byte)0x0B, AbCipStatusMapper.Good)]
|
||||
[InlineData((byte)0x0E, AbCipStatusMapper.BadNotWritable)]
|
||||
[InlineData((byte)0x10, AbCipStatusMapper.BadDeviceFailure)]
|
||||
[InlineData((byte)0x16, AbCipStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData((byte)0xFF, AbCipStatusMapper.BadInternalError)]
|
||||
public void MapCipGeneralStatus_maps_known_codes(byte status, uint expected)
|
||||
{
|
||||
AbCipStatusMapper.MapCipGeneralStatus(status).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -77,6 +77,38 @@ public sealed class AbCipTypeMismatchTests
|
||||
MismatchWarnings(log).ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>#585: the libplctag-style width-mismatch path (status Ok, but the buffer is not the authored
|
||||
/// type's width — a REAL authored as INT) gets the same treatment as the native TypeMismatch status.</summary>
|
||||
[Fact]
|
||||
public async Task A_width_mismatched_read_keeps_the_handle_leaves_health_alone_and_warns_once()
|
||||
{
|
||||
var creates = 0;
|
||||
var factory = new FakeAbCipTagFactory
|
||||
{
|
||||
Customise = p => { Interlocked.Increment(ref creates); return new FakeAbCipTag(p) { Value = 1, Size = 4 }; },
|
||||
};
|
||||
var log = new CapturingLogger();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Host)],
|
||||
RawTags = AbCipRawTags.From(new AbCipTagDefinition("T", Host, "T", AbCipDataType.Int)),
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-585", factory, logger: log);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var health = drv.GetHealth();
|
||||
|
||||
for (var n = 0; n < Polls; n++)
|
||||
(await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode
|
||||
.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
||||
|
||||
Volatile.Read(ref creates).ShouldBe(1);
|
||||
factory.Tags["T"].Disposed.ShouldBeFalse();
|
||||
factory.Tags["T"].ReadCount.ShouldBe(Polls); // positive control: every poll did reach the runtime
|
||||
drv.GetHealth().ShouldBe(health);
|
||||
drv.GetHealth().State.ShouldNotBe(DriverState.Degraded);
|
||||
MismatchWarnings(log).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_type_mismatched_write_keeps_the_handle_leaves_health_alone_and_warns_once()
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag.NativeImport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
@@ -39,6 +39,66 @@ public sealed class LibplctagStatusTranslationTests
|
||||
LibplctagStatusTranslation.ToAbStatus(new LibPlcTagException("gibberish")).ShouldBe(AbStatus.BadConnection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #568: the message-parse is pinned against the REAL exception libplctag builds from a <see cref="Status"/>
|
||||
/// (not a hand-typed message), for every defined status — if a libplctag bump changes the message format
|
||||
/// the translation would silently degrade to BadConnection, and this fails instead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_real_LibPlcTagException_built_from_a_Status_round_trips_through_the_message_parse()
|
||||
{
|
||||
foreach (var status in Enum.GetValues<Status>())
|
||||
{
|
||||
if (status == Status.Ok || status == Status.Pending) continue; // libplctag throws only on errors
|
||||
var ex = new LibPlcTagException(status);
|
||||
LibplctagStatusTranslation.ToAbStatus(ex).ShouldBe(LibplctagStatusTranslation.ToAbStatus(status), $"{status}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>#568: a cancellation is not a libplctag failure — WrapSync must not translate or swallow it.</summary>
|
||||
[Fact]
|
||||
public void WrapSync_passes_OperationCanceledException_through()
|
||||
{
|
||||
Should.Throw<OperationCanceledException>(
|
||||
() => LibplctagStatusTranslation.WrapSync<int>(() => throw new OperationCanceledException()));
|
||||
Should.Throw<OperationCanceledException>(
|
||||
() => LibplctagStatusTranslation.WrapSync(() => throw new OperationCanceledException()));
|
||||
}
|
||||
|
||||
/// <summary>WrapSync translates a libplctag failure and returns a successful result unchanged.</summary>
|
||||
[Fact]
|
||||
public void WrapSync_translates_libplctag_failures_and_returns_results()
|
||||
{
|
||||
LibplctagStatusTranslation.WrapSync(() => 42).ShouldBe(42);
|
||||
var ex = Should.Throw<AbTransportException>(
|
||||
() => LibplctagStatusTranslation.WrapSync<int>(() => throw new LibPlcTagException("ErrorTimeout")));
|
||||
ex.Status.ShouldBe(AbStatus.Timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #568: the ADAPTER (not just the helper) routes decode through WrapSync. A real, never-initialised native
|
||||
/// <c>Tag</c> (no network I/O — nothing was created) throws a libplctag exception on a decode, and the runtime
|
||||
/// must surface it as an <see cref="AbTransportException"/>. Remove the WrapSync around the decode and this
|
||||
/// sees a raw <see cref="LibPlcTagException"/> (or <see cref="InvalidOperationException"/>) instead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LibplctagTagRuntime_decode_on_an_unread_tag_surfaces_AbTransportException()
|
||||
{
|
||||
var rt = new LibplctagTagRuntime(new AbCipTagCreateParams(
|
||||
Gateway: "127.0.0.1", Port: 1, CipPath: "1,0", LibplctagPlcAttribute: "controllogix",
|
||||
TagName: "NeverInitialised", Timeout: TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
Should.Throw<AbTransportException>(() => rt.DecodeValueAt(AbCipDataType.DInt, 0, null));
|
||||
}
|
||||
|
||||
/// <summary>Wrap passes a cancellation through untranslated.</summary>
|
||||
[Fact]
|
||||
public async Task Wrap_passes_OperationCanceledException_through()
|
||||
{
|
||||
await Should.ThrowAsync<OperationCanceledException>(
|
||||
() => LibplctagStatusTranslation.Wrap(() => throw new OperationCanceledException()));
|
||||
}
|
||||
|
||||
/// <summary>Wrap rethrows as a transport exception keeping the original as InnerException.</summary>
|
||||
[Fact]
|
||||
public async Task Wrap_rethrows_as_AbTransportException_with_inner()
|
||||
|
||||
@@ -49,23 +49,6 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
new AbLegacyHostAddress("10.0.0.5", 2222, "1,0").ToString().ShouldBe("ab://10.0.0.5:2222/1,0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that PCCC status codes are mapped to OPC UA status codes correctly.</summary>
|
||||
/// <param name="sts">The PCCC status byte to map.</param>
|
||||
/// <param name="expected">The expected OPC UA status code.</param>
|
||||
[Theory]
|
||||
[InlineData((byte)0x00, AbLegacyStatusMapper.Good)]
|
||||
[InlineData((byte)0x10, AbLegacyStatusMapper.BadNotSupported)]
|
||||
[InlineData((byte)0x20, AbLegacyStatusMapper.BadNodeIdUnknown)]
|
||||
[InlineData((byte)0x30, AbLegacyStatusMapper.BadNotWritable)]
|
||||
[InlineData((byte)0x40, AbLegacyStatusMapper.BadDeviceFailure)]
|
||||
[InlineData((byte)0x50, AbLegacyStatusMapper.BadDeviceFailure)]
|
||||
[InlineData((byte)0xF0, AbLegacyStatusMapper.BadInternalError)]
|
||||
[InlineData((byte)0xFF, AbLegacyStatusMapper.BadCommunicationError)]
|
||||
public void PcccStatus_maps_known_codes(byte sts, uint expected)
|
||||
{
|
||||
AbLegacyStatusMapper.MapPcccStatus(sts).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -92,16 +75,6 @@ public sealed class AbLegacyHostAndStatusTests
|
||||
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
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag.NativeImport;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
Reference in New Issue
Block a user