using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests; /// /// Gitea #610 — pins 's Modbus row against the REAL write path. For every /// region × element type, the server advertises an array node writable exactly when the driver accepts a /// whole-array write for it. A driver change that gains or loses an array encoder fails here until the table /// follows — so the server can never advertise a write this driver refuses (nor hide one it accepts). /// [Trait("Category", "Unit")] public sealed class ModbusArrayWriteSupportParityTests { private static IEnumerable<(ModbusRegion Region, ModbusDataType DataType)> Shapes() => from region in Enum.GetValues() from dt in Enum.GetValues() select (region, dt); public static TheoryData AllShapes() { var data = new TheoryData(); foreach (var (region, dt) in Shapes()) data.Add(region, dt); return data; } [Theory] [MemberData(nameof(AllShapes))] public async Task Table_agrees_with_the_driver_write_path(ModbusRegion region, ModbusDataType dataType) { var json = $$"""{"region":"{{region}}","address":10,"dataType":"{{dataType}}","stringLength":4,"isArray":true,"arrayLength":2}"""; var fake = new ModbusDriverTests.FakeTransport(); var drv = new ModbusDriver( new ModbusDriverOptions { Host = "fake", RawTags = [new RawTagEntry("cell/modbus/arr", json, false)] }, "modbus-array-parity", _ => fake); await drv.InitializeAsync("{}", CancellationToken.None); // A two-element value every element type can convert (Convert.ToBoolean / ToInt16 / ToSingle … of 1). var result = await drv.WriteAsync([new WriteRequest("cell/modbus/arr", new object[] { 1, 1 })], CancellationToken.None); var driverAccepts = result.Single().StatusCode == 0u; DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", json).ShouldBe( driverAccepts, $"region={region} dataType={dataType}: driver status 0x{result.Single().StatusCode:X8}"); } /// Positive control: the theory above is not vacuously "everything refused". [Fact] public void The_matrix_contains_both_answers() { var answers = Shapes() .Select(s => DriverArrayWriteSupport.Supports( DriverTypeNames.Modbus, "Int16", $$"""{"region":"{{s.Region}}","address":10,"dataType":"{{s.DataType}}","isArray":true,"arrayLength":2}""")) .ToHashSet(); answers.ShouldBe(new[] { true, false }, ignoreOrder: true); } /// /// #610 review MINOR-1/-2 — the table must also agree with the driver on the SHAPE edges: the FC16 write cap /// (the driver refuses rather than chunks an over-cap array; default 123 registers, or the configured /// MaxRegistersPerWrite) and a missing / zero arrayLength (the factory maps it to a scalar tag, /// whose scalar encoder refuses an array value). /// [Theory] [InlineData("Int32", "\"arrayLength\":61", 0, 61)] // 122 registers — fits the default cap [InlineData("Int32", "\"arrayLength\":62", 0, 62)] // 124 registers — over it [InlineData("Float64", "\"arrayLength\":31", 0, 31)] // 124 registers [InlineData("Int16", "\"arrayLength\":10", 10, 10)] // exactly a configured cap [InlineData("Int16", "\"arrayLength\":11", 10, 11)] // one over it [InlineData("Int16", "\"arrayLength\":0", 0, 2)] // declared length 0 ⇒ scalar to the driver [InlineData("Int16", "\"isArrayLengthMissing\":true", 0, 2)] public async Task Table_agrees_with_the_driver_on_write_cap_and_declared_length( string dataType, string lengthProp, ushort maxRegistersPerWrite, int elements) { var json = $$"""{"region":"HoldingRegisters","address":10,"dataType":"{{dataType}}","isArray":true,{{lengthProp}}}"""; var driverConfig = maxRegistersPerWrite == 0 ? "{}" : $$"""{"maxRegistersPerWrite":{{maxRegistersPerWrite}}}"""; var fake = new ModbusDriverTests.FakeTransport(); var options = new ModbusDriverOptions { Host = "fake", RawTags = [new RawTagEntry("cell/modbus/arr", json, false)], MaxRegistersPerWrite = maxRegistersPerWrite == 0 ? (ushort)123 : maxRegistersPerWrite, }; var drv = new ModbusDriver(options, "modbus-array-cap-parity", _ => fake); await drv.InitializeAsync("{}", CancellationToken.None); var value = Enumerable.Repeat(1, elements).ToArray(); var result = await drv.WriteAsync([new WriteRequest("cell/modbus/arr", value)], CancellationToken.None); var driverAccepts = result.Single().StatusCode == 0u; DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", json, driverConfig).ShouldBe( driverAccepts, $"{dataType} {lengthProp} cap={maxRegistersPerWrite}: driver status 0x{result.Single().StatusCode:X8}"); } /// Positive control for the theory above: both answers occur, so it is not vacuously all-refused. [Fact] public void The_shape_edges_contain_both_answers() { DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int32", """{"region":"HoldingRegisters","address":10,"dataType":"Int32","isArray":true,"arrayLength":61}""").ShouldBeTrue(); DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int32", """{"region":"HoldingRegisters","address":10,"dataType":"Int32","isArray":true,"arrayLength":62}""").ShouldBeFalse(); } }