Skip to content

Commit 4f9f6c7

Browse files
authored
"Run All Tests" and "Debug All Tests" (#1961)
* Run All Tests Running But Building repeatedly * Disposable variable for run all test * Added code for debug all tests * Code Cleaning * Run all Tests running - Better logs required * Run Tests running, all output shown at the end of all the tests * Renamed variable to methodsInClass * Changes for Debug All Tests * Changes for debug tests request * Debug All Tests running * Added common helpers for single test and all test functions * Improved logs for Run All Tests * Changes to get only 1 response for a set of methods * Extracted a common helper to get the test feature * Resolved review comments * Changes to not show this change for legacy projects * Renamed incorrect variable * Removing foreach for proper order of execution * Remove unnecessary import * Do not show the festure for legacy projects
1 parent 73e16a7 commit 4f9f6c7

File tree

4 files changed

+231
-64
lines changed

4 files changed

+231
-64
lines changed

src/features/codeLensProvider.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen
3131

3232
private _options: Options;
3333

34-
constructor(server: OmniSharpServer, reporter: TelemetryReporter, testManager: TestManager)
35-
{
34+
constructor(server: OmniSharpServer, reporter: TelemetryReporter, testManager: TestManager) {
3635
super(server, reporter);
3736

3837
this._resetCachedOptions();
@@ -52,20 +51,23 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen
5251
'ToString': true
5352
};
5453

55-
provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {
56-
if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens)
57-
{
54+
async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken) {
55+
if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens) {
5856
return [];
5957
}
6058

61-
return serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token).then(tree => {
62-
let ret: vscode.CodeLens[] = [];
63-
tree.TopLevelTypeDefinitions.forEach(node => this._convertQuickFix(ret, document.fileName, node));
64-
return ret;
65-
});
59+
let tree = await serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token);
60+
let ret: vscode.CodeLens[] = [];
61+
62+
for (let node of tree.TopLevelTypeDefinitions) {
63+
await this._convertQuickFix(ret, document.fileName, node);
64+
}
65+
66+
return ret;
6667
}
6768

68-
private _convertQuickFix(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): void {
69+
70+
private async _convertQuickFix(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): Promise<void> {
6971

7072
if (node.Kind === 'MethodDeclaration' && OmniSharpCodeLensProvider.filteredSymbolNames[node.Location.Text]) {
7173
return;
@@ -81,7 +83,7 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen
8183
}
8284

8385
if (this._options.showTestsCodeLens) {
84-
this._updateCodeLensForTest(bucket, fileName, node);
86+
await this._updateCodeLensForTest(bucket, fileName, node);
8587
}
8688
}
8789

@@ -113,15 +115,64 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen
113115
}
114116
}
115117

116-
private _updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node) {
118+
private async _updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): Promise<void> {
117119
// backward compatible check: Features property doesn't present on older version OmniSharp
118120
if (node.Features === undefined) {
119121
return;
120122
}
121123

124+
if (node.Kind === "ClassDeclaration" && node.ChildNodes.length > 0) {
125+
let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName });
126+
if (!projectInfo.DotNetProject && projectInfo.MsBuildProject) {
127+
this._updateCodeLensForTestClass(bucket, fileName, node);
128+
}
129+
}
130+
131+
let [testFeature, testFrameworkName] = this._getTestFeatureAndFramework(node);
132+
if (testFeature) {
133+
bucket.push(new vscode.CodeLens(
134+
toRange(node.Location),
135+
{ title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] }));
136+
137+
bucket.push(new vscode.CodeLens(
138+
toRange(node.Location),
139+
{ title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] }));
140+
}
141+
}
142+
143+
private _updateCodeLensForTestClass(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node) {
144+
// if the class doesnot contain any method then return
145+
if (!node.ChildNodes.find(value => (value.Kind === "MethodDeclaration"))) {
146+
return;
147+
}
148+
149+
let testMethods = new Array<string>();
150+
let testFrameworkName: string = null;
151+
for (let child of node.ChildNodes) {
152+
let [testFeature, frameworkName] = this._getTestFeatureAndFramework(child);
153+
if (testFeature) {
154+
// this test method has a test feature
155+
if (!testFrameworkName) {
156+
testFrameworkName = frameworkName;
157+
}
158+
159+
testMethods.push(testFeature.Data);
160+
}
161+
}
162+
163+
if (testMethods.length > 0) {
164+
bucket.push(new vscode.CodeLens(
165+
toRange(node.Location),
166+
{ title: "run all tests", command: 'dotnet.classTests.run', arguments: [testMethods, fileName, testFrameworkName] }));
167+
bucket.push(new vscode.CodeLens(
168+
toRange(node.Location),
169+
{ title: "debug all tests", command: 'dotnet.classTests.debug', arguments: [testMethods, fileName, testFrameworkName] }));
170+
}
171+
}
172+
173+
private _getTestFeatureAndFramework(node: protocol.Node): [protocol.SyntaxFeature, string] {
122174
let testFeature = node.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod' || value.Name == 'MSTestMethod'));
123175
if (testFeature) {
124-
// this test method has a test feature
125176
let testFrameworkName = 'xunit';
126177
if (testFeature.Name == 'NUnitTestMethod') {
127178
testFrameworkName = 'nunit';
@@ -130,13 +181,9 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen
130181
testFrameworkName = 'mstest';
131182
}
132183

133-
bucket.push(new vscode.CodeLens(
134-
toRange(node.Location),
135-
{ title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] }));
136-
137-
bucket.push(new vscode.CodeLens(
138-
toRange(node.Location),
139-
{ title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] }));
184+
return [testFeature, testFrameworkName];
140185
}
186+
187+
return [null, null];
141188
}
142189
}

src/features/dotnetTest.ts

Lines changed: 141 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ export default class TestManager extends AbstractProvider {
3636
'dotnet.test.debug',
3737
(testMethod, fileName, testFrameworkName) => this._debugDotnetTest(testMethod, fileName, testFrameworkName));
3838

39+
let d4 = vscode.commands.registerCommand(
40+
'dotnet.classTests.run',
41+
(methodsInClass, fileName, testFrameworkName) => this._runDotnetTestsInClass(methodsInClass, fileName, testFrameworkName));
42+
43+
let d5 = vscode.commands.registerCommand(
44+
'dotnet.classTests.debug',
45+
(methodsInClass, fileName, testFrameworkName) => this._debugDotnetTestsInClass(methodsInClass, fileName, testFrameworkName));
46+
3947
this._telemetryIntervalId = setInterval(() =>
4048
this._reportTelemetry(), TelemetryReportingDelay);
4149

@@ -48,7 +56,7 @@ export default class TestManager extends AbstractProvider {
4856
}
4957
});
5058

51-
this.addDisposables(d1, d2, d3);
59+
this.addDisposables(d1, d2, d3, d4, d5);
5260
}
5361

5462
private _getOutputChannel(): vscode.OutputChannel {
@@ -126,9 +134,11 @@ export default class TestManager extends AbstractProvider {
126134

127135
private _reportResults(results: protocol.V2.DotNetTestResult[]): Promise<void> {
128136
const totalTests = results.length;
137+
const output = this._getOutputChannel();
129138

130139
let totalPassed = 0, totalFailed = 0, totalSkipped = 0;
131140
for (let result of results) {
141+
output.appendLine(`${result.MethodName}: ${result.Outcome}`);
132142
switch (result.Outcome) {
133143
case protocol.V2.TestOutcomes.Failed:
134144
totalFailed += 1;
@@ -142,15 +152,35 @@ export default class TestManager extends AbstractProvider {
142152
}
143153
}
144154

145-
const output = this._getOutputChannel();
146155
output.appendLine('');
147156
output.appendLine(`Total tests: ${totalTests}. Passed: ${totalPassed}. Failed: ${totalFailed}. Skipped: ${totalSkipped}`);
148157
output.appendLine('');
149158

150159
return Promise.resolve();
151160
}
152161

153-
private _runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) {
162+
private async _recordRunAndGetFrameworkVersion(fileName: string, testFrameworkName: string) {
163+
164+
await this._saveDirtyFiles();
165+
this._recordRunRequest(testFrameworkName);
166+
let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName });
167+
168+
let targetFrameworkVersion: string;
169+
170+
if (projectInfo.DotNetProject) {
171+
targetFrameworkVersion = undefined;
172+
}
173+
else if (projectInfo.MsBuildProject) {
174+
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework;
175+
}
176+
else {
177+
throw new Error('Expected project.json or .csproj project.');
178+
}
179+
180+
return targetFrameworkVersion;
181+
}
182+
183+
private async _runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) {
154184
const output = this._getOutputChannel();
155185

156186
output.show();
@@ -161,25 +191,9 @@ export default class TestManager extends AbstractProvider {
161191
output.appendLine(e.Message);
162192
});
163193

164-
this._saveDirtyFiles()
165-
.then(_ => this._recordRunRequest(testFrameworkName))
166-
.then(_ => serverUtils.requestProjectInformation(this._server, { FileName: fileName }))
167-
.then(projectInfo =>
168-
{
169-
let targetFrameworkVersion: string;
170-
171-
if (projectInfo.DotNetProject) {
172-
targetFrameworkVersion = undefined;
173-
}
174-
else if (projectInfo.MsBuildProject) {
175-
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework;
176-
}
177-
else {
178-
throw new Error('Expected project.json or .csproj project.');
179-
}
194+
let targetFrameworkVersion = await this._recordRunAndGetFrameworkVersion(fileName, testFrameworkName);
180195

181-
return this._runTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion);
182-
})
196+
return this._runTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion)
183197
.then(results => this._reportResults(results))
184198
.then(() => listener.dispose())
185199
.catch(reason => {
@@ -188,6 +202,37 @@ export default class TestManager extends AbstractProvider {
188202
});
189203
}
190204

205+
private async _runDotnetTestsInClass(methodsInClass: string[], fileName: string, testFrameworkName: string) {
206+
const output = this._getOutputChannel();
207+
208+
output.show();
209+
const listener = this._server.onTestMessage(e => {
210+
output.appendLine(e.Message);
211+
});
212+
213+
let targetFrameworkVersion = await this._recordRunAndGetFrameworkVersion(fileName, testFrameworkName);
214+
215+
return this._runTestsInClass(fileName, testFrameworkName, targetFrameworkVersion, methodsInClass)
216+
.then(results => this._reportResults(results))
217+
.then(() => listener.dispose())
218+
.catch(reason => {
219+
listener.dispose();
220+
vscode.window.showErrorMessage(`Failed to run tests because ${reason}.`);
221+
});
222+
}
223+
224+
private _runTestsInClass(fileName: string, testFrameworkName: string, targetFrameworkVersion: string, methodsToRun: string[]): Promise<protocol.V2.DotNetTestResult[]> {
225+
const request: protocol.V2.RunTestsInClassRequest = {
226+
FileName: fileName,
227+
TestFrameworkName: testFrameworkName,
228+
TargetFrameworkVersion: targetFrameworkVersion,
229+
MethodNames: methodsToRun
230+
};
231+
232+
return serverUtils.runTestsInClass(this._server, request)
233+
.then(response => response.Results);
234+
}
235+
191236
private _createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) {
192237
let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebuggingOptions');
193238

@@ -271,39 +316,64 @@ export default class TestManager extends AbstractProvider {
271316
}
272317
}
273318

274-
private _debugDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) {
275-
// We support to styles of 'dotnet test' for debugging: The legacy 'project.json' testing, and the newer csproj support
276-
// using VS Test. These require a different level of communication.
319+
private async _recordDebugAndGetDebugValues(fileName: string, testFrameworkName: string, output: vscode.OutputChannel) {
320+
await this._saveDirtyFiles();
321+
this._recordDebugRequest(testFrameworkName);
322+
let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName });
323+
277324
let debugType: string;
278325
let debugEventListener: DebugEventListener = null;
279326
let targetFrameworkVersion: string;
280327

328+
if (projectInfo.DotNetProject) {
329+
debugType = 'legacy';
330+
targetFrameworkVersion = '';
331+
}
332+
else if (projectInfo.MsBuildProject) {
333+
debugType = 'vstest';
334+
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework;
335+
debugEventListener = new DebugEventListener(fileName, this._server, output);
336+
debugEventListener.start();
337+
}
338+
else {
339+
throw new Error('Expected project.json or .csproj project.');
340+
}
341+
342+
return { debugType, debugEventListener, targetFrameworkVersion };
343+
}
344+
345+
private async _debugDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) {
346+
// We support to styles of 'dotnet test' for debugging: The legacy 'project.json' testing, and the newer csproj support
347+
// using VS Test. These require a different level of communication.
348+
281349
const output = this._getOutputChannel();
282350

283351
output.show();
284352
output.appendLine(`Debugging method '${testMethod}'...`);
285353
output.appendLine('');
286354

287-
return this._saveDirtyFiles()
288-
.then(_ => this._recordDebugRequest(testFrameworkName))
289-
.then(_ => serverUtils.requestProjectInformation(this._server, { FileName: fileName }))
290-
.then(projectInfo => {
291-
if (projectInfo.DotNetProject) {
292-
debugType = 'legacy';
293-
targetFrameworkVersion = '';
294-
return Promise.resolve();
295-
}
296-
else if (projectInfo.MsBuildProject) {
297-
debugType = 'vstest';
298-
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework;
299-
debugEventListener = new DebugEventListener(fileName, this._server, output);
300-
return debugEventListener.start();
301-
}
302-
else {
303-
throw new Error('Expected project.json or .csproj project.');
304-
}
355+
let { debugType, debugEventListener, targetFrameworkVersion } = await this._recordDebugAndGetDebugValues(fileName, testFrameworkName, output);
356+
357+
return this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, targetFrameworkVersion, debugEventListener)
358+
.then(config => {
359+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fileName));
360+
return vscode.debug.startDebugging(workspaceFolder, config);
305361
})
306-
.then(() => this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, targetFrameworkVersion, debugEventListener))
362+
.catch(reason => {
363+
vscode.window.showErrorMessage(`Failed to start debugger: ${reason}`);
364+
if (debugEventListener != null) {
365+
debugEventListener.close();
366+
}
367+
});
368+
}
369+
370+
private async _debugDotnetTestsInClass(methodsToRun: string[], fileName: string, testFrameworkName: string) {
371+
372+
const output = this._getOutputChannel();
373+
374+
let { debugType, debugEventListener, targetFrameworkVersion } = await this._recordDebugAndGetDebugValues(fileName, testFrameworkName, output);
375+
376+
return await this._getLaunchConfigurationForClass(debugType, fileName, methodsToRun, testFrameworkName, targetFrameworkVersion, debugEventListener)
307377
.then(config => {
308378
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fileName));
309379
return vscode.debug.startDebugging(workspaceFolder, config);
@@ -315,6 +385,34 @@ export default class TestManager extends AbstractProvider {
315385
}
316386
});
317387
}
388+
389+
private _getLaunchConfigurationForClass(debugType: string, fileName: string, methodsToRun: string[], testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise<any> {
390+
if (debugType == 'vstest') {
391+
return this._getLaunchConfigurationForVSTestClass(fileName, methodsToRun, testFrameworkName, targetFrameworkVersion, debugEventListener);
392+
}
393+
throw new Error(`Unexpected debug type: ${debugType}`);
394+
}
395+
396+
private _getLaunchConfigurationForVSTestClass(fileName: string, methodsToRun: string[], testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise<any> {
397+
const output = this._getOutputChannel();
398+
399+
const listener = this._server.onTestMessage(e => {
400+
output.appendLine(e.Message);
401+
});
402+
403+
const request: protocol.V2.DebugTestClassGetStartInfoRequest = {
404+
FileName: fileName,
405+
MethodNames: methodsToRun,
406+
TestFrameworkName: testFrameworkName,
407+
TargetFrameworkVersion: targetFrameworkVersion
408+
};
409+
410+
return serverUtils.debugTestClassGetStartInfo(this._server, request)
411+
.then(response => {
412+
listener.dispose();
413+
return this._createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, debugEventListener.pipePath());
414+
});
415+
}
318416
}
319417

320418
class DebugEventListener {

0 commit comments

Comments
 (0)