120 lines
4.9 KiB
C#
120 lines
4.9 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="IFocasClient.WritePmcRangeAsync"/> + <see cref="IFocasClient.WritePmcBitAsync"/>
|
|
/// entry points — the bit path performs RMW via <c>ReadPmcRangeAsync</c> +
|
|
/// <c>WritePmcRangeAsync</c>, so this fake overrides those to drive a shared
|
|
/// <see cref="PmcBytes"/> buffer the tests can assert against. <see cref="PmcBytes"/>
|
|
/// is the unit-test surface; we mirror writes to <see cref="FakeFocasClient.PmcByteRanges"/>
|
|
/// too so any helper that reads from there sees the same source of truth.
|
|
/// </summary>
|
|
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<uint> 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);
|
|
}
|
|
}
|