using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; [Trait("Category", "Unit")] public sealed class AbLegacyAddressTests { [Theory] [InlineData("N7:0", "N", 7, 0, null, null)] [InlineData("N7:15", "N", 7, 15, null, null)] [InlineData("F8:5", "F", 8, 5, null, null)] [InlineData("B3:0/0", "B", 3, 0, 0, null)] [InlineData("B3:2/7", "B", 3, 2, 7, null)] [InlineData("ST9:0", "ST", 9, 0, null, null)] [InlineData("L9:3", "L", 9, 3, null, null)] [InlineData("I:0/0", "I", null, 0, 0, null)] [InlineData("O:1/2", "O", null, 1, 2, null)] [InlineData("S:1", "S", null, 1, null, null)] [InlineData("T4:0.ACC", "T", 4, 0, null, "ACC")] [InlineData("T4:0.PRE", "T", 4, 0, null, "PRE")] [InlineData("C5:2.CU", "C", 5, 2, null, "CU")] [InlineData("R6:0.LEN", "R", 6, 0, null, "LEN")] [InlineData("N7:0/3", "N", 7, 0, 3, null)] public void TryParse_accepts_valid_pccc_addresses(string input, string letter, int? file, int word, int? bit, string? sub) { var a = AbLegacyAddress.TryParse(input); a.ShouldNotBeNull(); a.FileLetter.ShouldBe(letter); a.FileNumber.ShouldBe(file); a.WordNumber.ShouldBe(word); a.BitIndex.ShouldBe(bit); a.SubElement.ShouldBe(sub); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("N7")] // missing :word [InlineData(":0")] // missing file [InlineData("X7:0")] // unknown file letter [InlineData("N7:-1")] // negative word [InlineData("N7:abc")] // non-numeric word [InlineData("N7:0/-1")] // negative bit [InlineData("N7:0/32")] // bit out of range [InlineData("Nabc:0")] // non-numeric file number public void TryParse_rejects_invalid_forms(string? input) { AbLegacyAddress.TryParse(input).ShouldBeNull(); } [Theory] [InlineData("N7:0")] [InlineData("F8:5")] [InlineData("B3:0/0")] [InlineData("ST9:0")] [InlineData("T4:0.ACC")] [InlineData("I:0/0")] [InlineData("S:1")] public void ToLibplctagName_roundtrips(string input) { var a = AbLegacyAddress.TryParse(input); a.ShouldNotBeNull(); a.ToLibplctagName().ShouldBe(input); } // ---- PLC-5 octal I:/O: addressing (Issue #244) ---- // // RSLogix 5 displays I:/O: word + bit indices as octal. `I:001/17` means rack 1, bit 15 // (octal 17). Other PCCC families (SLC500, MicroLogix, LogixPccc) keep decimal indices. // Non-I/O file letters are always decimal regardless of family. [Theory] [InlineData("I:001/17", 1, 15)] // octal 17 → bit 15 [InlineData("I:0/0", 0, 0)] // boundary: octal 0 [InlineData("O:1/2", 1, 2)] // octal 1, 2 happen to match decimal [InlineData("I:010/10", 8, 8)] // octal 10 → 8 (both word + bit) [InlineData("I:007/7", 7, 7)] // boundary: largest single octal digit public void TryParse_Plc5_parses_io_indices_as_octal(string input, int expectedWord, int expectedBit) { var a = AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5); a.ShouldNotBeNull(); a.WordNumber.ShouldBe(expectedWord); a.BitIndex.ShouldBe(expectedBit); } [Theory] [InlineData("I:8/0")] // word digit 8 illegal in octal [InlineData("I:0/9")] // bit digit 9 illegal in octal [InlineData("O:128/0")] // contains digit 8 [InlineData("I:0/18")] // bit field octal-illegal because of '8' public void TryParse_Plc5_rejects_octal_invalid_io_digits(string input) { AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5).ShouldBeNull(); } [Theory] // Non-I/O files stay decimal even on PLC-5 (e.g. N7:8 is integer 7, word 8). [InlineData("N7:8", 7, 8)] [InlineData("F8:9", 8, 9)] public void TryParse_Plc5_keeps_non_io_indices_decimal(string input, int? expectedFile, int expectedWord) { var a = AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5); a.ShouldNotBeNull(); a.FileNumber.ShouldBe(expectedFile); a.WordNumber.ShouldBe(expectedWord); } [Fact] public void TryParse_Slc500_keeps_io_indices_decimal_back_compat() { // SLC500 has OctalIoAddressing=false — the digits are decimal as before. var a = AbLegacyAddress.TryParse("I:10/15", AbLegacyPlcFamily.Slc500); a.ShouldNotBeNull(); a.WordNumber.ShouldBe(10); a.BitIndex.ShouldBe(15); // Decimal '8' that PLC-5 would reject is fine on SLC500. var b = AbLegacyAddress.TryParse("I:8/0", AbLegacyPlcFamily.Slc500); b.ShouldNotBeNull(); b.WordNumber.ShouldBe(8); } [Fact] public void TryParse_MicroLogix_and_LogixPccc_keep_io_indices_decimal() { AbLegacyAddress.TryParse("I:9/0", AbLegacyPlcFamily.MicroLogix).ShouldNotBeNull(); AbLegacyAddress.TryParse("I:9/0", AbLegacyPlcFamily.LogixPccc).ShouldNotBeNull(); } [Fact] public void Plc5Profile_advertises_octal_io_addressing() { AbLegacyPlcFamilyProfile.Plc5.OctalIoAddressing.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Slc500.OctalIoAddressing.ShouldBeFalse(); AbLegacyPlcFamilyProfile.MicroLogix.OctalIoAddressing.ShouldBeFalse(); AbLegacyPlcFamilyProfile.LogixPccc.OctalIoAddressing.ShouldBeFalse(); } }