Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverArrayTests.cs
Joseph Doherty ce98c2ada3 Auto: s7-a4 — array tags (ValueRank=1)
- S7TagDefinition gets optional ElementCount; >1 marks the tag as a 1-D array.
- ReadOneAsync / WriteOneAsync: one byte-range Read/WriteBytesAsync covering
  N × elementBytes, sliced/packed client-side via the existing big-endian scalar
  codecs and S7DateTimeCodec.
- DiscoverAsync surfaces IsArray=true and ArrayDim=ElementCount → ValueRank=1.
- Init-time validation (now ahead of TCP open) caps ElementCount at 8000 and
  rejects unsupported element types: STRING/WSTRING/CHAR/WCHAR (variable-width)
  and BOOL (packed-bit layout) — both follow-ups.
- Supported element types: Byte, Int16/UInt16, Int32/UInt32, Int64/UInt64,
  Float32, Float64, Date, Time, TimeOfDay.

Closes #290
2026-04-25 16:49:02 -04:00

147 lines
6.2 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
/// <summary>
/// Unit tests for the S7 driver's 1-D array surface (PR-S7-A4). Wire-level round-trip
/// tests against a live S7 still need a real PLC, so these tests exercise the
/// driver's slice / pack helpers directly (visible via <c>InternalsVisibleTo</c>) and
/// the init-time validation that rejects unsupported element types and over-budget
/// ElementCount values up-front.
/// </summary>
[Trait("Category", "Unit")]
public sealed class S7DriverArrayTests
{
[Fact]
public void Int16_array_roundtrip_via_pack_then_slice()
{
// Big-endian 16-bit elements: validate that PackArray + SliceArray round-trip
// a representative range including negatives and the boundary values.
var input = new short[] { 0, 1, -1, short.MinValue, short.MaxValue, 12345 };
var elem = S7Driver.ArrayElementBytes(S7DataType.Int16);
var bytes = S7Driver.PackArray(input, S7DataType.Int16, input.Length, elem, "t");
bytes.Length.ShouldBe(input.Length * elem);
// Sanity-check big-endian layout: element[2] = -1 → 0xFFFF at byte offset 4.
bytes[4].ShouldBe((byte)0xFF); bytes[5].ShouldBe((byte)0xFF);
var output = (short[])S7Driver.SliceArray(bytes, S7DataType.Int16, input.Length, elem);
output.ShouldBe(input);
}
[Fact]
public void Int32_array_roundtrip_via_pack_then_slice()
{
var input = new[] { 0, 1, -1, int.MinValue, int.MaxValue, 0x12345678 };
var elem = S7Driver.ArrayElementBytes(S7DataType.Int32);
var bytes = S7Driver.PackArray(input, S7DataType.Int32, input.Length, elem, "t");
var output = (int[])S7Driver.SliceArray(bytes, S7DataType.Int32, input.Length, elem);
output.ShouldBe(input);
}
[Fact]
public void Float32_array_roundtrip_via_pack_then_slice()
{
var input = new[] { 0f, 1.5f, -3.25f, float.MinValue, float.MaxValue, float.Epsilon };
var elem = S7Driver.ArrayElementBytes(S7DataType.Float32);
var bytes = S7Driver.PackArray(input, S7DataType.Float32, input.Length, elem, "t");
var output = (float[])S7Driver.SliceArray(bytes, S7DataType.Float32, input.Length, elem);
output.ShouldBe(input);
}
[Fact]
public void Float64_array_roundtrip_via_pack_then_slice()
{
var input = new[] { 0d, Math.PI, -Math.E, double.MinValue, double.MaxValue };
var elem = S7Driver.ArrayElementBytes(S7DataType.Float64);
var bytes = S7Driver.PackArray(input, S7DataType.Float64, input.Length, elem, "t");
var output = (double[])S7Driver.SliceArray(bytes, S7DataType.Float64, input.Length, elem);
output.ShouldBe(input);
}
[Fact]
public void IsArrayElementSupported_rejects_strings_and_bool()
{
// Variable-width string types and BOOL (packed-bit layout) are explicit follow-ups —
// surface them as init-time rejections rather than mysterious BadInternalError on read.
S7Driver.IsArrayElementSupported(S7DataType.Bool).ShouldBeFalse();
S7Driver.IsArrayElementSupported(S7DataType.String).ShouldBeFalse();
S7Driver.IsArrayElementSupported(S7DataType.WString).ShouldBeFalse();
S7Driver.IsArrayElementSupported(S7DataType.Char).ShouldBeFalse();
S7Driver.IsArrayElementSupported(S7DataType.WChar).ShouldBeFalse();
S7Driver.IsArrayElementSupported(S7DataType.Int16).ShouldBeTrue();
S7Driver.IsArrayElementSupported(S7DataType.Float64).ShouldBeTrue();
}
[Fact]
public async Task Initialize_rejects_array_of_String_with_FormatException()
{
// Init-time guard: even if the address parses cleanly, an array of variable-width
// STRING is not yet supported and must fail-fast at config-load. The driver never
// gets as far as opening the TcpClient because parsing is the first step.
var opts = new S7DriverOptions
{
Host = "192.0.2.1", // reserved — TCP would never connect anyway
Timeout = TimeSpan.FromMilliseconds(250),
Tags =
[
new S7TagDefinition(
Name: "BadStrArr",
Address: "DB1.DBB0",
DataType: S7DataType.String,
ElementCount: 4),
],
};
using var drv = new S7Driver(opts, "s7-arr-bad-string");
await Should.ThrowAsync<FormatException>(async () =>
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
}
[Fact]
public async Task Initialize_rejects_array_of_Bool_with_FormatException()
{
// BOOL arrays are stored as packed bits (one bit per element rounded up to a byte) —
// the byte-range read trick used here for word-shaped elements doesn't generalize, so
// arrays of Bool are explicitly out-of-scope for PR-S7-A4 and reject at init.
var opts = new S7DriverOptions
{
Host = "192.0.2.1",
Timeout = TimeSpan.FromMilliseconds(250),
Tags =
[
new S7TagDefinition(
Name: "BadBoolArr",
Address: "DB1.DBX0.0",
DataType: S7DataType.Bool,
ElementCount: 8),
],
};
using var drv = new S7Driver(opts, "s7-arr-bad-bool");
await Should.ThrowAsync<FormatException>(async () =>
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
}
[Fact]
public async Task Initialize_rejects_oversized_ElementCount_with_FormatException()
{
var opts = new S7DriverOptions
{
Host = "192.0.2.1",
Timeout = TimeSpan.FromMilliseconds(250),
Tags =
[
new S7TagDefinition(
Name: "TooBig",
Address: "DB1.DBW0",
DataType: S7DataType.Int16,
ElementCount: S7Driver.MaxArrayElements + 1),
],
};
using var drv = new S7Driver(opts, "s7-arr-too-big");
await Should.ThrowAsync<FormatException>(async () =>
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
}
}