using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; [Trait("Category", "Unit")] public sealed class FocasPmcBitRmwTests { /// /// Fake client simulating PMC byte storage + exposing it as a sbyte so RMW callers can /// observe the read-modify-write round-trip. ReadAsync for a Bit with bitIndex surfaces /// the current bit; WriteAsync stores the full byte the driver issues. /// private sealed class PmcRmwFake : FakeFocasClient { public byte[] PmcBytes { get; } = new byte[1024]; public override Task<(object? value, uint status)> ReadAsync( FocasAddress address, FocasDataType type, CancellationToken ct) { if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Byte) return Task.FromResult(((object?)(sbyte)PmcBytes[address.Number], FocasStatusMapper.Good)); if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Bit && address.BitIndex is int bit) return Task.FromResult(((object?)((PmcBytes[address.Number] & (1 << bit)) != 0), FocasStatusMapper.Good)); return base.ReadAsync(address, type, ct); } public override Task WriteAsync( FocasAddress address, FocasDataType type, object? value, CancellationToken ct) { // Driver writes the full byte after RMW (type==Byte with full byte value), OR a raw // bit write (type==Bit, bitIndex non-null) — depending on how the driver routes it. if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Byte) { PmcBytes[address.Number] = (byte)Convert.ToSByte(value); return Task.FromResult(FocasStatusMapper.Good); } if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Bit && address.BitIndex is int bit) { var current = PmcBytes[address.Number]; PmcBytes[address.Number] = Convert.ToBoolean(value) ? (byte)(current | (1 << bit)) : (byte)(current & ~(1 << bit)); return Task.FromResult(FocasStatusMapper.Good); } return base.WriteAsync(address, type, value, ct); } } private static (FocasDriver drv, PmcRmwFake fake) NewDriver(params FocasTagDefinition[] tags) { var fake = new PmcRmwFake(); var factory = new FakeFocasClientFactory { Customise = () => fake }; var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], Tags = tags, Probe = new FocasProbeOptions { Enabled = false }, }, "drv-1", factory); return (drv, fake); } [Fact] public async Task Bit_set_surfaces_as_Good_status_and_flips_bit() { var (drv, fake) = NewDriver( new FocasTagDefinition("Run", "focas://10.0.0.5:8193", "R100.3", FocasDataType.Bit)); await drv.InitializeAsync("{}", CancellationToken.None); fake.PmcBytes[100] = 0b0000_0001; var results = await drv.WriteAsync([new WriteRequest("Run", true)], CancellationToken.None); results.Single().StatusCode.ShouldBe(FocasStatusMapper.Good); fake.PmcBytes[100].ShouldBe((byte)0b0000_1001); } [Fact] public async Task Bit_clear_preserves_other_bits() { var (drv, fake) = NewDriver( new FocasTagDefinition("Flag", "focas://10.0.0.5:8193", "R100.3", FocasDataType.Bit)); await drv.InitializeAsync("{}", CancellationToken.None); fake.PmcBytes[100] = 0xFF; await drv.WriteAsync([new WriteRequest("Flag", false)], CancellationToken.None); fake.PmcBytes[100].ShouldBe((byte)0b1111_0111); } [Fact] public async Task Subsequent_bit_sets_in_same_byte_compose_correctly() { var tags = Enumerable.Range(0, 8) .Select(b => new FocasTagDefinition($"Bit{b}", "focas://10.0.0.5:8193", $"R100.{b}", FocasDataType.Bit)) .ToArray(); var (drv, fake) = NewDriver(tags); await drv.InitializeAsync("{}", CancellationToken.None); fake.PmcBytes[100] = 0; for (var b = 0; b < 8; b++) await drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None); fake.PmcBytes[100].ShouldBe((byte)0xFF); } [Fact] public async Task Bit_write_to_different_bytes_does_not_contend() { var tags = Enumerable.Range(0, 4) .Select(i => new FocasTagDefinition($"Bit{i}", "focas://10.0.0.5:8193", $"R{50 + i}.0", FocasDataType.Bit)) .ToArray(); var (drv, fake) = NewDriver(tags); await drv.InitializeAsync("{}", CancellationToken.None); await Task.WhenAll(Enumerable.Range(0, 4).Select(i => drv.WriteAsync([new WriteRequest($"Bit{i}", true)], CancellationToken.None))); for (var i = 0; i < 4; i++) fake.PmcBytes[50 + i].ShouldBe((byte)0x01); } }