Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FwlibNativeHelperTests.cs
Joseph Doherty 8c309aebf3 RMW pass 1 — Modbus BitInRegister + FOCAS PMC Bit write paths. First half of task #181 — the two drivers where read-modify-write is a clean protocol-level insertion (Modbus FC03/FC06 round-trip + FOCAS pmc_rdpmcrng / pmc_wrpmcrng round-trip). Per-driver SemaphoreSlim registry keyed on the parent word address serialises concurrent bit writes so two writers targeting different bits in the same word don't lose one another's update. Modbus — ModbusDriver gains WriteBitInRegisterAsync + _rmwLocks ConcurrentDictionary. WriteOneAsync routes BitInRegister (HoldingRegisters region only) through RMW ahead of the normal encode path. Read uses FC03 Read Holding Registers for 1 register at tag.Address, bit-op on the returned ushort via (current | 1<<bit) for set / (current & ~(1<<bit)) for clear, write back via FC06 Write Single Register. Per-address lock prevents concurrent bit writes to the same register from racing. Rejects out-of-range bits (0-15) with InvalidOperationException. EncodeRegister's BitInRegister branch repurposed as a defensive guard — if a non-RMW caller ever reaches it, throw so an unintended bypass stays loud rather than silently clobbering. FOCAS — FwlibFocasClient gains WritePmcBitAsync + _rmwLocks keyed on {addrType}:{byteAddr}. Driver-layer WriteAsync routes Bit writes with a bitIndex through the new path; other Pmc writes still hit the direct pmc_wrpmcrng path. RMW uses cnc_rdpmcrng + Byte dataType to grab the parent byte, bit-op with (current | 1<<bit) or (current & ~(1<<bit)), cnc_wrpmcrng to write back. Rejects out-of-range bits (0-7, FOCAS PMC bytes are 8-bit) with InvalidOperationException. EncodePmcValue's Bit branch now treats a no-bitIndex case as whole-byte boolean (non-zero / zero); bitIndex-present writes never hit this path because they dispatch to WritePmcBitAsync upstream. Tests — 5 new ModbusBitRmwTests + 4 new FocasPmcBitRmwTests + 1 renamed pre-existing test each covering — bit set preserves other bits, bit clear preserves other bits, concurrent bit writes to same word/byte compose correctly (8-parallel stress), bit writes on different parent words proceed without contention (4-parallel), sequential bit sets compose into 0xFF after all 8. Fake PmcRmwFake in FOCAS tests simulates the PMC byte storage + surfaces it through the IFocasClient contract so the test asserts driver-level behavior without needing Fwlib32.dll. FwlibNativeHelperTests.EncodePmcValue_Bit_throws_NotSupported_for_RMW_gap replaced with EncodePmcValue_Bit_without_bit_index_writes_byte_boolean reflecting the new behavior. ModbusDataTypeTests.BitInRegister_write_is_not_supported_in_PR24 renamed to BitInRegister_EncodeRegister_still_rejects_direct_calls; the message assertion updated to match the new defensive message. Modbus tests now 182/182, FOCAS tests now 119/119; full solution builds 0 errors; AbCip/AbLegacy/TwinCAT untouched (those get their RMW pass in a follow-up since libplctag bit access may need a parallel parent-word handle). Task #181 stays pending until that second pass lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:25:27 -04:00

107 lines
3.5 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
/// <summary>
/// Tests for the managed helpers inside FwlibNative + FwlibFocasClient that don't require the
/// licensed Fwlib32.dll — letter→ADR_* mapping, FocasDataType→data-type mapping, byte encoding.
/// The actual P/Invoke calls can only run where the DLL is present; field testing covers those.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FwlibNativeHelperTests
{
[Theory]
[InlineData("G", 0)]
[InlineData("F", 1)]
[InlineData("Y", 2)]
[InlineData("X", 3)]
[InlineData("A", 4)]
[InlineData("R", 5)]
[InlineData("T", 6)]
[InlineData("K", 7)]
[InlineData("C", 8)]
[InlineData("D", 9)]
[InlineData("E", 10)]
[InlineData("g", 0)] // case-insensitive
public void PmcAddrType_maps_every_valid_letter(string letter, short expected)
{
FocasPmcAddrType.FromLetter(letter).ShouldBe(expected);
}
[Theory]
[InlineData("Z")]
[InlineData("")]
[InlineData("XX")]
public void PmcAddrType_rejects_unknown_letters(string letter)
{
FocasPmcAddrType.FromLetter(letter).ShouldBeNull();
}
[Theory]
[InlineData(FocasDataType.Bit, 0)] // byte
[InlineData(FocasDataType.Byte, 0)]
[InlineData(FocasDataType.Int16, 1)] // word
[InlineData(FocasDataType.Int32, 2)] // long
[InlineData(FocasDataType.Float32, 4)]
[InlineData(FocasDataType.Float64, 5)]
public void PmcDataType_maps_FocasDataType_to_FOCAS_code(FocasDataType input, short expected)
{
FocasPmcDataType.FromFocasDataType(input).ShouldBe(expected);
}
[Fact]
public void EncodePmcValue_Byte_writes_signed_byte_at_offset_0()
{
var buf = new byte[40];
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Byte, (sbyte)-5, bitIndex: null);
((sbyte)buf[0]).ShouldBe((sbyte)-5);
}
[Fact]
public void EncodePmcValue_Int16_writes_little_endian()
{
var buf = new byte[40];
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Int16, (short)0x1234, bitIndex: null);
buf[0].ShouldBe((byte)0x34);
buf[1].ShouldBe((byte)0x12);
}
[Fact]
public void EncodePmcValue_Int32_writes_little_endian()
{
var buf = new byte[40];
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Int32, 0x12345678, bitIndex: null);
buf[0].ShouldBe((byte)0x78);
buf[1].ShouldBe((byte)0x56);
buf[2].ShouldBe((byte)0x34);
buf[3].ShouldBe((byte)0x12);
}
[Fact]
public void EncodePmcValue_Bit_without_bit_index_writes_byte_boolean()
{
// Task #181 closed the Bit-write gap — PMC Bit with a bitIndex now routes through
// WritePmcBitAsync's RMW path upstream, and raw EncodePmcValue only gets the
// no-bit-index case (treated as a whole-byte boolean).
var buf = new byte[40];
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Bit, true, bitIndex: null);
buf[0].ShouldBe((byte)1);
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Bit, false, bitIndex: null);
buf[0].ShouldBe((byte)0);
}
[Fact]
public void EncodeParamValue_Int32_writes_little_endian()
{
var buf = new byte[32];
FwlibFocasClient.EncodeParamValue(buf, FocasDataType.Int32, 0x0A0B0C0D);
buf[0].ShouldBe((byte)0x0D);
buf[1].ShouldBe((byte)0x0C);
buf[2].ShouldBe((byte)0x0B);
buf[3].ShouldBe((byte)0x0A);
}
}