Files
UA-.NETStandard/tests/Opc.Ua.Server.Tests/ServerComplexTypeSystemTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# 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>
2026-10-08 08:48:13 +02:00

624 lines
26 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/
* ======================================================================*/
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;
using Opc.Ua.Schema;
using Opc.Ua.Tests;
namespace Opc.Ua.Server.Tests
{
/// <summary>
/// Tests for building and registering dynamic stand-in encodeables for the
/// runtime-loaded custom DataTypes of a server address space.
/// </summary>
[TestFixture]
[Category("ComplexTypes")]
[Parallelizable]
public class ServerComplexTypeSystemTests
{
private const string TestNamespaceUri = "http://test.org/UA/ComplexTypes/";
private static readonly int[] s_decoyValue = [42];
[SetUp]
public void SetUp()
{
m_telemetry = NUnitTelemetryContext.Create();
m_namespaceUris = new NamespaceTable();
m_namespaceUris.GetIndexOrAppend(Types.Namespaces.OpcUa);
m_ns = m_namespaceUris.GetIndexOrAppend(TestNamespaceUri);
m_typeTree = new TypeTable(m_namespaceUris);
m_factory = EncodeableFactory.Create();
m_nodesById = [];
m_structTypeId = new NodeId(6001, m_ns);
m_structEncodingId = new NodeId(7001, m_ns);
m_enumTypeId = new NodeId(6002, m_ns);
BuildAddressSpace();
m_mockNodeManager = new Mock<IMasterNodeManager>();
m_mockNodeManager
.Setup(m => m.FindNodeInAddressSpaceAsync(
It.IsAny<NodeId>(),
It.IsAny<CancellationToken>()))
.Returns<NodeId, CancellationToken>((id, _) =>
new ValueTask<NodeState?>(
(m_nodesById.TryGetValue(id, out NodeState? node) ? node : null)!));
m_mockServer = new Mock<IServerInternal>();
m_mockServer.Setup(s => s.NamespaceUris).Returns(m_namespaceUris);
m_mockServer.Setup(s => s.TypeTree).Returns(m_typeTree);
m_mockServer.Setup(s => s.Factory).Returns(m_factory);
m_mockServer.Setup(s => s.NodeManager).Returns(m_mockNodeManager.Object);
m_mockServer.Setup(s => s.Telemetry).Returns(m_telemetry);
m_mockServer.Setup(s => s.DefaultSystemContext)
.Returns(new ServerSystemContext(m_mockServer.Object));
}
[Test]
public async Task LoadComplexTypesBuildsStandInsForRuntimeStructAndEnum()
{
var options = new ServerComplexTypeOptions { ThrowOnError = true };
await m_mockServer.Object
.LoadComplexTypesAsync(m_telemetry, options)
.ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(
m_factory.TryGetEncodeableType(
NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris),
out IEncodeableType? structType),
Is.True,
"the runtime structure type should be registered in the factory");
Assert.That(structType, Is.Not.Null);
Assert.That(
m_factory.TryGetEncodeableType(
NodeId.ToExpandedNodeId(m_structEncodingId, m_namespaceUris),
out _),
Is.True,
"the binary encoding id should resolve to the structure stand-in");
Assert.That(
m_factory.TryGetEnumeratedType(
NodeId.ToExpandedNodeId(m_enumTypeId, m_namespaceUris),
out IEnumeratedType? enumType),
Is.True,
"the runtime enumeration type should be registered in the factory");
Assert.That(enumType, Is.Not.Null);
});
}
[Test]
public async Task LoadComplexTypesUsesAddressSpaceMetadataForImportedDefinitions()
{
var node = (DataTypeState)m_nodesById[m_structTypeId];
Assert.That(node.DataTypeDefinition.TryGetValue(out StructureDefinition? definition), Is.True);
var importedDefinition = new StructureDefinition
{
StructureType = definition!.StructureType,
Fields = definition.Fields
};
node.DataTypeDefinition = new ExtensionObject(importedDefinition);
await m_mockServer.Object.LoadComplexTypesAsync(m_telemetry,
new ServerComplexTypeOptions { ThrowOnError = true }).ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(m_factory.TryGetEncodeableType(
NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris), out _), Is.True);
Assert.That(m_factory.TryGetEncodeableType(
NodeId.ToExpandedNodeId(m_structEncodingId, m_namespaceUris), out _), Is.True);
Assert.That(node.DataTypeDefinition.TryGetValue(out StructureDefinition? retainedDefinition), Is.True);
Assert.That(retainedDefinition, Is.SameAs(importedDefinition));
Assert.That(importedDefinition.BaseDataType.IsNull, Is.True);
Assert.That(importedDefinition.DefaultEncodingId.IsNull, Is.True);
});
}
[Test]
public async Task LoadComplexTypesSkipsAlreadyKnownTypes()
{
var options = new ServerComplexTypeOptions { ThrowOnError = true };
// first pass builds the stand-ins into the factory
await m_mockServer.Object
.LoadComplexTypesAsync(m_telemetry, options)
.ConfigureAwait(false);
int knownAfterFirstPass = m_factory.KnownTypeIds.Count();
// second pass must skip everything that is already known to the factory
await m_mockServer.Object
.LoadComplexTypesAsync(m_telemetry, options)
.ConfigureAwait(false);
Assert.That(
m_factory.KnownTypeIds.Count(),
Is.EqualTo(knownAfterFirstPass),
"types already present in the factory must not be rebuilt");
}
[Test]
public async Task LoadComplexTypesReturnsFactoryBackedResolver()
{
var options = new ServerComplexTypeOptions { ThrowOnError = true };
// a schema-only type that has no encodeable in the factory, supplied
// through the optional registry, must still resolve via the composite
var registry = new DataTypeDefinitionRegistry();
var schemaOnlyId = new NodeId(6009, m_ns);
registry.Add(new UaTypeDescription(
new ExpandedNodeId(schemaOnlyId),
new QualifiedName("SchemaOnly", m_ns),
new StructureDefinition
{
BaseDataType = DataTypeIds.Structure,
StructureType = StructureType.Structure,
Fields = []
},
TestNamespaceUri));
IDataTypeDefinitionResolver resolver = await m_mockServer.Object
.LoadComplexTypesAsync(m_telemetry, options, registry)
.ConfigureAwait(false);
Assert.Multiple(() =>
{
// the primed factory is the source for the runtime stand-ins
Assert.That(
resolver.TryResolve(
NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris),
out UaTypeDescription? structDescription),
Is.True);
Assert.That(structDescription!.Definition, Is.InstanceOf<StructureDefinition>());
Assert.That(structDescription.NamespaceUri, Is.EqualTo(TestNamespaceUri));
Assert.That(
resolver.TryResolve(
NodeId.ToExpandedNodeId(m_enumTypeId, m_namespaceUris),
out UaTypeDescription? enumDescription),
Is.True);
Assert.That(enumDescription!.Definition, Is.InstanceOf<EnumDefinition>());
// the composite falls through to the registry for schema-only types
Assert.That(
resolver.TryResolve(
new ExpandedNodeId(schemaOnlyId),
out UaTypeDescription? schemaOnlyDescription),
Is.True);
Assert.That(schemaOnlyDescription!.Name, Is.EqualTo("SchemaOnly"));
});
}
[Test]
public void AddComplexTypeSystemRegistersOptionsAndResolver()
{
var services = new ServiceCollection();
services
.AddOpcUa()
.AddServer(options => options.ApplicationName = "TestComplexTypeServer")
.AddComplexTypeSystem(options => options.OnlyEnumTypes = true);
using ServiceProvider provider = services.BuildServiceProvider();
ServerComplexTypeOptions registeredOptions =
provider.GetService<ServerComplexTypeOptions>()!;
Assert.That(registeredOptions, Is.Not.Null);
Assert.That(registeredOptions.OnlyEnumTypes, Is.True);
Assert.That(provider.GetService<IDataTypeDefinitionResolver>(), Is.Not.Null);
}
[Test]
public async Task GetEnumTypeArrayIgnoresUnrelatedPropertyAndSelectsEnumStrings()
{
// DataType with a decoy HasProperty listed BEFORE the real EnumStrings
// property. The old "first non-null" logic would have returned the
// decoy; the resolver must match by BrowseName instead.
var enumWithProps = new NodeId(6003, m_ns);
var decoyPropId = new NodeId(7101, m_ns);
var enumStringsPropId = new NodeId(7102, m_ns);
var dataTypeNode = new DataTypeState
{
NodeId = enumWithProps,
BrowseName = new QualifiedName("EnumWithProps", m_ns),
SuperTypeId = DataTypeIds.Enumeration,
IsAbstract = false
};
dataTypeNode.AddReference(
ReferenceTypeIds.HasProperty, false, new ExpandedNodeId(decoyPropId));
dataTypeNode.AddReference(
ReferenceTypeIds.HasProperty, false, new ExpandedNodeId(enumStringsPropId));
m_nodesById[enumWithProps] = dataTypeNode;
m_nodesById[decoyPropId] = new BaseDataVariableState(null)
{
NodeId = decoyPropId,
BrowseName = new QualifiedName("SomethingElse", m_ns),
Value = new Variant(s_decoyValue)
};
m_nodesById[enumStringsPropId] = new BaseDataVariableState(null)
{
NodeId = enumStringsPropId,
BrowseName = new QualifiedName(BrowseNames.EnumStrings),
Value = new Variant(new LocalizedText[]
{
new("Red"),
new("Green")
})
};
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
Variant result = await resolver
.GetEnumTypeArrayAsync(NodeId.ToExpandedNodeId(enumWithProps, m_namespaceUris))
.ConfigureAwait(false);
Assert.That(
result.TryGetValue(out ArrayOf<LocalizedText> texts),
Is.True,
"the EnumStrings property must be selected and the decoy ignored");
Assert.That(texts.Count, Is.EqualTo(2));
}
[Test]
public async Task GetEnumTypeArrayPrefersEnumValuesOverEnumStrings()
{
// Both EnumStrings (listed first) and EnumValues are present; the
// richer EnumValues must win regardless of reference order.
var enumWithProps = new NodeId(6004, m_ns);
var enumStringsPropId = new NodeId(7201, m_ns);
var enumValuesPropId = new NodeId(7202, m_ns);
var dataTypeNode = new DataTypeState
{
NodeId = enumWithProps,
BrowseName = new QualifiedName("EnumWithBoth", m_ns),
SuperTypeId = DataTypeIds.Enumeration,
IsAbstract = false
};
dataTypeNode.AddReference(
ReferenceTypeIds.HasProperty, false, new ExpandedNodeId(enumStringsPropId));
dataTypeNode.AddReference(
ReferenceTypeIds.HasProperty, false, new ExpandedNodeId(enumValuesPropId));
m_nodesById[enumWithProps] = dataTypeNode;
m_nodesById[enumStringsPropId] = new BaseDataVariableState(null)
{
NodeId = enumStringsPropId,
BrowseName = new QualifiedName(BrowseNames.EnumStrings),
Value = new Variant(new LocalizedText[] { new("Red") })
};
m_nodesById[enumValuesPropId] = new BaseDataVariableState(null)
{
NodeId = enumValuesPropId,
BrowseName = new QualifiedName("EnumValues"),
Value = new Variant(new ExtensionObject[]
{
new(new EnumValueType
{
Value = 0,
DisplayName = new LocalizedText("Red")
})
})
};
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
Variant result = await resolver
.GetEnumTypeArrayAsync(NodeId.ToExpandedNodeId(enumWithProps, m_namespaceUris))
.ConfigureAwait(false);
Assert.That(
result.TryGetValue(out ArrayOf<ExtensionObject> enumValues),
Is.True,
"EnumValues must be preferred over EnumStrings");
Assert.That(enumValues.Count, Is.EqualTo(1));
}
[Test]
public async Task LoadDataTypeSystemReturnsEmpty()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
IReadOnlyDictionary<NodeId, DataDictionary> result =
await resolver.LoadDataTypeSystem().ConfigureAwait(false);
Assert.That(result, Is.Empty);
}
[Test]
public async Task BrowseTypeIdsForDictionaryComponentReturnsDefault()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
(ExpandedNodeId typeId, ExpandedNodeId encodingId, DataTypeNode? dataTypeNode) = await resolver
.BrowseTypeIdsForDictionaryComponentAsync(
NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris))
.ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(typeId.IsNull, Is.True);
Assert.That(encodingId.IsNull, Is.True);
Assert.That(dataTypeNode, Is.Null);
});
}
[Test]
public async Task FindSuperTypeReturnsSuperType()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
NodeId superType = await resolver
.FindSuperTypeAsync(m_structTypeId)
.ConfigureAwait(false);
Assert.That(superType, Is.EqualTo(DataTypeIds.Structure));
}
[Test]
public async Task FindReturnsDataTypeNodeForDataTypeAndNullOtherwise()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
INode dataTypeNode = (await resolver
.FindAsync(NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris))
.ConfigureAwait(false))!;
// the encoding node is a BaseObjectState, not a DataType
INode encodingNode = (await resolver
.FindAsync(NodeId.ToExpandedNodeId(m_structEncodingId, m_namespaceUris))
.ConfigureAwait(false))!;
Assert.Multiple(() =>
{
Assert.That(dataTypeNode, Is.InstanceOf<DataTypeNode>());
Assert.That(
ExpandedNodeId.ToNodeId(dataTypeNode!.NodeId, m_namespaceUris),
Is.EqualTo(m_structTypeId));
Assert.That(encodingNode, Is.Null);
});
}
[Test]
public async Task BrowseForEncodingsReturnsBinaryEncodingId()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
string[] supported = [BrowseNames.DefaultBinary];
(ArrayOf<NodeId> encodings, ExpandedNodeId binaryEncodingId, ExpandedNodeId xmlEncodingId) = await resolver
.BrowseForEncodingsAsync(
NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris),
supported)
.ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(
ExpandedNodeId.ToNodeId(binaryEncodingId, m_namespaceUris),
Is.EqualTo(m_structEncodingId));
Assert.That(xmlEncodingId.IsNull, Is.True);
Assert.That(encodings.Count, Is.EqualTo(1));
});
}
[Test]
public async Task BrowseForEncodingsArrayOverloadAggregatesEncodings()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
string[] supported = [BrowseNames.DefaultBinary];
ArrayOf<ExpandedNodeId> ids =
[NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris)];
ArrayOf<NodeId> encodings = await resolver
.BrowseForEncodingsAsync(ids, supported)
.ConfigureAwait(false);
Assert.That(encodings.Count, Is.EqualTo(1));
}
[Test]
public async Task LoadDataTypesReturnsCustomSubtypesAndSkipsUaTypes()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
ArrayOf<INode> result = await resolver
.LoadDataTypesAsync(NodeId.ToExpandedNodeId(DataTypeIds.Structure, m_namespaceUris))
.ConfigureAwait(false);
bool hasCustomStruct = false;
for (int i = 0; i < result.Count; i++)
{
if (ExpandedNodeId.ToNodeId(result[i].NodeId, m_namespaceUris) == m_structTypeId)
{
hasCustomStruct = true;
}
}
Assert.That(hasCustomStruct, Is.True);
}
[Test]
public async Task LoadDataTypesAddsRootNodeWhenRequested()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
ArrayOf<INode> result = await resolver
.LoadDataTypesAsync(
NodeId.ToExpandedNodeId(m_structTypeId, m_namespaceUris),
addRootNode: true)
.ConfigureAwait(false);
bool hasRoot = false;
for (int i = 0; i < result.Count; i++)
{
if (ExpandedNodeId.ToNodeId(result[i].NodeId, m_namespaceUris) == m_structTypeId)
{
hasRoot = true;
}
}
Assert.That(hasRoot, Is.True);
}
[Test]
public void LoadDataTypesThrowsWhenRootNodeCannotBeResolvedAsDataType()
{
var resolver = new AddressSpaceComplexTypeResolver(m_mockServer.Object);
var unknownId =
NodeId.ToExpandedNodeId(new NodeId(9999, m_ns), m_namespaceUris);
// an existing node that is not a DataType surfaces the same way
var nonDataType =
NodeId.ToExpandedNodeId(m_structEncodingId, m_namespaceUris);
ServiceResultException missing = Assert.ThrowsAsync<ServiceResultException>(
async () => await resolver
.LoadDataTypesAsync(unknownId, addRootNode: true)
.ConfigureAwait(false));
ServiceResultException wrongClass = Assert.ThrowsAsync<ServiceResultException>(
async () => await resolver
.LoadDataTypesAsync(nonDataType, addRootNode: true)
.ConfigureAwait(false));
Assert.Multiple(() =>
{
Assert.That(missing.StatusCode.Code, Is.EqualTo(StatusCodes.BadNodeIdUnknown.Code));
Assert.That(
wrongClass.StatusCode.Code,
Is.EqualTo(StatusCodes.BadNodeIdUnknown.Code));
});
}
private void BuildAddressSpace()
{
m_typeTree.AddSubtype(DataTypeIds.BaseDataType, NodeId.Null);
m_typeTree.AddSubtype(DataTypeIds.Structure, DataTypeIds.BaseDataType);
m_typeTree.AddSubtype(DataTypeIds.Enumeration, DataTypeIds.BaseDataType);
m_typeTree.AddSubtype(m_structTypeId, DataTypeIds.Structure);
m_typeTree.AddSubtype(m_enumTypeId, DataTypeIds.Enumeration);
// Abstract base DataTypes are findable but carry no definition so the
// schema walk skips them cleanly (without falling back to a read).
AddAbstractBaseType(DataTypeIds.BaseDataType, "BaseDataType");
AddAbstractBaseType(DataTypeIds.Structure, "Structure");
AddAbstractBaseType(DataTypeIds.Enumeration, "Enumeration");
var structureDefinition = new StructureDefinition
{
BaseDataType = DataTypeIds.Structure,
StructureType = StructureType.Structure,
Fields =
[
new StructureField
{
Name = "Number",
DataType = DataTypeIds.Int32,
ValueRank = ValueRanks.Scalar
},
new StructureField
{
Name = "Text",
DataType = DataTypeIds.String,
ValueRank = ValueRanks.Scalar
}
]
};
var structureNode = new DataTypeState
{
NodeId = m_structTypeId,
BrowseName = new QualifiedName("TestStructure", m_ns),
SuperTypeId = DataTypeIds.Structure,
IsAbstract = false,
DataTypeDefinition = new ExtensionObject(structureDefinition)
};
structureNode.AddReference(
ReferenceTypeIds.HasEncoding,
false,
new ExpandedNodeId(m_structEncodingId));
m_nodesById[m_structTypeId] = structureNode;
m_nodesById[m_structEncodingId] = new BaseObjectState(null)
{
NodeId = m_structEncodingId,
BrowseName = new QualifiedName(BrowseNames.DefaultBinary)
};
var enumDefinition = new EnumDefinition
{
Fields =
[
new EnumField { Name = "Red", Value = 0 },
new EnumField { Name = "Green", Value = 1 },
new EnumField { Name = "Blue", Value = 2 }
]
};
m_nodesById[m_enumTypeId] = new DataTypeState
{
NodeId = m_enumTypeId,
BrowseName = new QualifiedName("TestEnum", m_ns),
SuperTypeId = DataTypeIds.Enumeration,
IsAbstract = false,
DataTypeDefinition = new ExtensionObject(enumDefinition)
};
}
private void AddAbstractBaseType(NodeId nodeId, string name)
{
m_nodesById[nodeId] = new DataTypeState
{
NodeId = nodeId,
BrowseName = new QualifiedName(name),
IsAbstract = true
};
}
private ITelemetryContext m_telemetry;
private NamespaceTable m_namespaceUris;
private ushort m_ns;
private TypeTable m_typeTree;
private IEncodeableFactory m_factory;
private Dictionary<NodeId, NodeState> m_nodesById;
private Mock<IMasterNodeManager> m_mockNodeManager;
private Mock<IServerInternal> m_mockServer;
private NodeId m_structTypeId;
private NodeId m_structEncodingId;
private NodeId m_enumTypeId;
}
}