/* ======================================================================== * 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.IO; using System.Linq; using System.Threading; using System.Xml; using NUnit.Framework; using Opc.Ua.Core.TestFramework; using Opc.Ua.Test; using ComplexStructure = Opc.Ua.Encoders.Structure; namespace Opc.Ua.Client.ComplexTypes.Tests { /// /// Namespace constants for the default complex type tests. /// public static class Namespaces { /// /// The URI for the OpcUa namespace. /// public const string OpcUa = "http://opcfoundation.org/UA/"; /// /// The URI for the OpcUaXsd namespace. /// public const string OpcUaXsd = "http://opcfoundation.org/UA/2008/02/Types.xsd"; /// /// The URI for the test encoder namespace. /// public const string OpcUaEncoderTests = "http://opcfoundation.org/UA/DefaultComplexTypeTests/"; } /// /// Common base class for default complex type tests. /// Uses DefaultComplexTypeFactory/Builder instead of Reflection.Emit. /// public abstract class DefaultComplexTypesCommon : EncoderCommon { protected DefaultComplexTypeFactory m_factory; protected IComplexTypeBuilder m_complexTypeBuilder; protected int m_nodeIdCount; [OneTimeSetUp] protected new void OneTimeSetUp() { m_nodeIdCount = 0; m_factory = new DefaultComplexTypeFactory(); m_complexTypeBuilder = m_factory.Create( Namespaces.OpcUaEncoderTests, 3, "DefaultTests"); } [OneTimeTearDown] protected new void OneTimeTearDown() { } [SetUp] protected new void SetUp() { } [TearDown] protected new void TearDown() { } /// /// Parameter class for structure field tests. /// public class StructureFieldParameter : IFormattable { public StructureFieldParameter(StructureField structureField) { Name = structureField.Name!; BuiltInType = TypeInfo.GetBuiltInType(structureField.DataType); } public string Name { get; set; } = null!; public BuiltInType BuiltInType { get; set; } public string ToString(string? format, IFormatProvider? formatProvider) { return Name; } } [DatapointSource] public StructureType[] StructureTypes = #if NET8_0_OR_GREATER Enum.GetValues(); #else (StructureType[])Enum.GetValues(typeof(StructureType)); #endif [DatapointSource] public StructureFieldParameter[] StructureField = [ .. GetAllBuiltInTypesFields().Select(s => new StructureFieldParameter(s)) ]; internal IEncodeableType BuildComplexTypeWithAllBuiltInTypes( StructureType structureType, string testFunc) { return BuildComplexTypeWithAllBuiltInTypes(null!, structureType, testFunc, out _); } /// /// Builds a complex type with all BuiltInTypes as properties /// using the DefaultComplexTypeBuilder. /// private IEncodeableType BuildComplexTypeWithAllBuiltInTypes( IServiceMessageContext context, StructureType structureType, string testFunc, out ExpandedNodeId nodeId) { uint typeId = (uint)Interlocked.Add(ref m_nodeIdCount, 100); var complexTypeStructure = new StructureDefinition { BaseDataType = structureType == StructureType.Union ? DataTypeIds.Union : DataTypeIds.Structure, DefaultEncodingId = default, Fields = GetAllBuiltInTypesFields(), StructureType = structureType }; IComplexTypeFieldBuilder fieldBuilder = m_complexTypeBuilder.AddStructuredType( QualifiedName.From(structureType + "." + testFunc), complexTypeStructure); nodeId = new ExpandedNodeId(typeId++, m_complexTypeBuilder.TargetNamespace); var binaryEncodingId = new ExpandedNodeId( typeId++, m_complexTypeBuilder.TargetNamespace); var xmlEncodingId = new ExpandedNodeId(typeId++, m_complexTypeBuilder.TargetNamespace); fieldBuilder.AddTypeIdAttribute(nodeId, binaryEncodingId, xmlEncodingId); int i = 1; foreach (StructureField field in complexTypeStructure.Fields) { IType fieldType = TypeInfo.GetSystemType(field.DataType, null!)!; field.IsOptional = structureType == StructureType.StructureWithOptionalFields; fieldBuilder.AddField(field, fieldType, i++, false); } IEncodeableType complexType = fieldBuilder.CreateType(); if (context != null) { context.Factory.Builder.AddEncodeableType(nodeId, complexType).Commit(); context.Factory.Builder.AddEncodeableType(binaryEncodingId, complexType).Commit(); context.Factory.Builder.AddEncodeableType(xmlEncodingId, complexType).Commit(); } return complexType; } /// /// Return a collection of fields with BuiltInTypes. /// private static List GetAllBuiltInTypesFields() { var collection = new List(); foreach (BuiltInType builtInType in BuiltInTypes) { if (builtInType is BuiltInType.Null or BuiltInType.Variant or BuiltInType.DataValue or BuiltInType.ExtensionObject or >= BuiltInType.Number) { continue; } collection.Add( new StructureField { Name = builtInType.ToString(), DataType = new NodeId((uint)builtInType), ArrayDimensions = default, Description = LocalizedText.From($"A BuiltInType.{builtInType} property."), IsOptional = false, MaxStringLength = 0, ValueRank = -1 }); } return collection; } /// /// Create complex types for all structure types and register them. /// internal void CreateComplexTypes( IServiceMessageContext context, Dictionary dict, string nameExtension) { foreach (StructureType structureType in StructureTypes) { IEncodeableType encodeableType = BuildComplexTypeWithAllBuiltInTypes( context, structureType, nameof(CreateComplexTypes) + nameExtension, out ExpandedNodeId nodeId); dict[structureType] = (nodeId, encodeableType); } } /// /// Helper to fill a Structure-based type with default or random values. /// internal void FillStructWithValues( ComplexStructure structType, bool randomValues, NamespaceTable namespaceUris) { int index = 0; foreach (IStructureField property in structType.GetFields()) { BuiltInType builtInType = property.TypeInfo.BuiltInType; Variant newObj = randomValues ? DataGenerator.GetRandomVariant(builtInType, !property.TypeInfo.IsScalar) : Variant.CreateDefault(TypeInfo.Create(builtInType, property.TypeInfo.ValueRank)); if (newObj.IsNull) { switch (builtInType) { case BuiltInType.XmlElement: var doc = new XmlDocument(); newObj = Variant.From(XmlElement.From(doc.CreateElement("name"))); break; case BuiltInType.ByteString: newObj = Variant.From(ByteString.From([1, 2, 3])); break; case BuiltInType.String: newObj = Variant.From("This is a test"); break; case BuiltInType.ExtensionObject: newObj = Variant.From(ExtensionObject.Null); break; default: newObj = Variant.CreateDefault(property.TypeInfo); break; } } structType[property.Name!] = newObj; Assert.That(newObj, Is.EqualTo(structType[property.Name!])); Assert.That(newObj, Is.EqualTo(structType[index])); index++; } } /// /// Encode and decode a default complex type, verify the result. /// protected void EncodeDecodeComplexType( IServiceMessageContext encoderContext, MemoryStreamType memoryStreamType, EncodingType encoderType, JsonEncodingType jsonEncodingType, bool useXmlParser, StructureType structureType, ExpandedNodeId nodeId, IEncodeable data) { string encodeInfo = $"Encoder: {encoderType} Type:{structureType}"; TestOutput.WriteLine(encodeInfo); TestOutput.WriteLine(data); ExtensionObject expected = CreateExtensionObject(structureType, nodeId, data); Assert.That(expected.IsNull, Is.False, "Expected DataValue is Null, " + encodeInfo); TestOutput.WriteLine("Expected:"); TestOutput.WriteLine(expected); byte[] buffer; using (MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using ( IEncoder encoder = CreateEncoder( encoderType, encoderContext, encoderStream, typeof(DataValue), jsonEncodingType)) { encoder.WriteExtensionObject("ExtensionObject", expected); } buffer = encoderStream.ToArray(); } switch (encoderType) { case EncodingType.Binary: TestOutput.WriteLine(PrettifyAndValidateBinary(buffer)); break; case EncodingType.Json: TestOutput.WriteLine(PrettifyAndValidateJson(buffer)); break; case EncodingType.Xml: TestOutput.WriteLine(PrettifyAndValidateXml(buffer)); break; } using var decoderStream = new MemoryStream(buffer); using IDecoder decoder = CreateDecoder( encoderType, useXmlParser, encoderContext, decoderStream, typeof(DataValue)); ExtensionObject result = decoder.ReadExtensionObject("ExtensionObject"); TestOutput.WriteLine("Result:"); TestOutput.WriteLine(result); Assert.That(result.IsNull, Is.False, "Resulting DataValue is Null, " + encodeInfo); Assert.That(result.Encoding, Is.EqualTo(expected.Encoding), encodeInfo); Assert.That(result, Is.EqualTo(expected), $"Failed to compare expected and result.\r\n{encodeInfo}.\r\n{expected}\r\n!=\r\n{result}."); } /// /// Create an ExtensionObject for a complex type. /// /// protected ExtensionObject CreateExtensionObject( StructureType structureType, ExpandedNodeId nodeId, object data) { switch (data) { case ByteString b: return new ExtensionObject(nodeId, b); case XmlElement x: return new ExtensionObject(nodeId, x); case IEncodeable e: return new ExtensionObject(nodeId, e, true); default: throw new ArgumentException( $"Unsupported data type {data.GetType()}" + $" for structure type {structureType}."); } } } }