mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 16:52:07 +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>
1431 lines
62 KiB
C#
1431 lines
62 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/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Export;
|
|
using Opc.Ua.Server.Nodes;
|
|
using Opc.Ua.Server.RuntimeNodeSet;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests.RuntimeNodeSet
|
|
{
|
|
[TestFixture]
|
|
[Category("RuntimeNodeSet")]
|
|
[Category("NodeSource")]
|
|
[Parallelizable]
|
|
public sealed class NodeSetImporterTests
|
|
{
|
|
private const string kNamespaceUri =
|
|
"urn:opcfoundation.org:Tests:TypedNodeSetImport";
|
|
private const string kExternalNamespaceUri =
|
|
"urn:opcfoundation.org:Tests:TypedNodeSetImport:External";
|
|
|
|
[Test]
|
|
public void FactoriesSelectTypedStatesByEachDiscriminator()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
context.NamespaceUris.Append(
|
|
"urn:opcfoundation.org:Tests:TypedNodeSetImport:Preexisting");
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAObjectType NodeId="ns=1;i=100" BrowseName="1:TypedObjectType" IsAbstract="true">
|
|
<DisplayName>TypedObjectType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=45" IsForward="false">i=58</Reference>
|
|
</References>
|
|
</UAObjectType>
|
|
<UAVariableType NodeId="ns=1;i=101" BrowseName="1:TypedVariableType"
|
|
DataType="i=11" ValueRank="1" ArrayDimensions="2">
|
|
<DisplayName>TypedVariableType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=45" IsForward="false">i=63</Reference>
|
|
</References>
|
|
</UAVariableType>
|
|
<UADataType NodeId="ns=1;i=102" BrowseName="1:TypedDataType" IsAbstract="true">
|
|
<DisplayName>TypedDataType</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=45" IsForward="false">i=24</Reference>
|
|
</References>
|
|
<Definition Name="TypedDataType">
|
|
<Field Name="Code" DataType="i=6" />
|
|
</Definition>
|
|
</UADataType>
|
|
<UAReferenceType NodeId="ns=1;i=103" BrowseName="1:TypedReferenceType"
|
|
IsAbstract="true" Symmetric="false">
|
|
<DisplayName>TypedReferenceType</DisplayName>
|
|
<InverseName>IsTypedBy</InverseName>
|
|
<References>
|
|
<Reference ReferenceType="i=45" IsForward="false">i=32</Reference>
|
|
</References>
|
|
</UAReferenceType>
|
|
<UAView NodeId="ns=1;i=104" BrowseName="1:TypedView"
|
|
ContainsNoLoops="true" EventNotifier="1">
|
|
<DisplayName>TypedView</DisplayName>
|
|
</UAView>
|
|
<UAObject NodeId="ns=1;i=200" BrowseName="1:TypedObject">
|
|
<DisplayName>TypedObject</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=100</Reference>
|
|
</References>
|
|
</UAObject>
|
|
<UAVariable NodeId="ns=1;i=201" BrowseName="1:TypedVariable"
|
|
DataType="i=11" ValueRank="-1">
|
|
<DisplayName>TypedVariable</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=101</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
<UAMethod NodeId="ns=1;i=202" BrowseName="1:TypedMethod"
|
|
MethodDeclarationId="ns=1;i=105">
|
|
<DisplayName>TypedMethod</DisplayName>
|
|
</UAMethod>
|
|
""");
|
|
var importer = new NodeSetImporter(
|
|
context,
|
|
new ManualFactoryProvider(
|
|
Factory(NodeClass.Object, 100, static () => new TypedObjectState(null!)),
|
|
Factory(NodeClass.Variable, 101, static () => new TypedVariableState(null!)),
|
|
Factory(NodeClass.Method, 105, static () => new TypedMethodState(null!)),
|
|
Factory(NodeClass.ObjectType, 100, static () => new TypedObjectTypeState()),
|
|
Factory(NodeClass.VariableType, 101, static () => new TypedVariableTypeState()),
|
|
Factory(NodeClass.DataType, 102, static () => new TypedDataTypeState()),
|
|
Factory(NodeClass.ReferenceType, 103, static () => new TypedReferenceTypeState()),
|
|
Factory(NodeClass.View, 104, static () => new TypedViewState())));
|
|
|
|
importer.Import(nodeSet);
|
|
importer.Complete();
|
|
|
|
var objectType = (TypedObjectTypeState)Find(importer, 100);
|
|
var variableType = (TypedVariableTypeState)Find(importer, 101);
|
|
var dataType = (TypedDataTypeState)Find(importer, 102);
|
|
var referenceType = (TypedReferenceTypeState)Find(importer, 103);
|
|
var view = (TypedViewState)Find(importer, 104);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(objectType, Is.TypeOf<TypedObjectTypeState>());
|
|
Assert.That(objectType.IsAbstract, Is.True);
|
|
Assert.That(variableType, Is.TypeOf<TypedVariableTypeState>());
|
|
Assert.That(variableType.DataType, Is.EqualTo(DataTypeIds.Double));
|
|
Assert.That(variableType.ValueRank, Is.EqualTo(ValueRanks.OneDimension));
|
|
Assert.That(variableType.ArrayDimensions, Is.EqualTo(new uint[] { 2 }));
|
|
Assert.That(dataType, Is.TypeOf<TypedDataTypeState>());
|
|
Assert.That(
|
|
dataType.DataTypeDefinition.TryGetValue(
|
|
out DataTypeDefinition? definition),
|
|
Is.True);
|
|
Assert.That(definition, Is.Not.Null);
|
|
Assert.That(referenceType, Is.TypeOf<TypedReferenceTypeState>());
|
|
Assert.That(referenceType.IsAbstract, Is.True);
|
|
Assert.That(referenceType.InverseName.Text, Is.EqualTo("IsTypedBy"));
|
|
Assert.That(view, Is.TypeOf<TypedViewState>());
|
|
Assert.That(view.ContainsNoLoops, Is.True);
|
|
Assert.That(view.EventNotifier, Is.EqualTo((byte)1));
|
|
Assert.That(Find(importer, 200), Is.TypeOf<TypedObjectState>());
|
|
Assert.That(Find(importer, 201), Is.TypeOf<TypedVariableState>());
|
|
Assert.That(Find(importer, 202), Is.TypeOf<TypedMethodState>());
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void UnknownDiscriminatorsUseExactGenericFallbacks()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAObjectType NodeId="ns=1;i=100" BrowseName="1:GenericObjectType">
|
|
<DisplayName>GenericObjectType</DisplayName>
|
|
</UAObjectType>
|
|
<UAObject NodeId="ns=1;i=200" BrowseName="1:GenericObject">
|
|
<DisplayName>GenericObject</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=100</Reference>
|
|
</References>
|
|
</UAObject>
|
|
<UAVariable NodeId="ns=1;i=201" BrowseName="1:GenericProperty"
|
|
DataType="i=12">
|
|
<DisplayName>GenericProperty</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
<UAMethod NodeId="ns=1;i=202" BrowseName="1:GenericMethod"
|
|
MethodDeclarationId="ns=1;i=105">
|
|
<DisplayName>GenericMethod</DisplayName>
|
|
</UAMethod>
|
|
""");
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
|
|
importer.Import(nodeSet);
|
|
importer.Complete();
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(Find(importer, 100), Is.TypeOf<BaseObjectTypeState>());
|
|
Assert.That(Find(importer, 200), Is.TypeOf<BaseObjectState>());
|
|
Assert.That(Find(importer, 201), Is.TypeOf<PropertyState>());
|
|
Assert.That(Find(importer, 202), Is.TypeOf<MethodState>());
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void TypedImportMatchesGenericFieldsValuesAndReferences()
|
|
{
|
|
const string nodes =
|
|
"""
|
|
<UAObjectType NodeId="ns=1;i=100" BrowseName="1:TypedObjectType">
|
|
<DisplayName>TypedObjectType</DisplayName>
|
|
</UAObjectType>
|
|
<UAVariableType NodeId="ns=1;i=101" BrowseName="1:TypedVariableType"
|
|
DataType="i=11">
|
|
<DisplayName>TypedVariableType</DisplayName>
|
|
</UAVariableType>
|
|
<UAObject NodeId="ns=1;i=200" BrowseName="1:TypedObject"
|
|
SymbolicName="TypedObjectSymbol" EventNotifier="1"
|
|
WriteMask="2097151" UserWriteMask="31"
|
|
AccessRestrictions="3" ReleaseStatus="Released"
|
|
DesignToolOnly="true">
|
|
<DisplayName Locale="en-US">Typed object</DisplayName>
|
|
<Description Locale="en-US">Typed object description</Description>
|
|
<Category>First</Category>
|
|
<Category>Second</Category>
|
|
<Documentation>https://example.test/object</Documentation>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=100</Reference>
|
|
<Reference ReferenceType="i=37">i=78</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=2;i=900</Reference>
|
|
</References>
|
|
<RolePermissions>
|
|
<RolePermission Permissions="63">i=15644</RolePermission>
|
|
</RolePermissions>
|
|
<Extensions>
|
|
<Extension>
|
|
<test:Marker xmlns:test="urn:test:extension">object</test:Marker>
|
|
</Extension>
|
|
</Extensions>
|
|
</UAObject>
|
|
<UAVariable NodeId="ns=1;i=201" BrowseName="1:TypedVariable"
|
|
SymbolicName="TypedVariableSymbol" DataType="i=11"
|
|
ValueRank="1" ArrayDimensions="2" AccessLevel="3"
|
|
UserAccessLevel="1" MinimumSamplingInterval="12.5"
|
|
Historizing="true" AccessRestrictions="1"
|
|
ReleaseStatus="Draft" DesignToolOnly="true">
|
|
<DisplayName Locale="en-US">Typed variable</DisplayName>
|
|
<Description Locale="en-US">Typed variable description</Description>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=101</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=200</Reference>
|
|
</References>
|
|
<RolePermissions>
|
|
<RolePermission Permissions="3">i=15644</RolePermission>
|
|
</RolePermissions>
|
|
<Value>
|
|
<uax:ListOfDouble>
|
|
<uax:Double>1.5</uax:Double>
|
|
<uax:Double>2.5</uax:Double>
|
|
</uax:ListOfDouble>
|
|
</Value>
|
|
</UAVariable>
|
|
<UAMethod NodeId="ns=1;i=202" BrowseName="1:TypedMethod"
|
|
SymbolicName="TypedMethodSymbol" Executable="true"
|
|
UserExecutable="false" MethodDeclarationId="ns=1;i=105"
|
|
ReleaseStatus="Deprecated" DesignToolOnly="true">
|
|
<DisplayName Locale="en-US">Typed method</DisplayName>
|
|
<Description Locale="en-US">Typed method description</Description>
|
|
<References>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=200</Reference>
|
|
</References>
|
|
</UAMethod>
|
|
""";
|
|
UANodeSet genericNodeSet = ReadNodeSet(nodes);
|
|
UANodeSet typedNodeSet = ReadNodeSet(nodes);
|
|
SystemContext genericContext = CreateContext();
|
|
SystemContext typedContext = CreateContext();
|
|
var genericNodes = new NodeStateCollection();
|
|
genericNodeSet.Import(genericContext, genericNodes, linkParentChild: true);
|
|
var typedImporter = new NodeSetImporter(
|
|
typedContext,
|
|
new ManualFactoryProvider(
|
|
Factory(NodeClass.Object, 100, static () => new TypedObjectState(null!)),
|
|
Factory(NodeClass.Variable, 101, static () => new TypedVariableState(null!)),
|
|
Factory(NodeClass.Method, 105, static () => new TypedMethodState(null!))));
|
|
|
|
typedImporter.Import(typedNodeSet);
|
|
typedImporter.Complete();
|
|
|
|
AssertNodeParity(
|
|
genericContext,
|
|
genericNodes.Single(node => HasNumericIdentifier(node, 200)),
|
|
typedContext,
|
|
Find(typedImporter, 200));
|
|
AssertNodeParity(
|
|
genericContext,
|
|
genericNodes.Single(node => HasNumericIdentifier(node, 201)),
|
|
typedContext,
|
|
Find(typedImporter, 201));
|
|
AssertNodeParity(
|
|
genericContext,
|
|
genericNodes.Single(node => HasNumericIdentifier(node, 202)),
|
|
typedContext,
|
|
Find(typedImporter, 202));
|
|
Assert.That(
|
|
((MethodState)Find(typedImporter, 202)).UserExecutable,
|
|
Is.False);
|
|
|
|
var genericVariable = (BaseVariableState)genericNodes.Single(
|
|
node => HasNumericIdentifier(node, 201));
|
|
var typedVariable = (BaseVariableState)Find(typedImporter, 201);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(typedVariable.DataType, Is.EqualTo(genericVariable.DataType));
|
|
Assert.That(typedVariable.ValueRank, Is.EqualTo(genericVariable.ValueRank));
|
|
Assert.That(
|
|
typedVariable.ArrayDimensions,
|
|
Is.EqualTo(genericVariable.ArrayDimensions));
|
|
Assert.That(typedVariable.AccessLevelEx, Is.EqualTo(genericVariable.AccessLevelEx));
|
|
Assert.That(
|
|
typedVariable.UserAccessLevel,
|
|
Is.EqualTo(genericVariable.UserAccessLevel));
|
|
Assert.That(
|
|
typedVariable.MinimumSamplingInterval,
|
|
Is.EqualTo(genericVariable.MinimumSamplingInterval));
|
|
Assert.That(typedVariable.Historizing, Is.EqualTo(genericVariable.Historizing));
|
|
Assert.That(typedVariable.Value, Is.EqualTo(genericVariable.Value));
|
|
Assert.That(typedVariable.StatusCode, Is.EqualTo(genericVariable.StatusCode));
|
|
Assert.That(typedVariable.Timestamp, Is.EqualTo(genericVariable.Timestamp));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void CompleteLinksAcrossDocumentsOnceAndPreservesUnresolvedParent()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=2" BrowseName="1:Child"
|
|
ParentNodeId="ns=1;i=1">
|
|
<DisplayName>Child</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=58</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAObject>
|
|
<UAObject NodeId="ns=1;i=3" BrowseName="1:ExternalChild"
|
|
ParentNodeId="ns=2;i=9">
|
|
<DisplayName>ExternalChild</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=58</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=2;i=9</Reference>
|
|
</References>
|
|
</UAObject>
|
|
""",
|
|
includeExternalNamespace: true);
|
|
UANodeSet parent = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=1" BrowseName="1:Parent">
|
|
<DisplayName>Parent</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=58</Reference>
|
|
</References>
|
|
</UAObject>
|
|
""");
|
|
|
|
importer.Import(children);
|
|
importer.Import(parent);
|
|
importer.Complete();
|
|
importer.Complete();
|
|
|
|
NodeState parentState = Find(importer, 1);
|
|
var childState = (BaseInstanceState)Find(importer, 2);
|
|
var externalChild = (BaseInstanceState)Find(importer, 3);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parentState.GetChildren(context, linkedChildren);
|
|
var externalNamespaceIndex =
|
|
(ushort)context.NamespaceUris.GetIndex(kExternalNamespaceUri);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(childState.Parent, Is.SameAs(parentState));
|
|
Assert.That(
|
|
childState.ReferenceTypeId,
|
|
Is.EqualTo(ReferenceTypeIds.HasComponent));
|
|
Assert.That(linkedChildren, Is.EqualTo(new[] { childState }));
|
|
Assert.That(
|
|
UANodeSet.TryGetUnresolvedParentNodeId(childState, out _),
|
|
Is.False);
|
|
Assert.That(externalChild.Parent, Is.Null);
|
|
Assert.That(
|
|
UANodeSet.TryGetUnresolvedParentNodeId(
|
|
externalChild,
|
|
out NodeId unresolvedParent),
|
|
Is.True);
|
|
Assert.That(
|
|
unresolvedParent,
|
|
Is.EqualTo(new NodeId(9u, externalNamespaceIndex)));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void CompleteLinksImportedChildToAdditionalAuthoredParent()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=2" BrowseName="1:Child"
|
|
ParentNodeId="ns=1;i=1">
|
|
<DisplayName>Child</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=58</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAObject>
|
|
""");
|
|
var parent = new BaseObjectState(null)
|
|
{
|
|
NodeId = new NodeId(1u, 1),
|
|
BrowseName = new QualifiedName("Parent", 1),
|
|
DisplayName = new LocalizedText("Parent")
|
|
};
|
|
|
|
importer.Import(children);
|
|
importer.Complete(
|
|
new Dictionary<NodeId, NodeState>
|
|
{
|
|
[parent.NodeId] = parent
|
|
});
|
|
|
|
var child = (BaseInstanceState)Find(importer, 2);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, linkedChildren);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(child.Parent, Is.SameAs(parent));
|
|
Assert.That(linkedChildren, Is.EqualTo(new[] { child }));
|
|
Assert.That(
|
|
UANodeSet.TryGetUnresolvedParentNodeId(child, out _),
|
|
Is.False);
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void TypedImportedChildDoesNotEvictOrdinaryAuthoredChild()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var factoryProvider = new ManualFactoryProvider(
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(500u, kNamespaceUri),
|
|
static () => new TypedVariableState(null!)));
|
|
var importer = new NodeSetImporter(context, factoryProvider);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAVariable NodeId="ns=1;i=2" BrowseName="1:Value"
|
|
ParentNodeId="ns=1;i=1" DataType="i=6">
|
|
<DisplayName>Value</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=500</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var parent = new BaseObjectState(null)
|
|
{
|
|
NodeId = new NodeId(1u, 1),
|
|
BrowseName = new QualifiedName("Parent", 1),
|
|
DisplayName = new LocalizedText("Parent")
|
|
};
|
|
var existing = new BaseDataVariableState(parent)
|
|
{
|
|
NodeId = new NodeId(9u, 1),
|
|
BrowseName = new QualifiedName("Value", 1),
|
|
DisplayName = new LocalizedText("Value"),
|
|
DataType = DataTypeIds.Int32
|
|
};
|
|
parent.AddChild(existing);
|
|
|
|
importer.Import(children);
|
|
importer.Complete(
|
|
new Dictionary<NodeId, NodeState>
|
|
{
|
|
[parent.NodeId] = parent
|
|
});
|
|
|
|
var imported = (BaseInstanceState)Find(importer, 2);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, linkedChildren);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(existing.Parent, Is.SameAs(parent));
|
|
Assert.That(imported.Parent, Is.SameAs(parent));
|
|
Assert.That(linkedChildren, Has.Member(existing));
|
|
Assert.That(linkedChildren, Has.Member(imported));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void TypedImportedMethodArgumentReplacesInheritedExplicitSlot()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var factoryProvider = new ManualFactoryProvider(
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(3u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId));
|
|
var importer = new NodeSetImporter(context, factoryProvider);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAVariable NodeId="ns=1;i=3" BrowseName="InputArguments"
|
|
ParentNodeId="ns=1;i=1" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>InputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var parent = new MethodState(null)
|
|
{
|
|
NodeId = new NodeId(1u, 1),
|
|
BrowseName = new QualifiedName("Method", 1),
|
|
DisplayName = new LocalizedText("Method")
|
|
};
|
|
PropertyState<ArrayOf<Argument>> existing =
|
|
parent.CreateOrReplaceInputArguments(
|
|
context,
|
|
replacement: null,
|
|
assignInstanceNodeIds: false);
|
|
existing.NodeId = new NodeId(9u, 1);
|
|
|
|
importer.Import(children);
|
|
importer.Complete(
|
|
new Dictionary<NodeId, NodeState>
|
|
{
|
|
[parent.NodeId] = parent
|
|
},
|
|
_ => true);
|
|
|
|
var imported =
|
|
(PropertyState<ArrayOf<Argument>>)Find(importer, 3);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, linkedChildren);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(parent.InputArguments, Is.SameAs(imported));
|
|
Assert.That(existing.Parent, Is.Null);
|
|
Assert.That(
|
|
linkedChildren.Count(child =>
|
|
child.BrowseName.Name == BrowseNames.InputArguments),
|
|
Is.EqualTo(1));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void AdditionalTypedMethodArgumentsDoNotDisplaceFirstImportedSlot()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var factoryProvider = new ManualFactoryProvider(
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(3u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId),
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(4u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId));
|
|
var importer = new NodeSetImporter(context, factoryProvider);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAVariable NodeId="ns=1;i=3" BrowseName="InputArguments"
|
|
ParentNodeId="ns=1;i=1" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>InputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
<UAVariable NodeId="ns=1;i=4" BrowseName="1:InputArguments"
|
|
ParentNodeId="ns=1;i=1" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>InputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var parent = new MethodState(null)
|
|
{
|
|
NodeId = new NodeId(1u, 1),
|
|
BrowseName = new QualifiedName("Method", 1),
|
|
DisplayName = new LocalizedText("Method")
|
|
};
|
|
PropertyState<ArrayOf<Argument>> placeholder =
|
|
parent.CreateOrReplaceInputArguments(
|
|
context,
|
|
replacement: null,
|
|
assignInstanceNodeIds: false);
|
|
placeholder.NodeId = new NodeId(9u, 1);
|
|
|
|
importer.Import(children);
|
|
importer.Complete(
|
|
new Dictionary<NodeId, NodeState>
|
|
{
|
|
[parent.NodeId] = parent
|
|
},
|
|
_ => true);
|
|
|
|
var first = (BaseInstanceState)Find(importer, 3);
|
|
var second = (BaseInstanceState)Find(importer, 4);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, linkedChildren);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(parent.InputArguments, Is.SameAs(first));
|
|
Assert.That(first.Parent, Is.SameAs(parent));
|
|
Assert.That(second.Parent, Is.SameAs(parent));
|
|
Assert.That(second.BrowseName.NamespaceIndex, Is.EqualTo((ushort)1));
|
|
Assert.That(
|
|
linkedChildren.Count(child =>
|
|
child.BrowseName.Name == BrowseNames.InputArguments),
|
|
Is.EqualTo(2));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void NamespaceMismatchedChildFillsAnEmptyInheritedSlot()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var factoryProvider = new ManualFactoryProvider(
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(3u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId));
|
|
var importer = new NodeSetImporter(context, factoryProvider);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAVariable NodeId="ns=1;i=3" BrowseName="1:InputArguments"
|
|
ParentNodeId="ns=1;i=1" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>InputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var parent = new MethodState(null)
|
|
{
|
|
NodeId = new NodeId(1u, 1),
|
|
BrowseName = new QualifiedName("Method", 1),
|
|
DisplayName = new LocalizedText("Method")
|
|
};
|
|
|
|
importer.Import(children);
|
|
importer.Complete(
|
|
new Dictionary<NodeId, NodeState>
|
|
{
|
|
[parent.NodeId] = parent
|
|
},
|
|
_ => true);
|
|
|
|
var imported = (BaseInstanceState)Find(importer, 3);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, linkedChildren);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// A state resolves an explicitly defined child by browse name
|
|
// alone (see NodeInstanceExtensionsTests.FindChildReturnsThe-
|
|
// RequestedArgumentsProperty), so an imported child of the
|
|
// matching type fills the empty slot even when the document
|
|
// declared it in another namespace. The address space still
|
|
// exposes the browse name the document declared.
|
|
Assert.That(parent.InputArguments, Is.SameAs(imported));
|
|
Assert.That(
|
|
imported.BrowseName,
|
|
Is.EqualTo(new QualifiedName("InputArguments", 1)));
|
|
Assert.That(imported.Parent, Is.SameAs(parent));
|
|
Assert.That(linkedChildren, Is.EqualTo(new[] { imported }));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void NamespaceMismatchedChildDoesNotEvictAPopulatedInheritedSlot()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var factoryProvider = new ManualFactoryProvider(
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(3u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId));
|
|
var importer = new NodeSetImporter(context, factoryProvider);
|
|
UANodeSet children = ReadNodeSet(
|
|
"""
|
|
<UAVariable NodeId="ns=1;i=3" BrowseName="1:InputArguments"
|
|
ParentNodeId="ns=1;i=1" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>InputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var parent = new MethodState(null)
|
|
{
|
|
NodeId = new NodeId(1u, 1),
|
|
BrowseName = new QualifiedName("Method", 1),
|
|
DisplayName = new LocalizedText("Method")
|
|
};
|
|
PropertyState<ArrayOf<Argument>> declared =
|
|
parent.CreateOrReplaceInputArguments(context, null);
|
|
|
|
importer.Import(children);
|
|
importer.Complete(
|
|
new Dictionary<NodeId, NodeState>
|
|
{
|
|
[parent.NodeId] = parent
|
|
},
|
|
_ => true);
|
|
|
|
var imported = (BaseInstanceState)Find(importer, 3);
|
|
var linkedChildren = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, linkedChildren);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// The slot already holds the standard property, so a child
|
|
// declared in another namespace is attached as an ordinary
|
|
// child instead of displacing it.
|
|
Assert.That(parent.InputArguments, Is.SameAs(declared));
|
|
Assert.That(imported.Parent, Is.SameAs(parent));
|
|
Assert.That(linkedChildren, Does.Contain(imported));
|
|
});
|
|
}
|
|
|
|
[TestCase(false, false)]
|
|
[TestCase(false, true)]
|
|
[TestCase(true, false)]
|
|
[TestCase(true, true)]
|
|
public void ReferenceOnlyArgumentsBindToAvailableMethodWithoutConsumingHandle(
|
|
bool inverseReference,
|
|
bool namespaceUriTarget)
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
string target = namespaceUriTarget
|
|
? $"nsu={kNamespaceUri};i=1"
|
|
: "ns=1;i=1";
|
|
string inverse = inverseReference
|
|
? $"<Reference ReferenceType=\"i=46\" IsForward=\"false\">{target}</Reference>"
|
|
: string.Empty;
|
|
UANodeSet children = ReadNodeSet(
|
|
$"""
|
|
<UAVariable NodeId="ns=1;i=3" BrowseName="InputArguments" DataType="i=296" ValueRank="1">
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
{inverse}
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
importer.Import(children);
|
|
var child = (BaseInstanceState)Find(importer, 3);
|
|
var parent = new MethodState(null)
|
|
{
|
|
NodeId = new NodeId(1u, child.NodeId.NamespaceIndex),
|
|
BrowseName = new QualifiedName("Method", child.NodeId.NamespaceIndex)
|
|
};
|
|
if (!inverseReference)
|
|
{
|
|
parent.AddReference(
|
|
ReferenceTypeIds.HasProperty,
|
|
false,
|
|
namespaceUriTarget
|
|
? new ExpandedNodeId(3u, kNamespaceUri)
|
|
: child.NodeId);
|
|
}
|
|
var applicationHandle = new NodeId(999u, child.NodeId.NamespaceIndex);
|
|
child.Handle = applicationHandle;
|
|
|
|
importer.Complete(new Dictionary<NodeId, NodeState> { [parent.NodeId] = parent });
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(parent.InputArguments, Is.SameAs(child));
|
|
Assert.That(child.Parent, Is.SameAs(parent));
|
|
Assert.That(child.Handle, Is.EqualTo(applicationHandle));
|
|
Assert.That(child.ReferenceTypeId, Is.EqualTo(ReferenceTypeIds.HasProperty));
|
|
Assert.That(UANodeSet.TryGetUnresolvedParentNodeId(child, out _), Is.False);
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void DeferredLinkingPreservesApplicationHandle()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=1" BrowseName="1:Parent">
|
|
<DisplayName>Parent</DisplayName>
|
|
</UAObject>
|
|
<UAObject NodeId="ns=1;i=2" BrowseName="1:Child"
|
|
ParentNodeId="ns=1;i=1">
|
|
<DisplayName>Child</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAObject>
|
|
""");
|
|
|
|
importer.Import(nodeSet);
|
|
var child = (BaseInstanceState)Find(importer, 2);
|
|
var applicationHandle = new object();
|
|
child.Handle = applicationHandle;
|
|
importer.Complete();
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(child.Parent, Is.SameAs(Find(importer, 1)));
|
|
Assert.That(child.Handle, Is.SameAs(applicationHandle));
|
|
Assert.That(
|
|
child.ReferenceTypeId,
|
|
Is.EqualTo(ReferenceTypeIds.HasComponent));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void SameBatchCustomHierarchyRetainsItsReferenceType()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAReferenceType NodeId="ns=1;i=500" BrowseName="1:RelatedToParent">
|
|
<DisplayName>RelatedToParent</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=45" IsForward="false">i=32</Reference>
|
|
</References>
|
|
</UAReferenceType>
|
|
<UAReferenceType NodeId="ns=1;i=501" BrowseName="1:HasTypedChild">
|
|
<DisplayName>HasTypedChild</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=45" IsForward="false">i=33</Reference>
|
|
</References>
|
|
</UAReferenceType>
|
|
<UAObject NodeId="ns=1;i=1" BrowseName="1:Parent">
|
|
<DisplayName>Parent</DisplayName>
|
|
</UAObject>
|
|
<UAObject NodeId="ns=1;i=2" BrowseName="1:Child"
|
|
ParentNodeId="ns=1;i=1">
|
|
<DisplayName>Child</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="ns=1;i=500" IsForward="false">ns=1;i=1</Reference>
|
|
<Reference ReferenceType="ns=1;i=501" IsForward="false">ns=1;i=1</Reference>
|
|
</References>
|
|
</UAObject>
|
|
""");
|
|
|
|
importer.Import(nodeSet);
|
|
importer.Complete();
|
|
|
|
var child = (BaseInstanceState)Find(importer, 2);
|
|
Assert.That(child.ReferenceTypeId, Is.EqualTo(new NodeId(501u, 1)));
|
|
}
|
|
|
|
[Test]
|
|
public void TypedGeneratedLikeStateUsesOnlyFlatImportedChildren()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=200" BrowseName="1:GeneratedLike">
|
|
<DisplayName>GeneratedLike</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=100</Reference>
|
|
<Reference ReferenceType="i=47">ns=1;i=201</Reference>
|
|
</References>
|
|
</UAObject>
|
|
<UAVariable NodeId="ns=1;i=201" BrowseName="1:MandatoryValue"
|
|
ParentNodeId="ns=1;i=200" DataType="i=6">
|
|
<DisplayName>MandatoryValue</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=63</Reference>
|
|
<Reference ReferenceType="i=47" IsForward="false">ns=1;i=200</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var importer = new NodeSetImporter(
|
|
context,
|
|
new ManualFactoryProvider(
|
|
Factory(
|
|
NodeClass.Object,
|
|
100,
|
|
static () => new GeneratedLikeObjectState(null!)),
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(201u, kNamespaceUri),
|
|
static () => new TypedVariableState(null!),
|
|
NodeSetImportDiscriminator.NodeId)));
|
|
|
|
importer.Import(nodeSet);
|
|
importer.Complete();
|
|
var parent = (GeneratedLikeObjectState)Find(importer, 200);
|
|
NodeState importedChild = Find(importer, 201);
|
|
parent.CreateAsPredefinedNode(context);
|
|
var children = new List<BaseInstanceState>();
|
|
parent.GetChildren(context, children);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(parent.InitializeCount, Is.Zero);
|
|
Assert.That(parent.MandatoryValue, Is.SameAs(importedChild));
|
|
Assert.That(children, Is.EqualTo(new[] { importedChild }));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task ExactChildFactoriesPopulateTypedMethodArgumentsAsync()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAMethod NodeId="ns=1;i=202" BrowseName="1:Calculate"
|
|
MethodDeclarationId="ns=1;i=105">
|
|
<DisplayName>Calculate</DisplayName>
|
|
</UAMethod>
|
|
<UAVariable NodeId="ns=1;i=203" BrowseName="InputArguments"
|
|
ParentNodeId="ns=1;i=202" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>InputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=202</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
<UAVariable NodeId="ns=1;i=204" BrowseName="OutputArguments"
|
|
ParentNodeId="ns=1;i=202" DataType="i=296"
|
|
ValueRank="1">
|
|
<DisplayName>OutputArguments</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">i=68</Reference>
|
|
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=202</Reference>
|
|
</References>
|
|
</UAVariable>
|
|
""");
|
|
var importer = new NodeSetImporter(
|
|
context,
|
|
new ManualFactoryProvider(
|
|
Factory(
|
|
NodeClass.Method,
|
|
105,
|
|
static () => new TypedMethodState(null!)),
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(203u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId),
|
|
new ManualImportFactory(
|
|
NodeClass.Variable,
|
|
new ExpandedNodeId(204u, kNamespaceUri),
|
|
static () =>
|
|
PropertyState<ArrayOf<Argument>>
|
|
.With<StructureBuilder<Argument>>(null!),
|
|
NodeSetImportDiscriminator.NodeId)));
|
|
|
|
importer.Import(nodeSet);
|
|
importer.Complete();
|
|
|
|
var method = (TypedMethodState)Find(importer, 202);
|
|
method.InputArguments!.Value =
|
|
[
|
|
new Argument
|
|
{
|
|
Name = "Value",
|
|
DataType = DataTypeIds.Int32,
|
|
ValueRank = ValueRanks.Scalar
|
|
}
|
|
];
|
|
method.OutputArguments!.Value =
|
|
[
|
|
new Argument
|
|
{
|
|
Name = "Result",
|
|
DataType = DataTypeIds.Int32,
|
|
ValueRank = ValueRanks.Scalar
|
|
}
|
|
];
|
|
method.OnCallMethod = (_, _, inputArguments, outputArguments) =>
|
|
{
|
|
Assert.That(inputArguments[0].TryGetValue(out int input), Is.True);
|
|
outputArguments[0] = new Variant(input + 1);
|
|
return ServiceResult.Good;
|
|
};
|
|
var argumentErrors = new List<ServiceResult>();
|
|
var output = new List<Variant>();
|
|
|
|
ServiceResult result = await method.CallAsync(
|
|
context,
|
|
NodeId.Null,
|
|
[new Variant(41)],
|
|
argumentErrors,
|
|
output).ConfigureAwait(false);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(method.InputArguments.NodeId, Is.EqualTo(new NodeId(203u, 1)));
|
|
Assert.That(method.OutputArguments.NodeId, Is.EqualTo(new NodeId(204u, 1)));
|
|
Assert.That(method.InputArguments.Parent, Is.SameAs(method));
|
|
Assert.That(method.OutputArguments.Parent, Is.SameAs(method));
|
|
Assert.That(ServiceResult.IsGood(result), Is.True);
|
|
Assert.That(argumentErrors, Has.Count.EqualTo(1));
|
|
Assert.That(ServiceResult.IsGood(argumentErrors[0]), Is.True);
|
|
Assert.That(output, Has.Count.EqualTo(1));
|
|
Assert.That(output[0].TryGetValue(out int value), Is.True);
|
|
Assert.That(value, Is.EqualTo(42));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void DuplicateNodeIdsAcrossDocumentsAreRejected()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
var importer = new NodeSetImporter(context, factoryProvider: null);
|
|
UANodeSet first = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=1" BrowseName="1:First">
|
|
<DisplayName>First</DisplayName>
|
|
</UAObject>
|
|
""");
|
|
UANodeSet second = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=1" BrowseName="1:Second">
|
|
<DisplayName>Second</DisplayName>
|
|
</UAObject>
|
|
""");
|
|
|
|
importer.Import(first);
|
|
|
|
Assert.That(
|
|
() => importer.Import(second),
|
|
Throws.InvalidOperationException.With.Message.Contains("Duplicate NodeId"));
|
|
}
|
|
|
|
[Test]
|
|
public void FactoryThatMaterializesChildrenIsRejected()
|
|
{
|
|
SystemContext context = CreateContext();
|
|
UANodeSet nodeSet = ReadNodeSet(
|
|
"""
|
|
<UAObject NodeId="ns=1;i=200" BrowseName="1:GeneratedLike">
|
|
<DisplayName>GeneratedLike</DisplayName>
|
|
<References>
|
|
<Reference ReferenceType="i=40">ns=1;i=100</Reference>
|
|
</References>
|
|
</UAObject>
|
|
""");
|
|
var importer = new NodeSetImporter(
|
|
context,
|
|
new ManualFactoryProvider(
|
|
Factory(
|
|
NodeClass.Object,
|
|
100,
|
|
() =>
|
|
{
|
|
var state = new GeneratedLikeObjectState(null!);
|
|
state.Create(
|
|
context,
|
|
NodeId.Null,
|
|
new QualifiedName("GeneratedLike", 1),
|
|
LocalizedText.Null,
|
|
assignNodeIds: false);
|
|
return state;
|
|
})));
|
|
|
|
Assert.That(
|
|
() => importer.Import(nodeSet),
|
|
Throws.InvalidOperationException.With.Message.Contains("empty states"));
|
|
}
|
|
|
|
private static ManualImportFactory Factory(
|
|
NodeClass nodeClass,
|
|
uint discriminatorId,
|
|
Func<NodeState> create)
|
|
{
|
|
return new ManualImportFactory(
|
|
nodeClass,
|
|
new ExpandedNodeId(discriminatorId, kNamespaceUri),
|
|
create);
|
|
}
|
|
|
|
private static NodeState Find(NodeSetImporter importer, uint identifier)
|
|
{
|
|
return importer.ImportedNodes.Single(
|
|
node => HasNumericIdentifier(node, identifier));
|
|
}
|
|
|
|
private static bool HasNumericIdentifier(
|
|
NodeState node,
|
|
uint identifier)
|
|
{
|
|
return node.NodeId.TryGetValue(out uint actual) &&
|
|
actual == identifier;
|
|
}
|
|
|
|
private static SystemContext CreateContext()
|
|
{
|
|
var namespaceUris = new NamespaceTable();
|
|
return new SystemContext(NUnitTelemetryContext.Create())
|
|
{
|
|
NamespaceUris = namespaceUris,
|
|
ServerUris = new StringTable(),
|
|
TypeTable = new TypeTable(namespaceUris),
|
|
EncodeableFactory = EncodeableFactory.Create()
|
|
};
|
|
}
|
|
|
|
private static UANodeSet ReadNodeSet(
|
|
string nodes,
|
|
bool includeExternalNamespace = false)
|
|
{
|
|
string externalNamespace = includeExternalNamespace
|
|
? $" <Uri>{kExternalNamespaceUri}</Uri>\r\n"
|
|
: string.Empty;
|
|
string xml =
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" +
|
|
"<UANodeSet xmlns=\"http://opcfoundation.org/UA/2011/03/UANodeSet.xsd\"\r\n" +
|
|
" xmlns:uax=\"http://opcfoundation.org/UA/2008/02/Types.xsd\">\r\n" +
|
|
" <NamespaceUris>\r\n" +
|
|
$" <Uri>{kNamespaceUri}</Uri>\r\n" +
|
|
externalNamespace +
|
|
" </NamespaceUris>\r\n" +
|
|
nodes +
|
|
"\r\n</UANodeSet>";
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
|
|
return UANodeSet.Read(stream)!;
|
|
}
|
|
|
|
private static void AssertNodeParity(
|
|
ISystemContext genericContext,
|
|
NodeState generic,
|
|
ISystemContext typedContext,
|
|
NodeState typed)
|
|
{
|
|
var genericReferences = new List<IReference>();
|
|
var typedReferences = new List<IReference>();
|
|
generic.GetReferences(genericContext, genericReferences);
|
|
typed.GetReferences(typedContext, typedReferences);
|
|
string[] expectedReferences = genericReferences
|
|
.Select(FormatReference)
|
|
.OrderBy(value => value, StringComparer.Ordinal)
|
|
.ToArray();
|
|
string[] actualReferences = typedReferences
|
|
.Select(FormatReference)
|
|
.OrderBy(value => value, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(typed.NodeClass, Is.EqualTo(generic.NodeClass));
|
|
Assert.That(typed.NodeId, Is.EqualTo(generic.NodeId));
|
|
Assert.That(typed.BrowseName, Is.EqualTo(generic.BrowseName));
|
|
Assert.That(typed.DisplayName, Is.EqualTo(generic.DisplayName));
|
|
Assert.That(typed.Description, Is.EqualTo(generic.Description));
|
|
Assert.That(typed.SymbolicName, Is.EqualTo(generic.SymbolicName));
|
|
Assert.That(typed.WriteMask, Is.EqualTo(generic.WriteMask));
|
|
Assert.That(typed.UserWriteMask, Is.EqualTo(generic.UserWriteMask));
|
|
Assert.That(typed.AccessRestrictions, Is.EqualTo(generic.AccessRestrictions));
|
|
Assert.That(typed.ReleaseStatus, Is.EqualTo(generic.ReleaseStatus));
|
|
Assert.That(typed.DesignToolOnly, Is.EqualTo(generic.DesignToolOnly));
|
|
Assert.That(typed.NodeSetDocumentation, Is.EqualTo(generic.NodeSetDocumentation));
|
|
Assert.That(
|
|
typed.Categories?.ToArray(),
|
|
Is.EqualTo(generic.Categories?.ToArray()));
|
|
Assert.That(
|
|
FormatExtensions(typed.Extensions!),
|
|
Is.EqualTo(FormatExtensions(generic.Extensions!)));
|
|
Assert.That(
|
|
FormatRolePermissions(typed.RolePermissions),
|
|
Is.EqualTo(FormatRolePermissions(generic.RolePermissions)));
|
|
Assert.That(actualReferences, Is.EqualTo(expectedReferences));
|
|
});
|
|
|
|
if (generic is BaseInstanceState genericInstance &&
|
|
typed is BaseInstanceState typedInstance)
|
|
{
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
typedInstance.TypeDefinitionId,
|
|
Is.EqualTo(genericInstance.TypeDefinitionId));
|
|
Assert.That(
|
|
typedInstance.ModellingRuleId,
|
|
Is.EqualTo(genericInstance.ModellingRuleId));
|
|
Assert.That(
|
|
typedInstance.Parent?.NodeId,
|
|
Is.EqualTo(genericInstance.Parent?.NodeId));
|
|
});
|
|
}
|
|
if (generic is BaseObjectState genericObject &&
|
|
typed is BaseObjectState typedObject)
|
|
{
|
|
Assert.That(typedObject.EventNotifier, Is.EqualTo(genericObject.EventNotifier));
|
|
}
|
|
if (generic is MethodState genericMethod &&
|
|
typed is MethodState typedMethod)
|
|
{
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(typedMethod.Executable, Is.EqualTo(genericMethod.Executable));
|
|
Assert.That(
|
|
typedMethod.UserExecutable,
|
|
Is.EqualTo(genericMethod.UserExecutable));
|
|
Assert.That(
|
|
typedMethod.MethodDeclarationId,
|
|
Is.EqualTo(genericMethod.MethodDeclarationId));
|
|
});
|
|
}
|
|
}
|
|
|
|
private static string FormatReference(IReference reference)
|
|
{
|
|
return $"{reference.ReferenceTypeId}|{reference.IsInverse}|{reference.TargetId}";
|
|
}
|
|
|
|
private static string[] FormatExtensions(XmlElement[] extensions)
|
|
{
|
|
if (extensions is null)
|
|
{
|
|
return null!;
|
|
}
|
|
|
|
var formatted = new string[extensions.Length];
|
|
for (int i = 0; i < extensions.Length; i++)
|
|
{
|
|
formatted[i] = extensions[i].OuterXml!;
|
|
}
|
|
return formatted;
|
|
}
|
|
|
|
private static string[] FormatRolePermissions(
|
|
ArrayOf<RolePermissionType> permissions)
|
|
{
|
|
var formatted = new string[permissions.Count];
|
|
for (int i = 0; i < permissions.Count; i++)
|
|
{
|
|
formatted[i] =
|
|
$"{permissions[i].RoleId}|{permissions[i].Permissions}";
|
|
}
|
|
return formatted;
|
|
}
|
|
|
|
private sealed class ManualFactoryProvider : INodeSetImportFactoryProvider
|
|
{
|
|
public ManualFactoryProvider(params INodeSetImportFactory[] factories)
|
|
{
|
|
m_factories = factories;
|
|
}
|
|
|
|
public ArrayOf<INodeSetImportFactory> GetNodeSetImportFactories()
|
|
{
|
|
return m_factories;
|
|
}
|
|
|
|
private readonly ArrayOf<INodeSetImportFactory> m_factories;
|
|
}
|
|
|
|
private sealed class ManualImportFactory : INodeSetImportFactory
|
|
{
|
|
public ManualImportFactory(
|
|
NodeClass nodeClass,
|
|
ExpandedNodeId discriminatorId,
|
|
Func<NodeState> create,
|
|
NodeSetImportDiscriminator? discriminator = null)
|
|
{
|
|
NodeClass = nodeClass;
|
|
DiscriminatorId = discriminatorId;
|
|
Discriminator = discriminator ??
|
|
nodeClass switch
|
|
{
|
|
NodeClass.Object or NodeClass.Variable =>
|
|
NodeSetImportDiscriminator.TypeDefinition,
|
|
NodeClass.Method =>
|
|
NodeSetImportDiscriminator.MethodDeclaration,
|
|
_ => NodeSetImportDiscriminator.NodeId
|
|
};
|
|
m_create = create;
|
|
}
|
|
|
|
public NodeClass NodeClass { get; }
|
|
|
|
public NodeSetImportDiscriminator Discriminator { get; }
|
|
|
|
public ExpandedNodeId DiscriminatorId { get; }
|
|
|
|
public NodeState CreateEmptyState()
|
|
{
|
|
return m_create();
|
|
}
|
|
|
|
private readonly Func<NodeState> m_create;
|
|
}
|
|
|
|
private sealed class TypedObjectState : BaseObjectState
|
|
{
|
|
public TypedObjectState(NodeState parent)
|
|
: base(parent)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class TypedVariableState : BaseDataVariableState
|
|
{
|
|
public TypedVariableState(NodeState parent)
|
|
: base(parent)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class TypedMethodState : MethodState
|
|
{
|
|
public TypedMethodState(NodeState parent)
|
|
: base(parent)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class TypedObjectTypeState : BaseObjectTypeState;
|
|
|
|
private sealed class TypedVariableTypeState : BaseDataVariableTypeState;
|
|
|
|
private sealed class TypedDataTypeState : DataTypeState;
|
|
|
|
private sealed class TypedReferenceTypeState : ReferenceTypeState;
|
|
|
|
private sealed class TypedViewState : ViewState;
|
|
|
|
private sealed class GeneratedLikeObjectState : BaseObjectState
|
|
{
|
|
public GeneratedLikeObjectState(NodeState parent)
|
|
: base(parent)
|
|
{
|
|
}
|
|
|
|
public BaseVariableState MandatoryValue { get; private set; } = null!;
|
|
|
|
public int InitializeCount { get; private set; }
|
|
|
|
protected override void Initialize(ISystemContext context)
|
|
{
|
|
InitializeCount++;
|
|
base.Initialize(context);
|
|
var child = new BaseDataVariableState(this)
|
|
{
|
|
BrowseName = new QualifiedName("MandatoryValue", 1),
|
|
DisplayName = new LocalizedText("MandatoryValue"),
|
|
DataType = DataTypeIds.Int32
|
|
};
|
|
AddChild(child);
|
|
}
|
|
|
|
public override void GetChildren(
|
|
ISystemContext context,
|
|
IList<BaseInstanceState> children)
|
|
{
|
|
if (MandatoryValue is not null)
|
|
{
|
|
children.Add(MandatoryValue);
|
|
}
|
|
base.GetChildren(context, children);
|
|
}
|
|
|
|
protected override BaseInstanceState FindChild(
|
|
ISystemContext context,
|
|
QualifiedName browseName,
|
|
bool createOrReplace,
|
|
BaseInstanceState? replacement,
|
|
bool assignInstanceNodeIds = true)
|
|
{
|
|
if (browseName.Name == "MandatoryValue")
|
|
{
|
|
if (createOrReplace)
|
|
{
|
|
MandatoryValue = replacement as BaseVariableState ??
|
|
new BaseDataVariableState(this)
|
|
{
|
|
BrowseName = browseName,
|
|
DisplayName = new LocalizedText(browseName.Name),
|
|
DataType = DataTypeIds.Int32
|
|
};
|
|
}
|
|
return MandatoryValue;
|
|
}
|
|
|
|
return base.FindChild(
|
|
context,
|
|
browseName,
|
|
createOrReplace,
|
|
replacement,
|
|
assignInstanceNodeIds)!;
|
|
}
|
|
}
|
|
}
|
|
}
|