File: SolutionTests\TryApplyChangesTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.UnitTests;
 
[UseExportProvider]
public sealed class TryApplyChangesTests
{
    private sealed class CustomizedCanApplyWorkspace : Workspace
    {
        private readonly ImmutableArray<ApplyChangesKind> _allowedKinds;
        private readonly Func<ParseOptions, ParseOptions, bool>? _canApplyParseOptions;
        private readonly Func<CompilationOptions, CompilationOptions, bool>? _canApplyCompilationOptions;
 
        public CustomizedCanApplyWorkspace(params ApplyChangesKind[] allowedKinds)
            : this(allowedKinds, canApplyParseOptions: null)
        {
        }
 
        public CustomizedCanApplyWorkspace(ApplyChangesKind[] allowedKinds,
            Func<ParseOptions, ParseOptions, bool>? canApplyParseOptions = null,
            Func<CompilationOptions, CompilationOptions, bool>? canApplyCompilationOptions = null)
            : base(Host.Mef.MefHostServices.DefaultHost, workspaceKind: nameof(CustomizedCanApplyWorkspace))
        {
            _allowedKinds = [.. allowedKinds];
            _canApplyParseOptions = canApplyParseOptions;
            _canApplyCompilationOptions = canApplyCompilationOptions;
 
            // Add a C# project automatically so each test has something to try mutating
            OnProjectAdded(ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestProject", LanguageNames.CSharp));
        }
 
        public override bool CanApplyChange(ApplyChangesKind feature)
        {
            return _allowedKinds.Contains(feature);
        }
 
        public override bool CanApplyParseOptionChange(ParseOptions oldOptions, ParseOptions newOptions, Project project)
        {
            if (_canApplyParseOptions != null)
            {
                return _canApplyParseOptions(oldOptions, newOptions);
            }
 
            return base.CanApplyParseOptionChange(oldOptions, newOptions, project);
        }
 
        public override bool CanApplyCompilationOptionChange(CompilationOptions oldOptions, CompilationOptions newOptions, Project project)
        {
            if (_canApplyCompilationOptions != null)
            {
                return _canApplyCompilationOptions(oldOptions, newOptions);
            }
 
            return base.CanApplyCompilationOptionChange(oldOptions, newOptions, project);
        }
    }
 
    [Fact]
    public void TryApplyWorksIfAllowingAnyCompilationOption()
    {
        // If we simply support the main change kind, then any type of change should be supported and we should not get called
        // to the other method
        using var workspace = new CustomizedCanApplyWorkspace(
            allowedKinds: [ApplyChangesKind.ChangeCompilationOptions],
            canApplyCompilationOptions: (_, __) => throw new Exception("This should not have been called."));
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        Assert.True(workspace.TryApplyChanges(project.WithCompilationOptions(project.CompilationOptions!.WithMainTypeName("Test")).Solution));
    }
 
    [Fact]
    public void TryApplyWorksSpecificChangeIsAllowedForCompilationOption()
    {
        // If we don't support the main change kind, then the other method should be called
        using var workspace = new CustomizedCanApplyWorkspace(
            allowedKinds: [],
            canApplyCompilationOptions: (_, newCompilationOptions) => newCompilationOptions.MainTypeName == "Test");
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        Assert.True(workspace.TryApplyChanges(project.WithCompilationOptions(project.CompilationOptions!.WithMainTypeName("Test")).Solution));
    }
 
    [Fact]
    public void TryApplyWorksThrowsIfChangeIsDisallowedForCompilationOption()
    {
        // If we don't support the main change kind, then the other method should be called
        using var workspace = new CustomizedCanApplyWorkspace(
            allowedKinds: [],
            canApplyCompilationOptions: (_, newCompilationOptions) => newCompilationOptions.MainTypeName == "Expected");
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        var exception = Assert.Throws<NotSupportedException>(
            () => workspace.TryApplyChanges(project.WithCompilationOptions(project.CompilationOptions!.WithMainTypeName("WrongThing")).Solution));
 
        Assert.Equal(WorkspacesResources.Changing_compilation_options_is_not_supported, exception.Message);
    }
 
    [Fact]
    public void TryApplyWorksIfAllowingAnyParseOption()
    {
        // If we simply support the main change kind, then any type of change should be supported and we should not get called
        // to the other method
        using var workspace = new CustomizedCanApplyWorkspace(
            allowedKinds: [ApplyChangesKind.ChangeParseOptions],
            canApplyParseOptions: (_, __) => throw new Exception("This should not have been called."));
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        Assert.True(workspace.TryApplyChanges(
            project.WithParseOptions(
                project.ParseOptions!.WithFeatures([KeyValuePairUtil.Create("Feature", "")])).Solution));
    }
 
    [Fact]
    public void TryApplyWorksSpecificChangeIsAllowedForParseOption()
    {
        // If we don't support the main change kind, then the other method should be called
        using var workspace = new CustomizedCanApplyWorkspace(
            allowedKinds: [],
            canApplyParseOptions: (_, newParseOptions) => newParseOptions.Features["Feature"] == "ExpectedValue");
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        Assert.True(
            workspace.TryApplyChanges(
                project.WithParseOptions(project.ParseOptions!.WithFeatures([KeyValuePairUtil.Create("Feature", "ExpectedValue")])).Solution));
    }
 
    [Fact]
    public void TryApplyWorksThrowsIfChangeIsDisallowedForParseOption()
    {
        // If we don't support the main change kind, then the other method should be called
        using var workspace = new CustomizedCanApplyWorkspace(
            allowedKinds: [],
            canApplyParseOptions: (_, newParseOptions) => newParseOptions.Features["Feature"] == "ExpectedValue");
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        var exception = Assert.Throws<NotSupportedException>(
            () => workspace.TryApplyChanges(
                project.WithParseOptions(project.ParseOptions!.WithFeatures([KeyValuePairUtil.Create("Feature", "WrongThing")])).Solution));
 
        Assert.Equal(WorkspacesResources.Changing_parse_options_is_not_supported, exception.Message);
    }
 
    [Fact]
    public void TryApplyWorksWhenAddingEditorConfigWithoutSupportingCompilationOptionsChanging()
    {
        using var workspace = new CustomizedCanApplyWorkspace(allowedKinds: ApplyChangesKind.AddAnalyzerConfigDocument);
 
        var project = workspace.CurrentSolution.Projects.Single();
 
        Assert.True(workspace.TryApplyChanges(project.AddAnalyzerConfigDocument(".editorconfig", SourceText.From("")).Project.Solution));
    }
}