diff --git a/src/features/codeLensProvider.ts b/src/features/codeLensProvider.ts index 2f58640e9..b20d4e877 100644 --- a/src/features/codeLensProvider.ts +++ b/src/features/codeLensProvider.ts @@ -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(); @@ -52,20 +51,23 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen 'ToString': true }; - provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable { - if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens) - { + async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken) { + if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens) { return []; } - return serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token).then(tree => { - let ret: vscode.CodeLens[] = []; - tree.TopLevelTypeDefinitions.forEach(node => this._convertQuickFix(ret, document.fileName, node)); - return ret; - }); + let tree = await serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token); + let ret: vscode.CodeLens[] = []; + + for (let node of tree.TopLevelTypeDefinitions) { + await this._convertQuickFix(ret, document.fileName, node); + } + + return ret; } - private _convertQuickFix(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): void { + + private async _convertQuickFix(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): Promise { if (node.Kind === 'MethodDeclaration' && OmniSharpCodeLensProvider.filteredSymbolNames[node.Location.Text]) { return; @@ -81,7 +83,7 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen } if (this._options.showTestsCodeLens) { - this._updateCodeLensForTest(bucket, fileName, node); + await this._updateCodeLensForTest(bucket, fileName, node); } } @@ -113,15 +115,64 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen } } - private _updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node) { + private async _updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): Promise { // backward compatible check: Features property doesn't present on older version OmniSharp if (node.Features === undefined) { return; } + if (node.Kind === "ClassDeclaration" && node.ChildNodes.length > 0) { + let projectInfo = await serverUtils.requestProjectInformation(this._server, { FileName: fileName }); + if (!projectInfo.DotNetProject && projectInfo.MsBuildProject) { + this._updateCodeLensForTestClass(bucket, fileName, node); + } + } + + let [testFeature, testFrameworkName] = this._getTestFeatureAndFramework(node); + if (testFeature) { + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] })); + + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { 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"))) { + return; + } + + let testMethods = new Array(); + let testFrameworkName: string = null; + for (let child of node.ChildNodes) { + let [testFeature, frameworkName] = this._getTestFeatureAndFramework(child); + if (testFeature) { + // this test method has a test feature + if (!testFrameworkName) { + testFrameworkName = frameworkName; + } + + testMethods.push(testFeature.Data); + } + } + + if (testMethods.length > 0) { + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { title: "run all tests", command: 'dotnet.classTests.run', arguments: [testMethods, fileName, testFrameworkName] })); + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { title: "debug all tests", command: 'dotnet.classTests.debug', arguments: [testMethods, fileName, testFrameworkName] })); + } + } + + private _getTestFeatureAndFramework(node: protocol.Node): [protocol.SyntaxFeature, string] { let testFeature = node.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod' || value.Name == 'MSTestMethod')); if (testFeature) { - // this test method has a test feature let testFrameworkName = 'xunit'; if (testFeature.Name == 'NUnitTestMethod') { testFrameworkName = 'nunit'; @@ -130,13 +181,9 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen testFrameworkName = 'mstest'; } - bucket.push(new vscode.CodeLens( - toRange(node.Location), - { title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] })); - - bucket.push(new vscode.CodeLens( - toRange(node.Location), - { title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] })); + return [testFeature, testFrameworkName]; } + + return [null, null]; } } diff --git a/src/features/dotnetTest.ts b/src/features/dotnetTest.ts index 27a2c0bac..7b0a53274 100644 --- a/src/features/dotnetTest.ts +++ b/src/features/dotnetTest.ts @@ -36,6 +36,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 +56,7 @@ export default class TestManager extends AbstractProvider { } }); - this.addDisposables(d1, d2, d3); + this.addDisposables(d1, d2, d3, d4, d5); } private _getOutputChannel(): vscode.OutputChannel { @@ -126,9 +134,11 @@ export default class TestManager extends AbstractProvider { private _reportResults(results: protocol.V2.DotNetTestResult[]): Promise { const totalTests = results.length; + const output = this._getOutputChannel(); let totalPassed = 0, totalFailed = 0, totalSkipped = 0; for (let result of results) { + output.appendLine(`${result.MethodName}: ${result.Outcome}`); switch (result.Outcome) { case protocol.V2.TestOutcomes.Failed: totalFailed += 1; @@ -142,7 +152,6 @@ 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(''); @@ -150,7 +159,28 @@ export default class TestManager extends AbstractProvider { 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; + } + 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,25 +191,9 @@ 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); - }) + return this._runTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion) .then(results => this._reportResults(results)) .then(() => listener.dispose()) .catch(reason => { @@ -188,6 +202,37 @@ export default class TestManager extends AbstractProvider { }); } + private async _runDotnetTestsInClass(methodsInClass: string[], fileName: string, testFrameworkName: string) { + 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(results => this._reportResults(results)) + .then(() => listener.dispose()) + .catch(reason => { + listener.dispose(); + vscode.window.showErrorMessage(`Failed to run tests because ${reason}.`); + }); + } + + private _runTestsInClass(fileName: string, testFrameworkName: string, targetFrameworkVersion: string, methodsToRun: string[]): Promise { + const request: protocol.V2.RunTestsInClassRequest = { + FileName: fileName, + TestFrameworkName: testFrameworkName, + TargetFrameworkVersion: targetFrameworkVersion, + MethodNames: methodsToRun + }; + + return serverUtils.runTestsInClass(this._server, request) + .then(response => response.Results); + } + private _createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) { let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebuggingOptions'); @@ -271,39 +316,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 +385,34 @@ export default class TestManager extends AbstractProvider { } }); } + + private _getLaunchConfigurationForClass(debugType: string, fileName: string, methodsToRun: string[], testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise { + 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 { + const output = this._getOutputChannel(); + + const listener = this._server.onTestMessage(e => { + output.appendLine(e.Message); + }); + + const request: protocol.V2.DebugTestClassGetStartInfoRequest = { + FileName: fileName, + MethodNames: 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 { diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index 94e0835e0..0e9480f5d 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -461,7 +461,9 @@ export namespace V2 { export const RunCodeAction = '/v2/runcodeaction'; export const GetTestStartInfo = '/v2/getteststartinfo'; export const RunTest = '/v2/runtest'; + export const RunAllTestsInClass = "/v2/runtestsinclass"; export const DebugTestGetStartInfo = '/v2/debugtest/getstartinfo'; + export const DebugTestsInClassGetStartInfo = '/v2/debugtestsinclass/getstartinfo'; export const DebugTestLaunch = '/v2/debugtest/launch'; export const DebugTestStop = '/v2/debugtest/stop'; } @@ -545,6 +547,12 @@ export namespace V2 { TargetFrameworkVersion: string; } + export interface DebugTestClassGetStartInfoRequest extends Request { + MethodNames: string[]; + TestFrameworkName: string; + TargetFrameworkVersion: string; + } + export interface DebugTestGetStartInfoResponse { FileName: string; Arguments: string; @@ -583,6 +591,12 @@ export namespace V2 { TargetFrameworkVersion: string; } + export interface RunTestsInClassRequest extends Request { + MethodNames: string[]; + TestFrameworkName: string; + TargetFrameworkVersion: string; + } + export module TestOutcomes { export const None = 'none'; export const Passed = 'passed'; diff --git a/src/omnisharp/utils.ts b/src/omnisharp/utils.ts index 6ae0391d0..27e329e91 100644 --- a/src/omnisharp/utils.ts +++ b/src/omnisharp/utils.ts @@ -93,10 +93,18 @@ export function runTest(server: OmniSharpServer, request: protocol.V2.RunTestReq return server.makeRequest(protocol.V2.Requests.RunTest, request); } +export function runTestsInClass(server: OmniSharpServer, request: protocol.V2.RunTestsInClassRequest) { + return server.makeRequest(protocol.V2.Requests.RunAllTestsInClass, request); +} + export function debugTestGetStartInfo(server: OmniSharpServer, request: protocol.V2.DebugTestGetStartInfoRequest) { return server.makeRequest(protocol.V2.Requests.DebugTestGetStartInfo, request); } +export function debugTestClassGetStartInfo(server: OmniSharpServer, request: protocol.V2.DebugTestClassGetStartInfoRequest) { + return server.makeRequest(protocol.V2.Requests.DebugTestsInClassGetStartInfo, request); +} + export function debugTestLaunch(server: OmniSharpServer, request: protocol.V2.DebugTestLaunchRequest) { return server.makeRequest(protocol.V2.Requests.DebugTestLaunch, request); }