using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Pins 's AbCip row against the REAL write path (#737, review 07 U10), the /// same way the Modbus, S7 and Galaxy parity tests do: the server advertises an array node writable exactly when /// encodes a whole-array value for it (through the production /// , which the fake runtime's EncodeArray calls), so a driver change that adds /// or drops an array element type fails here until the table follows. /// /// /// The value written is the CLR array the server hands the driver for the node's DataType: the node manager /// converts a numeric client array (ScadaBridge's int[]) to the node's element type first, and the codec /// then converts each element to the controller's type. The fake controller holds exactly /// width × count bytes, so only the element type and the value can make the write fail. /// [Trait("Category", "Unit")] public sealed class AbCipArrayWriteSupportParityTests { private const string Device = "ab://10.0.0.5/1,0"; private static Array ClrArray(string dataType, int n) => dataType switch { "Boolean" => Enumerable.Repeat(true, n).ToArray(), "SByte" => Enumerable.Repeat((sbyte)1, n).ToArray(), "Byte" => Enumerable.Repeat((byte)1, n).ToArray(), "Int16" => Enumerable.Repeat((short)1, n).ToArray(), "UInt16" => Enumerable.Repeat((ushort)1, n).ToArray(), "Int32" => Enumerable.Repeat(1, n).ToArray(), "UInt32" => Enumerable.Repeat(1u, n).ToArray(), "Int64" => Enumerable.Repeat(1L, n).ToArray(), "UInt64" => Enumerable.Repeat(1UL, n).ToArray(), "Float" => Enumerable.Repeat(1f, n).ToArray(), "Double" => Enumerable.Repeat(1d, n).ToArray(), // Non-numeric text: a String node over a numeric element converts only when the text happens to be a // number, which is not a write the server can promise — the table keeps it read-only. "String" => Enumerable.Repeat("a", n).ToArray(), "DateTime" => Enumerable.Repeat(DateTime.UtcNow, n).ToArray(), _ => throw new ArgumentOutOfRangeException(nameof(dataType)), }; private static readonly string[] NodeTypes = ["Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64", "Float", "Double", "String", "DateTime"]; private static async Task DriverEncodesAsync(string tagConfig, AbCipDataType elementType, string nodeType, int count) { // The controller holds exactly what an encodable element array of this length occupies. var size = (AbCipArrayCodec.ElementWidth(elementType) ?? 1) * count; var factory = new FakeAbCipTagFactory { Customise = p => new FakeAbCipTag(p) { Size = size } }; using var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(Device)], RawTags = [new RawTagEntry("Arr", tagConfig, WriteIdempotent: false, DeviceName: Device)], Probe = new AbCipProbeOptions { Enabled = false }, }, "abcip-parity", factory); await drv.InitializeAsync("{}", CancellationToken.None); var r = await drv.WriteAsync([new WriteRequest("Arr", ClrArray(nodeType, count))], CancellationToken.None); return r[0].StatusCode == AbCipStatusMapper.Good && factory.Tags.TryGetValue("Arr", out var tag) && tag.EncodedArrayCount == count && tag.WriteCount == 1; } public static TheoryData TypeMatrix() { var data = new TheoryData(); foreach (var t in Enum.GetValues()) foreach (var n in NodeTypes) data.Add(t, n); return data; } /// Every element type × every node DataType: the table answers exactly what the write path does. [Theory] [MemberData(nameof(TypeMatrix))] public async Task Table_agrees_with_the_write_path_for_every_element_and_node_type(AbCipDataType dataType, string nodeType) { var json = $$"""{"tagPath":"Arr","dataType":"{{dataType}}","isArray":true,"arrayLength":3}"""; var driver = await DriverEncodesAsync(json, dataType, nodeType, 3); DriverArrayWriteSupport.Supports(DriverTypeNames.AbCip, nodeType, json).ShouldBe(driver, $"{dataType} on a {nodeType} node"); } /// /// Shapes beyond the element type, both directions: a safety tag (never writable), an undeclared length (a /// scalar to the factory, whose scalar encoder cannot take an array), a 1-element array, an element-offset path, /// and their positive controls. /// [Theory] [InlineData("""{"tagPath":"Arr","dataType":"DInt","isArray":true,"arrayLength":3}""", 3)] [InlineData("""{"tagPath":"Arr","dataType":"DInt","isArray":true,"arrayLength":1}""", 1)] [InlineData("""{"tagPath":"Arr","isArray":true,"arrayLength":3}""", 3)] // dataType ⇒ DInt [InlineData("""{"tagPath":"Arr","dataType":"DInt","safetyTag":true,"isArray":true,"arrayLength":3}""", 3)] [InlineData("""{"tagPath":"Arr","dataType":"DInt","isArray":true}""", 3)] [InlineData("""{"tagPath":"Arr","dataType":"DInt","isArray":true,"arrayLength":0}""", 3)] [InlineData("""{"tagPath":"Arr","dataType":"DInt","isArray":false,"arrayLength":3}""", 3)] public async Task Table_agrees_with_the_write_path_for_each_shape(string tagConfig, int nodeLength) { var driver = await DriverEncodesAsync(tagConfig, AbCipDataType.DInt, "Int32", nodeLength); DriverArrayWriteSupport.Supports(DriverTypeNames.AbCip, "Int32", tagConfig).ShouldBe(driver, tagConfig); } /// The ScadaBridge wire shape reaches the codec: a CLR int[] (and List<int>) to an /// INT array on an Int16 node the table advertises writable encodes. Positive control for the conversion rule. [Fact] public async Task A_ScadaBridge_int_list_to_an_INT_array_encodes() { const string json = """{"tagPath":"Arr","dataType":"Int","isArray":true,"arrayLength":3}"""; DriverArrayWriteSupport.Supports(DriverTypeNames.AbCip, "Int16", json).ShouldBeTrue(); foreach (object value in new object[] { new[] { 1, -2, 300 }, new List { 1, -2, 300 } }) { var factory = new FakeAbCipTagFactory { Customise = p => new FakeAbCipTag(p) { Size = 6 } }; using var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(Device)], RawTags = [new RawTagEntry("Arr", json, WriteIdempotent: false, DeviceName: Device)], Probe = new AbCipProbeOptions { Enabled = false }, }, "abcip-parity-sb", factory); await drv.InitializeAsync("{}", CancellationToken.None); (await drv.WriteAsync([new WriteRequest("Arr", value)], CancellationToken.None))[0].StatusCode .ShouldBe(AbCipStatusMapper.Good); factory.Tags["Arr"].EncodedArrayBytes.ShouldBe(Convert.FromHexString("0100FEFF2C01")); } } }