/* ========================================================================
* 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.Collections.Generic;
using System.Threading;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Core.Tests.Stack.State
{
///
/// Tests for concurrency issues in BaseVariableState class
///
[TestFixture]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Category("NodeStateConcurrency")]
[Parallelizable]
public class NodeStateHandlerConcurrencyTests
{
///
/// This data tests all handlers in NodeState and BaseVariableState class
///
public static IEnumerable VariableHandlerTestCases
{
get
{
// Test OnWriteValue
Action action = state =>
{
state.OnWriteValue = ValueChangedHandler;
state.OnWriteValue = null;
};
yield return new TestCaseData(Attributes.Value, new Variant(15.5), action);
// Test OnSimpleWriteValue
action = state =>
{
state.OnSimpleWriteValue = NodeAttributeEventHandler;
state.OnSimpleWriteValue = null;
};
yield return new TestCaseData(Attributes.Value, new Variant(15.5), action);
// Test OnWriteNodeId
action = state =>
{
state.OnWriteNodeId = NodeAttributeEventHandler;
state.OnWriteNodeId = null;
};
yield return new TestCaseData(
Attributes.NodeId,
new Variant(new NodeId(22, 7)),
action);
// Test OnWriteNodeClass
action = state =>
{
state.OnWriteNodeClass = NodeAttributeEventHandler;
state.OnWriteNodeClass = null;
};
yield return new TestCaseData(
Attributes.NodeClass,
new Variant((int)NodeClass.Variable),
action);
// Test OnWriteBrowseName
action = state =>
{
state.OnWriteBrowseName = NodeAttributeEventHandler;
state.OnWriteBrowseName = null;
};
yield return new TestCaseData(
Attributes.BrowseName,
new Variant(QualifiedName.From("test")),
action);
// Test OnWriteDisplayName
action = state =>
{
state.OnWriteDisplayName = NodeAttributeEventHandler;
state.OnWriteDisplayName = null;
};
yield return new TestCaseData(
Attributes.DisplayName,
new Variant(new LocalizedText("test")),
action);
// Test OnWriteDescription
action = state =>
{
state.OnWriteDescription = NodeAttributeEventHandler;
state.OnWriteDescription = null;
};
yield return new TestCaseData(
Attributes.Description,
new Variant(new LocalizedText("test")),
action);
// Test OnWriteWriteMask
action = state =>
{
state.OnWriteWriteMask = NodeAttributeEventHandler;
state.OnWriteWriteMask = null;
};
yield return new TestCaseData(
Attributes.WriteMask,
new Variant(
(uint)AttributeWriteMask.WriteMask |
(uint)AttributeWriteMask.UserWriteMask),
action);
// Test OnWriteUserWriteMask
action = state =>
{
state.OnWriteUserWriteMask = NodeAttributeEventHandler;
state.OnWriteUserWriteMask = null;
};
yield return new TestCaseData(
Attributes.UserWriteMask,
new Variant(
(uint)AttributeWriteMask.WriteMask |
(uint)AttributeWriteMask.UserWriteMask),
action);
// Test OnWriteRolePermissions
action = state =>
{
state.OnWriteRolePermissions = NodeAttributeEventHandler;
state.OnWriteRolePermissions = null;
};
yield return new TestCaseData(
Attributes.RolePermissions,
new Variant(s_value),
action);
// Test OnWriteAccessRestrictions
action = state =>
{
state.OnWriteAccessRestrictions = NodeAttributeEventHandler;
state.OnWriteAccessRestrictions = null;
};
yield return new TestCaseData(
Attributes.AccessRestrictions,
new Variant((ushort)AccessRestrictionType.EncryptionRequired),
action);
// Test OnWriteDataType
action = state =>
{
state.OnWriteDataType = NodeAttributeEventHandler;
state.OnWriteDataType = null;
};
yield return new TestCaseData(
Attributes.DataType,
new Variant(DataTypeIds.Double),
action);
// Test OnWriteValueRank
action = state =>
{
state.OnWriteValueRank = NodeAttributeEventHandler;
state.OnWriteValueRank = null;
};
yield return new TestCaseData(
Attributes.ValueRank,
new Variant(ValueRanks.Scalar),
action);
// Test OnWriteArrayDimensions
action = state =>
{
state.OnWriteArrayDimensions = NodeAttributeEventHandler;
state.OnWriteArrayDimensions = null;
};
yield return new TestCaseData(
Attributes.ArrayDimensions,
new Variant(new List { 2, 2 }),
action);
// Test OnWriteAccessLevel
action = state =>
{
state.OnWriteAccessLevel = NodeAttributeEventHandler;
state.OnWriteAccessLevel = null;
};
yield return new TestCaseData(
Attributes.AccessLevel,
new Variant(AccessLevels.CurrentRead),
action);
// Test OnWriteUserAccessLevel
action = state =>
{
state.OnWriteUserAccessLevel = NodeAttributeEventHandler;
state.OnWriteUserAccessLevel = null;
};
yield return new TestCaseData(
Attributes.UserAccessLevel,
new Variant(AccessLevels.CurrentRead),
action);
// Test OnWriteMinimumSamplingInterval
action = state =>
{
state.OnWriteMinimumSamplingInterval = NodeAttributeEventHandler;
state.OnWriteMinimumSamplingInterval = null;
};
yield return new TestCaseData(
Attributes.MinimumSamplingInterval,
new Variant(1000.0),
action);
// Test OnWriteHistorizing
action = state =>
{
state.OnWriteHistorizing = NodeAttributeEventHandler;
state.OnWriteHistorizing = null;
};
yield return new TestCaseData(Attributes.Historizing, new Variant(true), action);
}
}
///
/// This test data test all handlers in BaseVariableTypeState and BaseTypeState class
///
public static IEnumerable VariableTypeHandlerTestCases
{
get
{
// Test OnWriteDataType
Action action = state =>
{
state.OnWriteDataType = NodeAttributeEventHandler;
state.OnWriteDataType = null;
};
yield return new TestCaseData(
Attributes.DataType,
new Variant(DataTypeIds.Double),
action);
// Test OnWriteValueRank
action = state =>
{
state.OnWriteValueRank = NodeAttributeEventHandler;
state.OnWriteValueRank = null;
};
yield return new TestCaseData(
Attributes.ValueRank,
new Variant(ValueRanks.Scalar),
action);
// Test OnWriteArrayDimensions
action = state =>
{
state.OnWriteArrayDimensions = NodeAttributeEventHandler;
state.OnWriteArrayDimensions = null;
};
yield return new TestCaseData(
Attributes.ArrayDimensions,
new Variant(new List { 2, 2 }),
action);
// Test OnWriteIsAbstract
action = state =>
{
state.OnWriteIsAbstract = NodeAttributeEventHandler;
state.OnWriteIsAbstract = null;
};
yield return new TestCaseData(Attributes.IsAbstract, new Variant(false), action);
}
}
///
/// This test data test all handlers in BaseObjectState class
///
public static IEnumerable ObjectHandlerTestCases
{
get
{
// Test EventNotifier
Action action = state =>
{
state.OnWriteEventNotifier = NodeAttributeEventHandler;
state.OnWriteEventNotifier = null;
};
yield return new TestCaseData(
Attributes.EventNotifier,
new Variant(EventNotifiers.SubscribeToEvents),
action);
}
}
///
/// This test data test all handlers in MethodState class
///
public static IEnumerable MethodHandlerTestCases
{
get
{
// Test Executable
Action action = state =>
{
state.OnWriteExecutable = NodeAttributeEventHandler;
state.OnWriteExecutable = null;
};
yield return new TestCaseData(Attributes.Executable, new Variant(true), action);
// Test UserExecutable
action = state =>
{
state.OnWriteUserExecutable = NodeAttributeEventHandler;
state.OnWriteUserExecutable = null;
};
yield return new TestCaseData(Attributes.UserExecutable, new Variant(true), action);
}
}
///
/// This test data test all handlers in ReferenceTypeState class
///
public static IEnumerable ReferenceTypeHandlerTestCases
{
get
{
// Test InverseName
Action action = state =>
{
state.OnWriteInverseName = NodeAttributeEventHandler;
state.OnWriteInverseName = null;
};
yield return new TestCaseData(
Attributes.InverseName,
new Variant(new LocalizedText("inverse test")),
action);
// Test Symmetric
action = state =>
{
state.OnWriteSymmetric = NodeAttributeEventHandler;
state.OnWriteSymmetric = null;
};
yield return new TestCaseData(Attributes.Symmetric, new Variant(true), action);
}
}
///
/// This test data test all handlers in ViewState class
///
public static IEnumerable ViewHandlerTestCases
{
get
{
// Test EventNotifier
Action action = state =>
{
state.OnWriteEventNotifier = NodeAttributeEventHandler;
state.OnWriteEventNotifier = null;
};
yield return new TestCaseData(
Attributes.EventNotifier,
new Variant(EventNotifiers.SubscribeToEvents),
action);
// Test ContainsNoLoops
action = state =>
{
state.OnWriteContainsNoLoops = NodeAttributeEventHandler;
state.OnWriteContainsNoLoops = null;
};
yield return new TestCaseData(
Attributes.ContainsNoLoops,
new Variant(true),
action);
}
}
///
/// This test data test all handlers in DataTypeState class
///
public static IEnumerable DataTypeHandlerTestCases
{
get
{
// Test DataTypeDefinition
Action action = state =>
{
state.OnWriteDataTypeDefinition = NodeAttributeEventHandler;
state.OnWriteDataTypeDefinition = null;
};
yield return new TestCaseData(
Attributes.DataTypeDefinition,
new Variant(new ExtensionObject()),
action);
}
}
private static readonly ExtensionObject[] s_value = [];
[TestCaseSource(nameof(VariableHandlerTestCases))]
[Parallelizable]
public void VariableNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new AnalogUnitRangeState(null);
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(VariableTypeHandlerTestCases))]
[Parallelizable]
public void VariableTypeNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new BaseDataVariableTypeState();
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(ObjectHandlerTestCases))]
[Parallelizable]
public void ObjectNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new BaseObjectState(null);
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(MethodHandlerTestCases))]
[Parallelizable]
public void MethodNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new MethodState(null);
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(ReferenceTypeHandlerTestCases))]
[Parallelizable]
public void ReferenceTypeNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new ReferenceTypeState();
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
[TestCaseSource(nameof(ViewHandlerTestCases))]
[Parallelizable]
public void ViewNodeHandlerConcurrencyTest(
uint attribute,
Variant variant,
Action concurrentTaskAction)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var testNodeState = new ViewState();
var serviceMessageContext = ServiceMessageContext.Create(telemetry);
var systemContext = new SystemContext(telemetry)
{
NamespaceUris = serviceMessageContext.NamespaceUris
};
testNodeState.Create(
systemContext,
new NodeId("TestNode", 7),
new QualifiedName("TestNode", 7),
new LocalizedText("TestNode"),
true);
ExecuteNodeHandlerConcurrencyTest(
systemContext,
attribute,
variant,
testNodeState,
concurrentTaskAction);
}
private static void ExecuteNodeHandlerConcurrencyTest(
ISystemContext systemContext,
uint attribute,
Variant variant,
T node,
Action concurrentTaskAction)
where T : NodeState
{
node.WriteMask =
AttributeWriteMask.AccessLevel |
AttributeWriteMask.ArrayDimensions |
AttributeWriteMask.BrowseName |
AttributeWriteMask.ContainsNoLoops |
AttributeWriteMask.DataType |
AttributeWriteMask.Description |
AttributeWriteMask.DisplayName |
AttributeWriteMask.EventNotifier |
AttributeWriteMask.Executable |
AttributeWriteMask.Historizing |
AttributeWriteMask.InverseName |
AttributeWriteMask.IsAbstract |
AttributeWriteMask.MinimumSamplingInterval |
AttributeWriteMask.NodeClass |
AttributeWriteMask.NodeId |
AttributeWriteMask.Symmetric |
AttributeWriteMask.UserAccessLevel |
AttributeWriteMask.UserExecutable |
AttributeWriteMask.UserWriteMask |
AttributeWriteMask.ValueForVariableType |
AttributeWriteMask.ValueRank |
AttributeWriteMask.WriteMask |
AttributeWriteMask.RolePermissions |
AttributeWriteMask.AccessRestrictions;
if (node is BaseVariableState baseVariableState)
{
// Make Value attribute writable so that it is possible to test Value attribute writing
ServiceResult result = baseVariableState.WriteAttribute(
systemContext,
Attributes.AccessLevel,
default,
new DataValue(new Variant(AccessLevels.CurrentReadOrWrite)));
Assert.That(ServiceResult.IsGood(result), Is.True);
result = baseVariableState.WriteAttribute(
systemContext,
Attributes.UserAccessLevel,
default,
new DataValue(new Variant(AccessLevels.CurrentReadOrWrite)));
Assert.That(ServiceResult.IsGood(result), Is.True);
}
bool running = true;
Exception? workerError = null;
// The worker is a raw thread, so three things have to hold or it can
// take the whole test host down with it:
// * IsBackground, so a worker that somehow outlives this method
// cannot pin the process at exit. A foreground thread here is
// exactly the shape of an "all tests passed, then the host never
// exits" hang.
// * Volatile access to the stop flag, so the worker is guaranteed
// to observe the request rather than spinning on a cached read.
// * A catch, because an unhandled exception on a raw thread
// terminates the process and takes every remaining test with it.
// Capture it and surface it on the test thread instead.
var worker = new Thread(() =>
{
try
{
while (Volatile.Read(ref running))
{
concurrentTaskAction(node);
}
}
catch (Exception ex)
{
workerError = ex;
}
})
{
IsBackground = true,
Name = "NodeStateHandlerConcurrency.Worker"
};
worker.Start();
try
{
DateTime utcNow = DateTime.UtcNow;
while (DateTime.UtcNow - utcNow < TimeSpan.FromSeconds(1))
{
ServiceResult writeResult = node.WriteAttribute(
systemContext,
attribute,
default,
new DataValue(variant));
Assert.That(
ServiceResult.IsGood(writeResult),
Is.True,
$"Expected Good ServiceResult but was: {writeResult}");
}
}
finally
{
// Stop the worker even when the assertion above throws;
// otherwise a single failed assertion leaks a spinning thread
// for the rest of the run.
Volatile.Write(ref running, false);
if (!worker.Join(TimeSpan.FromSeconds(30)))
{
// Bounded so a stuck worker cannot hang the run. It is a
// background thread, so leaving it behind is survivable.
TestContext.Progress.WriteLine(
"NodeStateHandlerConcurrency worker did not stop within 30 seconds.");
}
}
if (workerError != null)
{
Assert.Fail($"The concurrent writer threw: {workerError}");
}
}
private static ServiceResult ValueChangedHandler(
ISystemContext context,
NodeState node,
NumericRange indexRange,
QualifiedName dataEncoding,
ref Variant value,
ref StatusCode statusCode,
ref DateTimeUtc timestamp)
{
return ServiceResult.Good;
}
private static ServiceResult NodeAttributeEventHandler(
ISystemContext context,
NodeState node,
ref T value)
{
return ServiceResult.Good;
}
}
}