337 lines
13 KiB
C#
337 lines
13 KiB
C#
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);
|
|
}
|
|
}
|