/* ======================================================================== * 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/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using NUnit.Framework; using Opc.Ua.Tests; namespace Opc.Ua.Core.Tests.Stack.State { /// /// Tests for the NodeState classes. /// [TestFixture] [Category("NodeStateTypes")] [SetCulture("en-us")] [SetUICulture("en-us")] [Parallelizable] public class StateTypesTests { public const string ApplicationUri = "uri:localhost:opcfoundation.org:NodeStates"; public const string OpcUa = "http://opcfoundation.org/UA/"; public const string OpcUaOnboarding = "http://opcfoundation.org/UA/Onboarding/"; public IServiceMessageContext Context; public ITelemetryContext Telemetry; [DatapointSource] public Type[] TypeArray = [.. typeof(BaseObjectState).Assembly.GetExportedTypes() .Where(IsNodeStateType)]; [OneTimeSetUp] protected void OneTimeSetUp() { Telemetry = NUnitTelemetryContext.Create(); Context = ServiceMessageContext.Create(Telemetry); NamespaceTable nameSpaceUris = Context.NamespaceUris; // namespace index 1 must be the ApplicationUri nameSpaceUris.GetIndexOrAppend(ApplicationUri); nameSpaceUris.GetIndexOrAppend(Namespaces.OpcUaGds); nameSpaceUris.GetIndexOrAppend(OpcUaOnboarding); } [OneTimeTearDown] protected void OneTimeTearDown() { (Context as IDisposable)?.Dispose(); } /// /// Verify independent ownership throughout copied alarm subtrees. /// [TestCase(false)] [TestCase(true)] public void CopiedAlarmChildrenHaveIndependentOwnership(bool useClone) { var context = new SystemContext(Telemetry) { NamespaceUris = Context.NamespaceUris }; var original = new BaseObjectState(null) { NodeId = new NodeId(1000), BrowseName = QualifiedName.From("Root") }; var alarm = new OffNormalAlarmState(original); alarm.Create(context, new NodeId(1001), QualifiedName.From("Alarm"), LocalizedText.From("Alarm"), false); original.AddChild(alarm); BaseObjectState copy; if (useClone) { copy = (BaseObjectState)original.Clone(); } else { copy = new BaseObjectState(null); copy.Create(context, original); } int count = AssertOwnedChildren(original, copy); Assert.That(count, Is.GreaterThan(30), "The inherited alarm subtree must be exercised."); int AssertOwnedChildren(NodeState source, NodeState target) { var children = new List(); var copiedChildren = new List(); source.GetChildren(context, children); target.GetChildren(context, copiedChildren); Assert.That(copiedChildren, Has.Count.EqualTo(children.Count)); int visited = children.Count; foreach (BaseInstanceState child in children) { BaseInstanceState copiedChild = target.FindChild(context, child.BrowseName)!; Assert.That(copiedChild, Is.Not.Null); Assert.Multiple(() => { Assert.That(copiedChild, Is.Not.SameAs(child), child.BrowseName.ToString()); Assert.That(copiedChild.Parent, Is.SameAs(target), child.BrowseName.ToString()); Assert.That(child.Parent, Is.SameAs(source), child.BrowseName.ToString()); Assert.That(copiedChild.NodeId, Is.EqualTo(child.NodeId)); }); visited += AssertOwnedChildren(child, copiedChild); LocalizedText originalName = child.DisplayName; copiedChild.DisplayName = LocalizedText.From("Changed copy"); Assert.That(child.DisplayName, Is.EqualTo(originalName)); } return visited; } } /// /// Verify activation of a NodeState type. /// [Theory] public void ActivateNodeStateType(Type systemType) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var testObject = CreateDefaultNodeStateType(systemType) as NodeState; Assert.That(testObject, Is.Not.Null); var context = new SystemContext(telemetry) { NamespaceUris = Context.NamespaceUris }; Assert.That(context.NamespaceUris.GetIndexOrAppend(OpcUa), Is.Zero); testObject.Create(context, new NodeId(1000), QualifiedName.From("Name"), LocalizedText.From("DisplayName"), true); } /// /// Instantiate NodeState types across Opc.Ua assemblies and fail on placeholder children. /// [Test] public void NodeStateTypesAcrossOpcUaAssemblies_ShouldNotInstantiatePlaceholderChildren() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var context = new SystemContext(telemetry) { NamespaceUris = Context.NamespaceUris }; var placeholders = new List(); uint nodeId = 200000; foreach (Type systemType in GetOpcUaNodeStateTypes().OrderBy(type => type.FullName)) { if (CreateDefaultNodeStateType(systemType) is not NodeState testObject) { continue; } testObject.Create(context, new NodeId(nodeId++), QualifiedName.From("Name"), LocalizedText.From("DisplayName"), true); CollectInstantiatedPlaceholders( context, testObject, systemType.Assembly.GetName().Name!, systemType.FullName!, placeholders); } Assert.That( placeholders, Is.Empty, "Instantiated placeholder children were found:" + Environment.NewLine + string.Join(Environment.NewLine, placeholders)); } /// /// Create an instance of a NodeState type with default values. /// /// The type to create private static object CreateDefaultNodeStateType(Type systemType) { System.Reflection.TypeInfo systemTypeInfo = systemType.GetTypeInfo(); object instance; try { if (typeof(BaseObjectState).GetTypeInfo().IsAssignableFrom(systemTypeInfo) || typeof(BaseVariableState).GetTypeInfo().IsAssignableFrom(systemTypeInfo) || typeof(MethodState).GetTypeInfo().IsAssignableFrom(systemTypeInfo)) { instance = Activator.CreateInstance(systemType, (NodeState)null!)!; } else if (systemType.IsAbstract) { instance = null!; } else { ConstructorInfo? defaultConstructor = systemType.GetConstructor([]); if (defaultConstructor == null || !defaultConstructor.IsPublic) { instance = null!; } else { instance = Activator.CreateInstance(systemType)!; } } } catch { return null!; } return instance!; } /// /// Return true if system Type is IEncodeable. /// private static bool IsNodeStateType(Type systemType) { if (systemType == null) { return false; } System.Reflection.TypeInfo systemTypeInfo = systemType.GetTypeInfo(); if (systemTypeInfo.IsAbstract || systemTypeInfo.IsGenericType || systemTypeInfo.IsGenericTypeDefinition || !typeof(NodeState).GetTypeInfo().IsAssignableFrom(systemTypeInfo)) { return false; } return CreateDefaultNodeStateType(systemType) is NodeState; } /// /// Recursively collect instantiated placeholder children for diagnostics. /// private static void CollectInstantiatedPlaceholders( ISystemContext context, NodeState nodeState, string ownerAssembly, string ownerType, List placeholders) { var children = new List(); nodeState.GetChildren(context, children); foreach (BaseInstanceState child in children) { string browseName = child.BrowseName.Name ?? string.Empty; bool hasPlaceholderName = browseName.Length > 1 && browseName[0] == '<' && browseName[^1] == '>'; bool hasPlaceholderModellingRule = child.ModellingRuleId == ObjectIds.ModellingRule_OptionalPlaceholder || child.ModellingRuleId == ObjectIds.ModellingRule_MandatoryPlaceholder; if (hasPlaceholderName || hasPlaceholderModellingRule) { string modellingRule = child.ModellingRuleId.ToString(); placeholders.Add( $"{ownerAssembly}: {ownerType}: {child.GetDisplayPath()} (BrowseName='{browseName}', ModellingRuleId='{modellingRule}')"); } CollectInstantiatedPlaceholders( context, child, ownerAssembly, ownerType, placeholders); } } /// /// Discover loadable public NodeState types from reachable Opc.Ua assemblies. /// private static IEnumerable GetOpcUaNodeStateTypes() { var assemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); var toScan = new Queue(); void TryEnqueueAssembly(Assembly assembly) { if (assembly == null || assembly.IsDynamic) { return; } AssemblyName assemblyName = assembly.GetName(); if (!assemblyName.Name!.StartsWith("Opc.Ua", StringComparison.Ordinal)) { return; } if (assemblies.ContainsKey(assembly.FullName!)) { return; } assemblies[assembly.FullName!] = assembly; toScan.Enqueue(assembly); } TryEnqueueAssembly(typeof(NodeState).Assembly); TryEnqueueAssembly(typeof(OrderedListState).Assembly); TryEnqueueAssembly(typeof(StateTypesTests).Assembly); foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { TryEnqueueAssembly(assembly); } while (toScan.Count > 0) { Assembly assembly = toScan.Dequeue(); foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) { if (!reference.Name!.StartsWith("Opc.Ua", StringComparison.Ordinal)) { continue; } try { TryEnqueueAssembly(Assembly.Load(reference)); } catch { // Nothing we can do if an assembly fails to load, just skip it. } } } return assemblies.Values .SelectMany(GetExportedTypesSafe) .Where(IsNodeStateType) .GroupBy(type => type.AssemblyQualifiedName) .Select(group => group.First()); } /// /// Return exported types while tolerating partial type-load failures. /// private static IEnumerable GetExportedTypesSafe(Assembly assembly) { try { return assembly.GetExportedTypes(); } catch (ReflectionTypeLoadException e) { // Continue with loadable public types if some types in the assembly fail to load. return e.Types.OfType().Where(type => type.IsPublic); } } } /// /// Tests for BaseEventState.MemberwiseClone. /// [TestFixture] [Category("BaseEventState")] [SetCulture("en-us")] [SetUICulture("en-us")] [Parallelizable] public class BaseEventStateTests { /// /// Verify that Clone works correctly for BaseEventState. /// [Test] public void CloneBaseEventStateSucceeds() { var parent = new BaseObjectState(null); var eventState = new BaseEventState(parent); var clone = (BaseEventState)eventState.Clone(); Assert.That(clone, Is.Not.Null); Assert.That(clone.Parent, Is.SameAs(parent)); } /// /// Verify that Clone works correctly for BaseEventState with null parent. /// [Test] public void CloneBaseEventStateWithNullParentSucceeds() { var eventState = new BaseEventState(null); var clone = (BaseEventState)eventState.Clone(); Assert.That(clone, Is.Not.Null); Assert.That(clone.Parent, Is.Null); } /// /// Verify that Clone works correctly for derived event types. /// [Test] public void CloneNonExclusiveLimitAlarmStateSucceeds() { var parent = new BaseObjectState(null); var alarmState = new NonExclusiveLimitAlarmState(parent); var clone = (NonExclusiveLimitAlarmState)alarmState.Clone(); Assert.That(clone, Is.Not.Null); Assert.That(clone.Parent, Is.SameAs(parent)); } [Test] public void BaseObjectStateConstructorSetsNodeClass() { var node = new BaseObjectState(null); Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Object)); } [Test] public void FolderStateConstructorSetsNodeClass() { var node = new FolderState(null); Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Object)); } [Test] public void BaseVariableStateConstructorSetsNodeClass() { var node = new BaseDataVariableState(null); Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Variable)); } [Test] public void MethodStateConstructorSetsNodeClass() { var node = new MethodState(null); Assert.That(node.NodeClass, Is.EqualTo(NodeClass.Method)); } [Test] public void NodeIdCanBeSetAndRetrieved() { var node = new BaseObjectState(null) { NodeId = new NodeId(1234, 2) }; Assert.That(node.NodeId, Is.EqualTo(new NodeId(1234, 2))); } [Test] public void BrowseNameCanBeSetAndRetrieved() { var node = new BaseObjectState(null) { BrowseName = new QualifiedName("TestObject", 2) }; Assert.That(node.BrowseName.Name, Is.EqualTo("TestObject")); Assert.That(node.BrowseName.NamespaceIndex, Is.EqualTo(2)); } [Test] public void DisplayNameCanBeSetAndRetrieved() { var node = new BaseObjectState(null) { DisplayName = new LocalizedText("en", "Test Display") }; Assert.That(node.DisplayName.Text, Is.EqualTo("Test Display")); } [Test] public void DescriptionCanBeSetAndRetrieved() { var node = new BaseObjectState(null) { Description = new LocalizedText("en", "A test node") }; Assert.That(node.Description.Text, Is.EqualTo("A test node")); } [Test] public void AddChildAddsToChildren() { var parent = new BaseObjectState(null) { NodeId = new NodeId(1, 0), BrowseName = new QualifiedName("Parent") }; var child = new BaseObjectState(parent) { NodeId = new NodeId(2, 0), BrowseName = new QualifiedName("Child") }; parent.AddChild(child); List children = []; parent.GetChildren(null!, children); Assert.That(children, Has.Count.GreaterThanOrEqualTo(1)); } [Test] public void AddReferenceCreatesReference() { var node = new BaseObjectState(null) { NodeId = new NodeId(100, 0), BrowseName = new QualifiedName("Source") }; node.AddReference( ReferenceTypeIds.Organizes, false, new NodeId(200, 0)); // Verify references exist List references = []; node.GetReferences(null!, references); Assert.That(references, Has.Count.GreaterThanOrEqualTo(1)); } [Test] public void AddMultipleReferences() { var node = new BaseObjectState(null) { NodeId = new NodeId(100, 0), BrowseName = new QualifiedName("Source") }; node.AddReference(ReferenceTypeIds.Organizes, false, new NodeId(200, 0)); node.AddReference(ReferenceTypeIds.HasComponent, false, new NodeId(201, 0)); List references = []; node.GetReferences(null!, references); Assert.That(references, Has.Count.GreaterThanOrEqualTo(2)); } [Test] public void MethodStateExecutableDefault() { var method = new MethodState(null); Assert.That(method.Executable, Is.True); } [Test] public void MethodStateUserExecutableDefault() { var method = new MethodState(null); Assert.That(method.UserExecutable, Is.True); } [Test] public void MethodStateExecutableCanBeSet() { var method = new MethodState(null) { Executable = false }; Assert.That(method.Executable, Is.False); } [Test] public void MethodStateUserExecutableCanBeSet() { var method = new MethodState(null) { UserExecutable = false }; Assert.That(method.UserExecutable, Is.False); } [Test] public void MethodStateConstructCreatesNewInstance() { var parent = new BaseObjectState(null); NodeState method = MethodState.Construct(parent); Assert.That(method, Is.Not.Null); Assert.That(method.NodeClass, Is.EqualTo(NodeClass.Method)); } [Test] public void MethodStateDeepEqualsIdentical() { var m1 = new MethodState(null) { NodeId = new NodeId(1), BrowseName = new QualifiedName("Method1"), Executable = true, UserExecutable = true }; var m2 = new MethodState(null) { NodeId = new NodeId(1), BrowseName = new QualifiedName("Method1"), Executable = true, UserExecutable = true }; Assert.That(m1.DeepEquals(m2), Is.True); } [Test] public void MethodStateDeepEqualsDifferent() { var m1 = new MethodState(null) { NodeId = new NodeId(1), BrowseName = new QualifiedName("Method1"), Executable = true }; var m2 = new MethodState(null) { NodeId = new NodeId(2), BrowseName = new QualifiedName("Method2"), Executable = false }; Assert.That(m1.DeepEquals(m2), Is.False); } [Test] public void MethodStateInputArgumentsDefault() { var method = new MethodState(null); Assert.That(method.InputArguments, Is.Null); } [Test] public void MethodStateDeepGetHashCodeReturnsValue() { var method = new MethodState(null) { NodeId = new NodeId(1), BrowseName = new QualifiedName("Test") }; int hash = method.DeepGetHashCode(); Assert.That(hash, Is.Not.Zero); } [Test] public void BaseDataVariableStateValueCanBeSet() { var variable = new BaseDataVariableState(null) { Value = 42 }; Assert.That(variable.Value, Is.EqualTo(42)); } [Test] public void BaseDataVariableStateDataTypeCanBeSet() { var variable = new BaseDataVariableState(null) { DataType = DataTypeIds.Int32 }; Assert.That(variable.DataType, Is.EqualTo(DataTypeIds.Int32)); } [Test] public void BaseDataVariableStateValueRankCanBeSet() { var variable = new BaseDataVariableState(null) { ValueRank = ValueRanks.OneDimension }; Assert.That(variable.ValueRank, Is.EqualTo(ValueRanks.OneDimension)); } [Test] public void BaseDataVariableStateAccessLevelCanBeSet() { var variable = new BaseDataVariableState(null) { AccessLevel = AccessLevels.CurrentRead }; Assert.That(variable.AccessLevel, Is.EqualTo(AccessLevels.CurrentRead)); } [Test] public void BaseDataVariableStateUserAccessLevelCanBeSet() { var variable = new BaseDataVariableState(null) { UserAccessLevel = AccessLevels.CurrentReadOrWrite }; Assert.That( variable.UserAccessLevel, Is.EqualTo(AccessLevels.CurrentReadOrWrite)); } [Test] public void BaseDataVariableStateHistorizingCanBeSet() { var variable = new BaseDataVariableState(null) { Historizing = true }; Assert.That(variable.Historizing, Is.True); } [Test] public void BaseDataVariableStateMinimumSamplingInterval() { var variable = new BaseDataVariableState(null) { MinimumSamplingInterval = 500.0 }; Assert.That(variable.MinimumSamplingInterval, Is.EqualTo(500.0)); } [Test] public void FolderStateAddChildVariable() { var folder = new FolderState(null) { NodeId = new NodeId(1, 0), BrowseName = new QualifiedName("MyFolder") }; var variable = new BaseDataVariableState(folder) { NodeId = new NodeId(2, 0), BrowseName = new QualifiedName("MyVar"), Value = "hello" }; folder.AddChild(variable); var children = new List(); folder.GetChildren(null!, children); Assert.That(children, Has.Count.GreaterThanOrEqualTo(1)); } [Test] public void NodeStateInitializedDefault() { var node = new BaseObjectState(null); Assert.That(node.Initialized, Is.False); } [Test] public void MethodStateMethodDeclarationIdCanBeSet() { var method = new MethodState(null) { MethodDeclarationId = new NodeId(999) }; Assert.That(method.MethodDeclarationId, Is.EqualTo(new NodeId(999))); } [Test] public void MethodStateOnCallMethodHandlerCanBeAssigned() { var method = new MethodState(null) { OnCallMethod = (context, methodState, inputArgs, outputArgs) => ServiceResult.Good }; Assert.That(method.OnCallMethod, Is.Not.Null); } [Test] public void MethodStateOnCallMethod2HandlerCanBeAssigned() { var method = new MethodState(null) { OnCallMethod2 = (context, methodToCall, objectId, inputArgs, outputArgs) => ServiceResult.Good }; Assert.That(method.OnCallMethod2, Is.Not.Null); } [Test] public void BaseDataVariableStateCopyPolicyDefault() { var variable = new BaseDataVariableState(null); Assert.That( variable.CopyPolicy, Is.EqualTo(VariableCopyPolicy.CopyOnRead)); } [Test] public void BaseDataVariableStateCopyPolicyCanBeSet() { var variable = new BaseDataVariableState(null) { CopyPolicy = VariableCopyPolicy.Never }; Assert.That(variable.CopyPolicy, Is.EqualTo(VariableCopyPolicy.Never)); } } }