fix(abcip): accept module-defined I/O tag paths (Local:1:I.Data, ABB_OmniCore:I.X)

Live read-only check: a production CompactLogix lists the module tag ABB_OmniCore:I in
@tags and libplctag reads ABB_OmniCore:I.ConnectionFaulted without complaint, but the
driver refused the path as malformed (identifier rule had no colon) and reported the
tag BadCommunicationError. Colon-separated identifier/slot parts are now legal in the
controller-scope base segment only (never past it, never under Program: scope).
This commit is contained in:
Joseph Doherty
2026-10-07 14:07:49 -04:00
parent 28bbb5e5e2
commit a0a024c80b
2 changed files with 58 additions and 1 deletions
@@ -128,6 +128,41 @@ public sealed class AbCipTagPathTests
AbCipTagPath.TryParse(input).ShouldBeNull();
}
/// <summary>
/// Module-defined I/O tags carry colons in their base name (<c>Local:1:I</c>,
/// <c>ABB_OmniCore:I</c>). Seen live: a CompactLogix lists <c>ABB_OmniCore:I</c> in
/// <c>@tags</c>, libplctag reads <c>ABB_OmniCore:I.ConnectionFaulted</c> fine, and the driver
/// refused it as a malformed path.
/// </summary>
/// <param name="input">A module tag path.</param>
/// <param name="baseName">The expected base (first segment) name.</param>
[Theory]
[InlineData("ABB_OmniCore:I.ConnectionFaulted", "ABB_OmniCore:I")]
[InlineData("Local:1:I.Data", "Local:1:I")]
[InlineData("Local:2:O.Data.3", "Local:2:O")]
[InlineData("Rack1:3:C.Ch0Config.RangeType", "Rack1:3:C")]
public void Module_IO_tag_base_names_with_colons_parse_and_round_trip(string input, string baseName)
{
var parsed = AbCipTagPath.TryParse(input);
parsed.ShouldNotBeNull();
parsed.ProgramScope.ShouldBeNull();
parsed.Segments[0].Name.ShouldBe(baseName);
parsed.ToLibplctagName().ShouldBe(input);
}
/// <summary>A colon is only legal in the base name, and never as a dangling or doubled separator.</summary>
/// <param name="input">A malformed path.</param>
[Theory]
[InlineData("Motor.Sub:I")] // colon past the base segment
[InlineData("Local::I.Data")] // empty colon part
[InlineData("Local:1:.Data")] // trailing colon
[InlineData(":Local.Data")] // leading colon
[InlineData("Program:P.Local:1:I")] // module tags are controller-scope only
public void Colons_outside_a_module_base_name_are_rejected(string input)
{
AbCipTagPath.TryParse(input).ShouldBeNull();
}
/// <summary>Verifies that identifiers with underscores are accepted.</summary>
[Fact]
public void Ident_with_underscore_accepted()