mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
9bfc780e14
# Description Update 55 NuGet dependencies to the latest stable releases available on 2026-10-19, preserving documented compatibility pins and framework-specific version bands. Existing OpenUSD prereleases remain unchanged because no newer releases are available. Migrate existing tests to NUnit 5: await asynchronous assertions, propagate async through helpers, handle nullable predicates, and replace obsolete `Order` attributes with sequencing dependencies. Sequencing dependencies use `AllowFailure = true`; filtering a dependent test also runs prerequisites. Adjust fixture-level parallelism where NUnit dependency scheduling requires it. Keep Roslyn API/runtime pins intact; select newer Immutable/Metadata runtime packages only for test and profiling hosts requiring them. Update the approved SDK pin to 10.0.402 across local, CI, and container builds. Document NUnit 5 conventions.
1354 lines
57 KiB
C#
1354 lines
57 KiB
C#
/* ========================================================================
|
|
* 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/
|
|
* ======================================================================*/
|
|
|
|
// CA2000: test code; disposables are ownership-transferred to the test
|
|
// harness or are short-lived, making CA2000 noisy without a real leak risk.
|
|
#pragma warning disable CA2000
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
|
|
namespace Opc.Ua.Server.Tests.NodeManager
|
|
{
|
|
/// <summary>
|
|
/// Deterministic, offline unit tests that drive the synchronous surface of
|
|
/// <see cref="CustomNodeManager2"/> (the members implemented in
|
|
/// CustomNodeManager.cs) directly against a fully mocked
|
|
/// <see cref="IServerInternal"/>. No sockets, no sampling timers and no
|
|
/// wall-clock assertions are used, so the tests are order independent and
|
|
/// stable on every target framework.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("NodeManager")]
|
|
[Category("CustomNodeManagerDeterministic")]
|
|
[Parallelizable(ParallelScope.All)]
|
|
public sealed class CustomNodeManagerDeterministicTests
|
|
{
|
|
private const string TestNamespaceUri = "http://test.org/UA/CustomNodeManagerDeterministic/";
|
|
|
|
[Test]
|
|
public void CreateNode_AddsNodeToPredefinedNodesAndEmitsModelChange()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
ushort ns = h.NamespaceIndex;
|
|
var instance = new BaseObjectState(null);
|
|
// Opt into unconditional emission so the model-change event fires for a
|
|
// node that carries no NodeVersion property (the default requires one).
|
|
h.Manager.RequireNodeVersionForModelChange = false;
|
|
|
|
NodeId resultId = h.Manager.CreateNode(
|
|
h.Context,
|
|
NodeId.Null,
|
|
ReferenceTypeIds.Organizes,
|
|
new QualifiedName("Created", ns),
|
|
instance);
|
|
|
|
Assert.That(resultId.IsNull, Is.False);
|
|
Assert.That(resultId, Is.EqualTo(instance.NodeId));
|
|
Assert.That(instance.BrowseName, Is.EqualTo(new QualifiedName("Created", ns)));
|
|
Assert.That(h.Manager.PredefinedNodes.ContainsKey(resultId), Is.True);
|
|
Assert.That(h.Manager.PredefinedNodes[resultId], Is.SameAs(instance));
|
|
// The model-change event is emitted through Server.ReportEvent.
|
|
h.MockServer.Verify(s => s.ReportEvent(It.IsAny<IFilterTarget>()), Times.AtLeastOnce);
|
|
}
|
|
|
|
[Test]
|
|
public void CreateNode_WithUnknownParent_ThrowsBadNodeIdUnknown()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
ushort ns = h.NamespaceIndex;
|
|
var child = new BaseObjectState(null)
|
|
{
|
|
NodeId = new NodeId("OrphanChild", ns)
|
|
};
|
|
|
|
ServiceResultException ex = Assert.Throws<ServiceResultException>(
|
|
() => h.Manager.CreateNode(
|
|
h.Context,
|
|
new NodeId("MissingParent", ns),
|
|
ReferenceTypeIds.Organizes,
|
|
new QualifiedName("OrphanChild", ns),
|
|
child))!;
|
|
|
|
Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
|
|
}
|
|
|
|
[Test]
|
|
public void AddPredefinedNode_AddsNodeToPredefinedNodes()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewVariable("Added", 7);
|
|
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
Assert.That(h.Manager.PredefinedNodes.ContainsKey(variable.NodeId), Is.True);
|
|
Assert.That(h.Manager.PredefinedNodes[variable.NodeId], Is.SameAs(variable));
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteNode_RemovesKnownNodeAndReturnsTrue()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("ToDelete");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
Assert.That(h.Manager.PredefinedNodes.ContainsKey(node.NodeId), Is.True);
|
|
|
|
bool result = h.Manager.DeleteNode(h.Context, node.NodeId);
|
|
|
|
Assert.That(result, Is.True);
|
|
Assert.That(h.Manager.PredefinedNodes.ContainsKey(node.NodeId), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteNode_ReturnsFalseForUnknownNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
|
|
bool result = h.Manager.DeleteNode(h.Context, new NodeId("Ghost", h.NamespaceIndex));
|
|
|
|
Assert.That(result, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void CreateAddressSpace_LoadsNodesFromOverride()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var folder = new FolderState(null);
|
|
folder.CreateAsPredefinedNode(h.Context);
|
|
folder.NodeId = new NodeId("Folder", h.NamespaceIndex);
|
|
folder.BrowseName = new QualifiedName("Folder", h.NamespaceIndex);
|
|
folder.DisplayName = new LocalizedText("Folder");
|
|
h.Manager.NodesToLoad = [folder];
|
|
|
|
h.Manager.CreateAddressSpace(new Dictionary<NodeId, IList<IReference>>());
|
|
|
|
Assert.That(h.Manager.PredefinedNodes.ContainsKey(folder.NodeId), Is.True);
|
|
Assert.That(h.Manager.PredefinedNodes[folder.NodeId], Is.SameAs(folder));
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteAddressSpace_ClearsPredefinedNodes()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
h.Manager.AddPredefinedNodePublic(h.Context, h.NewObject("A"));
|
|
h.Manager.AddPredefinedNodePublic(h.Context, h.NewObject("B"));
|
|
Assert.That(h.Manager.PredefinedNodes, Has.Count.EqualTo(2));
|
|
|
|
h.Manager.DeleteAddressSpace();
|
|
|
|
Assert.That(h.Manager.PredefinedNodes, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteAddressSpaceRemovesRootNotifiers()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState notifier = h.NewObject("Notifier");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, notifier);
|
|
h.Manager.AddRootNotifierPublic(notifier);
|
|
Assert.That(h.Manager.RootNotifiersDictionary.ContainsKey(notifier.NodeId), Is.True);
|
|
|
|
h.Manager.DeleteAddressSpace();
|
|
|
|
Assert.That(h.Manager.RootNotifiersDictionary, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteAddressSpaceTracksAndClearsRemovedExternalReferences()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("WithExternalRef");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
var externalTarget = new NodeId("External", h.NamespaceIndex);
|
|
node.AddReference(ReferenceTypeIds.HasNotifier, false, externalTarget);
|
|
|
|
Assert.That(h.Manager.GetRemovedExternalReferences(), Is.Empty);
|
|
|
|
h.Manager.DeleteAddressSpace();
|
|
|
|
List<LocalReference> removed = h.Manager.GetRemovedExternalReferences();
|
|
Assert.That(
|
|
removed,
|
|
Has.Some.Matches<LocalReference>(
|
|
r => r is not null && r.SourceId == externalTarget && r.TargetId == node.NodeId));
|
|
|
|
h.Manager.ClearRemovedExternalReferences();
|
|
|
|
Assert.That(h.Manager.GetRemovedExternalReferences(), Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteNodeWhenTypeNotOwnedByAnotherNodeManagerRemovesFromTypeTree()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectTypeState type = h.NewObjectType("SoleType");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, type);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsKnown(type.NodeId), Is.True);
|
|
|
|
bool result = h.Manager.DeleteNode(h.Context, type.NodeId);
|
|
|
|
Assert.That(result, Is.True);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsKnown(type.NodeId), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteNodeWhenTypeOwnedByAnotherNodeManagerKeepsTypeTreeEntry()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectTypeState type = h.NewObjectType("SharedType");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, type);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsKnown(type.NodeId), Is.True);
|
|
|
|
var otherSyncManager = new Mock<INodeManager>();
|
|
otherSyncManager.Setup(m => m.GetManagerHandle(type.NodeId)).Returns(new object());
|
|
var otherAsyncManager = new Mock<IAsyncNodeManager>();
|
|
otherAsyncManager.Setup(m => m.SyncNodeManager).Returns(otherSyncManager.Object);
|
|
var masterNodeManagerMock =
|
|
Mock.Get(h.MockServer.Object.NodeManager);
|
|
masterNodeManagerMock
|
|
.Setup(m => m.AsyncNodeManagers)
|
|
.Returns([h.Manager.ToAsyncNodeManager(), otherAsyncManager.Object]);
|
|
|
|
bool result = h.Manager.DeleteNode(h.Context, type.NodeId);
|
|
|
|
Assert.That(result, Is.True);
|
|
Assert.That(h.Manager.PredefinedNodes.ContainsKey(type.NodeId), Is.False);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsKnown(type.NodeId), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void RebuildTypeTreeRegistersSubtypeAndEncodingRelationshipsForDirectlyInsertedNodes()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
ushort ns = h.NamespaceIndex;
|
|
BaseObjectTypeState type = h.NewObjectType("RebuiltType");
|
|
// Insert directly into PredefinedNodes to bypass AddPredefinedNode's own
|
|
// automatic type-tree wiring, mirroring nodes restored after a reload where
|
|
// the caller explicitly triggers RebuildTypeTree afterwards.
|
|
h.Manager.PredefinedNodes.Add(type.NodeId, type);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsKnown(type.NodeId), Is.False);
|
|
|
|
var dataTypeId = new NodeId("RebuiltDataType", ns);
|
|
h.MockServer.Object.TypeTree.AddSubtype(dataTypeId, NodeId.Null);
|
|
BaseObjectState encoding = h.NewObject("RebuiltEncoding");
|
|
encoding.AddReference(ReferenceTypeIds.HasEncoding, true, dataTypeId);
|
|
h.Manager.PredefinedNodes.Add(encoding.NodeId, encoding);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsEncodingOf(encoding.NodeId, dataTypeId), Is.False);
|
|
|
|
h.Manager.RebuildTypeTree();
|
|
|
|
Assert.That(h.MockServer.Object.TypeTree.IsKnown(type.NodeId), Is.True);
|
|
Assert.That(h.MockServer.Object.TypeTree.IsEncodingOf(encoding.NodeId, dataTypeId), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void DisposeCalledTwiceDoesNotThrow()
|
|
{
|
|
Harness h = CreateHarness();
|
|
h.Manager.AddPredefinedNodePublic(h.Context, h.NewObject("DisposeTarget"));
|
|
|
|
h.Manager.Dispose();
|
|
|
|
Assert.DoesNotThrow(h.Manager.Dispose);
|
|
}
|
|
|
|
[Test]
|
|
public void FindPredefinedNodeGeneric_ReturnsNodeWhenTypeMatches()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewVariable("Find", 1);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
BaseDataVariableState? found = h.Manager.FindPredefinedNode<BaseDataVariableState>(variable.NodeId);
|
|
|
|
Assert.That(found, Is.SameAs(variable));
|
|
}
|
|
|
|
[Test]
|
|
public void FindPredefinedNodeGeneric_ReturnsNullWhenTypeMismatch()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("TypeMismatch");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
|
|
BaseDataVariableState? found = h.Manager.FindPredefinedNode<BaseDataVariableState>(node.NodeId);
|
|
|
|
Assert.That(found, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void FindPredefinedNodeGeneric_ReturnsNullWhenNotFound()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
|
|
NodeState? found = h.Manager.FindPredefinedNode<NodeState>(new NodeId("Unknown", h.NamespaceIndex));
|
|
|
|
Assert.That(found, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void FindPredefinedNodeGeneric_ReturnsNullForNullNodeId()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
|
|
NodeState? found = h.Manager.FindPredefinedNode<NodeState>(NodeId.Null);
|
|
|
|
Assert.That(found, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void FindPredefinedNodeObsolete_ChecksExpectedType()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("Obsolete");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
|
|
#pragma warning disable CS0618, CA2263 // exercising the obsolete overload on purpose.
|
|
NodeState? match = h.Manager.FindPredefinedNode(node.NodeId, typeof(BaseObjectState));
|
|
NodeState? mismatch = h.Manager.FindPredefinedNode(node.NodeId, typeof(BaseDataVariableState));
|
|
NodeState? missing = h.Manager.FindPredefinedNode(new NodeId("None", h.NamespaceIndex), typeof(NodeState));
|
|
#pragma warning restore CS0618, CA2263
|
|
|
|
Assert.That(match, Is.SameAs(node));
|
|
Assert.That(mismatch, Is.Null);
|
|
Assert.That(missing, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void GetManagerHandle_ReturnsValidatedHandleForKnownNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("Handle");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
|
|
object? handle = h.Manager.GetManagerHandle(node.NodeId);
|
|
|
|
Assert.That(handle, Is.InstanceOf<NodeHandle>());
|
|
var nodeHandle = (NodeHandle)handle!;
|
|
Assert.That(nodeHandle.NodeId, Is.EqualTo(node.NodeId));
|
|
Assert.That(nodeHandle.Node, Is.SameAs(node));
|
|
Assert.That(nodeHandle.Validated, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void GetManagerHandle_ReturnsNullForUnknownNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
|
|
object? foreignHandle = h.Manager.GetManagerHandle(ObjectIds.Server);
|
|
object? unknownHandle = h.Manager.GetManagerHandle(new NodeId("Unknown", h.NamespaceIndex));
|
|
|
|
Assert.That(foreignHandle, Is.Null);
|
|
Assert.That(unknownHandle, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void Read_ReturnsValueForKnownNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewVariable("ReadVar", 42);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToRead = new List<ReadValueId>
|
|
{
|
|
new() { NodeId = variable.NodeId, AttributeId = Attributes.Value },
|
|
new() { NodeId = ObjectIds.Server, AttributeId = Attributes.Value }
|
|
};
|
|
var values = new List<DataValue> { new() };
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Read(h.NewContext(RequestType.Read), 0, nodesToRead, values, errors);
|
|
|
|
Assert.That(nodesToRead[0].Processed, Is.True);
|
|
Assert.That(nodesToRead[1].Processed, Is.False);
|
|
Assert.That(ServiceResult.IsGood(errors[0]), Is.True);
|
|
Assert.That((int)values[0].WrappedValue, Is.EqualTo(42));
|
|
Assert.That(values[0].StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
Assert.That(values[0].ServerTimestamp, Is.EqualTo(values[0].SourceTimestamp));
|
|
}
|
|
|
|
[Test]
|
|
public void Read_StampsFreshServerTimestampForStaticNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewVariable("StaleVar", 42);
|
|
|
|
// simulate a static node whose value was produced in the past.
|
|
DateTimeUtc staleTimestamp = DateTimeUtc.Now - TimeSpan.FromMinutes(1);
|
|
variable.Timestamp = staleTimestamp;
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToRead = new List<ReadValueId>
|
|
{
|
|
new() { NodeId = variable.NodeId, AttributeId = Attributes.Value }
|
|
};
|
|
var values = new List<DataValue> { new() };
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
DateTimeUtc beforeRead = DateTimeUtc.Now;
|
|
h.Manager.Read(h.NewContext(RequestType.Read), 0, nodesToRead, values, errors);
|
|
|
|
Assert.That(ServiceResult.IsGood(errors[0]), Is.True);
|
|
|
|
// SourceTimestamp reflects the (stale) time the value was produced.
|
|
Assert.That(values[0].SourceTimestamp, Is.EqualTo(staleTimestamp));
|
|
|
|
// ServerTimestamp reflects when the server verified the value: the
|
|
// read time, so a MaxAge read is satisfied even for a static value.
|
|
Assert.That(values[0].ServerTimestamp, Is.GreaterThanOrEqualTo(beforeRead));
|
|
Assert.That(values[0].ServerTimestamp, Is.Not.EqualTo(values[0].SourceTimestamp));
|
|
}
|
|
|
|
[Test]
|
|
public void Read_ReturnsBrowseNameForNonValueAttribute()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewVariable("MetaVar", 1);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToRead = new List<ReadValueId>
|
|
{
|
|
new() { NodeId = variable.NodeId, AttributeId = Attributes.BrowseName }
|
|
};
|
|
var values = new List<DataValue> { new() };
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Read(h.NewContext(RequestType.Read), 0, nodesToRead, values, errors);
|
|
|
|
Assert.That(ServiceResult.IsGood(errors[0]), Is.True);
|
|
Assert.That(values[0].StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
var browseName = (QualifiedName)values[0].WrappedValue;
|
|
Assert.That(browseName, Is.EqualTo(new QualifiedName("MetaVar", h.NamespaceIndex)));
|
|
}
|
|
|
|
[Test]
|
|
public void Read_ReturnsBadAttributeIdInvalidForInvalidAttribute()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("ReadObject");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
|
|
var nodesToRead = new List<ReadValueId>
|
|
{
|
|
// Value is not a valid attribute of an Object node.
|
|
new() { NodeId = node.NodeId, AttributeId = Attributes.Value }
|
|
};
|
|
var values = new List<DataValue> { new() };
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Read(h.NewContext(RequestType.Read), 0, nodesToRead, values, errors);
|
|
|
|
Assert.That(nodesToRead[0].Processed, Is.True);
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadAttributeIdInvalid));
|
|
}
|
|
|
|
[Test]
|
|
public void Read_ReturnsBadNodeIdUnknownForPlaceholderNode()
|
|
{
|
|
using Harness h = CreatePlaceholderHarness(out PlaceholderNodeManager manager);
|
|
manager.PlaceholderNodeId = new NodeId("Ghost", h.NamespaceIndex);
|
|
|
|
var nodesToRead = new List<ReadValueId>
|
|
{
|
|
new() { NodeId = manager.PlaceholderNodeId, AttributeId = Attributes.Value }
|
|
};
|
|
var values = new List<DataValue> { new() };
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
manager.Read(h.NewContext(RequestType.Read), 0, nodesToRead, values, errors);
|
|
|
|
Assert.That(nodesToRead[0].Processed, Is.True);
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
|
|
}
|
|
|
|
[Test]
|
|
public void Write_WritesValueToKnownNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewWritableVariable("WriteVar", 0);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToWrite = new List<WriteValue>
|
|
{
|
|
new() {
|
|
NodeId = variable.NodeId,
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant(99))
|
|
},
|
|
new() {
|
|
NodeId = ObjectIds.Server,
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant(99))
|
|
}
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Write(h.NewContext(RequestType.Write), nodesToWrite, errors);
|
|
|
|
Assert.That(nodesToWrite[0].Processed, Is.True);
|
|
Assert.That(nodesToWrite[1].Processed, Is.False);
|
|
Assert.That(ServiceResult.IsGood(errors[0]), Is.True);
|
|
Assert.That(variable.Value, Is.EqualTo(99));
|
|
}
|
|
|
|
[Test]
|
|
public void Write_ReturnsBadNotWritableForReadOnlyNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewVariable("ReadOnlyVar", 0);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToWrite = new List<WriteValue>
|
|
{
|
|
new() {
|
|
NodeId = variable.NodeId,
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant(5))
|
|
}
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Write(h.NewContext(RequestType.Write), nodesToWrite, errors);
|
|
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadNotWritable));
|
|
}
|
|
|
|
[Test]
|
|
public void Write_ReturnsBadWriteNotSupportedForNonValueAttributeWithIndexRange()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewWritableVariable("IdxRangeVar", 0);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToWrite = new List<WriteValue>
|
|
{
|
|
new() {
|
|
NodeId = variable.NodeId,
|
|
AttributeId = Attributes.DisplayName,
|
|
IndexRange = "0",
|
|
Value = new DataValue(new Variant(new LocalizedText("X")))
|
|
}
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Write(h.NewContext(RequestType.Write), nodesToWrite, errors);
|
|
|
|
Assert.That(nodesToWrite[0].Processed, Is.True);
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadWriteNotSupported));
|
|
}
|
|
|
|
[Test]
|
|
public void Write_ReturnsBadTypeMismatchForWrongValueType()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseDataVariableState variable = h.NewWritableVariable("TypeVar", 0);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
|
|
var nodesToWrite = new List<WriteValue>
|
|
{
|
|
new() {
|
|
NodeId = variable.NodeId,
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant("not-an-int"))
|
|
}
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
h.Manager.Write(h.NewContext(RequestType.Write), nodesToWrite, errors);
|
|
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch));
|
|
}
|
|
|
|
[Test]
|
|
public void Write_ReturnsBadNodeIdUnknownForPlaceholderNode()
|
|
{
|
|
using Harness h = CreatePlaceholderHarness(out PlaceholderNodeManager manager);
|
|
manager.PlaceholderNodeId = new NodeId("Ghost", h.NamespaceIndex);
|
|
|
|
var nodesToWrite = new List<WriteValue>
|
|
{
|
|
new() {
|
|
NodeId = manager.PlaceholderNodeId,
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant(1))
|
|
}
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
|
|
manager.Write(h.NewContext(RequestType.Write), nodesToWrite, errors);
|
|
|
|
Assert.That(nodesToWrite[0].Processed, Is.True);
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_ReturnsChildReferences()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
(BaseObjectState parent, BaseObjectState child) = h.AddParentWithChild(ReferenceTypeIds.Organizes);
|
|
object handle = h.Manager.GetManagerHandle(parent.NodeId)!;
|
|
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(handle);
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references);
|
|
|
|
Assert.That(continuationPoint, Is.Null);
|
|
Assert.That(references, Has.Count.EqualTo(1));
|
|
Assert.That(references[0].BrowseName, Is.EqualTo(child.BrowseName));
|
|
Assert.That(references[0].NodeId, Is.EqualTo(new ExpandedNodeId(child.NodeId)));
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_WithMaxResultsToReturn_LeavesContinuationPoint()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState parent = h.NewObject("BrowseParent");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, parent);
|
|
BaseObjectState child1 = h.NewObject("BrowseChild1");
|
|
BaseObjectState child2 = h.NewObject("BrowseChild2");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, child1);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, child2);
|
|
parent.AddReference(ReferenceTypeIds.Organizes, false, child1.NodeId);
|
|
parent.AddReference(ReferenceTypeIds.Organizes, false, child2.NodeId);
|
|
object handle = h.Manager.GetManagerHandle(parent.NodeId)!;
|
|
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(handle);
|
|
continuationPoint.MaxResultsToReturn = 1;
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references);
|
|
|
|
Assert.That(references, Has.Count.EqualTo(1));
|
|
Assert.That(continuationPoint, Is.Not.Null);
|
|
continuationPoint.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_WithReferenceTypeFilter_ExcludesNonMatchingReferences()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
(BaseObjectState parent, _) = h.AddParentWithChild(ReferenceTypeIds.Organizes);
|
|
object handle = h.Manager.GetManagerHandle(parent.NodeId)!;
|
|
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(handle);
|
|
// The child is linked with Organizes only, so a HasComponent filter
|
|
// (without subtypes) must return no references.
|
|
continuationPoint.ReferenceTypeId = ReferenceTypeIds.HasComponent;
|
|
continuationPoint.IncludeSubtypes = false;
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references);
|
|
|
|
Assert.That(references, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_WithInverseDirection_ReturnsParentReference()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState parent = h.NewObject("InverseParent");
|
|
BaseObjectState child = h.NewObject("InverseChild");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, parent);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, child);
|
|
parent.AddReference(ReferenceTypeIds.Organizes, false, child.NodeId);
|
|
child.AddReference(ReferenceTypeIds.Organizes, true, parent.NodeId);
|
|
object handle = h.Manager.GetManagerHandle(child.NodeId)!;
|
|
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(handle);
|
|
continuationPoint.BrowseDirection = BrowseDirection.Inverse;
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references);
|
|
|
|
Assert.That(references, Has.Count.EqualTo(1));
|
|
Assert.That(references[0].NodeId, Is.EqualTo(new ExpandedNodeId(parent.NodeId)));
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_NullContinuationPoint_ThrowsArgumentNullException()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
ContinuationPoint continuationPoint = null!;
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
Assert.Throws<ArgumentNullException>(
|
|
() => h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references));
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_NullReferences_ThrowsArgumentNullException()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("BrowseNullRefs");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
object handle = h.Manager.GetManagerHandle(node.NodeId)!;
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(handle);
|
|
|
|
Assert.Throws<ArgumentNullException>(
|
|
() => h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, null!));
|
|
|
|
continuationPoint.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_ForeignHandle_ThrowsBadNodeIdUnknown()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var foreignHandle = new NodeHandle { NodeId = ObjectIds.Server };
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(foreignHandle);
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
ServiceResultException ex = Assert.Throws<ServiceResultException>(
|
|
() => h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references))!;
|
|
|
|
Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
|
|
}
|
|
|
|
[Test]
|
|
public void Browse_UnknownView_ThrowsBadViewIdUnknown()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("BrowseView");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
object handle = h.Manager.GetManagerHandle(node.NodeId)!;
|
|
|
|
ContinuationPoint continuationPoint = h.NewContinuationPoint(handle);
|
|
continuationPoint.View = new ViewDescription { ViewId = new NodeId("NoView", h.NamespaceIndex) };
|
|
var references = new List<ReferenceDescription>();
|
|
|
|
ServiceResultException ex = Assert.Throws<ServiceResultException>(
|
|
() => h.Manager.Browse(h.NewContext(RequestType.Browse), ref continuationPoint, references))!;
|
|
|
|
Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadViewIdUnknown));
|
|
continuationPoint.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void TranslateBrowsePath_ResolvesTarget()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
(BaseObjectState parent, BaseObjectState child) = h.AddParentWithChild(ReferenceTypeIds.Organizes);
|
|
object handle = h.Manager.GetManagerHandle(parent.NodeId)!;
|
|
|
|
var relativePath = new RelativePathElement
|
|
{
|
|
IncludeSubtypes = true,
|
|
IsInverse = false,
|
|
TargetName = child.BrowseName
|
|
};
|
|
var targetIds = new List<ExpandedNodeId>();
|
|
var unresolved = new List<NodeId>();
|
|
|
|
h.Manager.TranslateBrowsePath(
|
|
h.NewContext(RequestType.TranslateBrowsePathsToNodeIds),
|
|
handle,
|
|
relativePath,
|
|
targetIds,
|
|
unresolved);
|
|
|
|
Assert.That(targetIds, Has.Count.EqualTo(1));
|
|
Assert.That(targetIds[0], Is.EqualTo(new ExpandedNodeId(child.NodeId)));
|
|
Assert.That(unresolved, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void TranslateBrowsePath_NoMatch_ReturnsEmptyTargets()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
(BaseObjectState parent, _) = h.AddParentWithChild(ReferenceTypeIds.Organizes);
|
|
object handle = h.Manager.GetManagerHandle(parent.NodeId)!;
|
|
|
|
var relativePath = new RelativePathElement
|
|
{
|
|
IncludeSubtypes = true,
|
|
IsInverse = false,
|
|
TargetName = new QualifiedName("DoesNotExist", h.NamespaceIndex)
|
|
};
|
|
var targetIds = new List<ExpandedNodeId>();
|
|
var unresolved = new List<NodeId>();
|
|
|
|
h.Manager.TranslateBrowsePath(
|
|
h.NewContext(RequestType.TranslateBrowsePathsToNodeIds),
|
|
handle,
|
|
relativePath,
|
|
targetIds,
|
|
unresolved);
|
|
|
|
Assert.That(targetIds, Is.Empty);
|
|
Assert.That(unresolved, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void TranslateBrowsePath_ForeignHandle_ReturnsEmptyTargets()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var foreignHandle = new NodeHandle { NodeId = ObjectIds.Server };
|
|
|
|
var relativePath = new RelativePathElement
|
|
{
|
|
TargetName = new QualifiedName("Anything", h.NamespaceIndex)
|
|
};
|
|
var targetIds = new List<ExpandedNodeId>();
|
|
var unresolved = new List<NodeId>();
|
|
|
|
h.Manager.TranslateBrowsePath(
|
|
h.NewContext(RequestType.TranslateBrowsePathsToNodeIds),
|
|
foreignHandle,
|
|
relativePath,
|
|
targetIds,
|
|
unresolved);
|
|
|
|
Assert.That(targetIds, Is.Empty);
|
|
Assert.That(unresolved, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void GetNodeMetadata_ReturnsMetadataForNode()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var variable = new BaseDataVariableState(null);
|
|
variable.CreateAsPredefinedNode(h.Context);
|
|
variable.NodeId = new NodeId("MetaVar", h.NamespaceIndex);
|
|
variable.BrowseName = new QualifiedName("MetaVar", h.NamespaceIndex);
|
|
variable.DisplayName = new LocalizedText("MetaVar");
|
|
variable.DataType = DataTypeIds.Int32;
|
|
variable.ValueRank = ValueRanks.Scalar;
|
|
variable.AccessLevel = AccessLevels.CurrentRead;
|
|
variable.UserAccessLevel = AccessLevels.CurrentRead;
|
|
variable.Value = 10;
|
|
h.Manager.AddPredefinedNodePublic(h.Context, variable);
|
|
object handle = h.Manager.GetManagerHandle(variable.NodeId)!;
|
|
|
|
NodeMetadata? metadata = h.Manager.GetNodeMetadata(
|
|
h.NewContext(RequestType.Read), handle, BrowseResultMask.All);
|
|
|
|
Assert.That(metadata, Is.Not.Null);
|
|
Assert.That(metadata!.NodeClass, Is.EqualTo(NodeClass.Variable));
|
|
Assert.That(metadata.DataType, Is.EqualTo(DataTypeIds.Int32));
|
|
Assert.That(metadata.ValueRank, Is.EqualTo(ValueRanks.Scalar));
|
|
Assert.That(metadata.AccessLevel, Is.EqualTo(AccessLevels.CurrentRead));
|
|
}
|
|
|
|
[Test]
|
|
public void GetNodeMetadata_ForeignHandle_ReturnsNull()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var foreignHandle = new NodeHandle { NodeId = ObjectIds.Server };
|
|
|
|
NodeMetadata? metadata = h.Manager.GetNodeMetadata(
|
|
h.NewContext(RequestType.Read), foreignHandle, BrowseResultMask.All);
|
|
|
|
Assert.That(metadata, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void GetNodeMetadata_UnvalidatedHandle_ReturnsNull()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
// In namespace, but not backed by a node in memory.
|
|
var placeholder = new NodeHandle { NodeId = new NodeId("Ghost", h.NamespaceIndex) };
|
|
|
|
NodeMetadata? metadata = h.Manager.GetNodeMetadata(
|
|
h.NewContext(RequestType.Read), placeholder, BrowseResultMask.All);
|
|
|
|
Assert.That(metadata, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void AddReferences_AddsExternalReferenceAndIsIdempotent()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("RefSource");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
var targetId = new NodeId("Target", 0);
|
|
|
|
var references = new Dictionary<NodeId, IList<IReference>>
|
|
{
|
|
[node.NodeId] =
|
|
[
|
|
new ReferenceNode
|
|
{
|
|
ReferenceTypeId = ReferenceTypeIds.HasComponent,
|
|
IsInverse = false,
|
|
TargetId = targetId
|
|
}
|
|
],
|
|
// Unknown source node is silently skipped.
|
|
[new NodeId("Missing", h.NamespaceIndex)] =
|
|
[
|
|
new ReferenceNode
|
|
{
|
|
ReferenceTypeId = ReferenceTypeIds.HasComponent,
|
|
IsInverse = false,
|
|
TargetId = targetId
|
|
}
|
|
]
|
|
};
|
|
|
|
h.Manager.AddReferences(references);
|
|
h.Manager.AddReferences(references);
|
|
|
|
var refs = new List<IReference>();
|
|
node.GetReferences(h.Context, refs);
|
|
List<IReference> matching = refs.FindAll(
|
|
r => r.TargetId == targetId && r.ReferenceTypeId == ReferenceTypeIds.HasComponent);
|
|
Assert.That(matching, Has.Count.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteReference_RemovesBidirectionalReference()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState source = h.NewObject("DelSource");
|
|
BaseObjectState target = h.NewObject("DelTarget");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, source);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, target);
|
|
source.AddReference(ReferenceTypeIds.Organizes, false, target.NodeId);
|
|
target.AddReference(ReferenceTypeIds.Organizes, true, source.NodeId);
|
|
object handle = h.Manager.GetManagerHandle(source.NodeId)!;
|
|
|
|
ServiceResult result = h.Manager.DeleteReference(
|
|
handle, ReferenceTypeIds.Organizes, false, target.NodeId, true);
|
|
|
|
Assert.That(ServiceResult.IsGood(result), Is.True);
|
|
Assert.That(source.ReferenceExists(ReferenceTypeIds.Organizes, false, target.NodeId), Is.False);
|
|
Assert.That(target.ReferenceExists(ReferenceTypeIds.Organizes, true, source.NodeId), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteReference_ForeignHandle_ReturnsBadNodeIdUnknown()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var foreignHandle = new NodeHandle { NodeId = ObjectIds.Server };
|
|
|
|
ServiceResult result = h.Manager.DeleteReference(
|
|
foreignHandle, ReferenceTypeIds.Organizes, false, ObjectIds.Server, false);
|
|
|
|
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadNodeIdUnknown));
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteReference_UnvalidatedHandle_ReturnsBadNotSupported()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
// In namespace, but not validated / no node in memory.
|
|
var placeholder = new NodeHandle { NodeId = new NodeId("Ghost", h.NamespaceIndex) };
|
|
|
|
ServiceResult result = h.Manager.DeleteReference(
|
|
placeholder, ReferenceTypeIds.Organizes, false, ObjectIds.Server, false);
|
|
|
|
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadNotSupported));
|
|
}
|
|
|
|
[Test]
|
|
public void CreateMonitoredItems_SkipsNodeNotOwnedByManager()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var itemToCreate = new MonitoredItemCreateRequest
|
|
{
|
|
ItemToMonitor = new ReadValueId { NodeId = ObjectIds.Server, AttributeId = Attributes.Value },
|
|
RequestedParameters = new MonitoringParameters { SamplingInterval = -1, QueueSize = 1 }
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
var filterErrors = new List<MonitoringFilterResult> { null! };
|
|
var monitoredItems = new List<IMonitoredItem> { null! };
|
|
var idFactory = new MonitoredItemIdFactory();
|
|
|
|
h.Manager.CreateMonitoredItems(
|
|
h.NewContext(RequestType.CreateMonitoredItems),
|
|
1, 100, TimestampsToReturn.Both,
|
|
new List<MonitoredItemCreateRequest> { itemToCreate },
|
|
errors, filterErrors, monitoredItems, false, idFactory);
|
|
|
|
Assert.That(itemToCreate.Processed, Is.False);
|
|
Assert.That(monitoredItems[0], Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void CreateMonitoredItems_ReturnsBadAttributeIdInvalidForInvalidAttribute()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
BaseObjectState node = h.NewObject("MonitorObject");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
var itemToCreate = new MonitoredItemCreateRequest
|
|
{
|
|
// Value is invalid for an Object node.
|
|
ItemToMonitor = new ReadValueId { NodeId = node.NodeId, AttributeId = Attributes.Value },
|
|
RequestedParameters = new MonitoringParameters { SamplingInterval = -1, QueueSize = 1 }
|
|
};
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
var filterErrors = new List<MonitoringFilterResult> { null! };
|
|
var monitoredItems = new List<IMonitoredItem> { null! };
|
|
var idFactory = new MonitoredItemIdFactory();
|
|
|
|
h.Manager.CreateMonitoredItems(
|
|
h.NewContext(RequestType.CreateMonitoredItems),
|
|
1, 100, TimestampsToReturn.Both,
|
|
new List<MonitoredItemCreateRequest> { itemToCreate },
|
|
errors, filterErrors, monitoredItems, false, idFactory);
|
|
|
|
Assert.That(itemToCreate.Processed, Is.True);
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadAttributeIdInvalid));
|
|
Assert.That(monitoredItems[0], Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void ModifyMonitoredItems_SkipsNullItem()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
var filterErrors = new List<MonitoringFilterResult> { null! };
|
|
var itemsToModify = new List<MonitoredItemModifyRequest> { new() };
|
|
var monitoredItems = new List<IMonitoredItem> { null! };
|
|
|
|
h.Manager.ModifyMonitoredItems(
|
|
h.NewContext(RequestType.ModifyMonitoredItems),
|
|
TimestampsToReturn.Both, monitoredItems, itemsToModify, errors, filterErrors);
|
|
|
|
Assert.That(itemsToModify[0].Processed, Is.False);
|
|
Assert.That(ServiceResult.IsGood(errors[0]), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ModifyMonitoredItems_SkipsForeignHandle()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var mockItem = new Mock<IMonitoredItem>();
|
|
mockItem.Setup(m => m.ManagerHandle).Returns(new NodeHandle { NodeId = new NodeId("x", 0) });
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
var filterErrors = new List<MonitoringFilterResult> { null! };
|
|
var itemsToModify = new List<MonitoredItemModifyRequest> { new() };
|
|
var monitoredItems = new List<IMonitoredItem> { mockItem.Object };
|
|
|
|
h.Manager.ModifyMonitoredItems(
|
|
h.NewContext(RequestType.ModifyMonitoredItems),
|
|
TimestampsToReturn.Both, monitoredItems, itemsToModify, errors, filterErrors);
|
|
|
|
Assert.That(itemsToModify[0].Processed, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteMonitoredItems_SkipsNullItem()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
var processed = new List<bool> { false };
|
|
var monitoredItems = new List<IMonitoredItem> { null! };
|
|
|
|
h.Manager.DeleteMonitoredItems(
|
|
h.NewContext(RequestType.DeleteMonitoredItems), monitoredItems, processed, errors);
|
|
|
|
Assert.That(processed[0], Is.False);
|
|
Assert.That(ServiceResult.IsGood(errors[0]), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void DeleteMonitoredItems_SkipsForeignHandle()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var mockItem = new Mock<IMonitoredItem>();
|
|
mockItem.Setup(m => m.ManagerHandle).Returns(new NodeHandle { NodeId = new NodeId("x", 0) });
|
|
var errors = new List<ServiceResult> { ServiceResult.Good };
|
|
var processed = new List<bool> { false };
|
|
var monitoredItems = new List<IMonitoredItem> { mockItem.Object };
|
|
|
|
h.Manager.DeleteMonitoredItems(
|
|
h.NewContext(RequestType.DeleteMonitoredItems), monitoredItems, processed, errors);
|
|
|
|
Assert.That(processed[0], Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void IsNodeInView_ReturnsFalseForNonHandle()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
|
|
bool result = h.Manager.IsNodeInView(
|
|
h.NewContext(RequestType.Browse), ObjectIds.ViewsFolder, new object());
|
|
|
|
Assert.That(result, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void IsNodeInView_ReturnsFalseWhenHandleNodeIsNull()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
var placeholder = new NodeHandle { NodeId = new NodeId("Ghost", h.NamespaceIndex) };
|
|
|
|
bool result = h.Manager.IsNodeInView(
|
|
h.NewContext(RequestType.Browse), ObjectIds.ViewsFolder, placeholder);
|
|
|
|
Assert.That(result, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void IsNodeInView_ValidatedHandleDelegatesToProtectedOverload()
|
|
{
|
|
// Regression test: the public IsNodeInView(OperationContext, NodeId, object)
|
|
// used to forward the OperationContext unchanged, so overload resolution bound
|
|
// back to the same public overload instead of the protected
|
|
// (ServerSystemContext, NodeId, NodeState) helper, and every call reported
|
|
// "not in view". It now copies the context and delegates to the overridable
|
|
// protected helper, which accepts any view that is a predefined node.
|
|
using Harness h = CreateHarness();
|
|
var view = new ViewState();
|
|
view.CreateAsPredefinedNode(h.Context);
|
|
view.NodeId = new NodeId("MyView", h.NamespaceIndex);
|
|
view.BrowseName = new QualifiedName("MyView", h.NamespaceIndex);
|
|
h.Manager.AddPredefinedNodePublic(h.Context, view);
|
|
BaseObjectState node = h.NewObject("ViewedNode");
|
|
h.Manager.AddPredefinedNodePublic(h.Context, node);
|
|
var handle = new NodeHandle(node.NodeId, node);
|
|
|
|
Assert.That(h.Manager.FindPredefinedNode<ViewState>(view.NodeId), Is.SameAs(view));
|
|
|
|
bool result = h.Manager.IsNodeInView(
|
|
h.NewContext(RequestType.Browse), view.NodeId, handle);
|
|
|
|
Assert.That(result, Is.True);
|
|
|
|
bool unknownView = h.Manager.IsNodeInView(
|
|
h.NewContext(RequestType.Browse), new NodeId("UnknownView", h.NamespaceIndex), handle);
|
|
|
|
Assert.That(unknownView, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void SessionActivated_WithNoMonitoredNodes_DoesNotThrow()
|
|
{
|
|
using Harness h = CreateHarness();
|
|
|
|
Assert.DoesNotThrow(
|
|
() => h.Manager.SessionActivated(
|
|
h.NewContext(RequestType.ActivateSession), new NodeId("Session", h.NamespaceIndex)));
|
|
}
|
|
|
|
private static Harness CreateHarness()
|
|
{
|
|
Mock<IServerInternal> mockServer = BuildMockServer(out MonitoredItemQueueFactory queueFactory);
|
|
var manager = new TestableCustomNodeManager2(
|
|
mockServer.Object, CreateConfiguration(), false, new Mock<ILogger>().Object, TestNamespaceUri);
|
|
return new Harness(manager, mockServer, queueFactory);
|
|
}
|
|
|
|
private static Harness CreatePlaceholderHarness(out PlaceholderNodeManager manager)
|
|
{
|
|
Mock<IServerInternal> mockServer = BuildMockServer(out MonitoredItemQueueFactory queueFactory);
|
|
manager = new PlaceholderNodeManager(
|
|
mockServer.Object, CreateConfiguration(), new Mock<ILogger>().Object, TestNamespaceUri);
|
|
return new Harness(manager, mockServer, queueFactory);
|
|
}
|
|
|
|
private static ApplicationConfiguration CreateConfiguration()
|
|
{
|
|
return new ApplicationConfiguration
|
|
{
|
|
ServerConfiguration = new ServerConfiguration
|
|
{
|
|
MaxNotificationQueueSize = 100,
|
|
MaxDurableNotificationQueueSize = 200
|
|
}
|
|
};
|
|
}
|
|
|
|
private static Mock<IServerInternal> BuildMockServer(out MonitoredItemQueueFactory queueFactory)
|
|
{
|
|
var mockServer = new Mock<IServerInternal>();
|
|
var mockMasterNodeManager = new Mock<IMasterNodeManager>();
|
|
var mockConfigurationNodeManager = new Mock<IConfigurationNodeManager>();
|
|
|
|
var namespaceTable = new NamespaceTable();
|
|
namespaceTable.Append(TestNamespaceUri);
|
|
|
|
mockServer.Setup(s => s.NamespaceUris).Returns(namespaceTable);
|
|
mockServer.Setup(s => s.ServerUris).Returns(new StringTable());
|
|
mockServer.Setup(s => s.TypeTree).Returns(new TypeTable(namespaceTable));
|
|
mockServer.Setup(s => s.Factory).Returns(EncodeableFactory.Create());
|
|
mockServer.Setup(s => s.NodeManager).Returns(mockMasterNodeManager.Object);
|
|
mockMasterNodeManager.Setup(m => m.ConfigurationNodeManager)
|
|
.Returns(mockConfigurationNodeManager.Object);
|
|
mockMasterNodeManager
|
|
.Setup(m => m.AsyncNodeManagers)
|
|
.Returns([]);
|
|
|
|
var mockTelemetry = new Mock<ITelemetryContext>();
|
|
mockServer.Setup(s => s.Telemetry).Returns(mockTelemetry.Object);
|
|
|
|
queueFactory = new MonitoredItemQueueFactory(mockTelemetry.Object);
|
|
mockServer.Setup(s => s.MonitoredItemQueueFactory).Returns(queueFactory);
|
|
|
|
var serverSystemContext = new ServerSystemContext(mockServer.Object);
|
|
mockServer.Setup(s => s.DefaultSystemContext).Returns(serverSystemContext);
|
|
|
|
return mockServer;
|
|
}
|
|
|
|
private sealed class Harness : IDisposable
|
|
{
|
|
private readonly MonitoredItemQueueFactory m_queueFactory;
|
|
|
|
public Harness(
|
|
TestableCustomNodeManager2 manager,
|
|
Mock<IServerInternal> mockServer,
|
|
MonitoredItemQueueFactory queueFactory)
|
|
{
|
|
Manager = manager;
|
|
MockServer = mockServer;
|
|
m_queueFactory = queueFactory;
|
|
}
|
|
|
|
public TestableCustomNodeManager2 Manager { get; }
|
|
|
|
public Mock<IServerInternal> MockServer { get; }
|
|
|
|
public ServerSystemContext Context => Manager.SystemContext;
|
|
|
|
public ushort NamespaceIndex => Manager.NamespaceIndex;
|
|
|
|
public OperationContext NewContext(RequestType requestType)
|
|
{
|
|
return new OperationContext(
|
|
new RequestHeader(), null, requestType, RequestLifetime.None);
|
|
}
|
|
|
|
public BaseObjectState NewObject(string name)
|
|
{
|
|
var node = new BaseObjectState(null);
|
|
node.CreateAsPredefinedNode(Context);
|
|
node.NodeId = new NodeId(name, NamespaceIndex);
|
|
node.BrowseName = new QualifiedName(name, NamespaceIndex);
|
|
node.DisplayName = new LocalizedText(name);
|
|
return node;
|
|
}
|
|
|
|
public BaseObjectTypeState NewObjectType(string name)
|
|
{
|
|
var type = new BaseObjectTypeState
|
|
{
|
|
SuperTypeId = NodeId.Null
|
|
};
|
|
type.CreateAsPredefinedNode(Context);
|
|
type.NodeId = new NodeId(name, NamespaceIndex);
|
|
type.BrowseName = new QualifiedName(name, NamespaceIndex);
|
|
type.DisplayName = new LocalizedText(name);
|
|
return type;
|
|
}
|
|
|
|
public BaseDataVariableState NewVariable(string name, int value)
|
|
{
|
|
var variable = new BaseDataVariableState(null);
|
|
variable.CreateAsPredefinedNode(Context);
|
|
variable.NodeId = new NodeId(name, NamespaceIndex);
|
|
variable.BrowseName = new QualifiedName(name, NamespaceIndex);
|
|
variable.DisplayName = new LocalizedText(name);
|
|
variable.DataType = DataTypeIds.Int32;
|
|
variable.ValueRank = ValueRanks.Scalar;
|
|
variable.AccessLevel = AccessLevels.CurrentRead;
|
|
variable.UserAccessLevel = AccessLevels.CurrentRead;
|
|
variable.Value = value;
|
|
return variable;
|
|
}
|
|
|
|
public BaseDataVariableState NewWritableVariable(string name, int value)
|
|
{
|
|
BaseDataVariableState variable = NewVariable(name, value);
|
|
variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
|
|
variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
|
|
return variable;
|
|
}
|
|
|
|
public (BaseObjectState Parent, BaseObjectState Child) AddParentWithChild(NodeId referenceTypeId)
|
|
{
|
|
BaseObjectState parent = NewObject("Parent");
|
|
BaseObjectState child = NewObject("Child");
|
|
Manager.AddPredefinedNodePublic(Context, parent);
|
|
Manager.AddPredefinedNodePublic(Context, child);
|
|
parent.AddReference(referenceTypeId, false, child.NodeId);
|
|
child.AddReference(referenceTypeId, true, parent.NodeId);
|
|
return (parent, child);
|
|
}
|
|
|
|
public ContinuationPoint NewContinuationPoint(object handle)
|
|
{
|
|
return new ContinuationPoint
|
|
{
|
|
NodeToBrowse = handle,
|
|
View = new ViewDescription(),
|
|
BrowseDirection = BrowseDirection.Forward,
|
|
IncludeSubtypes = true,
|
|
ResultMask = BrowseResultMask.All
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
m_queueFactory.Dispose();
|
|
Manager.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A node manager that returns an unvalidated placeholder handle for a
|
|
/// designated NodeId so the "node is not in memory" branches of Read and
|
|
/// Write (which the default GetManagerHandle never reaches) can be
|
|
/// exercised deterministically.
|
|
/// </summary>
|
|
private sealed class PlaceholderNodeManager : TestableCustomNodeManager2
|
|
{
|
|
public PlaceholderNodeManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
ILogger logger,
|
|
params string[] namespaceUris)
|
|
: base(server, configuration, false, logger, namespaceUris)
|
|
{
|
|
}
|
|
|
|
public NodeId PlaceholderNodeId { get; set; } = NodeId.Null;
|
|
|
|
protected override NodeHandle? GetManagerHandle(
|
|
ServerSystemContext context,
|
|
NodeId nodeId,
|
|
IDictionary<NodeId, NodeState> cache)
|
|
{
|
|
if (!PlaceholderNodeId.IsNull && nodeId == PlaceholderNodeId)
|
|
{
|
|
return new NodeHandle { NodeId = nodeId, RootId = nodeId, Validated = false };
|
|
}
|
|
|
|
return base.GetManagerHandle(context, nodeId, cache);
|
|
}
|
|
}
|
|
}
|
|
}
|