using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; [Trait("Category", "Unit")] public sealed class TwinCATBitWriteTests { // ---- Helper unit tests ---- [Theory] [InlineData("Flags.3", "Flags")] [InlineData("MAIN.Status.7", "MAIN.Status")] [InlineData("GVL.Motors[0].Status.5", "GVL.Motors[0].Status")] public void TryGetParentSymbolPath_strips_trailing_bit_selector(string input, string expected) { AdsTwinCATClient.TryGetParentSymbolPath(input).ShouldBe(expected); } [Theory] [InlineData("Counter")] // single segment — no parent [InlineData(".Bad")] // leading dot public void TryGetParentSymbolPath_returns_null_for_pathless(string input) { AdsTwinCATClient.TryGetParentSymbolPath(input).ShouldBeNull(); } [Theory] [InlineData(0u, 0, true, 0x0000_0001u)] [InlineData(0u, 31, true, 0x8000_0000u)] [InlineData(0xFFFF_FFFFu, 0, false, 0xFFFF_FFFEu)] [InlineData(0xFFFF_FFFFu, 31, false, 0x7FFF_FFFFu)] [InlineData(0x0000_0008u, 3, true, 0x0000_0008u)] // already set — idempotent [InlineData(0x0000_0000u, 3, false, 0x0000_0000u)] // already clear — idempotent public void ApplyBit_sets_or_clears_bit(uint word, int bit, bool setBit, uint expected) { AdsTwinCATClient.ApplyBit(word, bit, setBit).ShouldBe(expected); } // ---- Driver-level round-trip ---- private static (TwinCATDriver drv, FakeTwinCATClientFactory factory) NewDriver(params TwinCATTagDefinition[] tags) { var factory = new FakeTwinCATClientFactory(); var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Tags = tags, Probe = new TwinCATProbeOptions { Enabled = false }, }, "drv-1", factory); return (drv, factory); } [Fact] public async Task Bit_write_sets_bit_in_parent_word() { var (drv, factory) = NewDriver( new TwinCATTagDefinition("FlagBit3", "ads://5.23.91.23.1.1:851", "Flags.3", TwinCATDataType.Bool)); await drv.InitializeAsync("{}", CancellationToken.None); // Parent word starts at 0. factory.Customise = () => new FakeTwinCATClient { Values = { ["Flags"] = 0u } }; var results = await drv.WriteAsync( [new Core.Abstractions.WriteRequest("FlagBit3", true)], CancellationToken.None); results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good); // Parent should now have bit 3 set. Convert.ToUInt32(factory.Clients[0].Values["Flags"]!).ShouldBe(0x0000_0008u); } [Fact] public async Task Bit_write_clears_bit_without_disturbing_neighbours() { var (drv, factory) = NewDriver( new TwinCATTagDefinition("FlagBit3", "ads://5.23.91.23.1.1:851", "Flags.3", TwinCATDataType.Bool)); await drv.InitializeAsync("{}", CancellationToken.None); // Parent word has bits 0, 3, 7 set initially. factory.Customise = () => new FakeTwinCATClient { Values = { ["Flags"] = 0x0000_0089u } }; var results = await drv.WriteAsync( [new Core.Abstractions.WriteRequest("FlagBit3", false)], CancellationToken.None); results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good); // Bit 3 cleared; bits 0 and 7 untouched. Convert.ToUInt32(factory.Clients[0].Values["Flags"]!).ShouldBe(0x0000_0081u); } [Fact] public async Task Bit_write_does_not_throw_NotSupported() { // Regression: AdsTwinCATClient previously threw NotSupportedException for bit writes. var (drv, factory) = NewDriver( new TwinCATTagDefinition("Bit", "ads://5.23.91.23.1.1:851", "GVL.Word.0", TwinCATDataType.Bool)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.Word"] = 0u } }; var results = await drv.WriteAsync( [new Core.Abstractions.WriteRequest("Bit", true)], CancellationToken.None); results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good); // Status should be Good, not BadNotSupported (which is what the catch block produced). results.Single().StatusCode.ShouldNotBe(TwinCATStatusMapper.BadNotSupported); } }