|
// 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.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeRefactorings;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions;
public class ApplyChangesOperationTests : AbstractCSharpCodeActionTest
{
protected override CodeRefactoringProvider CreateCodeRefactoringProvider(EditorTestWorkspace workspace, TestParameters parameters)
=> new MyCodeRefactoringProvider((Func<Solution, Solution>)parameters.fixProviderData);
private class MyCodeRefactoringProvider : CodeRefactoringProvider
{
private readonly Func<Solution, Solution> _changeSolution;
public MyCodeRefactoringProvider(Func<Solution, Solution> changeSolution)
{
_changeSolution = changeSolution;
}
public sealed override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var codeAction = new TestCodeAction(_changeSolution(context.Document.Project.Solution));
context.RegisterRefactoring(codeAction);
return Task.CompletedTask;
}
private sealed class TestCodeAction : CodeAction
{
private readonly Solution _changedSolution;
public TestCodeAction(Solution changedSolution)
{
_changedSolution = changedSolution;
}
public override string Title => "Title";
protected override Task<Solution?> GetChangedSolutionAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
=> Task.FromResult<Solution?>(_changedSolution);
}
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1419139")]
public async Task TestMakeTextChangeWithInterveningEditToDifferentFile()
{
// This should succeed as the code action is trying to edit a file that is not touched by the actual
// workspace edit that already went in.
await TestSuccessfulApplicationAsync(
@"<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" CommonReferences=""true"">
<Document FilePath=""Program1.cs"">
class Program1
{
}
</Document>
<Document FilePath=""Program2.cs"">
class Program2
{
}
</Document>
</Project>
</Workspace>",
codeActionTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.WithDocumentText(document1.Id, SourceText.From("NewProgram1Content"));
},
intermediaryTransform: solution =>
{
var document2 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program2"));
return solution.WithDocumentText(document2.Id, SourceText.From("NewProgram2Content"));
});
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1419139")]
public async Task TestMakeTextChangeWithInterveningRemovalToDifferentFile()
{
// This should succeed as the code action is trying to edit a file that is not touched by the actual
// workspace edit that already went in.
await TestSuccessfulApplicationAsync(
@"<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" CommonReferences=""true"">
<Document FilePath=""Program1.cs"">
class Program1
{
}
</Document>
<Document FilePath=""Program2.cs"">
class Program2
{
}
</Document>
</Project>
</Workspace>",
codeActionTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.WithDocumentText(document1.Id, SourceText.From("NewProgram1Content"));
},
intermediaryTransform: solution =>
{
var document2 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program2"));
return solution.RemoveDocument(document2.Id);
});
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1419139")]
public async Task TestMakeTextChangeWithInterveningEditToSameFile()
{
// This should fail as the code action is trying to edit a file that is was already edited by the actual
// workspace edit that already went in.
await TestFailureApplicationAsync(
@"<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" CommonReferences=""true"">
<Document FilePath=""Program1.cs"">
class Program1
{
}
</Document>
<Document FilePath=""Program2.cs"">
class Program2
{
}
</Document>
</Project>
</Workspace>",
codeActionTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.WithDocumentText(document1.Id, SourceText.From("NewProgram1Content1"));
},
intermediaryTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.WithDocumentText(document1.Id, SourceText.From("NewProgram1Content2"));
});
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1419139")]
public async Task TestMakeTextChangeWithInterveningRemovalOfThatFile()
{
// This should fail as the code action is trying to edit a file that is subsequently removed.
await TestFailureApplicationAsync(
@"<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" CommonReferences=""true"">
<Document FilePath=""Program1.cs"">
class Program1
{
}
</Document>
<Document FilePath=""Program2.cs"">
class Program2
{
}
</Document>
</Project>
</Workspace>",
codeActionTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.WithDocumentText(document1.Id, SourceText.From("NewProgram1Content1"));
},
intermediaryTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.RemoveDocument(document1.Id);
});
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1419139")]
public async Task TestMakeProjectChangeWithInterveningTextEdit()
{
// This should fail as we don't want to make non-text changes that may have undesirable results to the solution
// given the intervening edits.
await TestFailureApplicationAsync(
@"<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" CommonReferences=""true"">
<Document FilePath=""Program1.cs"">
class Program1
{
}
</Document>
<Document FilePath=""Program2.cs"">
class Program2
{
}
</Document>
</Project>
</Workspace>",
codeActionTransform: solution =>
{
var document1 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program1"));
return solution.RemoveDocument(document1.Id);
},
intermediaryTransform: solution =>
{
var document2 = solution.Projects.Single().Documents.Single(d => d.FilePath!.Contains("Program2"));
return solution.WithDocumentText(document2.Id, SourceText.From("NewProgram1Content2"));
});
}
private async Task TestSuccessfulApplicationAsync(
string workspaceXml,
Func<Solution, Solution> codeActionTransform,
Func<Solution, Solution> intermediaryTransform)
{
await TestApplicationAsync(workspaceXml, codeActionTransform, intermediaryTransform, success: true);
}
private async Task TestFailureApplicationAsync(
string workspaceXml,
Func<Solution, Solution> codeActionTransform,
Func<Solution, Solution> intermediaryTransform)
{
await TestApplicationAsync(workspaceXml, codeActionTransform, intermediaryTransform, success: false);
}
private async Task TestApplicationAsync(
string workspaceXml,
Func<Solution, Solution> codeActionTransform,
Func<Solution, Solution> intermediaryTransform,
bool success)
{
var parameters = new TestParameters(fixProviderData: codeActionTransform);
using var workspace = CreateWorkspaceFromOptions(workspaceXml, parameters);
var originalSolution = workspace.CurrentSolution;
var document = GetDocument(workspace);
var provider = CreateCodeRefactoringProvider(workspace, parameters);
var refactorings = new List<CodeAction>();
var context = new CodeRefactoringContext(document, new TextSpan(), refactorings.Add, CancellationToken.None);
// Compute refactorings based on the original solution.
await provider.ComputeRefactoringsAsync(context);
var action = refactorings.Single();
var operations = await action.GetOperationsAsync(CancellationToken.None);
var operation = operations.Single();
// Now make an intermediary edit to the workspace that is applied back in.
var changedSolution = intermediaryTransform(originalSolution);
Assert.True(workspace.TryApplyChanges(changedSolution));
// Now try to apply the refactoring, even though an intervening edit happened.
var result = await operation.TryApplyAsync(workspace, originalSolution, CodeAnalysisProgress.None, CancellationToken.None);
Assert.Equal(success, result);
}
}
|