Integration tests for code actions (#15142)

This commit is contained in:
Petr 2023-05-02 15:04:20 +02:00 коммит произвёл GitHub
Родитель 8746c3adb4
Коммит ec122d90ec
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 192 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,103 @@
// 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 Microsoft.CodeAnalysis.Testing;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace FSharp.Editor.IntegrationTests;
public class CodeActionTests : AbstractIntegrationTest
{
[IdeFact]
public async Task UnusedOpenDeclarations()
{
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
var code = """
module Library
open System
let x = 42
""";
await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
await Editor.SetTextAsync(code, TestToken);
await Editor.PlaceCaretAsync("open System", TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);
var codeActions = await Editor.InvokeCodeActionListAsync(TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);
Assert.Single(codeActions);
var actionSet = codeActions.Single();
Assert.Equal("CodeFix", actionSet.CategoryName);
Assert.Single(actionSet.Actions);
var codeFix = actionSet.Actions.Single();
Assert.Equal("Remove unused open declarations", codeFix.DisplayText);
}
[IdeFact]
public async Task AddMissingFunKeyword()
{
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
var code = """
module Library
let original = []
let transformed = original |> List.map (x -> x)
""";
await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
await Editor.SetTextAsync(code, TestToken);
await Editor.PlaceCaretAsync("->", TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);
var codeActions = await Editor.InvokeCodeActionListAsync(TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);
Assert.Single(codeActions);
var actionSet = codeActions.Single();
Assert.Equal("ErrorFix", actionSet.CategoryName);
Assert.Single(actionSet.Actions);
var errorFix = actionSet.Actions.Single();
Assert.Equal("Add missing 'fun' keyword", errorFix.DisplayText);
}
[IdeFact]
public async Task AddNewKeywordToDisposables()
{
var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
var code = """
module Library
let sr = System.IO.StreamReader("")
""";
await SolutionExplorer.CreateSingleProjectSolutionAsync("Library", template, TestToken);
await SolutionExplorer.RestoreNuGetPackagesAsync(TestToken);
await Editor.SetTextAsync(code, TestToken);
await Editor.PlaceCaretAsync("let sr", TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);
var codeActions = await Editor.InvokeCodeActionListAsync(TestToken);
await Workspace.WaitForProjectSystemAsync(TestToken);
Assert.Single(codeActions);
var actionSet = codeActions.Single();
Assert.Equal("CodeFix", actionSet.CategoryName);
Assert.Single(actionSet.Actions);
var codeFix = actionSet.Actions.Single();
Assert.Equal("Add 'new' keyword", codeFix.DisplayText);
}
}

Просмотреть файл

@ -0,0 +1,66 @@
// 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 Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace FSharp.Editor.IntegrationTests.Helpers
{
// I stole this voodoo from Razor and removed the obscurest bits
internal static class LightBulbHelper
{
public static async Task<IEnumerable<SuggestedActionSet>> WaitForItemsAsync(
ILightBulbBroker broker,
IWpfTextView view,
CancellationToken cancellationToken)
{
var activeSession = broker.GetSession(view);
var asyncSession = (IAsyncLightBulbSession)activeSession;
var tcs = new TaskCompletionSource<IEnumerable<SuggestedActionSet>>();
void Handler(object s, SuggestedActionsUpdatedArgs e)
{
// ignore these. we care about when the lightbulb items are all completed.
if (e.Status == QuerySuggestedActionCompletionStatus.InProgress)
{
return;
}
if (e.Status == QuerySuggestedActionCompletionStatus.Completed ||
e.Status == QuerySuggestedActionCompletionStatus.CompletedWithoutData)
{
tcs.SetResult(e.ActionSets);
}
else
{
tcs.SetException(new InvalidOperationException($"Light bulb transitioned to non-complete state: {e.Status}"));
}
asyncSession.SuggestedActionsUpdated -= Handler;
}
asyncSession.SuggestedActionsUpdated += Handler;
asyncSession.Dismissed += (_, _) => tcs.TrySetCanceled(new CancellationToken(true));
if (asyncSession.IsDismissed)
{
tcs.TrySetCanceled(new CancellationToken(true));
}
// Calling PopulateWithDataAsync ensures the underlying session will call SuggestedActionsUpdated at least once
// with the latest data computed. This is needed so that if the lightbulb computation is already complete
// that we hear about the results.
await asyncSession.PopulateWithDataAsync(overrideRequestedActionCategories: null, operationContext: null).ConfigureAwait(false);
return await tcs.Task.WithCancellation(cancellationToken);
}
}
}

Просмотреть файл

@ -3,9 +3,13 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FSharp.Editor.IntegrationTests.Extensions;
using FSharp.Editor.IntegrationTests.Helpers;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
@ -68,4 +72,23 @@ internal partial class EditorInProcess
view.Selection.Clear();
}
public async Task<IEnumerable<SuggestedActionSet>> InvokeCodeActionListAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var shell = await GetRequiredGlobalServiceAsync<SVsUIShell, IVsUIShell>(cancellationToken);
var cmdGroup = typeof(VSConstants.VSStd14CmdID).GUID;
var cmdExecOpt = OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
var cmdID = VSConstants.VSStd14CmdID.ShowQuickFixes;
object? obj = null;
shell.PostExecCommand(cmdGroup, (uint)cmdID, (uint)cmdExecOpt, ref obj);
var view = await GetActiveTextViewAsync(cancellationToken);
var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);
var lightbulbs = await LightBulbHelper.WaitForItemsAsync(broker, view, cancellationToken);
return lightbulbs;
}
}