Files
UA-.NETStandard/tests/Opc.Ua.InformationModel.Tests/AttributeWriteTests.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

971 lines
39 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;
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 Values.
/// Based on test scripts: Attribute Write 001–018 and Err tests.
/// </summary>
[TestFixture]
[Category("Conformance")]
[Category("AttributeWrite")]
public class AttributeWriteTests : TestFixture
{
[Description("Write a single Int32 value and read it back.")]
[Test]
public async Task AttributeWrite001WriteSingleValueAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(42))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Int32 value 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(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
Assert.That(readResponse.Results[0].WrappedValue.TryGetValue(out int readBack), Is.True);
Assert.That(readBack, Is.EqualTo(42));
}
[Description("Write multiple values in a single call. Both should return Good.")]
[Test]
public async Task AttributeWrite002WriteMultipleValuesAsync()
{
NodeId int32Node = ToNodeId(Constants.ScalarStaticInt32);
NodeId doubleNode = ToNodeId(Constants.ScalarStaticDouble);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = int32Node,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(100))
},
new() {
NodeId = doubleNode,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(3.14))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(2));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Int32 value should return Good.");
Assert.That(StatusCode.IsGood(writeResponse.Results[1]), Is.True,
"Write of Double value should return Good.");
}
[Description("Write a Boolean value and read it back.")]
[Test]
public async Task AttributeWrite003WriteBooleanAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticBoolean);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(true))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Boolean value 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(readResponse.Results[0].WrappedValue.TryGetValue(out bool readBack), Is.True);
Assert.That(readBack, Is.True);
}
[Description("Write a negative Int32 value and read it back.")]
[Test]
public async Task AttributeWrite004WriteInt32Async()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(-12345))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True);
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results[0].WrappedValue.TryGetValue(out int readBack), Is.True);
Assert.That(readBack, Is.EqualTo(-12345));
}
[Description("Write a Double value and read it back.")]
[Test]
public async Task AttributeWrite005WriteDoubleAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticDouble);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(2.71828))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True);
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results[0].WrappedValue.TryGetValue(out double readBack), Is.True);
Assert.That(readBack, Is.EqualTo(2.71828));
}
[Description("Write a String value and read it back.")]
[Test]
public async Task AttributeWrite006WriteStringAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticString);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant("Hello World"))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True);
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results[0].WrappedValue.TryGetValue(out string readBack), Is.True);
Assert.That(readBack, Is.EqualTo("Hello World"));
}
[Description("Write a DateTime value. Expect Good.")]
[Test]
public async Task AttributeWrite007WriteDateTimeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticDateTime);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(DateTime.UtcNow))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of DateTime value should return Good.");
}
[Description("Write a ByteString value. Expect Good.")]
[Test]
public async Task AttributeWrite008WriteByteStringAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticByteString);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(new ByteString(new byte[] { 0x01, 0x02, 0x03 })))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of ByteString value should return Good.");
}
[Description("Write a Float value and read it back.")]
[Test]
public async Task AttributeWrite009WriteFloatAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticFloat);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(1.5f))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True);
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results[0].WrappedValue.TryGetValue(out float readBack), Is.True);
Assert.That(readBack, Is.EqualTo(1.5f));
}
[Description("Write an SByte value. Expect Good.")]
[Test]
public async Task AttributeWrite010WriteSByteAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticSByte);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((sbyte)-10))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of SByte value should return Good.");
}
[Description("Write a Byte value. Expect Good.")]
[Test]
public async Task AttributeWrite011WriteByteAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticByte);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((byte)255))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Byte value should return Good.");
}
[Description("Write an Int16 value. Expect Good.")]
[Test]
public async Task AttributeWrite012WriteInt16Async()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt16);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((short)1000))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Int16 value should return Good.");
}
[Description("Write a UInt16 value. Expect Good.")]
[Test]
public async Task AttributeWrite013WriteUInt16Async()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticUInt16);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((ushort)50000))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of UInt16 value should return Good.");
}
[Description("Write an Int64 value. Expect Good.")]
[Test]
public async Task AttributeWrite014WriteInt64Async()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt64);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(123456789L))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Int64 value should return Good.");
}
[Description("Write a UInt64 value. Expect Good.")]
[Test]
public async Task AttributeWrite015WriteUInt64Async()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticUInt64);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(999999999UL))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of UInt64 value should return Good.");
}
[Description("Write a Guid value. Expect Good.")]
[Test]
public async Task AttributeWrite016WriteGuidAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticGuid);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(new Uuid(Guid.NewGuid())))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
"Write of Guid value should return Good.");
}
[Description("Write Boolean, Int32, String, and Double in one call. Read all back and verify.")]
[Test]
public async Task AttributeWrite017WriteAndReadBackMultipleTypesAsync()
{
NodeId boolNode = ToNodeId(Constants.ScalarStaticBoolean);
NodeId int32Node = ToNodeId(Constants.ScalarStaticInt32);
NodeId stringNode = ToNodeId(Constants.ScalarStaticString);
NodeId doubleNode = ToNodeId(Constants.ScalarStaticDouble);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = boolNode,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(false))
},
new() {
NodeId = int32Node,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(777))
},
new() {
NodeId = stringNode,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant("MultiWrite"))
},
new() {
NodeId = doubleNode,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(9.81))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(4));
for (int i = 0; i < 4; i++)
{
Assert.That(StatusCode.IsGood(writeResponse.Results[i]), Is.True,
$"Write result at index {i} should be Good.");
}
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = boolNode, AttributeId = Attributes.Value },
new() { NodeId = int32Node, AttributeId = Attributes.Value },
new() { NodeId = stringNode, AttributeId = Attributes.Value },
new() { NodeId = doubleNode, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(4));
Assert.That(readResponse.Results[0].WrappedValue.TryGetValue(out bool boolVal), Is.True);
Assert.That(boolVal, Is.False);
Assert.That(readResponse.Results[1].WrappedValue.TryGetValue(out int intVal), Is.True);
Assert.That(intVal, Is.EqualTo(777));
Assert.That(readResponse.Results[2].WrappedValue.TryGetValue(out string strVal), Is.True);
Assert.That(strVal, Is.EqualTo("MultiWrite"));
Assert.That(readResponse.Results[3].WrappedValue.TryGetValue(out double dblVal), Is.True);
Assert.That(dblVal, Is.EqualTo(9.81));
}
[Description("Write an Int32 array and read it back. Verify length.")]
[Test]
public async Task AttributeWrite018WriteArrayValueAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
ArrayOf<int> arrayValue = s_intValues1.ToArrayOf();
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(arrayValue))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
$"Write of Int32 array should return Good, got {writeResponse.Results[0]}.");
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<int> result), Is.True);
Assert.That(result.Count, Is.EqualTo(3));
}
[Description("Err 001: Write to an invalid NodeId from Constants. Expect BadNodeIdUnknown.")]
[Test]
public async Task AttributeWriteErr001WriteToInvalidNodeIdAsync()
{
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = Constants.InvalidNodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(0))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(writeResponse.Results[0], Is.EqualTo(StatusCodes.BadNodeIdUnknown),
"Writing to an invalid NodeId should return BadNodeIdUnknown.");
}
[Description("Err 002: Write a String to a node that expects Int32. Expect BadTypeMismatch.")]
[Test]
public async Task AttributeWriteErr002WriteWrongDataTypeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant("hello"))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(writeResponse.Results[0], Is.EqualTo(StatusCodes.BadTypeMismatch),
"Writing wrong data type should return BadTypeMismatch.");
}
[Description("Err 003: Write to a completely made-up NodeId. Expect BadNodeIdUnknown.")]
[Test]
public async Task AttributeWriteErr003WriteBadNodeIdUnknownAsync()
{
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = new NodeId(99999, 99),
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(0))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(writeResponse.Results[0], Is.EqualTo(StatusCodes.BadNodeIdUnknown),
"Writing to a non-existent NodeId should return BadNodeIdUnknown.");
}
[Description("Write Int32 with SourceTimestamp set to UtcNow. Server may accept or reject the timestamp.")]
[Test]
public async Task AttributeWriteWithSourceTimestampAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(99), StatusCodes.Good, DateTime.UtcNow);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with ServerTimestamp set. Most servers ignore or reject server timestamp on write.")]
[Test]
public async Task AttributeWriteWithServerTimestampAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(100), StatusCodes.Good, DateTimeUtc.MinValue, DateTime.UtcNow);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with both SourceTimestamp and ServerTimestamp set.")]
[Test]
public async Task AttributeWriteWithBothTimestampsAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
DateTime now = DateTime.UtcNow;
var dv = new DataValue(new Variant(101), StatusCodes.Good, now, now);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with explicit StatusCode.Good.")]
[Test]
public async Task AttributeWriteWithStatusCodeGoodAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(101), StatusCodes.Good);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with StatusCode.Bad. Some servers do not allow writing bad status.")]
[Test]
public async Task AttributeWriteWithStatusCodeBadAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(102), StatusCodes.Bad);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with SourceTimestamp set to one year in the past.")]
[Test]
public async Task AttributeWriteWithSourceTimestampInPastAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(103), StatusCodes.Good, DateTime.UtcNow.AddYears(-1));
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with SourceTimestamp set to one hour in the future.")]
[Test]
public async Task AttributeWriteWithSourceTimestampInFutureAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(104), StatusCodes.Good, DateTime.UtcNow.AddHours(1));
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with SourceTimestamp, then read back with TimestampsToReturn.Source and verify SourceTimestamp is set.")]
[Test]
public async Task AttributeWriteReadBackTimestampAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
DateTime sourceTs = DateTime.UtcNow;
var dv = new DataValue(new Variant(200), StatusCodes.Good, sourceTs);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
// If the server does not support writing timestamps, skip read-back check
if (writeResponse.Results[0] == StatusCodes.BadWriteNotSupported)
{
Assert.Ignore("Server does not support writing timestamps.");
}
Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True,
$"Write should return Good, got {writeResponse.Results[0]}");
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Source,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
Assert.That(readResponse.Results[0].SourceTimestamp, Is.Not.EqualTo(DateTime.MinValue),
"SourceTimestamp should be set after writing with a timestamp.");
}
[Description("Write Int32 with StatusCode = Uncertain. Server may accept or reject.")]
[Test]
public async Task AttributeWriteStatusCodeOverrideToUncertainAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(105), StatusCodes.Uncertain);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
[Description("Write Int32 with SourceTimestamp = DateTime.MinValue. Server should accept the value and ignore or reset the timestamp.")]
[Test]
public async Task AttributeWriteValueWithMinDateTimeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
var dv = new DataValue(new Variant(106), StatusCodes.Good, DateTime.MinValue);
WriteResponse writeResponse = await Session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dv
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(writeResponse.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(writeResponse.Results[0]) ||
writeResponse.Results[0] == StatusCodes.BadWriteNotSupported,
Is.True,
$"Expected Good or BadWriteNotSupported, got {writeResponse.Results[0]}");
}
private static readonly int[] s_intValues1 = [10, 20, 30];
}
}