Skip to content

Commit a77fb5d

Browse files
Merge pull request #47 from tintoy/feature/client-tests
Add request-level tests for LspConnection
2 parents fe0a81a + ba583fd commit a77fb5d

File tree

6 files changed

+509
-8
lines changed

6 files changed

+509
-8
lines changed

src/Client/Processes/NamedPipeServerProcess.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ public override async Task Start()
9191
{
9292
ServerExitCompletion = new TaskCompletionSource<object>();
9393

94-
ServerInputStream = new NamedPipeServerStream(BaseName + "/in", PipeDirection.Out, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 1024, outBufferSize: 1024);
95-
ServerOutputStream = new NamedPipeServerStream(BaseName + "/out", PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 1024, outBufferSize: 1024);
96-
ClientInputStream = new NamedPipeClientStream(".", BaseName + "/out", PipeDirection.Out, PipeOptions.Asynchronous);
97-
ClientOutputStream = new NamedPipeClientStream(".", BaseName + "/in", PipeDirection.In, PipeOptions.Asynchronous);
94+
ServerInputStream = new NamedPipeServerStream(BaseName + "_in", PipeDirection.Out, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 1024, outBufferSize: 1024);
95+
ServerOutputStream = new NamedPipeServerStream(BaseName + "_out", PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 1024, outBufferSize: 1024);
96+
ClientInputStream = new NamedPipeClientStream(".", BaseName + "_out", PipeDirection.Out, PipeOptions.Asynchronous);
97+
ClientOutputStream = new NamedPipeClientStream(".", BaseName + "_in", PipeDirection.In, PipeOptions.Asynchronous);
9898

9999
// Ensure all pipes are connected before proceeding.
100100
await Task.WhenAll(

src/Client/Protocol/LspConnection.cs

+17-2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ public sealed class LspConnection
8484
/// </summary>
8585
int _nextRequestId = 0;
8686

87+
/// <summary>
88+
/// Has the connection been disposed?
89+
/// </summary>
90+
bool _disposed;
91+
8792
/// <summary>
8893
/// The cancellation source for the read and write loops.
8994
/// </summary>
@@ -158,9 +163,19 @@ public LspConnection(ILoggerFactory loggerFactory, Stream input, Stream output)
158163
/// </summary>
159164
public void Dispose()
160165
{
161-
Disconnect();
166+
if (_disposed)
167+
return;
162168

163-
_cancellationSource?.Dispose();
169+
try
170+
{
171+
Disconnect();
172+
173+
_cancellationSource?.Dispose();
174+
}
175+
finally
176+
{
177+
_disposed = true;
178+
}
164179
}
165180

166181
/// <summary>

test/Client.Tests/ClientTests.cs

+306
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
using Microsoft.Extensions.Logging;
2+
using OmniSharp.Extensions.LanguageServer.Capabilities.Server;
3+
using OmniSharp.Extensions.LanguageServer.Models;
4+
using OmniSharp.Extensions.LanguageServerProtocol.Client.Dispatcher;
5+
using OmniSharp.Extensions.LanguageServerProtocol.Client.Protocol;
6+
using OmniSharp.Extensions.LanguageServerProtocol.Client.Utilities;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
using Xunit;
11+
using Xunit.Abstractions;
12+
using System.Collections.Generic;
13+
using System;
14+
15+
namespace OmniSharp.Extensions.LanguageServerProtocol.Client.Tests
16+
{
17+
/// <summary>
18+
/// Tests for <see cref="LanguageClient"/>.
19+
/// </summary>
20+
public class ClientTests
21+
: PipeServerTestBase
22+
{
23+
/// <summary>
24+
/// Create a new <see cref="LanguageClient"/> test suite.
25+
/// </summary>
26+
/// <param name="testOutput">
27+
/// Output for the current test.
28+
/// </param>
29+
public ClientTests(ITestOutputHelper testOutput)
30+
: base(testOutput)
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Get an absolute document path for use in tests.
36+
/// </summary>
37+
string AbsoluteDocumentPath => IsWindows ? @"C:\Foo.txt" : "/Foo.txt";
38+
39+
/// <summary>
40+
/// The <see cref="LanguageClient"/> under test.
41+
/// </summary>
42+
LanguageClient LanguageClient { get; set; }
43+
44+
/// <summary>
45+
/// The server-side dispatcher.
46+
/// </summary>
47+
LspDispatcher ServerDispatcher { get; } = new LspDispatcher();
48+
49+
/// <summary>
50+
/// The server-side connection.
51+
/// </summary>
52+
LspConnection ServerConnection { get; set; }
53+
54+
/// <summary>
55+
/// Ensure that the language client can successfully request Hover information.
56+
/// </summary>
57+
[Fact(DisplayName = "Language client can successfully request hover info")]
58+
public async Task Hover_Success()
59+
{
60+
await Connect();
61+
62+
const int line = 5;
63+
const int column = 5;
64+
var expectedHoverContent = new MarkedStringContainer("123", "456", "789");
65+
66+
ServerDispatcher.HandleRequest<TextDocumentPositionParams, Hover>("textDocument/hover", (request, cancellationToken) =>
67+
{
68+
Assert.NotNull(request.TextDocument);
69+
70+
Assert.Equal(AbsoluteDocumentPath,
71+
DocumentUri.GetFileSystemPath(request.TextDocument.Uri)
72+
);
73+
74+
Assert.Equal(line, request.Position.Line);
75+
Assert.Equal(column, request.Position.Character);
76+
77+
return Task.FromResult(new Hover
78+
{
79+
Contents = expectedHoverContent,
80+
Range = new Range
81+
{
82+
Start = request.Position,
83+
End = request.Position
84+
}
85+
});
86+
});
87+
88+
Hover hover = await LanguageClient.TextDocument.Hover(AbsoluteDocumentPath, line, column);
89+
90+
Assert.NotNull(hover.Range);
91+
Assert.NotNull(hover.Range.Start);
92+
Assert.NotNull(hover.Range.End);
93+
94+
Assert.Equal(line, hover.Range.Start.Line);
95+
Assert.Equal(column, hover.Range.Start.Character);
96+
97+
Assert.Equal(line, hover.Range.End.Line);
98+
Assert.Equal(column, hover.Range.End.Character);
99+
100+
Assert.NotNull(hover.Contents);
101+
Assert.Equal(expectedHoverContent.Select(markedString => markedString.Value),
102+
hover.Contents.Select(
103+
markedString => markedString.Value
104+
)
105+
);
106+
}
107+
108+
/// <summary>
109+
/// Ensure that the language client can successfully request Completions.
110+
/// </summary>
111+
[Fact(DisplayName = "Language client can successfully request completions")]
112+
public async Task Completions_Success()
113+
{
114+
await Connect();
115+
116+
const int line = 5;
117+
const int column = 5;
118+
string expectedDocumentPath = AbsoluteDocumentPath;
119+
Uri expectedDocumentUri = DocumentUri.FromFileSystemPath(expectedDocumentPath);
120+
121+
var expectedCompletionItems = new CompletionItem[]
122+
{
123+
new CompletionItem
124+
{
125+
Kind = CompletionItemKind.Class,
126+
Label = "Class1",
127+
TextEdit = new TextEdit
128+
{
129+
Range = new Range
130+
{
131+
Start = new Position
132+
{
133+
Line = line,
134+
Character = column
135+
},
136+
End = new Position
137+
{
138+
Line = line,
139+
Character = column
140+
}
141+
},
142+
NewText = "Class1",
143+
}
144+
}
145+
};
146+
147+
ServerDispatcher.HandleRequest<TextDocumentPositionParams, CompletionList>("textDocument/completion", (request, cancellationToken) =>
148+
{
149+
Assert.NotNull(request.TextDocument);
150+
151+
Assert.Equal(expectedDocumentUri, request.TextDocument.Uri);
152+
153+
Assert.Equal(line, request.Position.Line);
154+
Assert.Equal(column, request.Position.Character);
155+
156+
return Task.FromResult(new CompletionList(
157+
expectedCompletionItems,
158+
isIncomplete: true
159+
));
160+
});
161+
162+
CompletionList actualCompletions = await LanguageClient.TextDocument.Completions(AbsoluteDocumentPath, line, column);
163+
164+
Assert.True(actualCompletions.IsIncomplete, "completions.IsIncomplete");
165+
Assert.NotNull(actualCompletions.Items);
166+
167+
CompletionItem[] actualCompletionItems = actualCompletions.Items.ToArray();
168+
Assert.Collection(actualCompletionItems, actualCompletionItem =>
169+
{
170+
CompletionItem expectedCompletionItem = expectedCompletionItems[0];
171+
172+
Assert.Equal(expectedCompletionItem.Kind, actualCompletionItem.Kind);
173+
Assert.Equal(expectedCompletionItem.Label, actualCompletionItem.Label);
174+
175+
Assert.NotNull(actualCompletionItem.TextEdit);
176+
Assert.Equal(expectedCompletionItem.TextEdit.NewText, actualCompletionItem.TextEdit.NewText);
177+
178+
Assert.NotNull(actualCompletionItem.TextEdit.Range);
179+
Assert.NotNull(actualCompletionItem.TextEdit.Range.Start);
180+
Assert.NotNull(actualCompletionItem.TextEdit.Range.End);
181+
Assert.Equal(expectedCompletionItem.TextEdit.Range.Start.Line, actualCompletionItem.TextEdit.Range.Start.Line);
182+
Assert.Equal(expectedCompletionItem.TextEdit.Range.Start.Character, actualCompletionItem.TextEdit.Range.Start.Character);
183+
Assert.Equal(expectedCompletionItem.TextEdit.Range.End.Line, actualCompletionItem.TextEdit.Range.End.Line);
184+
Assert.Equal(expectedCompletionItem.TextEdit.Range.End.Character, actualCompletionItem.TextEdit.Range.End.Character);
185+
});
186+
}
187+
188+
/// <summary>
189+
/// Ensure that the language client can successfully receive Diagnostics from the server.
190+
/// </summary>
191+
[Fact(DisplayName = "Language client can successfully receive diagnostics")]
192+
public async Task Diagnostics_Success()
193+
{
194+
await Connect();
195+
196+
string documentPath = AbsoluteDocumentPath;
197+
Uri expectedDocumentUri = DocumentUri.FromFileSystemPath(documentPath);
198+
List<Diagnostic> expectedDiagnostics = new List<Diagnostic>
199+
{
200+
new Diagnostic
201+
{
202+
Source = "Test",
203+
Code = new DiagnosticCode(1234),
204+
Message = "This is a diagnostic message.",
205+
Range = new Range
206+
{
207+
Start = new Position
208+
{
209+
Line = 2,
210+
Character = 5
211+
},
212+
End = new Position
213+
{
214+
Line = 3,
215+
Character = 7
216+
}
217+
},
218+
Severity = DiagnosticSeverity.Warning
219+
}
220+
};
221+
222+
TaskCompletionSource<object> receivedDiagnosticsNotification = new TaskCompletionSource<object>();
223+
224+
Uri actualDocumentUri = null;
225+
List<Diagnostic> actualDiagnostics = null;
226+
LanguageClient.TextDocument.OnPublishDiagnostics((documentUri, diagnostics) =>
227+
{
228+
actualDocumentUri = documentUri;
229+
actualDiagnostics = diagnostics;
230+
231+
receivedDiagnosticsNotification.SetResult(null);
232+
});
233+
234+
ServerConnection.SendNotification("textDocument/publishDiagnostics", new PublishDiagnosticsParams
235+
{
236+
Uri = DocumentUri.FromFileSystemPath(documentPath),
237+
Diagnostics = expectedDiagnostics
238+
});
239+
240+
// Timeout.
241+
Task winner = await Task.WhenAny(
242+
receivedDiagnosticsNotification.Task,
243+
Task.Delay(
244+
TimeSpan.FromSeconds(2)
245+
)
246+
);
247+
Assert.Same(receivedDiagnosticsNotification.Task, winner);
248+
249+
Assert.NotNull(actualDocumentUri);
250+
Assert.Equal(expectedDocumentUri, actualDocumentUri);
251+
252+
Assert.NotNull(actualDiagnostics);
253+
Assert.Equal(1, actualDiagnostics.Count);
254+
255+
Diagnostic expectedDiagnostic = expectedDiagnostics[0];
256+
Diagnostic actualDiagnostic = actualDiagnostics[0];
257+
258+
Assert.Equal(expectedDiagnostic.Code, actualDiagnostic.Code);
259+
Assert.Equal(expectedDiagnostic.Message, actualDiagnostic.Message);
260+
Assert.Equal(expectedDiagnostic.Range.Start.Line, actualDiagnostic.Range.Start.Line);
261+
Assert.Equal(expectedDiagnostic.Range.Start.Character, actualDiagnostic.Range.Start.Character);
262+
Assert.Equal(expectedDiagnostic.Range.End.Line, actualDiagnostic.Range.End.Line);
263+
Assert.Equal(expectedDiagnostic.Range.End.Character, actualDiagnostic.Range.End.Character);
264+
Assert.Equal(expectedDiagnostic.Severity, actualDiagnostic.Severity);
265+
Assert.Equal(expectedDiagnostic.Source, actualDiagnostic.Source);
266+
}
267+
268+
/// <summary>
269+
/// Connect the client and server.
270+
/// </summary>
271+
/// <param name="handleServerInitialize">
272+
/// Add standard handlers for server initialisation?
273+
/// </param>
274+
async Task Connect(bool handleServerInitialize = true)
275+
{
276+
ServerConnection = await CreateServerConnection();
277+
ServerConnection.Connect(ServerDispatcher);
278+
279+
if (handleServerInitialize)
280+
HandleServerInitialize();
281+
282+
LanguageClient = await CreateClient(initialize: true);
283+
}
284+
285+
/// <summary>
286+
/// Add standard handlers for sever initialisation.
287+
/// </summary>
288+
void HandleServerInitialize()
289+
{
290+
ServerDispatcher.HandleRequest<InitializeParams, InitializeResult>("initialize", (request, cancellationToken) =>
291+
{
292+
return Task.FromResult(new InitializeResult
293+
{
294+
Capabilities = new ServerCapabilities
295+
{
296+
HoverProvider = true
297+
}
298+
});
299+
});
300+
ServerDispatcher.HandleEmptyNotification("initialized", () =>
301+
{
302+
Log.LogInformation("Server initialized.");
303+
});
304+
}
305+
}
306+
}

0 commit comments

Comments
 (0)