mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
9b034f8291
# Description Nullable reference types are now enabled once, in `common.props`, and every project in the repository builds warning-clean with them. **Configuration** - `<Nullable>enable</Nullable>` lives in `common.props`. All per-project `<Nullable>` settings are removed: 178 project files (enable, annotations, and the `disable` in the three Aot test projects). - 419 per-file `#nullable enable`/`restore` directives and the 4 `#nullable disable` directives are removed. - Kept on purpose: - The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`, `PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the project setting in generated code, so the directive is required there. - `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against the 1.5.378 packages and does not import the repository props. - `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now describe the central setting. **Migration** (the 43 unset and 13 annotations-only projects, roughly 10k diagnostics) - **Production code: annotated at the declarations.** Covers `tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the LDS sample. - Members, parameters and returns are nullable where null actually flows. - Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`, `IsMethodTypeDesign`). - `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take `params object?[]`. `Template.AddReplacement` takes `string?` (rendering already skips null replacements). - `IRegisteredServerStore.RegisterAsync` returns `Task<RegistrationEntry?>`: it returns null when a registration is removed. - Dereferences backed by an invariant (for example, the validated model) use `!`, so runtime behaviour is unchanged. - **Behavioural changes in production code** (small, deliberate): - `NodeSetToModelDesign` throws `InvalidOperationException` when the file is not a NodeSet. Before, it threw a NullReferenceException on the next line. - `ModelDesignValidator` starts its lookup tables empty instead of null. A redundant `if (m_nodes == null) return;` guard is gone. - **Test code: minimal annotations.** - `?` on values that can be null, `!` where the test asserts non-null. - `= null!` for fixture members assigned in SetUp. - `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the `IEncodeable` constraint. - A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`) folded into the adjacent `IsNull`/`Count` checks. **Notes for reviewers** - The diff is large (~1,500 files) but almost entirely mechanical: `?`, `!` and directive removal. The non-annotation changes are listed above. - Known follow-up, not done here: `IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay non-nullable even though the validator returns null before it loads a dictionary. Making them nullable would ripple through every generator call site. ## Related Issues - Fixes #4439 ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. The "no new warnings" box covers net10.0 and net48: full non-incremental builds of `UA.slnx`, `tools/SourceGeneration.slnx`, `tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0 warnings and 0 errors under the repository's warnings-as-errors settings. net9.0 and net8.0 also had no nullable diagnostics, but they were not rebuilt after the final fixes. Tests have not been run locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1769 lines
70 KiB
C#
1769 lines
70 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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/
|
|
* ======================================================================*/
|
|
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
|
|
|
// CA2000: test code; many disposables are ownership-transferred to test fixtures or short-lived,
|
|
// making CA2000 noisy without a real leak risk. Disabled file-level for the suite.
|
|
#pragma warning disable CA2000
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.Metrics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Client.Tests
|
|
{
|
|
[TestFixture]
|
|
[Category("Client")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
public sealed class NodeCacheTests
|
|
{
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public async Task GetNodeCancellationDoesNotPoisonSharedFetchAsync(bool cancelFirst)
|
|
{
|
|
var nodeId = new NodeId("shared-node", 2);
|
|
var expected = new VariableNode { NodeId = nodeId, BrowseName = new QualifiedName("Shared", 2) };
|
|
var fetch = new TaskCompletionSource<Node>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var context = new Mock<INodeCacheContext>();
|
|
context.Setup(value => value.FetchNodeAsync(
|
|
null, nodeId, NodeClass.Unspecified, false, It.IsAny<CancellationToken>()))
|
|
.Returns((RequestHeader _, NodeId _, NodeClass _, bool _, CancellationToken ct) =>
|
|
new ValueTask<Node>(fetch.Task.WaitAsync(ct)));
|
|
context.Setup(value => value.FetchReferencesAsync(null, nodeId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([]);
|
|
using var cache = new NodeCache(context.Object, NUnitTelemetryContext.Create());
|
|
using var canceled = new CancellationTokenSource();
|
|
Task<INode> first = cache.GetNodeAsync(
|
|
nodeId, cancelFirst ? canceled.Token : CancellationToken.None).AsTask();
|
|
Task<INode> second = cache.GetNodeAsync(
|
|
nodeId, cancelFirst ? CancellationToken.None : canceled.Token).AsTask();
|
|
Task<INode> canceledLookup = cancelFirst ? first : second;
|
|
Task<INode> liveLookup = cancelFirst ? second : first;
|
|
|
|
try
|
|
{
|
|
canceled.Cancel();
|
|
Assert.That(
|
|
async () => _ = await canceledLookup.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false),
|
|
Throws.InstanceOf<OperationCanceledException>());
|
|
Assert.That(liveLookup.IsCompleted, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
fetch.TrySetResult(expected);
|
|
try
|
|
{
|
|
await Task.WhenAll(first, second).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) when (canceled.IsCancellationRequested)
|
|
{
|
|
}
|
|
}
|
|
|
|
INode node = await liveLookup.ConfigureAwait(false);
|
|
Assert.That(node.NodeId, Is.EqualTo(nodeId));
|
|
Assert.That(node.BrowseName, Is.EqualTo(new QualifiedName("Shared", 2)));
|
|
Assert.That(await cache.GetNodeAsync(nodeId, default).ConfigureAwait(false), Is.SameAs(node));
|
|
context.Verify(value => value.FetchNodeAsync(
|
|
null, nodeId, NodeClass.Unspecified, false, It.IsAny<CancellationToken>()), Times.Once);
|
|
context.Verify(value => value.FetchReferencesAsync(
|
|
null, nodeId, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetValueCancellationDoesNotPoisonSharedFetchAsync()
|
|
{
|
|
var nodeId = new NodeId("shared-value", 2);
|
|
var expected = new DataValue(Variant.From(42), StatusCodes.GoodClamped);
|
|
var fetch = new TaskCompletionSource<DataValue>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var context = new Mock<INodeCacheContext>();
|
|
context.Setup(value => value.FetchValueAsync(null, nodeId, It.IsAny<CancellationToken>()))
|
|
.Returns((RequestHeader _, NodeId _, CancellationToken ct) =>
|
|
new ValueTask<DataValue>(fetch.Task.WaitAsync(ct)));
|
|
using var cache = new NodeCache(context.Object, NUnitTelemetryContext.Create());
|
|
using var canceled = new CancellationTokenSource();
|
|
Task<DataValue> first = cache.GetValueAsync(nodeId, canceled.Token).AsTask();
|
|
Task<DataValue> second = cache.GetValueAsync(nodeId, default).AsTask();
|
|
|
|
try
|
|
{
|
|
canceled.Cancel();
|
|
Assert.That(
|
|
async () => _ = await first.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false),
|
|
Throws.InstanceOf<OperationCanceledException>());
|
|
Assert.That(second.IsCompleted, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
fetch.TrySetResult(expected);
|
|
try
|
|
{
|
|
await Task.WhenAll(first, second).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) when (canceled.IsCancellationRequested)
|
|
{
|
|
}
|
|
}
|
|
|
|
DataValue result = await second.ConfigureAwait(false);
|
|
Assert.That(result.WrappedValue, Is.EqualTo(Variant.From(42)));
|
|
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.GoodClamped));
|
|
Assert.That(await cache.GetValueAsync(nodeId, default).ConfigureAwait(false), Is.EqualTo(expected));
|
|
context.Verify(value => value.FetchValueAsync(
|
|
null, nodeId, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetReferencesCancellationDoesNotPoisonSharedFetchAsync()
|
|
{
|
|
var nodeId = new NodeId("shared-references", 2);
|
|
var childId = new NodeId("child", 2);
|
|
var expected = new VariableNode { NodeId = childId, BrowseName = new QualifiedName("Child", 2) };
|
|
var fetch = new TaskCompletionSource<ArrayOf<ReferenceDescription>>(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var context = new Mock<INodeCacheContext>();
|
|
context.SetupGet(value => value.NamespaceUris).Returns(new NamespaceTable());
|
|
context.Setup(value => value.FetchReferencesAsync(null, nodeId, It.IsAny<CancellationToken>()))
|
|
.Returns((RequestHeader _, NodeId _, CancellationToken ct) =>
|
|
new ValueTask<ArrayOf<ReferenceDescription>>(fetch.Task.WaitAsync(ct)));
|
|
context.Setup(value => value.FetchNodesAsync(
|
|
null,
|
|
It.Is<ArrayOf<NodeId>>(ids => ids.Count == 1 && ids[0] == childId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results = [expected],
|
|
Errors = [ServiceResult.Good]
|
|
});
|
|
using var cache = new NodeCache(context.Object, NUnitTelemetryContext.Create());
|
|
using var canceled = new CancellationTokenSource();
|
|
Task<ArrayOf<INode>> first = cache.GetReferencesAsync(
|
|
nodeId, ReferenceTypeIds.HasComponent, false, false, canceled.Token).AsTask();
|
|
Task<ArrayOf<INode>> second = cache.GetReferencesAsync(
|
|
nodeId, ReferenceTypeIds.HasComponent, false, false, default).AsTask();
|
|
|
|
try
|
|
{
|
|
canceled.Cancel();
|
|
Assert.That(
|
|
async () => _ = await first.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false),
|
|
Throws.InstanceOf<OperationCanceledException>());
|
|
Assert.That(second.IsCompleted, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
fetch.TrySetResult(
|
|
[
|
|
new ReferenceDescription
|
|
{
|
|
NodeId = childId,
|
|
ReferenceTypeId = ReferenceTypeIds.HasComponent,
|
|
IsForward = true
|
|
}
|
|
]);
|
|
try
|
|
{
|
|
await Task.WhenAll(first, second).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) when (canceled.IsCancellationRequested)
|
|
{
|
|
}
|
|
}
|
|
|
|
ArrayOf<INode> result = await second.ConfigureAwait(false);
|
|
Assert.That(result, Has.Count.EqualTo(1));
|
|
Assert.That(result[0].NodeId, Is.EqualTo(childId));
|
|
Assert.That(result[0].BrowseName, Is.EqualTo(new QualifiedName("Child", 2)));
|
|
ArrayOf<INode> cached = await cache.GetReferencesAsync(
|
|
nodeId, ReferenceTypeIds.HasComponent, false, false, default).ConfigureAwait(false);
|
|
Assert.That(cached, Has.Count.EqualTo(1));
|
|
Assert.That(cached[0], Is.SameAs(result[0]));
|
|
context.Verify(value => value.FetchReferencesAsync(
|
|
null, nodeId, It.IsAny<CancellationToken>()), Times.Once);
|
|
context.Verify(value => value.FetchNodesAsync(
|
|
null, It.IsAny<ArrayOf<NodeId>>(), false, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchRemainingNodesAsyncShouldHandleErrorsAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var id = new NodeId("test", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<ArrayOf<NodeId>>(),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results = [new()],
|
|
Errors = [new ServiceResult(StatusCodes.BadUnexpectedError)]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<INode> result = await nodeCache.GetNodesAsync([id], default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result.Count, Is.EqualTo(1));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetBuiltInTypeAsyncShouldHandleUnknownTypeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var datatypeId = new NodeId("unknownType", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == datatypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i != datatypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Never);
|
|
|
|
// Act
|
|
BuiltInType result = await nodeCache.GetBuiltInTypeAsync(datatypeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(BuiltInType.Null));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetBuiltInTypeAsyncShouldReturnBuiltInTypeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var datatypeId = new NodeId((uint)BuiltInType.Int32, 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
BuiltInType result = await nodeCache.GetBuiltInTypeAsync(datatypeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(BuiltInType.Int32));
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetNodeAsyncShouldHandleEmptyListAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<INode> result = await nodeCache.GetNodesAsync([], default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result.IsEmpty, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetNodeAsyncShouldReturnNodeFromCacheAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var expected = new Node();
|
|
var id = new NodeId("test", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchNodeAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
NodeClass.Unspecified,
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expected)
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
INode result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetNodeTestAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
var expected = new Node();
|
|
var id = new NodeId("test", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchNodeAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
NodeClass.Unspecified,
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, NodeId, NodeClass, bool, CancellationToken>((_, nodeId, _, _, ct)
|
|
=> new ValueTask<Node>(expected))
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
INode result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void GetNodeThrowsTest()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
var expected = new Node();
|
|
var id = new NodeId("test", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchNodeAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
NodeClass.Unspecified,
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, NodeId, NodeClass, bool, CancellationToken>((_, nodeId, _, _, ct)
|
|
=> new ValueTask<Node>(Task.FromException<Node>(new ServiceResultException())))
|
|
.Verifiable(Times.Exactly(3));
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
_ = Assert.ThrowsAsync<ServiceResultException>(async () =>
|
|
await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false));
|
|
_ = Assert.ThrowsAsync<ServiceResultException>(async () =>
|
|
await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false));
|
|
_ = Assert.ThrowsAsync<ServiceResultException>(async () =>
|
|
await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetNodeWithBrowsePathAsyncShouldHandleInvalidBrowsePathAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var id = new NodeId("test", 0);
|
|
ArrayOf<QualifiedName> browsePath = [QualifiedName.From("invalid")];
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == ReferenceTypeIds.HierarchicalReferences),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
INode? result = await nodeCache.GetNodeWithBrowsePathAsync(id, browsePath, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.Null);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetNodeWithBrowsePathAsyncShouldReturnNodeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var id = new NodeId("test", 0);
|
|
var expected = new VariableNode
|
|
{
|
|
BrowseName = QualifiedName.From("child"),
|
|
NodeId = id,
|
|
NodeClass = NodeClass.Variable
|
|
};
|
|
ArrayOf<QualifiedName> browsePath = [QualifiedName.From("child")];
|
|
var references = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = new ExpandedNodeId(id),
|
|
BrowseName = QualifiedName.From("child"),
|
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
|
IsForward = true
|
|
}
|
|
};
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == ReferenceTypeIds.HierarchicalReferences),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. references])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == id),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results = [expected],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
INode? result = await nodeCache.GetNodeWithBrowsePathAsync(id, browsePath, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
|
|
// Act
|
|
result = await nodeCache.GetNodeWithBrowsePathAsync(id, browsePath, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetNodeWithBrowsePathAsyncShouldReturnNodeWithMultipleElementsAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var rootId = new NodeId("root", 0);
|
|
var childId = new NodeId("child", 0);
|
|
var grandChildId = new NodeId("grandChild", 0);
|
|
ArrayOf<QualifiedName> browsePath =
|
|
[
|
|
new QualifiedName("child"),
|
|
new QualifiedName("grandChild")
|
|
];
|
|
|
|
var rootReferences = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = new ExpandedNodeId(childId),
|
|
BrowseName = QualifiedName.From("child"),
|
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
|
IsForward = true
|
|
}
|
|
};
|
|
var childNode = new VariableNode
|
|
{
|
|
BrowseName = QualifiedName.From("child"),
|
|
NodeId = childId,
|
|
NodeClass = NodeClass.Variable
|
|
};
|
|
var childReferences = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = new ExpandedNodeId(grandChildId),
|
|
BrowseName = QualifiedName.From("grandChild"),
|
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
|
IsForward = true
|
|
}
|
|
};
|
|
var expected = new VariableNode
|
|
{
|
|
BrowseName = QualifiedName.From("grandChild"),
|
|
NodeId = grandChildId,
|
|
NodeClass = NodeClass.Variable
|
|
};
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == ReferenceTypeIds.HierarchicalReferences),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == rootId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. rootReferences])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == childId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. childReferences])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == childId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results = [childNode],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == grandChildId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results = [expected],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
INode? result = await nodeCache
|
|
.GetNodeWithBrowsePathAsync(rootId, browsePath, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
|
|
// Act
|
|
result = await nodeCache.GetNodeWithBrowsePathAsync(rootId, browsePath, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetReferencesAsyncShouldHandleEmptyListOfNodeIdsAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<INode> result = await nodeCache
|
|
.GetReferencesAsync([], [], false, false, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result.IsEmpty, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetReferencesAsyncShouldReturnReferencesFromCacheAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var referenceTypeId = new NodeId("referenceType", 0);
|
|
var targetExpandedNodeId = new ExpandedNodeId("target", 0);
|
|
var targetNodeId = new NodeId("target", 0);
|
|
var expected = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = targetExpandedNodeId,
|
|
ReferenceTypeId = referenceTypeId,
|
|
IsForward = false
|
|
}
|
|
};
|
|
var id = new NodeId("test", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. expected])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == targetNodeId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new VariableNode
|
|
{
|
|
NodeId = targetNodeId,
|
|
NodeClass = NodeClass.Variable
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<INode> result1 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, true, false, default)
|
|
.ConfigureAwait(false);
|
|
ArrayOf<INode> result2 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, false, false, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result1.Count, Is.EqualTo(1));
|
|
Assert.That(result2.IsEmpty, Is.True);
|
|
// Act
|
|
result1 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, true, false, default)
|
|
.ConfigureAwait(false);
|
|
result2 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, false, false, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result1.Count, Is.EqualTo(1));
|
|
Assert.That(result2.IsEmpty, Is.True);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetReferencesAsyncWithMoreThanOneSubtypeShouldReturnReferencesFromCacheAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var referenceTypeId = new NodeId("referenceType", 0);
|
|
var referenceSubTypeId = new NodeId("referenceSubType", 0);
|
|
var targetExpandedNodeId = new ExpandedNodeId("target", 0);
|
|
var targetNodeId = new NodeId("target", 0);
|
|
var expected = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = targetExpandedNodeId,
|
|
ReferenceTypeId = referenceSubTypeId,
|
|
IsForward = false
|
|
}
|
|
};
|
|
var id = new NodeId("test", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == referenceTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new()
|
|
{
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
BrowseName = QualifiedName.From("HasSubtype"),
|
|
NodeId = new ExpandedNodeId(referenceSubTypeId)
|
|
}
|
|
])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == referenceSubTypeId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new ReferenceTypeNode
|
|
{
|
|
NodeId = referenceSubTypeId,
|
|
NodeClass = NodeClass.ReferenceType
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == referenceSubTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new()
|
|
{
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = false,
|
|
BrowseName = QualifiedName.From("HasSuperType"),
|
|
NodeId = new ExpandedNodeId(referenceTypeId)
|
|
}
|
|
])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. expected])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == targetNodeId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new VariableNode
|
|
{
|
|
NodeId = targetNodeId,
|
|
NodeClass = NodeClass.Variable
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<INode> result1 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, true, true, default)
|
|
.ConfigureAwait(false);
|
|
ArrayOf<INode> result2 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, false, true, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result1.Count, Is.EqualTo(1));
|
|
Assert.That(result2.IsEmpty, Is.True);
|
|
// Act
|
|
result1 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, true, true, default)
|
|
.ConfigureAwait(false);
|
|
result2 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, false, true, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result1.Count, Is.EqualTo(1));
|
|
Assert.That(result2.IsEmpty, Is.True);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetReferencesAsyncWithSubtypesShouldReturnReferencesFromCacheAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var referenceTypeId = new NodeId("referenceType", 0);
|
|
var targetExpandedNodeId = new ExpandedNodeId("target", 0);
|
|
var targetNodeId = new NodeId("target", 0);
|
|
var expected = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = targetExpandedNodeId,
|
|
ReferenceTypeId = referenceTypeId,
|
|
IsForward = false
|
|
}
|
|
};
|
|
var id = new NodeId("test", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == referenceTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. expected])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == targetNodeId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new VariableNode
|
|
{
|
|
NodeId = targetNodeId,
|
|
NodeClass = NodeClass.Variable
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<INode> result1 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, true, true, default)
|
|
.ConfigureAwait(false);
|
|
ArrayOf<INode> result2 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, false, true, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result1.Count, Is.EqualTo(1));
|
|
Assert.That(result2.IsEmpty, Is.True);
|
|
// Act
|
|
result1 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, true, true, default)
|
|
.ConfigureAwait(false);
|
|
result2 = await nodeCache
|
|
.GetReferencesAsync(id, referenceTypeId, false, true, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result1.Count, Is.EqualTo(1));
|
|
Assert.That(result2.IsEmpty, Is.True);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FindSuperTypeAsyncShouldHandleNoSupertypeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var typeId = new NodeId("type", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == typeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
NodeId result = await nodeCache.FindSuperTypeAsync(typeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(NodeId.Null));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FindSuperTypeAsyncShouldReturnSuperTypeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var superTypeId = new NodeId("superType", 0);
|
|
var subTypeId = new NodeId("subType", 0);
|
|
var references = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = new ExpandedNodeId(superTypeId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = false
|
|
}
|
|
};
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == subTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. references])
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
NodeId result = await nodeCache.FindSuperTypeAsync(subTypeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(superTypeId));
|
|
|
|
// Act
|
|
result = await nodeCache.FindSuperTypeAsync(subTypeId, default).ConfigureAwait(false);
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(superTypeId));
|
|
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FindReferenceTypeAsyncReturnsNodeIdFromHierarchyAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
NodeId rootId = ReferenceTypeIds.References;
|
|
var childId = new NodeId("CustomReference", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchNodeAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == rootId),
|
|
NodeClass.Unspecified,
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ReferenceTypeNode
|
|
{
|
|
NodeId = rootId,
|
|
BrowseName = QualifiedName.From("References"),
|
|
NodeClass = NodeClass.ReferenceType
|
|
})
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(ids => ids.Count == 1 && ids[0] == childId),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new ReferenceTypeNode
|
|
{
|
|
NodeId = childId,
|
|
BrowseName = QualifiedName.From("CustomReference"),
|
|
NodeClass = NodeClass.ReferenceType
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == rootId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new ReferenceDescription
|
|
{
|
|
NodeId = new ExpandedNodeId(childId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = true
|
|
}
|
|
])
|
|
.Verifiable(Times.Exactly(2));
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
NodeId result = await nodeCache.FindReferenceTypeAsync(
|
|
QualifiedName.From("CustomReference"),
|
|
default).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.EqualTo(childId));
|
|
Assert.That(
|
|
await nodeCache.FindReferenceTypeAsync(QualifiedName.Null, default).ConfigureAwait(false),
|
|
Is.EqualTo(NodeId.Null));
|
|
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FindReferenceTypeAsyncStopsAtHierarchyCycleAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
NodeId rootId = ReferenceTypeIds.References;
|
|
var childId = new NodeId("Cycle", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
context.Setup(c => c.NamespaceUris).Returns(new NamespaceTable());
|
|
context
|
|
.Setup(c => c.FetchNodeAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<NodeId>(),
|
|
NodeClass.Unspecified,
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns((RequestHeader _, NodeId nodeId, NodeClass _, bool _, CancellationToken _) =>
|
|
new ValueTask<Node>(new ReferenceTypeNode
|
|
{
|
|
NodeId = nodeId,
|
|
BrowseName = QualifiedName.From(nodeId == rootId ? "References" : "Cycle"),
|
|
NodeClass = NodeClass.ReferenceType
|
|
}));
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(ids => ids.Count == 1 && ids[0] == childId),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new ReferenceTypeNode
|
|
{
|
|
NodeId = childId,
|
|
BrowseName = QualifiedName.From("Cycle"),
|
|
NodeClass = NodeClass.ReferenceType
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
});
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<NodeId>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((RequestHeader _, NodeId nodeId, CancellationToken _) =>
|
|
[
|
|
new ReferenceDescription
|
|
{
|
|
NodeId = new ExpandedNodeId(nodeId == rootId ? childId : rootId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = true
|
|
}
|
|
]);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
NodeId result = await nodeCache.FindReferenceTypeAsync(
|
|
QualifiedName.From("Missing"),
|
|
default).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.EqualTo(NodeId.Null));
|
|
}
|
|
|
|
[Test]
|
|
public async Task FindDataTypeIdAsyncAndIsEncodingOfAsyncReturnMatchesAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
var dataTypeId = new NodeId("DataType", 0);
|
|
var encodingId = new NodeId("DataTypeEncoding", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == encodingId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new ReferenceDescription
|
|
{
|
|
NodeId = new ExpandedNodeId(dataTypeId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasEncoding,
|
|
IsForward = false
|
|
}
|
|
])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(ids => ids.Count == 1 && ids[0] == dataTypeId),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new Node
|
|
{
|
|
NodeId = dataTypeId,
|
|
BrowseName = QualifiedName.From("DataType"),
|
|
NodeClass = NodeClass.DataType
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
NodeId result = await nodeCache.FindDataTypeIdAsync(
|
|
new ExpandedNodeId(encodingId),
|
|
default).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.EqualTo(dataTypeId));
|
|
Assert.That(
|
|
await nodeCache.IsEncodingOfAsync(
|
|
new ExpandedNodeId(encodingId),
|
|
new ExpandedNodeId(dataTypeId),
|
|
default).ConfigureAwait(false),
|
|
Is.True);
|
|
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task IsTypeOfAsyncReturnsTrueForRecursiveSubtypeChainAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
var superTypeId = new NodeId("SuperType", 0);
|
|
var middleTypeId = new NodeId("MiddleType", 0);
|
|
var subTypeId = new NodeId("SubType", 0);
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == subTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new ReferenceDescription
|
|
{
|
|
NodeId = new ExpandedNodeId(middleTypeId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = false
|
|
}
|
|
])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == middleTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(
|
|
[
|
|
new ReferenceDescription
|
|
{
|
|
NodeId = new ExpandedNodeId(superTypeId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = false
|
|
}
|
|
])
|
|
.Verifiable(Times.Once);
|
|
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
Assert.That(await nodeCache.IsTypeOfAsync(subTypeId, superTypeId, default).ConfigureAwait(false), Is.True);
|
|
Assert.That(await nodeCache.IsTypeOfAsync(superTypeId, superTypeId, default).ConfigureAwait(false), Is.True);
|
|
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void GetValueAsyncShouldHandleErrors()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var id = new NodeId("test", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchValueAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<NodeId>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError))
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act && Assert
|
|
_ = Assert.ThrowsAsync<ServiceResultException>(async () =>
|
|
await nodeCache.GetValueAsync(id, default).ConfigureAwait(false));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetValueAsyncShouldReturnValueFromCacheAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var expected = new DataValue(new Variant(123), StatusCodes.Good, DateTime.UtcNow);
|
|
var id = new NodeId("test", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchValueAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expected)
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
DataValue result = await nodeCache.GetValueAsync(id, default).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
result = await nodeCache.GetValueAsync(id, default).ConfigureAwait(false);
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetValuesAsyncShouldHandleErrorsAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
ArrayOf<NodeId> ids = [new("test1", 0), new("test2", 0)];
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchValuesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<ArrayOf<NodeId>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<DataValue>
|
|
{
|
|
Results = [new(), new()],
|
|
Errors = [new ServiceResult(StatusCodes.BadUnexpectedError), ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<DataValue> result = await nodeCache.GetValuesAsync(ids, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result.Count, Is.EqualTo(2));
|
|
Assert.That((uint)result[0].StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
Assert.That((uint)result[1].StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
context.Verify();
|
|
}
|
|
|
|
/// <summary>
|
|
/// L9-8: the lookups swallowed every exception, so a cancelled lookup
|
|
/// reported "node does not exist" instead of honouring cancellation.
|
|
/// </summary>
|
|
[Test]
|
|
public void LookupsPropagateCancellation()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
var context = new Mock<INodeCacheContext>();
|
|
context.Setup(c => c.NamespaceUris).Returns(new NamespaceTable());
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
var id = new NodeId("uncached", 0);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
async () => await nodeCache.FindAsync(id, cts.Token).ConfigureAwait(false),
|
|
Throws.InstanceOf<OperationCanceledException>());
|
|
Assert.That(
|
|
async () => await nodeCache.IsKnownAsync(id, cts.Token).ConfigureAwait(false),
|
|
Throws.InstanceOf<OperationCanceledException>());
|
|
Assert.That(
|
|
async () => await nodeCache.FindReferenceTypeNameAsync(id, cts.Token)
|
|
.ConfigureAwait(false),
|
|
Throws.InstanceOf<OperationCanceledException>());
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// L9-6: an empty DataValue (legal on the wire) collided with the
|
|
/// "missing slot" sentinel, so the next node's value landed in its
|
|
/// slot and the next slot stayed empty.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task GetValuesAsyncKeepsEmptyDataValueInItsOwnSlotAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
ArrayOf<NodeId> ids = [new("empty", 0), new("value", 0)];
|
|
var context = new Mock<INodeCacheContext>();
|
|
context
|
|
.Setup(c => c.FetchValuesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<ArrayOf<NodeId>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<DataValue>
|
|
{
|
|
Results = [default, new(new Variant(456), StatusCodes.Good, DateTime.UtcNow)],
|
|
Errors = [ServiceResult.Good, ServiceResult.Good]
|
|
});
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
ArrayOf<DataValue> result = await nodeCache.GetValuesAsync(ids, default)
|
|
.ConfigureAwait(false);
|
|
|
|
Assert.That(result.Count, Is.EqualTo(2));
|
|
Assert.That(result[0].IsNull, Is.True, "the empty value belongs to the first node");
|
|
Assert.That(result[1].WrappedValue, Is.EqualTo(new Variant(456)));
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetValuesAsyncShouldReturnValuesFromCacheAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
ArrayOf<DataValue> expected =
|
|
[
|
|
new(new Variant(123), StatusCodes.Good, DateTime.UtcNow),
|
|
new(new Variant(456), StatusCodes.Good, DateTime.UtcNow)
|
|
];
|
|
ArrayOf<NodeId> ids = [new("test1", 0), new("test2", 0)];
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchValuesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(i => i.ToList().ToHashSet().SetEquals(ids.ToList().ToHashSet())),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<DataValue>
|
|
{
|
|
Results = expected,
|
|
Errors = [ServiceResult.Good, ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<DataValue> result = await nodeCache.GetValuesAsync(ids, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
|
|
result = await nodeCache.GetValuesAsync(ids, default).ConfigureAwait(false);
|
|
Assert.That(result, Is.EqualTo(expected));
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetValuesAsyncShouldReturnValuesFromCacheButHonorStatusOfReadAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
ArrayOf<DataValue> expected =
|
|
[
|
|
new(new Variant(123), StatusCodes.Good, DateTime.UtcNow),
|
|
new(new Variant(456), StatusCodes.Good, DateTime.UtcNow)
|
|
];
|
|
ArrayOf<NodeId> ids = [new("test1", 0), new("test2", 0)];
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchValuesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(i => i.ToList().ToHashSet().SetEquals(ids.ToList().ToHashSet())),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<DataValue>
|
|
{
|
|
Results = expected,
|
|
Errors = [ServiceResult.Good, new ServiceResult(StatusCodes.Bad)]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
ArrayOf<DataValue> result = await nodeCache.GetValuesAsync(ids, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result[0], Is.EqualTo(expected[0]));
|
|
Assert.That((uint)result[1].StatusCode, Is.EqualTo(StatusCodes.Bad));
|
|
Assert.That(result[1].WrappedValue, Is.EqualTo(expected[1].WrappedValue));
|
|
|
|
context
|
|
.Setup(c => c.FetchValuesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(i => i.Count == 1 && i[0] == ids[1]),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<DataValue>
|
|
{
|
|
Results = [expected[1]],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
result = await nodeCache.GetValuesAsync(ids, default).ConfigureAwait(false);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task IsTypeOfShouldHandleNoReferencesAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var superTypeId = new NodeId("superType", 0);
|
|
var subTypeId = new NodeId("subType", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == subTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i != subTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Never);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
bool result = await nodeCache.IsTypeOfAsync(subTypeId, superTypeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.False);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task IsTypeOfShouldReturnTrueForSuperTypeAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var superTypeId = new NodeId("superType", 0);
|
|
var subTypeId = new NodeId("subType", 0);
|
|
var references = new List<ReferenceDescription>
|
|
{
|
|
new()
|
|
{
|
|
NodeId = new ExpandedNodeId(superTypeId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = false
|
|
}
|
|
};
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == subTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([.. references])
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
bool result = await nodeCache.IsTypeOfAsync(subTypeId, superTypeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.True);
|
|
|
|
// Act
|
|
result = await nodeCache.IsTypeOfAsync(subTypeId, superTypeId, default)
|
|
.ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.True);
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task LoadTypeHierarchyAsyncShouldHandleNoSubtypesAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var typeId = new NodeId("type", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == typeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task LoadTypeHierarchyAsyncShouldLoadTypeHierarchyAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange
|
|
var typeId = new NodeId("type", 0);
|
|
var subTypeId = new NodeId("subType", 0);
|
|
ArrayOf<ReferenceDescription> references =
|
|
[
|
|
new()
|
|
{
|
|
NodeId = new ExpandedNodeId(subTypeId),
|
|
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
|
|
IsForward = true
|
|
}
|
|
];
|
|
|
|
var context = new Mock<INodeCacheContext>();
|
|
var nsTable = new NamespaceTable();
|
|
context.Setup(c => c.NamespaceUris).Returns(nsTable);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == typeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(references)
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchReferencesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == subTypeId),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync([])
|
|
.Verifiable(Times.Once);
|
|
context
|
|
.Setup(c => c.FetchNodesAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<ArrayOf<NodeId>>(n => n.Count == 1 && n[0] == subTypeId),
|
|
false,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ResultSet<Node>
|
|
{
|
|
Results =
|
|
[
|
|
new DataTypeNode
|
|
{
|
|
NodeId = subTypeId,
|
|
NodeClass = NodeClass.DataType
|
|
}
|
|
],
|
|
Errors = [ServiceResult.Good]
|
|
})
|
|
.Verifiable(Times.Once);
|
|
var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act
|
|
await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false);
|
|
await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false);
|
|
await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
context.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task MetricsAreEmittedViaTelemetryContextAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
// Arrange — capture instruments published on the OPC UA client meter
|
|
var capturedHits = new Dictionary<string, long>();
|
|
var capturedMisses = new Dictionary<string, long>();
|
|
var capturedSize = new Dictionary<string, long>();
|
|
|
|
using var listener = new MeterListener
|
|
{
|
|
InstrumentPublished = (instrument, l) =>
|
|
{
|
|
if (instrument.Name.StartsWith("opc.ua.client.nodecache.", StringComparison.Ordinal))
|
|
{
|
|
l.EnableMeasurementEvents(instrument);
|
|
}
|
|
}
|
|
};
|
|
listener.SetMeasurementEventCallback<long>((instrument, value, tags, state) =>
|
|
{
|
|
string cacheTag = string.Empty;
|
|
foreach (KeyValuePair<string, object?> kv in tags)
|
|
{
|
|
if (kv.Key == "cache" && kv.Value is string s)
|
|
{
|
|
cacheTag = s;
|
|
break;
|
|
}
|
|
}
|
|
switch (instrument.Name)
|
|
{
|
|
case "opc.ua.client.nodecache.hits":
|
|
capturedHits[cacheTag] = value;
|
|
break;
|
|
case "opc.ua.client.nodecache.misses":
|
|
capturedMisses[cacheTag] = value;
|
|
break;
|
|
case "opc.ua.client.nodecache.size":
|
|
capturedSize[cacheTag] = value;
|
|
break;
|
|
}
|
|
});
|
|
listener.Start();
|
|
|
|
var id = new NodeId("metricsNode", 0);
|
|
var context = new Mock<INodeCacheContext>();
|
|
context
|
|
.Setup(c => c.FetchNodeAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.Is<NodeId>(i => i == id),
|
|
NodeClass.Unspecified,
|
|
It.IsAny<bool>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new Node { NodeId = id });
|
|
|
|
using var nodeCache = new NodeCache(context.Object, telemetry);
|
|
|
|
// Act — first call is a miss (fetches), second call is a hit
|
|
_ = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
_ = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false);
|
|
|
|
listener.RecordObservableInstruments();
|
|
|
|
// Assert — at least one miss and one hit on the nodes cache, size == 1
|
|
Assert.That(capturedMisses.ContainsKey("nodes"), Is.True);
|
|
Assert.That(capturedHits.ContainsKey("nodes"), Is.True);
|
|
Assert.That(capturedSize.ContainsKey("nodes"), Is.True);
|
|
Assert.That(capturedMisses["nodes"], Is.GreaterThanOrEqualTo(1));
|
|
Assert.That(capturedHits["nodes"], Is.GreaterThanOrEqualTo(1));
|
|
Assert.That(capturedSize["nodes"], Is.EqualTo(1));
|
|
}
|
|
}
|
|
}
|