diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
index 5a441bb..5c7cfa3 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
@@ -54,6 +54,15 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
/// OPC UA StatusCode used when S7 returns ErrorCode.WrongCPU / PUT/GET disabled.
private const uint StatusBadDeviceFailure = 0x80550000u;
+ ///
+ /// Hard upper bound on . The S7 PDU envelope
+ /// for negotiated default 240-byte and extended 960-byte payloads cannot fit a single
+ /// byte-range read larger than ~960 bytes, so a Float64 array of more than ~120
+ /// elements is already lossy. 8000 is an order-of-magnitude generous ceiling that still
+ /// rejects obvious config typos (e.g. ElementCount = 65535) at init time.
+ ///
+ internal const int MaxArrayElements = 8000;
+
private readonly Dictionary _tagsByName = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _parsedByName = new(StringComparer.OrdinalIgnoreCase);
@@ -85,6 +94,32 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
_health = new DriverHealth(DriverState.Initializing, null, null);
try
{
+ // Parse + validate every tag before opening the TCP socket so config bugs
+ // (bad address, oversized array, unsupported array element) surface as
+ // FormatException without waiting on a connect timeout. Per the v1 driver-config
+ // story this lets the Admin UI's "Save" round-trip stay sub-second on bad input.
+ _tagsByName.Clear();
+ _parsedByName.Clear();
+ foreach (var t in _options.Tags)
+ {
+ var parsed = S7AddressParser.Parse(t.Address); // throws FormatException
+ if (t.ElementCount is int n && n > 1)
+ {
+ // Array sanity: cap at S7 PDU realistic limit, reject variable-width
+ // element types and BOOL (packed-bit layout) up-front so a config typo
+ // fails at init instead of surfacing as BadInternalError on every read.
+ if (n > MaxArrayElements)
+ throw new FormatException(
+ $"S7 tag '{t.Name}' ElementCount {n} exceeds S7 PDU realistic limit ({MaxArrayElements})");
+ if (!IsArrayElementSupported(t.DataType))
+ throw new FormatException(
+ $"S7 tag '{t.Name}' DataType {t.DataType} not supported as an array element " +
+ $"(variable-width string types and BOOL packed-bit arrays are a follow-up)");
+ }
+ _tagsByName[t.Name] = t;
+ _parsedByName[t.Name] = parsed;
+ }
+
var plc = new Plc(_options.CpuType, _options.Host, _options.Port, _options.Rack, _options.Slot);
// S7netplus writes timeouts into the underlying TcpClient via Plc.WriteTimeout /
// Plc.ReadTimeout (milliseconds). Set before OpenAsync so the handshake itself
@@ -98,18 +133,6 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
Plc = plc;
- // Parse every tag's address once at init so config typos fail fast here instead
- // of surfacing as BadInternalError on every Read against the bad tag. The parser
- // also rejects bit-offset > 7, DB 0, unknown area letters, etc.
- _tagsByName.Clear();
- _parsedByName.Clear();
- foreach (var t in _options.Tags)
- {
- var parsed = S7AddressParser.Parse(t.Address); // throws FormatException
- _tagsByName[t.Name] = t;
- _parsedByName[t.Name] = parsed;
- }
-
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
// Kick off the probe loop once the connection is up. Initial HostState stays
@@ -222,6 +245,26 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
{
var addr = _parsedByName[tag.Name];
+ // 1-D array path: one byte-range read covering N×elementBytes, sliced client-side.
+ // Init-time validation guarantees only fixed-width element types reach here.
+ if (tag.ElementCount is int n && n > 1)
+ {
+ var elemBytes = ArrayElementBytes(tag.DataType);
+ var totalBytes = checked(n * elemBytes);
+ if (addr.Size == S7Size.Bit)
+ throw new System.IO.InvalidDataException(
+ $"S7 Read type-mismatch: tag '{tag.Name}' is array of {tag.DataType} but address '{tag.Address}' " +
+ $"parsed as bit-access; arrays require byte-addressing");
+
+ var arrBytes = await plc.ReadBytesAsync(MapArea(addr.Area), addr.DbNumber, addr.ByteOffset, totalBytes, ct)
+ .ConfigureAwait(false);
+ if (arrBytes is null || arrBytes.Length != totalBytes)
+ throw new System.IO.InvalidDataException(
+ $"S7.Net returned {arrBytes?.Length ?? 0} bytes for array '{tag.Address}' (n={n}), expected {totalBytes}");
+
+ return SliceArray(arrBytes, tag.DataType, n, elemBytes);
+ }
+
// String-shaped types (STRING/WSTRING/CHAR/WCHAR): S7.Net's string-keyed ReadAsync
// has no syntax for these, so the driver issues a raw byte read and decodes via
// S7StringCodec. Wire order is big-endian for the WSTRING/WCHAR UTF-16 payload.
@@ -421,6 +464,23 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
private async Task WriteOneAsync(global::S7.Net.Plc plc, S7TagDefinition tag, object? value, CancellationToken ct)
{
+ // 1-D array path: pack all N elements into a single buffer then push via WriteBytesAsync.
+ // Init-time validation guarantees only fixed-width element types reach here.
+ if (tag.ElementCount is int n && n > 1)
+ {
+ var addr = _parsedByName[tag.Name];
+ if (addr.Size == S7Size.Bit)
+ throw new InvalidOperationException(
+ $"S7 Write type-mismatch: tag '{tag.Name}' is array of {tag.DataType} but address '{tag.Address}' " +
+ $"parsed as bit-access; arrays require byte-addressing");
+ if (value is null)
+ throw new ArgumentNullException(nameof(value));
+ var elemBytes = ArrayElementBytes(tag.DataType);
+ var buf = PackArray(value, tag.DataType, n, elemBytes, tag.Name);
+ await plc.WriteBytesAsync(MapArea(addr.Area), addr.DbNumber, addr.ByteOffset, buf, ct).ConfigureAwait(false);
+ return;
+ }
+
// String-shaped types: encode via S7StringCodec then push via WriteBytesAsync. The
// codec rejects out-of-range lengths and non-ASCII for CHAR — we let the resulting
// ArgumentException bubble out so the WriteAsync caller maps it to BadInternalError.
@@ -531,11 +591,12 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
var folder = builder.Folder("S7", "S7");
foreach (var t in _options.Tags)
{
+ var isArr = t.ElementCount is int ec && ec > 1;
folder.Variable(t.Name, t.Name, new DriverAttributeInfo(
FullName: t.Name,
DriverDataType: MapDataType(t.DataType),
- IsArray: false,
- ArrayDim: null,
+ IsArray: isArr,
+ ArrayDim: isArr ? (uint)t.ElementCount!.Value : null,
SecurityClass: t.Writable ? SecurityClassification.Operate : SecurityClassification.ViewOnly,
IsHistorized: false,
IsAlarm: false,
@@ -544,6 +605,173 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
return Task.CompletedTask;
}
+ ///
+ /// True when can be used as an array element. Variable-width string
+ /// types and BOOL (packed-bit layout) are rejected — both need bespoke addressing
+ /// beyond a flat N × elementBytes byte-range read and ship as a follow-up.
+ ///
+ internal static bool IsArrayElementSupported(S7DataType t) => t is
+ S7DataType.Byte or
+ S7DataType.Int16 or S7DataType.UInt16 or
+ S7DataType.Int32 or S7DataType.UInt32 or
+ S7DataType.Int64 or S7DataType.UInt64 or
+ S7DataType.Float32 or S7DataType.Float64 or
+ S7DataType.Date or S7DataType.Time or S7DataType.TimeOfDay;
+
+ ///
+ /// On-wire bytes per array element for the supported fixed-width element types. DATE
+ /// is a 16-bit days-since-1990 counter, TIME and TOD are 32-bit ms counters.
+ ///
+ internal static int ArrayElementBytes(S7DataType t) => t switch
+ {
+ S7DataType.Byte => 1,
+ S7DataType.Int16 or S7DataType.UInt16 or S7DataType.Date => 2,
+ S7DataType.Int32 or S7DataType.UInt32 or S7DataType.Float32
+ or S7DataType.Time or S7DataType.TimeOfDay => 4,
+ S7DataType.Int64 or S7DataType.UInt64 or S7DataType.Float64 => 8,
+ _ => throw new InvalidOperationException($"S7 array element bytes undefined for {t}"),
+ };
+
+ ///
+ /// Slice a flat S7 byte buffer into a typed array using the existing big-endian scalar
+ /// codec for each element. Returns the typed array boxed as object so the
+ /// surface can carry it without further conversion.
+ ///
+ internal static object SliceArray(byte[] bytes, S7DataType t, int n, int elemBytes)
+ {
+ switch (t)
+ {
+ case S7DataType.Byte:
+ {
+ var a = new byte[n];
+ Buffer.BlockCopy(bytes, 0, a, 0, n);
+ return a;
+ }
+ case S7DataType.Int16:
+ {
+ var a = new short[n];
+ for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadInt16BigEndian(bytes.AsSpan(i * elemBytes, 2));
+ return a;
+ }
+ case S7DataType.UInt16:
+ {
+ var a = new ushort[n];
+ for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(i * elemBytes, 2));
+ return a;
+ }
+ case S7DataType.Int32:
+ {
+ var a = new int[n];
+ for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(i * elemBytes, 4));
+ return a;
+ }
+ case S7DataType.UInt32:
+ {
+ var a = new uint[n];
+ for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(i * elemBytes, 4));
+ return a;
+ }
+ case S7DataType.Int64:
+ {
+ var a = new long[n];
+ for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadInt64BigEndian(bytes.AsSpan(i * elemBytes, 8));
+ return a;
+ }
+ case S7DataType.UInt64:
+ {
+ var a = new ulong[n];
+ for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadUInt64BigEndian(bytes.AsSpan(i * elemBytes, 8));
+ return a;
+ }
+ case S7DataType.Float32:
+ {
+ var a = new float[n];
+ for (var i = 0; i < n; i++)
+ a[i] = BitConverter.UInt32BitsToSingle(BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(i * elemBytes, 4)));
+ return a;
+ }
+ case S7DataType.Float64:
+ {
+ var a = new double[n];
+ for (var i = 0; i < n; i++)
+ a[i] = BitConverter.UInt64BitsToDouble(BinaryPrimitives.ReadUInt64BigEndian(bytes.AsSpan(i * elemBytes, 8)));
+ return a;
+ }
+ case S7DataType.Date:
+ {
+ var a = new DateTime[n];
+ for (var i = 0; i < n; i++)
+ a[i] = S7DateTimeCodec.DecodeDate(bytes.AsSpan(i * elemBytes, 2));
+ return a;
+ }
+ case S7DataType.Time:
+ {
+ // Surface as Int32 ms — matches the scalar Time read path (driver-specs §5).
+ var a = new int[n];
+ for (var i = 0; i < n; i++)
+ a[i] = (int)S7DateTimeCodec.DecodeTime(bytes.AsSpan(i * elemBytes, 4)).TotalMilliseconds;
+ return a;
+ }
+ case S7DataType.TimeOfDay:
+ {
+ var a = new int[n];
+ for (var i = 0; i < n; i++)
+ a[i] = (int)S7DateTimeCodec.DecodeTod(bytes.AsSpan(i * elemBytes, 4)).TotalMilliseconds;
+ return a;
+ }
+ default:
+ throw new InvalidOperationException($"S7 array slice undefined for {t}");
+ }
+ }
+
+ ///
+ /// Pack a caller-supplied array (object) into the on-wire S7 byte layout for
+ /// . Accepts both the strongly-typed array
+ /// (short[], int[], ...) and a generic System.Array / IEnumerable
+ /// so OPC UA Variant-boxed values flow through unchanged.
+ ///
+ internal static byte[] PackArray(object value, S7DataType elementType, int n, int elemBytes, string tagName)
+ {
+ if (value is not System.Collections.IEnumerable enumerable)
+ throw new ArgumentException($"S7 Write tag '{tagName}' is array but value is not enumerable (got {value.GetType().Name})", nameof(value));
+
+ var buf = new byte[n * elemBytes];
+ var i = 0;
+ foreach (var raw in enumerable)
+ {
+ if (i >= n)
+ throw new ArgumentException($"S7 Write tag '{tagName}': value has more than ElementCount={n} elements", nameof(value));
+ var span = buf.AsSpan(i * elemBytes, elemBytes);
+ switch (elementType)
+ {
+ case S7DataType.Byte: span[0] = Convert.ToByte(raw); break;
+ case S7DataType.Int16: BinaryPrimitives.WriteInt16BigEndian(span, Convert.ToInt16(raw)); break;
+ case S7DataType.UInt16: BinaryPrimitives.WriteUInt16BigEndian(span, Convert.ToUInt16(raw)); break;
+ case S7DataType.Int32: BinaryPrimitives.WriteInt32BigEndian(span, Convert.ToInt32(raw)); break;
+ case S7DataType.UInt32: BinaryPrimitives.WriteUInt32BigEndian(span, Convert.ToUInt32(raw)); break;
+ case S7DataType.Int64: BinaryPrimitives.WriteInt64BigEndian(span, Convert.ToInt64(raw)); break;
+ case S7DataType.UInt64: BinaryPrimitives.WriteUInt64BigEndian(span, Convert.ToUInt64(raw)); break;
+ case S7DataType.Float32: BinaryPrimitives.WriteUInt32BigEndian(span, BitConverter.SingleToUInt32Bits(Convert.ToSingle(raw))); break;
+ case S7DataType.Float64: BinaryPrimitives.WriteUInt64BigEndian(span, BitConverter.DoubleToUInt64Bits(Convert.ToDouble(raw))); break;
+ case S7DataType.Date:
+ S7DateTimeCodec.EncodeDate(Convert.ToDateTime(raw)).CopyTo(span);
+ break;
+ case S7DataType.Time:
+ S7DateTimeCodec.EncodeTime(raw is TimeSpan ts ? ts : TimeSpan.FromMilliseconds(Convert.ToInt32(raw))).CopyTo(span);
+ break;
+ case S7DataType.TimeOfDay:
+ S7DateTimeCodec.EncodeTod(raw is TimeSpan tod ? tod : TimeSpan.FromMilliseconds(Convert.ToInt64(raw))).CopyTo(span);
+ break;
+ default:
+ throw new InvalidOperationException($"S7 array pack undefined for {elementType}");
+ }
+ i++;
+ }
+ if (i != n)
+ throw new ArgumentException($"S7 Write tag '{tagName}': value had {i} elements, expected ElementCount={n}", nameof(value));
+ return buf;
+ }
+
private static DriverDataType MapDataType(S7DataType t) => t switch
{
S7DataType.Bool => DriverDataType.Boolean,
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverOptions.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverOptions.cs
index 6b1cd26..3832f5c 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverOptions.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverOptions.cs
@@ -95,13 +95,23 @@ public sealed class S7ProbeOptions
/// value can be written again without side-effects. Unsafe: M (merker) bits or Q (output)
/// coils that drive edge-triggered routines in the PLC program.
///
+///
+/// Optional 1-D array length. null (or 1) = scalar tag; > 1 = array.
+/// The driver issues one byte-range read covering ElementCount × bytes-per-element
+/// and slices client-side via the existing scalar codec. Multi-dim arrays are deferred;
+/// array-of-UDT lands with PR-S7-D2. Variable-width element types
+/// (STRING/WSTRING/CHAR/WCHAR) and BOOL (packed bits) are rejected at init time —
+/// they need bespoke layout handling and are tracked as a follow-up. Capped at 8000 to
+/// keep the byte-range request inside a single S7 PDU envelope.
+///
public sealed record S7TagDefinition(
string Name,
string Address,
S7DataType DataType,
bool Writable = true,
int StringLength = 254,
- bool WriteIdempotent = false);
+ bool WriteIdempotent = false,
+ int? ElementCount = null);
public enum S7DataType
{
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverArrayTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverArrayTests.cs
new file mode 100644
index 0000000..5aaf936
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverArrayTests.cs
@@ -0,0 +1,146 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
+
+///
+/// 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 InternalsVisibleTo) and
+/// the init-time validation that rejects unsupported element types and over-budget
+/// ElementCount values up-front.
+///
+[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(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(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(async () =>
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
+ }
+}