7cc8415310
U10 (user decision: implement array encode now): config, the AdminUI and discovery all advertised array tags Operate-writable, but both runtimes encoded a scalar - an int[] threw InvalidCastException (BadTypeMismatch AND Degraded), a scalar wrote element 0 natively, and libplctag re-sent its whole last-read buffer with element 0 patched (stale elements 1..n-1). - AbCipArrayCodec encodes count elements little-endian at the element width, CONVERTING each element (ScadaBridge writes int[] to INT arrays; overflow is BadOutOfRange, unconvertible BadTypeMismatch, both before anything is sent). - The driver checks the shape before any runtime exists: not a list -> BadTypeMismatch, length != elementCount -> BadOutOfRange, BOOL/STRING/ structure element types -> BadNotSupported; and, before sending, a controller buffer that is not width x count -> BadTypeMismatch. Refusals warn once and write no health. - Native: ONE Write Tag of count elements with the controller's type code (the client fragments a large one); libplctag: the codec's bytes copied into the elem_count-sized buffer, refused TooSmall if it does not fit. - IAbCipTagRuntime.EncodeArray (default: NotSupportedException). Real-controller proof is a hardware userGate (see the lane report recipe). Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
179 lines
8.4 KiB
C#
179 lines
8.4 KiB
C#
using Shouldly;
|
||
using Xunit;
|
||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||
|
||
/// <summary>
|
||
/// Review 07 U10: array writes. Config and the AdminUI allowed them and discovery advertised array nodes
|
||
/// Operate-writable, but both runtimes encoded a SCALAR: an <c>int[]</c> threw InvalidCastException (BadTypeMismatch
|
||
/// AND Degraded), a scalar wrote element 0 natively, and libplctag re-sent the whole last-read buffer with only
|
||
/// element 0 patched. The driver now encodes <c>count</c> elements at the element width, refuses a length mismatch
|
||
/// with <c>BadOutOfRange</c>, and converts element values (ScadaBridge writes <c>int[]</c> to INT arrays).
|
||
/// </summary>
|
||
[Trait("Category", "Unit")]
|
||
public sealed class AbCipArrayWriteTests
|
||
{
|
||
private const string Device = "ab://10.0.0.5/1,0";
|
||
|
||
private static async Task<(AbCipDriver Driver, FakeAbCipTagFactory Factory)> BuildAsync(
|
||
AbCipDataType type, int count, int size = -1)
|
||
{
|
||
var factory = new FakeAbCipTagFactory { Customise = p => new FakeAbCipTag(p) { Size = size } };
|
||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||
{
|
||
Devices = [new AbCipDeviceOptions(Device)],
|
||
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Arr", Device, "Arr", type, ElementCount: count, IsArray: true)),
|
||
Probe = new AbCipProbeOptions { Enabled = false },
|
||
}, "abcip-array-write", factory);
|
||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||
return (drv, factory);
|
||
}
|
||
|
||
// ---- the codec ----
|
||
|
||
/// <summary>Each element is converted and laid out little-endian at the element width.</summary>
|
||
[Fact]
|
||
public void The_codec_converts_elements_and_lays_them_out_at_the_element_width()
|
||
{
|
||
AbCipArrayCodec.Encode(AbCipDataType.Int, [1, -2, 300]).ShouldBe(Convert.FromHexString("0100FEFF2C01"));
|
||
AbCipArrayCodec.Encode(AbCipDataType.SInt, [(short)-1, 5L]).ShouldBe(Convert.FromHexString("FF05"));
|
||
AbCipArrayCodec.Encode(AbCipDataType.DInt, [1.0, 2]).ShouldBe(Convert.FromHexString("0100000002000000"));
|
||
AbCipArrayCodec.Encode(AbCipDataType.Real, [1, 2.5]).ShouldBe(Convert.FromHexString("0000803F00002040"));
|
||
AbCipArrayCodec.Encode(AbCipDataType.ULInt, [ulong.MaxValue]).ShouldBe(Convert.FromHexString("FFFFFFFFFFFFFFFF"));
|
||
Should.Throw<OverflowException>(() => AbCipArrayCodec.Encode(AbCipDataType.Int, [40000]));
|
||
Should.Throw<NotSupportedException>(() => AbCipArrayCodec.Encode(AbCipDataType.Bool, [true]));
|
||
}
|
||
|
||
// ---- the driver ----
|
||
|
||
/// <summary>ScadaBridge's wire shape: an <c>int[]</c> to an INT[3] encodes 3 two-byte elements and writes once, Good.</summary>
|
||
[Fact]
|
||
public async Task An_int_array_to_an_INT_array_encodes_every_element()
|
||
{
|
||
var (drv, factory) = await BuildAsync(AbCipDataType.Int, 3, size: 6);
|
||
|
||
var r = await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 2, 3 })], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||
var tag = factory.Tags["Arr"];
|
||
tag.EncodedArrayType.ShouldBe(AbCipDataType.Int);
|
||
tag.EncodedArrayCount.ShouldBe(3);
|
||
tag.EncodedArrayBytes.ShouldBe(Convert.FromHexString("010002000300"));
|
||
tag.WriteCount.ShouldBe(1);
|
||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||
}
|
||
|
||
/// <summary>A list of a different length is BadOutOfRange; nothing is created, encoded or written.</summary>
|
||
/// <param name="length">The client's element count.</param>
|
||
[Theory]
|
||
[InlineData(2)]
|
||
[InlineData(4)]
|
||
public async Task A_length_mismatch_is_BadOutOfRange_and_nothing_is_written(int length)
|
||
{
|
||
var (drv, factory) = await BuildAsync(AbCipDataType.DInt, 3);
|
||
|
||
var r = await drv.WriteAsync([new WriteRequest("Arr", Enumerable.Range(0, length).ToArray())], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange);
|
||
factory.Tags.ShouldNotContainKey("Arr"); // refused before any runtime exists
|
||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||
}
|
||
|
||
/// <summary>A scalar (or a string) to an array tag is BadTypeMismatch — it used to write element 0 only.</summary>
|
||
[Fact]
|
||
public async Task A_scalar_to_an_array_tag_is_BadTypeMismatch()
|
||
{
|
||
var (drv, factory) = await BuildAsync(AbCipDataType.DInt, 3);
|
||
|
||
var r = await drv.WriteAsync(
|
||
[new WriteRequest("Arr", 5), new WriteRequest("Arr", "1,2,3")], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
||
r[1].StatusCode.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
||
factory.Tags.ShouldNotContainKey("Arr");
|
||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||
}
|
||
|
||
/// <summary>An element that does not fit is BadOutOfRange with no health write and nothing sent.</summary>
|
||
[Fact]
|
||
public async Task An_element_overflow_is_BadOutOfRange_and_nothing_is_sent()
|
||
{
|
||
var (drv, factory) = await BuildAsync(AbCipDataType.Int, 2, size: 4);
|
||
|
||
var r = await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 70000 })], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange);
|
||
factory.Tags["Arr"].WriteCount.ShouldBe(0);
|
||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||
}
|
||
|
||
/// <summary>BOOL / STRING arrays are refused BadNotSupported before any runtime exists.</summary>
|
||
/// <param name="type">The array element type.</param>
|
||
[Theory]
|
||
[InlineData(AbCipDataType.Bool)]
|
||
[InlineData(AbCipDataType.String)]
|
||
public async Task Unencodable_element_types_are_BadNotSupported(AbCipDataType type)
|
||
{
|
||
var (drv, factory) = await BuildAsync(type, 2);
|
||
|
||
var r = await drv.WriteAsync([new WriteRequest("Arr", new object[] { true, false })], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadNotSupported);
|
||
factory.Tags.ShouldNotContainKey("Arr");
|
||
}
|
||
|
||
/// <summary>The controller holding a different shape than authored (size ≠ width × count) is BadTypeMismatch, nothing sent.</summary>
|
||
[Fact]
|
||
public async Task A_controller_shape_mismatch_is_BadTypeMismatch_and_nothing_is_sent()
|
||
{
|
||
var (drv, factory) = await BuildAsync(AbCipDataType.Int, 3, size: 12); // DINT[3] on the controller
|
||
|
||
var r = await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 2, 3 })], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
||
factory.Tags["Arr"].WriteCount.ShouldBe(0);
|
||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||
}
|
||
|
||
/// <summary>
|
||
/// End to end over the REAL native runtime: an <c>int[]</c> to an INT[3] goes out as ONE Write Tag of type
|
||
/// 0xC3 with element count 3 and the three 2-byte elements.
|
||
/// </summary>
|
||
[Fact]
|
||
public async Task A_native_array_write_is_one_write_of_count_elements()
|
||
{
|
||
await using var target = new FakeCipTarget();
|
||
target.ConnectedHandler = req => Task.FromResult<byte[]?>(req[0] switch
|
||
{
|
||
0x52 => [0xD2, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // INT[3] = 0,0,0
|
||
0x4D => [0xCD, 0x00, 0x00, 0x00],
|
||
_ => target.ServiceReply(req),
|
||
});
|
||
var host = $"ab://127.0.0.1:{target.Port}/1,0";
|
||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||
{
|
||
Devices = [new AbCipDeviceOptions(host, AbCipPlcFamily.ControlLogix)],
|
||
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Arr", host, "Arr", AbCipDataType.Int, ElementCount: 3, IsArray: true)),
|
||
Probe = new AbCipProbeOptions { Enabled = false },
|
||
Timeout = TimeSpan.FromSeconds(5),
|
||
}, "abcip-array-native", new NativeAbCipTagFactory(new DeviceSessionPool()));
|
||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||
try
|
||
{
|
||
var r = await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, -2, 300 })], CancellationToken.None);
|
||
|
||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||
var write = target.ConnectedCips.Single(c => c[0] == 0x4D);
|
||
var typeAt = 2 + write[1] * 2;
|
||
write.AsSpan(typeAt).ToArray().ShouldBe(Convert.FromHexString("C300" + "0300" + "0100FEFF2C01"));
|
||
}
|
||
finally
|
||
{
|
||
await drv.DisposeAsync();
|
||
}
|
||
}
|
||
}
|