Files
UA-.NETStandard/tests/Opc.Ua.SourceGeneration.Tests/ModelGeneratorTests.cs
Marc Schier 12ebfba603 Add ISA95 JobControl and ManagedSession regression coverage (#3998)
## Summary

- Adds the official ISA95 JobControl 2.0.0 NodeSet2 as a complete
source-generator regression fixture.
- Extends the lightweight generated-stack test stub with
`NamespaceMetadataState`, required by the complete model.
- Adds a deterministic ManagedSession cancellation regression proving
that a started connect attempt is stopped.

The ManagedSession cleanup implementation and Quickstarts utility
coverage now come from `master`; this PR retains the additional
deterministic cleanup regression.

## ISA95 fixture provenance and integrity

The fixture is byte-for-byte the official file:

- Source:
`OPCFoundation/UA-Nodeset@e0b5d80cfff698f0276393e28df138a7fbc4b541`
- Path: `ISA95-JOBCONTROL/opc.ua.isa95-jobcontrol.nodeset2.xml`
- Git blob: `82f008cd126ade54e5f2c737bcc116621a8c3087`
- SHA-256:
`52327e736beb604c7253a5a5393d1ad525b9ecf35b71fbacb4f429cacb334064`

The fixture test retains the OPC Foundation MIT header and verifies the
2.0.0 model/date, 258 nodes (including 134 variables), representative
late variables, and that every `ns=1` reference target resolves locally.

## Validation

- ManagedSession tests: 17 passing on net10.0; 15 passing and 2
framework-gated skips on net48.
- Quickstarts server utility tests: 3 passing on net10.0 and net48.
- ISA95 generator and fixture integrity regression: 5 passing on
net10.0.
2026-07-17 13:19:00 +02:00

694 lines
33 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Framework;
namespace Opc.Ua.SourceGeneration
{
/// <summary>
/// Test generation and compilation
/// </summary>
[TestFixture]
[Category("SourceGeneration")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
// [Parallelizable(ParallelScope.All)]
public class ModelGeneratorTests
{
[DatapointSource]
public OptimizationLevel[] OptimizationLevels =
CompilerUtils.SupportedOptimizationLevels;
[DatapointSource]
public LanguageVersion[] LanguageVersions =
CompilerUtils.SupportedLanguageVersions;
[Theory]
public void GenerateAndCompileDemoModelXmlTest(
LanguageVersion languageVersion,
OptimizationLevel optimizationLevel)
{
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = optimizationLevel.CreateCompilation()
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
["build_property.ModelSourceGeneratorVersion"] = "v105",
["build_property.ModelSourceGeneratorExclude"] = "Draft",
["build_property.ModelSourceGeneratorUseAllowSubtypes"] = "true",
// The test compilation includes Core stubs via
// WithOpcUaGeneratedStack() but no Server reference.
// Suppress fluent-builder emission so the generated
// code compiles standalone (matches model-only
// production csprojs).
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
// Create the driver the executes the generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
.AddAdditionalTexts([EmbeddedText.From("DemoModel.xml")])
.WithUpdatedAnalyzerConfigOptions(options)
;
GeneratorRunResult generatorResult = GenerateAndCompile(driver, compilation);
Assert.That(generatorResult.GeneratedSources, Has.Length.EqualTo(9));
}
[Theory]
public void GenerateAndCompileDemoModelNodeSetsTest(
LanguageVersion languageVersion,
OptimizationLevel optimizationLevel)
{
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = optimizationLevel.CreateCompilation()
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
["build_property.ModelSourceGeneratorStartId"] = "1000",
// The test compilation includes Core stubs but no
// Server reference. Suppress fluent-builder
// emission so the generated code compiles standalone.
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
// Create the driver the executes the generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
.AddAdditionalTexts([
EmbeddedText.From("DemoModel.NodeSet2.xml"),
EmbeddedText.From("Opc.Ua.Di.NodeSet2.xml")
])
.WithUpdatedAnalyzerConfigOptions(options)
;
GeneratorRunResult generatorResult = GenerateAndCompile(driver, compilation);
// 19 generated files: 9 per model (Constants, DataTypes,
// Identifiers, ModelDependencies, NodeStates, NodeStates.ex,
// NodeStates.i, TypeProxies, XmlSchemas) for DemoModel + 9
// for DI, plus 1 DI.StateMachineIds.g.cs (DI declares Part
// 16 FSM subtypes for the software-update facet; DemoModel
// declares none, so it gets no StateMachineIds output).
Assert.That(generatorResult.GeneratedSources, Has.Length.EqualTo(19));
}
[Theory]
public void GenerateAndCompileIsa95JobControlNodeSet2Test(
LanguageVersion languageVersion)
{
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
.AddAdditionalTexts([EmbeddedText.From("Isa95JobControl.NodeSet2.xml")])
.WithUpdatedAnalyzerConfigOptions(options);
GeneratorRunResult generatorResult = GenerateAndCompile(driver, compilation);
GeneratedSourceResult dataTypesSource = generatorResult.GeneratedSources.Single(
source => source.HintName.EndsWith(
".DataTypes.g.cs",
StringComparison.Ordinal));
Assert.That(
dataTypesSource.SourceText.ToString(),
Does.Contain("ISA95JobOrderDataType"));
}
[Test]
public void Isa95JobControlNodeSet2FixtureIsComplete()
{
string nodeSet = EmbeddedText.From("Isa95JobControl.NodeSet2.xml")
.GetText()!
.ToString();
Assert.That(nodeSet, Does.Contain("OPC Foundation MIT License 1.00"));
XDocument document = XDocument.Parse(nodeSet);
XNamespace ua = "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd";
XElement root = document.Root!;
XElement[] models = [.. root.Element(ua + "Models")!.Elements(ua + "Model")];
Assert.That(models, Has.Length.EqualTo(1));
Assert.That(
models[0].Attribute("ModelUri")!.Value,
Is.EqualTo("http://opcfoundation.org/UA/ISA95-JOBCONTROL_V2/"));
Assert.That(models[0].Attribute("Version")!.Value, Is.EqualTo("2.0.0"));
Assert.That(
models[0].Attribute("PublicationDate")!.Value,
Is.EqualTo("2024-01-31T00:00:00Z"));
XElement[] nodes = [.. root.Elements().Where(element =>
element.Name.LocalName.StartsWith("UA", StringComparison.Ordinal))];
Assert.That(nodes, Has.Length.EqualTo(258));
Assert.That(root.Elements(ua + "UADataType").Count(), Is.EqualTo(11));
Assert.That(root.Elements(ua + "UAObjectType").Count(), Is.EqualTo(8));
Assert.That(root.Elements(ua + "UAVariable").Count(), Is.EqualTo(134));
Assert.That(root.Elements(ua + "UAObject").Count(), Is.EqualTo(91));
Assert.That(root.Elements(ua + "UAMethod").Count(), Is.EqualTo(14));
Assert.That(
root.Elements(ua + "UAVariable").Any(variable =>
variable.Attribute("NodeId")!.Value == "ns=1;i=6088" &&
variable.Attribute("BrowseName")!.Value == "1:MaxDownloadableJobOrders"),
Is.True);
Assert.That(
root.Elements(ua + "UAVariable").Any(variable =>
variable.Attribute("NodeId")!.Value == "ns=1;i=6029" &&
variable.Attribute("BrowseName")!.Value == "StaticStringNodeIdPattern"),
Is.True);
var nodeIds = new HashSet<string>(
nodes.Select(node => node.Attribute("NodeId")!.Value),
StringComparer.Ordinal);
string[] unresolvedSameNamespaceReferences =
[
.. document.Descendants(ua + "Reference")
.Select(reference => reference.Value.Trim())
.Where(reference =>
reference.StartsWith("ns=1;", StringComparison.Ordinal) &&
!nodeIds.Contains(reference))
.Distinct(StringComparer.Ordinal)
];
Assert.That(unresolvedSameNamespaceReferences, Is.Empty);
}
[Theory]
public void GenerateAndCompileTestDataDesignTest(
LanguageVersion languageVersion)
{
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
["build_property.ModelSourceGeneratorVersion"] = "v105",
["build_property.ModelSourceGeneratorExclude"] = "Draft",
["build_property.ModelSourceGeneratorUseAllowSubtypes"] = "true",
// The test compilation includes Core stubs but no
// Server reference. Suppress fluent-builder
// emission so the generated code compiles standalone.
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
// Create the driver that executes the generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
.AddAdditionalTexts(
[
EmbeddedText.From("TestDataDesign.xml"),
EmbeddedText.From("TestDataDesign.csv")
])
.WithUpdatedAnalyzerConfigOptions(options)
;
// There will be 120 errors due to missing Opc.Ua dll reference
GeneratorRunResult generatorResult = GenerateAndCompile(driver, compilation);
// Adding the HeaterStatus enum (used by HeaterStatusMatrix in
// MatrixValueDataType to exercise the typed-enum matrix codegen
// path) caused the generator to emit one additional output file
// for TestDataDesign, bringing the total from 8 to 9 (the same
// per-model count that DemoModel produces above).
Assert.That(generatorResult.GeneratedSources, Has.Length.EqualTo(9));
string testDataXmlSchema = ValidateXmlSchema(languageVersion, generatorResult);
Assert.That(testDataXmlSchema,
Does.Contain("<ua:Model ModelUri=\"http://test.org/UA/Data/\" Version=\"1.0.0\""));
}
[Theory]
public void GenerateAndCompileModelDesignReferencingNodeSet2TypesTest(
LanguageVersion languageVersion)
{
// Regression test for issue #3937: a ModelDesign describing an
// object instance (CrossModelInstances.ModelDesign.xml) whose
// TypeDefinition references an ObjectType defined in a separate
// NodeSet2 AdditionalFile (CrossModelTypes.NodeSet2.xml). The
// ModelDesign generation pass must receive the NodeSet2 input as a
// dependency so the cross-model type reference resolves; otherwise
// generation fails with MODELGEN003 ("The TypeDefinition reference
// for node Widget1 is not the expected type: ObjectTypeDesign.").
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
// The test compilation includes Core stubs but no Server
// reference. Suppress fluent-builder emission so the
// generated code compiles standalone.
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
// Per-file metadata: the NodeSet2 declares the type model; the
// ModelDesign declares the instance model that references it.
options.TextOptions["CrossModelTypes.NodeSet2.xml"] =
new Dictionary<string, string>
{
["build_metadata.AdditionalFiles.ModelSourceGeneratorModelUri"] =
"http://test.org/UA/CrossModel/Types",
["build_metadata.AdditionalFiles.ModelSourceGeneratorName"] = "CrossModelTypes",
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "CrossModelTypes"
};
options.TextOptions["CrossModelInstances.ModelDesign.xml"] =
new Dictionary<string, string>
{
["build_metadata.AdditionalFiles.ModelSourceGeneratorModelUri"] =
"http://test.org/UA/CrossModel/Instances"
};
// Create the driver that executes the generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
.AddAdditionalTexts(
[
EmbeddedText.From("CrossModelTypes.NodeSet2.xml"),
EmbeddedText.From("CrossModelInstances.ModelDesign.xml")
])
.WithUpdatedAnalyzerConfigOptions(options)
;
// Before the fix the ModelDesign pass could not resolve the
// NodeSet2-defined type and generation failed with MODELGEN003;
// after the fix generation completes and the generated code compiles.
GeneratorRunResult generatorResult = GenerateAndCompile(driver, compilation);
// The instance output must reference the type class generated from
// the NodeSet2 input. A resolved cross-model reference proves the
// NodeSet2 input was wired into the ModelDesign pass as a dependency.
string allSources = string.Join(
"\n",
generatorResult.GeneratedSources.Select(s => s.SourceText.ToString()));
Assert.That(allSources,
Does.Contain("CrossModelTypes.WidgetState"),
"instance output should reference the generated NodeSet2 type class");
}
[Theory]
public void GenerateAndCompileModelDesignReferencingNodeSet2TypesReversedInputOrderTest(
LanguageVersion languageVersion)
{
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
options.TextOptions["CrossModelTypes.NodeSet2.xml"] =
new Dictionary<string, string>
{
["build_metadata.AdditionalFiles.ModelSourceGeneratorModelUri"] =
"http://test.org/UA/CrossModel/Types",
["build_metadata.AdditionalFiles.ModelSourceGeneratorName"] = "CrossModelTypes",
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "CrossModelTypes"
};
options.TextOptions["CrossModelInstances.ModelDesign.xml"] =
new Dictionary<string, string>
{
["build_metadata.AdditionalFiles.ModelSourceGeneratorModelUri"] =
"http://test.org/UA/CrossModel/Instances"
};
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
// Ensure resolution is independent of AdditionalFiles order.
.AddAdditionalTexts(
[
EmbeddedText.From("CrossModelInstances.ModelDesign.xml"),
EmbeddedText.From("CrossModelTypes.NodeSet2.xml")
])
.WithUpdatedAnalyzerConfigOptions(options)
;
GeneratorRunResult generatorResult = GenerateAndCompile(driver, compilation);
string allSources = string.Join(
"\n",
generatorResult.GeneratedSources.Select(s => s.SourceText.ToString()));
Assert.That(allSources,
Does.Contain("CrossModelTypes.WidgetState"),
"cross-model type resolution should not depend on file order");
}
[Theory]
public void NodeManagerBoundToNodeSet2TypesIsNotReportedUnmatchedWithModelDesign(
LanguageVersion languageVersion)
{
// Regression for issue #3937 (follow-up comment): a [NodeManager]
// bound to the NodeSet2 *types* model must not be reported as
// unmatched (MODELGEN010) merely because the project also contains
// a ModelDesign *instances* model. Binding resolution runs in two
// passes; the NodeSet2 pass matches the binding, so the ModelDesign
// pass must not false-positive it. Before the fix the ModelDesign
// pass reported the binding it never saw as unmatched, which — with
// TreatWarningsAsErrors — blocked the build.
const string bindingSource =
"""
namespace Opc.Ua.Server.Fluent
{
public sealed class NodeManagerAttribute : global::System.Attribute
{
public string NamespaceUri { get; set; }
public string Design { get; set; }
public bool GenerateFactory { get; set; }
}
}
namespace CrossModelConsumer
{
[global::Opc.Ua.Server.Fluent.NodeManager(NamespaceUri = "http://test.org/UA/CrossModel/Types")]
public partial class TypesNodeManager
{
}
}
""";
(ImmutableArray<Diagnostic> diagnostics, GeneratorDriverRunResult runResult) =
RunMixedModelGenerator(languageVersion, bindingSource);
Assert.That(
diagnostics.Where(d => d.Id == "MODELGEN010"),
Is.Empty,
"a [NodeManager] matched by the NodeSet2 pass must not be " +
"reported as unmatched by the ModelDesign pass");
string generated = string.Join(
"\n",
runResult.Results[0].GeneratedSources.Select(s => s.SourceText.ToString()));
Assert.That(
generated,
Does.Contain("class TypesNodeManager"),
"the matched [NodeManager] should generate a node manager partial");
}
[Theory]
public void NodeManagerBoundToModelDesignInstancesIsNotReportedUnmatchedWithNodeSet2(
LanguageVersion languageVersion)
{
// Symmetric case: a [NodeManager] bound to the ModelDesign
// *instances* model must not be reported unmatched by the NodeSet2
// pass. The ModelDesign pass matches it; aggregate reporting keeps
// it silent.
const string bindingSource =
"""
namespace Opc.Ua.Server.Fluent
{
public sealed class NodeManagerAttribute : global::System.Attribute
{
public string NamespaceUri { get; set; }
public string Design { get; set; }
public bool GenerateFactory { get; set; }
}
}
namespace CrossModelConsumer
{
[global::Opc.Ua.Server.Fluent.NodeManager(NamespaceUri = "http://test.org/UA/CrossModel/Instances")]
public partial class InstancesNodeManager
{
}
}
""";
(ImmutableArray<Diagnostic> diagnostics, GeneratorDriverRunResult runResult) =
RunMixedModelGenerator(languageVersion, bindingSource);
Assert.That(
diagnostics.Where(d => d.Id == "MODELGEN010"),
Is.Empty,
"a [NodeManager] matched by the ModelDesign pass must not be " +
"reported as unmatched by the NodeSet2 pass");
string generated = string.Join(
"\n",
runResult.Results[0].GeneratedSources.Select(s => s.SourceText.ToString()));
Assert.That(
generated,
Does.Contain("class InstancesNodeManager"),
"the matched [NodeManager] should generate a node manager partial");
}
[Theory]
public void SelectorlessNodeManagerAcrossTwoModelsReportsSingleAmbiguity(
LanguageVersion languageVersion)
{
// A [NodeManager] with no NamespaceUri/Design selector cannot bind
// when the project has multiple models. It must be reported exactly
// once (aggregated across both passes, not once per pass) with an
// ambiguity message that guides the user to add a NamespaceUri.
const string bindingSource =
"""
namespace Opc.Ua.Server.Fluent
{
public sealed class NodeManagerAttribute : global::System.Attribute
{
public string NamespaceUri { get; set; }
public string Design { get; set; }
public bool GenerateFactory { get; set; }
}
}
namespace CrossModelConsumer
{
[global::Opc.Ua.Server.Fluent.NodeManager]
public partial class AmbiguousNodeManager
{
}
}
""";
(ImmutableArray<Diagnostic> diagnostics, _) =
RunMixedModelGenerator(languageVersion, bindingSource);
Diagnostic[] unmatched = [.. diagnostics.Where(d => d.Id == "MODELGEN010")];
Assert.That(
unmatched,
Has.Length.EqualTo(1),
"a selector-less binding must be reported once, not once per pass");
Assert.That(
unmatched[0].GetMessage(System.Globalization.CultureInfo.InvariantCulture),
Does.Contain("multiple models"),
"the diagnostic should explain the ambiguity");
}
/// <summary>
/// Run the model generator over the shared cross-model NodeSet2 +
/// ModelDesign fixture plus a user-supplied source containing a
/// <c>[NodeManager]</c> attribute, and return the generator
/// diagnostics and run result. The output is intentionally not
/// strictly compiled: a matched <c>[NodeManager]</c> emits Fluent
/// node-manager code that references <c>Opc.Ua.Server</c> types not
/// present in this model-only test compilation.
/// </summary>
private static (ImmutableArray<Diagnostic> Diagnostics, GeneratorDriverRunResult RunResult)
RunMixedModelGenerator(LanguageVersion languageVersion, string bindingSource)
{
var generator = new ModelSourceGenerator();
var host = new ModelSourceGeneratorHoist(generator);
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
.AddCode(new Dictionary<string, string>
{
["NodeManagerBinding.cs"] = bindingSource
}.WithOpcUaGeneratedStack(), languageVersion);
var options = new AnalyzerOptionsProvider(
new Dictionary<string, string>
{
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
});
options.TextOptions["CrossModelTypes.NodeSet2.xml"] =
new Dictionary<string, string>
{
["build_metadata.AdditionalFiles.ModelSourceGeneratorModelUri"] =
"http://test.org/UA/CrossModel/Types",
["build_metadata.AdditionalFiles.ModelSourceGeneratorName"] = "CrossModelTypes",
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "CrossModelTypes"
};
options.TextOptions["CrossModelInstances.ModelDesign.xml"] =
new Dictionary<string, string>
{
["build_metadata.AdditionalFiles.ModelSourceGeneratorModelUri"] =
"http://test.org/UA/CrossModel/Instances"
};
GeneratorDriver driver = CSharpGeneratorDriver.Create(host)
.WithUpdatedParseOptions(new CSharpParseOptions()
.WithKind(SourceCodeKind.Regular)
.WithLanguageVersion(languageVersion))
.AddAdditionalTexts(
[
EmbeddedText.From("CrossModelTypes.NodeSet2.xml"),
EmbeddedText.From("CrossModelInstances.ModelDesign.xml")
])
.WithUpdatedAnalyzerConfigOptions(options);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out Compilation _,
out ImmutableArray<Diagnostic> diagnostics);
return (diagnostics, driver.GetRunResult());
}
private static string ValidateXmlSchema(
LanguageVersion languageVersion,
GeneratorRunResult generatorResult)
{
// Get the XmlSchemas.g.cs generated source which is what we care about
var xmlSchemaSource = generatorResult.GeneratedSources
.Where(s => s.HintName.EndsWith("XmlSchemas.g.cs", StringComparison.Ordinal))
.ToList();
Assert.That(xmlSchemaSource, Has.Count.EqualTo(1));
// Parse the generated source and verify it contains expected schema
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(xmlSchemaSource[0].SourceText);
SyntaxNode root = syntaxTree.GetRoot();
var classNodes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Identifier.Text == "XmlSchemas")
.ToList();
Assert.That(classNodes, Has.Count.EqualTo(1));
ClassDeclarationSyntax classNode = classNodes[0];
var properrtyNodes = classNode.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Where(f => f.Identifier.Text == "TypesXsd")
.ToList();
Assert.That(properrtyNodes, Has.Count.EqualTo(1));
PropertyDeclarationSyntax propertyNode = properrtyNodes[0];
LiteralExpressionSyntax stringLiteral = propertyNode.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.FirstOrDefault();
Assert.That(stringLiteral, Is.Not.Null);
// Verify that the getter contains the expected schema string
var stringTokens = stringLiteral.ChildTokens().ToList();
Assert.That(stringTokens, Has.Count.EqualTo(1));
string stringLiteralText = stringTokens[0].ToFullString().Trim();
if (stringLiteralText.EndsWith("u8", StringComparison.Ordinal))
{
Assert.That(languageVersion, Is.GreaterThanOrEqualTo(LanguageVersion.CSharp11));
#pragma warning disable IDE0057 // No range available
stringLiteralText = stringLiteralText.Substring(0, stringLiteralText.Length - 2);
#pragma warning restore IDE0057 // No range available
}
stringLiteralText = stringLiteralText.Trim('"');
if (languageVersion < LanguageVersion.CSharp11)
{
// Base 64 encoded string is split into multiple lines
return Encoding.UTF8.GetString(
Convert.FromBase64String(stringLiteralText));
}
return stringLiteralText;
}
private static GeneratorRunResult GenerateAndCompile(
GeneratorDriver driver,
CSharpCompilation compilation)
{
// Run it
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out Compilation outputCompilation,
out ImmutableArray<Diagnostic> diagnostics);
Assert.That(diagnostics, Is.Empty);
Assert.That(outputCompilation.SyntaxTrees.Count(), Is.GreaterThan(1));
outputCompilation.GetDiagnostics().Check(
TestContext.Out,
out int errors,
out int warnings);
Assert.That(errors, Is.Zero, $"Compilation produced {errors} errors");
#if NETFRAMEWORK
TestContext.Out.WriteLine($"Compilation produced {warnings} warnings");
#else
Assert.That(warnings, Is.Zero, $"Compilation produced {warnings} warnings");
#endif
// Get the results
GeneratorDriverRunResult runResult = driver.GetRunResult();
// Test the results
Assert.That(runResult.GeneratedTrees, Is.Not.Empty);
runResult.Diagnostics.Check(TestContext.Out, out errors, out warnings);
Assert.That(errors, Is.Zero);
TestContext.Out.WriteLine($"Run result produced {warnings} warnings");
GeneratorRunResult generatorResult = runResult.Results[0];
generatorResult.Diagnostics.Check(TestContext.Out, out errors, out warnings);
Assert.That(errors, Is.Zero);
TestContext.Out.WriteLine($"Generate run produced {warnings} warnings");
Assert.That(generatorResult.Exception, Is.Null);
return generatorResult;
}
}
}