/* ========================================================================
* 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/
* ======================================================================*/
#pragma warning disable CA2000
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
namespace Opc.Ua.Server.Tests
{
public partial class AsyncCustomNodeManagerTests
{
///
/// Verifies that the service calls of one request share one system context per node
/// manager: two node managers in one request each get a context made from their own
/// template, the copies are reused for the rest of the request, and a different
/// request gets new copies.
///
[Test]
public async Task ReadSharesOneSystemContextPerNodeManagerAndRequestAsync()
{
if (m_managerType == AsyncCustomNodeManagerType.CustomNodeManager2ViaAdapter)
{
Assert.Ignore("CustomNodeManager2 copies its own system context per call.");
}
using ITestNodeManager first = CreateManager();
using ITestNodeManager second = CreateManager();
var firstContexts = new List();
var secondContexts = new List();
NodeId firstId = await AddCapturingVariableAsync(first, "First", firstContexts).ConfigureAwait(false);
NodeId secondId = await AddCapturingVariableAsync(second, "Second", secondContexts).ConfigureAwait(false);
var request = new OperationContext(new RequestHeader(), null, RequestType.Read, RequestLifetime.None);
// Interleave the two node managers within one request twice.
for (int round = 0; round < 2; round++)
{
await ReadValueAsync(first, request, firstId).ConfigureAwait(false);
await ReadValueAsync(second, request, secondId).ConfigureAwait(false);
}
Assert.That(firstContexts, Has.Count.EqualTo(2));
Assert.That(secondContexts, Has.Count.EqualTo(2));
Assert.That(firstContexts[1], Is.SameAs(firstContexts[0]), "first manager's copy evicted");
Assert.That(secondContexts[1], Is.SameAs(secondContexts[0]), "second manager's copy evicted");
Assert.That(firstContexts[0], Is.Not.SameAs(secondContexts[0]));
Assert.That(firstContexts[0], Is.Not.SameAs(first.SystemContext), "the template must not be handed out");
var firstContext = (ServerSystemContext)firstContexts[0];
var secondContext = (ServerSystemContext)secondContexts[0];
Assert.That(firstContext.NodeIdFactory, Is.SameAs(first.SystemContext.NodeIdFactory));
Assert.That(secondContext.NodeIdFactory, Is.SameAs(second.SystemContext.NodeIdFactory));
Assert.That(firstContext.OperationContext, Is.SameAs(request));
Assert.That(secondContext.OperationContext, Is.SameAs(request));
// A different request (or session) never sees the copy of another one.
var nextRequest = new OperationContext(new RequestHeader(), null, RequestType.Read, RequestLifetime.None);
await ReadValueAsync(first, nextRequest, firstId).ConfigureAwait(false);
Assert.That(firstContexts, Has.Count.EqualTo(3));
Assert.That(firstContexts[2], Is.Not.SameAs(firstContexts[0]));
Assert.That(((ServerSystemContext)firstContexts[2]).OperationContext, Is.SameAs(nextRequest));
Assert.That(first.SystemContext.OperationContext, Is.Null, "the template must stay unbound");
}
///
/// Verifies that the per-request cache keeps a copy for every node manager a request
/// touches up to its bound, and still returns a correct copy past it.
///
[Test]
public void OperationSystemContextCacheKeepsOneCopyPerTemplate()
{
var request = new OperationContext(new RequestHeader(), null, RequestType.Read, RequestLifetime.None);
var templates = new List();
for (int ii = 0; ii < 12; ii++)
{
templates.Add(new ServerSystemContext(m_mockServer.Object) { SystemHandle = ii });
}
// Eight node managers in one request are all kept.
var copies = new List();
for (int ii = 0; ii < 8; ii++)
{
copies.Add(request.GetSystemContext(templates[ii]));
}
for (int ii = 0; ii < 8; ii++)
{
Assert.That(request.GetSystemContext(templates[ii]), Is.SameAs(copies[ii]));
}
// Past the bound a template still gets a correct copy bound to the request.
for (int ii = 0; ii < templates.Count; ii++)
{
ServerSystemContext again = request.GetSystemContext(templates[ii]);
Assert.That(again.SystemHandle, Is.EqualTo(ii));
Assert.That(again.OperationContext, Is.SameAs(request));
Assert.That(again, Is.Not.SameAs(templates[ii]));
}
}
private async Task AddCapturingVariableAsync(
ITestNodeManager manager,
string name,
List contexts)
{
ServerSystemContext context = manager.SystemContext;
ushort nsIdx = manager.NamespaceIndexes[0];
var variable = new BaseDataVariableState(null);
variable.CreateAsPredefinedNode(context);
variable.NodeId = new NodeId(name, nsIdx);
variable.BrowseName = new QualifiedName(name, nsIdx);
variable.DataType = DataTypeIds.Int32;
variable.ValueRank = ValueRanks.Scalar;
variable.AccessLevel = AccessLevels.CurrentRead;
variable.UserAccessLevel = AccessLevels.CurrentRead;
variable.Value = 1;
variable.OnReadValue = (
ISystemContext readContext,
NodeState node,
NumericRange indexRange,
QualifiedName dataEncoding,
ref Variant value,
ref StatusCode statusCode,
ref DateTimeUtc timestamp) =>
{
lock (contexts)
{
contexts.Add(readContext);
}
return ServiceResult.Good;
};
await manager.AddNodeAsync(context, default, variable).ConfigureAwait(false);
return variable.NodeId;
}
private static async Task ReadValueAsync(
ITestNodeManager manager,
OperationContext request,
NodeId nodeId)
{
var nodesToRead = new List
{
new() { NodeId = nodeId, AttributeId = Attributes.Value }
};
var values = new List { default };
var errors = new List { null! };
await manager.ReadAsync(request, 0, nodesToRead, values, errors).ConfigureAwait(false);
Assert.That(ServiceResult.IsGood(errors[0]), Is.True, errors[0]?.ToString());
}
}
}