Files
UA-.NETStandard/tests/Opc.Ua.SourceGeneration.Tests/ModelDependencyScannerTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# 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>
2026-10-08 08:48:13 +02:00

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;
}
}
}