diff --git a/README.md b/README.md index b6e330f..0f2eca5 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,93 @@ public async Task HasFix(string testCase) } ``` +### Fixture configuration + +Every `RoslynFixtureFactory.Create()` overload accepts an optional config object. The config classes share a common base (`BaseTestFixtureConfig`) that exposes project-level settings. Fixture-specific config classes inherit from this base and can add extra options on top. + +#### Shared options (all fixture types) + +| Property | Type | Default | Description | +|---|---|---|---| +| `Language` | `string` | `LanguageNames.AL` | Language used for the test project. | +| `ThrowsWhenInputDocumentContainsError` | `bool` | `true` | Throw when the code under test has compiler errors. | +| `References` | `IReadOnlyList` | empty | Extra metadata references added to the project. | +| `AdditionalFiles` | `IReadOnlyList` | empty | Additional files exposed to analyzers (e.g. `.editorconfig`). | +| `RuleSetPath` | `string?` | `null` | Path to a `.ruleset` file that controls diagnostic severity. | +| `PackageCachePaths` | `IReadOnlyList` | empty | Directories containing `.app` packages the compiler resolves symbols from. | +| `CompilationOptions` | `CompilationOptions?` | `null` | Override the default `CompilationOptions`. | +| `ParseOptions` | `ParseOptions?` | `null` | Override the default `ParseOptions`. | +| `ProjectInfoCustomizer` | `Func?` | `null` | Escape hatch to set any `ProjectInfo` property not exposed above. | + +#### CodeFixTestFixtureConfig extras + +| Property | Type | Default | Description | +|---|---|---|---| +| `AdditionalAnalyzers` | `IReadOnlyCollection` | empty | Analyzers that produce the diagnostics the code fix needs to act on. | + +#### Example: apply a ruleset to an analyzer test + +```csharp +var fixture = RoslynFixtureFactory.Create( + new AnalyzerTestFixtureConfig + { + RuleSetPath = @"rules\my.ruleset" + }); + +fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.FlowFieldsShouldNotBeEditable); +``` + +#### Example: resolve symbols from a package cache + +```csharp +var fixture = RoslynFixtureFactory.Create( + new AnalyzerTestFixtureConfig + { + PackageCachePaths = [@"C:\packages\base-app"] + }); + +fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.FlowFieldsShouldNotBeEditable); +``` + +#### Example: set compilation target to OnPrem + +```csharp +var fixture = RoslynFixtureFactory.Create( + new AnalyzerTestFixtureConfig + { + CompilationOptions = new CompilationOptions(target: CompilationTarget.OnPrem) + }); + +fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.FlowFieldsShouldNotBeEditable); +``` + +#### Example: code fix with additional analyzers and a ruleset + +```csharp +var fixture = RoslynFixtureFactory.Create( + new CodeFixTestFixtureConfig + { + AdditionalAnalyzers = [new Analyzer.FlowFieldsShouldNotBeEditable()], + RuleSetPath = @"rules\my.ruleset" + }); + +fixture.TestCodeFix(currentCode, expectedCode, DiagnosticDescriptors.FlowFieldsShouldNotBeEditable); +``` + +#### Example: escape hatch for advanced ProjectInfo settings + +When you need to configure a `ProjectInfo` property that has no dedicated config option, use `ProjectInfoCustomizer`. + +```csharp +var fixture = RoslynFixtureFactory.Create( + new AnalyzerTestFixtureConfig + { + ProjectInfoCustomizer = info => info.WithAssemblyProbingPaths([@"C:\probing"]) + }); + +fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.FlowFieldsShouldNotBeEditable); +``` + ### NavCodeAnalysisBase The `NavCodeAnalysisBase` class is a base class for analyzer tests that need to behave differently depending on the version of the AL Language (`Microsoft.Dynamics.Nav.CodeAnalysis`). By inheriting from this class, your tests automatically gain utilities for: * Detecting the currently loaded Microsoft.Dynamics.Nav.CodeAnalysis assembly version. diff --git a/src/RoslynTestKit/BaseTestFixture.cs b/src/RoslynTestKit/BaseTestFixture.cs index e56ab8e..bbaa2c0 100644 --- a/src/RoslynTestKit/BaseTestFixture.cs +++ b/src/RoslynTestKit/BaseTestFixture.cs @@ -3,10 +3,12 @@ using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; using Document = Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.Document; using LanguageNames = Microsoft.Dynamics.Nav.CodeAnalysis.LanguageNames; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; namespace RoslynTestKit { @@ -20,6 +22,36 @@ public abstract class BaseTestFixture protected virtual IReadOnlyCollection? AdditionalFiles => null; + /// + /// Path to a ruleset file (.ruleset) applied to the test project. + /// When null, the default ruleset behaviour applies. + /// + protected virtual string? RuleSetPath => null; + + /// + /// Package cache paths made available to the compiler when resolving symbols. + /// + protected virtual IReadOnlyList? PackageCachePaths => null; + + /// + /// Override the used when compiling the test project. + /// When null, new CompilationOptions() is used. + /// + protected virtual CompilationOptions? CustomCompilationOptions => null; + + /// + /// Override the applied to the test project. + /// When null, the platform default applies. + /// + protected virtual ParseOptions? ParseOptions => null; + + /// + /// Optional callback applied to the after all other settings have + /// been applied. Use this escape hatch to configure any property + /// not directly exposed on the fixture. + /// + protected virtual Func? ProjectInfoCustomizer => null; + protected Document CreateDocumentFromCode(string code) { return CreateDocumentFromCode(code, LanguageName, References ?? Array.Empty()); @@ -35,7 +67,15 @@ protected virtual Document CreateDocumentFromCode(string code, string languageNa { var frameworkReferences = CreateFrameworkMetadataReferences(); - var compilationOptions = GetCompilationOptions(languageName); + var compilationOptions = CustomCompilationOptions ?? GetCompilationOptions(languageName); + + var settings = new ProjectSettings + { + RuleSetPath = RuleSetPath, + PackageCachePaths = PackageCachePaths, + ParseOptions = ParseOptions, + ProjectInfoCustomizer = ProjectInfoCustomizer + }; var docs = FileSeparatorPattern.Split(code).Reverse().ToList(); if (docs.Count == 0) @@ -44,7 +84,7 @@ protected virtual Document CreateDocumentFromCode(string code, string languageNa } var project = new AdhocWorkspace() - .AddProject("TestProject", languageName) + .AddProject("TestProject", languageName, settings) .WithCompilationOptions(compilationOptions) .AddMetadataReferences(frameworkReferences) .AddMetadataReferences(extraReferences); diff --git a/src/RoslynTestKit/BaseTestFixtureConfig.cs b/src/RoslynTestKit/BaseTestFixtureConfig.cs index a8e1f96..91fde6a 100644 --- a/src/RoslynTestKit/BaseTestFixtureConfig.cs +++ b/src/RoslynTestKit/BaseTestFixtureConfig.cs @@ -1,8 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; using LanguageNames = Microsoft.Dynamics.Nav.CodeAnalysis.LanguageNames; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; namespace RoslynTestKit { @@ -14,5 +18,35 @@ public abstract class BaseTestFixtureConfig public string Language { get; set; } = LanguageNames.AL; public IReadOnlyList AdditionalFiles { get; set; } = ImmutableArray.Empty; + + /// + /// Path to a ruleset file (.ruleset) that controls which diagnostics are enabled and at what severity. + /// When null, the default ruleset behaviour applies. + /// + public string? RuleSetPath { get; set; } = null; + + /// + /// Paths to directories that contain .app packages the compiler should resolve symbols from. + /// + public IReadOnlyList PackageCachePaths { get; set; } = ImmutableArray.Empty; + + /// + /// Override the default used when compiling the test project. + /// When null, new CompilationOptions() is used. + /// + public CompilationOptions? CompilationOptions { get; set; } = null; + + /// + /// Override the default used when parsing test documents. + /// When null, the platform default applies. + /// + public ParseOptions? ParseOptions { get; set; } = null; + + /// + /// Optional callback applied to the after all other settings have been + /// applied. Use this escape hatch to set any property not exposed + /// directly on the config. + /// + public Func? ProjectInfoCustomizer { get; set; } = null; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs b/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs index cb2283a..94914a2 100644 --- a/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; namespace RoslynTestKit { @@ -21,5 +25,10 @@ public ConfigurableAnalyzerTestFixture(DiagnosticAnalyzer diagnosticAnalyzer, An protected override IReadOnlyCollection References => _config.References; protected override IReadOnlyCollection AdditionalFiles => _config.AdditionalFiles; protected override bool ThrowsWhenInputDocumentContainsError => _config.ThrowsWhenInputDocumentContainsError; + protected override string? RuleSetPath => _config.RuleSetPath; + protected override IReadOnlyList? PackageCachePaths => _config.PackageCachePaths; + protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; + protected override ParseOptions? ParseOptions => _config.ParseOptions; + protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs b/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs index 07adac6..134eabb 100644 --- a/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs @@ -1,8 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.CodeFixes; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; namespace RoslynTestKit { @@ -23,5 +27,10 @@ public ConfigurableCodeFixTestFixture(CodeFixProvider provider, CodeFixTestFixtu protected override IReadOnlyCollection References => _config.References; protected override IReadOnlyCollection AdditionalFiles => _config.AdditionalFiles; protected override bool ThrowsWhenInputDocumentContainsError => _config.ThrowsWhenInputDocumentContainsError; + protected override string? RuleSetPath => _config.RuleSetPath; + protected override IReadOnlyList? PackageCachePaths => _config.PackageCachePaths; + protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; + protected override ParseOptions? ParseOptions => _config.ParseOptions; + protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs b/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs index 7f4b5cc..4810deb 100644 --- a/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.CodeRefactoring; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; namespace RoslynTestKit { @@ -21,5 +25,10 @@ public ConfigurableCodeRefactoringTestFixture(CodeRefactoringProvider provider, protected override IReadOnlyCollection References => _config.References; protected override IReadOnlyCollection AdditionalFiles => _config.AdditionalFiles; protected override bool ThrowsWhenInputDocumentContainsError => _config.ThrowsWhenInputDocumentContainsError; + protected override string? RuleSetPath => _config.RuleSetPath; + protected override IReadOnlyList? PackageCachePaths => _config.PackageCachePaths; + protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; + protected override ParseOptions? ParseOptions => _config.ParseOptions; + protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs b/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs index e32e381..02ff96f 100644 --- a/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.Completion; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; namespace RoslynTestKit { @@ -21,5 +25,10 @@ public ConfigurableCompletionProviderTestFixture(CompletionProvider provider, Co protected override IReadOnlyCollection References => _config.References; protected override IReadOnlyCollection AdditionalFiles => _config.AdditionalFiles; protected override bool ThrowsWhenInputDocumentContainsError => _config.ThrowsWhenInputDocumentContainsError; + protected override string? RuleSetPath => _config.RuleSetPath; + protected override IReadOnlyList? PackageCachePaths => _config.PackageCachePaths; + protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; + protected override ParseOptions? ParseOptions => _config.ParseOptions; + protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; } } \ No newline at end of file diff --git a/src/RoslynTestKit/Helpers/AdhocWorkspace.cs b/src/RoslynTestKit/Helpers/AdhocWorkspace.cs index c7cd61e..0739d14 100644 --- a/src/RoslynTestKit/Helpers/AdhocWorkspace.cs +++ b/src/RoslynTestKit/Helpers/AdhocWorkspace.cs @@ -80,16 +80,37 @@ public Solution AddSolution(SolutionInfo solutionInfo) /// #if NETSTANDARD2_1 public Project? AddProject(string name, string language) + => AddProjectInternal(name, language, null); + + internal Project? AddProject(string name, string language, ProjectSettings? settings) + => AddProjectInternal(name, language, settings); + + private Project? AddProjectInternal(string name, string language, ProjectSettings? settings) { - var info = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(), name, name, language); + var info = ProjectInfo.Create( + ProjectId.CreateNewId(), VersionStamp.Create(), name, name, language, + packageCachePaths: settings?.PackageCachePaths, + parseOptions: settings?.ParseOptions, + ruleSetPath: settings?.RuleSetPath); + + if (settings?.ProjectInfoCustomizer != null) + info = settings.ProjectInfoCustomizer(info); + return AddProject(info); } #endif #if NET8_0_OR_GREATER public Project? AddProject(string name, string language) + => AddProject(name, language, null); + + internal Project? AddProject(string name, string language, ProjectSettings? settings) { - var info = CreateProjectInfoViaReflection(name, language); + var info = CreateProjectInfoViaReflection(name, language, settings); + + if (settings?.ProjectInfoCustomizer != null) + info = settings.ProjectInfoCustomizer(info); + return AddProject(info); } @@ -102,10 +123,10 @@ public Solution AddSolution(SolutionInfo solutionInfo) /// version v17.0.28.6483 and v17.0.28.26016 of Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.dll. /// By using reflection, we can call the method regardless of which version is loaded at runtime. /// - private static ProjectInfo CreateProjectInfoViaReflection(string name, string language) + private static ProjectInfo CreateProjectInfoViaReflection(string name, string language, ProjectSettings? settings = null) { var (method, parameters) = _projectInfoCreateMethod.Value; - var args = BuildMethodArguments(parameters, name, language); + var args = BuildMethodArguments(parameters, name, language, settings); var result = method.Invoke(null, args); if (result is not ProjectInfo projectInfo) @@ -119,13 +140,13 @@ private static ProjectInfo CreateProjectInfoViaReflection(string name, string la /// /// Builds the argument array for calling ProjectInfo.Create via reflection. /// - private static object?[] BuildMethodArguments(ParameterInfo[] parameters, string name, string language) + private static object?[] BuildMethodArguments(ParameterInfo[] parameters, string name, string language, ProjectSettings? settings) { var args = new object?[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { - args[i] = GetParameterValue(parameters[i], name, language); + args[i] = GetParameterValue(parameters[i], name, language, settings); } return args; @@ -133,20 +154,23 @@ private static ProjectInfo CreateProjectInfoViaReflection(string name, string la /// /// Determines the value to pass for a given parameter of ProjectInfo.Create. + /// When a instance is provided, its values take precedence over + /// the parameter defaults for the supported curated properties. /// - private static object? GetParameterValue(ParameterInfo parameter, string name, string language) + private static object? GetParameterValue(ParameterInfo parameter, string name, string language, ProjectSettings? settings) { var paramName = parameter.Name?.ToLowerInvariant() ?? string.Empty; - // Handle the required parameters (first 5 in the method signature) return paramName switch { - "id" => ProjectId.CreateNewId(), - "version" => VersionStamp.Create(), - "name" => name, - "assemblyname" => name, - "language" => language, - // For optional parameters, use their declared default value or an appropriate fallback + "id" => ProjectId.CreateNewId(), + "version" => VersionStamp.Create(), + "name" => name, + "assemblyname" => name, + "language" => language, + "packagecachepaths" when settings?.PackageCachePaths != null => settings.PackageCachePaths, + "parseoptions" when settings?.ParseOptions != null => settings.ParseOptions, + "rulesetpath" when settings?.RuleSetPath != null => settings.RuleSetPath, _ => GetDefaultParameterValue(parameter) }; } diff --git a/src/RoslynTestKit/Helpers/ProjectSettings.cs b/src/RoslynTestKit/Helpers/ProjectSettings.cs new file mode 100644 index 0000000..313fd6e --- /dev/null +++ b/src/RoslynTestKit/Helpers/ProjectSettings.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; + +namespace RoslynTestKit +{ + /// + /// Carries -level settings from the fixture config down to + /// . + /// Kept internal so that it does not become part of the public API surface. + /// + internal sealed class ProjectSettings + { + public string? RuleSetPath { get; set; } + public IReadOnlyList? PackageCachePaths { get; set; } + public CompilationOptions? CompilationOptions { get; set; } + public ParseOptions? ParseOptions { get; set; } + public Func? ProjectInfoCustomizer { get; set; } + } +}