using System; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Opc.Ua.Server.TestFramework; using Opc.Ua.Tests; using Quickstarts.ReferenceServer; namespace Opc.Ua.Server.Tests { /// /// Test /// [TestFixture] [Category("CustomNodeManager")] [SetCulture("en-us")] [SetUICulture("en-us")] [Parallelizable] public class CustomNodeManagerTests { /// /// Tests the componentCache methods with multiple threads /// [Test] public async Task TestComponentCacheAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var fixture = new ServerFixture(t => new ReferenceServer(t)); try { // Arrange const string ns = "http://test.org/UA/Data/"; StandardServer server = await fixture.StartAsync() .ConfigureAwait(false); using var nodeManager = new TestableCustomNodeManger2(server.CurrentInstance, ns); var baseObject = new BaseObjectState(null); var nodeHandle = new NodeHandle( CommonTestWorkers.NodeIdTestSetStatic[0] .WithNamespaceIndex(0) .InnerNodeId, baseObject); //Act await RunTaskInParallelAsync( () => UseComponentCacheAsync(nodeManager, baseObject, nodeHandle), 100) .ConfigureAwait(false); //Assert, that entry was deleted from cache after parallel operations on the same node NodeState handleFromCache = nodeManager.LookupNodeInComponentCache( nodeManager.SystemContext, nodeHandle); Assert.That(handleFromCache, Is.Null); } finally { await fixture.StopAsync().ConfigureAwait(false); } } /// /// Part 4 ยง5.12.2.2: a Call whose methodId is the Method declaration of the /// ObjectType resolves to the Method of the Object, so the RolePermissions /// of the Object's Method are verified and that Method is invoked. /// [Test] public async Task FindMethodStateResolvesTypeDeclarationToObjectMethodAsync() { var fixture = new ServerFixture(t => new ReferenceServer(t)); try { const string ns = "http://test.org/UA/MethodResolution/"; StandardServer server = await fixture.StartAsync() .ConfigureAwait(false); using var nodeManager = new TestableCustomNodeManger2(server.CurrentInstance, ns); ushort index = (ushort)server.CurrentInstance.NamespaceUris.GetIndex(ns); ServerSystemContext context = nodeManager.SystemContext; var objectType = new BaseObjectTypeState { NodeId = new NodeId("ResolveType", index), BrowseName = new QualifiedName("ResolveType", index), SuperTypeId = ObjectTypeIds.BaseObjectType }; var declaration = new MethodState(objectType) { NodeId = new NodeId("ResolveType.Reset", index), BrowseName = new QualifiedName("Reset", index) }; objectType.AddChild(declaration); var instance = new BaseObjectState(null) { NodeId = new NodeId("ResolveInstance", index), BrowseName = new QualifiedName("ResolveInstance", index), TypeDefinitionId = objectType.NodeId }; var instanceMethod = new MethodState(instance) { NodeId = new NodeId("ResolveInstance.Reset", index), BrowseName = new QualifiedName("Reset", index) }; instance.AddChild(instanceMethod); nodeManager.AddPredefinedNode(context, objectType); nodeManager.AddPredefinedNode(context, instance); server.CurrentInstance.TypeTree.AddSubtype( objectType.NodeId, ObjectTypeIds.BaseObjectType); var operationContext = new OperationContext( new RequestHeader(), null, RequestType.Call, RequestLifetime.None); MethodState resolved = nodeManager.FindMethodState( operationContext, new CallMethodRequest { ObjectId = instance.NodeId, MethodId = declaration.NodeId }); Assert.That(resolved, Is.SameAs(instanceMethod)); } finally { await fixture.StopAsync().ConfigureAwait(false); } } /// /// Tests the Predefined Nodes methods with multiple threads /// [Test] public async Task TestPredefinedNodesAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var fixture = new ServerFixture(t => new ReferenceServer(t)); try { // Arrange const string ns = "http://test.org/UA/Data/"; StandardServer server = await fixture.StartAsync() .ConfigureAwait(false); using var nodeManager = new TestableCustomNodeManger2(server.CurrentInstance, ns); int index = server.CurrentInstance.NamespaceUris.GetIndex(ns); var baseObject = new DataItemState(null); NodeId nodeId = CommonTestWorkers.NodeIdTestSetStatic[0] .WithNamespaceIndex((ushort)index) .InnerNodeId; baseObject.NodeId = nodeId; //single threaded test nodeManager.AddPredefinedNode(nodeManager.SystemContext, baseObject); Assert.That(nodeManager.PredefinedNodes.ContainsKey(nodeId), Is.True); NodeState nodeState = nodeManager.Find(nodeId)!; Assert.That(nodeState, Is.Not.Null); var handle = nodeManager.GetManagerHandle(nodeId) as NodeHandle; Assert.That(handle, Is.Not.Null); nodeManager.DeleteNode(nodeManager.SystemContext, nodeId); Assert.That(nodeManager.PredefinedNodes, Is.Empty); nodeState = nodeManager.Find(nodeId)!; Assert.That(nodeState, Is.Null); handle = nodeManager.GetManagerHandle(nodeId) as NodeHandle; Assert.That(handle, Is.Null); nodeManager.AddPredefinedNode(nodeManager.SystemContext, baseObject); nodeManager.DeleteAddressSpace(); Assert.That(nodeManager.PredefinedNodes, Is.Empty); //Act await RunTaskInParallelAsync( () => UsePredefinedNodesAsync(nodeManager, baseObject, nodeId), 100) .ConfigureAwait(false); //last operation added the Node back into the dictionary Assert.That(nodeManager.PredefinedNodes.ContainsKey(nodeId), Is.True); //delete full adress space nodeManager.DeleteAddressSpace(); Assert.That(nodeManager.PredefinedNodes, Is.Empty); } finally { await fixture.StopAsync().ConfigureAwait(false); } } private static Task UsePredefinedNodesAsync( TestableCustomNodeManger2 nodeManager, DataItemState baseObject, NodeId nodeId) { nodeManager.AddPredefinedNode(nodeManager.SystemContext, baseObject); _ = nodeManager.Find(nodeId); _ = nodeManager.GetManagerHandle(nodeId) as NodeHandle; nodeManager.DeleteNode(nodeManager.SystemContext, nodeId); _ = nodeManager.Find(nodeId); _ = nodeManager.GetManagerHandle(nodeId) as NodeHandle; nodeManager.AddPredefinedNode(nodeManager.SystemContext, baseObject); return Task.CompletedTask; } /// /// Test Methods AddNodeToComponentCache, RemoveNodeFromComponentCache & LookupNodeInComponentCache & verify the node is added to the cache /// private static async Task UseComponentCacheAsync( TestableCustomNodeManger2 nodeManager, BaseObjectState baseObject, NodeHandle nodeHandle) { //-- Act nodeManager.AddNodeToComponentCache(nodeManager.SystemContext, nodeHandle, baseObject); nodeManager.AddNodeToComponentCache(nodeManager.SystemContext, nodeHandle, baseObject); NodeState handleFromCache = nodeManager.LookupNodeInComponentCache( nodeManager.SystemContext, nodeHandle); //-- Assert Assert.That(handleFromCache, Is.Not.Null); nodeManager.RemoveNodeFromComponentCache(nodeManager.SystemContext, nodeHandle); handleFromCache = nodeManager.LookupNodeInComponentCache( nodeManager.SystemContext, nodeHandle); Assert.That(handleFromCache, Is.Not.Null); nodeManager.RemoveNodeFromComponentCache(nodeManager.SystemContext, nodeHandle); await Task.CompletedTask.ConfigureAwait(false); } private static async Task<(bool IsSuccess, Exception? Error)> RunTaskInParallelAsync( Func task, int iterations) { using var cancellationTokenSource = new CancellationTokenSource(); Exception? error = null; int tasksCompletedCount = 0; ParallelLoopResult result = Parallel.For( 0, iterations, new ParallelOptions(), async _ => { try { await task().ConfigureAwait(false); } catch (Exception ex) { error = ex; cancellationTokenSource.Cancel(); } finally { tasksCompletedCount++; } }); int spinWaitCount = 0; const int maxSpinWaitCount = 100; while (iterations > tasksCompletedCount && error is null && spinWaitCount < maxSpinWaitCount) { await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false); spinWaitCount++; } return (error == null, error); } } public class TestableCustomNodeManger2 : CustomNodeManager2 { public TestableCustomNodeManger2(IServerInternal server, params string[] namespaceUris) : base(server, namespaceUris) { } public new NodeState AddNodeToComponentCache( ISystemContext context, NodeHandle handle, NodeState node) { return base.AddNodeToComponentCache(context, handle, node); } public new void RemoveNodeFromComponentCache(ISystemContext context, NodeHandle handle) { base.RemoveNodeFromComponentCache(context, handle); } public new NodeState LookupNodeInComponentCache(ISystemContext context, NodeHandle handle) { return base.LookupNodeInComponentCache(context, handle)!; } public new NodeIdDictionary PredefinedNodes => base.PredefinedNodes; public new virtual void AddPredefinedNode(ISystemContext context, NodeState node) { base.AddPredefinedNode(context, node); } } }