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(); } // ---- MicroLogix function-file letters (Issue #245) ---- // // MicroLogix 1100/1400 expose RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI function files. Other // PCCC families (SLC500 / PLC-5 / LogixPccc) reject those file letters. [Theory] [InlineData("RTC:0.HR", "RTC", "HR")] [InlineData("RTC:0.MIN", "RTC", "MIN")] [InlineData("RTC:0.YR", "RTC", "YR")] [InlineData("HSC:0.ACC", "HSC", "ACC")] [InlineData("HSC:0.PRE", "HSC", "PRE")] [InlineData("HSC:0.EN", "HSC", "EN")] [InlineData("DLS:0.STR", "DLS", "STR")] [InlineData("PTO:0.OF", "PTO", "OF")] [InlineData("PWM:0.EN", "PWM", "EN")] [InlineData("STI:0.SPM", "STI", "SPM")] [InlineData("EII:0.PFN", "EII", "PFN")] [InlineData("MMI:0.FT", "MMI", "FT")] [InlineData("BHI:0.OS", "BHI", "OS")] [InlineData("IOS:0.ID", "IOS", "ID")] public void TryParse_MicroLogix_accepts_function_files(string input, string expectedLetter, string expectedSub) { var a = AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.MicroLogix); a.ShouldNotBeNull(); a.FileLetter.ShouldBe(expectedLetter); a.SubElement.ShouldBe(expectedSub); } [Theory] [InlineData("RTC:0.HR")] [InlineData("HSC:0.ACC")] [InlineData("PTO:0.OF")] [InlineData("BHI:0.OS")] public void TryParse_Slc500_rejects_function_files(string input) { AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Slc500).ShouldBeNull(); } [Theory] [InlineData("RTC:0.HR")] [InlineData("HSC:0.ACC")] public void TryParse_Plc5_and_LogixPccc_reject_function_files(string input) { AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5).ShouldBeNull(); AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.LogixPccc).ShouldBeNull(); } [Fact] public void TryParse_Default_overload_rejects_function_files() { // Without a family the parser cannot allow MicroLogix-only letters — back-compat with // the family-less overload from before #244. AbLegacyAddress.TryParse("RTC:0.HR").ShouldBeNull(); AbLegacyAddress.TryParse("HSC:0.ACC").ShouldBeNull(); } [Fact] public void MicroLogixProfile_advertises_function_file_support() { AbLegacyPlcFamilyProfile.MicroLogix.SupportsFunctionFiles.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Slc500.SupportsFunctionFiles.ShouldBeFalse(); AbLegacyPlcFamilyProfile.Plc5.SupportsFunctionFiles.ShouldBeFalse(); 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)] [InlineData("HSC", "ACC", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] [InlineData("HSC", "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData("DLS", "STR", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] [InlineData("DLS", "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData("PWM", "OUT", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] public void FunctionFile_subelement_catalogue_maps_to_expected_driver_type( string letter, string sub, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType expected) { AbLegacyFunctionFile.SubElementType(letter, sub).ShouldBe(expected); } // ---- Structure files PD/MG/PLS/BT (Issue #248) ---- // // PD (PID), MG (Message), PLS (Programmable Limit Switch), BT (Block Transfer) — accepted on // SLC500 + PLC-5 for PD/MG, PLC-5 only for PLS/BT. MicroLogix and LogixPccc reject all four. [Theory] [InlineData("PD10:0.SP")] [InlineData("PD10:0.PV")] [InlineData("PD10:0.KP")] [InlineData("PD10:0.EN")] [InlineData("MG11:0.LEN")] [InlineData("MG11:0.DN")] public void TryParse_Slc500_accepts_pd_and_mg(string input) { AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Slc500).ShouldNotBeNull(); } [Theory] [InlineData("PLS12:0.LEN")] [InlineData("BT13:0.RLEN")] [InlineData("BT13:0.EN")] public void TryParse_Slc500_rejects_pls_and_bt(string input) { // PLS/BT are PLC-5 only; SLC500 must reject. AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Slc500).ShouldBeNull(); } [Theory] [InlineData("PD10:0.KP", "PD", "KP")] [InlineData("MG11:0.EN", "MG", "EN")] [InlineData("PLS12:0.LEN", "PLS", "LEN")] [InlineData("BT13:0.RLEN", "BT", "RLEN")] [InlineData("BT13:0.DN", "BT", "DN")] public void TryParse_Plc5_accepts_all_structure_files(string input, string letter, string sub) { var a = AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5); a.ShouldNotBeNull(); a.FileLetter.ShouldBe(letter); a.SubElement.ShouldBe(sub); } [Theory] [InlineData("PD10:0.SP")] [InlineData("MG11:0.LEN")] [InlineData("PLS12:0.LEN")] [InlineData("BT13:0.RLEN")] public void TryParse_MicroLogix_rejects_all_structure_files(string input) { AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.MicroLogix).ShouldBeNull(); } [Theory] [InlineData("PD10:0.SP")] [InlineData("MG11:0.LEN")] [InlineData("PLS12:0.LEN")] [InlineData("BT13:0.RLEN")] public void TryParse_LogixPccc_rejects_all_structure_files(string input) { AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.LogixPccc).ShouldBeNull(); } [Fact] public void TryParse_Default_overload_rejects_structure_files() { // Without a family the parser cannot allow structure-file letters. AbLegacyAddress.TryParse("PD10:0.SP").ShouldBeNull(); AbLegacyAddress.TryParse("MG11:0.LEN").ShouldBeNull(); AbLegacyAddress.TryParse("PLS12:0.LEN").ShouldBeNull(); AbLegacyAddress.TryParse("BT13:0.RLEN").ShouldBeNull(); } [Fact] public void Profiles_advertise_structure_file_support_per_family() { AbLegacyPlcFamilyProfile.Slc500.SupportsPidFile.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Slc500.SupportsMessageFile.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Slc500.SupportsPlsFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.Slc500.SupportsBlockTransferFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.Plc5.SupportsPidFile.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Plc5.SupportsMessageFile.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Plc5.SupportsPlsFile.ShouldBeTrue(); AbLegacyPlcFamilyProfile.Plc5.SupportsBlockTransferFile.ShouldBeTrue(); AbLegacyPlcFamilyProfile.MicroLogix.SupportsPidFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.MicroLogix.SupportsMessageFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.MicroLogix.SupportsPlsFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.MicroLogix.SupportsBlockTransferFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.LogixPccc.SupportsPidFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.LogixPccc.SupportsMessageFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.LogixPccc.SupportsPlsFile.ShouldBeFalse(); AbLegacyPlcFamilyProfile.LogixPccc.SupportsBlockTransferFile.ShouldBeFalse(); } [Theory] // PID Float members. [InlineData(AbLegacyDataType.PidElement, "SP", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] [InlineData(AbLegacyDataType.PidElement, "PV", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] [InlineData(AbLegacyDataType.PidElement, "KP", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] [InlineData(AbLegacyDataType.PidElement, "KI", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] [InlineData(AbLegacyDataType.PidElement, "KD", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] [InlineData(AbLegacyDataType.PidElement, "OUT", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] // PID status bits. [InlineData(AbLegacyDataType.PidElement, "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData(AbLegacyDataType.PidElement, "DN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData(AbLegacyDataType.PidElement, "MO", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData(AbLegacyDataType.PidElement, "PE", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] // MG Int32 control words. [InlineData(AbLegacyDataType.MessageElement, "RBE", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] [InlineData(AbLegacyDataType.MessageElement, "LEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] // MG status bits. [InlineData(AbLegacyDataType.MessageElement, "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData(AbLegacyDataType.MessageElement, "DN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData(AbLegacyDataType.MessageElement, "TO", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] // PLS LEN. [InlineData(AbLegacyDataType.PlsElement, "LEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] // BT control words + status bits. [InlineData(AbLegacyDataType.BlockTransferElement, "RLEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] [InlineData(AbLegacyDataType.BlockTransferElement, "DLEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] [InlineData(AbLegacyDataType.BlockTransferElement, "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] [InlineData(AbLegacyDataType.BlockTransferElement, "DN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] public void Structure_subelements_resolve_to_expected_driver_type( AbLegacyDataType type, string sub, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType expected) { AbLegacyDataTypeExtensions.EffectiveDriverDataType(type, sub).ShouldBe(expected); } [Theory] // PD bits in word 0. [InlineData(AbLegacyDataType.PidElement, "EN", 0)] [InlineData(AbLegacyDataType.PidElement, "PE", 1)] [InlineData(AbLegacyDataType.PidElement, "DN", 2)] [InlineData(AbLegacyDataType.PidElement, "MO", 3)] // MG/BT share the same 8..15 layout. [InlineData(AbLegacyDataType.MessageElement, "TO", 8)] [InlineData(AbLegacyDataType.MessageElement, "EN", 15)] [InlineData(AbLegacyDataType.BlockTransferElement, "TO", 8)] [InlineData(AbLegacyDataType.BlockTransferElement, "EN", 15)] public void Structure_status_bit_indices_match_rockwell( AbLegacyDataType type, string sub, int expectedBit) { AbLegacyDataTypeExtensions.StatusBitIndex(type, sub).ShouldBe(expectedBit); } [Theory] // PD: PE + DN + SP_VAL/SP_LL/SP_HL are PLC-set (read-only); EN + MO + AUTO + MAN are // operator-controllable. [InlineData(AbLegacyDataType.PidElement, "PE", true)] [InlineData(AbLegacyDataType.PidElement, "DN", true)] [InlineData(AbLegacyDataType.PidElement, "SP_VAL", true)] [InlineData(AbLegacyDataType.PidElement, "EN", false)] [InlineData(AbLegacyDataType.PidElement, "MO", false)] // MG/BT: ST/DN/ER/CO/EW/NR/TO are PLC-set; EN is operator-driven. [InlineData(AbLegacyDataType.MessageElement, "DN", true)] [InlineData(AbLegacyDataType.MessageElement, "ER", true)] [InlineData(AbLegacyDataType.MessageElement, "TO", true)] [InlineData(AbLegacyDataType.MessageElement, "EN", false)] [InlineData(AbLegacyDataType.BlockTransferElement, "DN", true)] [InlineData(AbLegacyDataType.BlockTransferElement, "EN", false)] public void Structure_plc_set_status_bits_are_marked_read_only( AbLegacyDataType type, string sub, bool expected) { AbLegacyDataTypeExtensions.IsPlcSetStatusBit(type, sub).ShouldBe(expected); } }