using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests; [Trait("Category", "Unit")] public sealed class S7AddressParserTests { // --- Data blocks --- [Theory] [InlineData("DB1.DBX0.0", 1, S7Size.Bit, 0, 0)] [InlineData("DB1.DBX0.7", 1, S7Size.Bit, 0, 7)] [InlineData("DB1.DBB0", 1, S7Size.Byte, 0, 0)] [InlineData("DB1.DBW0", 1, S7Size.Word, 0, 0)] [InlineData("DB1.DBD4", 1, S7Size.DWord, 4, 0)] [InlineData("DB10.DBW100", 10, S7Size.Word, 100, 0)] [InlineData("DB1.DBX15.3", 1, S7Size.Bit, 15, 3)] public void Parse_data_block_addresses(string input, int db, S7Size size, int byteOff, int bitOff) { var r = S7AddressParser.Parse(input); r.Area.ShouldBe(S7Area.DataBlock); r.DbNumber.ShouldBe(db); r.Size.ShouldBe(size); r.ByteOffset.ShouldBe(byteOff); r.BitOffset.ShouldBe(bitOff); } [Theory] [InlineData("db1.dbw0", 1, S7Size.Word, 0)] [InlineData(" DB1.DBW0 ", 1, S7Size.Word, 0)] // trim whitespace public void Parse_is_case_insensitive_and_trims(string input, int db, S7Size size, int off) { var r = S7AddressParser.Parse(input); r.Area.ShouldBe(S7Area.DataBlock); r.DbNumber.ShouldBe(db); r.Size.ShouldBe(size); r.ByteOffset.ShouldBe(off); } // --- M / I / Q --- [Theory] [InlineData("MB0", S7Area.Memory, S7Size.Byte, 0, 0)] [InlineData("MW10", S7Area.Memory, S7Size.Word, 10, 0)] [InlineData("MD4", S7Area.Memory, S7Size.DWord, 4, 0)] [InlineData("M0.0", S7Area.Memory, S7Size.Bit, 0, 0)] [InlineData("M255.7", S7Area.Memory, S7Size.Bit, 255, 7)] [InlineData("IB0", S7Area.Input, S7Size.Byte, 0, 0)] [InlineData("IW0", S7Area.Input, S7Size.Word, 0, 0)] [InlineData("I0.0", S7Area.Input, S7Size.Bit, 0, 0)] [InlineData("QB0", S7Area.Output, S7Size.Byte, 0, 0)] [InlineData("QW0", S7Area.Output, S7Size.Word, 0, 0)] [InlineData("Q0.0", S7Area.Output, S7Size.Bit, 0, 0)] [InlineData("QD4", S7Area.Output, S7Size.DWord, 4, 0)] public void Parse_MIQ_addresses(string input, S7Area area, S7Size size, int byteOff, int bitOff) { var r = S7AddressParser.Parse(input); r.Area.ShouldBe(area); r.DbNumber.ShouldBe(0); r.Size.ShouldBe(size); r.ByteOffset.ShouldBe(byteOff); r.BitOffset.ShouldBe(bitOff); } // --- Timers / counters --- [Theory] [InlineData("T0", S7Area.Timer, 0)] [InlineData("T15", S7Area.Timer, 15)] [InlineData("C0", S7Area.Counter, 0)] [InlineData("C10", S7Area.Counter, 10)] public void Parse_timer_and_counter(string input, S7Area area, int number) { var r = S7AddressParser.Parse(input); r.Area.ShouldBe(area); r.ByteOffset.ShouldBe(number); r.Size.ShouldBe(S7Size.Word, "timers + counters are 16-bit opaque"); } // --- Reject garbage --- [Theory] [InlineData("")] [InlineData(" ")] [InlineData("Z0")] // unknown area [InlineData("DB")] // no number or tail [InlineData("DB1")] // no tail [InlineData("DB1.")] // empty tail [InlineData("DB1.DBX0")] // bit size without .bit [InlineData("DB1.DBX0.8")] // bit 8 out of range [InlineData("DB1.DBW0.0")] // word with bit suffix [InlineData("DB0.DBW0")] // db 0 invalid [InlineData("DBA.DBW0")] // non-numeric db [InlineData("DB1.DBQ0")] // invalid size letter [InlineData("M")] // no offset [InlineData("M0")] // bit access needs .bit [InlineData("M0.8")] // bit 8 [InlineData("MB-1")] // negative offset [InlineData("MW")] // no offset digits [InlineData("TA")] // non-numeric timer public void Parse_rejects_invalid(string bad) => Should.Throw(() => S7AddressParser.Parse(bad)); [Fact] public void TryParse_returns_false_for_garbage_without_throwing() { S7AddressParser.TryParse("not-an-address", out var r).ShouldBeFalse(); r.ShouldBe(default); } [Fact] public void TryParse_returns_true_for_valid_address() { S7AddressParser.TryParse("DB1.DBW0", out var r).ShouldBeTrue(); r.DbNumber.ShouldBe(1); r.Size.ShouldBe(S7Size.Word); } }