Files
UA-.NETStandard/tests/Opc.Ua.InformationModel.Tests/AttributeWriteIndexTests.cs
Marc Schier da7412c429 Add native xRegistry events and distinct Resource versions (#4396)
## Summary

- support the xRegistry 0.6.0 distinct Resource/Version hierarchy:
  `Registry -> Group -> Resource -> Versions -> Version`
- instantiate stable logical Resource nodes separately from exact
Version nodes
  while preserving existing Version NodeIds
- delegate logical Resource FileType operations and Version-view
metadata to the
selected default Version with handle pinning and shared writer
reservations
- make Resource and Version delete behavior role-stable and use
`MetaEpoch` /
  `Epoch` respectively
- use stable Resource SourceNodes, exact Version SourceNodes and the
  `Server -> Registry -> Group -> Resource -> Version` notifier chain
- keep a model-metadata-gated client fallback for 0.5 flat Servers; 0.6
Servers
  expose no legacy aliases
- target `HasWoTProjection` at the real stable logical Resource node
- use `AsyncCustomNodeManager` for the registration, fast-path, and
federation managers,
  with awaited lifecycle, node mutations, event delivery, and cleanup
- handle startup rollback, reentrant deletion callbacks, undisclosed
file handles,
  and retryable cleanup without blocking unrelated file operations

## Authoritative dependencies

- companion specification: OPCF-Members/spec-drafts#32 at
  `d3399fca025025a73e5a6b599f850a81d985c060`
- OPC UA binding: xregistry/spec#568
- xRegistry event semantics: xregistry/spec#563, merged as
  `66ee31e47fbadff6ab982ae1516f7d9065e46b64`
- companion NodeSet SHA-256:
  `369f5f3f1814b333e4eeb52bfb4b6a730cdff372505a5f91a54c94f0016ac4cf`

## Review feedback

- keep the WoT Connectivity information-model designation at `1.1`;
draft revisions update the publication date without changing the model
version
- eliminate all 198 unique CA1861 diagnostics across 54 files and remove
seven obsolete project suppressions
- fix the analyzer-clean touched-path CA1307 and the hierarchy follow-up
CA1846/CA1851 diagnostics without dropping legacy-TFM compatibility

- recover the reconcile queue after callback faults/cancellation and
continue accepted pending changes
- remove migration guidance for the xRegistry managers, which are new in
2.0

## Compatibility

- public WoT Connectivity model designation remains `1.1`
- generated event-record ABI, constructors, options, NodeIds, and stored
documents remain compatible
- the generic xRegistry managers are new in 2.0 and implement
`IAsyncNodeManager`;
use `IAsyncNodeManagerFactory` and the async lifecycle hooks (see
`docs\XRegistry.md`)
- persistence schema and document blobs remain compatible unless noted
below

## Validation

- merge validation at `980422f76` with upstream `e8bec27d5`: 454
targeted WoT/materialization/model/registry cases and all 327 XRegistry
cases passed on both net10.0 and net48; all 29 net10 aggregation sample
cases passed
- feedback regressions at `308e8ba74`: 55 targeted queue and live
registry event/projection/lifecycle cases passed on each of net10.0 and
net48
- prior CI: all 266 checks passed at `6489a0908`; the unchanged Ubuntu
Core certificate-counter test and dependent jobs passed on the one-time
retry ([run 34117304035, attempt
2](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/34117304035/attempts/2))
- `Opc.Ua.XRegistry.Tests` — 327 passed on net10.0 and net48 for
migration commit `32c914aaf`
- `Opc.Ua.WotCon.Tests` net10.0 — 1348 passed
- `Opc.Ua.WotCon.Bindings.Tests` net10.0 — 683 passed
- `Opc.Ua.SourceGeneration.Core.Tests` net10.0 — 3807 passed, 8 skipped
- `Opc.Ua.SourceGeneration.Tests` net10.0 — 145 passed
- `Opc.Ua.SourceGeneration.Stack.Tests` net10.0 — 94 passed
- affected XRegistry and WoT client/server projects build successfully
- xRegistry NodeSet is byte-identical to the companion artifact
- repository-wide CA1861 inventory — 198 unique diagnostics across 54
files reduced to zero
- targeted changed-fixture validation — 1,098 passed, 3 skipped, 0
failed
- constrained full `UA.slnx` net10.0 Release build — CA1861 = 0
- `Opc.Ua.XRegistry.Client` net472 and netstandard2.1 builds — 0
warnings, 0 errors
- independent shared-array safety review — no significant issues
- `git diff --check`

The final review regressions cover concurrent reconcile/open, synthetic
logical
handle collisions across default switches, cross-session Close
isolation,
shared writer reservations, logical FileType property/change
propagation,
logical Labels delegation, role-fixed deletion, event sources,
model-version
gating, and stable `HasWoTProjection` targets.

The companion specification and protocol binding are reviewed in the
linked PRs above.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5c79a1ed-6dab-4522-a9fc-6548caa78ad5
Copilot-Session: b605797f-a97a-4cd0-8f12-016d33c2fd88
Copilot-Session: 7ef369a5-2307-4f71-8a11-15af0e72f240
Copilot-Session: e716d455-8397-4d22-9c5a-398847ab7317
2026-09-08 04:46:40 +00:00

667 lines
28 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
namespace Opc.Ua.InformationModel.Tests
{
/// <summary>
/// compliance tests for Attribute Service Set – Write with IndexRange.
/// Validates index range writes on array nodes, error handling for scalar
/// nodes, out-of-bounds ranges, and element preservation semantics.
/// </summary>
[TestFixture]
[Category("Conformance")]
[Category("AttributeWriteIndex")]
public class AttributeWriteIndexTests : TestFixture
{
[Description("Write a single Int32 element at index 0 using IndexRange=\"0\". The server should accept the write and return Good.")]
[Test]
public async Task WriteArrayElementAtIndexZeroAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(
new Variant(s_intValues1.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Index range write at element 0 should return Good.");
}
[Description("Write a single Int32 element at index 2 using IndexRange=\"2\". The server should accept the write and return Good.")]
[Test]
public async Task WriteArrayElementAtIndexTwoAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "2",
Value = new DataValue(
new Variant(s_intValues2.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Index range write at element 2 should return Good.");
}
[Description("Write a subset of three elements using IndexRange=\"1:3\". The server should accept the write and return Good.")]
[Test]
public async Task WriteArraySubsetWithRangeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "1:3",
Value = new DataValue(
new Variant(
s_intValues3.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Index range write for range 1:3 should return Good.");
}
[Description("Write element[0]=999 via IndexRange=\"0\", then read back the full array and verify element 0 was changed to 999.")]
[Test]
public async Task ReadBackAfterIndexWriteVerifyTargetChangedAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(
new Variant(s_intValues1.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
ArrayOf<int> result = await ReadInt32ArrayAsync(nodeId)
.ConfigureAwait(false);
Assert.That(result[0], Is.EqualTo(999),
"Element 0 should have been updated to 999.");
}
[Description("Write element[0]=999 via IndexRange=\"0\", then read back the full array and verify elements 1–4 are unchanged.")]
[Test]
public async Task ReadBackAfterIndexWriteVerifyOthersPreservedAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(
new Variant(s_intValues1.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
ArrayOf<int> result = await ReadInt32ArrayAsync(nodeId)
.ConfigureAwait(false);
Assert.That(result.Count, Is.GreaterThanOrEqualTo(5),
"Array should still have at least 5 elements.");
Assert.That(result[1], Is.EqualTo(20), "Element 1 should be preserved.");
Assert.That(result[2], Is.EqualTo(30), "Element 2 should be preserved.");
Assert.That(result[3], Is.EqualTo(40), "Element 3 should be preserved.");
Assert.That(result[4], Is.EqualTo(50), "Element 4 should be preserved.");
}
[Description("Attempt to write with an IndexRange on a scalar (non-array) node. The server should return BadIndexRangeNoData.")]
[Test]
public async Task WriteWithIndexRangeOnScalarNodeFailsAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(
new Variant(s_intValues4.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]),
Is.False,
"IndexRange write on scalar node should not return Good.");
}
[Description("Write with IndexRange=\"999\" which exceeds the array bounds. The server should return BadIndexRangeNoData.")]
[Test]
public async Task WriteWithIndexRangeOutOfBoundsAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "999",
Value = new DataValue(
new Variant(s_intValues4.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
writeResponse.Results[0],
Is.EqualTo(StatusCodes.BadIndexRangeNoData),
"IndexRange beyond array bounds should return BadIndexRangeNoData.");
}
[Description("Write with an invalid (non-numeric) IndexRange string. The server should return BadIndexRangeInvalid.")]
[Test]
public async Task WriteWithInvalidIndexRangeFormatAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "abc",
Value = new DataValue(
new Variant(s_intValues4.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
writeResponse.Results[0],
Is.EqualTo(StatusCodes.BadIndexRangeInvalid),
"Invalid IndexRange format should return BadIndexRangeInvalid.");
}
[Description("Write with IndexRange on a String array node. Depending on the server implementation this may succeed or return a type mismatch.")]
[Test]
public async Task WriteWithIndexRangeOnStringValueAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayString);
// Seed a known string array.
WriteResponse seedResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(
s_stringValues1.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(seedResponse.Results[0]), Is.True,
"Seeding the string array should succeed.");
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(
s_stringValues2.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
StatusCode status = writeResponse.Results[0];
Assert.That(
StatusCode.IsGood(status) ||
status == StatusCodes.BadTypeMismatch ||
status == StatusCodes.BadIndexRangeNoData,
Is.True,
"IndexRange write on string array should be Good, " +
$"BadTypeMismatch, or BadIndexRangeNoData; got {status}.");
}
[Description("Write a full Int32 array without IndexRange and verify the entire array is replaced successfully.")]
[Test]
public async Task WriteFullArrayWithoutIndexRangeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
ArrayOf<int> newArray = s_intValues3.ToArrayOf();
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(newArray))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Full array write without IndexRange should return Good.");
ArrayOf<int> result = await ReadInt32ArrayAsync(nodeId)
.ConfigureAwait(false);
Assert.That(result.Count, Is.EqualTo(3));
Assert.That(result[0], Is.EqualTo(100));
Assert.That(result[1], Is.EqualTo(200));
Assert.That(result[2], Is.EqualTo(300));
}
[Description("Write range \"1:2\" with new values, then verify that elements at index 0 and 3 remain unchanged from the original array.")]
[Test]
public async Task WriteWithIndexRangeSubsetVerifyPreservationAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
await WriteKnownInt32ArrayAsync(nodeId).ConfigureAwait(false);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "1:2",
Value = new DataValue(
new Variant(
s_intValues5.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Index range write 1:2 should return Good.");
ArrayOf<int> result = await ReadInt32ArrayAsync(nodeId)
.ConfigureAwait(false);
Assert.That(result[0], Is.EqualTo(10),
"Element 0 should be preserved as 10.");
Assert.That(result[1], Is.EqualTo(777),
"Element 1 should be updated to 777.");
Assert.That(result[2], Is.EqualTo(888),
"Element 2 should be updated to 888.");
Assert.That(result[3], Is.EqualTo(40),
"Element 3 should be preserved as 40.");
}
[Description("Write a single boolean element at index 0 using IndexRange=\"0\" on the boolean array node. The server should return Good.")]
[Test]
public async Task WriteIndexRangeOnBooleanArrayAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayBoolean);
// Seed a known boolean array.
WriteResponse seedResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(
s_booleanValues1.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(seedResponse.Results[0]), Is.True,
"Seeding the boolean array should succeed.");
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(
s_booleanValues2.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"IndexRange write on boolean array element 0 should return Good.");
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
Assert.That(
readResponse.Results[0].WrappedValue.TryGetValue(out ArrayOf<bool> result),
Is.True, "Value should be a Boolean array.");
Assert.That(result[0], Is.True,
"Element 0 should have been updated to true.");
}
[Description("Write to last 3 elements of array using IndexRange.")]
[Test]
public async Task WriteArrayLastThreeElementsAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(s_intValues6))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
}
[Description("IndexRange for single element of a multi-dimensional array.")]
[Test]
public async Task WriteMultiDimArraySingleElementAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(s_intValues6))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
}
[Description("IndexRange writing first element from each dimension.")]
[Test]
public async Task WriteMultiDimArrayFirstElementsAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(s_intValues6))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
}
[Description("IndexRange writing first 3 elements of each dimension.")]
[Test]
public async Task WriteMultiDimArrayFirstThreeElementsAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(s_intValues6))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
}
[Description("IndexRange writing last 3 elements of each dimension.")]
[Test]
public async Task WriteMultiDimArrayLastThreeElementsAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(s_intValues6))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
}
[Description("Write to subset and verify Timestamps are updated.")]
[Test]
public async Task WriteSubsetVerifyTimestampsAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "0",
Value = new DataValue(new Variant(s_intValues6))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
}
[Description("Write to invalid IndexRange \"2:1\".")]
[Test]
public async Task WriteInvalidIndexRangeReversedAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "999999",
Value = new DataValue(new Variant(s_intValues7))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsBad(response.Results[0]), Is.True);
}
[Description("Invalid IndexRange syntax \"2:2\".")]
[Test]
public async Task WriteInvalidIndexRangeSameValueAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "999999",
Value = new DataValue(new Variant(s_intValues7))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsBad(response.Results[0]), Is.True);
}
[Description("Invalid IndexRange syntax \"-1:0\".")]
[Test]
public async Task WriteInvalidIndexRangeNegativeAsync()
{
ArrayOf<WriteValue> wv = new WriteValue[]
{
new() {
NodeId = ToNodeId(Constants.ScalarStaticArrayInt32),
AttributeId = Attributes.Value,
IndexRange = "999999",
Value = new DataValue(new Variant(s_intValues7))
}
}.ToArrayOf();
WriteResponse response = await Session.WriteAsync(null, wv, CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsBad(response.Results[0]), Is.True);
}
/// <summary>
/// Writes the <see cref="KnownArray"/> to the Int32 array node so
/// that subsequent index-range tests start from a deterministic state.
/// </summary>
private async Task WriteKnownInt32ArrayAsync(NodeId nodeId)
{
WriteResponse response = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(
new Variant(KnownArray.ToArrayOf()))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(response.Results[0]), Is.True,
"Seeding the known array should succeed.");
}
/// <summary>
/// Reads the full Int32 array back from the server.
/// </summary>
private async Task<ArrayOf<int>> ReadInt32ArrayAsync(NodeId nodeId)
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"Read of Int32 array should return Good.");
Assert.That(
response.Results[0].WrappedValue.TryGetValue(out ArrayOf<int> result),
Is.True, "Value should be an Int32 array.");
return result;
}
private static readonly int[] KnownArray = [10, 20, 30, 40, 50];
private static readonly int[] s_intValues1 = [999];
private static readonly int[] s_intValues2 = [888];
private static readonly int[] s_intValues3 = [100, 200, 300];
private static readonly int[] s_intValues4 = [1];
private static readonly string[] s_stringValues1 = ["A", "B", "C"];
private static readonly string[] s_stringValues2 = ["Z"];
private static readonly int[] s_intValues5 = [777, 888];
private static readonly bool[] s_booleanValues1 = [false, true, false];
private static readonly bool[] s_booleanValues2 = [true];
private static readonly int[] s_intValues6 = [42];
private static readonly int[] s_intValues7 = [99];
}
}