using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class AbCipBoolInDIntRmwTests { /// /// Fake tag runtime that stores a DINT value + exposes Read/Write/EncodeValue/DecodeValue /// for DInt. RMW tests use one instance as the "parent" runtime (tag name "Motor.Flags") /// which the driver's WriteBitInDIntAsync reads + writes. /// private sealed class ParentDintFake(AbCipTagCreateParams p) : FakeAbCipTag(p) { // Uses the base FakeAbCipTag's Value + ReadCount + WriteCount. } [Fact] public async Task Bit_set_reads_parent_ORs_bit_writes_back() { var factory = new FakeAbCipTagFactory { Customise = p => new ParentDintFake(p) { Value = 0b0001 }, }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Tags = [ new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); var results = await drv.WriteAsync( [new WriteRequest("Flag3", true)], CancellationToken.None); results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good); // Parent runtime created under name "Motor.Flags" — distinct from the bit-selector tag. factory.Tags.ShouldContainKey("Motor.Flags"); factory.Tags["Motor.Flags"].Value.ShouldBe(0b1001); // bit 3 set, bit 0 preserved factory.Tags["Motor.Flags"].ReadCount.ShouldBe(1); factory.Tags["Motor.Flags"].WriteCount.ShouldBe(1); } [Fact] public async Task Bit_clear_preserves_other_bits() { var factory = new FakeAbCipTagFactory { Customise = p => new ParentDintFake(p) { Value = unchecked((int)0xFFFFFFFF) }, }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Tags = [new AbCipTagDefinition("F", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool)], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None); var updated = Convert.ToInt32(factory.Tags["Motor.Flags"].Value); (updated & (1 << 3)).ShouldBe(0); // bit 3 cleared (updated & ~(1 << 3)).ShouldBe(unchecked((int)0xFFFFFFF7)); // every other bit preserved } [Fact] public async Task Concurrent_bit_writes_to_same_parent_compose_correctly() { var factory = new FakeAbCipTagFactory { Customise = p => new ParentDintFake(p) { Value = 0 }, }; var tags = Enumerable.Range(0, 8) .Select(b => new AbCipTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"Flags.{b}", AbCipDataType.Bool)) .ToArray(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Tags = tags, Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await Task.WhenAll(Enumerable.Range(0, 8).Select(b => drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None))); Convert.ToInt32(factory.Tags["Flags"].Value).ShouldBe(0xFF); } [Fact] public async Task Bit_writes_to_different_parents_each_get_own_runtime() { var factory = new FakeAbCipTagFactory { Customise = p => new ParentDintFake(p) { Value = 0 }, }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Tags = [ new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "Motor1.Flags.0", AbCipDataType.Bool), new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "Motor2.Flags.0", AbCipDataType.Bool), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.WriteAsync([new WriteRequest("A", true)], CancellationToken.None); await drv.WriteAsync([new WriteRequest("B", true)], CancellationToken.None); factory.Tags.ShouldContainKey("Motor1.Flags"); factory.Tags.ShouldContainKey("Motor2.Flags"); } [Fact] public async Task Repeat_bit_writes_reuse_one_parent_runtime() { var factory = new FakeAbCipTagFactory { Customise = p => new ParentDintFake(p) { Value = 0 }, }; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")], Tags = [ new AbCipTagDefinition("Bit0", "ab://10.0.0.5/1,0", "Flags.0", AbCipDataType.Bool), new AbCipTagDefinition("Bit5", "ab://10.0.0.5/1,0", "Flags.5", AbCipDataType.Bool), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.WriteAsync([new WriteRequest("Bit0", true)], CancellationToken.None); await drv.WriteAsync([new WriteRequest("Bit5", true)], CancellationToken.None); // Three factory invocations: two bit-selector tags (never used for writes, but the // driver may create them opportunistically) + one shared parent. Assert the parent was // init'd exactly once + used for both writes. factory.Tags["Flags"].InitializeCount.ShouldBe(1); factory.Tags["Flags"].WriteCount.ShouldBe(2); Convert.ToInt32(factory.Tags["Flags"].Value).ShouldBe(0x21); // bits 0 + 5 } }