/* ======================================================================== * 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.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.Extensions.Logging; using NUnit.Framework; using Opc.Ua.Schema.Binary; using Opc.Ua.Schema.Xml; using Opc.Ua.Tests; namespace Opc.Ua.SourceGeneration.Api.Tests { /// /// Test generating and compiling model design /// [TestFixture] [Category("Api")] [SetCulture("en-us")] [SetUICulture("en-us")] // [Parallelizable(ParallelScope.All)] public class GenerateCodeTests { [DatapointSource] public OptimizationLevel[] OptimizationLevels = CompilerUtils.SupportedOptimizationLevels; [DatapointSource] public string[] ModelDesignFiles = [ "TestDataDesign.xml" #if TEST_ALL , "DemoModel.xml" , "TestModel.xml" , "OpcUaOnboardingModel.xml" , "OpcUaSchedulerModel.xml" , "DemoModel.json" , "TestDataDesign.json" #endif ]; /// /// Only support modern language versions /// [DatapointSource] public LanguageVersion[] LanguageVersions = [ LanguageVersion.CSharp11 #if TEST_ALL_LANG_VERSIONS ,LanguageVersion.CSharp12 ,LanguageVersion.CSharp13 // ,LanguageVersion.CSharp14 #endif ]; [Theory] [Pairwise] public async Task GenerateAndCompileDesignFileAsync( string modelDesignFile, OptimizationLevel optimizationLevel, LanguageVersion languageVersion, bool withAnalyzers, bool withNodeLoader) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); Dictionary generatedText = GenerateCodeFromModel( modelDesignFile, languageVersion, telemetry, out Dictionary generatedOther); Dictionary generatedTextStack = GenerateStackTests.GenerateStack( StackGenerationType.Models, telemetry, out Dictionary generatedStackOther); foreach (KeyValuePair item in generatedTextStack) { generatedText.Add(item.Key, item.Value); } if (withNodeLoader) { AddPredefinedNodeLoader(generatedText); } var xmlSchemas = generatedOther .Concat(generatedStackOther) .Where(c => Path.GetExtension(c.Key) == ".xsd") .Select(c => c.Value) .ToList(); Assert.That(xmlSchemas, Has.Count.EqualTo(2)); // Validate xsd schemas using (var xsd = new MemoryStream(Encoding.UTF8.GetBytes(xmlSchemas[0]))) { var xmlValidator = new XmlSchemaValidator(new Dictionary { ["http://opcfoundation.org/UA/2008/02/Types.xsd"] = Encoding.UTF8.GetBytes(xmlSchemas[1]) }); xmlValidator.Validate(xsd, telemetry.LoggerFactory.CreateLogger()); } var binSchemas = generatedOther .Concat(generatedStackOther) .Where(c => Path.GetExtension(c.Key) == ".bsd") .Select(c => c.Value) .ToList(); Assert.That(binSchemas, Has.Count.EqualTo(2)); // Validate binary schema using (var bsd = new MemoryStream(Encoding.UTF8.GetBytes(binSchemas[0]))) { var binValidator = new BinarySchemaValidator(new Dictionary { ["http://opcfoundation.org/UA/"] = Encoding.UTF8.GetBytes(binSchemas[1]) }); binValidator.Validate(bsd); } // Parse and compile the generated code var sw = Stopwatch.StartNew(); using var peStream = new MemoryStream(); using var xmlStream = new MemoryStream(); bool success = optimizationLevel .CreateCompilation() .AddCode(generatedText.WithOpcUaCoreStubs(), languageVersion) .WithAnalyzers(withAnalyzers, out CompilationWithAnalyzers compilationWithAnalyzers) .Emit(peStream, xmlDocumentationStream: xmlStream) .Check(TestContext.Out, out int errorCount, out int warnCount); TestContext.Out.WriteLine("Compilation completed in {0} ms", sw.ElapsedMilliseconds); if (withAnalyzers) { if (compilationWithAnalyzers == null) { Assert.Ignore("Setup does not support analyzer testing"); } AnalysisResult analysisResults = await compilationWithAnalyzers.GetAnalysisResultAsync(default).ConfigureAwait(false); analysisResults.GetAllDiagnostics().Check(TestContext.Out, out int analyzerErrors, out int analzyerWarnings); Assert.That(analyzerErrors, Is.Zero, $"Analyzers produced {analyzerErrors} errors"); TestContext.Out.WriteLine($"Analyzers produced {analzyerWarnings} warnings"); } Assert.That( success, Is.True, $"Compilation failed with {errorCount} errors and {warnCount} warnings."); xmlStream.Position = 0; var xmlDoc = XDocument.Load(xmlStream); Assert.That(xmlDoc, Is.Not.Null); } [Theory] public void GenerateFromDesignFileWithCsharp8(string modelDesignFile) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); Dictionary generatedText = GenerateCodeFromModel( modelDesignFile, LanguageVersion.CSharp8, telemetry, out _); Assert.That(generatedText, Is.Not.Empty); } /// /// Verifies that DataType structure fields with /// ValueRank="OneOrMoreDimensions" are generated as typed /// MatrixOf<T> properties and that the encode/decode /// pipeline writes the inline matrix of OPC 10000-6 5.2.5 with the /// calls the DataTypeDefinition driven Structure codec makes: /// dedicated WriteEncodeableMatrix / ReadEncodeableMatrix /// for concrete encodeable matrices and WriteVariantValue / /// ReadVariantValue with the field's type info (no Variant /// framing) together with the matching Variant.GetXxxMatrix /// getters for everything else. /// [Test] public void GenerateMatrixValueDataType_EmitsMatrixOfPropertiesAndCalls() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); Dictionary generatedText = GenerateCodeFromModel( "TestDataDesign.xml", LanguageVersion.CSharp11, telemetry, out _); // Concatenate every generated source file so the snippet match // is resilient to whichever file the generator chooses to put // the type in (each pipeline groups types differently). string code = string.Join("\n", generatedText.Values); Assert.That(code, Does.Contain("class MatrixValueDataType"), "Expected MatrixValueDataType to be generated."); // Property type assertions - each matrix field must render as a // typed MatrixOf in the public surface. Assert.Multiple(() => { Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf BooleanMatrix")); Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf Int32Matrix")); Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf StringMatrix")); Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf NodeIdMatrix")); Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf VariantMatrix")); Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf ExtensionObjectMatrix")); Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf VectorMatrix")); // AllowSubTypes structure fields collapse to ExtensionObject // during DataTypeNode resolution, so the property emits the // matrix-of-extension-object surface even though the design // declared the element type as Vector. Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf AbstractVectorMatrix")); // Typed enum matrix surface — fully qualified to the // TestData namespace where HeaterStatus is generated. Assert.That(code, Does.Contain( "global::Opc.Ua.MatrixOf HeaterStatusMatrix")); }); // Encode assertions: matrix fields are written as the inline // matrix of OPC 10000-6 5.2.5, the same calls the // DataTypeDefinition driven Structure codec makes. Assert.Multiple(() => { // Concrete encodeable matrix uses the typed // WriteEncodeableMatrix overload. Assert.That(code, Does.Contain( """encoder.WriteEncodeableMatrix("VectorMatrix", VectorMatrix);""")); Assert.That(code, Does.Not.Match(@"encoder\.WriteVariant\(""\w*Matrix"""), "No matrix field may be framed as a Variant."); foreach (string name in s_variantValueMatrixFields) { Assert.That(code, Does.Contain( $"global::Opc.Ua.EncoderExtensions.WriteInlineMatrixValue(encoder, \"{name}\", global::Opc.Ua.Variant.From({name}));")); } }); // Decode assertions: read with the field's type info, like // Structure.DecodeProperty does. Assert.Multiple(() => { Assert.That(code, Does.Contain( """VectorMatrix = decoder.ReadEncodeableMatrix("VectorMatrix");""")); Assert.That(code, Does.Not.Match(@"decoder\.ReadVariant\(""\w*Matrix"""), "No matrix field may be read as a Variant."); Assert.That(code, Does.Contain(ReadMatrix("BooleanMatrix", "Boolean", "GetBooleanMatrix()"))); Assert.That(code, Does.Contain(ReadMatrix("Int32Matrix", "Int32", "GetInt32Matrix()"))); Assert.That(code, Does.Contain(ReadMatrix("StringMatrix", "String", "GetStringMatrix()"))); Assert.That(code, Does.Contain(ReadMatrix("NodeIdMatrix", "NodeId", "GetNodeIdMatrix()"))); Assert.That(code, Does.Contain(ReadMatrix("VariantMatrix", "Variant", "GetVariantMatrix()"))); Assert.That(code, Does.Contain( ReadMatrix("ExtensionObjectMatrix", "ExtensionObject", "GetExtensionObjectMatrix()"))); Assert.That(code, Does.Contain( ReadMatrix("AbstractVectorMatrix", "ExtensionObject", "GetExtensionObjectMatrix()"))); // Typed enum matrix decodes through GetEnumerationMatrix // (the EnumerationBuilder-backed Variant getter). Assert.That(code, Does.Contain( ReadMatrix( "HeaterStatusMatrix", "Enumeration", "GetEnumerationMatrix()"))); }); static string ReadMatrix(string name, string builtInType, string getter) { return $"{name} = decoder.ReadVariantValue(\"{name}\", global::Opc.Ua.TypeInfo.Create(" + $"global::Opc.Ua.BuiltInType.{builtInType}, global::Opc.Ua.ValueRanks.TwoDimensions)).{getter};"; } } private static readonly string[] s_variantValueMatrixFields = [ "BooleanMatrix", "Int32Matrix", "StringMatrix", "NodeIdMatrix", "VariantMatrix", "ExtensionObjectMatrix", // AllowSubTypes -> field resolves to ExtensionObject. "AbstractVectorMatrix", // Typed enum matrix -> Variant.From(MatrixOf). "HeaterStatusMatrix" ]; private static Dictionary GenerateCodeFromModel( string modelDesignFile, LanguageVersion languageVersion, ITelemetryContext telemetry, out Dictionary nonSourceCode) { // Generate var sw = Stopwatch.StartNew(); using var fileSystem = new VirtualFileSystem(); Generators.GenerateCode(new DesignFileCollection { Targets = [Path.Combine( Directory.GetCurrentDirectory(), "Resources", modelDesignFile)], IdentifierFilePath = Path.Combine( Directory.GetCurrentDirectory(), "Resources", Path.GetFileNameWithoutExtension(modelDesignFile) + ".csv"), Options = new DesignFileOptions() }, fileSystem, string.Empty, telemetry, new GeneratorOptions { UseUtf8StringLiterals = languageVersion >= LanguageVersion.CSharp11, // The test compilation provides Core stubs via // WithOpcUaCoreStubs() but no Opc.Ua.Server reference. // Suppress fluent-builder emission to keep the generated // code self-contained (mirrors model-only csproj // configuration in production: Opc.Ua.Di, // Opc.Ua.Gds, Opc.Ua.WotCon all opt out via // ModelSourceGeneratorOmitFluentApi=true). OmitFluentApi = true, OmitEventRecords = true }); var generatedText = fileSystem.CreatedFiles .Where(c => Path.GetExtension(c) == ".cs") .ToDictionary(c => c, c => Encoding.UTF8.GetString(fileSystem.Get(c))); TestContext.Out.WriteLine("Generation completed in {0} ms", sw.ElapsedMilliseconds); Assert.That(generatedText.Values, Is.All.StartsWith("// ")); nonSourceCode = fileSystem.CreatedFiles .Where(c => Path.GetExtension(c) != ".cs") .ToDictionary(c => c, c => Encoding.UTF8.GetString(fileSystem.Get(c))); return generatedText; } private static void AddPredefinedNodeLoader(Dictionary generated) { generated.Add("Test.cs", """ namespace Opc.Ua { public static partial class LoadingTestData { public static NodeStateCollection Load() { // Use predefined nodes return new NodeStateCollection().AddOpcUa(new SystemContext(null)); } } } """); } } }