Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeRobich committed Jan 11, 2025
1 parent 00d3a5a commit 5399edf
Show file tree
Hide file tree
Showing 9 changed files with 434 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@

<ItemGroup>
<ProjectReference Include="..\..\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj" />
<ProjectReference Include="..\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj"/>
<ProjectReference Include="..\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj" />
<ProjectReference Include="..\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj" />

<ProjectReference Include="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj" ReferenceOutputAssembly="false" Private="false" />
</ItemGroup>

<ProjectReference Include="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj"
ReferenceOutputAssembly="false"
Private="false" />
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
</ItemGroup>

<!--
Copy files contained in the NPM package to a DevKit subdirectory to emulate deployment of the DevKit extension in VS Code.
-->
<Target Name="_CopyDevKitExtensionFiles" AfterTargets="ResolveProjectReferences">
<MSBuild Projects="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj"
Targets="GetPackInputs">
<Output TaskParameter="TargetOutputs" ItemName="_DevKitExtensionFile"/>
<MSBuild Projects="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj" Targets="GetPackInputs">
<Output TaskParameter="TargetOutputs" ItemName="_DevKitExtensionFile" />
</MSBuild>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Roslyn.Test.Utilities;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Services;

public class ExtractRefactoringTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerHostTests(testOutputHelper)
{
[Theory]
[CombinatorialData]
public async Task TestExtractBaseClass(bool includeDevKitComponents)
{
var markup =
"""
class {|caret:A|}
{
public void M()
{
}
}
""";
var expected =
"""
internal class NewBaseType
{
public void M()
{
}
}
class A : NewBaseType
{
}
""";

await using var testLspServer = await CreateCSharpLanguageServerAsync(markup, includeDevKitComponents);
var caretLocation = testLspServer.GetLocations("caret").Single();

await TestCodeActionAsync(testLspServer, caretLocation, "Extract base class...", expected);
}

[Theory]
[CombinatorialData]
public async Task TestExtractInterface(bool includeDevKitComponents)
{
var markup =
"""
class {|caret:A|}
{
public void M()
{
}
}
""";
var expected =
"""
interface IA
{
void M();
}
class A : IA
{
public void M()
{
}
}
""";

await using var testLspServer = await CreateCSharpLanguageServerAsync(markup, includeDevKitComponents);
var caretLocation = testLspServer.GetLocations("caret").Single();

await TestCodeActionAsync(testLspServer, caretLocation, "Extract interface...", expected);
}

private static async Task TestCodeActionAsync(TestLspServer testLspServer, LSP.Location caretLocation, string codeActionTitle, [StringSyntax("c#-test")] string expected)
{
var codeActionResults = await testLspServer.RunGetCodeActionsAsync(CreateCodeActionParams(caretLocation));

var unresolvedCodeAction = Assert.Single(codeActionResults, codeAction => codeAction.Title == codeActionTitle);

var resolvedCodeAction = await testLspServer.RunGetCodeActionResolveAsync(unresolvedCodeAction);

Assert.True(testLspServer.TryApplyWorkspaceEdit(resolvedCodeAction.Edit));

var updatedCode = testLspServer.GetDocumentText(caretLocation.Uri);

AssertEx.Equal(expected, updatedCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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.Collections.Immutable;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Nerdbank.Streams;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using StreamJsonRpc;
using LSP = Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;

public partial class AbstractLanguageServerHostTests
{
internal sealed class TestLspServer : IAsyncDisposable
{
private readonly Dictionary<Uri, SourceText> _files;
private readonly Dictionary<string, IList<LSP.Location>> _locations;
private readonly Task _languageServerHostCompletionTask;
private readonly JsonRpc _clientRpc;

private ServerCapabilities? _serverCapabilities;

internal static async Task<TestLspServer> CreateAsync(
ClientCapabilities clientCapabilities,
TestOutputLogger logger,
string cacheDirectory,
bool includeDevKitComponents = true,
string[]? extensionPaths = null,
Dictionary<Uri, SourceText>? files = null,
Dictionary<string, IList<LSP.Location>>? locations = null)
{
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
logger.Factory, includeDevKitComponents, cacheDirectory, extensionPaths, out var serverConfiguration, out var assemblyLoader);
exportProvider.GetExportedValue<ServerConfigurationFactory>().InitializeConfiguration(serverConfiguration);

var testLspServer = new TestLspServer(exportProvider, logger, assemblyLoader, files ?? [], locations ?? []);
var initializeResponse = await testLspServer.Initialize(clientCapabilities);
Assert.NotNull(initializeResponse?.Capabilities);
testLspServer._serverCapabilities = initializeResponse!.Capabilities;

await testLspServer.Initialized();

return testLspServer;
}

internal LanguageServerHost LanguageServerHost { get; }
public ExportProvider ExportProvider { get; }

internal ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("Initialize has not been called");

private TestLspServer(ExportProvider exportProvider, TestOutputLogger logger, IAssemblyLoader assemblyLoader, Dictionary<Uri, SourceText> files, Dictionary<string, IList<LSP.Location>> locations)
{
_files = files;
_locations = locations;

var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, logger.Factory);

var (clientStream, serverStream) = FullDuplexStream.CreatePair();
LanguageServerHost = new LanguageServerHost(serverStream, serverStream, exportProvider, logger, typeRefResolver);

var messageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter();
_clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, messageFormatter))
{
AllowModificationWhileListening = true,
ExceptionStrategy = ExceptionProcessing.ISerializable,
};

_clientRpc.StartListening();

// This task completes when the server shuts down. We store it so that we can wait for completion
// when we dispose of the test server.
LanguageServerHost.Start();

_languageServerHostCompletionTask = LanguageServerHost.WaitForExitAsync();
ExportProvider = exportProvider;
}

public async Task<TResponseType?> ExecuteRequestAsync<TRequestType, TResponseType>(string methodName, TRequestType request, CancellationToken cancellationToken) where TRequestType : class
{
var result = await _clientRpc.InvokeWithParameterObjectAsync<TResponseType>(methodName, request, cancellationToken: cancellationToken);
return result;
}

public Task ExecuteNotificationAsync<RequestType>(string methodName, RequestType request) where RequestType : class
{
return _clientRpc.NotifyWithParameterObjectAsync(methodName, request);
}

public Task ExecuteNotification0Async(string methodName)
{
return _clientRpc.NotifyWithParameterObjectAsync(methodName);
}

public void AddClientLocalRpcTarget(object target)
{
_clientRpc.AddLocalRpcTarget(target);
}

public void AddClientLocalRpcTarget(string methodName, Delegate handler)
{
_clientRpc.AddLocalRpcMethod(methodName, handler);
}

public bool TryApplyWorkspaceEdit(WorkspaceEdit? workspaceEdit)
{
Assert.NotNull(workspaceEdit);

// We do not support applying the following edits
Assert.Null(workspaceEdit.Changes);
Assert.Null(workspaceEdit.ChangeAnnotations);

// Currently we only support applying TextDocumentEdits
var textDocumentEdits = (TextDocumentEdit[]?)workspaceEdit.DocumentChanges?.Value;
Assert.NotNull(textDocumentEdits);

foreach (var documentEdit in textDocumentEdits)
{
var uri = documentEdit.TextDocument.Uri;
var document = _files[uri];

var changes = documentEdit.Edits
.Select(edit => edit.Value)
.Cast<TextEdit>()
.SelectAsArray(edit => ProtocolConversions.TextEditToTextChange(edit, document));

var updatedDocument = document.WithChanges(changes);
_files[uri] = updatedDocument;
}

return true;
}

public string GetDocumentText(Uri uri) => _files[uri].ToString();

public IList<LSP.Location> GetLocations(string locationName) => _locations[locationName];

public async ValueTask DisposeAsync()
{
await _clientRpc.InvokeAsync(Methods.ShutdownName);
await _clientRpc.NotifyAsync(Methods.ExitName);

// The language server host task should complete once shutdown and exit are called.
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
await _languageServerHostCompletionTask;
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks

_clientRpc.Dispose();
}
}
}
Loading

0 comments on commit 5399edf

Please sign in to comment.