-
Notifications
You must be signed in to change notification settings - Fork 682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
"Run All Tests" and "Debug All Tests" #1961
Changes from 15 commits
05aaa2d
e93efc8
9386779
70b126f
ee93887
f7b5eb8
8645f42
63aa55b
45792a5
c30ee75
75c9d64
2f33327
08337b5
ba21a1d
a6f6390
5a796b7
2eab3f2
c1eed78
d8ddfb6
1ec2448
128641d
f3d873e
7ca1540
6d6608d
5a62bfa
1d8f653
5b9b090
50fca56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,8 +31,7 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen | |
|
||
private _options: Options; | ||
|
||
constructor(server: OmniSharpServer, reporter: TelemetryReporter, testManager: TestManager) | ||
{ | ||
constructor(server: OmniSharpServer, reporter: TelemetryReporter, testManager: TestManager) { | ||
super(server, reporter); | ||
|
||
this._resetCachedOptions(); | ||
|
@@ -53,8 +52,7 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen | |
}; | ||
|
||
provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> { | ||
if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens) | ||
{ | ||
if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens) { | ||
return []; | ||
} | ||
|
||
|
@@ -119,6 +117,10 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen | |
return; | ||
} | ||
|
||
if (node.Kind == "ClassDeclaration" && node.ChildNodes.length > 0) { | ||
this._updateCodeLensForTestClass(bucket, fileName, node); | ||
} | ||
|
||
let testFeature = node.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod' || value.Name == 'MSTestMethod')); | ||
if (testFeature) { | ||
// this test method has a test feature | ||
|
@@ -139,4 +141,41 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen | |
{ title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] })); | ||
} | ||
} | ||
|
||
private _updateCodeLensForTestClass(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node) { | ||
//if the class doesnot contain any method then return | ||
if (!node.ChildNodes.find(value => (value.Kind == "MethodDeclaration"))) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return; | ||
} | ||
|
||
let testMethodsInClass = new Array<string>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the most idiomatic way to declare an empty array in TS? (@TheRealPiotrP ) |
||
let testFrameworkName: string = null; | ||
for (let child of node.ChildNodes) { | ||
if (child.Kind == "MethodDeclaration") { | ||
let testFeature = child.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod' || value.Name == 'MSTestMethod')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a copy-paste from line 124--let's extract a helper |
||
if (testFeature) { | ||
// this test method has a test feature | ||
if (testFrameworkName == null) { | ||
testFrameworkName = 'xunit'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible for someone to have tests for different frameworks in the same class? What happens if they do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. The code currently assumes that every test method in the class has the same frameworkname as the first testmethod in the class and the test request also contains only one field There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably fine then. Thanks for checking. |
||
if (testFeature.Name == 'NUnitTestMethod') { | ||
testFrameworkName = 'nunit'; | ||
} | ||
else if (testFeature.Name == 'MSTestMethod') { | ||
testFrameworkName = 'mstest'; | ||
} | ||
} | ||
testMethodsInClass.push(testFeature.Data); | ||
} | ||
} | ||
} | ||
|
||
if (testMethodsInClass.length) { | ||
bucket.push(new vscode.CodeLens( | ||
toRange(node.Location), | ||
{ title: "run all test", command: 'dotnet.classTests.run', arguments: [testMethodsInClass, fileName, testFrameworkName] })); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tests |
||
bucket.push(new vscode.CodeLens( | ||
toRange(node.Location), | ||
{ title: "debug all test", command: 'dotnet.classTests.debug', arguments: [testMethodsInClass, fileName, testFrameworkName] })); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tests |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,10 +14,12 @@ import * as os from 'os'; | |
import * as path from 'path'; | ||
import TelemetryReporter from 'vscode-extension-telemetry'; | ||
import AbstractProvider from './abstractProvider'; | ||
import { outputFileSync } from 'fs-extra'; | ||
|
||
const TelemetryReportingDelay = 2 * 60 * 1000; // two minutes | ||
|
||
export default class TestManager extends AbstractProvider { | ||
|
||
private _channel: vscode.OutputChannel; | ||
|
||
private _runCounts: { [testFrameworkName: string]: number }; | ||
|
@@ -36,6 +38,14 @@ export default class TestManager extends AbstractProvider { | |
'dotnet.test.debug', | ||
(testMethod, fileName, testFrameworkName) => this._debugDotnetTest(testMethod, fileName, testFrameworkName)); | ||
|
||
let d4 = vscode.commands.registerCommand( | ||
'dotnet.classTests.run', | ||
(methodsInClass, fileName, testFrameworkName) => this._runDotnetTestsInClass(methodsInClass, fileName, testFrameworkName, )); | ||
|
||
let d5 = vscode.commands.registerCommand( | ||
'dotnet.classTests.debug', | ||
(methodsInClass, fileName, testFrameworkName) => this._debugDotnetTestsInClass(methodsInClass, fileName, testFrameworkName)); | ||
|
||
this._telemetryIntervalId = setInterval(() => | ||
this._reportTelemetry(), TelemetryReportingDelay); | ||
|
||
|
@@ -48,7 +58,7 @@ export default class TestManager extends AbstractProvider { | |
} | ||
}); | ||
|
||
this.addDisposables(d1, d2, d3); | ||
this.addDisposables(d1, d2, d3, d4, d5); | ||
} | ||
|
||
private _getOutputChannel(): vscode.OutputChannel { | ||
|
@@ -117,18 +127,22 @@ export default class TestManager extends AbstractProvider { | |
FileName: fileName, | ||
MethodName: testMethod, | ||
TestFrameworkName: testFrameworkName, | ||
TargetFrameworkVersion: targetFrameworkVersion | ||
TargetFrameworkVersion: targetFrameworkVersion, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unintended comma? |
||
}; | ||
|
||
return serverUtils.runTest(this._server, request) | ||
.then(response => response.Results); | ||
} | ||
|
||
private _reportResults(results: protocol.V2.DotNetTestResult[]): Promise<void> { | ||
private _reportResults(results: protocol.V2.DotNetTestResult[], printEachResult: boolean): Promise<void> { | ||
const totalTests = results.length; | ||
const output = this._getOutputChannel(); | ||
|
||
let totalPassed = 0, totalFailed = 0, totalSkipped = 0; | ||
for (let result of results) { | ||
if (printEachResult) { | ||
output.appendLine(`${result.MethodName}: ${result.Outcome}`); | ||
} | ||
switch (result.Outcome) { | ||
case protocol.V2.TestOutcomes.Failed: | ||
totalFailed += 1; | ||
|
@@ -142,15 +156,35 @@ export default class TestManager extends AbstractProvider { | |
} | ||
} | ||
|
||
const output = this._getOutputChannel(); | ||
output.appendLine(''); | ||
output.appendLine(`Total tests: ${totalTests}. Passed: ${totalPassed}. Failed: ${totalFailed}. Skipped: ${totalSkipped}`); | ||
output.appendLine(''); | ||
|
||
return Promise.resolve(); | ||
} | ||
|
||
private _runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) { | ||
private async _recordRunAndGetFrameworkVersion(fileName: string, testFrameworkName: string) { | ||
|
||
await this._saveDirtyFiles(); | ||
this._recordRunRequest(testFrameworkName); | ||
let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName }); | ||
|
||
let targetFrameworkVersion: string; | ||
|
||
if (projectInfo.DotNetProject) { | ||
targetFrameworkVersion = undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wat There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the same as was previously being used: https://github.com/OmniSharp/omnisharp-vscode/blob/master/src/features/dotnetTest.ts#L172 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rchande: This is because we never pass the target framework in the legacy case. |
||
} | ||
else if (projectInfo.MsBuildProject) { | ||
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework; | ||
} | ||
else { | ||
throw new Error('Expected project.json or .csproj project.'); | ||
} | ||
|
||
return targetFrameworkVersion; | ||
} | ||
|
||
private async _runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) { | ||
const output = this._getOutputChannel(); | ||
|
||
output.show(); | ||
|
@@ -161,33 +195,54 @@ export default class TestManager extends AbstractProvider { | |
output.appendLine(e.Message); | ||
}); | ||
|
||
this._saveDirtyFiles() | ||
.then(_ => this._recordRunRequest(testFrameworkName)) | ||
.then(_ => serverUtils.requestProjectInformation(this._server, { FileName: fileName })) | ||
.then(projectInfo => | ||
{ | ||
let targetFrameworkVersion: string; | ||
|
||
if (projectInfo.DotNetProject) { | ||
targetFrameworkVersion = undefined; | ||
} | ||
else if (projectInfo.MsBuildProject) { | ||
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework; | ||
} | ||
else { | ||
throw new Error('Expected project.json or .csproj project.'); | ||
} | ||
let targetFrameworkVersion = await this._recordRunAndGetFrameworkVersion(fileName, testFrameworkName); | ||
|
||
return this._runTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion); | ||
}) | ||
.then(results => this._reportResults(results)) | ||
return this._runTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion) | ||
.then(results => this._reportResults(results, false)) | ||
.then(() => listener.dispose()) | ||
.catch(reason => { | ||
listener.dispose(); | ||
vscode.window.showErrorMessage(`Failed to run test because ${reason}.`); | ||
}); | ||
} | ||
|
||
private async _runDotnetTestsInClass(methodsInClass: string[], fileName: string, testFrameworkName: string) { | ||
let allResults: protocol.V2.DotNetTestResult[] = new Array(); | ||
const output = this._getOutputChannel(); | ||
|
||
output.show(); | ||
const listener = this._server.onTestMessage(e => { | ||
output.appendLine(e.Message); | ||
}); | ||
|
||
let targetFrameworkVersion = await this._recordRunAndGetFrameworkVersion(fileName, testFrameworkName); | ||
|
||
return this._runTestsInClass(fileName, testFrameworkName, targetFrameworkVersion, methodsInClass) | ||
.then(responses => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is already async, can we |
||
responses.forEach(response => { | ||
Array.prototype.push.apply(allResults, response.Results); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just allREsults.push? |
||
}); | ||
}).then(async () => { | ||
listener.dispose(); | ||
await this._reportResults(allResults, true); | ||
}) | ||
.catch(reason => { | ||
listener.dispose(); | ||
vscode.window.showErrorMessage(`Could not run tests`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do something with reason? |
||
}); | ||
} | ||
|
||
private _runTestsInClass(fileName: string, testFrameworkName: string, targetFrameworkVersion: string, methodsToRun: string[]): Promise<protocol.V2.RunTestResponse[]> { | ||
const request: protocol.V2.RunTestsInClassRequest = { | ||
FileName: fileName, | ||
TestFrameworkName: testFrameworkName, | ||
TargetFrameworkVersion: targetFrameworkVersion, | ||
MethodNamesInClass: methodsToRun | ||
}; | ||
|
||
return serverUtils.runTestsInClass(this._server, request); | ||
} | ||
|
||
private _createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) { | ||
let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebuggingOptions'); | ||
|
||
|
@@ -271,39 +326,64 @@ export default class TestManager extends AbstractProvider { | |
} | ||
} | ||
|
||
private _debugDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) { | ||
// We support to styles of 'dotnet test' for debugging: The legacy 'project.json' testing, and the newer csproj support | ||
// using VS Test. These require a different level of communication. | ||
private async _recordDebugAndGetDebugValues(fileName: string, testFrameworkName: string, output: vscode.OutputChannel) { | ||
await this._saveDirtyFiles(); | ||
this._recordDebugRequest(testFrameworkName); | ||
let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName }); | ||
|
||
let debugType: string; | ||
let debugEventListener: DebugEventListener = null; | ||
let targetFrameworkVersion: string; | ||
|
||
if (projectInfo.DotNetProject) { | ||
debugType = 'legacy'; | ||
targetFrameworkVersion = ''; | ||
} | ||
else if (projectInfo.MsBuildProject) { | ||
debugType = 'vstest'; | ||
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework; | ||
debugEventListener = new DebugEventListener(fileName, this._server, output); | ||
debugEventListener.start(); | ||
} | ||
else { | ||
throw new Error('Expected project.json or .csproj project.'); | ||
} | ||
|
||
return { debugType, debugEventListener, targetFrameworkVersion }; | ||
} | ||
|
||
private async _debugDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) { | ||
// We support to styles of 'dotnet test' for debugging: The legacy 'project.json' testing, and the newer csproj support | ||
// using VS Test. These require a different level of communication. | ||
|
||
const output = this._getOutputChannel(); | ||
|
||
output.show(); | ||
output.appendLine(`Debugging method '${testMethod}'...`); | ||
output.appendLine(''); | ||
|
||
return this._saveDirtyFiles() | ||
.then(_ => this._recordDebugRequest(testFrameworkName)) | ||
.then(_ => serverUtils.requestProjectInformation(this._server, { FileName: fileName })) | ||
.then(projectInfo => { | ||
if (projectInfo.DotNetProject) { | ||
debugType = 'legacy'; | ||
targetFrameworkVersion = ''; | ||
return Promise.resolve(); | ||
} | ||
else if (projectInfo.MsBuildProject) { | ||
debugType = 'vstest'; | ||
targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework; | ||
debugEventListener = new DebugEventListener(fileName, this._server, output); | ||
return debugEventListener.start(); | ||
} | ||
else { | ||
throw new Error('Expected project.json or .csproj project.'); | ||
} | ||
let { debugType, debugEventListener, targetFrameworkVersion } = await this._recordDebugAndGetDebugValues(fileName, testFrameworkName, output); | ||
|
||
return this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, targetFrameworkVersion, debugEventListener) | ||
.then(config => { | ||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fileName)); | ||
return vscode.debug.startDebugging(workspaceFolder, config); | ||
}) | ||
.then(() => this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, targetFrameworkVersion, debugEventListener)) | ||
.catch(reason => { | ||
vscode.window.showErrorMessage(`Failed to start debugger: ${reason}`); | ||
if (debugEventListener != null) { | ||
debugEventListener.close(); | ||
} | ||
}); | ||
} | ||
|
||
private async _debugDotnetTestsInClass(methodsToRun: string[], fileName: string, testFrameworkName: string) { | ||
|
||
const output = this._getOutputChannel(); | ||
|
||
let { debugType, debugEventListener, targetFrameworkVersion } = await this._recordDebugAndGetDebugValues(fileName, testFrameworkName, output); | ||
|
||
return await this._getLaunchConfigurationForClass(debugType, fileName, methodsToRun, testFrameworkName, targetFrameworkVersion, debugEventListener) | ||
.then(config => { | ||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fileName)); | ||
return vscode.debug.startDebugging(workspaceFolder, config); | ||
|
@@ -315,6 +395,34 @@ export default class TestManager extends AbstractProvider { | |
} | ||
}); | ||
} | ||
|
||
private _getLaunchConfigurationForClass(debugType: string, fileName: string, methodsToRun: string[], testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise<any> { | ||
if (debugType == 'vstest') { | ||
return this._getLaunchConfigurationForVSTestClass(fileName, methodsToRun, testFrameworkName, targetFrameworkVersion, debugEventListener); | ||
} | ||
throw new Error(`Unexpected debug type: ${debugType}`); | ||
} | ||
|
||
private _getLaunchConfigurationForVSTestClass(fileName: string, methodsToRun: string[], testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise<any> { | ||
const output = this._getOutputChannel(); | ||
|
||
const listener = this._server.onTestMessage(e => { | ||
output.appendLine(e.Message); | ||
}); | ||
|
||
const request: protocol.V2.DebugTestClassGetStartInfoRequest = { | ||
FileName: fileName, | ||
MethodsInClass: methodsToRun, | ||
TestFrameworkName: testFrameworkName, | ||
TargetFrameworkVersion: targetFrameworkVersion | ||
}; | ||
|
||
return serverUtils.debugTestClassGetStartInfo(this._server, request) | ||
.then(response => { | ||
listener.dispose(); | ||
return this._createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, debugEventListener.pipePath()); | ||
}); | ||
} | ||
} | ||
|
||
class DebugEventListener { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
space after //