fix(abcip,ablegacy): pass a null libplctag path for an empty CIP route (Micro800 / unrouted MicroLogix)

The libplctag adapters assigned Tag.Path straight from the device's parsed
CipPath. A device address with no route (ab://host:44818/ - the normal
Micro800 form, and a MicroLogix with no route) parses to "", and libplctag
rejects an empty path attribute with ErrorBadData; omitting the attribute
(null) is what it accepts. So Micro800 could not connect on the default
libplctag transport.

This predates the native transport work and exists on master.

Add an internal LibplctagPath.Normalize helper per driver project (blank ->
null) and route all four Tag constructions through it (AbCip tag runtime,
symbol-list enumerator, template reader; AbLegacy tag runtime). The runtime
tag construction is extracted to an internal BuildTag so tests can inspect
Tag.Path without connecting. Tests fail with Normalize reverted to identity.
Update the Micro800 smoke-test comment: both transports are expected to pass.
This commit is contained in:
Joseph Doherty
2026-10-07 19:05:43 -04:00
parent c8b35663e5
commit afe6850f3d
9 changed files with 135 additions and 9 deletions
@@ -32,7 +32,9 @@ public sealed class AbCipReadSmokeTests
// smoke test was missed.
// Micro800 has no backplane: its cip-path is empty (the compose service runs
// ab_server --plc=Micro800 with no --path, which refuses a routed request). Every
// other family reaches slot 0 through `1,0`.
// other family reaches slot 0 through `1,0`. Both transports are expected to pass
// for Micro800: the libplctag adapters map the empty route to a null `path` attribute
// (LibplctagPath.Normalize) because libplctag rejects "" with ErrorBadData.
var cipPath = profile.Family == AbCipPlcFamily.Micro800 ? "" : "1,0";
var deviceUri = $"ab://{fixture.Host}:{fixture.Port}/{cipPath}";
var drv = new AbCipDriver(new AbCipDriverOptions
@@ -0,0 +1,51 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
/// <summary>
/// libplctag rejects an empty-string <c>path</c> with <c>ErrorBadData</c>; "no route" (Micro800, or any
/// unrouted device authored as <c>ab://host:44818/</c>) must reach it as null. Pre-dates the native transport.
/// </summary>
public sealed class LibplctagEmptyPathTests
{
private static AbCipTagCreateParams Params(string cipPath) =>
new(Gateway: "10.100.0.35", Port: 44818, CipPath: cipPath, LibplctagPlcAttribute: "micro800",
TagName: "TestDINT", Timeout: TimeSpan.FromSeconds(5));
[Theory]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData(null, null)]
[InlineData("1,0", "1,0")]
public void Normalize_maps_blank_to_null_and_keeps_a_route(string? input, string? expected) =>
LibplctagPath.Normalize(input).ShouldBe(expected);
[Fact]
public void The_data_tag_omits_the_path_for_an_empty_route()
{
using var tag = LibplctagTagRuntime.BuildTag(Params(""));
tag.Path.ShouldBeNull();
}
[Fact]
public void The_data_tag_keeps_a_real_route()
{
using var tag = LibplctagTagRuntime.BuildTag(Params("1,0"));
tag.Path.ShouldBe("1,0");
}
[Fact]
public void The_symbol_list_tag_omits_the_path_for_an_empty_route()
{
using var tag = LibplctagListingReader.BuildSymbolListTag(Params(""));
tag.Path.ShouldBeNull();
}
[Fact]
public void The_template_tag_omits_the_path_for_an_empty_route()
{
using var tag = LibplctagTemplateReader.BuildTemplateTag(Params(""), templateInstanceId: 1);
tag.Path.ShouldBeNull();
}
}
@@ -0,0 +1,37 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
/// <summary>
/// libplctag rejects an empty-string <c>path</c> with <c>ErrorBadData</c>; an unrouted MicroLogix
/// (<c>ab://host:44818/</c>) must reach it as null.
/// </summary>
public sealed class LibplctagEmptyPathTests
{
private static AbLegacyTagCreateParams Params(string cipPath) =>
new(Gateway: "10.100.0.35", Port: 44818, CipPath: cipPath, LibplctagPlcAttribute: "micrologix",
TagName: "N7:0", Timeout: TimeSpan.FromSeconds(5));
[Theory]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData(null, null)]
[InlineData("1,0", "1,0")]
public void Normalize_maps_blank_to_null_and_keeps_a_route(string? input, string? expected) =>
LibplctagPath.Normalize(input).ShouldBe(expected);
[Fact]
public void The_data_tag_omits_the_path_for_an_empty_route()
{
using var tag = LibplctagLegacyTagRuntime.BuildTag(Params(""));
tag.Path.ShouldBeNull();
}
[Fact]
public void The_data_tag_keeps_a_real_route()
{
using var tag = LibplctagLegacyTagRuntime.BuildTag(Params("1,0"));
tag.Path.ShouldBe("1,0");
}
}