Skip to content

Commit b4582d8

Browse files
committed
Add tests
1 parent 00d3a5a commit b4582d8

File tree

10 files changed

+427
-77
lines changed

10 files changed

+427
-77
lines changed

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@
1717

1818
<ItemGroup>
1919
<ProjectReference Include="..\..\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj" />
20-
<ProjectReference Include="..\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj"/>
20+
<ProjectReference Include="..\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj" />
21+
<ProjectReference Include="..\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj" />
22+
23+
<ProjectReference Include="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj" ReferenceOutputAssembly="false" Private="false" />
24+
</ItemGroup>
2125

22-
<ProjectReference Include="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj"
23-
ReferenceOutputAssembly="false"
24-
Private="false" />
26+
<ItemGroup>
27+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
2528
</ItemGroup>
2629

2730
<!--
2831
Copy files contained in the NPM package to a DevKit subdirectory to emulate deployment of the DevKit extension in VS Code.
2932
-->
3033
<Target Name="_CopyDevKitExtensionFiles" AfterTargets="ResolveProjectReferences">
31-
<MSBuild Projects="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj"
32-
Targets="GetPackInputs">
33-
<Output TaskParameter="TargetOutputs" ItemName="_DevKitExtensionFile"/>
34+
<MSBuild Projects="..\..\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj" Targets="GetPackInputs">
35+
<Output TaskParameter="TargetOutputs" ItemName="_DevKitExtensionFile" />
3436
</MSBuild>
3537

3638
<ItemGroup>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.CodeAnalysis;
7+
using Roslyn.Test.Utilities;
8+
using Xunit.Abstractions;
9+
using LSP = Roslyn.LanguageServer.Protocol;
10+
11+
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Services;
12+
13+
public class ExtractRefactoringTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerHostTests(testOutputHelper)
14+
{
15+
[Theory]
16+
[CombinatorialData]
17+
public async Task TestExtractBaseClass(bool includeDevKitComponents)
18+
{
19+
var markup =
20+
"""
21+
class {|caret:A|}
22+
{
23+
public void M()
24+
{
25+
}
26+
}
27+
""";
28+
var expected =
29+
"""
30+
internal class NewBaseType
31+
{
32+
public void M()
33+
{
34+
}
35+
}
36+
37+
class A : NewBaseType
38+
{
39+
}
40+
""";
41+
42+
await using var testLspServer = await CreateCSharpLanguageServerAsync(markup, includeDevKitComponents);
43+
var caretLocation = testLspServer.GetLocations("caret").Single();
44+
45+
await TestCodeActionAsync(testLspServer, caretLocation, "Extract base class...", expected);
46+
}
47+
48+
[Theory]
49+
[CombinatorialData]
50+
public async Task TestExtractInterface(bool includeDevKitComponents)
51+
{
52+
var markup =
53+
"""
54+
class {|caret:A|}
55+
{
56+
public void M()
57+
{
58+
}
59+
}
60+
""";
61+
var expected =
62+
"""
63+
interface IA
64+
{
65+
void M();
66+
}
67+
68+
class A : IA
69+
{
70+
public void M()
71+
{
72+
}
73+
}
74+
""";
75+
76+
await using var testLspServer = await CreateCSharpLanguageServerAsync(markup, includeDevKitComponents);
77+
var caretLocation = testLspServer.GetLocations("caret").Single();
78+
79+
await TestCodeActionAsync(testLspServer, caretLocation, "Extract interface...", expected);
80+
}
81+
82+
private static async Task TestCodeActionAsync(
83+
TestLspServer testLspServer,
84+
LSP.Location caretLocation,
85+
string codeActionTitle,
86+
[StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string expected)
87+
{
88+
var codeActionResults = await testLspServer.RunGetCodeActionsAsync(CreateCodeActionParams(caretLocation));
89+
90+
var unresolvedCodeAction = Assert.Single(codeActionResults, codeAction => codeAction.Title == codeActionTitle);
91+
92+
var resolvedCodeAction = await testLspServer.RunGetCodeActionResolveAsync(unresolvedCodeAction);
93+
94+
testLspServer.ApplyWorkspaceEdit(resolvedCodeAction.Edit);
95+
96+
var updatedCode = testLspServer.GetDocumentText(caretLocation.Uri);
97+
98+
AssertEx.Equal(expected, updatedCode);
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
7+
using Microsoft.CodeAnalysis.Text;
8+
using Microsoft.VisualStudio.Composition;
9+
using Nerdbank.Streams;
10+
using Roslyn.LanguageServer.Protocol;
11+
using Roslyn.Utilities;
12+
using StreamJsonRpc;
13+
using LSP = Roslyn.LanguageServer.Protocol;
14+
15+
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
16+
17+
public partial class AbstractLanguageServerHostTests
18+
{
19+
internal sealed class TestLspServer : IAsyncDisposable
20+
{
21+
private readonly Dictionary<Uri, SourceText> _documents;
22+
private readonly Dictionary<string, IList<LSP.Location>> _locations;
23+
private readonly Task _languageServerHostCompletionTask;
24+
private readonly JsonRpc _clientRpc;
25+
26+
private ServerCapabilities? _serverCapabilities;
27+
28+
internal static async Task<TestLspServer> CreateAsync(
29+
ClientCapabilities clientCapabilities,
30+
TestOutputLogger logger,
31+
string cacheDirectory,
32+
bool includeDevKitComponents = true,
33+
string[]? extensionPaths = null,
34+
Dictionary<Uri, SourceText>? documents = null,
35+
Dictionary<string, IList<LSP.Location>>? locations = null)
36+
{
37+
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
38+
logger.Factory, includeDevKitComponents, cacheDirectory, extensionPaths, out var serverConfiguration, out var assemblyLoader);
39+
exportProvider.GetExportedValue<ServerConfigurationFactory>().InitializeConfiguration(serverConfiguration);
40+
41+
var testLspServer = new TestLspServer(exportProvider, logger, assemblyLoader, documents ?? [], locations ?? []);
42+
var initializeResponse = await testLspServer.Initialize(clientCapabilities);
43+
Assert.NotNull(initializeResponse?.Capabilities);
44+
testLspServer._serverCapabilities = initializeResponse!.Capabilities;
45+
46+
await testLspServer.Initialized();
47+
48+
return testLspServer;
49+
}
50+
51+
internal LanguageServerHost LanguageServerHost { get; }
52+
public ExportProvider ExportProvider { get; }
53+
54+
internal ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("Initialize has not been called");
55+
56+
private TestLspServer(ExportProvider exportProvider, TestOutputLogger logger, IAssemblyLoader assemblyLoader, Dictionary<Uri, SourceText> documents, Dictionary<string, IList<LSP.Location>> locations)
57+
{
58+
_documents = documents;
59+
_locations = locations;
60+
61+
var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, logger.Factory);
62+
63+
var (clientStream, serverStream) = FullDuplexStream.CreatePair();
64+
LanguageServerHost = new LanguageServerHost(serverStream, serverStream, exportProvider, logger, typeRefResolver);
65+
66+
var messageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter();
67+
_clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, messageFormatter))
68+
{
69+
AllowModificationWhileListening = true,
70+
ExceptionStrategy = ExceptionProcessing.ISerializable,
71+
};
72+
73+
_clientRpc.StartListening();
74+
75+
// This task completes when the server shuts down. We store it so that we can wait for completion
76+
// when we dispose of the test server.
77+
LanguageServerHost.Start();
78+
79+
_languageServerHostCompletionTask = LanguageServerHost.WaitForExitAsync();
80+
ExportProvider = exportProvider;
81+
}
82+
83+
public async Task<TResponseType?> ExecuteRequestAsync<TRequestType, TResponseType>(string methodName, TRequestType request, CancellationToken cancellationToken) where TRequestType : class
84+
{
85+
var result = await _clientRpc.InvokeWithParameterObjectAsync<TResponseType>(methodName, request, cancellationToken: cancellationToken);
86+
return result;
87+
}
88+
89+
public Task ExecuteNotificationAsync<RequestType>(string methodName, RequestType request) where RequestType : class
90+
{
91+
return _clientRpc.NotifyWithParameterObjectAsync(methodName, request);
92+
}
93+
94+
public Task ExecuteNotification0Async(string methodName)
95+
{
96+
return _clientRpc.NotifyWithParameterObjectAsync(methodName);
97+
}
98+
99+
public void AddClientLocalRpcTarget(object target)
100+
{
101+
_clientRpc.AddLocalRpcTarget(target);
102+
}
103+
104+
public void AddClientLocalRpcTarget(string methodName, Delegate handler)
105+
{
106+
_clientRpc.AddLocalRpcMethod(methodName, handler);
107+
}
108+
109+
public void ApplyWorkspaceEdit(WorkspaceEdit? workspaceEdit)
110+
{
111+
Assert.NotNull(workspaceEdit);
112+
113+
// We do not support applying the following edits
114+
Assert.Null(workspaceEdit.Changes);
115+
Assert.Null(workspaceEdit.ChangeAnnotations);
116+
117+
// Currently we only support applying TextDocumentEdits
118+
var textDocumentEdits = (TextDocumentEdit[]?)workspaceEdit.DocumentChanges?.Value;
119+
Assert.NotNull(textDocumentEdits);
120+
121+
foreach (var documentEdit in textDocumentEdits)
122+
{
123+
var uri = documentEdit.TextDocument.Uri;
124+
var document = _documents[uri];
125+
126+
var changes = documentEdit.Edits
127+
.Select(edit => edit.Value)
128+
.Cast<TextEdit>()
129+
.SelectAsArray(edit => ProtocolConversions.TextEditToTextChange(edit, document));
130+
131+
var updatedDocument = document.WithChanges(changes);
132+
_documents[uri] = updatedDocument;
133+
}
134+
}
135+
136+
public string GetDocumentText(Uri uri) => _documents[uri].ToString();
137+
138+
public IList<LSP.Location> GetLocations(string locationName) => _locations[locationName];
139+
140+
public async ValueTask DisposeAsync()
141+
{
142+
await _clientRpc.InvokeAsync(Methods.ShutdownName);
143+
await _clientRpc.NotifyAsync(Methods.ExitName);
144+
145+
// The language server host task should complete once shutdown and exit are called.
146+
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
147+
await _languageServerHostCompletionTask;
148+
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
149+
150+
_clientRpc.Dispose();
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)