/* ======================================================================== * 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.IO; using System.Linq; using System.Text; using NUnit.Framework; using Opc.Ua.Core.TestFramework; namespace Opc.Ua.Core.Encoders.Tests { /// /// Tests for the IEncodeable classes. /// [TestFixture] [Category("EncodeableTypes")] [SetCulture("en-us")] [SetUICulture("en-us")] [Parallelizable] public class EncodeableTypesTests : EncoderCommon { [DatapointSource] public Type[] TypeArray = [ .. typeof(BaseObjectState).Assembly .GetExportedTypes() .Where(IsEncodeableType) .Where(PublishesStructureMetadata) ]; /// /// Excludes the encodeable types that are not ordinary Structures. /// /// /// Every assertion below assumes a generated Structure: that the type /// has its own binary and XML encoding Objects distinct from its /// DataType, and that the standard factory can resolve it from an /// encoding id. OPC 10000-6 5.1.10 defines /// as the exception - its ExtensionObject TypeId is "the identifier for /// the Decimal DataType" itself, it "is like a built-in type and a /// DevelopmentPlatform has to have hardcoded knowledge of the type", /// and "No Structure metadata is published for this type". So it has no /// separate encoding Objects to be distinct from, and this suite's /// premise does not apply to it. Its own encoding is covered end to end /// by DecimalEncodingTests across binary, XML and JSON. /// private static bool PublishesStructureMetadata(Type systemType) { return systemType != typeof(Opc.Ua.Decimal); } /// /// Verify encode and decode of an encodeable type. /// [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableType( [ValueSource( nameof(EncodingTypesJsonBinaryXmlAndJsonCompact))] EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, Type systemType) { EncodingType encoderType = encoderTypeGroup.EncoderType; JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; bool useXmlParser = encoderTypeGroup.UseXmlParser; var testObject = CreateDefaultEncodeableType(systemType) as IEncodeable; Assert.That(testObject, Is.Not.Null); if (testObject.BinaryEncodingId.IsNull) { return; } Assert.That(testObject.BinaryEncodingId.IsNull, Is.False); Assert.That(testObject.TypeId.IsNull, Is.False); Assert.That(testObject.XmlEncodingId.IsNull, Is.False); Assert.That(testObject.BinaryEncodingId, Is.Not.EqualTo(testObject.TypeId)); Assert.That(testObject.XmlEncodingId, Is.Not.EqualTo(testObject.TypeId)); Assert.That(testObject.XmlEncodingId, Is.Not.EqualTo(testObject.BinaryEncodingId)); EncodeDecode( encoderType, jsonEncodingType, useXmlParser, BuiltInType.ExtensionObject, memoryStreamType, new ExtensionObject(testObject)); } [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableTypeArray( [ValueSource( nameof(EncodingTypesJsonBinaryXmlAndJsonCompact))] EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, Type systemType) { EncodingType encoderType = encoderTypeGroup.EncoderType; JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; bool useXmlParser = encoderTypeGroup.UseXmlParser; int arrayLength = DataGenerator.GetRandomByte(); var array = new ExtensionObject[arrayLength]; ExpandedNodeId dataTypeId = NodeId.Null; for (int i = 0; i < array.Length; i++) { var testObject = CreateDefaultEncodeableType(systemType) as IEncodeable; array[i] = new ExtensionObject(testObject!); if (dataTypeId.IsNull) { dataTypeId = testObject!.TypeId; } } var expected = Variant.From(array); const string objectName = "Array"; byte[] buffer; using (MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using ( IEncoder encoder = CreateEncoder( encoderType, Context, encoderStream, systemType, jsonEncodingType)) { encoder.PushNamespace("urn:This:is:another:namespace"); encoder.WriteVariantValue(objectName, expected); encoder.PopNamespace(); } buffer = encoderStream.ToArray(); } switch (encoderType) { case EncodingType.Json: PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); break; case EncodingType.Xml: string xml = Encoding.UTF8.GetString(buffer); Assert.That( xml.Contains( "", StringComparison.Ordinal), Is.True); break; case EncodingType.Binary: break; default: Assert.Fail($"Encoder type {encoderType} not supported."); break; } Variant result; using (var decoderStream = new MemoryStream(buffer)) using (IDecoder decoder = CreateDecoder( encoderType, useXmlParser, Context, decoderStream, systemType)) { decoder.PushNamespace("urn:This:is:another:namespace"); result = decoder.ReadVariantValue(objectName, expected.TypeInfo); decoder.PopNamespace(); } TestOutput.WriteLine("Result:"); TestOutput.WriteLine(result); string encodeInfo = $"Encoder: {encoderType} Type: Array of {systemType}. Expected is different from result."; Assert.That(result, Is.EqualTo(expected), encodeInfo); } [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableTypeMatrix( [ValueSource( nameof(EncodingTypesJsonBinaryXmlAndJsonCompact))] EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, Type systemType) { EncodingType encoderType = encoderTypeGroup.EncoderType; JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; bool useXmlParser = encoderTypeGroup.UseXmlParser; int matrixDimension = RandomSource.NextInt32(2) + 2; int[] dimensions = new int[matrixDimension]; SetMatrixDimensions(dimensions); int elementsCount = ElementsFromDimension(dimensions); var array = new ExtensionObject[elementsCount]; ExpandedNodeId dataTypeId = NodeId.Null; for (int i = 0; i < array.Length; i++) { var testObject = CreateDefaultEncodeableType(systemType) as IEncodeable; array[i] = new ExtensionObject(testObject!); if (dataTypeId.IsNull) { dataTypeId = testObject!.TypeId; } } var expected = Variant.From(array); const string objectName = "Matrix"; byte[] buffer; using (MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using ( IEncoder encoder = CreateEncoder( encoderType, Context, encoderStream, systemType, jsonEncodingType)) { encoder.WriteVariantValue(objectName, expected); } buffer = encoderStream.ToArray(); } if (encoderType == EncodingType.Json) { PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); } Variant result; using (var decoderStream = new MemoryStream(buffer)) using (IDecoder decoder = CreateDecoder( encoderType, useXmlParser, Context, decoderStream, systemType)) { result = decoder.ReadVariantValue(objectName, expected.TypeInfo); } TestOutput.WriteLine("Result:"); TestOutput.WriteLine(result); Assert.That( result, Is.EqualTo(expected), $"Encoder: {encoderType} Type: Matrix of {systemType}. Expected is different from result."); } /// /// Create an instance of an encodeable type with default values. /// /// The type to create private static object CreateDefaultEncodeableType(Type systemType) { object instance = Activator.CreateInstance(systemType)!; SetDefaultEncodeableType(instance!); return instance; } /// /// Set encodeable type properties recursively /// to expected default values. /// /// private static void SetDefaultEncodeableType(object typeInstance) { foreach (System.Reflection.PropertyInfo property in typeInstance.GetType() .GetProperties()) { if (property.CanWrite) { var typeInfo = TypeInfo.Construct(property.PropertyType); switch (typeInfo.BuiltInType) { case BuiltInType.ExtensionObject: object? propertyObject = property.GetValue(typeInstance); if (propertyObject == null && property.PropertyType.IsAssignableFrom(typeof(ExtensionObject))) { property.SetValue(typeInstance, ExtensionObject.Null); } else if (propertyObject is not null and IEncodeable) { SetDefaultEncodeableType(propertyObject); } break; case BuiltInType.Null: break; case BuiltInType.DataValue: if (property.GetValue(typeInstance) == null) { property.SetValue(typeInstance, new DataValue()); } break; case BuiltInType.NodeId: if (property.GetValue(typeInstance) == null) { property.SetValue(typeInstance, NodeId.Null); } break; case BuiltInType.ExpandedNodeId: if (property.GetValue(typeInstance) == null) { property.SetValue(typeInstance, ExpandedNodeId.Null); } break; case BuiltInType.DiagnosticInfo: if (property.GetValue(typeInstance) == null) { if (typeInfo.ValueRank == ValueRanks.Scalar) { property.SetValue(typeInstance, new DiagnosticInfo()); } else { property.SetValue(typeInstance, Array.Empty()); } } break; case BuiltInType.Enumeration: if (typeInfo.ValueRank == ValueRanks.Scalar) { foreach (object ii in Enum.GetValues(property.PropertyType)) { property.SetValue(typeInstance, ii); break; } } break; case >= BuiltInType.Null and <= BuiltInType.Enumeration: if (typeInfo.ValueRank == ValueRanks.Scalar) { object value = TypeInfo.GetDefaultValue(typeInfo.BuiltInType)!; property.SetValue(typeInstance, value); } break; default: throw ServiceResultException.Unexpected( $"Unexpected BuiltInType {typeInfo.BuiltInType}"); } } } } } }