d758dd1e16
Review r0 I3. The array encoder used the AUTHORED dataType's width while both runtimes size their buffer from the FILE: an Int array on F8 wrote four shorts into a 16-byte float buffer and sent it all (garbage neighbours, answered Good); Long/Float on N overran it. - The write path resolves the element width from the data file the address names (N/B/A/I/O/S 16-bit word, L 32-bit, F float) and encodes at THAT width. - An authored type that does not match its file is refused BadTypeMismatch before any runtime exists (warned once, no health write). - The same rule (AbLegacyTagDefinitionFactory.ArrayFileMismatch) backs an Inspect deploy warning and the AdminUI model's Validate error; FromTagConfig still accepts the tag so persisted configs keep loading. - M6: the skipped F1 parity test gains the shapes the write path refuses (/bit, .sub, indirect, type/file mismatch, PLC-5 native). Tests: one case per file type, a mismatch per pairing with nothing created, and a positive control. Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
76 lines
4.4 KiB
C#
76 lines
4.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
/// <summary>
|
|
/// Pins <see cref="DriverArrayWriteSupport"/>'s AbLegacy row against the REAL array encoder
|
|
/// (<see cref="AbLegacyArrayWriteEncoder"/>), the same way the Modbus and S7 parity tests do: for every element
|
|
/// type the server must advertise an array node writable exactly when the driver encodes it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ SKIPPED until follow-up F1 lands: the driver gained an array encoder in this lane (review 07 U10), but
|
|
/// <see cref="DriverArrayWriteSupport"/> (Core.Abstractions, not owned by the AbLegacy lane) still lists AbLegacy as
|
|
/// scalar-only, so every AbLegacy array node stays read-only and the encoder is unreachable from OPC UA. When F1
|
|
/// adds the row, delete the Skip; this test then holds the table and the encoder together.
|
|
/// </remarks>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbLegacyArrayWriteSupportParityTests
|
|
{
|
|
public static TheoryData<AbLegacyDataType> AllTypes() => new(Enum.GetValues<AbLegacyDataType>());
|
|
|
|
private static string AddressFor(AbLegacyDataType t) => t switch
|
|
{
|
|
AbLegacyDataType.Float => "F8:0",
|
|
AbLegacyDataType.Long => "L9:0",
|
|
AbLegacyDataType.Bit => "B3:0",
|
|
AbLegacyDataType.String => "ST9:0",
|
|
AbLegacyDataType.TimerElement => "T4:0",
|
|
AbLegacyDataType.CounterElement => "C5:0",
|
|
AbLegacyDataType.ControlElement => "R6:0",
|
|
_ => "N7:0",
|
|
};
|
|
|
|
[Theory(Skip = "F1: DriverArrayWriteSupport has no AbLegacy row yet (Core.Abstractions is outside this lane)")]
|
|
[MemberData(nameof(AllTypes))]
|
|
public void Table_agrees_with_the_array_encoder(AbLegacyDataType dataType)
|
|
{
|
|
var json = $$"""{"address":"{{AddressFor(dataType)}}","dataType":"{{dataType}}","isArray":true,"arrayLength":2}""";
|
|
var encoderAccepts = AbLegacyTagDefinitionFactory.FromTagConfig(json, "p", out _)
|
|
&& AbLegacyArrayWriteEncoder.TryConvert(dataType, new object[] { 1, 1 }, 2, out _) == AbLegacyStatusMapper.Good;
|
|
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.AbLegacy, "Int16", json).ShouldBe(encoderAccepts, dataType.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// The shapes the WRITE PATH refuses although their element type alone looks writable (review 07 r0 M6): a
|
|
/// <c>/bit</c> or <c>.sub</c> address, an indirect address, a type that does not match its data file (I3), and
|
|
/// a native PLC-5 device. The F1 row must answer false for every one — keyed on <c>dataType</c> alone it would
|
|
/// advertise them writable. Skipped with the parity theory until F1 lands.
|
|
/// </summary>
|
|
[Theory(Skip = "F1: DriverArrayWriteSupport has no AbLegacy row yet (Core.Abstractions is outside this lane)")]
|
|
[InlineData("""{"address":"N7:0/3","dataType":"Int","isArray":true,"arrayLength":2}""", "{}")]
|
|
[InlineData("""{"address":"T4:0.ACC","dataType":"Int","isArray":true,"arrayLength":2}""", "{}")]
|
|
[InlineData("""{"address":"N7:[N7:0]","dataType":"Int","isArray":true,"arrayLength":2}""", "{}")]
|
|
[InlineData("""{"address":"F8:0","dataType":"Int","isArray":true,"arrayLength":2}""", "{}")]
|
|
[InlineData("""{"address":"N7:0","dataType":"Long","isArray":true,"arrayLength":2}""", "{}")]
|
|
[InlineData("""{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":2}""",
|
|
"""{"transport":"Native","devices":[{"hostAddress":"ab://10.0.0.5/1,0","plcFamily":"Plc5"}]}""")]
|
|
public void Table_refuses_the_shapes_the_write_path_refuses(string tagConfig, string driverConfig) =>
|
|
DriverArrayWriteSupport.Supports(DriverTypeNames.AbLegacy, "Int16", tagConfig, driverConfig).ShouldBeFalse();
|
|
|
|
/// <summary>What the encoder accepts today — the row F1 must add (Int, AnalogInt, Long, Float on a matching file).</summary>
|
|
[Theory]
|
|
[InlineData(AbLegacyDataType.Int, true)]
|
|
[InlineData(AbLegacyDataType.AnalogInt, true)]
|
|
[InlineData(AbLegacyDataType.Long, true)]
|
|
[InlineData(AbLegacyDataType.Float, true)]
|
|
[InlineData(AbLegacyDataType.Bit, false)]
|
|
[InlineData(AbLegacyDataType.String, false)]
|
|
[InlineData(AbLegacyDataType.TimerElement, false)]
|
|
public void Encoder_accepts_exactly_the_flat_numeric_element_types(AbLegacyDataType dataType, bool accepts) =>
|
|
(AbLegacyArrayWriteEncoder.TryConvert(dataType, new object[] { 1, 1 }, 2, out _) == AbLegacyStatusMapper.Good)
|
|
.ShouldBe(accepts);
|
|
}
|