Files
Copilot 5dd53ff9d0 [Server] Migrate all NodeManagers to AsyncCustomNodeManager (#3817)
Migrate all NodeManagers to IAsyncNodeManager
Add more Tests

## Related Issues

Issue linking is handled automatically by the system.

## Types of changes

What types of changes does your code introduce?  
_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR._

- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] Enhancement (non-breaking change which adds functionality)
- [ ] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected, requires version increase of
Nuget packages)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to
ask. We're here to help! This is simply a reminder of what we are going
to look for before merging your code._

- [ ] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [x] I ran tests locally with my changes, all passed.
- [x] I fixed all failing tests in the CI pipelines. 
- [x] I fixed all introduced issues with CodeQL and LGTM.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added necessary documentation (if appropriate).
- [ ] Any dependent changes have been merged and published in downstream
modules.

## Further comments

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-05-28 20:24:22 +02:00

265 lines
9.2 KiB
C#

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
{
/// <summary>
/// Test <see cref="CustomNodeManager2"/>
/// </summary>
[TestFixture]
[Category("CustomNodeManager")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public class CustomNodeManagerTests
{
/// <summary>
/// Tests the componentCache methods with multiple threads
/// </summary>
[Test]
public async Task TestComponentCacheAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var fixture = new ServerFixture<StandardServer>(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);
}
}
/// <summary>
/// Tests the Predefined Nodes methods with multiple threads
/// </summary>
[Test]
public async Task TestPredefinedNodesAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var fixture = new ServerFixture<StandardServer>(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;
}
/// <summary>
/// Test Methods AddNodeToComponentCache, RemoveNodeFromComponentCache & LookupNodeInComponentCache & verify the node is added to the cache
/// </summary>
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> 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<NodeState> PredefinedNodes => base.PredefinedNodes;
public new virtual void AddPredefinedNode(ISystemContext context, NodeState node)
{
base.AddPredefinedNode(context, node);
}
}
}