Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
4.7 KiB
C#
109 lines
4.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// #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.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
[Collection("ModbusSimulator")]
|
|
public sealed class AddressingGrammarTests
|
|
{
|
|
private readonly ModbusSimulatorFixture _sim;
|
|
public AddressingGrammarTests(ModbusSimulatorFixture sim) => _sim = sim;
|
|
|
|
private async Task<ModbusDriver> 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<short[]>();
|
|
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);
|
|
}
|
|
}
|