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 as a single 1024-byte buffer. Post-F4-c /// (issue #270) the FOCAS driver routes PMC writes through the typed /// + /// entry points — the bit path performs RMW via ReadPmcRangeAsync + /// WritePmcRangeAsync, so this fake overrides those to drive a shared /// buffer the tests can assert against. /// is the unit-test surface; we mirror writes to /// too so any helper that reads from there sees the same source of truth. /// private sealed class PmcRmwFake : FakeFocasClient { public byte[] PmcBytes { get; } = new byte[1024]; public override Task<(byte[]? buffer, uint status)> ReadPmcRangeAsync( string letter, int pathId, int startByte, int byteCount, CancellationToken ct) { var buf = new byte[byteCount]; Array.Copy(PmcBytes, startByte, buf, 0, byteCount); return Task.FromResult<(byte[]?, uint)>((buf, FocasStatusMapper.Good)); } public override Task WritePmcRangeAsync( string letter, int pathId, int startByte, byte[] bytes, CancellationToken ct) { Array.Copy(bytes, 0, PmcBytes, startByte, bytes.Length); return Task.FromResult(FocasStatusMapper.Good); } } private static (FocasDriver drv, PmcRmwFake fake) NewDriver(params FocasTagDefinition[] tags) { var fake = new PmcRmwFake(); var factory = new FakeFocasClientFactory { Customise = () => fake }; // PMC bit RMW exercises the write path; opt every supplied tag into Writable + flip the // driver-level Writes.Enabled gate so the tests still drive the wire path after F4-a's // safer-by-default flip (issue #268). var writableTags = tags .Select(t => t with { Writable = true }) .ToArray(); var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], Tags = writableTags, Probe = new FocasProbeOptions { Enabled = false }, Writes = new FocasWritesOptions { Enabled = true, AllowPmc = true }, }, "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); } }