diff --git a/.gitignore b/.gitignore index 75442c2235..8229cf2d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ bin node_modules out +.omnisharp/ .omnisharp-*/ .debugger .vscode-test diff --git a/src/features/abstractProvider.ts b/src/features/abstractProvider.ts index 9e7785d7bc..d1d9a8952b 100644 --- a/src/features/abstractProvider.ts +++ b/src/features/abstractProvider.ts @@ -5,19 +5,26 @@ 'use strict'; -import {OmniSharpServer} from '../omnisharp/server'; -import {Disposable} from 'vscode'; +import { OmniSharpServer } from '../omnisharp/server'; +import { Disposable } from 'vscode'; +import TelemetryReporter from 'vscode-extension-telemetry'; -export default class AbstractProvider { +export default abstract class AbstractProvider { protected _server: OmniSharpServer; - protected _disposables: Disposable[]; + protected _reporter: TelemetryReporter; + private _disposables: Disposable[]; - constructor(server: OmniSharpServer) { + constructor(server: OmniSharpServer, reporter: TelemetryReporter) { this._server = server; + this._reporter = reporter; this._disposables = []; } + protected addDisposables(...disposables: Disposable[]) { + this._disposables.push(...disposables); + } + dispose() { while (this._disposables.length) { this._disposables.pop().dispose(); diff --git a/src/features/codeActionProvider.ts b/src/features/codeActionProvider.ts index 073dc584df..f48803e419 100644 --- a/src/features/codeActionProvider.ts +++ b/src/features/codeActionProvider.ts @@ -11,21 +11,23 @@ import AbstractProvider from './abstractProvider'; import * as protocol from '../omnisharp/protocol'; import { toRange2 } from '../omnisharp/typeConvertion'; import * as serverUtils from '../omnisharp/utils'; +import TelemetryReporter from 'vscode-extension-telemetry'; export default class CodeActionProvider extends AbstractProvider implements vscode.CodeActionProvider { private _disabled: boolean; private _commandId: string; - constructor(server: OmniSharpServer) { - super(server); + constructor(server: OmniSharpServer, reporter: TelemetryReporter) { + super(server, reporter); + this._commandId = 'omnisharp.runCodeAction'; this._checkOption(); let d1 = vscode.workspace.onDidChangeConfiguration(this._checkOption, this); let d2 = vscode.commands.registerCommand(this._commandId, this._runCodeAction, this); - this._disposables.push(d1, d2); + this.addDisposables(d1, d2); } private _checkOption(): void { diff --git a/src/features/codeLensProvider.ts b/src/features/codeLensProvider.ts index f8d002ca61..202259caff 100644 --- a/src/features/codeLensProvider.ts +++ b/src/features/codeLensProvider.ts @@ -5,24 +5,35 @@ 'use strict'; -import {CancellationToken, CodeLens, Range, Uri, TextDocument, CodeLensProvider} from 'vscode'; -import {toRange, toLocation} from '../omnisharp/typeConvertion'; -import AbstractSupport from './abstractProvider'; -import {updateCodeLensForTest} from './dotnetTest'; +import { OmniSharpServer } from '../omnisharp/server'; +import TelemetryReporter from 'vscode-extension-telemetry'; +import TestManager from './dotnetTest'; +import * as vscode from 'vscode'; +import { toRange, toLocation } from '../omnisharp/typeConvertion'; +import AbstractProvider from './abstractProvider'; import * as protocol from '../omnisharp/protocol'; import * as serverUtils from '../omnisharp/utils'; -class OmniSharpCodeLens extends CodeLens { +class OmniSharpCodeLens extends vscode.CodeLens { fileName: string; - constructor(fileName: string, range: Range) { + constructor(fileName: string, range: vscode.Range) { super(range); this.fileName = fileName; } } -export default class OmniSharpCodeLensProvider extends AbstractSupport implements CodeLensProvider { +export default class OmniSharpCodeLensProvider extends AbstractProvider implements vscode.CodeLensProvider { + + private _testManager: TestManager; + + constructor(server: OmniSharpServer, reporter: TelemetryReporter, testManager: TestManager) + { + super(server, reporter); + + this._testManager = testManager; + } private static filteredSymbolNames: { [name: string]: boolean } = { 'Equals': true, @@ -31,15 +42,15 @@ export default class OmniSharpCodeLensProvider extends AbstractSupport implement 'ToString': true }; - provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { + provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable { return serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token).then(tree => { - let ret: CodeLens[] = []; + let ret: vscode.CodeLens[] = []; tree.TopLevelTypeDefinitions.forEach(node => this._convertQuickFix(ret, document.fileName, node)); return ret; }); } - private _convertQuickFix(bucket: CodeLens[], fileName: string, node: protocol.Node): void { + private _convertQuickFix(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node): void { if (node.Kind === 'MethodDeclaration' && OmniSharpCodeLensProvider.filteredSymbolNames[node.Location.Text]) { return; @@ -52,10 +63,10 @@ export default class OmniSharpCodeLensProvider extends AbstractSupport implement this._convertQuickFix(bucket, fileName, child); } - updateCodeLensForTest(bucket, fileName, node, this._server.isDebugEnable()); + this._updateCodeLensForTest(bucket, fileName, node); } - resolveCodeLens(codeLens: CodeLens, token: CancellationToken): Thenable { + resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken): Thenable { if (codeLens instanceof OmniSharpCodeLens) { let req = { @@ -75,11 +86,40 @@ export default class OmniSharpCodeLensProvider extends AbstractSupport implement codeLens.command = { title: len === 1 ? '1 reference' : `${len} references`, command: 'editor.action.showReferences', - arguments: [Uri.file(req.FileName), codeLens.range.start, res.QuickFixes.map(toLocation)] + arguments: [vscode.Uri.file(req.FileName), codeLens.range.start, res.QuickFixes.map(toLocation)] }; return codeLens; }); } } + + private _updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node) { + // backward compatible check: Features property doesn't present on older version OmniSharp + if (node.Features === undefined) { + return; + } + + 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'; + } + else if (testFeature.Name == 'MSTestMethod') { + testFrameworkName = 'mstest'; + } + + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] })); + + if (this._server.isDebugEnable()) { + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] })); + } + } + } } diff --git a/src/features/commands.ts b/src/features/commands.ts index 4aa22400b9..146a27c918 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -5,21 +5,21 @@ 'use strict'; -import {OmniSharpServer} from '../omnisharp/server'; +import { OmniSharpServer } from '../omnisharp/server'; import * as serverUtils from '../omnisharp/utils'; -import {findLaunchTargets} from '../omnisharp/launcher'; +import { findLaunchTargets } from '../omnisharp/launcher'; import * as cp from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as protocol from '../omnisharp/protocol'; import * as vscode from 'vscode'; -import * as dotnetTest from './dotnetTest'; -import {DotNetAttachItemsProviderFactory, AttachPicker, RemoteAttachPicker} from './processPicker'; -import {generateAssets} from '../assets'; +import { DotNetAttachItemsProviderFactory, AttachPicker, RemoteAttachPicker } from './processPicker'; +import { generateAssets } from '../assets'; +import TelemetryReporter from 'vscode-extension-telemetry'; let channel = vscode.window.createOutputChannel('.NET'); -export default function registerCommands(server: OmniSharpServer, extensionPath: string) { +export default function registerCommands(server: OmniSharpServer, reporter: TelemetryReporter) { let d1 = vscode.commands.registerCommand('o.restart', () => restartOmniSharp(server)); let d2 = vscode.commands.registerCommand('o.pickProjectAndStart', () => pickProjectAndStart(server)); let d3 = vscode.commands.registerCommand('o.showOutput', () => server.getChannel().show(vscode.ViewColumn.Three)); @@ -29,19 +29,15 @@ export default function registerCommands(server: OmniSharpServer, extensionPath: // running the command activates the extension, which is all we need for installation to kickoff let d5 = vscode.commands.registerCommand('csharp.downloadDebugger', () => { }); - // register two commands for running and debugging xunit tests - let d6 = dotnetTest.registerDotNetTestRunCommand(server); - let d7 = dotnetTest.registerDotNetTestDebugCommand(server); - // register process picker for attach let attachItemsProvider = DotNetAttachItemsProviderFactory.Get(); let attacher = new AttachPicker(attachItemsProvider); - let d8 = vscode.commands.registerCommand('csharp.listProcess', () => attacher.ShowAttachEntries()); + let d6 = vscode.commands.registerCommand('csharp.listProcess', () => attacher.ShowAttachEntries()); // Register command for generating tasks.json and launch.json assets. - let d9 = vscode.commands.registerCommand('dotnet.generateAssets', () => generateAssets(server)); - let d10 = vscode.commands.registerCommand('csharp.listRemoteProcess', (args) => RemoteAttachPicker.ShowAttachEntries(args)); + let d7 = vscode.commands.registerCommand('dotnet.generateAssets', () => generateAssets(server)); + let d8 = vscode.commands.registerCommand('csharp.listRemoteProcess', (args) => RemoteAttachPicker.ShowAttachEntries(args)); - return vscode.Disposable.from(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10); + return vscode.Disposable.from(d1, d2, d3, d4, d5, d6, d7, d8); } function restartOmniSharp(server: OmniSharpServer) { diff --git a/src/features/definitionProvider.ts b/src/features/definitionProvider.ts index b7f49d6008..4088420264 100644 --- a/src/features/definitionProvider.ts +++ b/src/features/definitionProvider.ts @@ -11,13 +11,14 @@ import * as serverUtils from '../omnisharp/utils'; import {createRequest, toLocation} from '../omnisharp/typeConvertion'; import {Uri, TextDocument, Position, Location, CancellationToken, DefinitionProvider} from 'vscode'; import DefinitionMetadataDocumentProvider from './definitionMetadataDocumentProvider'; - +import TelemetryReporter from 'vscode-extension-telemetry'; export default class CSharpDefinitionProvider extends AbstractSupport implements DefinitionProvider { private _definitionMetadataDocumentProvider: DefinitionMetadataDocumentProvider; - constructor(server, definitionMetadataDocumentProvider: DefinitionMetadataDocumentProvider) { - super(server); + constructor(server,reporter: TelemetryReporter, definitionMetadataDocumentProvider: DefinitionMetadataDocumentProvider) { + super(server, reporter); + this._definitionMetadataDocumentProvider = definitionMetadataDocumentProvider; } diff --git a/src/features/diagnosticsProvider.ts b/src/features/diagnosticsProvider.ts index 1cc3ae9cd3..fce318e7b5 100644 --- a/src/features/diagnosticsProvider.ts +++ b/src/features/diagnosticsProvider.ts @@ -9,6 +9,7 @@ import * as protocol from '../omnisharp/protocol'; import * as serverUtils from '../omnisharp/utils'; import { toRange } from '../omnisharp/typeConvertion'; import * as vscode from 'vscode'; +import TelemetryReporter from 'vscode-extension-telemetry'; export class Advisor { @@ -108,8 +109,8 @@ export class Advisor { } } -export default function reportDiagnostics(server: OmniSharpServer, advisor: Advisor): vscode.Disposable { - return new DiagnosticsProvider(server, advisor); +export default function reportDiagnostics(server: OmniSharpServer, reporter: TelemetryReporter, advisor: Advisor): vscode.Disposable { + return new DiagnosticsProvider(server, reporter, advisor); } class DiagnosticsProvider extends AbstractSupport { @@ -120,8 +121,9 @@ class DiagnosticsProvider extends AbstractSupport { private _projectValidation: vscode.CancellationTokenSource; private _diagnostics: vscode.DiagnosticCollection; - constructor(server: OmniSharpServer, validationAdvisor: Advisor) { - super(server); + constructor(server: OmniSharpServer, reporter: TelemetryReporter, validationAdvisor: Advisor) { + super(server, reporter); + this._validationAdvisor = validationAdvisor; this._diagnostics = vscode.languages.createDiagnosticCollection('csharp'); diff --git a/src/features/dotnetTest.ts b/src/features/dotnetTest.ts index 272b52db30..0b9332dbdf 100644 --- a/src/features/dotnetTest.ts +++ b/src/features/dotnetTest.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { OmniSharpServer } from '../omnisharp/server'; -import { toRange } from '../omnisharp/typeConvertion'; import { DebuggerEventsProtocol } from '../coreclr-debug/debuggerEventsProtocol'; import * as vscode from 'vscode'; import * as serverUtils from "../omnisharp/utils"; @@ -13,236 +12,284 @@ import * as utils from '../common'; import * as net from 'net'; import * as os from 'os'; import * as path from 'path'; +import TelemetryReporter from 'vscode-extension-telemetry'; +import AbstractProvider from './abstractProvider'; -let _testOutputChannel: vscode.OutputChannel = undefined; +const TelemetryReportingDelay = 2 * 60 * 1000; // two minutes -function getTestOutputChannel(): vscode.OutputChannel { - if (_testOutputChannel === undefined) { - _testOutputChannel = vscode.window.createOutputChannel(".NET Test Log"); +export default class TestManager extends AbstractProvider { + private _channel: vscode.OutputChannel; + + private _runCounts: { [testFrameworkName: string]: number }; + private _debugCounts: { [testFrameworkName: string]: number }; + private _telemetryIntervalId: NodeJS.Timer = undefined; + + constructor(server: OmniSharpServer, reporter: TelemetryReporter) { + super(server, reporter); + + // register commands + let d1 = vscode.commands.registerCommand( + 'dotnet.test.run', + (testMethod, fileName, testFrameworkName) => this._runDotnetTest(testMethod, fileName, testFrameworkName)); + + let d2 = vscode.commands.registerCommand( + 'dotnet.test.debug', + (testMethod, fileName, testFrameworkName) => this._debugDotnetTest(testMethod, fileName, testFrameworkName)); + + this._telemetryIntervalId = setInterval(() => + this._reportTelemetry(), TelemetryReportingDelay); + + let d3 = new vscode.Disposable(() => { + if (this._telemetryIntervalId !== undefined) { + // Stop reporting telemetry + clearInterval(this._telemetryIntervalId); + this._telemetryIntervalId = undefined; + this._reportTelemetry(); + } + }); + + this.addDisposables(d1, d2, d3); } - return _testOutputChannel; -} + private _getOutputChannel(): vscode.OutputChannel { + if (this._channel === undefined) { + this._channel = vscode.window.createOutputChannel(".NET Test Log"); + this.addDisposables(this._channel); + } -export function registerDotNetTestRunCommand(server: OmniSharpServer): vscode.Disposable { - return vscode.commands.registerCommand( - 'dotnet.test.run', - (testMethod, fileName, testFrameworkName) => runDotnetTest(testMethod, fileName, testFrameworkName, server)); -} + return this._channel; + } -export function registerDotNetTestDebugCommand(server: OmniSharpServer): vscode.Disposable { - return vscode.commands.registerCommand( - 'dotnet.test.debug', - (testMethod, fileName, testFrameworkName) => debugDotnetTest(testMethod, fileName, testFrameworkName, server)); -} + private _recordRunRequest(testFrameworkName: string): void { + if (this._runCounts === undefined) { + this._runCounts = {}; + } -function saveDirtyFiles(): Promise { - return Promise.resolve( - vscode.workspace.saveAll(/*includeUntitled*/ false)); -} + let count = this._runCounts[testFrameworkName]; -function runTest(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string): Promise { - const request: protocol.V2.RunTestRequest = { - FileName: fileName, - MethodName: testMethod, - TestFrameworkName: testFrameworkName - }; + if (!count) { + count = 1; + } + else { + count += 1; + } - return serverUtils.runTest(server, request) - .then(response => response.Results); -} + this._runCounts[testFrameworkName] = count; + } + + private _recordDebugRequest(testFrameworkName: string): void { + if (this._debugCounts === undefined) { + this._debugCounts = {}; + } -function reportResults(results: protocol.V2.DotNetTestResult[], output: vscode.OutputChannel): Promise { - const totalTests = results.length; - - let totalPassed = 0, totalFailed = 0, totalSkipped = 0; - for (let result of results) { - switch (result.Outcome) { - case protocol.V2.TestOutcomes.Failed: - totalFailed += 1; - break; - case protocol.V2.TestOutcomes.Passed: - totalPassed += 1; - break; - case protocol.V2.TestOutcomes.Skipped: - totalSkipped += 1; - break; + let count = this._debugCounts[testFrameworkName]; + + if (!count) { + count = 1; + } + else { + count += 1; } + + this._debugCounts[testFrameworkName] = count; } - output.appendLine(''); - output.appendLine(`Total tests: ${totalTests}. Passed: ${totalPassed}. Failed: ${totalFailed}. Skipped: ${totalSkipped}`); - output.appendLine(''); + private _reportTelemetry(): void { + if (this._runCounts) { + this._reporter.sendTelemetryEvent('RunTest', null, this._runCounts); + } - return Promise.resolve(); -} + if (this._debugCounts) { + this._reporter.sendTelemetryEvent('DebugTest', null, this._debugCounts); + } -// Run test through dotnet-test command. This function can be moved to a separate structure -export function runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string, server: OmniSharpServer) { - const output = getTestOutputChannel(); - - output.show(); - output.appendLine(`Running test ${testMethod}...`); - output.appendLine(''); - - const listener = server.onTestMessage(e => { - output.appendLine(e.Message); - }); - - saveDirtyFiles() - .then(_ => runTest(server, fileName, testMethod, testFrameworkName)) - .then(results => reportResults(results, output)) - .then(() => listener.dispose()) - .catch(reason => { - listener.dispose(); - vscode.window.showErrorMessage(`Failed to run test because ${reason}.`); - }); -} + this._runCounts = undefined; + this._debugCounts = undefined; + } -function createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) { - let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebuggingOptions'); + private _saveDirtyFiles(): Promise { + return Promise.resolve( + vscode.workspace.saveAll(/*includeUntitled*/ false)); + } - // Get the initial set of options from the workspace setting - let result: any; - if (typeof debugOptions === "object") { - // clone the options object to avoid changing it - result = JSON.parse(JSON.stringify(debugOptions)); - } else { - result = {}; + private _runTest(fileName: string, testMethod: string, testFrameworkName: string): Promise { + const request: protocol.V2.RunTestRequest = { + FileName: fileName, + MethodName: testMethod, + TestFrameworkName: testFrameworkName + }; + + return serverUtils.runTest(this._server, request) + .then(response => response.Results); } - if (!result.type) { - result.type = "coreclr"; + private _reportResults(results: protocol.V2.DotNetTestResult[]): Promise { + const totalTests = results.length; + + let totalPassed = 0, totalFailed = 0, totalSkipped = 0; + for (let result of results) { + switch (result.Outcome) { + case protocol.V2.TestOutcomes.Failed: + totalFailed += 1; + break; + case protocol.V2.TestOutcomes.Passed: + totalPassed += 1; + break; + case protocol.V2.TestOutcomes.Skipped: + totalSkipped += 1; + break; + } + } + + const output = this._getOutputChannel(); + output.appendLine(''); + output.appendLine(`Total tests: ${totalTests}. Passed: ${totalPassed}. Failed: ${totalFailed}. Skipped: ${totalSkipped}`); + output.appendLine(''); + + return Promise.resolve(); } - // Now fill in the rest of the options - result.name = ".NET Test Launch"; - result.request = "launch"; - result.debuggerEventsPipeName = debuggerEventsPipeName; - result.program = program; - result.args = args; - result.cwd = cwd; + private _runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string) { + const output = this._getOutputChannel(); - return result; -} + output.show(); + output.appendLine(`Running test ${testMethod}...`); + output.appendLine(''); -function getLaunchConfigurationForVSTest(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener, output: vscode.OutputChannel): Promise { - // Listen for test messages while getting start info. - const listener = server.onTestMessage(e => { - output.appendLine(e.Message); - }); - - const request: protocol.V2.DebugTestGetStartInfoRequest = { - FileName: fileName, - MethodName: testMethod, - TestFrameworkName: testFrameworkName - }; - - return serverUtils.debugTestGetStartInfo(server, request) - .then(response => { - listener.dispose(); - return createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, debugEventListener.pipePath()); + const listener = this._server.onTestMessage(e => { + output.appendLine(e.Message); }); -} -function getLaunchConfigurationForLegacy(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string, output: vscode.OutputChannel): Promise { - // Listen for test messages while getting start info. - const listener = server.onTestMessage(e => { - output.appendLine(e.Message); - }); - - const request: protocol.V2.GetTestStartInfoRequest = { - FileName: fileName, - MethodName: testMethod, - TestFrameworkName: testFrameworkName - }; - - return serverUtils.getTestStartInfo(server, request) - .then(response => { - listener.dispose(); - return createLaunchConfiguration(response.Executable, response.Argument, response.WorkingDirectory, null); - }); -} + this._saveDirtyFiles() + .then(_ => this._recordRunRequest(testFrameworkName)) + .then(_ => this._runTest(fileName, testMethod, testFrameworkName)) + .then(results => this._reportResults(results)) + .then(() => listener.dispose()) + .catch(reason => { + listener.dispose(); + vscode.window.showErrorMessage(`Failed to run test because ${reason}.`); + }); + } + private _createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) { + let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebuggingOptions'); -function getLaunchConfiguration(server: OmniSharpServer, debugType: string, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener, output: vscode.OutputChannel): Promise { - switch (debugType) { - case 'legacy': - return getLaunchConfigurationForLegacy(server, fileName, testMethod, testFrameworkName, output); - case 'vstest': - return getLaunchConfigurationForVSTest(server, fileName, testMethod, testFrameworkName, debugEventListener, output); + // Get the initial set of options from the workspace setting + let result: any; + if (typeof debugOptions === "object") { + // clone the options object to avoid changing it + result = JSON.parse(JSON.stringify(debugOptions)); + } else { + result = {}; + } - default: - throw new Error(`Unexpected debug type: ${debugType}`); + if (!result.type) { + result.type = "coreclr"; + } + + // Now fill in the rest of the options + result.name = ".NET Test Launch"; + result.request = "launch"; + result.debuggerEventsPipeName = debuggerEventsPipeName; + result.program = program; + result.args = args; + result.cwd = cwd; + + return result; } -} -// Run test through dotnet-test command with debugger attached -export function debugDotnetTest(testMethod: string, fileName: string, testFrameworkName: string, server: OmniSharpServer) { - // 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. - let debugType: string; - let debugEventListener: DebugEventListener = null; - - const output = getTestOutputChannel(); - - output.show(); - output.appendLine(`Debugging method '${testMethod}'...`); - output.appendLine(''); - - return saveDirtyFiles() - .then(_ => serverUtils.requestProjectInformation(server, { FileName: fileName })) - .then(projectInfo => { - if (projectInfo.DotNetProject) { - debugType = 'legacy'; - return Promise.resolve(); - } - else if (projectInfo.MsBuildProject) { - debugType = 'vstest'; - debugEventListener = new DebugEventListener(fileName, server, output); - return debugEventListener.start(); - } - else { - throw new Error('Expected project.json or .csproj project.'); - } - }) - .then(() => getLaunchConfiguration(server, debugType, fileName, testMethod, testFrameworkName, debugEventListener, output)) - .then(config => vscode.commands.executeCommand('vscode.startDebug', config)) - .catch(reason => { - vscode.window.showErrorMessage(`Failed to start debugger: ${reason}`); - if (debugEventListener != null) { - debugEventListener.close(); - } + private _getLaunchConfigurationForVSTest(fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener): Promise { + const output = this._getOutputChannel(); + + // Listen for test messages while getting start info. + const listener = this._server.onTestMessage(e => { + output.appendLine(e.Message); }); -} -export function updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node, isDebugEnable: boolean) { - // backward compatible check: Features property doesn't present on older version OmniSharp - if (node.Features === undefined) { - return; + const request: protocol.V2.DebugTestGetStartInfoRequest = { + FileName: fileName, + MethodName: testMethod, + TestFrameworkName: testFrameworkName + }; + + return serverUtils.debugTestGetStartInfo(this._server, request) + .then(response => { + listener.dispose(); + return this._createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, debugEventListener.pipePath()); + }); } - 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'; - } - else if (testFeature.Name == 'MSTestMethod') { - testFrameworkName = 'mstest'; - } + private _getLaunchConfigurationForLegacy(fileName: string, testMethod: string, testFrameworkName: string): Promise { + const output = this._getOutputChannel(); - bucket.push(new vscode.CodeLens( - toRange(node.Location), - { title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] })); + // Listen for test messages while getting start info. + const listener = this._server.onTestMessage(e => { + output.appendLine(e.Message); + }); + + const request: protocol.V2.GetTestStartInfoRequest = { + FileName: fileName, + MethodName: testMethod, + TestFrameworkName: testFrameworkName + }; - if (isDebugEnable) { - bucket.push(new vscode.CodeLens( - toRange(node.Location), - { title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] })); + return serverUtils.getTestStartInfo(this._server, request) + .then(response => { + listener.dispose(); + return this._createLaunchConfiguration(response.Executable, response.Argument, response.WorkingDirectory, null); + }); + } + + private _getLaunchConfiguration(debugType: string, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener): Promise { + switch (debugType) { + case 'legacy': + return this._getLaunchConfigurationForLegacy(fileName, testMethod, testFrameworkName); + case 'vstest': + return this._getLaunchConfigurationForVSTest(fileName, testMethod, testFrameworkName, debugEventListener); + + default: + throw new Error(`Unexpected debug type: ${debugType}`); } } + + 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. + let debugType: string; + let debugEventListener: DebugEventListener = null; + + 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'; + return Promise.resolve(); + } + else if (projectInfo.MsBuildProject) { + debugType = 'vstest'; + debugEventListener = new DebugEventListener(fileName, this._server, output); + return debugEventListener.start(); + } + else { + throw new Error('Expected project.json or .csproj project.'); + } + }) + .then(() => this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, debugEventListener)) + .then(config => vscode.commands.executeCommand('vscode.startDebug', config)) + .catch(reason => { + vscode.window.showErrorMessage(`Failed to start debugger: ${reason}`); + if (debugEventListener != null) { + debugEventListener.close(); + } + }); + } } class DebugEventListener { diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index 84f8ef4c8a..ee0a56534a 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -21,6 +21,7 @@ import CompletionItemProvider from '../features/completionItemProvider'; import WorkspaceSymbolProvider from '../features/workspaceSymbolProvider'; import reportDiagnostics, { Advisor } from '../features/diagnosticsProvider'; import SignatureHelpProvider from '../features/signatureHelpProvider'; +import TestManager from '../features/dotnetTest'; import registerCommands from '../features/commands'; import forwardChanges from '../features/changeForwarding'; import reportStatus from '../features/status'; @@ -47,25 +48,27 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe definitionMetadataDocumentProvider.register(); localDisposables.push(definitionMetadataDocumentProvider); - const definitionProvider = new DefinitionProvider(server, definitionMetadataDocumentProvider); + const definitionProvider = new DefinitionProvider(server, reporter, definitionMetadataDocumentProvider); localDisposables.push(vscode.languages.registerDefinitionProvider(documentSelector, definitionProvider)); localDisposables.push(vscode.languages.registerDefinitionProvider({ scheme: definitionMetadataDocumentProvider.scheme }, definitionProvider)); - localDisposables.push(vscode.languages.registerImplementationProvider(documentSelector, new ImplementationProvider(server))); - localDisposables.push(vscode.languages.registerCodeLensProvider(documentSelector, new CodeLensProvider(server))); - localDisposables.push(vscode.languages.registerDocumentHighlightProvider(documentSelector, new DocumentHighlightProvider(server))); - localDisposables.push(vscode.languages.registerDocumentSymbolProvider(documentSelector, new DocumentSymbolProvider(server))); - localDisposables.push(vscode.languages.registerReferenceProvider(documentSelector, new ReferenceProvider(server))); - localDisposables.push(vscode.languages.registerHoverProvider(documentSelector, new HoverProvider(server))); - localDisposables.push(vscode.languages.registerRenameProvider(documentSelector, new RenameProvider(server))); - localDisposables.push(vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, new FormatProvider(server))); - localDisposables.push(vscode.languages.registerOnTypeFormattingEditProvider(documentSelector, new FormatProvider(server), '}', ';')); - localDisposables.push(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionItemProvider(server), '.')); - localDisposables.push(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(server))); - localDisposables.push(vscode.languages.registerSignatureHelpProvider(documentSelector, new SignatureHelpProvider(server), '(', ',')); - const codeActionProvider = new CodeActionProvider(server); + localDisposables.push(vscode.languages.registerImplementationProvider(documentSelector, new ImplementationProvider(server, reporter))); + const testManager = new TestManager(server, reporter); + localDisposables.push(testManager); + localDisposables.push(vscode.languages.registerCodeLensProvider(documentSelector, new CodeLensProvider(server, reporter, testManager))); + localDisposables.push(vscode.languages.registerDocumentHighlightProvider(documentSelector, new DocumentHighlightProvider(server, reporter))); + localDisposables.push(vscode.languages.registerDocumentSymbolProvider(documentSelector, new DocumentSymbolProvider(server, reporter))); + localDisposables.push(vscode.languages.registerReferenceProvider(documentSelector, new ReferenceProvider(server, reporter))); + localDisposables.push(vscode.languages.registerHoverProvider(documentSelector, new HoverProvider(server, reporter))); + localDisposables.push(vscode.languages.registerRenameProvider(documentSelector, new RenameProvider(server, reporter))); + localDisposables.push(vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, new FormatProvider(server, reporter))); + localDisposables.push(vscode.languages.registerOnTypeFormattingEditProvider(documentSelector, new FormatProvider(server, reporter), '}', ';')); + localDisposables.push(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionItemProvider(server, reporter), '.')); + localDisposables.push(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(server, reporter))); + localDisposables.push(vscode.languages.registerSignatureHelpProvider(documentSelector, new SignatureHelpProvider(server, reporter), '(', ',')); + const codeActionProvider = new CodeActionProvider(server, reporter); localDisposables.push(codeActionProvider); localDisposables.push(vscode.languages.registerCodeActionsProvider(documentSelector, codeActionProvider)); - localDisposables.push(reportDiagnostics(server, advisor)); + localDisposables.push(reportDiagnostics(server, reporter, advisor)); localDisposables.push(forwardChanges(server)); })); @@ -74,7 +77,7 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe vscode.Disposable.from(...localDisposables).dispose(); })); - disposables.push(registerCommands(server, context.extensionPath)); + disposables.push(registerCommands(server, reporter)); disposables.push(reportStatus(server)); if (!context.workspaceState.get('assetPromptDisabled')) {