Skip to content

Commit 72b8022

Browse files
committed
Implement client side code for run tests command
1 parent 1c11b4e commit 72b8022

File tree

6 files changed

+218
-29
lines changed

6 files changed

+218
-29
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"strip-bom": "5.0.0",
9191
"strip-bom-buf": "2.0.0",
9292
"tmp": "0.0.33",
93+
"uuid": "^9.0.0",
9394
"vscode-html-languageservice": "^5.0.1",
9495
"vscode-js-debug-browsers": "^1.0.5",
9596
"vscode-jsonrpc": "8.2.0-next.0",
@@ -113,6 +114,7 @@
113114
"@types/node": "16.11.38",
114115
"@types/semver": "5.5.0",
115116
"@types/tmp": "0.0.33",
117+
"@types/uuid": "^9.0.1",
116118
"@types/unzipper": "^0.9.1",
117119
"@types/vscode": "1.69.0",
118120
"@types/yauzl": "2.10.0",

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
77
import * as fs from 'fs';
88
import * as path from 'path';
99
import * as cp from 'child_process';
10+
import * as uuid from 'uuid';
1011
import { registerCommands } from './commands';
1112
import { registerDebugger } from './debugger';
1213
import { UriConverter } from './uriConverter';
@@ -36,6 +37,8 @@ import {
3637
CompletionRequest,
3738
CompletionResolveRequest,
3839
CompletionItem,
40+
PartialResultParams,
41+
ProtocolRequestType,
3942
} from 'vscode-languageclient/node';
4043
import { PlatformInformation } from '../shared/platform';
4144
import { readConfigurations } from './configurationMiddleware';
@@ -57,6 +60,7 @@ import { randomUUID } from 'crypto';
5760
import { DotnetRuntimeExtensionResolver } from './dotnetRuntimeExtensionResolver';
5861
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
5962
import { RoslynLanguageClient } from './roslynLanguageClient';
63+
import { registerUnitTestingCommands } from './unitTesting';
6064

6165
let _languageServer: RoslynLanguageServer;
6266
let _channel: vscode.OutputChannel;
@@ -269,6 +273,28 @@ export class RoslynLanguageServer {
269273
return response;
270274
}
271275

276+
public async sendRequestWithProgress<P extends PartialResultParams, R, PR, E, RO>(
277+
type: ProtocolRequestType<P, R, PR, E, RO>,
278+
params: P,
279+
onProgress: (p: PR) => Promise<any>,
280+
cancellationToken?: vscode.CancellationToken
281+
): Promise<R> {
282+
if (!this._languageClient) {
283+
throw new Error('Tried to send request while server is not started.');
284+
}
285+
// Generate a UUID for our partial result token and apply it to our request.
286+
const partialResultToken: string = uuid.v4();
287+
params.partialResultToken = partialResultToken;
288+
// Register the callback for progress events.
289+
const disposable = this._languageClient.onProgress(type, partialResultToken, async (partialResult) => {
290+
await onProgress(partialResult);
291+
});
292+
const response = await this._languageClient
293+
.sendRequest(type, params, cancellationToken)
294+
.finally(() => disposable.dispose());
295+
return response;
296+
}
297+
272298
/**
273299
* Sends an LSP notification to the server with a given method and parameters.
274300
*/
@@ -423,6 +449,8 @@ export class RoslynLanguageServer {
423449
// shouldn't this arg only be set if it's running with CSDevKit?
424450
args.push('--telemetryLevel', this.telemetryReporter.telemetryLevel);
425451

452+
args.push('--extensionLogDirectory', this.context.logUri.fsPath);
453+
426454
let childProcess: cp.ChildProcessWithoutNullStreams;
427455
const cpOptions: cp.SpawnOptionsWithoutStdio = {
428456
detached: true,
@@ -574,6 +602,7 @@ export async function activateRoslynLanguageServer(
574602
platformInfo: PlatformInformation,
575603
optionProvider: OptionProvider,
576604
outputChannel: vscode.OutputChannel,
605+
dotnetTestChannel: vscode.OutputChannel,
577606
reporter: TelemetryReporter
578607
) {
579608
// Create a channel for outputting general logs from the language server.
@@ -589,6 +618,8 @@ export async function activateRoslynLanguageServer(
589618

590619
registerRazorCommands(context, _languageServer);
591620

621+
registerUnitTestingCommands(context, _languageServer, dotnetTestChannel);
622+
592623
// Register any needed debugger components that need to communicate with the language server.
593624
registerDebugger(context, _languageServer, platformInfo, optionProvider, _channel);
594625

src/lsptoolshost/roslynProtocol.ts

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import {
7-
FormattingOptions,
8-
InsertTextFormat,
9-
MessageDirection,
10-
NotificationType,
11-
Position,
12-
RequestType,
13-
RequestType0,
14-
TextDocumentIdentifier,
15-
TextEdit,
16-
URI,
17-
integer,
18-
} from 'vscode-languageserver-protocol';
6+
import * as lsp from 'vscode-languageserver-protocol';
197

208
export interface WorkspaceDebugConfigurationParams {
219
/**
2210
* Workspace path containing the solution/projects to get debug information for.
2311
* This will be important eventually for multi-workspace support.
2412
* If not provided, configurations are returned for the workspace the server was initialized for.
2513
*/
26-
workspacePath: URI | undefined;
14+
workspacePath: lsp.URI | undefined;
2715
}
2816

2917
export interface ProjectDebugConfiguration {
@@ -59,44 +47,96 @@ export interface ProjectDebugConfiguration {
5947
}
6048

6149
export interface OnAutoInsertParams {
62-
_vs_textDocument: TextDocumentIdentifier;
63-
_vs_position: Position;
50+
_vs_textDocument: lsp.TextDocumentIdentifier;
51+
_vs_position: lsp.Position;
6452
_vs_ch: string;
65-
_vs_options: FormattingOptions;
53+
_vs_options: lsp.FormattingOptions;
6654
}
6755

6856
export interface OnAutoInsertResponseItem {
69-
_vs_textEditFormat: InsertTextFormat;
70-
_vs_textEdit: TextEdit;
57+
_vs_textEditFormat: lsp.InsertTextFormat;
58+
_vs_textEdit: lsp.TextEdit;
7159
}
7260

7361
export interface RegisterSolutionSnapshotResponseItem {
7462
/**
7563
* Represents a solution snapshot.
7664
*/
77-
id: integer;
65+
id: lsp.integer;
66+
}
67+
68+
export interface RunTestsParams extends lsp.WorkDoneProgressParams, lsp.PartialResultParams {
69+
/**
70+
* The text document containing the tests to run.
71+
*/
72+
textDocument: lsp.TextDocumentIdentifier;
73+
74+
/**
75+
* The range encompasing the test methods to run.
76+
* Note that this does not have to only include tests, for example this could be a range representing a class.
77+
*/
78+
range: lsp.Range;
79+
}
80+
81+
export interface TestProgress {
82+
/**
83+
* The total number of tests passed at the time of the report.
84+
*/
85+
testsPassed: number;
86+
/**
87+
* The total number of tests failed at the time of the report.
88+
*/
89+
testsFailed: number;
90+
/**
91+
* The total number of tests skipped at the time of the report.
92+
*/
93+
testsSkipped: number;
94+
/**
95+
* The total number of tests that will eventually be run.
96+
*/
97+
totalTests: number;
98+
}
99+
100+
export interface RunTestsPartialResult {
101+
stage: string;
102+
message: string;
103+
progress?: TestProgress;
78104
}
79105

80106
export namespace WorkspaceDebugConfigurationRequest {
81107
export const method = 'workspace/debugConfiguration';
82-
export const messageDirection: MessageDirection = MessageDirection.clientToServer;
83-
export const type = new RequestType<WorkspaceDebugConfigurationParams, ProjectDebugConfiguration[], void>(method);
108+
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer;
109+
export const type = new lsp.RequestType<WorkspaceDebugConfigurationParams, ProjectDebugConfiguration[], void>(
110+
method
111+
);
84112
}
85113

86114
export namespace OnAutoInsertRequest {
87115
export const method = 'textDocument/_vs_onAutoInsert';
88-
export const messageDirection: MessageDirection = MessageDirection.clientToServer;
89-
export const type = new RequestType<OnAutoInsertParams, OnAutoInsertResponseItem, void>(method);
116+
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer;
117+
export const type = new lsp.RequestType<OnAutoInsertParams, OnAutoInsertResponseItem, void>(method);
90118
}
91119

92120
export namespace RegisterSolutionSnapshotRequest {
93121
export const method = 'workspace/_vs_registerSolutionSnapshot';
94-
export const messageDirection: MessageDirection = MessageDirection.clientToServer;
95-
export const type = new RequestType0<RegisterSolutionSnapshotResponseItem, void>(method);
122+
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer;
123+
export const type = new lsp.RequestType0<RegisterSolutionSnapshotResponseItem, void>(method);
96124
}
97125

98126
export namespace ProjectInitializationCompleteNotification {
99127
export const method = 'workspace/projectInitializationComplete';
100-
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
101-
export const type = new NotificationType(method);
128+
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.serverToClient;
129+
export const type = new lsp.NotificationType(method);
130+
}
131+
132+
export namespace RunTestsRequest {
133+
export const method = 'textDocument/runTests';
134+
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer;
135+
export const type = new lsp.ProtocolRequestType<
136+
RunTestsParams,
137+
RunTestsPartialResult[],
138+
RunTestsPartialResult,
139+
void,
140+
void
141+
>(method);
102142
}

src/lsptoolshost/unitTesting.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import * as languageClient from 'vscode-languageclient/node';
8+
import { RoslynLanguageServer } from './roslynLanguageServer';
9+
import { RunTestsParams, RunTestsPartialResult, RunTestsRequest } from './roslynProtocol';
10+
11+
export function registerUnitTestingCommands(
12+
context: vscode.ExtensionContext,
13+
languageServer: RoslynLanguageServer,
14+
dotnetTestChannel: vscode.OutputChannel
15+
) {
16+
context.subscriptions.push(
17+
vscode.commands.registerCommand('dotnet.test.run', async (request) =>
18+
runTests(request, languageServer, dotnetTestChannel)
19+
)
20+
);
21+
context.subscriptions.push(
22+
vscode.commands.registerTextEditorCommand(
23+
'dotnet.test.runTestsInContext',
24+
async (textEditor: vscode.TextEditor) => runTestsInContext(textEditor, languageServer, dotnetTestChannel)
25+
)
26+
);
27+
}
28+
29+
async function runTestsInContext(
30+
textEditor: vscode.TextEditor,
31+
languageServer: RoslynLanguageServer,
32+
dotnetTestChannel: vscode.OutputChannel
33+
) {
34+
const contextRange: languageClient.Range = { start: textEditor.selection.active, end: textEditor.selection.active };
35+
const textDocument: languageClient.TextDocumentIdentifier = { uri: textEditor.document.fileName };
36+
const request: RunTestsParams = { textDocument: textDocument, range: contextRange };
37+
await runTests(request, languageServer, dotnetTestChannel);
38+
}
39+
40+
async function runTests(
41+
request: RunTestsParams,
42+
languageServer: RoslynLanguageServer,
43+
dotnetTestChannel: vscode.OutputChannel
44+
) {
45+
dotnetTestChannel.show(true);
46+
vscode.window.withProgress(
47+
{
48+
location: vscode.ProgressLocation.Notification,
49+
title: 'Dotnet Test',
50+
cancellable: true,
51+
},
52+
async (progress, token) => {
53+
let totalReportedComplete = 0;
54+
const writeOutput = (output: RunTestsPartialResult) => {
55+
if (output.message) {
56+
dotnetTestChannel.appendLine(output.message);
57+
}
58+
59+
if (output.progress) {
60+
const totalTests = output.progress.totalTests;
61+
const completed =
62+
output.progress.testsPassed + output.progress.testsFailed + output.progress.testsSkipped;
63+
64+
// VSCode requires us to report the additional amount completed (in x out of 100) from this report compared to what we've previously reported.
65+
const reportIncrement = ((completed - totalReportedComplete) / totalTests) * 100;
66+
progress.report({ message: output.stage, increment: reportIncrement });
67+
totalReportedComplete = completed;
68+
} else {
69+
progress.report({ message: output.stage });
70+
}
71+
};
72+
73+
progress.report({ message: 'Saving files...' });
74+
// Ensure all files are saved before we run tests so they accurately reflect what the user has requested to run.
75+
await vscode.workspace.saveAll(/*includeUntitled*/ false);
76+
77+
progress.report({ message: 'Requesting server...' });
78+
const responsePromise = languageServer.sendRequestWithProgress(
79+
RunTestsRequest.type,
80+
request,
81+
async (p) => {
82+
writeOutput(p);
83+
},
84+
token
85+
);
86+
87+
await responsePromise.then(
88+
(result) => {
89+
result.forEach((r) => {
90+
writeOutput(r);
91+
});
92+
return;
93+
},
94+
(err) => {
95+
dotnetTestChannel.appendLine(err);
96+
return;
97+
}
98+
);
99+
}
100+
);
101+
}

0 commit comments

Comments
 (0)