mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 15:22:15 +08:00
9b034f8291
# 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>
1220 lines
56 KiB
C#
1220 lines
56 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.SourceGeneration.Dependency;
|
|
|
|
namespace Opc.Ua.SourceGeneration
|
|
{
|
|
/// <summary>
|
|
/// Tests for <see cref="ReferencedModelDependencyScanner"/> and the
|
|
/// generator-side override resolution + transitive dependency
|
|
/// suppression triggered by referenced assemblies.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("SourceGeneration")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
public class ModelDependencyScannerTests
|
|
{
|
|
/// <summary>
|
|
/// Regression: the referenced-model pick compared versions with the
|
|
/// non-transitive CompareVersionStrings (a date against a version is
|
|
/// equal) and broke the tie on the publication date, so three
|
|
/// assemblies formed a cycle and the winner depended on the reference
|
|
/// order. It must agree with NodesetFileCollection's total order.
|
|
/// </summary>
|
|
[Test]
|
|
public void ReferencedModelPickDoesNotDependOnReferenceOrder()
|
|
{
|
|
const string uri = "http://test.org/UA/Cycle/";
|
|
var x = new ModelDependencyReference("X", uri, "X", "1.0.2", "2019-01-01");
|
|
var y = new ModelDependencyReference("Y", uri, "Y", "2020-06-01", "2020-06-01");
|
|
var z = new ModelDependencyReference("Z", uri, "Z", "1.0.1", "2021-01-01");
|
|
ModelDependencyReference[] references = [x, y, z];
|
|
|
|
var winners = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (int[] order in new[]
|
|
{
|
|
new[] { 0, 1, 2 }, new[] { 0, 2, 1 }, new[] { 1, 0, 2 },
|
|
new[] { 1, 2, 0 }, new[] { 2, 0, 1 }, new[] { 2, 1, 0 }
|
|
})
|
|
{
|
|
ModelDependencyReference winner = references[order[0]];
|
|
for (int i = 1; i < order.Length; i++)
|
|
{
|
|
ModelDependencyReference candidate = references[order[i]];
|
|
if (ModelCompilation.CompareReferencedModels(candidate, winner) > 0)
|
|
{
|
|
winner = candidate;
|
|
}
|
|
}
|
|
winners.Add(winner.AssemblyName);
|
|
}
|
|
|
|
Assert.That(winners, Is.EquivalentTo(s_onlyX));
|
|
}
|
|
|
|
[Test]
|
|
public void ScanReturnsEmptyWhenAttributeTypeNotFound()
|
|
{
|
|
var compilation = CSharpCompilation.Create("Empty",
|
|
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
|
|
|
ImmutableArray<ModelDependencyReference> result =
|
|
ReferencedModelDependencyScanner.Scan(compilation);
|
|
|
|
Assert.That(result, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void ScanReturnsEmptyWhenNullCompilation()
|
|
{
|
|
ImmutableArray<ModelDependencyReference> result =
|
|
ReferencedModelDependencyScanner.Scan(null!);
|
|
|
|
Assert.That(result, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void ScanReadsAttributesFromReferencedCompilation()
|
|
{
|
|
CSharpCompilation producer = OptimizationLevel.Release.CreateCompilation("Producer")
|
|
.AddCode(new Dictionary<string, string>
|
|
{
|
|
["AssemblyAttributes.cs"] =
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute(" +
|
|
"\"urn:test:Producer\", \"TestProducer\", \"1.0\", \"2024-06-01\")]" +
|
|
Environment.NewLine +
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute(" +
|
|
"\"urn:test:Upstream\", \"TestUpstream\", null, null)]"
|
|
}, LanguageVersion.CSharp11);
|
|
|
|
ImmutableArray<Diagnostic> diags = producer.GetDiagnostics();
|
|
Assert.That(diags.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty, "Producer compilation must compile");
|
|
|
|
CSharpCompilation consumer = OptimizationLevel.Release.CreateCompilation("Consumer")
|
|
.AddReferences(producer.ToMetadataReference());
|
|
|
|
ImmutableArray<ModelDependencyReference> result =
|
|
ReferencedModelDependencyScanner.Scan(consumer);
|
|
|
|
Assert.That(result, Has.Length.EqualTo(2));
|
|
ModelDependencyReference producerEntry = result
|
|
.Single(r => r.ModelUri == "urn:test:Producer");
|
|
Assert.That(producerEntry.Prefix, Is.EqualTo("TestProducer"));
|
|
Assert.That(producerEntry.Version, Is.EqualTo("1.0"));
|
|
Assert.That(producerEntry.PublicationDate, Is.EqualTo("2024-06-01"));
|
|
Assert.That(producerEntry.AssemblyName, Is.EqualTo("Producer"));
|
|
Assert.That(producerEntry.IsValid, Is.True);
|
|
|
|
ModelDependencyReference upstream = result
|
|
.Single(r => r.ModelUri == "urn:test:Upstream");
|
|
Assert.That(upstream.Prefix, Is.EqualTo("TestUpstream"));
|
|
Assert.That(upstream.Version, Is.Empty);
|
|
Assert.That(upstream.PublicationDate, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void ScanIgnoresAttributesWithEmptyUriOrPrefix()
|
|
{
|
|
CSharpCompilation producer = OptimizationLevel.Release.CreateCompilation("Producer")
|
|
.AddCode(new Dictionary<string, string>
|
|
{
|
|
["AssemblyAttributes.cs"] =
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute(" +
|
|
"\"\", \"NoUri\", null, null)]" +
|
|
Environment.NewLine +
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute(" +
|
|
"\"urn:test:NoPrefix\", \"\", null, null)]"
|
|
}, LanguageVersion.CSharp11);
|
|
|
|
CSharpCompilation consumer = OptimizationLevel.Release.CreateCompilation("Consumer")
|
|
.AddReferences(producer.ToMetadataReference());
|
|
|
|
ImmutableArray<ModelDependencyReference> result =
|
|
ReferencedModelDependencyScanner.Scan(consumer);
|
|
|
|
Assert.That(result, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void EmittedAssemblyContainsModelDependencyAttribute()
|
|
{
|
|
var generator = new ModelSourceGenerator();
|
|
|
|
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
|
|
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(),
|
|
LanguageVersion.CSharp11);
|
|
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorVersion"] = "v105",
|
|
["build_property.ModelSourceGeneratorExclude"] = "Draft"
|
|
});
|
|
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([EmbeddedText.From("DemoModel.xml")])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out _,
|
|
out _);
|
|
|
|
GeneratorDriverRunResult runResult = driver.GetRunResult();
|
|
GeneratorRunResult generatorResult = runResult.Results[0];
|
|
|
|
GeneratedSourceResult dependencyFile = generatorResult.GeneratedSources
|
|
.Single(s => s.HintName.EndsWith(
|
|
".ModelDependencies.g.cs", StringComparison.Ordinal));
|
|
string text = dependencyFile.SourceText.ToString();
|
|
|
|
Assert.That(text, Does.StartWith("\uFEFF// <auto-generated />")
|
|
.Or.StartWith("// <auto-generated />"));
|
|
Assert.That(text, Does.Contain(
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute("));
|
|
Assert.That(text, Does.Contain("\"urn:opcfoundation.org:2024-01:DemoModel\""));
|
|
Assert.That(text, Does.Contain("\"DemoModel\""));
|
|
// OpcUa namespace is implicit; must not be re-emitted as a dependency.
|
|
Assert.That(text, Does.Not.Contain("\"http://opcfoundation.org/UA/\""));
|
|
}
|
|
|
|
[Test]
|
|
public void DeclarationBackedMethodIdentityCompilesAcrossAssemblies()
|
|
{
|
|
CSharpCompilation producer = GenerateModelCompilation(
|
|
"DeclarationBackedMethodProducer",
|
|
"DeclarationBackedMethod.NodeSet2.xml",
|
|
"DeclarationBackedMethod");
|
|
CSharpCompilation consumerBase =
|
|
CreateStackCompilation("DeclarationBackedMethodConsumer")
|
|
.AddReferences(producer.ToMetadataReference());
|
|
|
|
(GeneratorRunResult result, Compilation output, ImmutableArray<Diagnostic> diagnostics) = RunModelGenerator(
|
|
consumerBase,
|
|
"DeclarationBackedMethodConsumer.NodeSet2.xml",
|
|
"DeclarationBackedMethodConsumer");
|
|
|
|
ImmutableArray<Diagnostic> outputDiagnostics = output.GetDiagnostics();
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
diagnostics.Where(diagnostic =>
|
|
diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, diagnostics));
|
|
Assert.That(
|
|
outputDiagnostics.Where(diagnostic =>
|
|
diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputDiagnostics));
|
|
});
|
|
|
|
string generated = string.Join(
|
|
Environment.NewLine,
|
|
result.GeneratedSources.Select(source => source.SourceText.ToString()));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
generated,
|
|
Does.Contain(
|
|
"global::DeclarationBackedMethod.AdjustDeclarationMethodState"));
|
|
Assert.That(
|
|
generated,
|
|
Does.Not.Contain("global::DeclarationBackedMethod.AdjustMethodState"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void OverrideResolutionSilentlySkipsLocalGeneration()
|
|
{
|
|
// Arrange: a producer assembly that already declares the DemoModel
|
|
// model URI under the same prefix used by the local DemoModel.xml.
|
|
CSharpCompilation producer = OptimizationLevel.Release.CreateCompilation("Producer")
|
|
.AddCode(new Dictionary<string, string>
|
|
{
|
|
["AssemblyAttributes.cs"] =
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute(" +
|
|
"\"urn:opcfoundation.org:2024-01:DemoModel\", \"DemoModel\", " +
|
|
"\"1.0\", \"2024-01-01\")]"
|
|
}, LanguageVersion.CSharp11);
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
|
|
CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation()
|
|
.AddReferences(producer.ToMetadataReference())
|
|
.AddCode(new Dictionary<string, string>().WithOpcUaGeneratedStack(),
|
|
LanguageVersion.CSharp11);
|
|
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorVersion"] = "v105",
|
|
["build_property.ModelSourceGeneratorExclude"] = "Draft"
|
|
});
|
|
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([EmbeddedText.From("DemoModel.xml")])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out _,
|
|
out _);
|
|
|
|
GeneratorDriverRunResult runResult = driver.GetRunResult();
|
|
GeneratorRunResult generatorResult = runResult.Results[0];
|
|
|
|
// No local sources should be generated for the overridden model
|
|
// (no DataTypes / NodeIds / Constants / Schemas / etc.).
|
|
Assert.That(generatorResult.GeneratedSources, Is.Empty,
|
|
"Override resolution must silently skip local generation when " +
|
|
"a referenced assembly already declares the same model URI " +
|
|
"under the same C# prefix");
|
|
}
|
|
|
|
[Test]
|
|
public void FluentAccessorsOnlyEmitsAccessorsAgainstReferencedStateTypes()
|
|
{
|
|
CSharpCompilation producer = s_producerWithoutAccessors.Value;
|
|
(GeneratorRunResult generatorResult, Compilation outputCompilation,
|
|
ImmutableArray<Diagnostic> generatorDiagnostics) =
|
|
RunFluentAccessorsOnly(producer);
|
|
|
|
Assert.That(
|
|
generatorDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
"Fluent-accessors-only generation must not report errors.");
|
|
Assert.That(generatorResult.GeneratedSources, Has.Length.EqualTo(1));
|
|
|
|
GeneratedSourceResult source = generatorResult.GeneratedSources.Single();
|
|
string text = source.SourceText.ToString();
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(source.HintName, Does.EndWith(".FluentBuilders.g.cs"));
|
|
Assert.That(text, Does.Contain(
|
|
"public static partial class RestrictedObjectStateComponents"));
|
|
Assert.That(text, Does.Contain(
|
|
"global::DemoModel.RestrictedObjectState"));
|
|
Assert.That(text, Does.Not.Match(
|
|
@"internal\s+interface\s+I\w+NodeManagerBuilder"));
|
|
Assert.That(text, Does.Not.Match(
|
|
@"public\s+partial\s+class\s+RestrictedObjectState\b"));
|
|
Assert.That(generatorResult.GeneratedSources.Select(s => s.HintName),
|
|
Has.None.EndsWith(".Constants.g.cs"));
|
|
Assert.That(generatorResult.GeneratedSources.Select(s => s.HintName),
|
|
Has.None.EndsWith(".NodeStates.g.cs"));
|
|
Assert.That(generatorResult.GeneratedSources.Select(s => s.HintName),
|
|
Has.None.EndsWith(".ModelDependencies.g.cs"));
|
|
});
|
|
|
|
ImmutableArray<Diagnostic> outputDiagnostics = outputCompilation.GetDiagnostics();
|
|
Assert.That(
|
|
outputDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputDiagnostics));
|
|
}
|
|
|
|
[Test]
|
|
public void FluentAccessorsOnlyRejectsProducerThatAlreadyEmittedAccessors()
|
|
{
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) =
|
|
RunFluentAccessorsOnly(s_producerWithAccessors.Value);
|
|
|
|
Diagnostic diagnostic = diagnostics.Single(d => d.Id == "MODELGEN014");
|
|
string message = diagnostic.GetMessage(CultureInfo.InvariantCulture);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(result.GeneratedSources, Is.Empty);
|
|
Assert.That(message, Does.Contain(DemoModelUri));
|
|
Assert.That(message, Does.Contain("DemoModel"));
|
|
Assert.That(message,
|
|
Does.Contain("already provides generated fluent accessors"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void FluentAccessorsOnlyProviderPreventsDownstreamSecondEmission()
|
|
{
|
|
(GeneratorRunResult firstResult, Compilation firstCompilation,
|
|
ImmutableArray<Diagnostic> firstDiagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
assemblyName: "AccessorProvider");
|
|
Assert.That(
|
|
firstDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty);
|
|
Assert.That(firstResult.GeneratedSources, Has.Length.EqualTo(1));
|
|
|
|
MetadataReference accessorProvider = firstCompilation.ToMetadataReference();
|
|
CSharpCompilation probe = CreateStackCompilation("AccessorProviderProbe")
|
|
.AddReferences(accessorProvider);
|
|
ModelFluentAccessorProviderReference provider =
|
|
ReferencedFluentAccessorProviderScanner.Scan(probe)
|
|
.Single(reference => reference.ModelUri == DemoModelUri);
|
|
Assert.That(provider.Prefix, Is.EqualTo("DemoModel"));
|
|
|
|
(GeneratorRunResult secondResult, _, ImmutableArray<Diagnostic> secondDiagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
additionalReferences: [accessorProvider],
|
|
assemblyName: "DownstreamConsumer");
|
|
|
|
Diagnostic diagnostic = secondDiagnostics.Single(d => d.Id == "MODELGEN014");
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(secondResult.GeneratedSources, Is.Empty);
|
|
Assert.That(
|
|
diagnostic.GetMessage(CultureInfo.InvariantCulture),
|
|
Does.Contain("already provides generated fluent accessors"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void VendorRoboticsSubtypeExtensionCompilesAgainstReferencedModelAndAccessorProvider()
|
|
{
|
|
CSharpCompilation roboticsModel = CreateGeneratedRoboticsModelProducer();
|
|
CSharpCompilation roboticsAccessorProvider =
|
|
CreateGeneratedRoboticsAccessorProvider(roboticsModel);
|
|
|
|
CSharpCompilation vendorBase = CreateStackCompilation("VendorRobotics")
|
|
.AddReferences(
|
|
s_diProducer.Value.ToMetadataReference(),
|
|
roboticsModel.ToMetadataReference(),
|
|
roboticsAccessorProvider.ToMetadataReference());
|
|
|
|
(GeneratorRunResult result, Compilation outputCompilation,
|
|
ImmutableArray<Diagnostic> diagnostics) = RunVendorRoboticsGenerator(vendorBase);
|
|
|
|
ImmutableArray<Diagnostic> outputDiagnostics = outputCompilation.GetDiagnostics();
|
|
string generated = string.Join(
|
|
Environment.NewLine,
|
|
result.GeneratedSources.Select(source => source.SourceText.ToString()));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, diagnostics));
|
|
Assert.That(
|
|
outputDiagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputDiagnostics));
|
|
Assert.That(
|
|
generated,
|
|
Does.Match(
|
|
@"public\s+partial\s+class\s+VendorMotionDeviceState\s*:\s*" +
|
|
@"global::Opc\.Ua\.Robotics\.MotionDeviceState"));
|
|
Assert.That(
|
|
generated,
|
|
Does.Match(
|
|
@"public\s+partial\s+class\s+VendorAxisState\s*:\s*" +
|
|
@"global::Opc\.Ua\.Robotics\.AxisState"));
|
|
Assert.That(generated, Does.Contain("namespace Vendor.Robotics"));
|
|
Assert.That(generated, Does.Contain("public static partial class ObjectTypeIds"));
|
|
Assert.That(generated, Does.Contain("VendorMotionDeviceType"));
|
|
Assert.That(generated, Does.Contain("VendorAxisType"));
|
|
Assert.That(
|
|
generated,
|
|
Does.Match(@"public\s+static\s+partial\s+class\s+VendorMotionDeviceStateComponents\b"));
|
|
Assert.That(
|
|
generated,
|
|
Does.Match(@"public\s+static\s+partial\s+class\s+VendorAxisStateComponents\b"));
|
|
Assert.That(generated, Does.Not.Match(@"public\s+partial\s+class\s+MotionDeviceState\b"));
|
|
Assert.That(generated, Does.Not.Match(@"public\s+partial\s+class\s+AxisState\b"));
|
|
Assert.That(
|
|
generated,
|
|
Does.Not.Match(@"public\s+static\s+partial\s+class\s+MotionDeviceStateComponents\b"));
|
|
Assert.That(
|
|
generated,
|
|
Does.Not.Match(@"public\s+static\s+partial\s+class\s+AxisStateComponents\b"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void PayloadlessTransitiveReexportDoesNotWinOrRejectSelfProducer()
|
|
{
|
|
CSharpCompilation reexport = CreateModelMetadataAssembly(
|
|
"TransitiveReexport",
|
|
prefix: "DemoModel.Transitive",
|
|
version: "999.0",
|
|
payload: null!);
|
|
|
|
(GeneratorRunResult normalResult, _, ImmutableArray<Diagnostic> normalDiagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
additionalReferences: [reexport.ToMetadataReference()],
|
|
fluentAccessorsOnly: false,
|
|
assemblyName: "NormalConsumer");
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
normalDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty);
|
|
Assert.That(normalResult.GeneratedSources, Is.Empty,
|
|
"Normal duplicate suppression should select the payload-bearing producer.");
|
|
});
|
|
|
|
(GeneratorRunResult accessorResult, _, ImmutableArray<Diagnostic> accessorDiagnostics) =
|
|
RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
additionalReferences: [reexport.ToMetadataReference()],
|
|
assemblyName: "AccessorConsumer");
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
accessorDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty);
|
|
Assert.That(accessorResult.GeneratedSources, Has.Length.EqualTo(1));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void CorruptCandidateDoesNotAbortNormalGeneration()
|
|
{
|
|
string validPayload = new ModelDependencyV1
|
|
{
|
|
ModelUri = DemoModelUri,
|
|
FluentAccessorsEmitted = false
|
|
}.ToBase64Payload();
|
|
CSharpCompilation validProducer = CreateModelMetadataAssembly(
|
|
"ValidProducer",
|
|
prefix: "DemoModel",
|
|
version: "1.05.03",
|
|
payload: validPayload);
|
|
CSharpCompilation corruptCandidate = CreateModelMetadataAssembly(
|
|
"CorruptCandidate",
|
|
prefix: "DemoModel",
|
|
version: "0.0",
|
|
payload: Convert.ToBase64String([0xAA, 0xC7, 0x01, 0x01, 0xFF]));
|
|
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) = RunFluentAccessorsOnly(
|
|
validProducer,
|
|
additionalReferences: [corruptCandidate.ToMetadataReference()],
|
|
fluentAccessorsOnly: false,
|
|
assemblyName: "NormalConsumer");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty);
|
|
Assert.That(result.GeneratedSources, Is.Empty,
|
|
"The valid payload-bearing producer should suppress duplicate generation.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void FluentAccessorsOnlyRejectsCorruptSamePrefixProvider()
|
|
{
|
|
CSharpCompilation corruptProvider = CreateModelMetadataAssembly(
|
|
"CorruptProvider",
|
|
prefix: "DemoModel",
|
|
version: "0.0",
|
|
payload: "not!valid!base64!@@");
|
|
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
additionalReferences: [corruptProvider.ToMetadataReference()],
|
|
assemblyName: "ConservativeConsumer");
|
|
|
|
Diagnostic diagnostic = diagnostics.Single(d => d.Id == "MODELGEN014");
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(result.GeneratedSources, Is.Empty);
|
|
Assert.That(
|
|
diagnostic.GetMessage(CultureInfo.InvariantCulture),
|
|
Does.Contain("malformed model dependency payload"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void MultipleActualProducersRejectUnknownAccessorCapability()
|
|
{
|
|
string unknownCapabilityPayload = new ModelDependencyV1
|
|
{
|
|
ModelUri = DemoModelUri
|
|
}.ToBase64Payload();
|
|
CSharpCompilation unknownCapabilityProducer = CreateModelMetadataAssembly(
|
|
"UnknownCapabilityProducer",
|
|
prefix: "DemoModel",
|
|
version: "999.0",
|
|
payload: unknownCapabilityPayload);
|
|
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
additionalReferences: [unknownCapabilityProducer.ToMetadataReference()],
|
|
assemblyName: "ConservativeConsumer");
|
|
|
|
Diagnostic diagnostic = diagnostics.Single(d => d.Id == "MODELGEN014");
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(result.GeneratedSources, Is.Empty);
|
|
Assert.That(
|
|
diagnostic.GetMessage(CultureInfo.InvariantCulture),
|
|
Does.Contain("unknown legacy fluent-accessor capability"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void FluentAccessorsOnlyRejectsMissingReferencedModel()
|
|
{
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) =
|
|
RunFluentAccessorsOnly(producer: null!, includeDiNodeSet: true);
|
|
|
|
Diagnostic[] modelDiagnostics =
|
|
[.. diagnostics.Where(d => d.Id == "MODELGEN014")];
|
|
Diagnostic diagnostic = modelDiagnostics
|
|
.First(d => d.Id == "MODELGEN014" &&
|
|
d.GetMessage(CultureInfo.InvariantCulture).Contains(
|
|
DemoModelUri,
|
|
StringComparison.Ordinal));
|
|
string message = diagnostic.GetMessage(CultureInfo.InvariantCulture);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(result.GeneratedSources, Is.Empty);
|
|
Assert.That(modelDiagnostics, Has.Length.EqualTo(2),
|
|
"Both invalid local models should be diagnosed.");
|
|
Assert.That(message,
|
|
Does.Contain("no payload-bearing referenced model producer"));
|
|
Assert.That(message, Does.Contain("DemoModel.NodeSet2.xml"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void FluentAccessorsOnlyRejectsMismatchedReferencedPrefix()
|
|
{
|
|
const string consumerPrefix = "DemoModel.Consumer";
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
consumerPrefix: consumerPrefix);
|
|
|
|
Diagnostic diagnostic = diagnostics.Single(d => d.Id == "MODELGEN014");
|
|
string message = diagnostic.GetMessage(CultureInfo.InvariantCulture);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(result.GeneratedSources, Is.Empty);
|
|
Assert.That(message, Does.Contain(consumerPrefix));
|
|
Assert.That(message,
|
|
Does.Contain("supplies prefix 'DemoModel'"));
|
|
});
|
|
}
|
|
|
|
[TestCase("ModelSourceGeneratorOmitFluentApi")]
|
|
[TestCase("ModelSourceGeneratorGenerateNodeManager")]
|
|
public void FluentAccessorsOnlyRejectsInvalidOptionCombinations(string incompatibleOption)
|
|
{
|
|
var optionOverrides = new Dictionary<string, string>
|
|
{
|
|
["build_property." + incompatibleOption] = "true"
|
|
};
|
|
|
|
(GeneratorRunResult result, _, ImmutableArray<Diagnostic> diagnostics) = RunFluentAccessorsOnly(
|
|
s_producerWithoutAccessors.Value,
|
|
optionOverrides: optionOverrides);
|
|
|
|
Diagnostic diagnostic = diagnostics.Single(d => d.Id == "MODELGEN015");
|
|
string message = diagnostic.GetMessage(CultureInfo.InvariantCulture);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(result.GeneratedSources, Is.Empty);
|
|
Assert.That(message, Does.Contain(incompatibleOption));
|
|
});
|
|
}
|
|
|
|
private static (GeneratorRunResult Result, Compilation OutputCompilation,
|
|
ImmutableArray<Diagnostic> Diagnostics) RunFluentAccessorsOnly(
|
|
CSharpCompilation producer,
|
|
string? consumerPrefix = null,
|
|
bool includeDiNodeSet = false,
|
|
IReadOnlyDictionary<string, string>? optionOverrides = null,
|
|
IReadOnlyList<MetadataReference>? additionalReferences = null,
|
|
bool fluentAccessorsOnly = true,
|
|
string assemblyName = "Consumer")
|
|
{
|
|
CSharpCompilation compilation = CreateStackCompilation(assemblyName);
|
|
if (producer != null)
|
|
{
|
|
compilation = compilation.AddReferences(
|
|
s_diProducer.Value.ToMetadataReference(),
|
|
producer.ToMetadataReference());
|
|
}
|
|
if (additionalReferences != null)
|
|
{
|
|
compilation = compilation.AddReferences(additionalReferences);
|
|
}
|
|
|
|
var globalOptions = new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorVersion"] = "v105",
|
|
["build_property.ModelSourceGeneratorExclude"] = "Draft"
|
|
};
|
|
if (fluentAccessorsOnly)
|
|
{
|
|
globalOptions["build_property.ModelSourceGeneratorFluentAccessorsOnly"] = "true";
|
|
}
|
|
if (optionOverrides != null)
|
|
{
|
|
foreach (KeyValuePair<string, string> option in optionOverrides)
|
|
{
|
|
globalOptions[option.Key] = option.Value;
|
|
}
|
|
}
|
|
var options = new AnalyzerOptionsProvider(globalOptions);
|
|
if (!string.IsNullOrEmpty(consumerPrefix))
|
|
{
|
|
options.TextOptions["DemoModel.NodeSet2.xml"] = new Dictionary<string, string>
|
|
{
|
|
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = consumerPrefix
|
|
};
|
|
}
|
|
|
|
var additionalTexts = new List<AdditionalText>
|
|
{
|
|
EmbeddedText.From("DemoModel.NodeSet2.xml")
|
|
};
|
|
if (includeDiNodeSet)
|
|
{
|
|
additionalTexts.Add(EmbeddedText.From("Opc.Ua.Di.NodeSet2.xml"));
|
|
}
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([.. additionalTexts])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
|
|
return (driver.GetRunResult().Results[0], outputCompilation, diagnostics);
|
|
}
|
|
|
|
private static (GeneratorRunResult Result, Compilation OutputCompilation,
|
|
ImmutableArray<Diagnostic> Diagnostics) RunVendorRoboticsGenerator(
|
|
CSharpCompilation compilation)
|
|
{
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorVersion"] = "v105",
|
|
["build_property.ModelSourceGeneratorStartId"] = "9000",
|
|
["build_property.ModelSourceGeneratorUseAllowSubtypes"] = "true"
|
|
});
|
|
options.TextOptions["VendorRobotics.NodeSet2.xml"] = new Dictionary<string, string>
|
|
{
|
|
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "Vendor.Robotics"
|
|
};
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([EmbeddedText.From("VendorRobotics.NodeSet2.xml")])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
|
|
return (driver.GetRunResult().Results[0], outputCompilation, diagnostics);
|
|
}
|
|
|
|
private static CSharpCompilation GenerateModelCompilation(
|
|
string assemblyName,
|
|
string nodeSetResource,
|
|
string prefix)
|
|
{
|
|
CSharpCompilation compilation = CreateStackCompilation(assemblyName);
|
|
(GeneratorRunResult _, Compilation output, ImmutableArray<Diagnostic> diagnostics) =
|
|
RunModelGenerator(compilation, nodeSetResource, prefix);
|
|
ImmutableArray<Diagnostic> outputDiagnostics = output.GetDiagnostics();
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
diagnostics.Where(diagnostic =>
|
|
diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, diagnostics));
|
|
Assert.That(
|
|
outputDiagnostics.Where(diagnostic =>
|
|
diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputDiagnostics));
|
|
});
|
|
return (CSharpCompilation)output;
|
|
}
|
|
|
|
private static (GeneratorRunResult Result, Compilation OutputCompilation,
|
|
ImmutableArray<Diagnostic> Diagnostics) RunModelGenerator(
|
|
CSharpCompilation compilation,
|
|
string nodeSetResource,
|
|
string prefix)
|
|
{
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorStartId"] = "5000",
|
|
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
|
|
});
|
|
options.TextOptions[nodeSetResource] = new Dictionary<string, string>
|
|
{
|
|
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = prefix
|
|
};
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([EmbeddedText.From(nodeSetResource)])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
return (driver.GetRunResult().Results[0], outputCompilation, diagnostics);
|
|
}
|
|
|
|
private static CSharpCompilation CreateGeneratedRoboticsModelProducer()
|
|
{
|
|
CSharpCompilation compilation = CreateStackCompilation("RoboticsProducer")
|
|
.AddReferences(s_diProducer.Value.ToMetadataReference());
|
|
compilation = compilation.WithOptions(
|
|
compilation.Options.WithSpecificDiagnosticOptions(
|
|
new Dictionary<string, ReportDiagnostic>
|
|
{
|
|
["CS0108"] = ReportDiagnostic.Suppress
|
|
}));
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorVersion"] = "v105",
|
|
["build_property.ModelSourceGeneratorStartId"] = "2000",
|
|
["build_property.ModelSourceGeneratorUseAllowSubtypes"] = "true",
|
|
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
|
|
});
|
|
options.TextOptions["Opc.Ua.IA.NodeSet2.xml"] = new Dictionary<string, string>
|
|
{
|
|
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "Opc.Ua.IA"
|
|
};
|
|
options.TextOptions["Opc.Ua.Robotics.NodeSet2.xml"] = new Dictionary<string, string>
|
|
{
|
|
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "Opc.Ua.Robotics"
|
|
};
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts(
|
|
[
|
|
CreateModelText("Opc.Ua.IA", "Opc.Ua.IA.NodeSet2.xml"),
|
|
CreateRoboticsModelText("Opc.Ua.Robotics.NodeSet2.xml")
|
|
])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
|
|
Assert.That(
|
|
diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, diagnostics));
|
|
Assert.That(
|
|
outputCompilation.GetDiagnostics().Where(diagnostic =>
|
|
diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputCompilation.GetDiagnostics()));
|
|
return (CSharpCompilation)outputCompilation;
|
|
}
|
|
|
|
private static CSharpCompilation CreateGeneratedRoboticsAccessorProvider(
|
|
CSharpCompilation roboticsModel)
|
|
{
|
|
CSharpCompilation compilation = CreateStackCompilation("RoboticsAccessorProvider")
|
|
.AddReferences(
|
|
s_diProducer.Value.ToMetadataReference(),
|
|
roboticsModel.ToMetadataReference());
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorVersion"] = "v105",
|
|
["build_property.ModelSourceGeneratorUseAllowSubtypes"] = "true",
|
|
["build_property.ModelSourceGeneratorFluentAccessorsOnly"] = "true"
|
|
});
|
|
options.TextOptions["Opc.Ua.Robotics.NodeSet2.xml"] = new Dictionary<string, string>
|
|
{
|
|
["build_metadata.AdditionalFiles.ModelSourceGeneratorPrefix"] = "Opc.Ua.Robotics"
|
|
};
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([CreateRoboticsModelText("Opc.Ua.Robotics.NodeSet2.xml")])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
|
|
Assert.That(
|
|
diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, diagnostics));
|
|
Assert.That(
|
|
outputCompilation.GetDiagnostics().Where(diagnostic =>
|
|
diagnostic.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputCompilation.GetDiagnostics()));
|
|
return (CSharpCompilation)outputCompilation;
|
|
}
|
|
|
|
private static StringAdditionalText CreateRoboticsModelText(string fileName)
|
|
{
|
|
return CreateModelText("Opc.Ua.Robotics", fileName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a NodeSet from the model folder of the project that owns it.
|
|
/// IA used to sit in Opc.Ua.Robotics; it moved to its own assembly when
|
|
/// OPC 40001-1 Machinery started needing it as well, so the project name
|
|
/// is a parameter rather than a constant.
|
|
/// </summary>
|
|
private static StringAdditionalText CreateModelText(string projectName, string fileName)
|
|
{
|
|
string repositoryRoot = FindRepositoryRoot();
|
|
string path = Path.Combine(
|
|
repositoryRoot,
|
|
"src",
|
|
projectName,
|
|
"Model",
|
|
fileName);
|
|
return new StringAdditionalText(fileName, File.ReadAllText(path));
|
|
}
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
string assemblyDirectory =
|
|
Path.GetDirectoryName(typeof(ModelDependencyScannerTests).Assembly.Location)
|
|
?? throw new InvalidOperationException("Test assembly directory was not found.");
|
|
var directory = new DirectoryInfo(assemblyDirectory);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
directory = directory.Parent
|
|
?? throw new InvalidOperationException("Repository root was not found.");
|
|
}
|
|
return directory.FullName;
|
|
}
|
|
|
|
private static CSharpCompilation CreateGeneratedDemoModelProducer(bool omitFluentApi)
|
|
{
|
|
CSharpCompilation compilation = CreateStackCompilation(
|
|
omitFluentApi ? "ProducerWithoutAccessors" : "ProducerWithAccessors")
|
|
.AddReferences(s_diProducer.Value.ToMetadataReference());
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorStartId"] = "1000",
|
|
["build_property.ModelSourceGeneratorOmitFluentApi"] =
|
|
omitFluentApi ? "true" : "false"
|
|
});
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([EmbeddedText.From("DemoModel.NodeSet2.xml")])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
|
|
GeneratorRunResult result = driver.GetRunResult().Results[0];
|
|
Assert.That(
|
|
diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
"Generated producer must not report errors.");
|
|
Assert.That(
|
|
outputCompilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputCompilation.GetDiagnostics()));
|
|
Assert.That(
|
|
result.GeneratedSources.Any(s => s.HintName.EndsWith(
|
|
".FluentBuilders.g.cs",
|
|
StringComparison.Ordinal)),
|
|
Is.EqualTo(!omitFluentApi));
|
|
|
|
CSharpCompilation probe = CreateStackCompilation("ProducerProbe")
|
|
.AddReferences(
|
|
s_diProducer.Value.ToMetadataReference(),
|
|
outputCompilation.ToMetadataReference());
|
|
ModelDependencyReference dependency = ReferencedModelDependencyScanner.Scan(probe)
|
|
.Single(r => r.ModelUri == DemoModelUri);
|
|
Assert.That(
|
|
dependency.GetDependency()?.FluentAccessorsEmitted,
|
|
Is.EqualTo(!omitFluentApi));
|
|
ModelFluentAccessorProviderReference[] providers =
|
|
[.. ReferencedFluentAccessorProviderScanner.Scan(probe)
|
|
.Where(reference => reference.ModelUri == DemoModelUri)];
|
|
Assert.That(providers, Has.Length.EqualTo(omitFluentApi ? 0 : 1));
|
|
|
|
return (CSharpCompilation)outputCompilation;
|
|
}
|
|
|
|
private static CSharpCompilation CreateModelMetadataAssembly(
|
|
string assemblyName,
|
|
string prefix,
|
|
string version,
|
|
string payload)
|
|
{
|
|
string payloadLiteral = payload == null ? "null" : "\"" + payload + "\"";
|
|
string source =
|
|
"[assembly: global::Opc.Ua.ModelDependencyAttribute(" +
|
|
"\"" + DemoModelUri + "\", " +
|
|
"\"" + prefix + "\", " +
|
|
"\"" + version + "\", " +
|
|
"\"2099-01-01\", " +
|
|
"\"DemoModel\", " +
|
|
payloadLiteral + ")]";
|
|
CSharpCompilation compilation = CreateStackCompilation(assemblyName)
|
|
.AddCode(new Dictionary<string, string>
|
|
{
|
|
["ModelMetadata.cs"] = source
|
|
}, LanguageVersion.CSharp11);
|
|
Assert.That(
|
|
compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, compilation.GetDiagnostics()));
|
|
return compilation;
|
|
}
|
|
|
|
private static CSharpCompilation CreateGeneratedDiProducer()
|
|
{
|
|
CSharpCompilation compilation = CreateStackCompilation("DiProducer");
|
|
var options = new AnalyzerOptionsProvider(
|
|
new Dictionary<string, string>
|
|
{
|
|
["build_property.ModelSourceGeneratorStartId"] = "1000",
|
|
["build_property.ModelSourceGeneratorOmitFluentApi"] = "true"
|
|
});
|
|
|
|
var generator = new ModelSourceGenerator();
|
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator)
|
|
.WithUpdatedParseOptions(new CSharpParseOptions()
|
|
.WithKind(SourceCodeKind.Regular)
|
|
.WithLanguageVersion(LanguageVersion.CSharp11))
|
|
.AddAdditionalTexts([EmbeddedText.From("Opc.Ua.Di.NodeSet2.xml")])
|
|
.WithUpdatedAnalyzerConfigOptions(options);
|
|
|
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
|
compilation,
|
|
out Compilation outputCompilation,
|
|
out ImmutableArray<Diagnostic> diagnostics);
|
|
|
|
Assert.That(
|
|
diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
"Generated DI producer must not report errors.");
|
|
Assert.That(
|
|
outputCompilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error),
|
|
Is.Empty,
|
|
string.Join(Environment.NewLine, outputCompilation.GetDiagnostics()));
|
|
return (CSharpCompilation)outputCompilation;
|
|
}
|
|
|
|
internal static CSharpCompilation CreateStackCompilation(string assemblyName)
|
|
{
|
|
CSharpCompilation compilation =
|
|
OptimizationLevel.Release.CreateCompilation(assemblyName);
|
|
compilation = compilation
|
|
.WithOptions(compilation.Options.WithGeneralDiagnosticOption(
|
|
ReportDiagnostic.Error))
|
|
.AddReferences(GetStackReferences());
|
|
|
|
// Generated node managers use ILogger and IAsyncDisposable. The
|
|
// Opc.Ua.Server output folder of a .NET Framework build contains
|
|
// neither Microsoft.Extensions.Logging.Abstractions nor
|
|
// Microsoft.Bcl.AsyncInterfaces (and the base references of that
|
|
// leg do not either), so reference the copies this test process
|
|
// loaded when they are missing. On .NET the types live in
|
|
// assemblies that are already referenced.
|
|
foreach (Type dependency in s_generatedCodeDependencies)
|
|
{
|
|
string location = dependency.Assembly.Location;
|
|
string fileName = Path.GetFileName(location);
|
|
if (!compilation.References.Any(reference => string.Equals(
|
|
Path.GetFileName(reference.Display),
|
|
fileName,
|
|
StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
compilation = compilation.AddReferences(
|
|
MetadataReference.CreateFromFile(location));
|
|
}
|
|
}
|
|
return compilation;
|
|
}
|
|
|
|
private static readonly Type[] s_generatedCodeDependencies =
|
|
[
|
|
typeof(Microsoft.Extensions.Logging.ILogger),
|
|
typeof(IAsyncDisposable)
|
|
];
|
|
|
|
private static IEnumerable<MetadataReference> GetStackReferences()
|
|
{
|
|
string assemblyDirectory =
|
|
Path.GetDirectoryName(typeof(ModelDependencyScannerTests).Assembly.Location)
|
|
?? throw new InvalidOperationException("Test assembly directory was not found.");
|
|
var directory = new DirectoryInfo(assemblyDirectory);
|
|
string targetFramework = directory.Name;
|
|
string configuration = directory.Parent?.Name
|
|
?? throw new InvalidOperationException("Test configuration directory was not found.");
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
directory = directory.Parent
|
|
?? throw new InvalidOperationException("Repository root was not found.");
|
|
}
|
|
string serverBin = Path.Combine(
|
|
directory.FullName,
|
|
"src",
|
|
"Opc.Ua.Server",
|
|
"bin",
|
|
configuration);
|
|
string serverOutput = ResolveServerOutputFolder(serverBin, targetFramework);
|
|
return Directory.EnumerateFiles(serverOutput, "Opc.Ua*.dll")
|
|
.Where(path => !string.Equals(
|
|
Path.GetFileName(path),
|
|
"Opc.Ua.Types.dll",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
.Select(path => MetadataReference.CreateFromFile(path));
|
|
}
|
|
|
|
private static string ResolveServerOutputFolder(string serverBin, string testTargetFramework)
|
|
{
|
|
if (!Directory.Exists(serverBin))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Opc.Ua.Server output folder was not found at '{serverBin}'. "
|
|
+ "Build Opc.Ua.Server before running these tests.");
|
|
}
|
|
|
|
// The Opc.Ua.Server project may be built for a different target framework
|
|
// than the test assembly. Probe the actual bin folder for whichever TFM
|
|
// subfolder exists instead of assuming the test's TFM. Prefer an exact match,
|
|
// then the most recently built output.
|
|
var candidates = Directory
|
|
.EnumerateDirectories(serverBin)
|
|
.Select(path => new DirectoryInfo(path))
|
|
.ToList();
|
|
|
|
DirectoryInfo match =
|
|
(candidates.Find(d => string.Equals(
|
|
d.Name, testTargetFramework, StringComparison.OrdinalIgnoreCase))
|
|
?? candidates
|
|
.Select(d => new FileInfo(Path.Combine(d.FullName, "Opc.Ua.Server.dll")))
|
|
.Where(f => f.Exists)
|
|
.OrderByDescending(f => f.LastWriteTimeUtc)
|
|
.Select(f => f.Directory)
|
|
.FirstOrDefault())!;
|
|
|
|
if (match == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"No Opc.Ua.Server build output was found under '{serverBin}'.");
|
|
}
|
|
|
|
return match.FullName;
|
|
}
|
|
|
|
private const string DemoModelUri = "urn:opcfoundation.org:2024-01:DemoModel";
|
|
|
|
private static readonly string[] s_onlyX = ["X"];
|
|
|
|
private static readonly Lazy<CSharpCompilation> s_diProducer =
|
|
new(CreateGeneratedDiProducer);
|
|
|
|
private static readonly Lazy<CSharpCompilation> s_producerWithoutAccessors =
|
|
new(() => CreateGeneratedDemoModelProducer(omitFluentApi: true));
|
|
|
|
private static readonly Lazy<CSharpCompilation> s_producerWithAccessors =
|
|
new(() => CreateGeneratedDemoModelProducer(omitFluentApi: false));
|
|
|
|
private sealed class StringAdditionalText : AdditionalText
|
|
{
|
|
public StringAdditionalText(string path, string text)
|
|
{
|
|
Path = path;
|
|
m_text = SourceText.From(text);
|
|
}
|
|
|
|
public override string Path { get; }
|
|
|
|
public override SourceText GetText(System.Threading.CancellationToken cancellationToken = default)
|
|
{
|
|
return m_text;
|
|
}
|
|
|
|
private readonly SourceText m_text;
|
|
}
|
|
}
|
|
}
|