using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Modbus; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests; /// /// #138 e2e coverage for the new addressing grammar (#136-#144) against a live pymodbus /// simulator. Skips when the simulator is unreachable so the suite stays portable on dev /// boxes without Docker. Coverage is breadth-first — one round-trip per major feature /// (Modicon parse, suffix grammar, byte order, array, multi-unit) — rather than exhaustive, /// because the unit tests already pin the per-call semantics; the integration tests prove /// the wire glue agrees with the simulator end-to-end. /// [Trait("Category", "Integration")] [Collection("ModbusSimulator")] public sealed class AddressingGrammarTests { private readonly ModbusSimulatorFixture _sim; public AddressingGrammarTests(ModbusSimulatorFixture sim) => _sim = sim; private async Task NewDriverAsync(params ModbusTagDefinition[] tags) { var opts = new ModbusDriverOptions { Host = _sim.Host, Port = _sim.Port, Tags = tags, Probe = new ModbusProbeOptions { Enabled = false }, }; var drv = new ModbusDriver(opts, "addressing-e2e"); await drv.InitializeAsync("{}", CancellationToken.None); return drv; } [Fact] public async Task Modicon_5_And_6_Digit_Both_Map_To_Same_Wire_Offset() { if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason); // Both tags target HR offset 0 via different grammar forms; reads should be equivalent. var t5 = new ModbusTagDefinition("Five", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16); var t6 = new ModbusTagDefinition("Six", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16); var drv = await NewDriverAsync(t5, t6); var values = await drv.ReadAsync(["Five", "Six"], CancellationToken.None); values[0].StatusCode.ShouldBe(values[1].StatusCode); values[0].Value.ShouldBe(values[1].Value); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Float32_With_CDAB_Roundtrips_Through_Wire() { if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason); var tag = new ModbusTagDefinition("Tank", ModbusRegion.HoldingRegisters, 100, ModbusDataType.Float32, ByteOrder: ModbusByteOrder.WordSwap); var drv = await NewDriverAsync(tag); await drv.WriteAsync([new WriteRequest("Tank", 12.5f)], CancellationToken.None); var read = await drv.ReadAsync(["Tank"], CancellationToken.None); read[0].Value.ShouldBe(12.5f); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Int16_Array_Reads_Surface_As_Typed_Array() { if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason); var tag = new ModbusTagDefinition("Levels", ModbusRegion.HoldingRegisters, 200, ModbusDataType.Int16, ArrayCount: 5); var drv = await NewDriverAsync(tag); await drv.WriteAsync([new WriteRequest("Levels", new short[] { 10, 20, 30, 40, 50 })], CancellationToken.None); var read = await drv.ReadAsync(["Levels"], CancellationToken.None); var arr = read[0].Value.ShouldBeOfType(); arr.ShouldBe(new short[] { 10, 20, 30, 40, 50 }); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task Block_Read_Coalescing_Reduces_PDU_Count_End_To_End() { if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason); // Sanity check that the simulator accepts the larger PDU coalescing produces. var t1 = new ModbusTagDefinition("T1", ModbusRegion.HoldingRegisters, 300, ModbusDataType.Int16); var t2 = new ModbusTagDefinition("T2", ModbusRegion.HoldingRegisters, 302, ModbusDataType.Int16); var t3 = new ModbusTagDefinition("T3", ModbusRegion.HoldingRegisters, 304, ModbusDataType.Int16); var opts = new ModbusDriverOptions { Host = _sim.Host, Port = _sim.Port, Tags = [t1, t2, t3], MaxReadGap = 5, Probe = new ModbusProbeOptions { Enabled = false }, }; var drv = new ModbusDriver(opts, "addressing-e2e"); await drv.InitializeAsync("{}", CancellationToken.None); var read = await drv.ReadAsync(["T1", "T2", "T3"], CancellationToken.None); read.Count.ShouldBe(3); // All three should read Good (the simulator accepts a 5-register coalesced read). read.All(v => v.StatusCode == 0u).ShouldBeTrue("coalesced read must succeed against the simulator"); await drv.ShutdownAsync(CancellationToken.None); } }