Auto: ablegacy-4 — indirect/indexed addressing parser

Closes #247
This commit is contained in:
Joseph Doherty
2026-04-25 13:51:03 -04:00
parent b95eaacc05
commit 4ff4cc5899
3 changed files with 246 additions and 14 deletions

View File

@@ -205,6 +205,121 @@ public sealed class AbLegacyAddressTests
AbLegacyPlcFamilyProfile.LogixPccc.SupportsFunctionFiles.ShouldBeFalse();
}
// ---- Indirect / indexed addressing (Issue #247) ----
//
// PLC-5 / SLC permit `N7:[N7:0]` (word number sourced from another address) and
// `N[N7:0]:5` (file number sourced from another address). Recursion is capped at 1 — the
// inner address must itself be a plain direct PCCC reference.
[Fact]
public void TryParse_accepts_indirect_word_source()
{
var a = AbLegacyAddress.TryParse("N7:[N7:0]");
a.ShouldNotBeNull();
a.FileLetter.ShouldBe("N");
a.FileNumber.ShouldBe(7);
a.IndirectFileSource.ShouldBeNull();
a.IndirectWordSource.ShouldNotBeNull();
a.IndirectWordSource!.FileLetter.ShouldBe("N");
a.IndirectWordSource.FileNumber.ShouldBe(7);
a.IndirectWordSource.WordNumber.ShouldBe(0);
a.IsIndirect.ShouldBeTrue();
}
[Fact]
public void TryParse_accepts_indirect_file_source()
{
var a = AbLegacyAddress.TryParse("N[N7:0]:5");
a.ShouldNotBeNull();
a.FileLetter.ShouldBe("N");
a.FileNumber.ShouldBeNull();
a.WordNumber.ShouldBe(5);
a.IndirectFileSource.ShouldNotBeNull();
a.IndirectFileSource!.FileLetter.ShouldBe("N");
a.IndirectFileSource.FileNumber.ShouldBe(7);
a.IndirectFileSource.WordNumber.ShouldBe(0);
a.IndirectWordSource.ShouldBeNull();
a.IsIndirect.ShouldBeTrue();
}
[Fact]
public void TryParse_accepts_both_indirect_file_and_word()
{
var a = AbLegacyAddress.TryParse("N[N7:0]:[N7:1]");
a.ShouldNotBeNull();
a.IndirectFileSource.ShouldNotBeNull();
a.IndirectWordSource.ShouldNotBeNull();
a.IndirectWordSource!.WordNumber.ShouldBe(1);
}
[Theory]
[InlineData("N[N[N7:0]:0]:5")] // depth-2 file source
[InlineData("N7:[N[N7:0]:0]")] // depth-2 word source
[InlineData("N7:[N7:[N7:0]]")] // depth-2 word source (nested word)
public void TryParse_rejects_depth_greater_than_one(string input)
{
AbLegacyAddress.TryParse(input).ShouldBeNull();
}
[Theory]
[InlineData("N7:[")] // unbalanced bracket
[InlineData("N7:]")] // unbalanced bracket
[InlineData("N[:5")] // empty inner file source
[InlineData("N7:[]")] // empty inner word source
[InlineData("N[X9:0]:5")] // unknown file letter inside
public void TryParse_rejects_malformed_indirect(string input)
{
AbLegacyAddress.TryParse(input).ShouldBeNull();
}
[Fact]
public void ToLibplctagName_reemits_indirect_word_source()
{
var a = AbLegacyAddress.TryParse("N7:[N7:0]");
a.ShouldNotBeNull();
a.ToLibplctagName().ShouldBe("N7:[N7:0]");
}
[Fact]
public void ToLibplctagName_reemits_indirect_file_source()
{
var a = AbLegacyAddress.TryParse("N[N7:0]:5");
a.ShouldNotBeNull();
a.ToLibplctagName().ShouldBe("N[N7:0]:5");
}
[Fact]
public void TryParse_indirect_with_bit_outside_brackets()
{
// Outer bit applies to the resolved word; inner address is still depth-1.
var a = AbLegacyAddress.TryParse("N7:[N7:0]/3");
a.ShouldNotBeNull();
a.BitIndex.ShouldBe(3);
a.IndirectWordSource.ShouldNotBeNull();
a.ToLibplctagName().ShouldBe("N7:[N7:0]/3");
}
[Fact]
public void TryParse_Plc5_indirect_inner_address_obeys_octal()
{
// Inner I:/O: indices on PLC-5 must obey octal rules even when nested in brackets.
var a = AbLegacyAddress.TryParse("N7:[I:010/10]", AbLegacyPlcFamily.Plc5);
a.ShouldNotBeNull();
a.IndirectWordSource.ShouldNotBeNull();
a.IndirectWordSource!.WordNumber.ShouldBe(8); // octal 010 → 8
a.IndirectWordSource.BitIndex.ShouldBe(8); // octal 10 → 8
// Octal-illegal digit '8' inside an inner I: address is rejected on PLC-5.
AbLegacyAddress.TryParse("N7:[I:8/0]", AbLegacyPlcFamily.Plc5).ShouldBeNull();
}
[Fact]
public void TryParse_indirect_inner_cannot_itself_be_indirect()
{
AbLegacyAddress.TryParse("N7:[N7:[N7:0]]").ShouldBeNull();
AbLegacyAddress.TryParse("N[N[N7:0]:5]:5").ShouldBeNull();
}
[Theory]
[InlineData("RTC", "HR", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)]
[InlineData("RTC", "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)]