- 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
147 lines
6.2 KiB
C#
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));
|
|
}
|
|
}
|