using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// /// Regression coverage for Driver.AbLegacy-001 — a PCCC bit index must be range-checked /// against the parent word width: 0..15 for 16-bit element files (N/B/I/O/S/A), 0..31 for /// the 32-bit L file. Float files are not bit-addressable. /// [Trait("Category", "Unit")] public sealed class AbLegacyBitIndexRangeTests { [Theory] [InlineData("N7:0/15")] [InlineData("B3:0/15")] [InlineData("I:0/15")] [InlineData("O:1/15")] [InlineData("S:1/15")] [InlineData("A10:0/15")] public void Bit_index_0_to_15_accepted_on_16bit_files(string input) => AbLegacyAddress.TryParse(input).ShouldNotBeNull(); [Theory] [InlineData("N7:0/16")] // first bit past a 16-bit word [InlineData("N7:0/20")] [InlineData("N7:0/31")] [InlineData("B3:0/16")] [InlineData("I:0/16")] [InlineData("O:1/16")] [InlineData("S:1/16")] [InlineData("A10:0/16")] public void Bit_index_above_15_rejected_on_16bit_files(string input) => AbLegacyAddress.TryParse(input).ShouldBeNull(); [Theory] [InlineData("L9:0/0")] [InlineData("L9:0/15")] [InlineData("L9:0/16")] // L-file words are 32-bit, so 16..31 are valid [InlineData("L9:0/31")] public void Bit_index_0_to_31_accepted_on_L_file(string input) => AbLegacyAddress.TryParse(input).ShouldNotBeNull(); [Fact] public void Bit_index_above_31_rejected_on_L_file() => AbLegacyAddress.TryParse("L9:0/32").ShouldBeNull(); [Theory] [InlineData("F8:0/0")] // float files are not bit-addressable at all [InlineData("F8:0/3")] public void Bit_index_rejected_on_float_file(string input) => AbLegacyAddress.TryParse(input).ShouldBeNull(); [Fact] public void Negative_bit_index_still_rejected() => AbLegacyAddress.TryParse("N7:0/-1").ShouldBeNull(); [Fact] public async Task Bit_in_word_RMW_against_L_file_uses_32bit_parent_and_high_bit() { // L9:0/20 — bit 20 of a 32-bit L-file word. The parent must be read/written as a // 32-bit Long so the high bits are addressable; a 16-bit (short)cast would truncate. var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 0 }, }; var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], Tags = [new AbLegacyTagDefinition("LBit20", "ab://10.0.0.5/1,0", "L9:0/20", AbLegacyDataType.Bit)], Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); var results = await drv.WriteAsync([new WriteRequest("LBit20", true)], CancellationToken.None); results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); factory.Tags.ShouldContainKey("L9:0"); Convert.ToInt32(factory.Tags["L9:0"].Value).ShouldBe(1 << 20); } [Fact] public async Task Bit_in_word_RMW_high_bit_15_does_not_corrupt_via_sign_extension() { // Parent word has bit 15 set (0x8000) — DecodeValue returns a sign-extended negative // int. Setting bit 0 must yield exactly 0x8001, not a sign-extended value. var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0x8000) }, }; var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], Tags = [new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit)], Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.WriteAsync([new WriteRequest("Bit0", true)], CancellationToken.None); // (short)0x8001 round-trips through the fake as -32767. Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(unchecked((short)0x8001)); } }