124 lines
5.1 KiB
C#
124 lines
5.1 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 + 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.
|
|
/// </summary>
|
|
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<uint> 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);
|
|
}
|
|
}
|