diff --git a/src/client/common/telemetry.ts b/src/client/common/telemetry.ts deleted file mode 100644 index 64f05c58f6d6..000000000000 --- a/src/client/common/telemetry.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { extensions, workspace } from "vscode"; -import TelemetryReporter from "vscode-extension-telemetry"; - -// Borrowed from omnisharpServer.ts (omnisharp-vscode) -export class Delays { - immediateDelays: number = 0; // 0-25 milliseconds - nearImmediateDelays: number = 0; // 26-50 milliseconds - shortDelays: number = 0; // 51-250 milliseconds - mediumDelays: number = 0; // 251-500 milliseconds - idleDelays: number = 0; // 501-1500 milliseconds - nonFocusDelays: number = 0; // 1501-3000 milliseconds - bigDelays: number = 0; // 3000+ milliseconds - private startTime: number = Date.now(); - public stop() { - let endTime = Date.now(); - let elapsedTime = endTime - this.startTime; - - if (elapsedTime <= 25) { - this.immediateDelays += 1; - } - else if (elapsedTime <= 50) { - this.nearImmediateDelays += 1; - } - else if (elapsedTime <= 250) { - this.shortDelays += 1; - } - else if (elapsedTime <= 500) { - this.mediumDelays += 1; - } - else if (elapsedTime <= 1500) { - this.idleDelays += 1; - } - else if (elapsedTime <= 3000) { - this.nonFocusDelays += 1; - } - else { - this.bigDelays += 1; - } - } - public toMeasures(): { [key: string]: number } { - return { - immedateDelays: this.immediateDelays, - nearImmediateDelays: this.nearImmediateDelays, - shortDelays: this.shortDelays, - mediumDelays: this.mediumDelays, - idleDelays: this.idleDelays, - nonFocusDelays: this.nonFocusDelays, - bigDelays: this.bigDelays - }; - } -} - -const extensionId = "donjayamanne.python"; -const extension = extensions.getExtension(extensionId); -const extensionVersion = extension.packageJSON.version; -const aiKey = "fce7a3d5-4665-4404-b786-31a6306749a6"; -let reporter: TelemetryReporter; -let telemetryEnabled: boolean | undefined = undefined; -/** - * Sends a telemetry event - * @param {string} eventName The event name - * @param {object} properties An associative array of strings - * @param {object} measures An associative array of numbers - */ -export function sendTelemetryEvent(eventName: string, properties?: { - [key: string]: string; -}, measures?: { - [key: string]: number; -}) { - if (telemetryEnabled === undefined) { - telemetryEnabled = workspace.getConfiguration('telemetry').get('enableTelemetry', true); - } - if (!telemetryEnabled) { - return; - } - reporter = reporter ? reporter : new TelemetryReporter(extensionId, extensionVersion, aiKey); - reporter.sendTelemetryEvent.apply(reporter, arguments); -} - diff --git a/src/client/common/telemetry/constants.ts b/src/client/common/telemetry/constants.ts new file mode 100644 index 000000000000..89314e9c712f --- /dev/null +++ b/src/client/common/telemetry/constants.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +export const COMPLETION = 'COMPLETION'; +export const DEFINITION = 'DEFINITION'; +export const HOVER_DEFINITION = 'HOVER_DEFINITION'; +export const REFERENCE = 'REFERENCE'; +export const SIGNATURE = 'SIGNATURE'; +export const SYMBOL = 'SYMBOL'; +export const FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS'; +export const FORMAT = 'FORMAT.FORMAT'; +export const EDITOR_LOAD = 'EDITOR.LOAD'; +export const LINTING = 'LINTING'; +export const GO_TO_OBJECT_DEFINITION = 'GO_TO_OBJECT_DEFINITION'; +export const UPDATE_PYSPARK_LIBRARY = 'UPDATE_PYSPARK_LIBRARY'; +export const REFACTOR_RENAME = 'REFACTOR_RENAME'; +export const REFACTOR_EXTRACT_VAR = 'REFACTOR_EXTRACT_VAR'; +export const REFACTOR_EXTRACT_FUNCTION = 'REFACTOR_EXTRACT_FUNCTION'; +export const REPL = 'REPL'; +export const PYTHON_INTERPRETER = 'PYTHON_INTERPRETER'; +export const WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD'; +export const WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO'; +export const EXECUTION_CODE = 'EXECUTION_CODE'; +export const EXECUTION_DJANGO = 'EXECUTION_DJANGO'; +export const DEBUGGER = 'DEBUGGER'; +export const UNITTEST_STOP = 'UNITTEST.STOP'; +export const UNITTEST_RUN = 'UNITTEST.RUN'; +export const UNITTEST_DISCOVER = 'UNITTEST.DISCOVER'; +export const UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT'; diff --git a/src/client/common/telemetry/index.ts b/src/client/common/telemetry/index.ts new file mode 100644 index 000000000000..74f590f105cf --- /dev/null +++ b/src/client/common/telemetry/index.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { StopWatch } from './stopWatch'; +import { getTelemetryReporter } from './telemetry'; +import { TelemetryProperties } from './types'; + +export function sendTelemetryEvent(eventName: string, durationMs?: number, properties?: TelemetryProperties) { + const reporter = getTelemetryReporter(); + const measures = typeof durationMs === 'number' ? { duration: durationMs } : undefined; + + // tslint:disable-next-line:no-any + const customProperties: { [key: string]: string } = {}; + if (properties) { + // tslint:disable-next-line:prefer-type-cast no-any + const data = properties as any; + Object.getOwnPropertyNames(data).forEach(prop => { + // tslint:disable-next-line:prefer-type-cast no-any no-unsafe-any + (customProperties as any)[prop] = typeof data[prop] === 'string' ? data[prop] : data[prop].toString(); + }); + } + // + reporter.sendTelemetryEvent(eventName, properties ? customProperties : undefined, measures); +} + +// tslint:disable-next-line:no-any function-name +export function captureTelemetry(eventName: string) { + // tslint:disable-next-line:no-function-expression no-any + return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (...args: any[]) { + const stopWatch = new StopWatch(); + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = originalMethod.apply(this, args); + + // If method being wrapped returns a promise then wait for it. + // tslint:disable-next-line:no-unsafe-any + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + // tslint:disable-next-line:prefer-type-cast + (result as Promise) + .then(data => { + sendTelemetryEvent(eventName, stopWatch.elapsedTime); + return data; + }) + // tslint:disable-next-line:promise-function-async + .catch(ex => { + sendTelemetryEvent(eventName, stopWatch.elapsedTime); + return Promise.reject(ex); + }); + } else { + sendTelemetryEvent(eventName, stopWatch.elapsedTime); + } + + return result; + }; + + return descriptor; + }; +} + +// tslint:disable-next-line:no-any function-name +export function sendTelemetryWhenDone(eventName: string, promise: Promise | Thenable, + stopWatch?: StopWatch, properties?: TelemetryProperties) { + stopWatch = stopWatch ? stopWatch : new StopWatch(); + if (typeof promise.then === 'function') { + // tslint:disable-next-line:prefer-type-cast no-any + (promise as Promise) + .then(data => { + // tslint:disable-next-line:no-non-null-assertion + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties); + return data; + // tslint:disable-next-line:promise-function-async + }, ex => { + // tslint:disable-next-line:no-non-null-assertion + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties); + return Promise.reject(ex); + }); + } else { + throw new Error('Method is neither a Promise nor a Theneable'); + } +} diff --git a/src/client/common/telemetry/stopWatch.ts b/src/client/common/telemetry/stopWatch.ts new file mode 100644 index 000000000000..221d466754ab --- /dev/null +++ b/src/client/common/telemetry/stopWatch.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class StopWatch { + private started: number = Date.now(); + private stopped?: number; + public get elapsedTime() { + return (this.stopped ? this.stopped : Date.now()) - this.started; + } + public stop() { + this.stopped = Date.now(); + } +} diff --git a/src/client/common/telemetry/telemetry.ts b/src/client/common/telemetry/telemetry.ts new file mode 100644 index 000000000000..25db5eddd125 --- /dev/null +++ b/src/client/common/telemetry/telemetry.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable-next-line:no-reference +/// +import { extensions } from 'vscode'; +// tslint:disable-next-line:import-name +import TelemetryReporter from 'vscode-extension-telemetry'; + +// tslint:disable-next-line:no-any +let telemetryReporter: TelemetryReporter; +export function getTelemetryReporter() { + if (telemetryReporter) { + return telemetryReporter; + } + const extensionId = 'donjayamanne.python'; + // tslint:disable-next-line:no-non-null-assertion + const extension = extensions.getExtension(extensionId)!; + // tslint:disable-next-line:no-unsafe-any + const extensionVersion = extension.packageJSON.version; + // tslint:disable-next-line:no-unsafe-any + const aiKey = extension.packageJSON.contributes.debuggers[0].aiKey; + + // tslint:disable-next-line:no-unsafe-any + return telemetryReporter = new TelemetryReporter(extensionId, extensionVersion, aiKey); +} diff --git a/src/client/common/telemetry/types.ts b/src/client/common/telemetry/types.ts new file mode 100644 index 000000000000..467f0fdd905d --- /dev/null +++ b/src/client/common/telemetry/types.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +export type EditorLoadTelemetry = { + condaVersion: string; +}; +export type FormatTelemetry = { + tool: 'autoppep8' | 'yapf'; + hasCustomArgs: boolean; + formatSelection: boolean; +}; +export type LintingTelemetry = { + tool: 'flake8' | 'mypy' | 'pep8' | 'prospector' | 'pydocstyle' | 'pylama' | 'pylint'; + hasCustomArgs: boolean; + trigger: 'save' | 'auto'; + executableSpecified: boolean; +}; +export type PythonInterpreterTelemetry = { + trigger: 'ui' | 'shebang' | 'load'; + failed: boolean; + version: string; + pipVersion: string; +}; +export type CodeExecutionTelemetry = { + scope: 'file' | 'selection'; +}; +export type DebuggerTelemetry = { + trigger: 'launch' | 'attach' + console?: 'none' | 'integratedTerminal' | 'externalTerminal'; + debugOptions?: string; + pyspark?: boolean; + hasEnvVars?: boolean; +}; +export type TestRunTelemetry = { + tool: 'nosetest' | 'pytest' | 'unittest' + scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; + debugging: boolean; + trigger: 'ui' | 'codelens' | 'commandpalette' | 'auto'; + failed: boolean; +}; +export type TestDiscoverytTelemetry = { + tool: 'nosetest' | 'pytest' | 'unittest' + trigger: 'ui' | 'commandpalette'; + failed: boolean; +}; +export type TelemetryProperties = FormatTelemetry | LintingTelemetry | EditorLoadTelemetry | PythonInterpreterTelemetry | CodeExecutionTelemetry | TestRunTelemetry | TestDiscoverytTelemetry; diff --git a/src/client/typings/vscode-extension-telemetry/vscode-extension-telemetry.d.ts b/src/client/common/telemetry/vscode-extension-telemetry.d.ts similarity index 56% rename from src/client/typings/vscode-extension-telemetry/vscode-extension-telemetry.d.ts rename to src/client/common/telemetry/vscode-extension-telemetry.d.ts index 8b3e01df591a..63944bc57471 100644 --- a/src/client/typings/vscode-extension-telemetry/vscode-extension-telemetry.d.ts +++ b/src/client/common/telemetry/vscode-extension-telemetry.d.ts @@ -1,25 +1,13 @@ -declare module "vscode-extension-telemetry" { +declare module 'vscode-extension-telemetry' { export default class TelemetryReporter { - private extensionId; - private extensionVersion; - private appInsightsClient; - private commonProperties; - private static SQM_KEY; - private static REGISTRY_USERID_VALUE; - private static REGISTRY_MACHINEID_VALUE; - /** * Constructs a new telemetry reporter * @param {string} extensionId All events will be prefixed with this event name * @param {string} extensionVersion Extension version to be reported with each event * @param {string} key The application insights key */ + // tslint:disable-next-line:no-empty constructor(extensionId: string, extensionVersion: string, key: string); - private setupAIClient(client); - private loadVSCodeCommonProperties(machineId, sessionId, version); - private loadCommonProperties(); - private addCommonProperties(properties); - private getWinRegKeyData(key, name, hive, callback); /** * Sends a telemetry event @@ -27,10 +15,12 @@ declare module "vscode-extension-telemetry" { * @param {object} properties An associative array of strings * @param {object} measures An associative array of numbers */ - sendTelemetryEvent(eventName: string, properties?: { + // tslint:disable-next-line:member-access + public sendTelemetryEvent(eventName: string, properties?: { [key: string]: string; }, measures?: { [key: string]: number; + // tslint:disable-next-line:no-empty }): void; } -} \ No newline at end of file +} diff --git a/src/client/common/telemetryContracts.ts b/src/client/common/telemetryContracts.ts deleted file mode 100644 index 735793d078b5..000000000000 --- a/src/client/common/telemetryContracts.ts +++ /dev/null @@ -1,31 +0,0 @@ -export namespace Debugger { - export const Load = 'DEBUGGER_LOAD'; - export const Attach = 'DEBUGGER_ATTACH'; -} -export namespace Commands { - export const SortImports = 'COMMAND_SORT_IMPORTS'; - export const UnitTests = 'COMMAND_UNIT_TEST'; -} -export namespace IDE { - export const Completion = 'CODE_COMPLETION'; - export const Definition = 'CODE_DEFINITION'; - export const Format = 'CODE_FORMAT'; - export const HoverDefinition = 'CODE_HOVER_DEFINITION'; - export const Reference = 'CODE_REFERENCE'; - export const Rename = 'CODE_RENAME'; - export const Symbol = 'CODE_SYMBOL'; - export const Lint = 'LINTING'; -} -export namespace REFACTOR { - export const Rename = 'REFACTOR_RENAME'; - export const ExtractVariable = 'REFACTOR_EXTRACT_VAR'; - export const ExtractMethod = 'REFACTOR_EXTRACT_METHOD'; -} -export namespace UnitTests { - export const Run = 'UNITTEST_RUN'; - export const Discover = 'UNITTEST_DISCOVER'; -} -export namespace Jupyter { - export const Usage = 'JUPYTER'; -} -export const EVENT_LOAD = 'IDE_LOAD'; \ No newline at end of file diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index 85930a837a8c..820c81b596da 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -423,7 +423,7 @@ export function areBasePathsSame(path1: string, path2: string) { path2 = IS_WINDOWS ? path2.replace(/\//g, "\\") : path2; return path.dirname(path1).toUpperCase() === path.dirname(path2).toUpperCase(); } -export async function getInterpreterDisplayName(pythonPath: string) { +export async function getInterpreterVersion(pythonPath: string) { return await new Promise((resolve, reject) => { child_process.execFile(pythonPath, ['--version'], (error, stdout, stdErr) => { const out = (typeof stdErr === 'string' ? stdErr : '') + os.EOL + (typeof stdout === 'string' ? stdout : ''); diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts index 8c7d3a71bd79..dcc276a65c5a 100644 --- a/src/client/debugger/Main.ts +++ b/src/client/debugger/Main.ts @@ -12,9 +12,10 @@ import { BaseDebugServer } from "./DebugServers/BaseDebugServer"; import { DebugClient } from "./DebugClients/DebugClient"; import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients/DebugFactory"; import { LaunchRequestArguments, AttachRequestArguments, DebugOptions, TelemetryEvent, PythonEvaluationResultFlags } from "./Common/Contracts"; -import * as telemetryContracts from "../common/telemetryContracts"; import { validatePath, getPythonExecutable } from './Common/Utils'; import { isNotInstalledError } from '../common/helpers'; +import { DEBUGGER } from '../../client/common/telemetry/constants'; +import { DebuggerTelemetry } from '../../client/common/telemetry/types'; const CHILD_ENUMEARATION_TIMEOUT = 5000; @@ -190,7 +191,7 @@ export class PythonDebugger extends DebugSession { protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { // Some versions may still exist with incorrect launch.json values const setting = '${config.python.pythonPath}'; - if (args.pythonPath === setting){ + if (args.pythonPath === setting) { return this.sendErrorResponse(response, 2001, `Invalid launch.json (re-create it or replace 'config.python.pythonPath' with 'config:python.pythonPath')`); } // Add support for specifying just the directory where the python executable will be located @@ -228,17 +229,18 @@ export class PythonDebugger extends DebugSession { if (args && typeof args.cwd === 'string' && args.cwd.length > 0 && args.cwd !== 'null') { programDirectory = args.cwd; } - if (programDirectory.length > 0 && fs.existsSync(path.join(programDirectory, 'pyenv.cfg'))){ - this.sendEvent(new OutputEvent(`Warning 'pyenv.cfg' can interfere with the debugger. Please rename or delete this file (temporary solution)`)); + if (programDirectory.length > 0 && fs.existsSync(path.join(programDirectory, 'pyenv.cfg'))) { + this.sendEvent(new OutputEvent(`Warning 'pyenv.cfg' can interfere with the debugger. Please rename or delete this file (temporary solution)`)); } - - // this.sendEvent(new TelemetryEvent(telemetryContracts.Debugger.Load, { - // Debug_Console: args.console, - // Debug_DebugOptions: args.debugOptions.join(","), - // Debug_DJango: args.debugOptions.indexOf("DjangoDebugging") >= 0 ? "true" : "false", - // Debug_PySpark: typeof args.pythonPath === 'string' && args.pythonPath.indexOf('spark-submit') > 0 ? 'true' : 'false', - // Debug_HasEnvVaraibles: args.env && typeof args.env === "object" && Object.keys(args.env).length > 0 ? "true" : "false" - // })); + + const telemetryProps: DebuggerTelemetry = { + trigger: 'launch', + console: args.console, + debugOptions: args.debugOptions.join(","), + pyspark: typeof args.pythonPath === 'string' && args.pythonPath.indexOf('spark-submit') > 0, + hasEnvVars: args.env && typeof args.env === "object" && Object.keys(args.env).length > 0 + }; + this.sendEvent(new TelemetryEvent(DEBUGGER, telemetryProps)); this.launchArgs = args; this.debugClient = CreateLaunchDebugClient(args, this); @@ -274,7 +276,8 @@ export class PythonDebugger extends DebugSession { this.sendEvent(new TerminatedEvent()); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { - this.sendEvent(new TelemetryEvent(telemetryContracts.Debugger.Attach)); + this.sendEvent(new TelemetryEvent(DEBUGGER, { trigger: 'attach' })); + this.attachArgs = args; this.debugClient = CreateAttachDebugClient(args, this); this.entryResponse = response; diff --git a/src/client/extension.ts b/src/client/extension.ts index 827c95324eb3..bcb605e732ec 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,16 +1,19 @@ 'use strict'; +import { EDITOR_LOAD } from './common/telemetry/constants'; import * as os from 'os'; import * as vscode from 'vscode'; import * as settings from './common/configSettings'; import { Commands } from './common/constants'; import { createDeferred } from './common/helpers'; -import * as telemetryHelper from './common/telemetry'; -import * as telemetryContracts from './common/telemetryContracts'; +import { sendTelemetryEvent } from './common/telemetry'; +import { StopWatch } from './common/telemetry/stopWatch'; import { SimpleConfigurationProvider } from './debugger'; import { InterpreterManager } from './interpreter'; import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider'; import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; +import { getCondaVersion } from './interpreter/helpers'; +import { InterpreterVersionService } from './interpreter/interpreterVersion'; import * as jup from './jupyter/main'; import { JupyterProvider } from './jupyter/provider'; import { JediFactory } from './languageServices/jediProxyFactory'; @@ -44,7 +47,8 @@ export const activated = activationDeferred.promise; // tslint:disable-next-line:max-func-body-length export async function activate(context: vscode.ExtensionContext) { const pythonSettings = settings.PythonSettings.getInstance(); - sendStartupTelemetry(); + sendStartupTelemetry(activated); + lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow); formatOutChannel = lintingOutChannel; if (pythonSettings.linting.outputWindow !== pythonSettings.formatting.outputWindow) { @@ -61,7 +65,8 @@ export async function activate(context: vscode.ExtensionContext) { await interpreterManager.autoSetInterpreter(); await interpreterManager.refresh(); context.subscriptions.push(interpreterManager); - context.subscriptions.push(new SetInterpreterProvider(interpreterManager)); + const interpreterVersionService = new InterpreterVersionService(); + context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService)); context.subscriptions.push(...activateExecInTerminalProvider()); context.subscriptions.push(activateUpdateSparkLibraryProvider()); activateSimplePythonRefactorProvider(context, formatOutChannel); @@ -143,6 +148,15 @@ export async function activate(context: vscode.ExtensionContext) { activationDeferred.resolve(); } -function sendStartupTelemetry() { - telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD); +async function sendStartupTelemetry(activatedPromise: Promise) { + const stopWatch = new StopWatch(); + activatedPromise.then(async () => { + const duration = stopWatch.elapsedTime; + let condaVersion: string | undefined; + try { + condaVersion = await getCondaVersion(); + // tslint:disable-next-line:no-empty + } catch { } + sendTelemetryEvent(EDITOR_LOAD, duration, { condaVersion }); + }); } diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts index 35afef4a57cf..1953143618c5 100644 --- a/src/client/formatters/autoPep8Formatter.ts +++ b/src/client/formatters/autoPep8Formatter.ts @@ -1,9 +1,12 @@ 'use strict'; import * as vscode from 'vscode'; -import { BaseFormatter } from './baseFormatter'; import { PythonSettings } from '../common/configSettings'; import { Product } from '../common/installer'; +import { sendTelemetryWhenDone } from '../common/telemetry'; +import { FORMAT } from '../common/telemetry/constants'; +import { StopWatch } from '../common/telemetry/stopWatch'; +import { BaseFormatter } from './baseFormatter'; export class AutoPep8Formatter extends BaseFormatter { constructor(outputChannel: vscode.OutputChannel) { @@ -11,13 +14,20 @@ export class AutoPep8Formatter extends BaseFormatter { } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { + const stopWatch = new StopWatch(); const settings = PythonSettings.getInstance(document.uri); const autopep8Path = settings.formatting.autopep8Path; - let autoPep8Args = Array.isArray(settings.formatting.autopep8Args) ? settings.formatting.autopep8Args : []; - autoPep8Args = autoPep8Args.concat(['--diff']); - if (range && !range.isEmpty) { - autoPep8Args = autoPep8Args.concat(['--line-range', (range.start.line + 1).toString(), (range.end.line + 1).toString()]); + const autoPep8Args = Array.isArray(settings.formatting.autopep8Args) ? settings.formatting.autopep8Args : []; + const hasCustomArgs = autoPep8Args.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + autoPep8Args.push('--diff'); + if (formatSelection) { + // tslint:disable-next-line:no-non-null-assertion + autoPep8Args.push(...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()]); } - return super.provideDocumentFormattingEdits(document, options, token, autopep8Path, autoPep8Args); + const promise = super.provideDocumentFormattingEdits(document, options, token, autopep8Path, autoPep8Args); + sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'autoppep8', hasCustomArgs, formatSelection }); + return promise; } } diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts index ff8e40e21066..1a221098fe81 100644 --- a/src/client/formatters/yapfFormatter.ts +++ b/src/client/formatters/yapfFormatter.ts @@ -1,10 +1,12 @@ 'use strict'; import * as vscode from 'vscode'; -import * as path from 'path'; -import { BaseFormatter } from './baseFormatter'; import { PythonSettings } from '../common/configSettings'; import { Product } from '../common/installer'; +import { sendTelemetryWhenDone} from '../common/telemetry'; +import { FORMAT } from '../common/telemetry/constants'; +import { StopWatch } from '../common/telemetry/stopWatch'; +import { BaseFormatter } from './baseFormatter'; export class YapfFormatter extends BaseFormatter { constructor(outputChannel: vscode.OutputChannel) { @@ -12,16 +14,23 @@ export class YapfFormatter extends BaseFormatter { } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { + const stopWatch = new StopWatch(); const settings = PythonSettings.getInstance(document.uri); const yapfPath = settings.formatting.yapfPath; - let yapfArgs = Array.isArray(settings.formatting.yapfArgs) ? settings.formatting.yapfArgs : []; - yapfArgs = yapfArgs.concat(['--diff']); - if (range && !range.isEmpty) { - yapfArgs = yapfArgs.concat(['--lines', `${range.start.line + 1}-${range.end.line + 1}`]); + const yapfArgs = Array.isArray(settings.formatting.yapfArgs) ? settings.formatting.yapfArgs : []; + const hasCustomArgs = yapfArgs.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + yapfArgs.push('--diff'); + if (formatSelection) { + // tslint:disable-next-line:no-non-null-assertion + yapfArgs.push(...['--lines', `${range!.start.line + 1}-${range!.end.line + 1}`]); } - // Yapf starts looking for config file starting from the file path + // Yapf starts looking for config file starting from the file path. const fallbarFolder = this.getWorkspaceUri(document).fsPath; const cwd = this.getDocumentPath(document, fallbarFolder); - return super.provideDocumentFormattingEdits(document, options, token, yapfPath, yapfArgs, cwd); + const promise = super.provideDocumentFormattingEdits(document, options, token, yapfPath, yapfArgs, cwd); + sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); + return promise; } } diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index b81963f9d8fe..ff3e85d04fe0 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,21 +1,45 @@ import * as path from 'path'; import { ConfigurationTarget, Uri, window } from 'vscode'; -import { WorkspacePythonPath } from '../contracts'; -import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; +import { sendTelemetryEvent } from '../../common/telemetry'; +import { PYTHON_INTERPRETER } from '../../common/telemetry/constants'; +import { StopWatch } from '../../common/telemetry/stopWatch'; +import { IInterpreterVersionService } from '../interpreterVersion'; +import { IPythonPathUpdaterServiceFactory } from './types'; export class PythonPathUpdaterService { - constructor(private pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory) { } - public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, wkspace?: Uri): Promise { + constructor(private pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory, + private interpreterVersionService: IInterpreterVersionService) { } + public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise { + const stopWatch = new StopWatch(); const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); - + let failed = false; try { await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); } catch (reason) { + failed = true; // tslint:disable-next-line:no-unsafe-any prefer-type-cast const message = reason && typeof reason.message === 'string' ? reason.message as string : ''; window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); console.error(reason); } + // do not wait for this to complete + this.sendTelemetry(stopWatch.elapsedTime, failed, trigger, pythonPath); + } + private async sendTelemetry(duration: number, failed: boolean, trigger: 'ui' | 'shebang' | 'load', pythonPath: string) { + let version: string | undefined; + let pipVersion: string | undefined; + if (!failed) { + const pyVersionPromise = this.interpreterVersionService.getVersion(pythonPath, '') + .then(pyVersion => pyVersion.length === 0 ? undefined : pyVersion); + const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath) + .then(value => value.length === 0 ? undefined : value) + .catch(() => undefined); + const versions = await Promise.all([pyVersionPromise, pipVersionPromise]); + version = versions[0]; + // tslint:disable-next-line:prefer-type-cast + pipVersion = versions[1] as string; + } + sendTelemetryEvent(PYTHON_INTERPRETER, duration, { failed, trigger, version, pipVersion }); } private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { switch (configTarget) { diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts index a46c18275d35..c48d20cdac1c 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -1,5 +1,4 @@ import { Uri } from 'vscode'; -import { InterpreterManager } from '../index'; import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; import { WorkspacePythonPathUpdaterService } from './services/workspaceUpdaterService'; diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index 4b775adf0b71..d25a866bfbcd 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -1,6 +1,4 @@ -import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { InterpreterManager } from '../..'; -import { WorkspacePythonPath } from '../../contracts'; +import { workspace } from 'vscode'; import { IPythonPathUpdaterService } from '../types'; export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts index d39f9a831f93..37f45d5cde14 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -1,7 +1,5 @@ import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { InterpreterManager } from '../..'; -import { WorkspacePythonPath } from '../../contracts'; import { IPythonPathUpdaterService } from '../types'; export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts index b0da1b168345..a5a35c3483e5 100644 --- a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -1,7 +1,5 @@ import * as path from 'path'; -import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { InterpreterManager } from '../..'; -import { WorkspacePythonPath } from '../../contracts'; +import { Uri, workspace } from 'vscode'; import { IPythonPathUpdaterService } from '../types'; export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts index b155d7caa049..418c12550c5c 100644 --- a/src/client/interpreter/configuration/setInterpreterProvider.ts +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -5,9 +5,9 @@ import { InterpreterManager } from '../'; import * as settings from '../../common/configSettings'; import { PythonInterpreter, WorkspacePythonPath } from '../contracts'; import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; +import { IInterpreterVersionService } from '../interpreterVersion'; import { PythonPathUpdaterService } from './pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; -import { IPythonPathUpdaterServiceFactory } from './types'; // tslint:disable-next-line:interface-name interface PythonPathQuickPickItem extends QuickPickItem { @@ -17,10 +17,10 @@ interface PythonPathQuickPickItem extends QuickPickItem { export class SetInterpreterProvider implements Disposable { private disposables: Disposable[] = []; private pythonPathUpdaterService: PythonPathUpdaterService; - constructor(private interpreterManager: InterpreterManager) { + constructor(private interpreterManager: InterpreterManager, interpreterVersionService: IInterpreterVersionService) { this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); - this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); @@ -85,7 +85,7 @@ export class SetInterpreterProvider implements Disposable { const selection = await window.showQuickPick(suggestions, quickPickOptions); if (selection !== undefined) { - await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, wkspace); + await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, 'ui', wkspace); } } @@ -100,15 +100,15 @@ export class SetInterpreterProvider implements Disposable { const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; if (isGlobalChange) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global); + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); return; } if (isWorkspaceChange || !workspaceFolder) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, workspace.workspaceFolders[0].uri); + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', workspace.workspaceFolders[0].uri); return; } - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, workspaceFolder.uri); + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, 'shebang', workspaceFolder.uri); } } diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index dc8308344d2f..f78df63a31f8 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,5 +1,10 @@ +import * as child_process from 'child_process'; import { ConfigurationTarget, window, workspace } from 'vscode'; +import { RegistryImplementation } from '../common/registry'; +import { Is_64Bit, IS_WINDOWS } from '../common/utils'; import { WorkspacePythonPath } from './contracts'; +import { CondaEnvService } from './locators/services/condaEnvService'; +import { WindowsRegistryService } from './locators/services/windowsRegistryService'; export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { @@ -23,3 +28,24 @@ export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { } return undefined; } +export async function getCondaVersion() { + let condaService: CondaEnvService; + if (IS_WINDOWS) { + const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); + condaService = new CondaEnvService(windowsRegistryProvider); + } else { + condaService = new CondaEnvService(); + } + return condaService.getCondaFile() + .then(async condaFile => { + return new Promise((resolve, reject) => { + child_process.execFile(condaFile, ['--version'], (_, stdout) => { + if (stdout && stdout.length > 0) { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + } else { + reject(); + } + }); + }); + }); +} diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index a90c2592fcb0..c00d6b5973e2 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,11 +1,9 @@ 'use strict'; import * as path from 'path'; -import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; +import { Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { IS_WINDOWS } from '../common/utils'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; -import { WorkspacePythonPath } from './contracts'; import { InterpreterDisplay } from './display'; import { getActiveWorkspaceUri } from './helpers'; import { InterpreterVersionService } from './interpreterVersion'; @@ -25,7 +23,8 @@ export class InterpreterManager implements Disposable { this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr); const versionService = new InterpreterVersionService(); this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); - this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const interpreterVersionService = new InterpreterVersionService(); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); this.disposables.push(statusBar); @@ -58,7 +57,7 @@ export class InterpreterManager implements Disposable { const pythonPath = interpretersInWorkspace[0].path; const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { - await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, activeWorkspace.folderUri); + await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); } } public dispose(): void { diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts index de9fddc57575..0f7ef485e388 100644 --- a/src/client/interpreter/interpreterVersion.ts +++ b/src/client/interpreter/interpreterVersion.ts @@ -1,13 +1,36 @@ -import { getInterpreterDisplayName } from '../common/utils'; +import * as child_process from 'child_process'; +import { getInterpreterVersion } from '../common/utils'; export interface IInterpreterVersionService { getVersion(pythonPath: string, defaultValue: string): Promise; + getPipVersion(pythonPath: string): Promise; } +const PIP_VERSION_REGEX = '\\d\\.\\d(\\.\\d)+'; + export class InterpreterVersionService implements IInterpreterVersionService { - getVersion(pythonPath: string, defaultValue: string): Promise { - return getInterpreterDisplayName(pythonPath) + public async getVersion(pythonPath: string, defaultValue: string): Promise { + return getInterpreterVersion(pythonPath) + .then(version => version.length === 0 ? defaultValue : version) .catch(() => defaultValue); } + public async getPipVersion(pythonPath: string): Promise { + return new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['-m', 'pip', '--version'], (error, stdout, stdErr) => { + if (stdout && stdout.length > 0) { + // Take the first available version number, see below example. + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + // Take the second part, see below example. + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + const re = new RegExp(PIP_VERSION_REGEX, 'g'); + const matches = re.exec(stdout); + if (matches && matches.length > 0) { + resolve(matches[0].trim()); + return; + } + } + reject(); + }); + }); + } } - diff --git a/src/client/jupyter/editorIntegration/codeLensProvider.ts b/src/client/jupyter/editorIntegration/codeLensProvider.ts index 908bdd3a6d0d..a0548848aabf 100644 --- a/src/client/jupyter/editorIntegration/codeLensProvider.ts +++ b/src/client/jupyter/editorIntegration/codeLensProvider.ts @@ -1,7 +1,6 @@ 'use strict'; -import {CodeLensProvider, TextDocument, CancellationToken, CodeLens, Command} from 'vscode'; -import * as telemetryContracts from '../../common/telemetryContracts'; +import {CancellationToken, CodeLens, CodeLensProvider, Command, TextDocument} from 'vscode'; import {Commands} from '../../common/constants'; import {CellHelper} from '../common/cellHelper'; @@ -37,4 +36,4 @@ export class JupyterCodeLensProvider implements CodeLensProvider { this.cache.push({ fileName: document.fileName, documentVersion: document.version, lenses: lenses }); return Promise.resolve(lenses); } -} \ No newline at end of file +} diff --git a/src/client/jupyter/editorIntegration/symbolProvider.ts b/src/client/jupyter/editorIntegration/symbolProvider.ts index 4c9a49a6fa04..64afc98d3eb2 100644 --- a/src/client/jupyter/editorIntegration/symbolProvider.ts +++ b/src/client/jupyter/editorIntegration/symbolProvider.ts @@ -1,6 +1,5 @@ 'use strict'; -import {DocumentSymbolProvider, TextDocument, CancellationToken, SymbolInformation, SymbolKind} from 'vscode'; -import * as telemetryContracts from '../../common/telemetryContracts'; +import {CancellationToken, DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument} from 'vscode'; import {CellHelper} from '../common/cellHelper'; export class JupyterSymbolProvider implements DocumentSymbolProvider { diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 46c89d16dff9..4b4f1d61541e 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -1,11 +1,13 @@ 'use strict'; -import { IPythonSettings, PythonSettings } from '../common/configSettings'; -import { execPythonFile } from './../common/utils'; +import * as path from 'path'; import { OutputChannel, Uri } from 'vscode'; -import { Installer, Product } from '../common/installer'; import * as vscode from 'vscode'; +import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import { Installer, Product } from '../common/installer'; +import { execPythonFile } from './../common/utils'; import { ErrorHandler } from './errorHandlers/main'; +// tslint:disable-next-line:variable-name let NamedRegexp = null; const REGEX = '(?\\d+),(?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; @@ -35,11 +37,12 @@ export enum LintMessageSeverity { export function matchNamedRegEx(data, regex): IRegexGroup { if (NamedRegexp === null) { + // tslint:disable-next-line:no-require-imports NamedRegexp = require('named-js-regexp'); } - let compiledRegexp = NamedRegexp(regex, 'g'); - let rawMatch = compiledRegexp.exec(data); + const compiledRegexp = NamedRegexp(regex, 'g'); + const rawMatch = compiledRegexp.exec(data); if (rawMatch !== null) { return rawMatch.groups(); } @@ -47,31 +50,56 @@ export function matchNamedRegEx(data, regex): IRegexGroup { return null; } +type LinterId = 'flake8' | 'mypy' | 'pep8' | 'prospector' | 'pydocstyle' | 'pylama' | 'pylint'; export abstract class BaseLinter { - public Id: string; + // tslint:disable-next-line:variable-name + public Id: LinterId; + // tslint:disable-next-line:variable-name protected _columnOffset = 0; + // tslint:disable-next-line:variable-name private _errorHandler: ErrorHandler; + // tslint:disable-next-line:variable-name private _pythonSettings: IPythonSettings; protected get pythonSettings(): IPythonSettings { return this._pythonSettings; } - protected getWorkspaceRootPath(document: vscode.TextDocument): string { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; - return typeof workspaceRootPath === 'string' ? workspaceRootPath : __dirname; - } - constructor(id: string, public product: Product, protected outputChannel: OutputChannel) { + constructor(id: LinterId, public product: Product, protected outputChannel: OutputChannel) { this.Id = id; this._errorHandler = new ErrorHandler(this.Id, product, new Installer(), this.outputChannel); } + public isEnabled(resource: Uri) { + this._pythonSettings = PythonSettings.getInstance(resource); + const enabledSetting = `${this.Id}Enabled`; + // tslint:disable-next-line:prefer-type-cast + return this._pythonSettings.linting[enabledSetting] as boolean; + } + public linterArgs(resource: Uri) { + this._pythonSettings = PythonSettings.getInstance(resource); + const argsSetting = `${this.Id}Args`; + // tslint:disable-next-line:prefer-type-cast + return this._pythonSettings.linting[argsSetting] as string[]; + } + public isLinterExecutableSpecified(resource: Uri) { + this._pythonSettings = PythonSettings.getInstance(resource); + const argsSetting = `${this.Id}Path`; + // tslint:disable-next-line:prefer-type-cast + const executablePath = this._pythonSettings.linting[argsSetting] as string; + return path.basename(executablePath).length > 0 && path.basename(executablePath) !== executablePath; + } public lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { this._pythonSettings = PythonSettings.getInstance(document.uri); return this.runLinter(document, cancellation); } + protected getWorkspaceRootPath(document: vscode.TextDocument): string { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; + return typeof workspaceRootPath === 'string' ? workspaceRootPath : __dirname; + } protected abstract runLinter(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; + // tslint:disable-next-line:no-any protected parseMessagesSeverity(error: string, categorySeverity: any): LintMessageSeverity { if (categorySeverity[error]) { - let severityName = categorySeverity[error]; + const severityName = categorySeverity[error]; switch (severityName) { case 'Error': return LintMessageSeverity.Error; @@ -83,6 +111,7 @@ export abstract class BaseLinter { return LintMessageSeverity.Warning; default: { if (LintMessageSeverity[severityName]) { + // tslint:disable-next-line:no-any return LintMessageSeverity[severityName]; } } @@ -91,14 +120,32 @@ export abstract class BaseLinter { return LintMessageSeverity.Information; } + protected run(command: string, args: string[], document: vscode.TextDocument, cwd: string, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise { + return execPythonFile(document.uri, command, args, cwd, true, null, cancellation).then(data => { + if (!data) { + data = ''; + } + this.displayLinterResultHeader(data); + const outputLines = data.split(/\r?\n/g); + return this.parseLines(outputLines, regEx); + }).catch(error => { + this.handleError(this.Id, command, error, document.uri); + return []; + }); + } + protected handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { + this._errorHandler.handleError(expectedFileName, fileName, error, resource); + } private parseLine(line: string, regEx: string) { - let match = matchNamedRegEx(line, regEx); + const match = matchNamedRegEx(line, regEx); if (!match) { return; } + // tslint:disable-next-line:no-any match.line = Number(match.line); + // tslint:disable-next-line:no-any match.column = Number(match.column); return { @@ -111,15 +158,14 @@ export abstract class BaseLinter { }; } private parseLines(outputLines: string[], regEx: string) { - let diagnostics: ILintMessage[] = []; + const diagnostics: ILintMessage[] = []; outputLines.filter((value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems).forEach(line => { try { - let msg = this.parseLine(line, regEx); + const msg = this.parseLine(line, regEx); if (msg) { diagnostics.push(msg); } - } - catch (ex) { + } catch (ex) { // Hmm, need to handle this later // TODO: } @@ -127,24 +173,7 @@ export abstract class BaseLinter { return diagnostics; } private displayLinterResultHeader(data: string) { - this.outputChannel.append('#'.repeat(10) + 'Linting Output - ' + this.Id + '#'.repeat(10) + '\n'); + this.outputChannel.append(`${'#'.repeat(10)}Linting Output - ${this.Id}${'#'.repeat(10)}\n`); this.outputChannel.append(data); } - protected run(command: string, args: string[], document: vscode.TextDocument, cwd: string, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise { - return execPythonFile(document.uri, command, args, cwd, true, null, cancellation).then(data => { - if (!data) { - data = ''; - } - this.displayLinterResultHeader(data); - let outputLines = data.split(/\r?\n/g); - return this.parseLines(outputLines, regEx); - }).catch(error => { - this.handleError(this.Id, command, error, document.uri); - return []; - }); - } - - protected handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { - this._errorHandler.handleError(expectedFileName, fileName, error, resource); - } } diff --git a/src/client/providers/completionProvider.ts b/src/client/providers/completionProvider.ts index a72e45a679bf..7d2d67dfdaac 100644 --- a/src/client/providers/completionProvider.ts +++ b/src/client/providers/completionProvider.ts @@ -1,14 +1,13 @@ 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; -import * as telemetryHelper from '../common/telemetry'; -import * as telemetryContracts from '../common/telemetryContracts'; -import { extractSignatureAndDocumentation } from './jediHelpers'; -import { EOL } from 'os'; +import { ProviderResult, SnippetString, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { SnippetString, Uri } from 'vscode'; +import { captureTelemetry } from '../common/telemetry'; +import { COMPLETION } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; +import { extractSignatureAndDocumentation } from './jediHelpers'; +import * as proxy from './jediProxy'; export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { @@ -17,13 +16,13 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid if (data && data.items.length > 0) { return data.items.map(item => { const sigAndDocs = extractSignatureAndDocumentation(item); - let completionItem = new vscode.CompletionItem(item.text); + const completionItem = new vscode.CompletionItem(item.text); completionItem.kind = item.type; completionItem.documentation = sigAndDocs[1].length === 0 ? item.description : sigAndDocs[1]; completionItem.detail = sigAndDocs[0].split(/\r?\n/).join(''); if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && (item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { - completionItem.insertText = new SnippetString(item.text).appendText("(").appendTabstop().appendText(")"); + completionItem.insertText = new SnippetString(item.text).appendText('(').appendTabstop().appendText(')'); } // ensure the built in memebers are at the bottom @@ -33,7 +32,8 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid } return []; } - public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { + @captureTelemetry(COMPLETION) + public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): ProviderResult { if (position.character <= 0) { return Promise.resolve([]); } @@ -62,12 +62,8 @@ export class PythonCompletionItemProvider implements vscode.CompletionItemProvid source: source }; - const timer = new telemetryHelper.Delays(); return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { - timer.stop(); - telemetryHelper.sendTelemetryEvent(telemetryContracts.IDE.Completion, {}, timer.toMeasures()); - const completions = PythonCompletionItemProvider.parseData(data, document.uri); - return completions; + return PythonCompletionItemProvider.parseData(data, document.uri); }); } -} \ No newline at end of file +} diff --git a/src/client/providers/definitionProvider.ts b/src/client/providers/definitionProvider.ts index 9fd32b58c222..7c9a66aa92ea 100644 --- a/src/client/providers/definitionProvider.ts +++ b/src/client/providers/definitionProvider.ts @@ -1,9 +1,10 @@ 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; -import * as telemetryContracts from '../common/telemetryContracts'; +import { captureTelemetry } from '../common/telemetry'; +import { DEFINITION } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; +import * as proxy from './jediProxy'; export class PythonDefinitionProvider implements vscode.DefinitionProvider { public constructor(private jediFactory: JediFactory) { } @@ -19,8 +20,9 @@ export class PythonDefinitionProvider implements vscode.DefinitionProvider { } return null; } + @captureTelemetry(DEFINITION) public provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { return Promise.resolve(null); } @@ -28,9 +30,9 @@ export class PythonDefinitionProvider implements vscode.DefinitionProvider { return Promise.resolve(null); } - var range = document.getWordRangeAtPosition(position); - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { + const range = document.getWordRangeAtPosition(position); + const columnIndex = range.isEmpty ? position.character : range.end.character; + const cmd: proxy.ICommand = { command: proxy.CommandType.Definitions, fileName: filename, columnIndex: columnIndex, @@ -39,7 +41,7 @@ export class PythonDefinitionProvider implements vscode.DefinitionProvider { if (document.isDirty) { cmd.source = document.getText(); } - let possibleWord = document.getText(range); + const possibleWord = document.getText(range); return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { return PythonDefinitionProvider.parseData(data, possibleWord); }); diff --git a/src/client/providers/execInTerminalProvider.ts b/src/client/providers/execInTerminalProvider.ts index c25d784dc5d2..cd4cc73bab1e 100644 --- a/src/client/providers/execInTerminalProvider.ts +++ b/src/client/providers/execInTerminalProvider.ts @@ -7,6 +7,8 @@ import { Disposable, workspace } from 'vscode'; import * as settings from '../common/configSettings'; import { Commands, PythonLanguage } from '../common/constants'; import { ContextKey } from '../common/contextKey'; +import { sendTelemetryEvent } from '../common/telemetry'; +import { EXECUTION_CODE, EXECUTION_DJANGO } from '../common/telemetry/constants'; import { IS_WINDOWS } from '../common/utils'; let terminal: vscode.Terminal; @@ -71,7 +73,6 @@ function execInTerminal(fileUri?: vscode.Uri) { if (filePath.indexOf(' ') > 0) { filePath = `"${filePath}"`; } - terminal = terminal ? terminal : vscode.window.createTerminal('Python'); if (pythonSettings.terminal && pythonSettings.terminal.executeInFileDir) { const fileDirPath = path.dirname(filePath); @@ -94,6 +95,7 @@ function execInTerminal(fileUri?: vscode.Uri) { terminal.sendText(command); } terminal.show(); + sendTelemetryEvent(EXECUTION_CODE, undefined, { scope: 'file' }); } function execSelectionInTerminal() { @@ -147,6 +149,7 @@ function execSelectionInTerminal() { terminal.sendText(unix_code); } terminal.show(); + sendTelemetryEvent(EXECUTION_CODE, undefined, { scope: 'selection' }); } function execSelectionInDjangoShell() { @@ -203,6 +206,7 @@ function execSelectionInDjangoShell() { terminal.sendText(unix_code); } terminal.show(); + sendTelemetryEvent(EXECUTION_DJANGO); } class DjangoContextInitializer implements vscode.Disposable { diff --git a/src/client/providers/hoverProvider.ts b/src/client/providers/hoverProvider.ts index 376150f298a8..05c3e1c0a395 100644 --- a/src/client/providers/hoverProvider.ts +++ b/src/client/providers/hoverProvider.ts @@ -1,27 +1,29 @@ 'use strict'; -import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; -import { highlightCode } from './jediHelpers'; import { EOL } from 'os'; +import * as vscode from 'vscode'; +import { captureTelemetry } from '../common/telemetry'; +import { HOVER_DEFINITION } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; +import { highlightCode } from './jediHelpers'; +import * as proxy from './jediProxy'; export class PythonHoverProvider implements vscode.HoverProvider { public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IHoverResult, currentWord: string): vscode.Hover { - let results = []; - let capturedInfo: string[] = []; + const results = []; + const capturedInfo: string[] = []; data.items.forEach(item => { let { signature } = item; switch (item.kind) { case vscode.SymbolKind.Constructor: case vscode.SymbolKind.Function: case vscode.SymbolKind.Method: { - signature = 'def ' + signature; + signature = `def ${signature}`; break; } case vscode.SymbolKind.Class: { - signature = 'class ' + signature; + signature = `class ${signature}`; break; } default: { @@ -30,10 +32,10 @@ export class PythonHoverProvider implements vscode.HoverProvider { } if (item.docstring) { let lines = item.docstring.split(/\r?\n/); - // If the docstring starts with the signature, then remove those lines from the docstring + // If the docstring starts with the signature, then remove those lines from the docstring. if (lines.length > 0 && item.signature.indexOf(lines[0]) === 0) { lines.shift(); - let endIndex = lines.findIndex(line => item.signature.endsWith(line)); + const endIndex = lines.findIndex(line => item.signature.endsWith(line)); if (endIndex >= 0) { lines = lines.filter((line, index) => index > endIndex); } @@ -41,36 +43,38 @@ export class PythonHoverProvider implements vscode.HoverProvider { if (lines.length > 0 && item.signature.startsWith(currentWord) && lines[0].startsWith(currentWord) && lines[0].endsWith(')')) { lines.shift(); } - let descriptionWithHighlightedCode = highlightCode(lines.join(EOL)); - let hoverInfo = ['```python', signature, '```', descriptionWithHighlightedCode].join(EOL); - let key = signature + lines.join(''); - // Sometimes we have duplicate documentation, one with a period at the end - if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(key + '.') >= 0) { + const descriptionWithHighlightedCode = highlightCode(lines.join(EOL)); + const hoverInfo = ['```python', signature, '```', descriptionWithHighlightedCode].join(EOL); + const key = signature + lines.join(''); + // Sometimes we have duplicate documentation, one with a period at the end. + if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(`${key}.`) >= 0) { return; } capturedInfo.push(key); - capturedInfo.push(key + '.'); + capturedInfo.push(`${key}.`); results.push(hoverInfo); return; } if (item.description) { - let descriptionWithHighlightedCode = highlightCode(item.description); - let hoverInfo = '```python' + EOL + signature + EOL + '```' + EOL + descriptionWithHighlightedCode; - let lines = item.description.split(EOL); - let key = signature + lines.join(''); - // Sometimes we have duplicate documentation, one with a period at the end - if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(key + '.') >= 0) { + const descriptionWithHighlightedCode = highlightCode(item.description); + // tslint:disable-next-line:prefer-template + const hoverInfo = '```python' + EOL + signature + EOL + '```' + EOL + descriptionWithHighlightedCode; + const lines = item.description.split(EOL); + const key = signature + lines.join(''); + // Sometimes we have duplicate documentation, one with a period at the end. + if (capturedInfo.indexOf(key) >= 0 || capturedInfo.indexOf(`${key}.`) >= 0) { return; } capturedInfo.push(key); - capturedInfo.push(key + '.'); + capturedInfo.push(`${key}.`); results.push(hoverInfo); } }); return new vscode.Hover(results); } + @captureTelemetry(HOVER_DEFINITION) public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - var filename = document.fileName; + const filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { return null; } @@ -78,12 +82,12 @@ export class PythonHoverProvider implements vscode.HoverProvider { return null; } - var range = document.getWordRangeAtPosition(position); + const range = document.getWordRangeAtPosition(position); if (!range || range.isEmpty) { return null; } - let word = document.getText(range); - var cmd: proxy.ICommand = { + const word = document.getText(range); + const cmd: proxy.ICommand = { command: proxy.CommandType.Hover, fileName: filename, columnIndex: range.end.character, diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index 9084547de29f..8ec1b2cc0371 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -5,9 +5,12 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { getTempFileWithDocumentContents, getTextEditsFromPatch } from '../common/editor'; +import { captureTelemetry } from '../common/telemetry'; +import { FORMAT_SORT_IMPORTS } from '../common/telemetry/constants'; // tslint:disable-next-line:completed-docs export class PythonImportSortProvider { + @captureTelemetry(FORMAT_SORT_IMPORTS) public async sortImports(extensionDir: string, document: vscode.TextDocument): Promise { if (document.lineCount === 1) { return []; diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 9b01703ba13a..6fdb26278c85 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -281,7 +281,7 @@ export class JediProxy implements vscode.Disposable { var index = this.commandQueue.indexOf(cmd.id); this.commandQueue.splice(index, 1); - if (cmd.delays && typeof cmd.telemetryEvent === 'string') { + if (cmd.delay && typeof cmd.telemetryEvent === 'string') { // cmd.delays.stop(); // telemetryHelper.sendTelemetryEvent(cmd.telemetryEvent, null, cmd.delays.toMeasures()); } @@ -589,7 +589,7 @@ interface IExecutionCommand extends ICommand { id?: number; deferred?: Deferred; token: vscode.CancellationToken; - delays?: telemetryHelper.Delays; + delay?: number; } export interface ICommandError { diff --git a/src/client/providers/lintProvider.ts b/src/client/providers/lintProvider.ts index b9b5b92107a7..d1d878d0515f 100644 --- a/src/client/providers/lintProvider.ts +++ b/src/client/providers/lintProvider.ts @@ -1,19 +1,23 @@ 'use strict'; -import * as vscode from 'vscode'; +import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { LinterErrors } from '../common/constants'; +import { sendTelemetryWhenDone } from '../common/telemetry'; +import { LINTING } from '../common/telemetry/constants'; +import { StopWatch } from '../common/telemetry/stopWatch'; import * as linter from '../linters/baseLinter'; -import * as prospector from './../linters/prospector'; -import * as pylint from './../linters/pylint'; -import * as pep8 from './../linters/pep8Linter'; -import * as pylama from './../linters/pylama'; import * as flake8 from './../linters/flake8'; -import * as pydocstyle from './../linters/pydocstyle'; import * as mypy from './../linters/mypy'; -import { PythonSettings } from '../common/configSettings'; -import * as fs from 'fs'; -import { LinterErrors } from '../common/constants'; -const Minimatch = require("minimatch").Minimatch; +import * as pep8 from './../linters/pep8Linter'; +import * as prospector from './../linters/prospector'; +import * as pydocstyle from './../linters/pydocstyle'; +import * as pylama from './../linters/pylama'; +import * as pylint from './../linters/pylint'; +// tslint:disable-next-line:no-require-imports no-var-requires +const Minimatch = require('minimatch').Minimatch; const uriSchemesToIgnore = ['git', 'showModifications']; const lintSeverityToVSSeverity = new Map(); @@ -23,20 +27,22 @@ lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Information, vscode.Diag lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); function createDiagnostics(message: linter.ILintMessage, document: vscode.TextDocument): vscode.Diagnostic { - let position = new vscode.Position(message.line - 1, message.column); - let range = new vscode.Range(position, position); + const position = new vscode.Position(message.line - 1, message.column); + const range = new vscode.Range(position, position); - let severity = lintSeverityToVSSeverity.get(message.severity); - let diagnostic = new vscode.Diagnostic(range, message.code + ':' + message.message, severity); + const severity = lintSeverityToVSSeverity.get(message.severity); + const diagnostic = new vscode.Diagnostic(range, `${message.code}:${message.message}`, severity); diagnostic.code = message.code; diagnostic.source = message.provider; return diagnostic; } +// tslint:disable-next-line:interface-name interface DocumentHasJupyterCodeCells { + // tslint:disable-next-line:callable-types (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; } -export class LintProvider extends vscode.Disposable { +export class LintProvider implements vscode.Disposable { private diagnosticCollection: vscode.DiagnosticCollection; private linters: linter.BaseLinter[] = []; private pendingLintings = new Map(); @@ -45,13 +51,12 @@ export class LintProvider extends vscode.Disposable { private disposables: vscode.Disposable[]; public constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, public documentHasJupyterCodeCells: DocumentHasJupyterCodeCells) { - super(() => { }); this.outputChannel = outputChannel; this.context = context; this.disposables = []; this.initialize(); } - dispose() { + public dispose() { this.disposables.forEach(d => d.dispose()); } private isDocumentOpen(uri: vscode.Uri): boolean { @@ -74,7 +79,7 @@ export class LintProvider extends vscode.Disposable { if (e.languageId !== 'python' || !settings.linting.enabled || !settings.linting.lintOnSave) { return; } - this.lintDocument(e, 100); + this.lintDocument(e, 100, 'save'); }); this.context.subscriptions.push(disposable); @@ -90,7 +95,7 @@ export class LintProvider extends vscode.Disposable { if (!e.uri.path || (path.basename(e.uri.path) === e.uri.path && !fs.existsSync(e.uri.path))) { return; } - this.lintDocument(e, 100); + this.lintDocument(e, 100, 'auto'); }, this.context.subscriptions); disposable = vscode.workspace.onDidCloseTextDocument(textDocument => { @@ -106,8 +111,9 @@ export class LintProvider extends vscode.Disposable { this.context.subscriptions.push(disposable); } + // tslint:disable-next-line:member-ordering private lastTimeout: number; - private lintDocument(document: vscode.TextDocument, delay: number): void { + private lintDocument(document: vscode.TextDocument, delay: number, trigger: 'auto' | 'save'): void { // Since this is a hack, lets wait for 2 seconds before linting // Give user to continue typing before we waste CPU time if (this.lastTimeout) { @@ -116,11 +122,10 @@ export class LintProvider extends vscode.Disposable { } this.lastTimeout = setTimeout(() => { - this.onLintDocument(document); + this.onLintDocument(document, trigger); }, delay); } - - private onLintDocument(document: vscode.TextDocument): void { + private onLintDocument(document: vscode.TextDocument, trigger: 'auto' | 'save'): void { // Check if we need to lint this document const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; @@ -138,7 +143,7 @@ export class LintProvider extends vscode.Disposable { this.pendingLintings.delete(document.uri.fsPath); } - let cancelToken = new vscode.CancellationTokenSource(); + const cancelToken = new vscode.CancellationTokenSource(); cancelToken.token.onCancellationRequested(() => { if (this.pendingLintings.has(document.uri.fsPath)) { this.pendingLintings.delete(document.uri.fsPath); @@ -147,15 +152,22 @@ export class LintProvider extends vscode.Disposable { this.pendingLintings.set(document.uri.fsPath, cancelToken); this.outputChannel.clear(); - let promises: Promise[] = this.linters.map(linter => { - if (typeof workspaceRootPath !== 'string' && !settings.linting.enabledWithoutWorkspace) { - return Promise.resolve([]); - } - return linter.lint(document, cancelToken.token); - }); + const promises: Promise[] = this.linters + .filter(item => item.isEnabled(document.uri)) + .map(item => { + if (typeof workspaceRootPath !== 'string' && !settings.linting.enabledWithoutWorkspace) { + return Promise.resolve([]); + } + const stopWatch = new StopWatch(); + const promise = item.lint(document, cancelToken.token); + const hasCustomArgs = item.linterArgs(document.uri).length > 0; + const executableSpecified = item.isLinterExecutableSpecified(document.uri); + sendTelemetryWhenDone(LINTING, promise, stopWatch, { tool: item.Id, hasCustomArgs, trigger, executableSpecified }); + return promise; + }); this.documentHasJupyterCodeCells(document, cancelToken.token).then(hasJupyterCodeCells => { - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in + // linters will resolve asynchronously - keep a track of all + // diagnostics reported as them come in. let diagnostics: vscode.Diagnostic[] = []; promises.forEach(p => { diff --git a/src/client/providers/objectDefinitionProvider.ts b/src/client/providers/objectDefinitionProvider.ts index b455c4ba1933..2caf816b555f 100644 --- a/src/client/providers/objectDefinitionProvider.ts +++ b/src/client/providers/objectDefinitionProvider.ts @@ -1,8 +1,10 @@ 'use strict'; import * as vscode from 'vscode'; -import * as defProvider from './definitionProvider'; +import { captureTelemetry } from '../common/telemetry'; +import { GO_TO_OBJECT_DEFINITION } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; +import * as defProvider from './definitionProvider'; export function activateGoToObjectDefinitionProvider(jediFactory: JediFactory): vscode.Disposable[] { const def = new PythonObjectDefinitionProvider(jediFactory); @@ -16,6 +18,7 @@ export class PythonObjectDefinitionProvider { this._defProvider = new defProvider.PythonDefinitionProvider(jediFactory); } + @captureTelemetry(GO_TO_OBJECT_DEFINITION) public async goToObjectDefinition() { let pathDef = await this.getObjectDefinition(); if (typeof pathDef !== 'string' || pathDef.length === 0) { diff --git a/src/client/providers/referenceProvider.ts b/src/client/providers/referenceProvider.ts index c8f2031cf5ed..afb5ec7c148a 100644 --- a/src/client/providers/referenceProvider.ts +++ b/src/client/providers/referenceProvider.ts @@ -1,23 +1,25 @@ 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; +import { captureTelemetry } from '../common/telemetry'; +import { REFERENCE } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; - +import * as proxy from './jediProxy'; export class PythonReferenceProvider implements vscode.ReferenceProvider { public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IReferenceResult): vscode.Location[] { if (data && data.references.length > 0) { - var references = data.references.filter(ref => { + // tslint:disable-next-line:no-unnecessary-local-variable + const references = data.references.filter(ref => { if (!ref || typeof ref.columnIndex !== 'number' || typeof ref.lineIndex !== 'number' || typeof ref.fileName !== 'string' || ref.columnIndex === -1 || ref.lineIndex === -1 || ref.fileName.length === 0) { return false; } return true; }).map(ref => { - var definitionResource = vscode.Uri.file(ref.fileName); - var range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); + const definitionResource = vscode.Uri.file(ref.fileName); + const range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); return new vscode.Location(definitionResource, range); }); @@ -27,8 +29,9 @@ export class PythonReferenceProvider implements vscode.ReferenceProvider { return []; } + @captureTelemetry(REFERENCE) public provideReferences(document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { return Promise.resolve(null); } @@ -36,9 +39,9 @@ export class PythonReferenceProvider implements vscode.ReferenceProvider { return Promise.resolve(null); } - var range = document.getWordRangeAtPosition(position); - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { + const range = document.getWordRangeAtPosition(position); + const columnIndex = range.isEmpty ? position.character : range.end.character; + const cmd: proxy.ICommand = { command: proxy.CommandType.Usages, fileName: filename, columnIndex: columnIndex, diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts index e65b915b856b..4f1270e5a423 100644 --- a/src/client/providers/renameProvider.ts +++ b/src/client/providers/renameProvider.ts @@ -1,11 +1,13 @@ 'use strict'; -import * as vscode from 'vscode'; -import { RefactorProxy } from '../refactor/proxy'; -import { getWorkspaceEditsFromPatch } from '../common/editor'; import * as path from 'path'; +import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { getWorkspaceEditsFromPatch } from '../common/editor'; import { Installer, Product } from '../common/installer'; +import { captureTelemetry } from '../common/telemetry'; +import { REFACTOR_RENAME } from '../common/telemetry/constants'; +import { RefactorProxy } from '../refactor/proxy'; const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); interface RenameResponse { @@ -17,6 +19,7 @@ export class PythonRenameProvider implements vscode.RenameProvider { constructor(private outputChannel: vscode.OutputChannel) { this.installer = new Installer(outputChannel); } + @captureTelemetry(REFACTOR_RENAME) public provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Thenable { return vscode.workspace.saveAll(false).then(() => { return this.doRename(document, position, newName, token); @@ -31,7 +34,7 @@ export class PythonRenameProvider implements vscode.RenameProvider { return; } - var range = document.getWordRangeAtPosition(position); + const range = document.getWordRangeAtPosition(position); if (!range || range.isEmpty) { return; } @@ -47,7 +50,7 @@ export class PythonRenameProvider implements vscode.RenameProvider { const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); - let proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, workspaceRoot); + const proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, workspaceRoot); return proxy.rename(document, newName, document.uri.fsPath, range).then(response => { const fileDiffs = response.results.map(fileChanges => fileChanges.diff); return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot); @@ -55,8 +58,7 @@ export class PythonRenameProvider implements vscode.RenameProvider { if (reason === 'Not installed') { this.installer.promptToInstall(Product.rope, document.uri); return Promise.reject(''); - } - else { + } else { vscode.window.showErrorMessage(reason); this.outputChannel.appendLine(reason); } diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts index 1ef8b6cffc8e..60dc4f040d3d 100644 --- a/src/client/providers/replProvider.ts +++ b/src/client/providers/replProvider.ts @@ -1,6 +1,8 @@ -import { commands, Disposable, Uri, window, workspace } from 'vscode'; +import { commands, Disposable, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { Commands } from '../common/constants'; +import { captureTelemetry } from '../common/telemetry'; +import { REPL } from '../common/telemetry/constants'; import { getPathFromPythonCommand } from '../common/utils'; export class ReplProvider implements Disposable { @@ -15,6 +17,7 @@ export class ReplProvider implements Disposable { const disposable = commands.registerCommand(Commands.Start_REPL, this.commandHandler, this); this.disposables.push(disposable); } + @captureTelemetry(REPL) private async commandHandler() { const pythonPath = await this.getPythonPath(); if (!pythonPath) { diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 5ed411ed47b6..135886a528d8 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -1,42 +1,43 @@ -"use strict"; +'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; -import { TextDocument, Position, CancellationToken, SignatureHelp } from "vscode"; +import { CancellationToken, Position, SignatureHelp, TextDocument } from 'vscode'; +import { captureTelemetry } from '../common/telemetry'; +import { SIGNATURE } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; +import * as proxy from './jediProxy'; const DOCSTRING_PARAM_PATTERNS = [ - "\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)", // Sphinx - "\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+", // Sphinx param with type - "\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)" // Epydoc + '\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)', // Sphinx + '\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+', // Sphinx param with type + '\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)' // Epydoc ]; /** - * Extrct the documentation for parameters from a given docstring - * + * Extract the documentation for parameters from a given docstring. * @param {string} paramName Name of the parameter * @param {string} docString The docstring for the function * @returns {string} Docstring for the parameter */ function extractParamDocString(paramName: string, docString: string): string { - let paramDocString = ""; + let paramDocString = ''; // In docstring the '*' is escaped with a backslash - paramName = paramName.replace(new RegExp("\\*", "g"), "\\\\\\*"); + paramName = paramName.replace(new RegExp('\\*', 'g'), '\\\\\\*'); DOCSTRING_PARAM_PATTERNS.forEach(pattern => { if (paramDocString.length > 0) { return; } - pattern = pattern.replace("PARAMNAME", paramName); - let regExp = new RegExp(pattern); - let matches = regExp.exec(docString); + pattern = pattern.replace('PARAMNAME', paramName); + const regExp = new RegExp(pattern); + const matches = regExp.exec(docString); if (matches && matches.length > 0) { paramDocString = matches[0]; - if (paramDocString.indexOf(":") >= 0) { - paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1); + if (paramDocString.indexOf(':') >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); } - if (paramDocString.indexOf(":") >= 0) { - paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1); + if (paramDocString.indexOf(':') >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); } } }); @@ -47,15 +48,14 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { public constructor(private jediFactory: JediFactory) { } private static parseData(data: proxy.IArgumentsResult): vscode.SignatureHelp { if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { - let signature = new SignatureHelp(); + const signature = new SignatureHelp(); signature.activeSignature = 0; data.definitions.forEach(def => { signature.activeParameter = def.paramindex; - // Don't display the documentation, as vs code doesn't format the docmentation - // i.e. line feeds are not respected, long content is stripped - let sig = { - // documentation: def.docstring, + // Don't display the documentation, as vs code doesn't format the docmentation. + // i.e. line feeds are not respected, long content is stripped. + const sig = { label: def.description, parameters: [] }; @@ -75,8 +75,9 @@ export class PythonSignatureProvider implements vscode.SignatureHelpProvider { return new SignatureHelp(); } - provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable { - let cmd: proxy.ICommand = { + @captureTelemetry(SIGNATURE) + public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable { + const cmd: proxy.ICommand = { command: proxy.CommandType.Arguments, fileName: document.fileName, columnIndex: position.character, diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts index 35f765165eb1..bc9cced0c019 100644 --- a/src/client/providers/simpleRefactorProvider.ts +++ b/src/client/providers/simpleRefactorProvider.ts @@ -1,10 +1,13 @@ 'use strict'; import * as vscode from 'vscode'; -import { RefactorProxy } from '../refactor/proxy'; -import { getTextEditsFromPatch } from '../common/editor'; import { PythonSettings } from '../common/configSettings'; +import { getTextEditsFromPatch } from '../common/editor'; import { Installer, Product } from '../common/installer'; +import { sendTelemetryWhenDone } from '../common/telemetry'; +import { REFACTOR_EXTRACT_FUNCTION, REFACTOR_EXTRACT_VAR } from '../common/telemetry/constants'; +import { StopWatch } from '../common/telemetry/stopWatch'; +import { RefactorProxy } from '../refactor/proxy'; interface RenameResponse { results: [{ diff: string }]; @@ -14,18 +17,24 @@ let installer: Installer; export function activateSimplePythonRefactorProvider(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { let disposable = vscode.commands.registerCommand('python.refactorExtractVariable', () => { - extractVariable(context.extensionPath, + const stopWatch = new StopWatch(); + const promise = extractVariable(context.extensionPath, vscode.window.activeTextEditor, vscode.window.activeTextEditor.selection, + // tslint:disable-next-line:no-empty outputChannel).catch(() => { }); + sendTelemetryWhenDone(REFACTOR_EXTRACT_VAR, promise, stopWatch); }); context.subscriptions.push(disposable); disposable = vscode.commands.registerCommand('python.refactorExtractMethod', () => { - extractMethod(context.extensionPath, + const stopWatch = new StopWatch(); + const promise = extractMethod(context.extensionPath, vscode.window.activeTextEditor, vscode.window.activeTextEditor.selection, + // tslint:disable-next-line:no-empty outputChannel).catch(() => { }); + sendTelemetryWhenDone(REFACTOR_EXTRACT_FUNCTION, promise, stopWatch); }); context.subscriptions.push(disposable); installer = new Installer(outputChannel); @@ -34,6 +43,7 @@ export function activateSimplePythonRefactorProvider(context: vscode.ExtensionCo // Exported for unit testing export function extractVariable(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, + // tslint:disable-next-line:no-any outputChannel: vscode.OutputChannel): Promise { let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); @@ -44,9 +54,9 @@ export function extractVariable(extensionDir: string, textEditor: vscode.TextEdi const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); return validateDocumentForRefactor(textEditor).then(() => { - let newName = 'newvariable' + new Date().getMilliseconds().toString(); - let proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); - let rename = proxy.extractVariable(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { + const newName = `newvariable${new Date().getMilliseconds().toString()}`; + const proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); + const rename = proxy.extractVariable(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { return response.results[0].diff; }); @@ -56,6 +66,7 @@ export function extractVariable(extensionDir: string, textEditor: vscode.TextEdi // Exported for unit testing export function extractMethod(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, + // tslint:disable-next-line:no-any outputChannel: vscode.OutputChannel): Promise { let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); @@ -66,9 +77,9 @@ export function extractMethod(extensionDir: string, textEditor: vscode.TextEdito const pythonSettings = PythonSettings.getInstance(workspaceFolder ? workspaceFolder.uri : undefined); return validateDocumentForRefactor(textEditor).then(() => { - let newName = 'newmethod' + new Date().getMilliseconds().toString(); - let proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); - let rename = proxy.extractMethod(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { + const newName = `newmethod${new Date().getMilliseconds().toString()}`; + const proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot); + const rename = proxy.extractMethod(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { return response.results[0].diff; }); @@ -76,17 +87,18 @@ export function extractMethod(extensionDir: string, textEditor: vscode.TextEdito }); } +// tslint:disable-next-line:no-any function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise { if (!textEditor.document.isDirty) { return Promise.resolve(); } + // tslint:disable-next-line:no-any return new Promise((resolve, reject) => { vscode.window.showInformationMessage('Please save changes before refactoring', 'Save').then(item => { if (item === 'Save') { textEditor.document.save().then(resolve, reject); - } - else { + } else { return reject(); } }); @@ -94,14 +106,14 @@ function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise, outputChannel: vscode.OutputChannel): Promise { let changeStartsAtLine = -1; return renameResponse.then(diff => { if (diff.length === 0) { return []; } - let edits = getTextEditsFromPatch(textEditor.document.getText(), diff); - return edits; + return getTextEditsFromPatch(textEditor.document.getText(), diff); }).then(edits => { return textEditor.edit(editBuilder => { edits.forEach(edit => { @@ -115,8 +127,8 @@ function extractName(extensionDir: string, textEditor: vscode.TextEditor, range: if (done && changeStartsAtLine >= 0) { let newWordPosition: vscode.Position; for (let lineNumber = changeStartsAtLine; lineNumber < textEditor.document.lineCount; lineNumber++) { - let line = textEditor.document.lineAt(lineNumber); - let indexOfWord = line.text.indexOf(newName); + const line = textEditor.document.lineAt(lineNumber); + const indexOfWord = line.text.indexOf(newName); if (indexOfWord >= 0) { newWordPosition = new vscode.Position(line.range.start.line, indexOfWord); break; diff --git a/src/client/providers/symbolProvider.ts b/src/client/providers/symbolProvider.ts index 78181b2a62e8..8c4ba6f42aa6 100644 --- a/src/client/providers/symbolProvider.ts +++ b/src/client/providers/symbolProvider.ts @@ -1,14 +1,16 @@ 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; +import { captureTelemetry } from '../common/telemetry'; +import { SYMBOL } from '../common/telemetry/constants'; import { JediFactory } from '../languageServices/jediProxyFactory'; +import * as proxy from './jediProxy'; export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { public constructor(private jediFactory: JediFactory) { } private static parseData(document: vscode.TextDocument, data: proxy.ISymbolResult): vscode.SymbolInformation[] { if (data) { - let symbols = data.definitions.filter(sym => sym.fileName === document.fileName); + const symbols = data.definitions.filter(sym => sym.fileName === document.fileName); return symbols.map(sym => { const symbol = sym.kind; const range = new vscode.Range( @@ -21,10 +23,11 @@ export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { } return []; } + @captureTelemetry(SYMBOL) public provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; - var cmd: proxy.ICommand = { + const cmd: proxy.ICommand = { command: proxy.CommandType.Symbols, fileName: filename, columnIndex: 0, @@ -40,9 +43,9 @@ export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { }); } public provideDocumentSymbolsForInternalUse(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { - var filename = document.fileName; + const filename = document.fileName; - var cmd: proxy.ICommand = { + const cmd: proxy.ICommand = { command: proxy.CommandType.Symbols, fileName: filename, columnIndex: 0, diff --git a/src/client/providers/updateSparkLibraryProvider.ts b/src/client/providers/updateSparkLibraryProvider.ts index 20defe5528b7..cc2f1616fcc7 100644 --- a/src/client/providers/updateSparkLibraryProvider.ts +++ b/src/client/providers/updateSparkLibraryProvider.ts @@ -1,7 +1,9 @@ -"use strict"; -import { Commands } from '../common/constants'; -import * as vscode from "vscode"; +'use strict'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Commands } from '../common/constants'; +import { sendTelemetryEvent } from '../common/telemetry'; +import { UPDATE_PYSPARK_LIBRARY } from '../common/telemetry/constants'; export function activateUpdateSparkLibraryProvider(): vscode.Disposable { return vscode.commands.registerCommand(Commands.Update_SparkLibrary, updateSparkLibrary); @@ -10,13 +12,15 @@ export function activateUpdateSparkLibraryProvider(): vscode.Disposable { function updateSparkLibrary() { const pythonConfig = vscode.workspace.getConfiguration('python'); const extraLibPath = 'autoComplete.extraPaths'; - let sparkHomePath = '${env.SPARK_HOME}'; + // tslint:disable-next-line:no-invalid-template-strings + const sparkHomePath = '${env.SPARK_HOME}'; pythonConfig.update(extraLibPath, [path.join(sparkHomePath, 'python'), path.join(sparkHomePath, 'python/pyspark')]).then(() => { //Done }, reason => { vscode.window.showErrorMessage(`Failed to update ${extraLibPath}. Error: ${reason.message}`); console.error(reason); - }); - vscode.window.showInformationMessage(`Make sure you have SPARK_HOME environment variable set to the root path of the local spark installation!`); -} \ No newline at end of file + }); + vscode.window.showInformationMessage('Make sure you have SPARK_HOME environment variable set to the root path of the local spark installation!'); + sendTelemetryEvent(UPDATE_PYSPARK_LIBRARY); +} diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 5a238273d8d8..2511c99450d9 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -1,12 +1,11 @@ 'use strict'; -import * as vscode from 'vscode'; -import * as path from 'path'; import * as child_process from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; import { IPythonSettings } from '../common/configSettings'; -import { REFACTOR } from '../common/telemetryContracts'; -import { getCustomEnvVars, getCustomEnvVarsSync, getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; import { mergeEnvVariables } from '../common/envFileParser'; +import { getCustomEnvVarsSync, getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; export class RefactorProxy extends vscode.Disposable { private _process: child_process.ChildProcess; @@ -57,7 +56,7 @@ export class RefactorProxy extends vscode.Disposable { "indent_size": options.tabSize }; - return this.sendCommand(JSON.stringify(command), REFACTOR.Rename); + return this.sendCommand(JSON.stringify(command)); } extractVariable(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { if (!options) { @@ -72,7 +71,7 @@ export class RefactorProxy extends vscode.Disposable { "name": name, "indent_size": options.tabSize }; - return this.sendCommand(JSON.stringify(command), REFACTOR.ExtractVariable); + return this.sendCommand(JSON.stringify(command)); } extractMethod(document: vscode.TextDocument, name: string, filePath: string, range: vscode.Range, options?: vscode.TextEditorOptions): Promise { if (!options) { @@ -91,9 +90,9 @@ export class RefactorProxy extends vscode.Disposable { "name": name, "indent_size": options.tabSize }; - return this.sendCommand(JSON.stringify(command), REFACTOR.ExtractMethod); + return this.sendCommand(JSON.stringify(command)); } - private sendCommand(command: string, telemetryEvent: string): Promise { + private sendCommand(command: string, telemetryEvent?: string): Promise { return this.initialize(this.pythonSettings.pythonPath).then(() => { return new Promise((resolve, reject) => { this._commandResolve = resolve; diff --git a/src/client/typings/node.d.ts b/src/client/typings/node.d.ts deleted file mode 100644 index 90d55c6f4c08..000000000000 --- a/src/client/typings/node.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// -/// -/// \ No newline at end of file diff --git a/src/client/typings/vscode-typings.d.ts b/src/client/typings/vscode-typings.d.ts deleted file mode 100644 index a9c71567c847..000000000000 --- a/src/client/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index bfb3fd550ffe..10f115148594 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -4,6 +4,7 @@ import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, import { Uri } from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { CommandSource } from '../common/constants'; import { ITestCollectionStorageService, TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/types'; type FunctionsAndSuites = { @@ -107,12 +108,12 @@ export class TestFileCodeLensProvider implements CodeLensProvider { new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [file, { testSuite: [cls] }] + arguments: [CommandSource.codelens, file, { testSuite: [cls] }] }), new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [file, { testSuite: [cls] }] + arguments: [CommandSource.codelens, file, { testSuite: [cls] }] }) ]; } @@ -181,12 +182,12 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [file, { testFunction: [fn] }] + arguments: [CommandSource.codelens, file, { testFunction: [fn] }] }), new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [file, { testFunction: [fn] }] + arguments: [CommandSource.codelens, file, { testFunction: [fn] }] }) ]; } @@ -202,12 +203,12 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, new CodeLens(range, { title: constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [file, { testFunction: functions }] + arguments: [CommandSource.codelens, file, { testFunction: functions }] }), new CodeLens(range, { title: constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [file, { testFunction: functions }] + arguments: [CommandSource.codelens, file, { testFunction: functions }] }) ]; } @@ -217,12 +218,12 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, - arguments: [file, functions] + arguments: [CommandSource.codelens, file, functions] }), new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [file, functions] + arguments: [CommandSource.codelens, file, functions] }) ]; } diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index 4b4612c7da85..633fbb3ad14d 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -4,7 +4,10 @@ import { Uri, workspace } from 'vscode'; import { IPythonSettings, PythonSettings } from '../../common/configSettings'; import { isNotInstalledError } from '../../common/helpers'; import { Installer, Product } from '../../common/installer'; -import { CANCELLATION_REASON } from './constants'; +import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/index'; +import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../common/telemetry/types'; +import { CANCELLATION_REASON, CommandSource } from './constants'; import { displayTestErrorMessage } from './testUtils'; import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; @@ -12,7 +15,7 @@ enum CancellationTokenType { testDiscovery, testRunner } - +type TestProvider = 'nosetest' | 'pytest' | 'unittest'; export abstract class BaseTestManager { public readonly workspace: Uri; protected readonly settings: IPythonSettings; @@ -23,7 +26,7 @@ export abstract class BaseTestManager { private testRunnerCancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; private discoverTestsPromise: Promise; - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, + constructor(public readonly testProvider: TestProvider, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel, private testCollectionStorage: ITestCollectionStorageService, protected testResultsService: ITestResultsService, protected testsHelper: ITestsHelper) { this._status = TestStatus.Unknown; @@ -66,7 +69,7 @@ export abstract class BaseTestManager { this.testResultsService.resetResults(this.tests); } - public async discoverTests(ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise { + public async discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise { if (this.discoverTestsPromise) { return this.discoverTestsPromise; } @@ -82,6 +85,12 @@ export abstract class BaseTestManager { if (userInitiated) { this.stop(); } + const telementryProperties: TestDiscoverytTelemetry = { + tool: this.testProvider, + // tslint:disable-next-line:no-any prefer-type-cast + trigger: cmdSource as any, + failed: false + }; this.createCancellationToken(CancellationTokenType.testDiscovery); return this.discoverTestsPromise = this.discoverTestsImpl(ignoreCache) @@ -108,7 +117,7 @@ export abstract class BaseTestManager { const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, tests); this.disposeCancellationToken(CancellationTokenType.testDiscovery); - + sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); return tests; }).catch(reason => { if (isNotInstalledError(reason) && !quietMode) { @@ -122,6 +131,8 @@ export abstract class BaseTestManager { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; } else { + telementryProperties.failed = true; + sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); this._status = TestStatus.Error; this.outputChannel.appendLine('Test Disovery failed: '); // tslint:disable-next-line:prefer-template @@ -133,7 +144,7 @@ export abstract class BaseTestManager { return Promise.reject(reason); }); } - public runTest(testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + public runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { const moreInfo = { Test_Provider: this.testProvider, Run_Failed_Tests: 'false', @@ -141,33 +152,46 @@ export abstract class BaseTestManager { Run_Specific_Class: 'false', Run_Specific_Function: 'false' }; - + const telementryProperties: TestRunTelemetry = { + tool: this.testProvider, + scope: 'all', + debugging: debug === true, + trigger: cmdSource, + failed: false + }; if (runFailedTests === true) { // tslint:disable-next-line:prefer-template moreInfo.Run_Failed_Tests = runFailedTests + ''; + telementryProperties.scope = 'failed'; } if (testsToRun && typeof testsToRun === 'object') { if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { + telementryProperties.scope = 'file'; moreInfo.Run_Specific_File = 'true'; } if (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) { + telementryProperties.scope = 'class'; moreInfo.Run_Specific_Class = 'true'; } if (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) { + telementryProperties.scope = 'function'; moreInfo.Run_Specific_Function = 'true'; } } + if (runFailedTests === false && testsToRun === null) { this.resetTestResults(); } this._status = TestStatus.Running; - this.stop(); + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); + } // If running failed tests, then don't clear the previously build UnitTests // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) const clearDiscoveredTestCache = runFailedTests || moreInfo.Run_Specific_File || moreInfo.Run_Specific_Class || moreInfo.Run_Specific_Function ? false : true; - return this.discoverTests(clearDiscoveredTestCache, true, true) + return this.discoverTests(cmdSource, clearDiscoveredTestCache, true, true) .catch(reason => { if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { return Promise.reject(reason); @@ -184,6 +208,7 @@ export abstract class BaseTestManager { }).then(() => { this._status = TestStatus.Idle; this.disposeCancellationToken(CancellationTokenType.testRunner); + sendTelemetryEvent(UNITTEST_RUN, undefined, telementryProperties); return this.tests; }).catch(reason => { if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { @@ -191,6 +216,8 @@ export abstract class BaseTestManager { this._status = TestStatus.Idle; } else { this._status = TestStatus.Error; + telementryProperties.failed = true; + sendTelemetryEvent(UNITTEST_RUN, undefined, telementryProperties); } this.disposeCancellationToken(CancellationTokenType.testRunner); return Promise.reject(reason); diff --git a/src/client/unittests/common/constants.ts b/src/client/unittests/common/constants.ts index 3d428cfad97c..9bd66948c6e3 100644 --- a/src/client/unittests/common/constants.ts +++ b/src/client/unittests/common/constants.ts @@ -1 +1,7 @@ export const CANCELLATION_REASON = 'cancelled_user_request'; +export enum CommandSource { + auto = 'auto', + ui = 'ui', + codelens = 'codelens', + commandPalette = 'commandpalette' +} diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 7ce389bdf66f..b552b4c82b7e 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -3,8 +3,8 @@ import * as vscode from 'vscode'; import { Uri, workspace } from 'vscode'; import { window } from 'vscode'; import * as constants from '../../common/constants'; +import { CommandSource } from './constants'; import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; -import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; import { TestFile, TestFolder, Tests, TestsToRun } from './types'; import { ITestsHelper } from './types'; @@ -23,7 +23,7 @@ export async function selectTestWorkspace(): Promise { export function displayTestErrorMessage(message: string) { vscode.window.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { if (action === constants.Button_Text_Tests_View_Output) { - vscode.commands.executeCommand(constants.Commands.Tests_ViewOutput); + vscode.commands.executeCommand(constants.Commands.Tests_ViewOutput, CommandSource.ui); } }); diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index 61c18840ae51..2b3d47652abf 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { QuickPickItem, Uri, window } from 'vscode'; import * as vscode from 'vscode'; import * as constants from '../../common/constants'; +import { CommandSource } from '../common/constants'; import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; export class TestDisplay { @@ -13,10 +14,10 @@ export class TestDisplay { } }); } - public displayTestUI(wkspace: Uri) { + public displayTestUI(cmdSource: CommandSource, wkspace: Uri) { const tests = this.testCollectionStorage.getTests(wkspace); window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) - .then(item => onItemSelected(wkspace, item, false)); + .then(item => onItemSelected(cmdSource, wkspace, item, false)); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { @@ -40,7 +41,7 @@ export class TestDisplay { }, reject); }); } - public displayFunctionTestPickerUI(wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + public displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { const tests = this.testCollectionStorage.getTests(wkspace); if (!tests) { return; @@ -57,7 +58,7 @@ export class TestDisplay { window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(wkspace, testItem, debug); + return onItemSelected(cmdSource, wkspace, testItem, debug); }); } } @@ -187,13 +188,13 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T }); return fileItems; } -function onItemSelected(wkspace: Uri, selection: TestItem, debug?: boolean) { +function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestItem, debug?: boolean) { if (!selection || typeof selection.type !== 'number') { return; } let cmd = ''; // tslint:disable-next-line:no-any - const args: any[] = [wkspace]; + const args: any[] = [cmdSource, wkspace]; switch (selection.type) { case Type.Null: { return; diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index adb86808d695..f7e0b0978f0a 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,25 +1,24 @@ 'use strict'; import { Uri, window, workspace } from 'vscode'; import * as vscode from 'vscode'; -import { IUnitTestSettings, PythonSettings } from '../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; import * as constants from '../common/constants'; +import { UNITTEST_STOP, UNITTEST_VIEW_OUTPUT } from '../common/telemetry/constants'; +import { sendTelemetryEvent } from '../common/telemetry/index'; import { PythonSymbolProvider } from '../providers/symbolProvider'; import { activateCodeLenses } from './codeLenses/main'; import { BaseTestManager } from './common/baseTestManager'; -import { CANCELLATION_REASON } from './common/constants'; +import { CANCELLATION_REASON, CommandSource } from './common/constants'; import { DebugLauncher } from './common/debugLauncher'; import { TestCollectionStorageService } from './common/storageService'; import { TestManagerServiceFactory } from './common/testManagerServiceFactory'; import { TestResultsService } from './common/testResultsService'; import { selectTestWorkspace, TestsHelper } from './common/testUtils'; -import { FlattenedTestFunction, ITestCollectionStorageService, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; +import { ITestCollectionStorageService, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; import { WorkspaceTestManagerService } from './common/workspaceTestManagerService'; import { displayTestFrameworkError } from './configuration'; import { TestResultDisplay } from './display/main'; import { TestDisplay } from './display/picker'; -import * as nosetests from './nosetest/main'; -import * as pytest from './pytest/main'; -import * as unittest from './unittest/main'; let workspaceTestManagerService: IWorkspaceTestManagerService; let testResultDisplay: TestResultDisplay; @@ -73,7 +72,7 @@ async function onDocumentSaved(doc: vscode.TextDocument): Promise { if (!testManager) { return; } - const tests = await testManager.discoverTests(false, true); + const tests = await testManager.discoverTests(CommandSource.auto, false, true); if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { return; } @@ -84,7 +83,7 @@ async function onDocumentSaved(doc: vscode.TextDocument): Promise { if (timeoutId) { clearTimeout(timeoutId); } - timeoutId = setTimeout(() => discoverTests(doc.uri, true), 1000); + timeoutId = setTimeout(() => discoverTests(CommandSource.auto, doc.uri, true), 1000); } function dispose() { @@ -93,62 +92,67 @@ function dispose() { } function registerCommands(): vscode.Disposable[] { const disposables = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (resource?: Uri) => { + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { // Ignore the exceptions returned. // This command will be invoked else where in the extension. // tslint:disable-next-line:no-empty - discoverTests(resource, true, true).catch(() => { }); + discoverTests(cmdSource, resource, true, true).catch(() => { }); })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (resource: Uri) => runTestsImpl(resource, undefined, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => runTestsImpl(cmdSource, resource, undefined, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (file: Uri, testToRun?: TestsToRun) => runTestsImpl(file, testToRun))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (file: Uri, testToRun: TestsToRun) => runTestsImpl(file, testToRun, false, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => runTestsImpl(cmdSource, file, testToRun))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => runTestsImpl(cmdSource, file, testToRun, false, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI())); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI(CommandSource.commandPalette))); // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file: Uri, testFunctions: TestFunction[]) => displayPickerUI(file, testFunctions))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (file, testFunctions) => displayPickerUI(file, testFunctions, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (resource: Uri) => stopTests(resource))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, () => outChannel.show())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, (cmdSource: CommandSource = CommandSource.commandPalette) => viewOutput(cmdSource))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => displayStopUI('Stop discovering tests'))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => displayStopUI('Stop running tests'))); // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (resource: Uri) => selectAndRunTestMethod(resource))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (resource: Uri) => selectAndRunTestMethod(resource, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, () => selectAndRunTestFile())); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, (cmdSource: CommandSource = CommandSource.commandPalette) => selectAndRunTestFile(cmdSource))); // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, () => runCurrentTestFile())); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, (cmdSource: CommandSource = CommandSource.commandPalette) => runCurrentTestFile(cmdSource))); return disposables; } -async function displayUI() { +function viewOutput(cmdSource: CommandSource) { + sendTelemetryEvent(UNITTEST_VIEW_OUTPUT); + outChannel.show(); +} +async function displayUI(cmdSource: CommandSource) { const testManager = await getTestManager(true); if (!testManager) { return; } testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayTestUI(testManager.workspace); + testDisplay.displayTestUI(cmdSource, testManager.workspace); } -async function displayPickerUI(file: Uri, testFunctions: TestFunction[], debug?: boolean) { +async function displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { const testManager = await getTestManager(true, file); if (!testManager) { return; } testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayFunctionTestPickerUI(testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); + testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); } -async function selectAndRunTestMethod(resource: Uri, debug?: boolean) { +async function selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { const testManager = await getTestManager(true, resource); if (!testManager) { return; } try { - await testManager.discoverTests(true, true, true); + await testManager.discoverTests(cmdSource, true, true, true); } catch (ex) { return; } @@ -160,15 +164,15 @@ async function selectAndRunTestMethod(resource: Uri, debug?: boolean) { return; } // tslint:disable-next-line:prefer-type-cast - await runTestsImpl(testManager.workspace, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, debug); + await runTestsImpl(cmdSource, testManager.workspace, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, debug); } -async function selectAndRunTestFile() { +async function selectAndRunTestFile(cmdSource: CommandSource) { const testManager = await getTestManager(true); if (!testManager) { return; } try { - await testManager.discoverTests(true, true, true); + await testManager.discoverTests(cmdSource, true, true, true); } catch (ex) { return; } @@ -180,9 +184,9 @@ async function selectAndRunTestFile() { return; } // tslint:disable-next-line:prefer-type-cast - await runTestsImpl(testManager.workspace, { testFile: [selectedFile] } as TestsToRun); + await runTestsImpl(cmdSource, testManager.workspace, { testFile: [selectedFile] } as TestsToRun); } -async function runCurrentTestFile() { +async function runCurrentTestFile(cmdSource: CommandSource) { if (!window.activeTextEditor) { return; } @@ -191,7 +195,7 @@ async function runCurrentTestFile() { return; } try { - await testManager.discoverTests(true, true, true); + await testManager.discoverTests(cmdSource, true, true, true); } catch (ex) { return; } @@ -203,7 +207,7 @@ async function runCurrentTestFile() { return; } // tslint:disable-next-line:prefer-type-cast - await runTestsImpl(testManager.workspace, { testFile: [testFiles[0]] } as TestsToRun); + await runTestsImpl(cmdSource, testManager.workspace, { testFile: [testFiles[0]] } as TestsToRun); } async function displayStopUI(message: string) { const testManager = await getTestManager(true); @@ -265,15 +269,16 @@ function autoDiscoverTests() { // No need to display errors. // tslint:disable-next-line:no-empty - discoverTests(workspace.workspaceFolders[0].uri, true).catch(() => { }); + discoverTests(CommandSource.auto, workspace.workspaceFolders[0].uri, true).catch(() => { }); } async function stopTests(resource: Uri) { + sendTelemetryEvent(UNITTEST_STOP); const testManager = await getTestManager(true, resource); if (testManager) { testManager.stop(); } } -async function discoverTests(resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean) { +async function discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean) { const testManager = await getTestManager(true, resource); if (!testManager) { return; @@ -281,7 +286,7 @@ async function discoverTests(resource?: Uri, ignoreCache?: boolean, userInitiate if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - const discoveryPromise = testManager.discoverTests(ignoreCache, false, userInitiated); + const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, false, userInitiated); testResultDisplay.displayDiscoverStatus(discoveryPromise); await discoveryPromise; } @@ -299,14 +304,14 @@ function isTestsToRun(arg: any): arg is TestsToRun { } return false; } -async function runTestsImpl(resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { +async function runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { const testManager = await getTestManager(true, resource); if (!testManager) { return; } testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - const promise = testManager.runTest(testsToRun, runFailedTests, debug) + const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug) .catch(reason => { if (reason !== CANCELLATION_REASON) { outChannel.appendLine(`Error: ${reason}`); diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 162ec396b6ef..09e96e307fa5 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -1,6 +1,5 @@ 'use strict'; import * as vscode from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; @@ -10,16 +9,17 @@ export class TestManager extends BaseTestManager { constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { - super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); + super('unittest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } // tslint:disable-next-line:no-empty public configure() { } - public discoverTestsImpl(ignoreCache: boolean): Promise { + public async discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.unittestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); + // tslint:disable-next-line:no-non-null-assertion + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken!, ignoreCache, this.outputChannel, this.testsHelper); } - public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { + public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.unittestArgs.slice(0); if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; diff --git a/src/client/workspaceSymbols/contracts.ts b/src/client/workspaceSymbols/contracts.ts index c5255c3fbcc2..cb53f8b7d397 100644 --- a/src/client/workspaceSymbols/contracts.ts +++ b/src/client/workspaceSymbols/contracts.ts @@ -1,4 +1,4 @@ -import { SymbolKind, Position } from 'vscode'; +import { Position, SymbolKind } from 'vscode'; export interface Tag { fileName: string; diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts index dcb1b5ae3d35..acbc3b5354cc 100644 --- a/src/client/workspaceSymbols/generator.ts +++ b/src/client/workspaceSymbols/generator.ts @@ -1,8 +1,10 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs'; import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import { captureTelemetry } from '../common/telemetry'; +import { WORKSPACE_SYMBOLS_BUILD } from '../common/telemetry/constants'; export class Generator implements vscode.Disposable { private optionsFile: string; @@ -20,7 +22,7 @@ export class Generator implements vscode.Disposable { this.pythonSettings = PythonSettings.getInstance(workspaceFolder); } - dispose() { + public dispose() { this.disposables.forEach(d => d.dispose()); } @@ -32,13 +34,13 @@ export class Generator implements vscode.Disposable { return [`--options=${optionsFile}`, '--languages=Python'].concat(excludes); } - async generateWorkspaceTags(): Promise { + public async generateWorkspaceTags(): Promise { if (!this.pythonSettings.workspaceSymbols.enabled) { return; } return await this.generateTags({ directory: this.workspaceFolder.fsPath }); } - + @captureTelemetry(WORKSPACE_SYMBOLS_BUILD) private generateTags(source: { directory?: string, file?: string }): Promise { const tagFile = path.normalize(this.pythonSettings.workspaceSymbols.tagFilePath); const cmd = this.pythonSettings.workspaceSymbols.ctagsPath; @@ -94,4 +96,4 @@ export class Generator implements vscode.Disposable { return promise; } -} \ No newline at end of file +} diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts index e9124212e9a8..736c6ff0f750 100644 --- a/src/client/workspaceSymbols/provider.ts +++ b/src/client/workspaceSymbols/provider.ts @@ -1,15 +1,18 @@ -import * as vscode from 'vscode'; import * as _ from 'lodash'; +import * as vscode from 'vscode'; +import { Commands } from '../common/constants'; +import { captureTelemetry } from '../common/telemetry'; +import { WORKSPACE_SYMBOLS_GO_TO } from '../common/telemetry/constants'; +import { fsExistsAsync } from '../common/utils'; import { Generator } from './generator'; import { parseTags } from './parser'; -import { fsExistsAsync } from '../common/utils'; -import { Commands } from '../common/constants'; export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { public constructor(private tagGenerators: Generator[], private outputChannel: vscode.OutputChannel) { } - async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Promise { + @captureTelemetry(WORKSPACE_SYMBOLS_GO_TO) + public async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Promise { if (this.tagGenerators.length === 0) { return []; } diff --git a/src/test/common/common.test.ts b/src/test/common/common.test.ts index 0e72ac770b71..aeccb54166da 100644 --- a/src/test/common/common.test.ts +++ b/src/test/common/common.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import { EOL } from 'os'; import * as vscode from 'vscode'; import { createDeferred } from '../../client/common/helpers'; -import { execPythonFile, getInterpreterDisplayName } from '../../client/common/utils'; +import { execPythonFile, getInterpreterVersion } from '../../client/common/utils'; import { initialize } from './../initialize'; // Defines a Mocha test suite to group tests of similar kind together @@ -83,7 +83,7 @@ suite('ChildProc', () => { }); test('Get Python display name', async () => { - const displayName = await getInterpreterDisplayName('python'); + const displayName = await getInterpreterVersion('python'); assert.equal(typeof displayName, 'string', 'Display name not returned'); assert.notEqual(displayName.length, 0, 'Display name cannot be empty'); }); diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.test.ts index 26bd45c9928d..88f3ae8934e3 100644 --- a/src/test/interpreters/condaEnvFileService.test.ts +++ b/src/test/interpreters/condaEnvFileService.test.ts @@ -1,24 +1,24 @@ import * as assert from 'assert'; -import * as path from 'path'; import * as fs from 'fs-extra'; import { EOL } from 'os'; -import { initialize, initializeTest } from '../initialize'; +import * as path from 'path'; import { IS_WINDOWS } from '../../client/common/utils'; -import { MockInterpreterVersionProvider } from './mocks'; -import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName, - CONDA_RELATIVE_PY_PATH, + CONDA_RELATIVE_PY_PATH } from '../../client/interpreter/locators/services/conda'; +import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; +import { initialize, initializeTest } from '../initialize'; +import { MockInterpreterVersionProvider } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); suite('Interpreters from Conda Environments Text File', () => { - suiteSetup(() => initialize()); - setup(() => initializeTest()); + suiteSetup(initialize); + setup(initializeTest); suiteTeardown(async () => { // Clear the file so we don't get unwanted changes prompting for a checkin of this file await updateEnvWithInterpreters([]); diff --git a/src/test/interpreters/interpreterVersion.test.ts b/src/test/interpreters/interpreterVersion.test.ts new file mode 100644 index 000000000000..c0d294fee060 --- /dev/null +++ b/src/test/interpreters/interpreterVersion.test.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { execPythonFile } from '../../client/common/utils'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { initialize, initializeTest } from '../initialize'; + +use(chaiAsPromised); + +suite('Interpreters display version', () => { + const interpreterVersion = new InterpreterVersionService(); + suiteSetup(initialize); + setup(initializeTest); + + test('Must return the Python Version', async () => { + const output = await execPythonFile(undefined, 'python', ['--version'], __dirname, true); + const version = getFirstNonEmptyLineFromMultilineString(output); + const pyVersion = await interpreterVersion.getVersion('python', 'DEFAULT_TEST_VALUE'); + assert.equal(pyVersion, version, 'Incorrect version'); + }); + test('Must return the default value when Python path is invalid', async () => { + const pyVersion = await interpreterVersion.getVersion('INVALID_INTERPRETER', 'DEFAULT_TEST_VALUE'); + assert.equal(pyVersion, 'DEFAULT_TEST_VALUE', 'Incorrect version'); + }); + test('Must return the pip Version', async () => { + const output = await execPythonFile(undefined, 'python', ['-m', 'pip', '--version'], __dirname, true); + // Take the second part, see below example. + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + const re = new RegExp('\\d\\.\\d(\\.\\d)+', 'g'); + const matches = re.exec(output); + assert.isNotNull(matches, 'No matches for version found'); + // tslint:disable-next-line:no-non-null-assertion + assert.isAtLeast(matches!.length, 1, 'Version number not found'); + + const pipVersionPromise = interpreterVersion.getPipVersion('python'); + // tslint:disable-next-line:no-non-null-assertion + await expect(pipVersionPromise).to.eventually.equal(matches![0].trim()); + }); + test('Must throw an exceptionn when pip version cannot be determine', async () => { + const pipVersionPromise = interpreterVersion.getPipVersion('INVALID_INTERPRETER'); + await expect(pipVersionPromise).to.be.rejectedWith(); + }); +}); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index 34c41bb41c75..db4cfed0c474 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -55,10 +55,14 @@ export class MockVirtualEnv implements IVirtualEnvironment { // tslint:disable-next-line:max-classes-per-file export class MockInterpreterVersionProvider implements IInterpreterVersionService { - constructor(private displayName: string, private useDefaultDisplayName: boolean = false) { } + constructor(private displayName: string, private useDefaultDisplayName: boolean = false, + private pipVersionPromise?: Promise) { } public getVersion(pythonPath: string, defaultDisplayName: string): Promise { return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); } + public getPipVersion(pythonPath: string): Promise { + return this.pipVersionPromise; + } // tslint:disable-next-line:no-empty public dispose() { } } diff --git a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts index 7f7c1040c68b..05025376fe3d 100644 --- a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts +++ b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts @@ -1,17 +1,13 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; -import { WorkspacePythonPath } from '../../client/interpreter/contracts'; -import { clearPythonPathInWorkspaceFolder } from '../common'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); @@ -55,9 +51,9 @@ suite('Multiroot Python Path Settings Updater', () => { test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { const workspaceUri = workspace3Uri; - const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), new InterpreterVersionService()); const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; - await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.WorkspaceFolder, workspace.getWorkspaceFolder(workspaceUri).uri); + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.WorkspaceFolder, 'ui', workspace.getWorkspaceFolder(workspaceUri).uri); const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); }); diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts index 4b11c96ebd60..c74aba47ec07 100644 --- a/src/test/interpreters/pythonPathUpdater.test.ts +++ b/src/test/interpreters/pythonPathUpdater.test.ts @@ -1,14 +1,11 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; -import { WorkspacePythonPath } from '../../client/interpreter/contracts'; -import { clearPythonPathInWorkspaceFolder } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); @@ -72,9 +69,9 @@ suite('Python Path Settings Updater', () => { test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { const workspaceUri = Uri.file(workspaceRoot); - const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), new InterpreterVersionService()); const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; - await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Workspace, workspace.getWorkspaceFolder(workspaceUri).uri); + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Workspace, 'ui', workspace.getWorkspaceFolder(workspaceUri).uri); const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); }); diff --git a/src/test/jupyter/jupyterKernelManager.test.ts b/src/test/jupyter/jupyterKernelManager.test.ts index 27b99e77c5e6..7d43024ccfca 100644 --- a/src/test/jupyter/jupyterKernelManager.test.ts +++ b/src/test/jupyter/jupyterKernelManager.test.ts @@ -5,7 +5,7 @@ import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize' import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelManagerImpl } from '../../client/jupyter/kernel-manager'; -suite('Kernel Manager', () => { +suite('Jupyter Kernel Manager', () => { suiteSetup(async function () { if (IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts index 23616ebb3e70..158f6e6db987 100644 --- a/src/test/providers/shebangCodeLenseProvider.test.ts +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -2,12 +2,10 @@ import * as assert from 'assert'; import * as child_process from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; -import { ConfigurationTarget } from 'vscode'; import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'shebang'); const fileShebang = path.join(autoCompPath, 'shebang.py'); diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts index c908a7f7080e..e02fd63422e1 100644 --- a/src/test/unittests/debugger.test.ts +++ b/src/test/unittests/debugger.test.ts @@ -1,15 +1,14 @@ -import { assert, expect, should, use } from 'chai'; +import { assert, expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; import { createDeferred } from '../../client/common/helpers'; import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; -import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { CANCELLATION_REASON, CommandSource } from '../../client/unittests/common/constants'; import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; import { TestResultsService } from '../../client/unittests/common/testResultsService'; import { TestsHelper } from '../../client/unittests/common/testUtils'; -import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; @@ -75,13 +74,14 @@ suite('Unit Tests Debugging', () => { } async function testStartingDebugger() { - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); const testFunction = [tests.testFunctions[0].testFunction]; - testManager.runTest({ testFunction }, false, true); + // tslint:disable-next-line:no-floating-promises + testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); const launched = await mockDebugLauncher.launched; assert.isTrue(launched, 'Debugger not launched'); } @@ -108,19 +108,20 @@ suite('Unit Tests Debugging', () => { }); async function testStoppingDebugger() { - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); const testFunction = [tests.testFunctions[0].testFunction]; - const runningPromise = testManager.runTest({ testFunction }, false, true); + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); const launched = await mockDebugLauncher.launched; assert.isTrue(launched, 'Debugger not launched'); - const discoveryPromise = testManager.discoverTests(true, true, true); + // tslint:disable-next-line:no-floating-promises + testManager.discoverTests(CommandSource.commandPalette, true, true, true); - expect(runningPromise).eventually.throws(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); + await expect(runningPromise).to.be.rejectedWith(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); } test('Debugger should stop when user invokes a test discovery (unittest)', async () => { @@ -145,24 +146,28 @@ suite('Unit Tests Debugging', () => { }); async function testDebuggerWhenRediscoveringTests() { - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); const testFunction = [tests.testFunctions[0].testFunction]; - const runningPromise = testManager.runTest({ testFunction }, false, true); + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); const launched = await mockDebugLauncher.launched; assert.isTrue(launched, 'Debugger not launched'); - const discoveryPromise = testManager.discoverTests(false, true); + const discoveryPromise = testManager.discoverTests(CommandSource.commandPalette, false, true); const deferred = createDeferred(); + // tslint:disable-next-line:no-floating-promises discoveryPromise + // tslint:disable-next-line:no-unsafe-any .then(() => deferred.resolve('')) + // tslint:disable-next-line:no-unsafe-any .catch(ex => deferred.reject(ex)); // This promise should never resolve nor reject. + // tslint:disable-next-line:no-floating-promises runningPromise .then(() => 'Debugger stopped when it shouldn\'t have') .catch(() => 'Debugger crashed when it shouldn\'t have') diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts index 036c0ffa14d1..fd9741543a78 100644 --- a/src/test/unittests/mocks.ts +++ b/src/test/unittests/mocks.ts @@ -1,13 +1,17 @@ import { CancellationToken, Disposable, OutputChannel } from 'vscode'; import { createDeferred, Deferred } from '../../client/common/helpers'; -import { ITestDebugLauncher, Tests } from '../../client/unittests/common/types'; +import { Product } from '../../client/common/installer'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../../client/unittests/common/types'; export class MockDebugLauncher implements ITestDebugLauncher, Disposable { public get launched(): Promise { return this._launched.promise; } public get debuggerPromise(): Deferred { - return this._promise; + // tslint:disable-next-line:no-non-null-assertion + return this._promise!; } public get cancellationToken(): CancellationToken { return this._token; @@ -38,3 +42,29 @@ export class MockDebugLauncher implements ITestDebugLauncher, Disposable { this._promise = undefined; } } + +export class MockTestManagerWithRunningTests extends BaseTestManager { + // tslint:disable-next-line:no-any + public readonly runnerDeferred = createDeferred(); + // tslint:disable-next-line:no-any + public readonly discoveryDeferred = createDeferred(); + constructor(testRunnerId: 'nosetest' | 'pytest' | 'unittest', product: Product, rootDirectory: string, + outputChannel: OutputChannel, storageService: ITestCollectionStorageService, resultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('nosetest', product, rootDirectory, outputChannel, storageService, resultsService, testsHelper); + } + // tslint:disable-next-line:no-any + protected async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + // tslint:disable-next-line:no-non-null-assertion + this.testRunnerCancellationToken!.onCancellationRequested(() => { + this.runnerDeferred.reject(CANCELLATION_REASON); + }); + return this.runnerDeferred.promise; + } + protected async discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise { + // tslint:disable-next-line:no-non-null-assertion + this.testDiscoveryCancellationToken!.onCancellationRequested(() => { + this.discoveryDeferred.reject(CANCELLATION_REASON); + }); + return this.discoveryDeferred.promise; + } +} diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index 9105c483fec3..c932a1950332 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; +import { CommandSource } from '../../client/unittests/common/constants'; import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; import { TestResultsService } from '../../client/unittests/common/testResultsService'; import { TestsHelper } from '../../client/unittests/common/testUtils'; @@ -68,7 +69,7 @@ suite('Unit Tests (nosetest)', () => { test('Discover Tests (single test file)', async () => { createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); @@ -77,7 +78,7 @@ suite('Unit Tests (nosetest)', () => { test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); @@ -91,7 +92,7 @@ suite('Unit Tests (nosetest)', () => { test('Discover Tests (-m=test)', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 6, 'Incorrect number of test suites'); @@ -105,7 +106,7 @@ suite('Unit Tests (nosetest)', () => { test('Discover Tests (-w=specific -m=tst)', async () => { await updateSetting('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); @@ -116,7 +117,7 @@ suite('Unit Tests (nosetest)', () => { test('Discover Tests (-m=test_)', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); @@ -126,7 +127,7 @@ suite('Unit Tests (nosetest)', () => { test('Run Tests', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); - const results = await testManager.runTest(); + const results = await testManager.runTest(CommandSource.ui); assert.equal(results.summary.errors, 1, 'Errors'); assert.equal(results.summary.failures, 7, 'Failures'); assert.equal(results.summary.passed, 6, 'Passed'); @@ -136,13 +137,13 @@ suite('Unit Tests (nosetest)', () => { test('Run Failed Tests', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); - let results = await testManager.runTest(); + let results = await testManager.runTest(CommandSource.ui); assert.equal(results.summary.errors, 1, 'Errors'); assert.equal(results.summary.failures, 7, 'Failures'); assert.equal(results.summary.passed, 6, 'Passed'); assert.equal(results.summary.skipped, 2, 'skipped'); - results = await testManager.runTest(undefined, true); + results = await testManager.runTest(CommandSource.ui, undefined, true); assert.equal(results.summary.errors, 1, 'Errors again'); assert.equal(results.summary.failures, 7, 'Failures again'); assert.equal(results.summary.passed, 0, 'Passed again'); @@ -152,12 +153,12 @@ suite('Unit Tests (nosetest)', () => { test('Run Specific Test File', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); const testFileToRun = tests.testFiles.find(t => t.fullPath.endsWith('test_root.py')); assert.ok(testFileToRun, 'Test file not found'); // tslint:disable-next-line:no-non-null-assertion const testFile: TestsToRun = { testFile: [testFileToRun!], testFolder: [], testFunction: [], testSuite: [] }; - const results = await testManager.runTest(testFile); + const results = await testManager.runTest(CommandSource.ui, testFile); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 1, 'Passed'); @@ -167,12 +168,12 @@ suite('Unit Tests (nosetest)', () => { test('Run Specific Test Suite', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); const testSuiteToRun = tests.testSuites.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); assert.ok(testSuiteToRun, 'Test suite not found'); // tslint:disable-next-line:no-non-null-assertion const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToRun!.testSuite] }; - const results = await testManager.runTest(testSuite); + const results = await testManager.runTest(CommandSource.ui, testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 1, 'Passed'); @@ -182,12 +183,12 @@ suite('Unit Tests (nosetest)', () => { test('Run Specific Test Function', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); const testFnToRun = tests.testFunctions.find(f => f.xmlClassName === 'test_root.Test_Root_test1'); assert.ok(testFnToRun, 'Test function not found'); // tslint:disable-next-line:no-non-null-assertion const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [testFnToRun!.testFunction], testSuite: [] }; - const results = await testManager.runTest(testFn); + const results = await testManager.runTest(CommandSource.ui, testFn); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 0, 'Passed'); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index cf0ba4130ca8..1b016f4228cc 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -1,6 +1,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; +import { CommandSource } from '../../client/unittests/common/constants'; import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; import { TestResultsService } from '../../client/unittests/common/testResultsService'; import { TestsHelper } from '../../client/unittests/common/testUtils'; @@ -55,7 +56,7 @@ suite('Unit Tests (PyTest)', () => { resultsService = new TestResultsService(); testsHelper = new TestsHelper(); testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); @@ -66,7 +67,7 @@ suite('Unit Tests (PyTest)', () => { test('Discover Tests (pattern = test_)', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 29, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 8, 'Incorrect number of test suites'); @@ -81,7 +82,7 @@ suite('Unit Tests (PyTest)', () => { test('Discover Tests (pattern = _test)', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); @@ -92,7 +93,7 @@ suite('Unit Tests (PyTest)', () => { await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); rootDirectory = UNITTEST_TEST_FILES_PATH_WITH_CONFIGS; createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 4, 'Incorrect number of test suites'); @@ -103,7 +104,7 @@ suite('Unit Tests (PyTest)', () => { test('Run Tests', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - const results = await testManager.runTest(); + const results = await testManager.runTest(CommandSource.ui); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 9, 'Failures'); assert.equal(results.summary.passed, 17, 'Passed'); @@ -113,13 +114,13 @@ suite('Unit Tests (PyTest)', () => { test('Run Failed Tests', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - let results = await testManager.runTest(); + let results = await testManager.runTest(CommandSource.ui); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 9, 'Failures'); assert.equal(results.summary.passed, 17, 'Passed'); assert.equal(results.summary.skipped, 3, 'skipped'); - results = await testManager.runTest(undefined, true); + results = await testManager.runTest(CommandSource.ui, undefined, true); assert.equal(results.summary.errors, 0, 'Failed Errors'); assert.equal(results.summary.failures, 9, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); @@ -129,7 +130,7 @@ suite('Unit Tests (PyTest)', () => { test('Run Specific Test File', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - await testManager.discoverTests(true, true); + await testManager.discoverTests(CommandSource.ui, true, true); const testFile: TestFile = { fullPath: path.join(rootDirectory, 'tests', 'test_another_pytest.py'), name: 'tests/test_another_pytest.py', @@ -140,7 +141,7 @@ suite('Unit Tests (PyTest)', () => { time: 0 }; const testFileToRun: TestsToRun = { testFile: [testFile], testFolder: [], testFunction: [], testSuite: [] }; - const results = await testManager.runTest(testFileToRun); + const results = await testManager.runTest(CommandSource.ui, testFileToRun); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 3, 'Passed'); @@ -150,9 +151,9 @@ suite('Unit Tests (PyTest)', () => { test('Run Specific Test Suite', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuites[0].testSuite] }; - const results = await testManager.runTest(testSuite); + const results = await testManager.runTest(CommandSource.ui, testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 1, 'Passed'); @@ -162,9 +163,9 @@ suite('Unit Tests (PyTest)', () => { test('Run Specific Test Function', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; - const results = await testManager.runTest(testFn); + const results = await testManager.runTest(CommandSource.ui, testFn); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 0, 'Passed'); @@ -175,7 +176,7 @@ suite('Unit Tests (PyTest)', () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts index 8478b9b8e14b..ade46afcbfd5 100644 --- a/src/test/unittests/rediscover.test.ts +++ b/src/test/unittests/rediscover.test.ts @@ -1,13 +1,13 @@ import { assert } from 'chai'; import * as fs from 'fs-extra'; import * as path from 'path'; -import { ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; +import { ConfigurationTarget } from 'vscode'; import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; -import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { CommandSource } from '../../client/unittests/common/constants'; import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; import { TestResultsService } from '../../client/unittests/common/testResultsService'; import { TestsHelper } from '../../client/unittests/common/testUtils'; -import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; @@ -30,7 +30,7 @@ const defaultUnitTestArgs = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Unit Tests Discovery', () => { +suite('Unit Tests re-discovery', () => { let testManager: BaseTestManager; let testResultDisplay: TestResultDisplay; let outChannel: MockOutputChannel; @@ -75,13 +75,13 @@ suite('Unit Tests Discovery', () => { } async function discoverUnitTests() { - let tests = await testManager.discoverTests(true, true); + let tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); - tests = await testManager.discoverTests(true, true); + tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); } diff --git a/src/test/unittests/stoppingDiscoverAndTest.test.ts b/src/test/unittests/stoppingDiscoverAndTest.test.ts new file mode 100644 index 000000000000..eba6a358aa8d --- /dev/null +++ b/src/test/unittests/stoppingDiscoverAndTest.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { Product } from '../../client/common/installer'; +import { CANCELLATION_REASON, CommandSource } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { initialize, initializeTest } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; +import { MockTestManagerWithRunningTests } from './mocks'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +// tslint:disable-next-line:variable-name +const EmptyTests = { + summary: { + passed: 0, + failures: 0, + errors: 0, + skipped: 0 + }, + testFiles: [], + testFunctions: [], + testSuites: [], + testFolders: [], + rootTestFolders: [] +}; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Stopping Discovery and Runner', () => { + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + suiteSetup(initialize); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await initializeTest(); + }); + teardown(() => { + outChannel.dispose(); + testResultDisplay.dispose(); + }); + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + } + + test('Running tests should not stop existing discovery', async () => { + createTestManagerDepedencies(); + const mockTestManager = new MockTestManagerWithRunningTests('unittest', Product.unittest, testFilesPath, outChannel, storageService, resultsService, testsHelper); + const discoveryPromise = mockTestManager.discoverTests(CommandSource.auto); + mockTestManager.discoveryDeferred.resolve(EmptyTests); + // tslint:disable-next-line:no-floating-promises + mockTestManager.runTest(CommandSource.ui); + + await expect(discoveryPromise).to.eventually.equal(EmptyTests); + }); + + test('Discovering tests should stop running tests', async () => { + createTestManagerDepedencies(); + const mockTestManager = new MockTestManagerWithRunningTests('unittest', Product.unittest, testFilesPath, outChannel, storageService, resultsService, testsHelper); + mockTestManager.discoveryDeferred.resolve(EmptyTests); + await mockTestManager.discoverTests(CommandSource.auto); + const runPromise = mockTestManager.runTest(CommandSource.ui); + // tslint:disable-next-line:no-string-based-set-timeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // User manually discovering tests will kill the existing test runner. + // tslint:disable-next-line:no-floating-promises + mockTestManager.discoverTests(CommandSource.ui, true, false, true); + await expect(runPromise).to.eventually.be.rejectedWith(CANCELLATION_REASON); + }); +}); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index bcf201bcc3dc..d333ed32fa9c 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; +import { CommandSource } from '../../client/unittests/common/constants'; import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; import { TestResultsService } from '../../client/unittests/common/testResultsService'; import { TestsHelper } from '../../client/unittests/common/testUtils'; @@ -68,7 +69,7 @@ suite('Unit Tests (unittest)', () => { resultsService = new TestResultsService(); testsHelper = new TestsHelper(); testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); @@ -78,7 +79,7 @@ suite('Unit Tests (unittest)', () => { test('Discover Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); @@ -89,7 +90,7 @@ suite('Unit Tests (unittest)', () => { test('Discover Tests (pattern = *_test_*.py)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); @@ -99,7 +100,7 @@ suite('Unit Tests (unittest)', () => { test('Run Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', 'test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); - const results = await testManager.runTest(); + const results = await testManager.runTest(CommandSource.ui); assert.equal(results.summary.errors, 1, 'Errors'); assert.equal(results.summary.failures, 4, 'Failures'); assert.equal(results.summary.passed, 3, 'Passed'); @@ -109,13 +110,13 @@ suite('Unit Tests (unittest)', () => { test('Run Failed Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); - let results = await testManager.runTest(); + let results = await testManager.runTest(CommandSource.ui); assert.equal(results.summary.errors, 1, 'Errors'); assert.equal(results.summary.failures, 4, 'Failures'); assert.equal(results.summary.passed, 3, 'Passed'); assert.equal(results.summary.skipped, 1, 'skipped'); - results = await testManager.runTest(undefined, true); + results = await testManager.runTest(CommandSource.ui, undefined, true); assert.equal(results.summary.errors, 1, 'Failed Errors'); assert.equal(results.summary.failures, 4, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); @@ -125,12 +126,12 @@ suite('Unit Tests (unittest)', () => { test('Run Specific Test File', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); // tslint:disable-next-line:no-non-null-assertion const testFileToTest = tests.testFiles.find(f => f.name === 'test_unittest_one.py')!; const testFile: TestsToRun = { testFile: [testFileToTest], testFolder: [], testFunction: [], testSuite: [] }; - const results = await testManager.runTest(testFile); + const results = await testManager.runTest(CommandSource.ui, testFile); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -141,12 +142,12 @@ suite('Unit Tests (unittest)', () => { test('Run Specific Test Suite', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); // tslint:disable-next-line:no-non-null-assertion const testSuiteToTest = tests.testSuites.find(s => s.testSuite.name === 'Test_test_one_1')!.testSuite; const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToTest] }; - const results = await testManager.runTest(testSuite); + const results = await testManager.runTest(CommandSource.ui, testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -157,9 +158,9 @@ suite('Unit Tests (unittest)', () => { test('Run Specific Test Function', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; - const results = await testManager.runTest(testFn); + const results = await testManager.runTest(CommandSource.ui, testFn); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 0, 'Passed'); @@ -170,7 +171,7 @@ suite('Unit Tests (unittest)', () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); - const tests = await testManager.discoverTests(true, true); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); diff --git a/tsconfig.json b/tsconfig.json index aacfa96ef3de..c6bac46ad4fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,11 @@ "es6" ], "sourceMap": true, - "rootDir": "src" + "rootDir": "src", + "experimentalDecorators": true // TODO: enable to ensure all code complies with strict coding standards + // , "noUnusedLocals": true + // , "noUnusedParameters": false // , "strict": true }, "exclude": [ diff --git a/tslint.json b/tslint.json index f55dca7c2b4c..7ff727e68b34 100644 --- a/tslint.json +++ b/tslint.json @@ -39,7 +39,8 @@ ], "await-promise": [ true, - "Thenable" + "Thenable", + "PromiseLike" ], "completed-docs": false }