/* ======================================================================== * 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; } } }