From 83169f45761669fb9ae78e54842e3752d5ebc892 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 17 Dec 2018 17:53:16 -0800 Subject: [PATCH 1/9] Rename folder --- build/existingFiles.json | 8 ++++---- .../{configProviders => configuration}/baseProvider.ts | 0 .../configurationProviderUtils.ts | 0 .../pythonV2Provider.ts | 0 .../extension/{configProviders => configuration}/types.ts | 0 src/client/debugger/extension/serviceRegistry.ts | 6 +++--- src/test/debugger/attach.ptvsd.test.ts | 2 +- .../extension/configProvider/provider.attach.unit.test.ts | 2 +- .../extension/configProvider/provider.unit.test.ts | 6 +++--- 9 files changed, 12 insertions(+), 12 deletions(-) rename src/client/debugger/extension/{configProviders => configuration}/baseProvider.ts (100%) rename src/client/debugger/extension/{configProviders => configuration}/configurationProviderUtils.ts (100%) rename src/client/debugger/extension/{configProviders => configuration}/pythonV2Provider.ts (100%) rename src/client/debugger/extension/{configProviders => configuration}/types.ts (100%) diff --git a/build/existingFiles.json b/build/existingFiles.json index adabddd7b23f..aa915e79acc5 100644 --- a/build/existingFiles.json +++ b/build/existingFiles.json @@ -157,10 +157,10 @@ "src/client/debugger/debugAdapter/serviceRegistry.ts", "src/client/debugger/debugAdapter/types.ts", "src/client/debugger/extension/banner.ts", - "src/client/debugger/extension/configProviders/baseProvider.ts", - "src/client/debugger/extension/configProviders/configurationProviderUtils.ts", - "src/client/debugger/extension/configProviders/pythonV2Provider.ts", - "src/client/debugger/extension/configProviders/types.ts", + "src/client/debugger/extension/configuration/baseProvider.ts", + "src/client/debugger/extension/configuration/configurationProviderUtils.ts", + "src/client/debugger/extension/configuration/pythonV2Provider.ts", + "src/client/debugger/extension/configuration/types.ts", "src/client/debugger/extension/hooks/childProcessAttachHandler.ts", "src/client/debugger/extension/hooks/childProcessAttachService.ts", "src/client/debugger/extension/hooks/constants.ts", diff --git a/src/client/debugger/extension/configProviders/baseProvider.ts b/src/client/debugger/extension/configuration/baseProvider.ts similarity index 100% rename from src/client/debugger/extension/configProviders/baseProvider.ts rename to src/client/debugger/extension/configuration/baseProvider.ts diff --git a/src/client/debugger/extension/configProviders/configurationProviderUtils.ts b/src/client/debugger/extension/configuration/configurationProviderUtils.ts similarity index 100% rename from src/client/debugger/extension/configProviders/configurationProviderUtils.ts rename to src/client/debugger/extension/configuration/configurationProviderUtils.ts diff --git a/src/client/debugger/extension/configProviders/pythonV2Provider.ts b/src/client/debugger/extension/configuration/pythonV2Provider.ts similarity index 100% rename from src/client/debugger/extension/configProviders/pythonV2Provider.ts rename to src/client/debugger/extension/configuration/pythonV2Provider.ts diff --git a/src/client/debugger/extension/configProviders/types.ts b/src/client/debugger/extension/configuration/types.ts similarity index 100% rename from src/client/debugger/extension/configProviders/types.ts rename to src/client/debugger/extension/configuration/types.ts diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 7d669c53ff70..b8fa26629cdb 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -6,9 +6,9 @@ import { DebugConfigurationProvider } from 'vscode'; import { IServiceManager } from '../../ioc/types'; import { DebuggerBanner } from './banner'; -import { ConfigurationProviderUtils } from './configProviders/configurationProviderUtils'; -import { PythonV2DebugConfigurationProvider } from './configProviders/pythonV2Provider'; -import { IConfigurationProviderUtils } from './configProviders/types'; +import { ConfigurationProviderUtils } from './configuration/configurationProviderUtils'; +import { PythonV2DebugConfigurationProvider } from './configuration/pythonV2Provider'; +import { IConfigurationProviderUtils } from './configuration/types'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index 65b2d490039b..9dbb012e7975 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -13,7 +13,7 @@ import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; import { IS_WINDOWS } from '../../client/common/platform/constants'; import { IPlatformService } from '../../client/common/platform/types'; import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; -import { PythonV2DebugConfigurationProvider } from '../../client/debugger/extension/configProviders/pythonV2Provider'; +import { PythonV2DebugConfigurationProvider } from '../../client/debugger/extension/configuration/pythonV2Provider'; import { AttachRequestArguments, DebugOptions } from '../../client/debugger/types'; import { IServiceContainer } from '../../client/ioc/types'; import { PYTHON_PATH, sleep } from '../common'; diff --git a/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts b/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts index bde71cbcba07..d8b3f3f9cb6b 100644 --- a/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts +++ b/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts @@ -14,7 +14,7 @@ import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { DebuggerTypeName } from '../../../../client/debugger/constants'; -import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configProviders/pythonV2Provider'; +import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configuration/pythonV2Provider'; import { AttachRequestArguments, DebugOptions } from '../../../../client/debugger/types'; import { IServiceContainer } from '../../../../client/ioc/types'; diff --git a/src/test/debugger/extension/configProvider/provider.unit.test.ts b/src/test/debugger/extension/configProvider/provider.unit.test.ts index 407c40f6ce68..067f37d5b285 100644 --- a/src/test/debugger/extension/configProvider/provider.unit.test.ts +++ b/src/test/debugger/extension/configProvider/provider.unit.test.ts @@ -17,9 +17,9 @@ import { IFileSystem, IPlatformService } from '../../../../client/common/platfor import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; import { IConfigurationService, ILogger, IPythonSettings } from '../../../../client/common/types'; import { DebuggerTypeName } from '../../../../client/debugger/constants'; -import { ConfigurationProviderUtils } from '../../../../client/debugger/extension/configProviders/configurationProviderUtils'; -import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configProviders/pythonV2Provider'; -import { IConfigurationProviderUtils } from '../../../../client/debugger/extension/configProviders/types'; +import { ConfigurationProviderUtils } from '../../../../client/debugger/extension/configuration/configurationProviderUtils'; +import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configuration/pythonV2Provider'; +import { IConfigurationProviderUtils } from '../../../../client/debugger/extension/configuration/types'; import { DebugOptions, LaunchRequestArguments } from '../../../../client/debugger/types'; import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; From c4206287154a7c92739042aabc04904239387542 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 18 Dec 2018 10:57:54 -0800 Subject: [PATCH 2/9] Refactor debug configuration provider --- .vscode/tasks.json | 2 +- package.json | 6 +- .../extension/configuration/baseProvider.ts | 133 --------- .../configurationProviderUtils.ts | 19 +- .../debugConfigurationProvider.ts | 23 ++ .../configuration/pythonV2Provider.ts | 154 ----------- .../configuration/resolvers/attach.ts | 98 +++++++ .../extension/configuration/resolvers/base.ts | 92 +++++++ .../configuration/resolvers/launch.ts | 128 +++++++++ .../debugger/extension/configuration/types.ts | 7 +- .../debugger/extension/serviceRegistry.ts | 11 +- src/test/debugger/attach.ptvsd.test.ts | 17 +- .../provider.attach.unit.test.ts | 254 ------------------ .../debugConfigurationProvider.unit.test.ts | 68 +++++ .../resolvers/attach.unit.test.ts | 253 +++++++++++++++++ .../configuration/resolvers/base.unit.test.ts | 204 ++++++++++++++ .../resolvers/launch.unit.test.ts} | 53 ++-- .../extension/serviceRegistry.unit.test.ts | 52 ++++ 18 files changed, 985 insertions(+), 589 deletions(-) delete mode 100644 src/client/debugger/extension/configuration/baseProvider.ts create mode 100644 src/client/debugger/extension/configuration/debugConfigurationProvider.ts delete mode 100644 src/client/debugger/extension/configuration/pythonV2Provider.ts create mode 100644 src/client/debugger/extension/configuration/resolvers/attach.ts create mode 100644 src/client/debugger/extension/configuration/resolvers/base.ts create mode 100644 src/client/debugger/extension/configuration/resolvers/launch.ts delete mode 100644 src/test/debugger/extension/configProvider/provider.attach.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/resolvers/base.unit.test.ts rename src/test/debugger/extension/{configProvider/provider.unit.test.ts => configuration/resolvers/launch.unit.test.ts} (93%) create mode 100644 src/test/debugger/extension/serviceRegistry.unit.test.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 11720634a566..58c524b0df0f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -38,7 +38,7 @@ { "label": "Run Unit Tests", "type": "npm", - "script": "test:unittests:cover", + "script": "test:unittests", "group": { "kind": "test", "isDefault": true diff --git a/package.json b/package.json index 120e30d5504e..fd73169b41a4 100644 --- a/package.json +++ b/package.json @@ -1860,10 +1860,10 @@ "compile-webviews-verbose": "npx webpack --config webpack.datascience-ui.config.js", "postinstall": "node ./node_modules/vscode/bin/install", "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", - "test:unittests": "mocha --opts ./build/.mocha.unittests.opts", + "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts", "test:unittests:cover": "nyc --nycrc-path ./build/.nycrc npm run test:unittests", - "test:functional": "mocha --opts ./build/.mocha.functional.opts", - "test:functional:cover": "nyc--nycrc-path ./build/.nycrc npm run test:functional", + "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", + "test:functional:cover": "nyc --nycrc-path ./build/.nycrc npm run test:functional", "testDebugger": "node ./out/test/debuggerTest.js", "testSingleWorkspace": "node ./out/test/standardTest.js", "testMultiWorkspace": "node ./out/test/multiRootTest.js", diff --git a/src/client/debugger/extension/configuration/baseProvider.ts b/src/client/debugger/extension/configuration/baseProvider.ts deleted file mode 100644 index b2f65c416026..000000000000 --- a/src/client/debugger/extension/configuration/baseProvider.ts +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-invalid-template-strings - -import { injectable, unmanaged } from 'inversify'; -import * as path from 'path'; -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, Uri, WorkspaceFolder } from 'vscode'; -import { InvalidPythonPathInDebuggerServiceId } from '../../../application/diagnostics/checks/invalidPythonPathInDebugger'; -import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../application/diagnostics/types'; -import { IDocumentManager, IWorkspaceService } from '../../../common/application/types'; -import { PYTHON_LANGUAGE } from '../../../common/constants'; -import { IConfigurationService } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { DebuggerTypeName } from '../../constants'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; - -@injectable() -export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { - constructor(@unmanaged() public debugType: typeof DebuggerTypeName, protected serviceContainer: IServiceContainer) { } - public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { - const workspaceFolder = this.getWorkspaceFolder(folder); - - if (debugConfiguration.request === 'attach') { - await this.provideAttachDefaults(workspaceFolder, debugConfiguration as AttachRequestArguments); - } else { - const config = debugConfiguration as LaunchRequestArguments; - const numberOfSettings = Object.keys(config); - - if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(); - - config.name = 'Launch'; - config.type = this.debugType; - config.request = 'launch'; - config.program = defaultProgram ? defaultProgram : ''; - config.env = {}; - } - - await this.provideLaunchDefaults(workspaceFolder, config); - const isValid = await this.validateLaunchConfiguration(folder, config); - if (!isValid) { - return; - } - } - - const dbgConfig = (debugConfiguration as (LaunchRequestArguments | AttachRequestArguments)); - if (Array.isArray(dbgConfig.debugOptions)) { - dbgConfig.debugOptions = dbgConfig.debugOptions!.filter((item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos); - } - return debugConfiguration; - } - protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: AttachRequestArguments): Promise { - if (!Array.isArray(debugConfiguration.debugOptions)) { - debugConfiguration.debugOptions = []; - } - if (!debugConfiguration.host) { - debugConfiguration.host = 'localhost'; - } - // Pass workspace folder so we can get this when we get debug events firing. - debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; - } - protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): Promise { - this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); - if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { - debugConfiguration.cwd = workspaceFolder.fsPath; - } - if (typeof debugConfiguration.envFile !== 'string' && workspaceFolder) { - const envFile = path.join(workspaceFolder.fsPath, '.env'); - debugConfiguration.envFile = envFile; - } - if (typeof debugConfiguration.stopOnEntry !== 'boolean') { - debugConfiguration.stopOnEntry = false; - } - if (typeof debugConfiguration.showReturnValue !== 'boolean') { - debugConfiguration.showReturnValue = false; - } - if (!debugConfiguration.console) { - debugConfiguration.console = 'integratedTerminal'; - } - // If using a terminal, then never open internal console. - if (debugConfiguration.console !== 'none' && !debugConfiguration.internalConsoleOptions) { - debugConfiguration.internalConsoleOptions = 'neverOpen'; - } - if (!Array.isArray(debugConfiguration.debugOptions)) { - debugConfiguration.debugOptions = []; - } - // Pass workspace folder so we can get this when we get debug events firing. - debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; - } - protected async validateLaunchConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments): Promise { - const diagnosticService = this.serviceContainer.get(IDiagnosticsService, InvalidPythonPathInDebuggerServiceId); - return diagnosticService.validatePythonPath(debugConfiguration.pythonPath, folder ? folder.uri : undefined); - } - private getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { - if (folder) { - return folder.uri; - } - const program = this.getProgram(); - const workspaceService = this.serviceContainer.get(IWorkspaceService); - if (!Array.isArray(workspaceService.workspaceFolders) || workspaceService.workspaceFolders.length === 0) { - return program ? Uri.file(path.dirname(program)) : undefined; - } - if (workspaceService.workspaceFolders.length === 1) { - return workspaceService.workspaceFolders[0].uri; - } - if (program) { - const workspaceFolder = workspaceService.getWorkspaceFolder(Uri.file(program)); - if (workspaceFolder) { - return workspaceFolder.uri; - } - } - } - private getProgram(): string | undefined { - const documentManager = this.serviceContainer.get(IDocumentManager); - const editor = documentManager.activeTextEditor; - if (editor && editor.document.languageId === PYTHON_LANGUAGE) { - return editor.document.fileName; - } - } - private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): void { - if (!debugConfiguration) { - return; - } - if (debugConfiguration.pythonPath === '${config:python.pythonPath}' || !debugConfiguration.pythonPath) { - const configService = this.serviceContainer.get(IConfigurationService); - const pythonPath = configService.getSettings(workspaceFolder).pythonPath; - debugConfiguration.pythonPath = pythonPath; - } - } -} diff --git a/src/client/debugger/extension/configuration/configurationProviderUtils.ts b/src/client/debugger/extension/configuration/configurationProviderUtils.ts index b13ca708b090..8a8c56103e3d 100644 --- a/src/client/debugger/extension/configuration/configurationProviderUtils.ts +++ b/src/client/debugger/extension/configuration/configurationProviderUtils.ts @@ -7,23 +7,19 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; import { IApplicationShell } from '../../../common/application/types'; +import { traceError } from '../../../common/logger'; import { IFileSystem } from '../../../common/platform/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; -import { ILogger } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; +import { noop } from '../../../common/utils/misc'; import { IConfigurationProviderUtils } from './types'; const PSERVE_SCRIPT_FILE_NAME = 'pserve.py'; @injectable() export class ConfigurationProviderUtils implements IConfigurationProviderUtils { - private readonly executionFactory: IPythonExecutionFactory; - private readonly fs: IFileSystem; - private readonly logger: ILogger; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.executionFactory = this.serviceContainer.get(IPythonExecutionFactory); - this.fs = this.serviceContainer.get(IFileSystem); - this.logger = this.serviceContainer.get(ILogger); + constructor(@inject(IPythonExecutionFactory) private readonly executionFactory: IPythonExecutionFactory, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IApplicationShell) private readonly shell: IApplicationShell) { } public async getPyramidStartupScriptFilePath(resource?: Uri): Promise { try { @@ -33,9 +29,8 @@ export class ConfigurationProviderUtils implements IConfigurationProviderUtils { return await this.fs.fileExists(pserveFilePath) ? pserveFilePath : undefined; } catch (ex) { const message = 'Unable to locate \'pserve.py\' required for debugging of Pyramid applications.'; - this.logger.logError(message, ex); - const app = this.serviceContainer.get(IApplicationShell); - app.showErrorMessage(message); + traceError(message, ex); + this.shell.showErrorMessage(message).then(noop, noop); return; } } diff --git a/src/client/debugger/extension/configuration/debugConfigurationProvider.ts b/src/client/debugger/extension/configuration/debugConfigurationProvider.ts new file mode 100644 index 000000000000..aa0230e2d605 --- /dev/null +++ b/src/client/debugger/extension/configuration/debugConfigurationProvider.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, WorkspaceFolder } from 'vscode'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugConfigurationResolver } from './types'; + +@injectable() +export class PythonDebugConfigurationProvider implements DebugConfigurationProvider { + constructor(@inject(IDebugConfigurationResolver) @named('attach') private readonly attachResolver: IDebugConfigurationResolver, + @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver) { + } + public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { + if (debugConfiguration.request === 'attach') { + return this.attachResolver.resolveDebugConfiguration(folder, debugConfiguration as AttachRequestArguments, token); + } else { + return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); + } } + +} diff --git a/src/client/debugger/extension/configuration/pythonV2Provider.ts b/src/client/debugger/extension/configuration/pythonV2Provider.ts deleted file mode 100644 index 79db5b12ece2..000000000000 --- a/src/client/debugger/extension/configuration/pythonV2Provider.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IPlatformService } from '../../../common/platform/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { DEBUGGER } from '../../../telemetry/constants'; -import { DebuggerTelemetry } from '../../../telemetry/types'; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../types'; -import { BaseConfigurationProvider } from './baseProvider'; -import { IConfigurationProviderUtils } from './types'; - -@injectable() -export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('python', serviceContainer); - } - protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): Promise { - await super.provideLaunchDefaults(workspaceFolder, debugConfiguration); - const debugOptions = debugConfiguration.debugOptions!; - if (debugConfiguration.debugStdLib) { - this.debugOption(debugOptions, DebugOptions.DebugStdLib); - } - if (debugConfiguration.stopOnEntry) { - this.debugOption(debugOptions, DebugOptions.StopOnEntry); - } - if (debugConfiguration.showReturnValue) { - this.debugOption(debugOptions, DebugOptions.ShowReturnValue); - } - if (debugConfiguration.django) { - this.debugOption(debugOptions, DebugOptions.Django); - } - if (debugConfiguration.jinja) { - this.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { - this.debugOption(debugOptions, DebugOptions.RedirectOutput); - } - if (debugConfiguration.sudo) { - this.debugOption(debugOptions, DebugOptions.Sudo); - } - if (debugConfiguration.subProcess === true) { - this.debugOption(debugOptions, DebugOptions.SubProcess); - } - if (this.serviceContainer.get(IPlatformService).isWindows) { - this.debugOption(debugOptions, DebugOptions.FixFilePathCase); - } - const isFlask = this.isDebuggingFlask(debugConfiguration); - if ((debugConfiguration.pyramid || isFlask) - && debugOptions.indexOf(DebugOptions.Jinja) === -1 - && debugConfiguration.jinja !== false) { - this.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.pyramid) { - const utils = this.serviceContainer.get(IConfigurationProviderUtils); - debugConfiguration.program = (await utils.getPyramidStartupScriptFilePath(workspaceFolder))!; - } - this.sendTelemetry('launch', debugConfiguration); - } - // tslint:disable-next-line:cyclomatic-complexity - protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: AttachRequestArguments): Promise { - await super.provideAttachDefaults(workspaceFolder, debugConfiguration); - const debugOptions = debugConfiguration.debugOptions!; - if (debugConfiguration.debugStdLib) { - this.debugOption(debugOptions, DebugOptions.DebugStdLib); - } - if (debugConfiguration.django) { - this.debugOption(debugOptions, DebugOptions.Django); - } - if (debugConfiguration.jinja) { - this.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.subProcess === true) { - this.debugOption(debugOptions, DebugOptions.SubProcess); - } - if (debugConfiguration.pyramid - && debugOptions.indexOf(DebugOptions.Jinja) === -1 - && debugConfiguration.jinja !== false) { - this.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { - this.debugOption(debugOptions, DebugOptions.RedirectOutput); - } - - // We'll need paths to be fixed only in the case where local and remote hosts are the same - // I.e. only if hostName === 'localhost' or '127.0.0.1' or '' - const isLocalHost = this.isLocalHost(debugConfiguration.host); - if (this.serviceContainer.get(IPlatformService).isWindows && isLocalHost) { - this.debugOption(debugOptions, DebugOptions.FixFilePathCase); - } - if (this.serviceContainer.get(IPlatformService).isWindows) { - this.debugOption(debugOptions, DebugOptions.WindowsClient); - } else { - this.debugOption(debugOptions, DebugOptions.UnixClient); - } - - if (!debugConfiguration.pathMappings) { - debugConfiguration.pathMappings = []; - } - // This is for backwards compatibility. - if (debugConfiguration.localRoot && debugConfiguration.remoteRoot) { - debugConfiguration.pathMappings!.push({ - localRoot: debugConfiguration.localRoot, - remoteRoot: debugConfiguration.remoteRoot - }); - } - // If attaching to local host, then always map local root and remote roots. - if (workspaceFolder && debugConfiguration.host && - debugConfiguration.pathMappings!.length === 0 && - ['LOCALHOST', '127.0.0.1', '::1'].indexOf(debugConfiguration.host.toUpperCase()) >= 0) { - debugConfiguration.pathMappings!.push({ - localRoot: workspaceFolder.fsPath, - remoteRoot: workspaceFolder.fsPath - }); - } - this.sendTelemetry('attach', debugConfiguration); - } - private debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { - if (debugOptions.indexOf(debugOption) >= 0) { - return; - } - debugOptions.push(debugOption); - } - private isLocalHost(hostName?: string) { - const LocalHosts = ['localhost', '127.0.0.1', '::1']; - return (hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0) ? true : false; - } - private isDebuggingFlask(debugConfiguration: Partial) { - return (debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK') ? true : false; - } - private sendTelemetry(trigger: 'launch' | 'attach', debugConfiguration: Partial) { - const telemetryProps: DebuggerTelemetry = { - trigger, - console: debugConfiguration.console, - hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, - django: !!debugConfiguration.django, - flask: this.isDebuggingFlask(debugConfiguration), - hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, - isLocalhost: this.isLocalHost(debugConfiguration.host), - isModule: typeof debugConfiguration.module === 'string' && debugConfiguration.module.length > 0, - isSudo: !!debugConfiguration.sudo, - jinja: !!debugConfiguration.jinja, - pyramid: !!debugConfiguration.pyramid, - stopOnEntry: !!debugConfiguration.stopOnEntry, - showReturnValue: !!debugConfiguration.showReturnValue, - subProcess: !!debugConfiguration.subProcess - }; - sendTelemetryEvent(DEBUGGER, undefined, telemetryProps); - } -} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts new file mode 100644 index 000000000000..70e732ec27d8 --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IConfigurationService } from '../../../../common/types'; +import { AttachRequestArguments, DebugOptions } from '../../../types'; +import { BaseConfigurationResolver } from './base'; + +@injectable() +export class AttachConfigurationResolver extends BaseConfigurationResolver { + constructor(@inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IConfigurationService) configurationService: IConfigurationService) { + super(workspaceService, documentManager, configurationService); + } + public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: AttachRequestArguments, token?: CancellationToken): Promise { + const workspaceFolder = this.getWorkspaceFolder(folder); + + await this.provideAttachDefaults(workspaceFolder, debugConfiguration as AttachRequestArguments); + + const dbgConfig = debugConfiguration; + if (Array.isArray(dbgConfig.debugOptions)) { + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter((item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos); + } + return debugConfiguration; + } + // tslint:disable-next-line:cyclomatic-complexity + protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: AttachRequestArguments): Promise { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + if (!debugConfiguration.host) { + debugConfiguration.host = 'localhost'; + } + // Pass workspace folder so we can get this when we get debug events firing. + debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; + const debugOptions = debugConfiguration.debugOptions!; + if (debugConfiguration.debugStdLib) { + this.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.django) { + this.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.subProcess === true) { + this.debugOption(debugOptions, DebugOptions.SubProcess); + } + if (debugConfiguration.pyramid + && debugOptions.indexOf(DebugOptions.Jinja) === -1 + && debugConfiguration.jinja !== false) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + this.debugOption(debugOptions, DebugOptions.RedirectOutput); + } + + // We'll need paths to be fixed only in the case where local and remote hosts are the same + // I.e. only if hostName === 'localhost' or '127.0.0.1' or '' + const isLocalHost = this.isLocalHost(debugConfiguration.host); + if (this.platformService.isWindows && isLocalHost) { + this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + if (this.platformService.isWindows) { + this.debugOption(debugOptions, DebugOptions.WindowsClient); + } else { + this.debugOption(debugOptions, DebugOptions.UnixClient); + } + + if (!debugConfiguration.pathMappings) { + debugConfiguration.pathMappings = []; + } + // This is for backwards compatibility. + if (debugConfiguration.localRoot && debugConfiguration.remoteRoot) { + debugConfiguration.pathMappings!.push({ + localRoot: debugConfiguration.localRoot, + remoteRoot: debugConfiguration.remoteRoot + }); + } + // If attaching to local host, then always map local root and remote roots. + if (workspaceFolder && debugConfiguration.host && + debugConfiguration.pathMappings!.length === 0 && + ['LOCALHOST', '127.0.0.1', '::1'].indexOf(debugConfiguration.host.toUpperCase()) >= 0) { + debugConfiguration.pathMappings!.push({ + localRoot: workspaceFolder.fsPath, + remoteRoot: workspaceFolder.fsPath + }); + } + this.sendTelemetry('attach', debugConfiguration); + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts new file mode 100644 index 000000000000..d8e9ad63550f --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-template-strings + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../common/constants'; +import { IConfigurationService } from '../../../../common/types'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { DEBUGGER } from '../../../../telemetry/constants'; +import { DebuggerTelemetry } from '../../../../telemetry/types'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../types'; +import { IDebugConfigurationResolver } from '../types'; + +@injectable() +export abstract class BaseConfigurationResolver implements IDebugConfigurationResolver { + constructor(protected readonly workspaceService: IWorkspaceService, + protected readonly documentManager: IDocumentManager, + protected readonly configurationService: IConfigurationService) { } + public abstract resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise; + protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + if (folder) { + return folder.uri; + } + const program = this.getProgram(); + if (!Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0) { + return program ? Uri.file(path.dirname(program)) : undefined; + } + if (this.workspaceService.workspaceFolders.length === 1) { + return this.workspaceService.workspaceFolders[0].uri; + } + if (program) { + const workspaceFolder = this.workspaceService.getWorkspaceFolder(Uri.file(program)); + if (workspaceFolder) { + return workspaceFolder.uri; + } + } + } + protected getProgram(): string | undefined { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.document.languageId === PYTHON_LANGUAGE) { + return editor.document.fileName; + } + } + protected resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): void { + if (!debugConfiguration) { + return; + } + if (debugConfiguration.pythonPath === '${config:python.pythonPath}' || !debugConfiguration.pythonPath) { + const pythonPath = this.configurationService.getSettings(workspaceFolder).pythonPath; + debugConfiguration.pythonPath = pythonPath; + } + } + protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + if (debugOptions.indexOf(debugOption) >= 0) { + return; + } + debugOptions.push(debugOption); + } + protected isLocalHost(hostName?: string) { + const LocalHosts = ['localhost', '127.0.0.1', '::1']; + return (hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0) ? true : false; + } + protected isDebuggingFlask(debugConfiguration: Partial) { + return (debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK') ? true : false; + } + protected sendTelemetry(trigger: 'launch' | 'attach', debugConfiguration: Partial) { + const telemetryProps: DebuggerTelemetry = { + trigger, + console: debugConfiguration.console, + hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, + django: !!debugConfiguration.django, + flask: this.isDebuggingFlask(debugConfiguration), + hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, + isLocalhost: this.isLocalHost(debugConfiguration.host), + isModule: typeof debugConfiguration.module === 'string' && debugConfiguration.module.length > 0, + isSudo: !!debugConfiguration.sudo, + jinja: !!debugConfiguration.jinja, + pyramid: !!debugConfiguration.pyramid, + stopOnEntry: !!debugConfiguration.stopOnEntry, + showReturnValue: !!debugConfiguration.showReturnValue, + subProcess: !!debugConfiguration.subProcess + }; + sendTelemetryEvent(DEBUGGER, undefined, telemetryProps); + } + +} diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts new file mode 100644 index 000000000000..4df970380e27 --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../application/diagnostics/types'; +import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IConfigurationService } from '../../../../common/types'; +import { DebuggerTypeName } from '../../../constants'; +import { DebugOptions, LaunchRequestArguments } from '../../../types'; +import { IConfigurationProviderUtils } from '../types'; +import { BaseConfigurationResolver } from './base'; + +@injectable() +export class LaunchConfigurationResolver extends BaseConfigurationResolver { + constructor(@inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(IConfigurationProviderUtils) private readonly configurationProviderUtils: IConfigurationProviderUtils, + @inject(IDiagnosticsService) @named(IInvalidPythonPathInDebuggerService) private readonly invalidPythonPathInDebuggerService: IInvalidPythonPathInDebuggerService, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IConfigurationService) configurationService: IConfigurationService) { + super(workspaceService, documentManager, configurationService); + } + public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments, token?: CancellationToken): Promise { + const workspaceFolder = this.getWorkspaceFolder(folder); + + const config = debugConfiguration as LaunchRequestArguments; + const numberOfSettings = Object.keys(config); + + if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { + const defaultProgram = this.getProgram(); + + config.name = 'Launch'; + config.type = DebuggerTypeName; + config.request = 'launch'; + config.program = defaultProgram ? defaultProgram : ''; + config.env = {}; + } + + await this.provideLaunchDefaults(workspaceFolder, config); + const isValid = await this.validateLaunchConfiguration(folder, config); + if (!isValid) { + return; + } + + const dbgConfig = debugConfiguration; + if (Array.isArray(dbgConfig.debugOptions)) { + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter((item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos); + } + return debugConfiguration; + } + // tslint:disable-next-line:cyclomatic-complexity + protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): Promise { + this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { + debugConfiguration.cwd = workspaceFolder.fsPath; + } + if (typeof debugConfiguration.envFile !== 'string' && workspaceFolder) { + const envFile = path.join(workspaceFolder.fsPath, '.env'); + debugConfiguration.envFile = envFile; + } + if (typeof debugConfiguration.stopOnEntry !== 'boolean') { + debugConfiguration.stopOnEntry = false; + } + if (typeof debugConfiguration.showReturnValue !== 'boolean') { + debugConfiguration.showReturnValue = false; + } + if (!debugConfiguration.console) { + debugConfiguration.console = 'integratedTerminal'; + } + // If using a terminal, then never open internal console. + if (debugConfiguration.console !== 'none' && !debugConfiguration.internalConsoleOptions) { + debugConfiguration.internalConsoleOptions = 'neverOpen'; + } + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + // Pass workspace folder so we can get this when we get debug events firing. + debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; + const debugOptions = debugConfiguration.debugOptions!; + if (debugConfiguration.debugStdLib) { + this.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.stopOnEntry) { + this.debugOption(debugOptions, DebugOptions.StopOnEntry); + } + if (debugConfiguration.showReturnValue) { + this.debugOption(debugOptions, DebugOptions.ShowReturnValue); + } + if (debugConfiguration.django) { + this.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + this.debugOption(debugOptions, DebugOptions.RedirectOutput); + } + if (debugConfiguration.sudo) { + this.debugOption(debugOptions, DebugOptions.Sudo); + } + if (debugConfiguration.subProcess === true) { + this.debugOption(debugOptions, DebugOptions.SubProcess); + } + if (this.platformService.isWindows) { + this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + const isFlask = this.isDebuggingFlask(debugConfiguration); + if ((debugConfiguration.pyramid || isFlask) + && debugOptions.indexOf(DebugOptions.Jinja) === -1 + && debugConfiguration.jinja !== false) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.pyramid) { + debugConfiguration.program = (await this.configurationProviderUtils.getPyramidStartupScriptFilePath(workspaceFolder))!; + } + this.sendTelemetry('launch', debugConfiguration); + } + + protected async validateLaunchConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments): Promise { + const diagnosticService = this.invalidPythonPathInDebuggerService; + return diagnosticService.validatePythonPath(debugConfiguration.pythonPath, folder ? folder.uri : undefined); + } +} diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts index 8ece886ab352..db8ba18046c4 100644 --- a/src/client/debugger/extension/configuration/types.ts +++ b/src/client/debugger/extension/configuration/types.ts @@ -3,10 +3,15 @@ 'use strict'; -import { Uri } from 'vscode'; +import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; export const IConfigurationProviderUtils = Symbol('IConfigurationProviderUtils'); export interface IConfigurationProviderUtils { getPyramidStartupScriptFilePath(resource?: Uri): Promise; } + +export const IDebugConfigurationResolver = Symbol('IDebugConfigurationResolver'); +export interface IDebugConfigurationResolver { + resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: T, token?: CancellationToken): Promise; +} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index b8fa26629cdb..548fb1a070be 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -5,19 +5,24 @@ import { DebugConfigurationProvider } from 'vscode'; import { IServiceManager } from '../../ioc/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../types'; import { DebuggerBanner } from './banner'; import { ConfigurationProviderUtils } from './configuration/configurationProviderUtils'; -import { PythonV2DebugConfigurationProvider } from './configuration/pythonV2Provider'; -import { IConfigurationProviderUtils } from './configuration/types'; +import { PythonDebugConfigurationProvider } from './configuration/debugConfigurationProvider'; +import { AttachConfigurationResolver } from './configuration/resolvers/attach'; +import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; +import { IConfigurationProviderUtils, IDebugConfigurationResolver } from './configuration/types'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; import { IDebugConfigurationProvider, IDebuggerBanner } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IDebugConfigurationProvider, PythonV2DebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationProvider, PythonDebugConfigurationProvider); serviceManager.addSingleton(IConfigurationProviderUtils, ConfigurationProviderUtils); serviceManager.addSingleton(IDebuggerBanner, DebuggerBanner); serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); + serviceManager.addSingleton>(IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'); + serviceManager.addSingleton>(IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'); } diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index 9dbb012e7975..372bc39595c9 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -9,12 +9,16 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { DebugConfiguration, Uri } from 'vscode'; import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; import { IS_WINDOWS } from '../../client/common/platform/constants'; import { IPlatformService } from '../../client/common/platform/types'; +import { IConfigurationService } from '../../client/common/types'; import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; -import { PythonV2DebugConfigurationProvider } from '../../client/debugger/extension/configuration/pythonV2Provider'; -import { AttachRequestArguments, DebugOptions } from '../../client/debugger/types'; +import { PythonDebugConfigurationProvider } from '../../client/debugger/extension/configuration/debugConfigurationProvider'; +import { AttachConfigurationResolver } from '../../client/debugger/extension/configuration/resolvers/attach'; +import { IDebugConfigurationResolver } from '../../client/debugger/extension/configuration/types'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; import { IServiceContainer } from '../../client/ioc/types'; import { PYTHON_PATH, sleep } from '../common'; import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; @@ -85,7 +89,14 @@ suite('Attach Debugger', () => { platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows); const serviceContainer = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(IPlatformService, TypeMoq.It.isAny())).returns(() => platformService.object); - const configProvider = new PythonV2DebugConfigurationProvider(serviceContainer.object); + + const workspaceService = TypeMoq.Mock.ofType(); + const documentManager = TypeMoq.Mock.ofType(); + const configurationService = TypeMoq.Mock.ofType(); + + const launchResolver = TypeMoq.Mock.ofType>(); + const attachResolver = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); + const configProvider = new PythonDebugConfigurationProvider(attachResolver, launchResolver.object); await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options); const attachPromise = debugClient.attachRequest(options); diff --git a/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts b/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts deleted file mode 100644 index d8b3f3f9cb6b..000000000000 --- a/src/test/debugger/extension/configProvider/provider.attach.unit.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion no-invalid-this - -import { expect } from 'chai'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; -import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; -import { getNamesAndValues } from '../../../../client/common/utils/enum'; -import { DebuggerTypeName } from '../../../../client/debugger/constants'; -import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configuration/pythonV2Provider'; -import { AttachRequestArguments, DebugOptions } from '../../../../client/debugger/types'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -enum OS { - Windows, - Mac, - Linux -} -[ - { debugType: DebuggerTypeName, class: PythonV2DebugConfigurationProvider } -].forEach(provider => { - getNamesAndValues(OS).forEach(os => { - suite(`Debugging - Config Provider attach, ${provider.debugType}, OS = ${os.name}`, () => { - let serviceContainer: TypeMoq.IMock; - let debugProvider: DebugConfigurationProvider; - let platformService: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - const debugOptionsAvailable = [DebugOptions.RedirectOutput]; - if (os.value === OS.Windows) { - debugOptionsAvailable.push(DebugOptions.FixFilePathCase); - debugOptionsAvailable.push(DebugOptions.WindowsClient); - } else { - debugOptionsAvailable.push(DebugOptions.UnixClient); - } - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - platformService.setup(p => p.isWindows).returns(() => os.value === OS.Windows); - platformService.setup(p => p.isMac).returns(() => os.value === OS.Mac); - platformService.setup(p => p.isLinux).returns(() => os.value === OS.Linux); - debugProvider = new provider.class(serviceContainer.object); - }); - function createMoqWorkspaceFolder(folderPath: string) { - const folder = TypeMoq.Mock.ofType(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); - return folder.object; - } - function setupActiveEditor(fileName: string | undefined, languageId: string) { - const documentManager = TypeMoq.Mock.ofType(); - if (fileName) { - const textEditor = TypeMoq.Mock.ofType(); - const document = TypeMoq.Mock.ofType(); - document.setup(d => d.languageId).returns(() => languageId); - document.setup(d => d.fileName).returns(() => fileName); - textEditor.setup(t => t.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - } else { - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); - } - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); - } - function setupWorkspaces(folders: string[]) { - const workspaceService = TypeMoq.Mock.ofType(); - const workspaceFolders = folders.map(createMoqWorkspaceFolder); - workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - } - test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { request: 'attach' } as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { - const pythonFile = 'xyz.py'; - - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); - expect(debugConfig).to.have.property('host', 'localhost'); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { - setupActiveEditor(undefined, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); - expect(debugConfig).to.have.property('host', 'localhost'); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { - const activeFile = 'xyz.js'; - - setupActiveEditor(activeFile, 'javascript'); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); - expect(debugConfig).to.not.have.property('localRoot'); - expect(debugConfig).to.have.property('host', 'localhost'); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { - const activeFile = 'xyz.py'; - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); - expect(debugConfig).to.have.property('host', 'localhost'); - }); - test('Ensure \'localRoot\' is left unaltered', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('localRoot', localRoot); - }); - ['localhost', '127.0.0.1', '::1'].forEach(host => { - test(`Ensure path mappings are automatically added when host is '${host}'`, async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - expect(pathMappings).to.be.lengthOf(1); - expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); - expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); - }); - }); - ['192.168.1.123', 'don.debugger.com'].forEach(host => { - test(`Ensure path mappings are not automatically added when host is '${host}'`, async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - expect(pathMappings).to.be.lengthOf(0); - }); - }); - test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; - const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig!.pathMappings).to.be.lengthOf(1); - expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); - }); - test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; - const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig!.pathMappings).to.be.lengthOf(1); - expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); - }); - test('Ensure \'remoteRoot\' is left unaltered', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('remoteRoot', remoteRoot); - }); - test('Ensure \'port\' is left unaltered', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const port = 12341234; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('port', port); - }); - test('Ensure \'debugOptions\' are left unaltered', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); - const expectedDebugOptions = debugOptions.slice(); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); - }); - }); - }); -}); diff --git a/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts new file mode 100644 index 000000000000..ebfd95f0162b --- /dev/null +++ b/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PythonDebugConfigurationProvider } from '../../../../client/debugger/extension/configuration/debugConfigurationProvider'; +import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; + +suite('xDebugging - Configuration Provider', () => { + let attachResolver: typemoq.IMock>; + let launchResolver: typemoq.IMock>; + let provider: PythonDebugConfigurationProvider; + + setup(() => { + attachResolver = typemoq.Mock.ofType>(); + launchResolver = typemoq.Mock.ofType>(); + provider = new PythonDebugConfigurationProvider(attachResolver.object, launchResolver.object); + }); + test('Should use attach resolver when passing attach config', async () => { + const config = { + request: 'attach' + } as any as AttachRequestArguments; + const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; + const expectedConfig = { yay: 1 }; + + attachResolver + .setup(a => a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config), typemoq.It.isAny())) + .returns(() => Promise.resolve(expectedConfig as any)) + .verifiable(typemoq.Times.once()); + launchResolver + .setup(a => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + const resolvedConfig = await provider.resolveDebugConfiguration(folder, config as any); + + expect(resolvedConfig).to.deep.equal(expectedConfig); + attachResolver.verifyAll(); + launchResolver.verifyAll(); + }); + [ + { request: 'launch' }, { request: undefined } + ].forEach(config => { + test(`Should use launch resolver when passing launch config with request=${config.request}`, async () => { + const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; + const expectedConfig = { yay: 1 }; + + launchResolver + .setup(a => a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config as any as LaunchRequestArguments), typemoq.It.isAny())) + .returns(() => Promise.resolve(expectedConfig as any)) + .verifiable(typemoq.Times.once()); + attachResolver + .setup(a => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + const resolvedConfig = await provider.resolveDebugConfiguration(folder, config as any); + + expect(resolvedConfig).to.deep.equal(expectedConfig); + attachResolver.verifyAll(); + launchResolver.verifyAll(); + }); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts new file mode 100644 index 000000000000..f4f1d36fe8de --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion no-invalid-this + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../../../client/common/platform/types'; +import { IConfigurationService } from '../../../../../client/common/types'; +import { getNamesAndValues } from '../../../../../client/common/utils/enum'; +import { OSType } from '../../../../../client/common/utils/platform'; +import { AttachConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/attach'; +import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; +import { IServiceContainer } from '../../../../../client/ioc/types'; + +getNamesAndValues(OSType).forEach(os => { + if (os.value === OSType.Unknown) { + return; + } + suite(`Debugging - Config Resolver attach, OS = ${os.name}`, () => { + let serviceContainer: TypeMoq.IMock; + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + const debugOptionsAvailable = [DebugOptions.RedirectOutput]; + if (os.value === OSType.Windows) { + debugOptionsAvailable.push(DebugOptions.FixFilePathCase); + debugOptionsAvailable.push(DebugOptions.WindowsClient); + } else { + debugOptionsAvailable.push(DebugOptions.UnixClient); + } + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + platformService.setup(p => p.isWindows).returns(() => os.value === OSType.Windows); + platformService.setup(p => p.isMac).returns(() => os.value === OSType.OSX); + platformService.setup(p => p.isLinux).returns(() => os.value === OSType.Linux); + documentManager = TypeMoq.Mock.ofType(); + debugProvider = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); + }); + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType(); + folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + function setupActiveEditor(fileName: string | undefined, languageId: string) { + if (fileName) { + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.languageId).returns(() => languageId); + document.setup(d => d.fileName).returns(() => fileName); + textEditor.setup(t => t.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + } + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); + } + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + setupActiveEditor(undefined, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const activeFile = 'xyz.js'; + + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const activeFile = 'xyz.py'; + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Ensure \'localRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + ['localhost', '127.0.0.1', '::1'].forEach(host => { + test(`Ensure path mappings are automatically added when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + expect(pathMappings).to.be.lengthOf(1); + expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + }); + }); + ['192.168.1.123', 'don.debugger.com'].forEach(host => { + test(`Ensure path mappings are not automatically added when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + expect(pathMappings).to.be.lengthOf(0); + }); + }); + test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.lengthOf(1); + expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); + }); + test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.lengthOf(1); + expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); + }); + test('Ensure \'remoteRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test('Ensure \'port\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test('Ensure \'debugOptions\' are left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const expectedDebugOptions = debugOptions.slice(); + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); + }); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts new file mode 100644 index 000000000000..b977734bfb35 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-invalid-template-strings max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { DebugConfiguration, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { DocumentManager } from '../../../../../client/common/application/documentManager'; +import { IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../../../client/common/configuration/service'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IConfigurationService } from '../../../../../client/common/types'; +import { BaseConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/base'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; + +suite('Debugging - Config Resolver', () => { + class BaseResolver extends BaseConfigurationResolver { + public resolveDebugConfiguration(_folder: WorkspaceFolder | undefined, _debugConfiguration: DebugConfiguration, _token?: CancellationToken): Promise { + throw new Error('Not Implemented'); + } + public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + return super.getWorkspaceFolder(folder); + } + public getProgram(): string | undefined { + return super.getProgram(); + } + public resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): void { + return super.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + } + public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + return super.debugOption(debugOptions, debugOption); + } + public isLocalHost(hostName?: string) { + return super.isLocalHost(hostName); + } + public isDebuggingFlask(debugConfiguration: Partial) { + return super.isDebuggingFlask(debugConfiguration); + } + } + let resolver: BaseResolver; + let workspaceService: IWorkspaceService; + let documentManager: IDocumentManager; + let configurationService: IConfigurationService; + setup(() => { + workspaceService = mock(WorkspaceService); + documentManager = mock(DocumentManager); + configurationService = mock(ConfigurationService); + resolver = new BaseResolver(instance(workspaceService), instance(documentManager), instance(configurationService)); + }); + + test('Program should return filepath of active editor if file is python', () => { + const expectedFileName = 'my.py'; + const editor = typemoq.Mock.ofType(); + const doc = typemoq.Mock.ofType(); + + editor.setup(e => e.document).returns(() => doc.object).verifiable(typemoq.Times.once()); + doc.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE).verifiable(typemoq.Times.once()); + doc.setup(d => d.fileName).returns(() => expectedFileName).verifiable(typemoq.Times.once()); + when(documentManager.activeTextEditor).thenReturn(editor.object); + + const program = resolver.getProgram(); + + expect(program).to.be.equal(expectedFileName); + }); + test('Program should return undefined if active file is not python', () => { + const editor = typemoq.Mock.ofType(); + const doc = typemoq.Mock.ofType(); + + editor.setup(e => e.document).returns(() => doc.object).verifiable(typemoq.Times.once()); + doc.setup(d => d.languageId).returns(() => 'C#').verifiable(typemoq.Times.once()); + when(documentManager.activeTextEditor).thenReturn(editor.object); + + const program = resolver.getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Program should return undefined if there is no active editor', () => { + when(documentManager.activeTextEditor).thenReturn(undefined); + + const program = resolver.getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Should get workspace folder when workspace folder is provided', () => { + const expectedUri = Uri.parse('mock'); + const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; + + const uri = resolver.getWorkspaceFolder(folder); + + expect(uri).to.be.deep.equal(expectedUri); + }); + [ + { title: 'Should get directory of active program when there are not workspace folders', workspaceFolders: undefined }, + { title: 'Should get directory of active program when there are 0 workspace folders', workspaceFolders: [] } + ] + .forEach(item => { + test(item.title, () => { + const programPath = path.join('one', 'two', 'three.xyz'); + + resolver.getProgram = () => programPath; + when(workspaceService.workspaceFolders).thenReturn(item.workspaceFolders); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri!.fsPath).to.be.deep.equal(Uri.file(path.dirname(programPath)).fsPath); + }); + }); + test('Should return uri of workspace folder if there is only one workspace folder', () => { + const expectedUri = Uri.parse('mock'); + const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; + const folders: WorkspaceFolder[] = [folder]; + + resolver.getProgram = () => undefined; + when(workspaceService.workspaceFolders).thenReturn(folders); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri!.fsPath).to.be.deep.equal(expectedUri.fsPath); + }); + test('Should return uri of workspace folder corresponding to program if there is more than one workspace folder', () => { + const programPath = path.join('one', 'two', 'three.xyz'); + const folder1: WorkspaceFolder = { index: 0, uri: Uri.parse('mock'), name: 'mock' }; + const folder2: WorkspaceFolder = { index: 1, uri: Uri.parse('134'), name: 'mock2' }; + const folders: WorkspaceFolder[] = [folder1, folder2]; + + resolver.getProgram = () => programPath; + when(workspaceService.workspaceFolders).thenReturn(folders); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder2); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri!.fsPath).to.be.deep.equal(folder2.uri.fsPath); + }); + test('Should return undefined when program does not belong to any of the workspace folders', () => { + const programPath = path.join('one', 'two', 'three.xyz'); + const folder1: WorkspaceFolder = { index: 0, uri: Uri.parse('mock'), name: 'mock' }; + const folder2: WorkspaceFolder = { index: 1, uri: Uri.parse('134'), name: 'mock2' }; + const folders: WorkspaceFolder[] = [folder1, folder2]; + + resolver.getProgram = () => programPath; + when(workspaceService.workspaceFolders).thenReturn(folders); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri).to.be.deep.equal(undefined, 'not undefined'); + }); + test('Do nothing if debug configuration is undefined', () => { + resolver.resolveAndUpdatePythonPath(undefined, undefined as any); + }); + test('Python path in debug config must point to pythonpath in settings if pythonPath in config is not set', () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(configurationService.getSettings(anything())).thenReturn({ pythonPath } as any); + + resolver.resolveAndUpdatePythonPath(undefined, config as any); + + expect(config).to.have.property('pythonPath', pythonPath); + }); + test('Python path in debug config must point to pythonpath in settings if pythonPath in config is ${config:python.pythonPath}', () => { + const config = { + pythonPath: '${config:python.pythonPath}' + }; + const pythonPath = path.join('1', '2', '3'); + + when(configurationService.getSettings(anything())).thenReturn({ pythonPath } as any); + + resolver.resolveAndUpdatePythonPath(undefined, config as any); + + expect(config.pythonPath).to.equal(pythonPath); + }); + const localHostTestMatrix = { localhost: true, '127.0.0.1': true, '::1': true, '127.0.0.2': false, '156.1.2.3': false, '::2': false }; + Object.keys(localHostTestMatrix) + .forEach(key => { + test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { + const isLocalHost = resolver.isLocalHost(key); + + expect(isLocalHost).to.equal(localHostTestMatrix[key]); + }); + }); + test('Is debugging flask=true', () => { + const config = { module: 'flask' }; + const isFlask = resolver.isDebuggingFlask(config as any); + expect(isFlask).to.equal(true, 'not flask'); + }); + test('Is debugging flask=false', () => { + const config = { module: 'flask2' }; + const isFlask = resolver.isDebuggingFlask(config as any); + expect(isFlask).to.equal(false, 'flask'); + }); + test('Is debugging flask=false when not defined', () => { + const config = {}; + const isFlask = resolver.isDebuggingFlask(config as any); + expect(isFlask).to.equal(false, 'flask'); + }); +}); diff --git a/src/test/debugger/extension/configProvider/provider.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts similarity index 93% rename from src/test/debugger/extension/configProvider/provider.unit.test.ts rename to src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 067f37d5b285..2b07d892d5c5 100644 --- a/src/test/debugger/extension/configProvider/provider.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -9,22 +9,22 @@ import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { InvalidPythonPathInDebuggerServiceId } from '../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; -import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../client/application/diagnostics/types'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; -import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -import { IConfigurationService, ILogger, IPythonSettings } from '../../../../client/common/types'; -import { DebuggerTypeName } from '../../../../client/debugger/constants'; -import { ConfigurationProviderUtils } from '../../../../client/debugger/extension/configuration/configurationProviderUtils'; -import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configuration/pythonV2Provider'; -import { IConfigurationProviderUtils } from '../../../../client/debugger/extension/configuration/types'; -import { DebugOptions, LaunchRequestArguments } from '../../../../client/debugger/types'; -import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Debugging - Config Provider', () => { +import { InvalidPythonPathInDebuggerServiceId } from '../../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; +import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../../../client/common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../../client/common/process/types'; +import { IConfigurationService, ILogger, IPythonSettings } from '../../../../../client/common/types'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { ConfigurationProviderUtils } from '../../../../../client/debugger/extension/configuration/configurationProviderUtils'; +import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; +import { IConfigurationProviderUtils } from '../../../../../client/debugger/extension/configuration/types'; +import { DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; +import { IInterpreterHelper } from '../../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../../client/ioc/types'; + +suite('Debugging - Config Resolver Launch', () => { let serviceContainer: TypeMoq.IMock; let debugProvider: DebugConfigurationProvider; let platformService: TypeMoq.IMock; @@ -33,11 +33,9 @@ suite('Debugging - Config Provider', () => { let pythonExecutionService: TypeMoq.IMock; let logger: TypeMoq.IMock; let helper: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; let diagnosticsService: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - debugProvider = new PythonV2DebugConfigurationProvider(serviceContainer.object); - }); function createMoqWorkspaceFolder(folderPath: string) { const folder = TypeMoq.Mock.ofType(); folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); @@ -45,6 +43,10 @@ suite('Debugging - Config Provider', () => { } function setupIoc(pythonPath: string, isWindows: boolean = false, isMac: boolean = false, isLinux: boolean = false) { const confgService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -61,12 +63,14 @@ suite('Debugging - Config Provider', () => { .setup(h => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(true)); + const configProviderUtils = new ConfigurationProviderUtils(factory.object, fileSystem.object, appShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))).returns(() => factory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => confgService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationProviderUtils))).returns(() => new ConfigurationProviderUtils(serviceContainer.object)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationProviderUtils))).returns(() => configProviderUtils); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDiagnosticsService), TypeMoq.It.isValue(InvalidPythonPathInDebuggerServiceId))).returns(() => diagnosticsService.object); @@ -75,9 +79,10 @@ suite('Debugging - Config Provider', () => { settings.setup(s => s.pythonPath).returns(() => pythonPath); confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); setupOs(isWindows, isMac, isLinux); + + debugProvider = new LaunchConfigurationResolver(workspaceService.object, documentManager.object, configProviderUtils, diagnosticsService.object, platformService.object, confgService.object); } function setupActiveEditor(fileName: string | undefined, languageId: string) { - const documentManager = TypeMoq.Mock.ofType(); if (fileName) { const textEditor = TypeMoq.Mock.ofType(); const document = TypeMoq.Mock.ofType(); @@ -91,7 +96,6 @@ suite('Debugging - Config Provider', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); } function setupWorkspaces(folders: string[]) { - const workspaceService = TypeMoq.Mock.ofType(); const workspaceFolders = folders.map(createMoqWorkspaceFolder); workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); @@ -354,8 +358,7 @@ suite('Debugging - Config Provider', () => { .returns(() => Promise.resolve(pyramidExists)) .verifiable(TypeMoq.Times.exactly(pyramidExists && addPyramidDebugOption ? 1 : 0)); appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.exactly(pyramidExists || !addPyramidDebugOption ? 0 : 1)); - logger.setup(a => a.logError(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.exactly(pyramidExists || !addPyramidDebugOption ? 0 : 1)); const options = addPyramidDebugOption ? { debugOptions: [DebugOptions.Pyramid], pyramid: true } : {}; diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..32d2965fe775 --- /dev/null +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-invalid-template-strings max-func-body-length no-any + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DebuggerBanner } from '../../../client/debugger/extension/banner'; +import { ConfigurationProviderUtils } from '../../../client/debugger/extension/configuration/configurationProviderUtils'; +import { PythonDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/debugConfigurationProvider'; +import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; +import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; +import { IConfigurationProviderUtils, IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; +import { ChildProcessAttachEventHandler } from '../../../client/debugger/extension/hooks/childProcessAttachHandler'; +import { ChildProcessAttachService } from '../../../client/debugger/extension/hooks/childProcessAttachService'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../../client/debugger/extension/hooks/types'; +import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; +import { IDebugConfigurationProvider, IDebuggerBanner } from '../../../client/debugger/extension/types'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Debugging - Service Registry', () => { + test('Registrations', () => { + const serviceManager = typemoq.Mock.ofType(); + + [ + [IDebugConfigurationProvider, PythonDebugConfigurationProvider], + [IConfigurationProviderUtils, ConfigurationProviderUtils], + [IDebuggerBanner, DebuggerBanner], + [IChildProcessAttachService, ChildProcessAttachService], + [IDebugSessionEventHandlers, ChildProcessAttachEventHandler], + [IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'], + [IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'] + ].forEach(mapping => { + if (mapping.length === 2) { + serviceManager + .setup(s => s.addSingleton(typemoq.It.isValue(mapping[0] as any), typemoq.It.isAny())) + .callback((_, cls) => expect(cls).to.equal(mapping[1])) + .verifiable(typemoq.Times.once()); + } else { + serviceManager + .setup(s => s.addSingleton(typemoq.It.isValue(mapping[0] as any), typemoq.It.isAny(), typemoq.It.isValue(mapping[2] as any))) + .callback((_, cls) => expect(cls).to.equal(mapping[1])) + .verifiable(typemoq.Times.once()); + } + }); + + registerTypes(serviceManager.object); + serviceManager.verifyAll(); + }); +}); From 10f8b7c70ad915bc6ae2597949b762c1265e9678 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 18 Dec 2018 11:44:22 -0800 Subject: [PATCH 3/9] refactor config and labels --- package.json | 14 +++----------- package.nls.de.json | 2 +- package.nls.es.json | 2 +- package.nls.fr.json | 2 +- package.nls.it.json | 2 +- package.nls.ja.json | 2 +- package.nls.json | 2 +- package.nls.ko-kr.json | 2 +- package.nls.pt-br.json | 2 +- package.nls.ru.json | 2 +- package.nls.zh-cn.json | 3 +-- package.nls.zh-tw.json | 4 ++-- 12 files changed, 15 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index fd73169b41a4..4b6ecca97e99 100644 --- a/package.json +++ b/package.json @@ -890,11 +890,10 @@ }, "initialConfigurations": [ { - "name": "Python: Current File (Integrated Terminal)", + "name": "Python: Current File", "type": "python", "request": "launch", - "program": "${file}", - "console": "integratedTerminal" + "program": "${file}" }, { "name": "Python: Attach", @@ -937,13 +936,6 @@ "--no-reload" ], "jinja": true - }, - { - "name": "Python: Current File (External Terminal)", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "externalTerminal" } ] } @@ -1860,7 +1852,7 @@ "compile-webviews-verbose": "npx webpack --config webpack.datascience-ui.config.js", "postinstall": "node ./node_modules/vscode/bin/install", "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", - "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts", + "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts --grep='Debugging'", "test:unittests:cover": "nyc --nycrc-path ./build/.nycrc npm run test:unittests", "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", "test:functional:cover": "nyc --nycrc-path ./build/.nycrc npm run test:functional", diff --git a/package.nls.de.json b/package.nls.de.json index 6744ee1cf235..4c6af269cd11 100644 --- a/package.nls.de.json +++ b/package.nls.de.json @@ -35,7 +35,7 @@ "python.snippet.launch.externalTerminal.description": "Python-Programm mit externem Terminal/Konsole debuggen", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Django-Anwendung debuggen", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x oder neuer)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Flask-Anwendung debuggen", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x oder früher)", "python.snippet.launch.flaskOld.description": "Ältere Flask-Anwendung debuggen", diff --git a/package.nls.es.json b/package.nls.es.json index d71abd699922..9160b00c73ee 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -35,7 +35,7 @@ "python.snippet.launch.externalTerminal.description": "Depurar una aplicación Python usando una terminal externa", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Depurar una aplicación de Django", - "python.snippet.launch.flask.label": "Python: Flask (Versión 0.11.x o posterior)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Depurar una aplicación de Flask", "python.snippet.launch.flaskOld.label": "Python: Flask (Versión 0.10.x o anterior)", "python.snippet.launch.flaskOld.description": "Depurar una aplicación de Flask de estilo antiguo", diff --git a/package.nls.fr.json b/package.nls.fr.json index c764405ca1da..196c4ac8db06 100644 --- a/package.nls.fr.json +++ b/package.nls.fr.json @@ -34,7 +34,7 @@ "python.snippet.launch.externalTerminal.description": "Déboguer un programme Python avec une console externe", "python.snippet.launch.django.label": "Python : Django", "python.snippet.launch.django.description": "Déboguer une application Django", - "python.snippet.launch.flask.label": "Python : Flask (0.11.x ou supérieur)", + "python.snippet.launch.flask.label": "Python : Flask", "python.snippet.launch.flask.description": "Déboguer une application Flask", "python.snippet.launch.flaskOld.label": "Python : Flask (0.10.x ou antérieur)", "python.snippet.launch.flaskOld.description": "Déboguer une application Flask (0.10.x ou antérieur)", diff --git a/package.nls.it.json b/package.nls.it.json index 01b231682a19..1643731c81f3 100644 --- a/package.nls.it.json +++ b/package.nls.it.json @@ -34,7 +34,7 @@ "python.snippet.launch.externalTerminal.description": "Esegui debug di un programma Python nel terminale esterno", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Esegui debug applicazione Django", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x o successiva)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Esegui debug applicazione Flask", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x o precedente)", "python.snippet.launch.flaskOld.description": "Esegui debug applicazione Flask in vecchio stile", diff --git a/package.nls.ja.json b/package.nls.ja.json index 8b8deec49c7d..7768ca8d097f 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -30,7 +30,7 @@ "python.snippet.launch.externalTerminal.description": "外部のターミナル/コンソールで Python プログラムをデバッグ", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Django アプリケーションをデバッグ", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x 以降)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Flask アプリケーションをデバッグ", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 以前)", "python.snippet.launch.flaskOld.description": "旧式の Flask アプリケーションをデバッグ", diff --git a/package.nls.json b/package.nls.json index cdc3df624adf..0a8050a67477 100644 --- a/package.nls.json +++ b/package.nls.json @@ -54,7 +54,7 @@ "python.snippet.launch.externalTerminal.description": "Debug a Python program with External Terminal/Console", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Debug a Django Application", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x or later)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Debug a Flask Application", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x or earlier)", "python.snippet.launch.flaskOld.description": "Debug an older styled Flask Application", diff --git a/package.nls.ko-kr.json b/package.nls.ko-kr.json index c668625209f2..5534ed4bef06 100644 --- a/package.nls.ko-kr.json +++ b/package.nls.ko-kr.json @@ -30,7 +30,7 @@ "python.snippet.launch.externalTerminal.description": "외부 터미널/콘솔에서 Python 프로그램 디버그", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Django 응용 프로그램 디버그", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x 또는 이후 버전)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Flask 응용 프로그램 디버그", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 또는 이전 버전)", "python.snippet.launch.flaskOld.description": "이전 스타일의 Flask 응용 프로그램 디버그", diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json index 5b6e4dc2b539..5b9505e81c9b 100644 --- a/package.nls.pt-br.json +++ b/package.nls.pt-br.json @@ -35,7 +35,7 @@ "python.snippet.launch.externalTerminal.description": "Depurar um Programa Python com Terminal/Console Externo", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Depurar uma Aplicação Django", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x ou superior)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Depurar uma Aplicação Flask", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x ou inferior)", "python.snippet.launch.flaskOld.description": "Depurar uma Aplicação Flask no Estilo Antigo", diff --git a/package.nls.ru.json b/package.nls.ru.json index 8208b0e44e1d..31247aa54535 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -34,7 +34,7 @@ "python.snippet.launch.externalTerminal.description": "Отладка программы Python во внешней консоли", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "Отладка приложения Django", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x или новее)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "Отладка приложения Flask", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x или старее)", "python.snippet.launch.flaskOld.description": "Отладка приложения Flask (старый стиль)", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 4580d1fa4b10..f7100b8ae56f 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -24,7 +24,6 @@ "python.command.python.enableLinting.title": "启用 Linting", "python.command.python.runLinting.title": "运行 Linting", "python.snippet.launch.standard.label": "Python: 当前文件", - "python.snippet.launch.standard.label": "Python: Current File", "python.snippet.launch.standard.description": "使用标准输出调试 Python 应用", "python.snippet.launch.pyspark.label": "Python: PySpark", "python.snippet.launch.pyspark.description": "调试 PySpark", @@ -36,7 +35,7 @@ "python.snippet.launch.externalTerminal.description": "使用外部终端调试 Python 程序", "python.snippet.launch.django.label": "Python: Django", "python.snippet.launch.django.description": "调试 Django 应用", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x 或以后)", + "python.snippet.launch.flask.label": "Python: Flask", "python.snippet.launch.flask.description": "调试 Flask 应用", "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 或之前)", "python.snippet.launch.flaskOld.description": "调试旧式 Flask 应用", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index caa905e79b3d..388527016a2c 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -34,7 +34,7 @@ "python.snippet.launch.externalTerminal.description": "使用外部終端機偵錯 Python 程式", "python.snippet.launch.django.label": "Python:Django", "python.snippet.launch.django.description": "偵錯 Django 程式", - "python.snippet.launch.flask.label": "Python:Flask(0.11.x 或以後)", + "python.snippet.launch.flask.label": "Python:Flask", "python.snippet.launch.flask.description": "偵錯 Flask 程式", "python.snippet.launch.flaskOld.label": "Python:Flask(0.10.x 或之前)", "python.snippet.launch.flaskOld.description": "偵錯舊式 Flask 程式", @@ -49,4 +49,4 @@ "python.command.python.discoverTests.title": "探索 Unit 測試項目", "python.snippet.launch.gevent.label": "Python: Gevent", "python.snippet.launch.gevent.description": "偵錯 Gevent 應用程式" -} \ No newline at end of file +} From 2d040e6239531bd6d4e773ba3c088a4dacab12b1 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 18 Dec 2018 11:44:51 -0800 Subject: [PATCH 4/9] Revert config --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b6ecca97e99..dfffcc6eeafe 100644 --- a/package.json +++ b/package.json @@ -1852,7 +1852,7 @@ "compile-webviews-verbose": "npx webpack --config webpack.datascience-ui.config.js", "postinstall": "node ./node_modules/vscode/bin/install", "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", - "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts --grep='Debugging'", + "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts", "test:unittests:cover": "nyc --nycrc-path ./build/.nycrc npm run test:unittests", "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", "test:functional:cover": "nyc --nycrc-path ./build/.nycrc npm run test:functional", From 2ca5fbc1c024b5aeeb4df9dda5ebdd7f419a3dd4 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 18 Dec 2018 12:07:20 -0800 Subject: [PATCH 5/9] Add prompts --- package.nls.json | 23 +++++-- src/client/common/platform/pathUtils.ts | 3 + src/client/common/types.ts | 6 ++ src/client/common/utils/localize.ts | 17 +++++ .../debugConfigurationProvider.ts | 23 ------- .../debugConfigurationService.ts | 41 +++++++++++ .../configuration/providers/djangoLaunch.ts | 69 +++++++++++++++++++ .../configuration/providers/fileLaunch.ts | 27 ++++++++ .../configuration/providers/flaskLaunch.ts | 62 +++++++++++++++++ .../configuration/providers/moduleLaunch.ts | 39 +++++++++++ .../configuration/providers/picker.ts | 29 ++++++++ .../configuration/providers/remoteAttach.ts | 58 ++++++++++++++++ .../configuration/resolvers/launch.ts | 3 +- .../debugger/extension/serviceRegistry.ts | 19 +++-- src/client/debugger/extension/types.ts | 31 ++++++++- src/client/extension.ts | 6 +- src/test/common/platform/pathUtils.test.ts | 18 +++++ src/test/debugger/attach.ptvsd.test.ts | 5 +- ...=> debugConfigurationService.unit.test.ts} | 11 +-- .../extension/serviceRegistry.unit.test.ts | 6 +- 20 files changed, 449 insertions(+), 47 deletions(-) delete mode 100644 src/client/debugger/extension/configuration/debugConfigurationProvider.ts create mode 100644 src/client/debugger/extension/configuration/debugConfigurationService.ts create mode 100644 src/client/debugger/extension/configuration/providers/djangoLaunch.ts create mode 100644 src/client/debugger/extension/configuration/providers/fileLaunch.ts create mode 100644 src/client/debugger/extension/configuration/providers/flaskLaunch.ts create mode 100644 src/client/debugger/extension/configuration/providers/moduleLaunch.ts create mode 100644 src/client/debugger/extension/configuration/providers/picker.ts create mode 100644 src/client/debugger/extension/configuration/providers/remoteAttach.ts create mode 100644 src/test/common/platform/pathUtils.test.ts rename src/test/debugger/extension/configuration/{debugConfigurationProvider.unit.test.ts => debugConfigurationService.unit.test.ts} (81%) diff --git a/package.nls.json b/package.nls.json index 0a8050a67477..e837cb046758 100644 --- a/package.nls.json +++ b/package.nls.json @@ -135,9 +135,22 @@ "Common.canceled": "Canceled", "DataScience.importChangeDirectoryComment": "#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataSciece.changeDirOnImportExport setting", "DataScience.exportChangeDirectoryComment": "# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataSciece.changeDirOnImportExport setting", - "DataScience.interruptKernelStatus" : "Interrupting iPython Kernel", - "DataScience.restartKernelAfterInterruptMessage" : "Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.", - "DataScience.pythonInterruptFailedHeader" : "Keyboard interrupt crashed the kernel. Kernel restarted.", - "DataScience.sysInfoURILabel" : "Jupyter Server URI: ", - "Common.loadingPythonExtension": "Python extension loading..." + "DataScience.interruptKernelStatus": "Interrupting iPython Kernel", + "DataScience.restartKernelAfterInterruptMessage": "Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.", + "DataScience.pythonInterruptFailedHeader": "Keyboard interrupt crashed the kernel. Kernel restarted.", + "DataScience.sysInfoURILabel": "Jupyter Server URI: ", + "Common.loadingPythonExtension": "Python extension loading...", + "debug.selectConfiguration": "Select Debug Configuration", + "debug.debugFileConfigurationLabel": "Debug Python File", + "debug.attachConfigurationLabel": "Attach debugger to remote application", + "debug.debugDjangoConfigurationLabel": "Debug Django", + "debug.debugFlaskConfigurationLabel": "Debug Flask", + "debug.attachRemotePortPlaceholder": "Port", + "debug.attachRemotePortPrompt": "Enter Port Number", + "debug.attachRemotePortValidationError": "Enter a valid Port Number", + "debug.attachRemoteHostPlaceholder": "Host", + "debug.attachRemoteHostPrompt": "Enter Host Name", + "debug.attachRemoteHostValidationError": "Enter a Host Name", + "debug.debugDjangoSelectManagePyOpenDialogLabel": "Select 'manage.py' file", + "debug.debugFlaskSelectAppOpenDialogLabel": "Select Flask Application file/package" } diff --git a/src/client/common/platform/pathUtils.ts b/src/client/common/platform/pathUtils.ts index 16fc16c530de..87b2b5c22354 100644 --- a/src/client/common/platform/pathUtils.ts +++ b/src/client/common/platform/pathUtils.ts @@ -14,6 +14,9 @@ export class PathUtils implements IPathUtils { public get delimiter(): string { return path.delimiter; } + public get separator(): string { + return path.sep; + } // TO DO: Deprecate in favor of IPlatformService public getPathVariableName() { return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 0416a9529cfa..fa56d0ab27ba 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -101,6 +101,12 @@ export const IPathUtils = Symbol('IPathUtils'); export interface IPathUtils { readonly delimiter: string; readonly home: string; + /** + * The platform-specific file separator. '\\' or '/'. + * @type {string} + * @memberof IPathUtils + */ + readonly separator: string; getPathVariableName(): 'Path' | 'PATH'; basename(pathValue: string, ext?: string): string; getDisplayName(pathValue: string, cwd?: string): string; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 16ab6158a610..61b4fa7c7a07 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -105,6 +105,23 @@ export namespace DataScience { export const sysInfoURILabel = localize('DataScience.sysInfoURILabel', 'Jupyter Server URI: '); } +export namespace Debug { + export const selectConfiguration = localize('debug.selectConfiguration', 'Select Debug Configuration'); + export const debugFileConfigurationLabel = localize('debug.debugFileConfigurationLabel', 'Debug Python File'); + export const attachConfigurationLabel = localize('debug.attachConfigurationLabel', 'Attach debugger to remote application'); + export const debugDjangoConfigurationLabel = localize('debug.debugDjangoConfigurationLabel', 'Debug Django'); + export const debugFlaskConfigurationLabel = localize('debug.debugFlaskConfigurationLabel', 'Debug Flask'); + export const attachRemotePortPlaceholder = localize('debug.attachRemotePortPlaceholder', 'Port'); + export const attachRemotePortPrompt = localize('debug.attachRemotePortPrompt', 'Enter Port Number'); + export const attachRemotePortValidationError = localize('debug.attachRemotePortValidationError', 'Enter a valid Port Number'); + + export const attachRemoteHostPlaceholder = localize('debug.attachRemoteHostPlaceholder', 'Host'); + export const attachRemoteHostPrompt = localize('debug.attachRemoteHostPrompt', 'Enter Host Name'); + export const attachRemoteHostValidationError = localize('debug.attachRemoteHostValidationError', 'Enter a Host Name'); + export const debugDjangoSelectManagePyOpenDialogLabel = localize('debug.debugDjangoSelectManagePyOpenDialogLabel', 'Select \'manage.py\' file'); + export const debugFlaskSelectAppOpenDialogLabel = localize('debug.debugFlaskSelectAppOpenDialogLabel', 'Select Flask Application file/package'); +} + // Skip using vscode-nls and instead just compute our strings based on key values. Key values // can be loaded out of the nls..json files let loadedCollection: { [index: string]: string } | undefined; diff --git a/src/client/debugger/extension/configuration/debugConfigurationProvider.ts b/src/client/debugger/extension/configuration/debugConfigurationProvider.ts deleted file mode 100644 index aa0230e2d605..000000000000 --- a/src/client/debugger/extension/configuration/debugConfigurationProvider.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, WorkspaceFolder } from 'vscode'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { IDebugConfigurationResolver } from './types'; - -@injectable() -export class PythonDebugConfigurationProvider implements DebugConfigurationProvider { - constructor(@inject(IDebugConfigurationResolver) @named('attach') private readonly attachResolver: IDebugConfigurationResolver, - @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver) { - } - public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { - if (debugConfiguration.request === 'attach') { - return this.attachResolver.resolveDebugConfiguration(folder, debugConfiguration as AttachRequestArguments, token); - } else { - return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); - } } - -} diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts new file mode 100644 index 000000000000..038fd4e3db9a --- /dev/null +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, multiInject, named } from 'inversify'; +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugConfigurationPicker, IDebugConfigurationProvider, IDebugConfigurationService } from '../types'; +import { IDebugConfigurationResolver } from './types'; + +@injectable() +export class PythonDebugConfigurationService implements IDebugConfigurationService { + constructor(@inject(IDebugConfigurationResolver) @named('attach') private readonly attachResolver: IDebugConfigurationResolver, + @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, + @inject(IDebugConfigurationPicker) private readonly picker: IDebugConfigurationPicker, + @multiInject(IDebugConfigurationProvider) private readonly providers: IDebugConfigurationProvider[]) { + } + public async provideDebugConfigurations?(folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise { + const debugConfigType = await this.picker.getSelectedConfiguration(folder, token); + if (!debugConfigType) { + return; + } + const providers = this.providers.filter(p => p.isSupported(debugConfigType)); + if (providers.length === 0) { + return; + } + + const configs = await Promise.all(providers.map(provider => provider.provideDebugConfigurations(folder, token))); + // tslint:disable-next-line:no-require-imports no-var-requires + const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + return flatten(configs.filter(item => Array.isArray(item)).map(item => item!)); + } + public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { + if (debugConfiguration.request === 'attach') { + return this.attachResolver.resolveDebugConfiguration(folder, debugConfiguration as AttachRequestArguments, token); + } else { + return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); + } + } +} diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts new file mode 100644 index 000000000000..a0c8f553fd62 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, DebugConfiguration, OpenDialogOptions, WorkspaceFolder } from 'vscode'; +import { IApplicationShell } from '../../../../common/application/types'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IPathUtils } from '../../../../common/types'; +import { Debug, localize } from '../../../../common/utils/localize'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +// tslint:disable-next-line:no-invalid-template-strings +const workspaceFolderToken = '${workspaceFolder}'; + +@injectable() +export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor(@inject(IApplicationShell) private shell: IApplicationShell, + @inject(IFileSystem) private fs: IFileSystem, + @inject(IPathUtils) private pathUtils: IPathUtils) { } + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.launchDjango; + } + public async provideDebugConfigurations(folder: WorkspaceFolder, token?: CancellationToken): Promise { + const program = await this.getManagePyPath(folder); + return [ + { + name: localize('python.snippet.launch.django.label', 'Python: Django')(), + type: 'python', + request: 'launch', + program: program, + args: [ + 'runserver', + '--noreload', + '--nothreading' + ], + django: true + } + ]; + } + protected async getManagePyPath(folder: WorkspaceFolder): Promise { + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; + } + + const options: OpenDialogOptions = { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + defaultUri: folder.uri, + filters: { Python: ['py'] }, + openLabel: Debug.debugDjangoSelectManagePyOpenDialogLabel() + }; + const files = await this.shell.showOpenDialog(options); + if (!Array.isArray(files) || files.length !== 1) { + return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; + } + + const managePy = files[0].fsPath; + const relativePath = path.relative(folder.uri.fsPath, folder.uri.fsPath); + if (relativePath.startsWith('..') || relativePath.startsWith(this.pathUtils.separator)) { + return managePy; + } + return `${workspaceFolderToken}${this.pathUtils.separator}${relativePath}`; + } +} diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts new file mode 100644 index 000000000000..86b8c822d163 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; +import { localize } from '../../../../common/utils/localize'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.launchFile; + } + public async provideDebugConfigurations(_folder: WorkspaceFolder, _token?: CancellationToken): Promise { + return [ + { + name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), + type: 'python', + request: 'launch', + // tslint:disable-next-line:no-invalid-template-strings + program: '${file}' + } + ]; + } +} diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts new file mode 100644 index 000000000000..be315421ad8d --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, DebugConfiguration, OpenDialogOptions, WorkspaceFolder } from 'vscode'; +import { IApplicationShell } from '../../../../common/application/types'; +import { IFileSystem } from '../../../../common/platform/types'; +import { Debug, localize } from '../../../../common/utils/localize'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class FlaskLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor(@inject(IApplicationShell) private shell: IApplicationShell, + @inject(IFileSystem) private fs: IFileSystem) { } + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.launchFlask; + } + public async provideDebugConfigurations(folder: WorkspaceFolder, token?: CancellationToken): Promise { + const application = await this.getApplicationPath(folder); + return [ + { + name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), + type: 'python', + request: 'launch', + module: 'flask', + env: { + FLASK_APP: application + }, + args: [ + 'run', + '--no-debugger', + '--no-reload' + ], + jinja: true + } + ]; + } + protected async getApplicationPath(folder: WorkspaceFolder): Promise { + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return 'app.py'; + } + + const options: OpenDialogOptions = { + canSelectFiles: true, + canSelectFolders: true, + canSelectMany: false, + defaultUri: folder.uri, + filters: { Python: ['py'] }, + openLabel: Debug.debugFlaskSelectAppOpenDialogLabel() + }; + const selections = await this.shell.showOpenDialog(options); + if (!Array.isArray(selections) || selections.length !== 1) { + return 'app.py'; + } + + return path.basename(selections[0].fsPath); + } +} diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts new file mode 100644 index 000000000000..d8ab34f3e3ff --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, DebugConfiguration, InputBoxOptions, WorkspaceFolder } from 'vscode'; +import { IApplicationShell } from '../../../../common/application/types'; +import { localize } from '../../../../common/utils/localize'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor(@inject(IApplicationShell) private shell: IApplicationShell) { } + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.launchModule; + } + public async provideDebugConfigurations(_folder: WorkspaceFolder, token?: CancellationToken): Promise { + const moduleName = await this.getModuleName(token); + return [ + { + name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), + type: 'python', + request: 'launch', + module: moduleName + } + ]; + } + protected async getModuleName(token): Promise { + const options: InputBoxOptions = { + ignoreFocusOut: false, + placeHolder: 'my.module', + prompt: 'Enter Python Module/Package name', + value: '' + }; + return this.shell.showInputBox(options, token); + } + +} diff --git a/src/client/debugger/extension/configuration/providers/picker.ts b/src/client/debugger/extension/configuration/providers/picker.ts new file mode 100644 index 000000000000..2e220e8b8dd2 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/picker.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, QuickPickItem, QuickPickOptions, WorkspaceFolder } from 'vscode'; +import { IApplicationShell } from '../../../../common/application/types'; +import { Debug } from '../../../../common/utils/localize'; +import { DebugConfigurationType, IDebugConfigurationPicker } from '../../types'; + +type OptionItem = QuickPickItem & { type: DebugConfigurationType }; +@injectable() +export class DebugConfigurationPicker implements IDebugConfigurationPicker { + constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) { } + + public async getSelectedConfiguration(_folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise { + const items: OptionItem[] = [ + { label: Debug.debugFileConfigurationLabel(), type: DebugConfigurationType.launchFile, description: 'hello there', detail: 'details' }, + { label: Debug.attachConfigurationLabel(), type: DebugConfigurationType.remoteAttach, description: 'hello there', detail: 'details' }, + { label: Debug.debugDjangoConfigurationLabel(), type: DebugConfigurationType.launchDjango, description: 'hello there', detail: 'details' }, + { label: Debug.debugFlaskConfigurationLabel(), type: DebugConfigurationType.launchFlask, description: 'hello there', detail: 'details' }, + { label: 'Module', type: DebugConfigurationType.launchFlask, description: 'Debug Python module/package', detail: 'Debug a python module invoking it with `-m`' } + ]; + const options: QuickPickOptions = { ignoreFocusOut: true, matchOnDescription: true, matchOnDetail: true, placeHolder: Debug.selectConfiguration() }; + const selection = await this.shell.showQuickPick(items, options, token); + return selection ? selection.type : undefined; + } +} diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts new file mode 100644 index 000000000000..1e5bc719e24b --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/remoteAttach.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, DebugConfiguration, InputBoxOptions, WorkspaceFolder } from 'vscode'; +import { IApplicationShell } from '../../../../common/application/types'; +import { Debug, localize } from '../../../../common/utils/localize'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class AttachDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor(@inject(IApplicationShell) private shell: IApplicationShell) { } + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.remoteAttach; + } + public async provideDebugConfigurations(_folder: WorkspaceFolder, token?: CancellationToken): Promise { + const host = await this.getHost(token); + const port = await this.getPort(token); + return [ + { + name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), + type: 'python', + request: 'attach', + port: port, + host: host + } + ]; + } + protected async getHost(token?: CancellationToken): Promise { + const validateHost = (selection?: string | undefined) => { + return (selection && selection.trim().length > 0) ? undefined : Debug.attachRemoteHostValidationError(); + }; + const options: InputBoxOptions = { + placeHolder: Debug.attachRemoteHostPlaceholder(), + value: 'localhost', + validateInput: validateHost, + ignoreFocusOut: true, + prompt: Debug.attachRemoteHostPrompt() + }; + return this.shell.showInputBox(options, token); + } + protected async getPort(token?: CancellationToken): Promise { + const validatePort = (selection?: string | undefined) => { + return (selection && /^\d+$/.test(selection.trim())) ? undefined : Debug.attachRemotePortValidationError(); + }; + const options: InputBoxOptions = { + placeHolder: Debug.attachRemotePortPlaceholder(), + value: '5678', + validateInput: validatePort, + ignoreFocusOut: true, + prompt: Debug.attachRemotePortPrompt() + }; + const port = await this.shell.showInputBox(options, token); + return port ? parseInt(port.trim(), 10) : undefined; + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index 4df970380e27..4fd6feb4efc8 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -6,6 +6,7 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { InvalidPythonPathInDebuggerServiceId } from '../../../../application/diagnostics/checks/invalidPythonPathInDebugger'; import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../application/diagnostics/types'; import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; import { IPlatformService } from '../../../../common/platform/types'; @@ -20,7 +21,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver(IDebugConfigurationProvider, PythonDebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationService, PythonDebugConfigurationService); serviceManager.addSingleton(IConfigurationProviderUtils, ConfigurationProviderUtils); serviceManager.addSingleton(IDebuggerBanner, DebuggerBanner); serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>(IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'); serviceManager.addSingleton>(IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'); + serviceManager.addSingleton(IDebugConfigurationPicker, DebugConfigurationPicker); + serviceManager.addSingleton(IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationProvider, AttachDebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 419f5ca16131..a2a2cf30d798 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,8 +3,37 @@ 'use strict'; -export const IDebugConfigurationProvider = Symbol('DebugConfigurationProvider'); +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, WorkspaceFolder } from 'vscode'; + +export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); +export interface IDebugConfigurationService extends DebugConfigurationProvider { } export const IDebuggerBanner = Symbol('IDebuggerBanner'); export interface IDebuggerBanner { initialize(): void; } + +export const IDebugConfigurationProvider = Symbol('IDebugConfigurationProvider'); +export interface IDebugConfigurationProvider { + isSupported(debugConfigurationType: DebugConfigurationType): boolean; + /** + * Provides initial [debug configuration](#DebugConfiguration). If more than one debug configuration provider is + * registered for the same type, debug configurations are concatenated in arbitrary order. + * + * @param folder The workspace folder for which the configurations are used or undefined for a folderless setup. + * @param token A cancellation token. + * @return An array of [debug configurations](#DebugConfiguration). + */ + provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; +} +export const IDebugConfigurationPicker = Symbol('IDebugConfigurationPicker'); +export interface IDebugConfigurationPicker { + getSelectedConfiguration(folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise; +} + +export enum DebugConfigurationType { + launchFile = 'launchFile', + remoteAttach = 'remoteAttach', + launchDjango = 'launchDjango', + launchFlask = 'launchFlask', + launchModule = 'launchModule' +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 741b79dcc180..2ce85e65649f 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -61,7 +61,7 @@ import { DebuggerTypeName } from './debugger/constants'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; -import { IDebugConfigurationProvider, IDebuggerBanner } from './debugger/extension/types'; +import { IDebugConfigurationService, IDebuggerBanner } from './debugger/extension/types'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterSelector } from './interpreter/configuration/types'; import { @@ -189,8 +189,8 @@ export async function activate(context: ExtensionContext): Promise(IDebugConfigurationProvider).forEach(debugConfig => { - context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfig)); + serviceContainer.getAll(IDebugConfigurationService).forEach(debugConfigProvider => { + context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); }); serviceContainer.get(IDebuggerBanner).initialize(); diff --git a/src/test/common/platform/pathUtils.test.ts b/src/test/common/platform/pathUtils.test.ts new file mode 100644 index 000000000000..f8f9d2d32597 --- /dev/null +++ b/src/test/common/platform/pathUtils.test.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import { expect } from 'chai'; +import * as path from 'path'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { getOSType, OSType } from '../../common'; + +suite('PathUtils', () => { + let utils: PathUtils; + suiteSetup(() => { + utils = new PathUtils(getOSType() === OSType.Windows); + }); + test('Path Separator', () => { + expect(utils.separator).to.be.equal(path.sep); + }); +}); diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index 372bc39595c9..a08bbafebefc 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -15,9 +15,10 @@ import { IS_WINDOWS } from '../../client/common/platform/constants'; import { IPlatformService } from '../../client/common/platform/types'; import { IConfigurationService } from '../../client/common/types'; import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; -import { PythonDebugConfigurationProvider } from '../../client/debugger/extension/configuration/debugConfigurationProvider'; +import { PythonDebugConfigurationService } from '../../client/debugger/extension/configuration/debugConfigurationService'; import { AttachConfigurationResolver } from '../../client/debugger/extension/configuration/resolvers/attach'; import { IDebugConfigurationResolver } from '../../client/debugger/extension/configuration/types'; +import { IDebugConfigurationPicker } from '../../client/debugger/extension/types'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; import { IServiceContainer } from '../../client/ioc/types'; import { PYTHON_PATH, sleep } from '../common'; @@ -96,7 +97,7 @@ suite('Attach Debugger', () => { const launchResolver = TypeMoq.Mock.ofType>(); const attachResolver = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); - const configProvider = new PythonDebugConfigurationProvider(attachResolver, launchResolver.object); + const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, TypeMoq.Mock.ofType().object, []); await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options); const attachPromise = debugClient.attachRequest(options); diff --git a/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts similarity index 81% rename from src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts rename to src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index ebfd95f0162b..0e0d6627806d 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -8,19 +8,20 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; -import { PythonDebugConfigurationProvider } from '../../../../client/debugger/extension/configuration/debugConfigurationProvider'; +import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; +import { IDebugConfigurationPicker } from '../../../../client/debugger/extension/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; suite('xDebugging - Configuration Provider', () => { let attachResolver: typemoq.IMock>; let launchResolver: typemoq.IMock>; - let provider: PythonDebugConfigurationProvider; + let configService: PythonDebugConfigurationService; setup(() => { attachResolver = typemoq.Mock.ofType>(); launchResolver = typemoq.Mock.ofType>(); - provider = new PythonDebugConfigurationProvider(attachResolver.object, launchResolver.object); + configService = new PythonDebugConfigurationService(attachResolver.object, launchResolver.object, typemoq.Mock.ofType().object, []); }); test('Should use attach resolver when passing attach config', async () => { const config = { @@ -37,7 +38,7 @@ suite('xDebugging - Configuration Provider', () => { .setup(a => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); - const resolvedConfig = await provider.resolveDebugConfiguration(folder, config as any); + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as any); expect(resolvedConfig).to.deep.equal(expectedConfig); attachResolver.verifyAll(); @@ -58,7 +59,7 @@ suite('xDebugging - Configuration Provider', () => { .setup(a => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); - const resolvedConfig = await provider.resolveDebugConfiguration(folder, config as any); + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as any); expect(resolvedConfig).to.deep.equal(expectedConfig); attachResolver.verifyAll(); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 32d2965fe775..37740a2cd960 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -9,7 +9,7 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { DebuggerBanner } from '../../../client/debugger/extension/banner'; import { ConfigurationProviderUtils } from '../../../client/debugger/extension/configuration/configurationProviderUtils'; -import { PythonDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/debugConfigurationProvider'; +import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; import { IConfigurationProviderUtils, IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; @@ -17,7 +17,7 @@ import { ChildProcessAttachEventHandler } from '../../../client/debugger/extensi import { ChildProcessAttachService } from '../../../client/debugger/extension/hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../../client/debugger/extension/hooks/types'; import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; -import { IDebugConfigurationProvider, IDebuggerBanner } from '../../../client/debugger/extension/types'; +import { IDebugConfigurationService, IDebuggerBanner } from '../../../client/debugger/extension/types'; import { IServiceManager } from '../../../client/ioc/types'; suite('Debugging - Service Registry', () => { @@ -25,7 +25,7 @@ suite('Debugging - Service Registry', () => { const serviceManager = typemoq.Mock.ofType(); [ - [IDebugConfigurationProvider, PythonDebugConfigurationProvider], + [IDebugConfigurationService, PythonDebugConfigurationService], [IConfigurationProviderUtils, ConfigurationProviderUtils], [IDebuggerBanner, DebuggerBanner], [IChildProcessAttachService, ChildProcessAttachService], From 37a6289bd684e0cf991dfe6428207b7d1818f63a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Dec 2018 13:52:22 -0800 Subject: [PATCH 6/9] Add tests --- package.json | 52 +---- package.nls.json | 39 +++- .../common/application/applicationShell.ts | 8 +- src/client/common/application/types.ts | 24 ++ src/client/common/serviceRegistry.ts | 2 + src/client/common/utils/localize.ts | 42 +++- src/client/common/utils/multiStepInput.ts | 206 ++++++++++++++++++ .../debugConfigurationService.ts | 56 +++-- .../configuration/providers/djangoLaunch.ts | 105 +++++---- .../configuration/providers/fileLaunch.ts | 29 ++- .../configuration/providers/flaskLaunch.ts | 87 ++++---- .../configuration/providers/moduleLaunch.ts | 50 ++--- .../configuration/providers/picker.ts | 29 --- .../providers/providerFactory.ts | 32 +++ .../configuration/providers/pyramidLaunch.ts | 87 ++++++++ .../configuration/providers/remoteAttach.ts | 89 ++++---- .../debugger/extension/configuration/types.ts | 6 + .../debugger/extension/serviceRegistry.ts | 22 +- src/client/debugger/extension/types.ts | 23 +- src/client/debugger/types.ts | 9 +- src/client/telemetry/types.ts | 2 +- .../common/managers/baseTestManager.ts | 5 +- src/test/debugger/attach.ptvsd.test.ts | 10 +- .../debugConfigurationProvider.unit.test.ts | 2 +- .../debugConfigurationService.unit.test.ts | 49 ++++- .../providers/djangoLaunch.unit.test.ts | 199 +++++++++++++++++ .../providers/fileLaunch.unit.test.ts | 37 ++++ .../providers/flaskLaunch.unit.test.ts | 140 ++++++++++++ .../providers/moduleLaunch.unit.test.ts | 59 +++++ .../providers/providerFactory.unit.test.ts | 37 ++++ .../providers/pyramidLaunch.unit.test.ts | 193 ++++++++++++++++ .../providers/remoteAttach.unit.test.ts | 119 ++++++++++ .../extension/serviceRegistry.unit.test.ts | 20 +- 33 files changed, 1530 insertions(+), 339 deletions(-) create mode 100644 src/client/common/utils/multiStepInput.ts delete mode 100644 src/client/debugger/extension/configuration/providers/picker.ts create mode 100644 src/client/debugger/extension/configuration/providers/providerFactory.ts create mode 100644 src/client/debugger/extension/configuration/providers/pyramidLaunch.ts create mode 100644 src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts diff --git a/package.json b/package.json index dfffcc6eeafe..ee4e3f376bcd 100644 --- a/package.json +++ b/package.json @@ -887,57 +887,7 @@ } } } - }, - "initialConfigurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}" - }, - { - "name": "Python: Attach", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost" - }, - { - "name": "Python: Module", - "type": "python", - "request": "launch", - "module": "enter-your-module-name-here", - "console": "integratedTerminal" - }, - { - "name": "Python: Django", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/manage.py", - "console": "integratedTerminal", - "args": [ - "runserver", - "--noreload", - "--nothreading" - ], - "django": true - }, - { - "name": "Python: Flask", - "type": "python", - "request": "launch", - "module": "flask", - "env": { - "FLASK_APP": "app.py" - }, - "args": [ - "run", - "--no-debugger", - "--no-reload" - ], - "jinja": true - } - ] + } } ], "configuration": { diff --git a/package.nls.json b/package.nls.json index e837cb046758..8d8c2362f4c3 100644 --- a/package.nls.json +++ b/package.nls.json @@ -140,17 +140,36 @@ "DataScience.pythonInterruptFailedHeader": "Keyboard interrupt crashed the kernel. Kernel restarted.", "DataScience.sysInfoURILabel": "Jupyter Server URI: ", "Common.loadingPythonExtension": "Python extension loading...", - "debug.selectConfiguration": "Select Debug Configuration", - "debug.debugFileConfigurationLabel": "Debug Python File", - "debug.attachConfigurationLabel": "Attach debugger to remote application", - "debug.debugDjangoConfigurationLabel": "Debug Django", - "debug.debugFlaskConfigurationLabel": "Debug Flask", - "debug.attachRemotePortPlaceholder": "Port", + "debug.selectConfigurationTitle": "Select a debug configuration", + "debug.selectConfigurationPlaceholder": "Debug Configuration", + "debug.debugFileConfigurationLabel": "Python File", + "debug.debugFileConfigurationDescription": "Debug python file", + "debug.debugModuleConfigurationLabel": "Module", + "debug.debugModuleConfigurationDescription": "Debug Python module/package", + "debug.remoteAttachConfigurationLabel": "Remote Attach", + "debug.remoteAttachConfigurationDescription": "Debug a remote python program", + "debug.debugDjangoConfigurationLabel": "Django", + "debug.debugDjangoConfigurationDescription": "Web Application", + "debug.debugFlaskConfigurationLabel": "Flask", + "debug.debugFlaskConfigurationDescription": "Web Application", + "debug.debugPyramidConfigurationLabel": "Pyramid", + "debug.debugPyramidConfigurationDescription": "Web Application", + "debug.djangoEnterManagePyPathTitle": "Debug Django", + "debug.djangoEnterManagePyPathPrompt": "Enter path to manage.py", + "debug.djangoEnterManagePyPathInvalidFilePathError": "Enter a valid python file path", + "debug.flaskEnterAppPathOrNamePathTitle": "Debug Flask", + "debug.flaskEnterAppPathOrNamePathPrompt": "Enter path to Application, e.g. 'app.py' or 'app'", + "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Enter a valid name", + "debug.moduleEnterModuleTitle": "Debug Module", + "debug.moduleEnterModulePrompt": "Enter Python module/package name", + "debug.moduleEnterModuleInvalidNameError": "Enter a valid name", + "debug.pyramidEnterDevelopmentIniPathTitle": "Debug Pyramid", + "debug.pyramidEnterDevelopmentIniPathPrompt": "`Enter path to development.ini ('${workspaceFolderToken}' points to the root of the current workspace folder)`", + "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Enter a valid file path", + "debug.attachRemotePortTitle": "Remote Debugging", "debug.attachRemotePortPrompt": "Enter Port Number", "debug.attachRemotePortValidationError": "Enter a valid Port Number", - "debug.attachRemoteHostPlaceholder": "Host", + "debug.attachRemoteHostTitle": "Remote Debugging", "debug.attachRemoteHostPrompt": "Enter Host Name", - "debug.attachRemoteHostValidationError": "Enter a Host Name", - "debug.debugDjangoSelectManagePyOpenDialogLabel": "Select 'manage.py' file", - "debug.debugFlaskSelectAppOpenDialogLabel": "Select Flask Application file/package" + "debug.attachRemoteHostValidationError": "Enter a Host Name or IP Address" } diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index a66e5c4e9594..ae4ff32cb017 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -6,7 +6,7 @@ const opn = require('opn'); import { injectable } from 'inversify'; -import { CancellationToken, Disposable, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, Progress, ProgressOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { CancellationToken, Disposable, InputBox, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, Progress, ProgressOptions, QuickPick, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { IApplicationShell } from './types'; @injectable() @@ -70,4 +70,10 @@ export class ApplicationShell implements IApplicationShell { public withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable { return window.withProgress(options, task); } + public createQuickPick(): QuickPick { + return window.createQuickPick(); + } + public createInputBox(): InputBox { + return window.createInputBox(); + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index c2f62db37405..f3e342226412 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -15,12 +15,14 @@ import { Event, FileSystemWatcher, GlobPattern, + InputBox, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, Progress, ProgressOptions, + QuickPick, QuickPickItem, QuickPickOptions, SaveDialogOptions, @@ -227,6 +229,28 @@ export interface IApplicationShell { */ showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; + /** + * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list + * of items of type T. + * + * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * + * @return A new [QuickPick](#QuickPick). + */ + createQuickPick(): QuickPick; + + /** + * Creates a [InputBox](#InputBox) to let the user enter some text input. + * + * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) + * is easier to use. [window.createInputBox](#window.createInputBox) should be used + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * + * @return A new [InputBox](#InputBox). + */ + createInputBox(): InputBox; /** * Opens URL in a default browser. * diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index e5c8f59fcac1..fea49c7b2c24 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -62,6 +62,7 @@ import { IRandom, IsWindows } from './types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput'; import { Random } from './utils/random'; export function registerTypes(serviceManager: IServiceManager) { @@ -100,4 +101,5 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IFeatureDeprecationManager, FeatureDeprecationManager); serviceManager.addSingleton(IAsyncDisposableRegistry, AsyncDisposableRegistry); + serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 61b4fa7c7a07..eb98f1e0d4f0 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -106,20 +106,40 @@ export namespace DataScience { } export namespace Debug { - export const selectConfiguration = localize('debug.selectConfiguration', 'Select Debug Configuration'); - export const debugFileConfigurationLabel = localize('debug.debugFileConfigurationLabel', 'Debug Python File'); - export const attachConfigurationLabel = localize('debug.attachConfigurationLabel', 'Attach debugger to remote application'); - export const debugDjangoConfigurationLabel = localize('debug.debugDjangoConfigurationLabel', 'Debug Django'); - export const debugFlaskConfigurationLabel = localize('debug.debugFlaskConfigurationLabel', 'Debug Flask'); - export const attachRemotePortPlaceholder = localize('debug.attachRemotePortPlaceholder', 'Port'); + export const selectConfigurationTitle = localize('debug.selectConfigurationTitle', 'Select a debug configuration'); + export const selectConfigurationPlaceholder = localize('debug.selectConfigurationPlaceholder', 'Debug Configuration'); + export const debugFileConfigurationLabel = localize('debug.debugFileConfigurationLabel', 'Python File'); + export const debugFileConfigurationDescription = localize('debug.debugFileConfigurationDescription', 'Debug python file'); + export const debugModuleConfigurationLabel = localize('debug.debugModuleConfigurationLabel', 'Module'); + export const debugModuleConfigurationDescription = localize('debug.debugModuleConfigurationDescription', 'Debug Python module/package'); + export const remoteAttachConfigurationLabel = localize('debug.remoteAttachConfigurationLabel', 'Remote Attach'); + export const remoteAttachConfigurationDescription = localize('debug.remoteAttachConfigurationDescription', 'Debug a remote python program'); + export const debugDjangoConfigurationLabel = localize('debug.debugDjangoConfigurationLabel', 'Django'); + export const debugDjangoConfigurationDescription = localize('debug.debugDjangoConfigurationDescription', 'Web Application'); + export const debugFlaskConfigurationLabel = localize('debug.debugFlaskConfigurationLabel', 'Flask'); + export const debugFlaskConfigurationDescription = localize('debug.debugFlaskConfigurationDescription', 'Web Application'); + export const debugPyramidConfigurationLabel = localize('debug.debugPyramidConfigurationLabel', 'Pyramid'); + export const debugPyramidConfigurationDescription = localize('debug.debugPyramidConfigurationDescription', 'Web Application'); + export const djangoEnterManagePyPathTitle = localize('debug.djangoEnterManagePyPathTitle', 'Debug Django'); + export const djangoEnterManagePyPathPrompt = localize('debug.djangoEnterManagePyPathPrompt', 'Enter path to manage.py'); + export const djangoEnterManagePyPathInvalidFilePathError = localize('debug.djangoEnterManagePyPathInvalidFilePathError', 'Enter a valid python file path'); + export const flaskEnterAppPathOrNamePathTitle = localize('debug.flaskEnterAppPathOrNamePathTitle', 'Debug Flask'); + export const flaskEnterAppPathOrNamePathPrompt = localize('debug.flaskEnterAppPathOrNamePathPrompt', 'Enter path to Application, e.g. \'app.py\' or \'app\''); + export const flaskEnterAppPathOrNamePathInvalidNameError = localize('debug.flaskEnterAppPathOrNamePathInvalidNameError', 'Enter a valid name'); + + export const moduleEnterModuleTitle = localize('debug.moduleEnterModuleTitle', 'Debug Module'); + export const moduleEnterModulePrompt = localize('debug.moduleEnterModulePrompt', 'Enter Python module/package name'); + export const moduleEnterModuleInvalidNameError = localize('debug.moduleEnterModuleInvalidNameError', 'Enter a valid name'); + export const pyramidEnterDevelopmentIniPathTitle = localize('debug.pyramidEnterDevelopmentIniPathTitle', 'Debug Pyramid'); + // tslint:disable-next-line:no-invalid-template-strings + export const pyramidEnterDevelopmentIniPathPrompt = localize('debug.pyramidEnterDevelopmentIniPathPrompt', '`Enter path to development.ini (\'${workspaceFolderToken}\' points to the root of the current workspace folder)`'); + export const pyramidEnterDevelopmentIniPathInvalidFilePathError = localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError', 'Enter a valid file path'); + export const attachRemotePortTitle = localize('debug.attachRemotePortTitle', 'Remote Debugging'); export const attachRemotePortPrompt = localize('debug.attachRemotePortPrompt', 'Enter Port Number'); export const attachRemotePortValidationError = localize('debug.attachRemotePortValidationError', 'Enter a valid Port Number'); - - export const attachRemoteHostPlaceholder = localize('debug.attachRemoteHostPlaceholder', 'Host'); + export const attachRemoteHostTitle = localize('debug.attachRemoteHostTitle', 'Remote Debugging'); export const attachRemoteHostPrompt = localize('debug.attachRemoteHostPrompt', 'Enter Host Name'); - export const attachRemoteHostValidationError = localize('debug.attachRemoteHostValidationError', 'Enter a Host Name'); - export const debugDjangoSelectManagePyOpenDialogLabel = localize('debug.debugDjangoSelectManagePyOpenDialogLabel', 'Select \'manage.py\' file'); - export const debugFlaskSelectAppOpenDialogLabel = localize('debug.debugFlaskSelectAppOpenDialogLabel', 'Select Flask Application file/package'); + export const attachRemoteHostValidationError = localize('debug.attachRemoteHostValidationError', 'Enter a Host Name or IP Address'); } // Skip using vscode-nls and instead just compute our strings based on key values. Key values diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts new file mode 100644 index 000000000000..90e3a6192a8a --- /dev/null +++ b/src/client/common/utils/multiStepInput.ts @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-unnecessary-class + +import { inject, injectable } from 'inversify'; +import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPickItem } from 'vscode'; +import { IApplicationShell } from '../application/types'; + +// Borrowed from https://github.com/Microsoft/vscode-extension-samples/blob/master/quickinput-sample/src/multiStepInput.ts +// Why re-invent the wheel :) + +export class InputFlowAction { + public static back = new InputFlowAction(); + public static cancel = new InputFlowAction(); + public static resume = new InputFlowAction(); + private constructor() { } +} + +export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; + +export interface IQuickPickParameters { + title: string; + step?: number; + totalSteps?: number; + canGoBack?: boolean; + items: T[]; + activeItem?: T; + placeholder: string; + buttons?: QuickInputButton[]; + shouldResume?(): Promise; +} + +export interface InputBoxParameters { + title: string; + step?: number; + totalSteps?: number; + value: string; + prompt: string; + buttons?: QuickInputButton[]; + validate(value: string): Promise; + shouldResume?(): Promise; +} + +type MultiStepInputQuickPicResponseType = T | (P extends { buttons: (infer I)[] } ? I : never); +type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never); +export interface IMultiStepInput { + run(start: InputStep, state: S): Promise; + showQuickPick>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume }: P): Promise>; + showInputBox

({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P): Promise>; +} + +export class MultiStepInput implements IMultiStepInput { + private current?: QuickInput; + private steps: InputStep[] = []; + constructor(private readonly shell: IApplicationShell) { } + public run(start: InputStep, state: S) { + return this.stepThrough(start, state); + } + + public async showQuickPick>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume }: P): Promise> { + const disposables: Disposable[] = []; + try { + return await new Promise>((resolve, reject) => { + const input = this.shell.createQuickPick(); + input.title = title; + input.step = step; + input.totalSteps = totalSteps; + input.placeholder = placeholder; + input.ignoreFocusOut = true; + input.items = items; + if (activeItem) { + input.activeItems = [activeItem]; + } + input.buttons = [ + ...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), + ...(buttons || []) + ]; + disposables.push( + input.onDidTriggerButton(item => { + if (item === QuickInputButtons.Back) { + reject(InputFlowAction.back); + } else { + resolve(item); + } + }), + input.onDidChangeSelection(selectedItems => resolve(selectedItems[0])), + input.onDidHide(() => { + (async () => { + reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); + })() + .catch(reject); + }) + ); + if (this.current) { + this.current.dispose(); + } + this.current = input; + this.current.show(); + }); + } finally { + disposables.forEach(d => d.dispose()); + } + } + + public async showInputBox

({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P): Promise> { + const disposables: Disposable[] = []; + try { + return await new Promise>((resolve, reject) => { + const input = this.shell.createInputBox(); + input.title = title; + input.step = step; + input.totalSteps = totalSteps; + input.value = value || ''; + input.prompt = prompt; + input.ignoreFocusOut = true; + input.buttons = [ + ...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), + ...(buttons || []) + ]; + let validating = validate(''); + disposables.push( + input.onDidTriggerButton(item => { + if (item === QuickInputButtons.Back) { + reject(InputFlowAction.back); + } else { + resolve(item); + } + }), + input.onDidAccept(async () => { + const inputValue = input.value; + input.enabled = false; + input.busy = true; + if (!(await validate(inputValue))) { + resolve(inputValue); + } + input.enabled = true; + input.busy = false; + }), + input.onDidChangeValue(async text => { + const current = validate(text); + validating = current; + const validationMessage = await current; + if (current === validating) { + input.validationMessage = validationMessage; + } + }), + input.onDidHide(() => { + (async () => { + reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); + })() + .catch(reject); + }) + ); + if (this.current) { + this.current.dispose(); + } + this.current = input; + this.current.show(); + }); + } finally { + disposables.forEach(d => d.dispose()); + } + } + + private async stepThrough(start: InputStep, state: S) { + let step: InputStep | void = start; + while (step) { + this.steps.push(step); + if (this.current) { + this.current.enabled = false; + this.current.busy = true; + } + try { + step = await step(this, state); + } catch (err) { + if (err === InputFlowAction.back) { + this.steps.pop(); + step = this.steps.pop(); + } else if (err === InputFlowAction.resume) { + step = this.steps.pop(); + } else if (err === InputFlowAction.cancel) { + step = undefined; + } else { + throw err; + } + } + } + if (this.current) { + this.current.dispose(); + } + } +} +export const IMultiStepInputFactory = Symbol('IMultiStepInputFactory'); +export interface IMultiStepInputFactory { + create(): IMultiStepInput; +} +@injectable() +export class MultiStepInputFactory { + constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) { } + public create(): IMultiStepInput { + return new MultiStepInput(this.shell); + } +} diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index 038fd4e3db9a..4196c68ac55c 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -3,33 +3,27 @@ 'use strict'; -import { inject, injectable, multiInject, named } from 'inversify'; -import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { IDebugConfigurationPicker, IDebugConfigurationProvider, IDebugConfigurationService } from '../types'; -import { IDebugConfigurationResolver } from './types'; +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; +import { Debug } from '../../../common/utils/localize'; +import { IMultiStepInput, IMultiStepInputFactory, InputStep, IQuickPickParameters } from '../../../common/utils/multiStepInput'; +import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types'; +import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from './types'; @injectable() export class PythonDebugConfigurationService implements IDebugConfigurationService { constructor(@inject(IDebugConfigurationResolver) @named('attach') private readonly attachResolver: IDebugConfigurationResolver, @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, - @inject(IDebugConfigurationPicker) private readonly picker: IDebugConfigurationPicker, - @multiInject(IDebugConfigurationProvider) private readonly providers: IDebugConfigurationProvider[]) { + @inject(IDebugConfigurationProviderFactory) private readonly providerFactory: IDebugConfigurationProviderFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory) { } public async provideDebugConfigurations?(folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise { - const debugConfigType = await this.picker.getSelectedConfiguration(folder, token); - if (!debugConfigType) { - return; - } - const providers = this.providers.filter(p => p.isSupported(debugConfigType)); - if (providers.length === 0) { - return; - } - - const configs = await Promise.all(providers.map(provider => provider.provideDebugConfigurations(folder, token))); - // tslint:disable-next-line:no-require-imports no-var-requires - const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); - return flatten(configs.filter(item => Array.isArray(item)).map(item => item!)); + const config: Partial = {}; + const state = { config, folder, token }; + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); + return state.config as DebugConfiguration[]; } public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { if (debugConfiguration.request === 'attach') { @@ -38,4 +32,26 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); } } + protected async pickDebugConfiguration(input: IMultiStepInput, state: DebugConfigurationState): Promise | void> { + type DebugConfigurationQuickPickItem = QuickPickItem & { type: DebugConfigurationType }; + const items: DebugConfigurationQuickPickItem[] = [ + { label: Debug.debugFileConfigurationLabel(), type: DebugConfigurationType.launchFile, description: Debug.debugFileConfigurationDescription() }, + { label: Debug.debugModuleConfigurationLabel(), type: DebugConfigurationType.launchModule, description: Debug.debugModuleConfigurationDescription() }, + { label: Debug.remoteAttachConfigurationLabel(), type: DebugConfigurationType.remoteAttach, description: Debug.remoteAttachConfigurationDescription() }, + { label: Debug.debugDjangoConfigurationLabel(), type: DebugConfigurationType.launchDjango, description: Debug.debugDjangoConfigurationDescription() }, + { label: Debug.debugFlaskConfigurationLabel(), type: DebugConfigurationType.launchFlask, description: Debug.debugFlaskConfigurationDescription() }, + { label: Debug.debugPyramidConfigurationLabel(), type: DebugConfigurationType.launchPyramid, description: Debug.debugPyramidConfigurationDescription() } + ]; + state.config = {}; + const pick = await input.showQuickPick>({ + title: Debug.selectConfigurationTitle(), + placeholder: Debug.selectConfigurationPlaceholder(), + activeItem: items[0], + items: items + }); + if (pick) { + const provider = this.providerFactory.create(pick.type); + return provider.buildConfiguration.bind(provider); + } + } } diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts index a0c8f553fd62..06d0571c50c2 100644 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts @@ -5,65 +5,82 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { CancellationToken, DebugConfiguration, OpenDialogOptions, WorkspaceFolder } from 'vscode'; -import { IApplicationShell } from '../../../../common/application/types'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; import { IFileSystem } from '../../../../common/platform/types'; import { IPathUtils } from '../../../../common/types'; import { Debug, localize } from '../../../../common/utils/localize'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; // tslint:disable-next-line:no-invalid-template-strings const workspaceFolderToken = '${workspaceFolder}'; @injectable() export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IApplicationShell) private shell: IApplicationShell, - @inject(IFileSystem) private fs: IFileSystem, + constructor(@inject(IFileSystem) private fs: IFileSystem, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, @inject(IPathUtils) private pathUtils: IPathUtils) { } - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchDjango; - } - public async provideDebugConfigurations(folder: WorkspaceFolder, token?: CancellationToken): Promise { - const program = await this.getManagePyPath(folder); - return [ - { - name: localize('python.snippet.launch.django.label', 'Python: Django')(), - type: 'python', - request: 'launch', - program: program, - args: [ - 'runserver', - '--noreload', - '--nothreading' - ], - django: true + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const program = await this.getManagePyPath(state.folder); + + const defaultProgram = `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; + const config: Partial = { + name: localize('python.snippet.launch.django.label', 'Python: Django')(), + type: DebuggerTypeName, + request: 'launch', + program: program || defaultProgram, + args: [ + 'runserver', + '--noreload', + '--nothreading' + ], + django: true + }; + if (!program) { + const selectedProgram = await input.showInputBox({ + title: Debug.djangoEnterManagePyPathTitle(), + value: defaultProgram, + prompt: Debug.djangoEnterManagePyPathPrompt(), + validate: value => this.validateManagePy(state.folder, defaultProgram, value) + }); + if (selectedProgram) { + config.program = selectedProgram; } - ]; - } - protected async getManagePyPath(folder: WorkspaceFolder): Promise { - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); - if (await this.fs.fileExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; } - const options: OpenDialogOptions = { - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - defaultUri: folder.uri, - filters: { Python: ['py'] }, - openLabel: Debug.debugDjangoSelectManagePyOpenDialogLabel() - }; - const files = await this.shell.showOpenDialog(options); - if (!Array.isArray(files) || files.length !== 1) { - return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; + Object.assign(state.config, config); + } + public async validateManagePy(folder: WorkspaceFolder | undefined, defaultValue: string, selected?: string): Promise { + const error = Debug.djangoEnterManagePyPathInvalidFilePathError(); + if (!selected || selected.trim().length === 0) { + return error; + } + const resolvedPath = this.resolveVariables(selected, folder ? folder.uri : undefined); + if (selected !== defaultValue && !await this.fs.fileExists(resolvedPath)) { + return error; + } + if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { + return error; } + return; + } + protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { + const workspaceFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; + const systemVariables = new SystemVariables(workspaceFolder ? workspaceFolder.uri.fsPath : undefined); + return systemVariables.resolveAny(pythonPath); + } - const managePy = files[0].fsPath; - const relativePath = path.relative(folder.uri.fsPath, folder.uri.fsPath); - if (relativePath.startsWith('..') || relativePath.startsWith(this.pathUtils.separator)) { - return managePy; + protected async getManagePyPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; } - return `${workspaceFolderToken}${this.pathUtils.separator}${relativePath}`; } } diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts index 86b8c822d163..d5fdf38405c4 100644 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -4,24 +4,23 @@ 'use strict'; import { injectable } from 'inversify'; -import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; import { localize } from '../../../../common/utils/localize'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; @injectable() export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchFile; - } - public async provideDebugConfigurations(_folder: WorkspaceFolder, _token?: CancellationToken): Promise { - return [ - { - name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), - type: 'python', - request: 'launch', - // tslint:disable-next-line:no-invalid-template-strings - program: '${file}' - } - ]; + public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { + const config: Partial = { + name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), + type: DebuggerTypeName, + request: 'launch', + // tslint:disable-next-line:no-invalid-template-strings + program: '${file}', + console: 'integratedTerminal' + }; + Object.assign(state.config, config); } } diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts index be315421ad8d..b1584c38e7c6 100644 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -5,58 +5,61 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { CancellationToken, DebugConfiguration, OpenDialogOptions, WorkspaceFolder } from 'vscode'; -import { IApplicationShell } from '../../../../common/application/types'; +import { WorkspaceFolder } from 'vscode'; import { IFileSystem } from '../../../../common/platform/types'; import { Debug, localize } from '../../../../common/utils/localize'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; @injectable() export class FlaskLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IApplicationShell) private shell: IApplicationShell, - @inject(IFileSystem) private fs: IFileSystem) { } - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchFlask; - } - public async provideDebugConfigurations(folder: WorkspaceFolder, token?: CancellationToken): Promise { - const application = await this.getApplicationPath(folder); - return [ - { - name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), - type: 'python', - request: 'launch', - module: 'flask', - env: { - FLASK_APP: application - }, - args: [ - 'run', - '--no-debugger', - '--no-reload' - ], - jinja: true + constructor(@inject(IFileSystem) private fs: IFileSystem) { } + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.launchFlask; + } + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const application = await this.getApplicationPath(state.folder); + const config: Partial = { + name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: application || 'app.py', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: [ + 'run', + '--no-debugger', + '--no-reload' + ], + jinja: true + }; + + if (!application) { + const selectedApp = await input.showInputBox({ + title: Debug.flaskEnterAppPathOrNamePathTitle(), + value: 'app.py', + prompt: Debug.debugFlaskConfigurationDescription(), + validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : Debug.flaskEnterAppPathOrNamePathInvalidNameError()) + }); + if (selectedApp) { + config.env!.FLASK_APP = selectedApp; } - ]; + } + + Object.assign(state.config, config); } - protected async getApplicationPath(folder: WorkspaceFolder): Promise { + protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); if (await this.fs.fileExists(defaultLocationOfManagePy)) { return 'app.py'; } - - const options: OpenDialogOptions = { - canSelectFiles: true, - canSelectFolders: true, - canSelectMany: false, - defaultUri: folder.uri, - filters: { Python: ['py'] }, - openLabel: Debug.debugFlaskSelectAppOpenDialogLabel() - }; - const selections = await this.shell.showOpenDialog(options); - if (!Array.isArray(selections) || selections.length !== 1) { - return 'app.py'; - } - - return path.basename(selections[0].fsPath); } } diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts index d8ab34f3e3ff..a299dfa8b705 100644 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts @@ -3,37 +3,31 @@ 'use strict'; -import { inject, injectable } from 'inversify'; -import { CancellationToken, DebugConfiguration, InputBoxOptions, WorkspaceFolder } from 'vscode'; -import { IApplicationShell } from '../../../../common/application/types'; -import { localize } from '../../../../common/utils/localize'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { injectable } from 'inversify'; +import { Debug, localize } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; @injectable() export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IApplicationShell) private shell: IApplicationShell) { } - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchModule; - } - public async provideDebugConfigurations(_folder: WorkspaceFolder, token?: CancellationToken): Promise { - const moduleName = await this.getModuleName(token); - return [ - { - name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), - type: 'python', - request: 'launch', - module: moduleName - } - ]; - } - protected async getModuleName(token): Promise { - const options: InputBoxOptions = { - ignoreFocusOut: false, - placeHolder: 'my.module', - prompt: 'Enter Python Module/Package name', - value: '' + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const config: Partial = { + name: localize('python.snippet.launch.module.label', 'Python: Module')(), + type: DebuggerTypeName, + request: 'launch', + module: 'enter-your-module-name-here' }; - return this.shell.showInputBox(options, token); + const selectedModule = await input.showInputBox({ + title: Debug.moduleEnterModuleTitle(), + value: config.module || 'enter-your-module-name-here', + prompt: Debug.moduleEnterModulePrompt(), + validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : Debug.moduleEnterModuleInvalidNameError()) + }); + if (selectedModule) { + config.module = selectedModule; + } + Object.assign(state.config, config); } - } diff --git a/src/client/debugger/extension/configuration/providers/picker.ts b/src/client/debugger/extension/configuration/providers/picker.ts deleted file mode 100644 index 2e220e8b8dd2..000000000000 --- a/src/client/debugger/extension/configuration/providers/picker.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { CancellationToken, QuickPickItem, QuickPickOptions, WorkspaceFolder } from 'vscode'; -import { IApplicationShell } from '../../../../common/application/types'; -import { Debug } from '../../../../common/utils/localize'; -import { DebugConfigurationType, IDebugConfigurationPicker } from '../../types'; - -type OptionItem = QuickPickItem & { type: DebugConfigurationType }; -@injectable() -export class DebugConfigurationPicker implements IDebugConfigurationPicker { - constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) { } - - public async getSelectedConfiguration(_folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise { - const items: OptionItem[] = [ - { label: Debug.debugFileConfigurationLabel(), type: DebugConfigurationType.launchFile, description: 'hello there', detail: 'details' }, - { label: Debug.attachConfigurationLabel(), type: DebugConfigurationType.remoteAttach, description: 'hello there', detail: 'details' }, - { label: Debug.debugDjangoConfigurationLabel(), type: DebugConfigurationType.launchDjango, description: 'hello there', detail: 'details' }, - { label: Debug.debugFlaskConfigurationLabel(), type: DebugConfigurationType.launchFlask, description: 'hello there', detail: 'details' }, - { label: 'Module', type: DebugConfigurationType.launchFlask, description: 'Debug Python module/package', detail: 'Debug a python module invoking it with `-m`' } - ]; - const options: QuickPickOptions = { ignoreFocusOut: true, matchOnDescription: true, matchOnDetail: true, placeHolder: Debug.selectConfiguration() }; - const selection = await this.shell.showQuickPick(items, options, token); - return selection ? selection.type : undefined; - } -} diff --git a/src/client/debugger/extension/configuration/providers/providerFactory.ts b/src/client/debugger/extension/configuration/providers/providerFactory.ts new file mode 100644 index 000000000000..61f808d1e9e1 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/providerFactory.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { IDebugConfigurationProviderFactory } from '../types'; + +@injectable() +export class DebugConfigurationProviderFactory implements IDebugConfigurationProviderFactory { + private readonly providers: Map; + constructor( + @inject(IDebugConfigurationProvider) @named(DebugConfigurationType.launchFlask) flaskProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) @named(DebugConfigurationType.launchDjango) djangoProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) @named(DebugConfigurationType.launchModule) moduleProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) @named(DebugConfigurationType.launchFile) fileProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) @named(DebugConfigurationType.launchPyramid) pyramidProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) @named(DebugConfigurationType.remoteAttach) remoteAttachProvider: IDebugConfigurationProvider + ) { + this.providers = new Map(); + this.providers.set(DebugConfigurationType.launchDjango, djangoProvider); + this.providers.set(DebugConfigurationType.launchFlask, flaskProvider); + this.providers.set(DebugConfigurationType.launchFile, fileProvider); + this.providers.set(DebugConfigurationType.launchModule, moduleProvider); + this.providers.set(DebugConfigurationType.launchPyramid, pyramidProvider); + this.providers.set(DebugConfigurationType.remoteAttach, remoteAttachProvider); + } + public create(configurationType: DebugConfigurationType): IDebugConfigurationProvider { + return this.providers.get(configurationType)!; + } +} diff --git a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts new file mode 100644 index 000000000000..0353c97c9f1d --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IPathUtils } from '../../../../common/types'; +import { Debug, localize } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; + +// tslint:disable-next-line:no-invalid-template-strings +const workspaceFolderToken = '${workspaceFolder}'; + +@injectable() +export class PyramidLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor(@inject(IFileSystem) private fs: IFileSystem, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPathUtils) private pathUtils: IPathUtils) { } + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const iniPath = await this.getDevelopmentIniPath(state.folder); + const defaultIni = `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; + + const config: Partial = { + name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), + type: DebuggerTypeName, + request: 'launch', + args: [ + iniPath || defaultIni + ], + pyramid: true, + jinja: true + }; + + if (!iniPath) { + const selectedIniPath = await input.showInputBox({ + title: Debug.pyramidEnterDevelopmentIniPathTitle(), + value: defaultIni, + prompt: Debug.pyramidEnterDevelopmentIniPathPrompt(), + validate: value => this.validateIniPath(state ? state.folder : undefined, defaultIni, value) + }); + if (selectedIniPath) { + config.args = [selectedIniPath]; + } + } + + Object.assign(state.config, config); + } + public async validateIniPath(folder: WorkspaceFolder | undefined, defaultValue: string, selected?: string): Promise { + if (!folder) { + return; + } + const error = Debug.pyramidEnterDevelopmentIniPathInvalidFilePathError(); + if (!selected || selected.trim().length === 0) { + return error; + } + const resolvedPath = this.resolveVariables(selected, folder.uri); + if (selected !== defaultValue && !await this.fs.fileExists(resolvedPath)) { + return error; + } + if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { + return error; + } + } + protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { + const workspaceFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; + const systemVariables = new SystemVariables(workspaceFolder ? workspaceFolder.uri.fsPath : undefined); + return systemVariables.resolveAny(pythonPath); + } + + protected async getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; + } + } +} diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts index 1e5bc719e24b..ddf532e101fd 100644 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ b/src/client/debugger/extension/configuration/providers/remoteAttach.ts @@ -3,56 +3,53 @@ 'use strict'; -import { inject, injectable } from 'inversify'; -import { CancellationToken, DebugConfiguration, InputBoxOptions, WorkspaceFolder } from 'vscode'; -import { IApplicationShell } from '../../../../common/application/types'; +import { injectable } from 'inversify'; import { Debug, localize } from '../../../../common/utils/localize'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../constants'; +import { AttachRequestArguments } from '../../../types'; +import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; @injectable() -export class AttachDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IApplicationShell) private shell: IApplicationShell) { } - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.remoteAttach; - } - public async provideDebugConfigurations(_folder: WorkspaceFolder, token?: CancellationToken): Promise { - const host = await this.getHost(token); - const port = await this.getPort(token); - return [ - { - name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), - type: 'python', - request: 'attach', - port: port, - host: host - } - ]; - } - protected async getHost(token?: CancellationToken): Promise { - const validateHost = (selection?: string | undefined) => { - return (selection && selection.trim().length > 0) ? undefined : Debug.attachRemoteHostValidationError(); +export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurationProvider { + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState): Promise | void> { + const config: Partial = { + name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), + type: DebuggerTypeName, + request: 'attach', + port: 5678, + host: 'localhost' }; - const options: InputBoxOptions = { - placeHolder: Debug.attachRemoteHostPlaceholder(), - value: 'localhost', - validateInput: validateHost, - ignoreFocusOut: true, - prompt: Debug.attachRemoteHostPrompt() - }; - return this.shell.showInputBox(options, token); + + config.host = await input.showInputBox({ + title: Debug.attachRemoteHostTitle(), + step: 1, + totalSteps: 2, + value: config.host || 'localhost', + prompt: Debug.attachRemoteHostPrompt(), + validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : Debug.attachRemoteHostValidationError()) + }); + if (!config.host) { + config.host = 'localhost'; + } + + Object.assign(state.config, config); + return _ => this.configurePort(input, state.config); } - protected async getPort(token?: CancellationToken): Promise { - const validatePort = (selection?: string | undefined) => { - return (selection && /^\d+$/.test(selection.trim())) ? undefined : Debug.attachRemotePortValidationError(); - }; - const options: InputBoxOptions = { - placeHolder: Debug.attachRemotePortPlaceholder(), - value: '5678', - validateInput: validatePort, - ignoreFocusOut: true, - prompt: Debug.attachRemotePortPrompt() - }; - const port = await this.shell.showInputBox(options, token); - return port ? parseInt(port.trim(), 10) : undefined; + protected async configurePort(input: MultiStepInput, config: Partial) { + const port = await input.showInputBox({ + title: Debug.attachRemotePortTitle(), + step: 2, + totalSteps: 2, + value: (config.port || 5678).toString(), + prompt: Debug.attachRemotePortPrompt(), + validate: value => Promise.resolve((value && /^\d+$/.test(value.trim())) ? undefined : Debug.attachRemotePortValidationError()) + }); + if (port && /^\d+$/.test(port.trim())) { + config.port = parseInt(port, 10); + } + if (!config.port) { + config.port = 5678; + } } } diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts index db8ba18046c4..8a5a7473249c 100644 --- a/src/client/debugger/extension/configuration/types.ts +++ b/src/client/debugger/extension/configuration/types.ts @@ -4,6 +4,7 @@ 'use strict'; import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../types'; export const IConfigurationProviderUtils = Symbol('IConfigurationProviderUtils'); @@ -15,3 +16,8 @@ export const IDebugConfigurationResolver = Symbol('IDebugConfigurationResolver') export interface IDebugConfigurationResolver { resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: T, token?: CancellationToken): Promise; } + +export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); +export interface IDebugConfigurationProviderFactory { + create(configurationType: DebugConfigurationType): IDebugConfigurationProvider; +} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 0cbe435d41be..a3fed3aab68e 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -12,15 +12,16 @@ import { DjangoLaunchDebugConfigurationProvider } from './configuration/provider import { FileLaunchDebugConfigurationProvider } from './configuration/providers/fileLaunch'; import { FlaskLaunchDebugConfigurationProvider } from './configuration/providers/flaskLaunch'; import { ModuleLaunchDebugConfigurationProvider } from './configuration/providers/moduleLaunch'; -import { DebugConfigurationPicker } from './configuration/providers/picker'; -import { AttachDebugConfigurationProvider } from './configuration/providers/remoteAttach'; +import { DebugConfigurationProviderFactory } from './configuration/providers/providerFactory'; +import { PyramidLaunchDebugConfigurationProvider } from './configuration/providers/pyramidLaunch'; +import { RemoteAttachDebugConfigurationProvider } from './configuration/providers/remoteAttach'; import { AttachConfigurationResolver } from './configuration/resolvers/attach'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; -import { IConfigurationProviderUtils, IDebugConfigurationResolver } from './configuration/types'; +import { IConfigurationProviderUtils, IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from './configuration/types'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; -import { IDebugConfigurationPicker, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from './types'; +import { DebugConfigurationType, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugConfigurationService, PythonDebugConfigurationService); @@ -30,10 +31,11 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>(IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'); serviceManager.addSingleton>(IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'); - serviceManager.addSingleton(IDebugConfigurationPicker, DebugConfigurationPicker); - serviceManager.addSingleton(IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider); - serviceManager.addSingleton(IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider); - serviceManager.addSingleton(IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider); - serviceManager.addSingleton(IDebugConfigurationProvider, AttachDebugConfigurationProvider); - serviceManager.addSingleton(IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider); + serviceManager.addSingleton(IDebugConfigurationProviderFactory, DebugConfigurationProviderFactory); + serviceManager.addSingleton(IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider, DebugConfigurationType.launchFile); + serviceManager.addSingleton(IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider, DebugConfigurationType.launchDjango); + serviceManager.addSingleton(IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider, DebugConfigurationType.launchFlask); + serviceManager.addSingleton(IDebugConfigurationProvider, RemoteAttachDebugConfigurationProvider, DebugConfigurationType.remoteAttach); + serviceManager.addSingleton(IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider, DebugConfigurationType.launchModule); + serviceManager.addSingleton(IDebugConfigurationProvider, PyramidLaunchDebugConfigurationProvider, DebugConfigurationType.launchPyramid); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index a2a2cf30d798..63d8691252ff 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,7 +3,9 @@ 'use strict'; -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, WorkspaceFolder } from 'vscode'; +import { CancellationToken, DebugConfigurationProvider, WorkspaceFolder } from 'vscode'; +import { InputStep, MultiStepInput } from '../../common/utils/multiStepInput'; +import { DebugConfigurationArguments } from '../types'; export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); export interface IDebugConfigurationService extends DebugConfigurationProvider { } @@ -13,21 +15,9 @@ export interface IDebuggerBanner { } export const IDebugConfigurationProvider = Symbol('IDebugConfigurationProvider'); +export type DebugConfigurationState = { config: Partial; folder?: WorkspaceFolder; token?: CancellationToken }; export interface IDebugConfigurationProvider { - isSupported(debugConfigurationType: DebugConfigurationType): boolean; - /** - * Provides initial [debug configuration](#DebugConfiguration). If more than one debug configuration provider is - * registered for the same type, debug configurations are concatenated in arbitrary order. - * - * @param folder The workspace folder for which the configurations are used or undefined for a folderless setup. - * @param token A cancellation token. - * @return An array of [debug configurations](#DebugConfiguration). - */ - provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; -} -export const IDebugConfigurationPicker = Symbol('IDebugConfigurationPicker'); -export interface IDebugConfigurationPicker { - getSelectedConfiguration(folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise; + buildConfiguration(input: MultiStepInput, state: DebugConfigurationState): Promise | void>; } export enum DebugConfigurationType { @@ -35,5 +25,6 @@ export enum DebugConfigurationType { remoteAttach = 'remoteAttach', launchDjango = 'launchDjango', launchFlask = 'launchFlask', - launchModule = 'launchModule' + launchModule = 'launchModule', + launchPyramid = 'launchPyramid' } diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 3c7ccb2f2f35..50eec0f94a92 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -58,9 +58,9 @@ export interface IKnownLaunchRequestArguments extends ICommonDebugArguments { args: string[]; cwd?: string; debugOptions?: DebugOptions[]; - env?: Object; + env?: { [key: string]: string | undefined }; envFile: string; - console?: 'none' | 'integratedTerminal' | 'externalTerminal'; + console?: ConsoleType; } // tslint:disable-next-line:interface-name export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, IKnownLaunchRequestArguments, DebugConfiguration { @@ -71,3 +71,8 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments, IKnownAttachDebugArguments, DebugConfiguration { type: typeof DebuggerTypeName; } + +// tslint:disable-next-line:interface-name +export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments { } + +export type ConsoleType = 'none' | 'integratedTerminal' | 'externalTerminal'; diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index 2ca12126e513..67c03e35a896 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -72,7 +72,7 @@ export type TestRunTelemetry = { tool: 'nosetest' | 'pytest' | 'unittest'; scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; debugging: boolean; - triggeredBy: 'ui' | 'codelens' | 'commandpalette' | 'auto'; + triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto'; failed: boolean; }; export type TestDiscoverytTelemetry = { diff --git a/src/client/unittests/common/managers/baseTestManager.ts b/src/client/unittests/common/managers/baseTestManager.ts index 89a54b12e40e..9fc61c5f9126 100644 --- a/src/client/unittests/common/managers/baseTestManager.ts +++ b/src/client/unittests/common/managers/baseTestManager.ts @@ -2,6 +2,7 @@ import { CancellationToken, CancellationTokenSource, Disposable, OutputChannel, import { IWorkspaceService } from '../../../common/application/types'; import { isNotInstalledError } from '../../../common/helpers'; import { IConfigurationService, IDisposableRegistry, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../../common/types'; +import { getNamesAndValues } from '../../../common/utils/enum'; import { IServiceContainer } from '../../../ioc/types'; import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry/index'; @@ -171,11 +172,13 @@ export abstract class BaseTestManager implements ITestManager { Run_Specific_Class: 'false', Run_Specific_Function: 'false' }; + //Ensure valid values are sent. + const validCmdSourceValues = getNamesAndValues(CommandSource).map(item => item.value); const telementryProperties: TestRunTelemetry = { tool: this.testProvider, scope: 'all', debugging: debug === true, - triggeredBy: cmdSource, + triggerSource: validCmdSourceValues.indexOf(cmdSource) === -1 ? 'commandpalette' : cmdSource, failed: false }; if (runFailedTests === true) { diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index a08bbafebefc..576a90ac7329 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -14,11 +14,11 @@ import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; import { IS_WINDOWS } from '../../client/common/platform/constants'; import { IPlatformService } from '../../client/common/platform/types'; import { IConfigurationService } from '../../client/common/types'; +import { IMultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; import { PythonDebugConfigurationService } from '../../client/debugger/extension/configuration/debugConfigurationService'; import { AttachConfigurationResolver } from '../../client/debugger/extension/configuration/resolvers/attach'; -import { IDebugConfigurationResolver } from '../../client/debugger/extension/configuration/types'; -import { IDebugConfigurationPicker } from '../../client/debugger/extension/types'; +import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from '../../client/debugger/extension/configuration/types'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; import { IServiceContainer } from '../../client/ioc/types'; import { PYTHON_PATH, sleep } from '../common'; @@ -28,7 +28,7 @@ import { continueDebugging, createDebugAdapter } from './utils'; // tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement no-unused-variable no-console const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); -suite('Attach Debugger', () => { +suite('Debugging - Attach Debugger', () => { let debugClient: DebugClient; let proc: ChildProcess; @@ -97,7 +97,9 @@ suite('Attach Debugger', () => { const launchResolver = TypeMoq.Mock.ofType>(); const attachResolver = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); - const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, TypeMoq.Mock.ofType().object, []); + const providerFactory = TypeMoq.Mock.ofType().object; + const multiStepIput = TypeMoq.Mock.ofType().object; + const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, providerFactory, multiStepIput); await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options); const attachPromise = debugClient.attachRequest(options); diff --git a/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts index ebfd95f0162b..c0dd996fbf8e 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationProvider.unit.test.ts @@ -12,7 +12,7 @@ import { PythonDebugConfigurationProvider } from '../../../../client/debugger/ex import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; -suite('xDebugging - Configuration Provider', () => { +suite('Debugging - Configuration Provider', () => { let attachResolver: typemoq.IMock>; let launchResolver: typemoq.IMock>; let provider: PythonDebugConfigurationProvider; diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 0e0d6627806d..2dccfa8619e5 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -6,22 +6,36 @@ // tslint:disable:no-any import { expect } from 'chai'; +import { instance, mock } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; +import { IMultiStepInput, IMultiStepInputFactory } from '../../../../client/common/utils/multiStepInput'; import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; +import { DebugConfigurationProviderFactory } from '../../../../client/debugger/extension/configuration/providers/providerFactory'; import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; -import { IDebugConfigurationPicker } from '../../../../client/debugger/extension/types'; +import { DebugConfigurationState } from '../../../../client/debugger/extension/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; -suite('xDebugging - Configuration Provider', () => { +// tslint:disable-next-line:max-func-body-length +suite('Debugging - Configuration Provider', () => { let attachResolver: typemoq.IMock>; let launchResolver: typemoq.IMock>; - let configService: PythonDebugConfigurationService; + let configService: TestPythonDebugConfigurationService; + let multiStepFactory: typemoq.IMock; + let providerFactory: DebugConfigurationProviderFactory; + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { + // tslint:disable-next-line:no-unnecessary-override + public async pickDebugConfiguration(input: IMultiStepInput, state: DebugConfigurationState) { + return super.pickDebugConfiguration(input, state); + } + } setup(() => { attachResolver = typemoq.Mock.ofType>(); launchResolver = typemoq.Mock.ofType>(); - configService = new PythonDebugConfigurationService(attachResolver.object, launchResolver.object, typemoq.Mock.ofType().object, []); + multiStepFactory = typemoq.Mock.ofType(); + providerFactory = mock(DebugConfigurationProviderFactory); + configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object, instance(providerFactory), multiStepFactory.object); }); test('Should use attach resolver when passing attach config', async () => { const config = { @@ -66,4 +80,31 @@ suite('xDebugging - Configuration Provider', () => { launchResolver.verifyAll(); }); }); + test('Picker should be displayed', async () => { + // tslint:disable-next-line:no-object-literal-type-assertion + const state = { configs: [], folder: {}, token: undefined } as any as DebugConfigurationState; + const multStepInput = typemoq.Mock.ofType>(); + multStepInput + .setup(i => i.showQuickPick(typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(typemoq.Times.once()); + + await configService.pickDebugConfiguration(multStepInput.object, state); + + multStepInput.verifyAll(); + }); + test('Existing Configuration items must be removed before displaying picker', async () => { + // tslint:disable-next-line:no-object-literal-type-assertion + const state = { configs: [1, 2, 3], folder: {}, token: undefined } as any as DebugConfigurationState; + const multStepInput = typemoq.Mock.ofType>(); + multStepInput + .setup(i => i.showQuickPick(typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(typemoq.Times.once()); + + await configService.pickDebugConfiguration(multStepInput.object, state); + + multStepInput.verifyAll(); + expect(Object.keys(state.config)).to.be.lengthOf(0); + }); }); diff --git a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts new file mode 100644 index 000000000000..18098627d4f8 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { FileSystem } from '../../../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../../../client/common/platform/pathUtils'; +import { IFileSystem } from '../../../../../client/common/platform/types'; +import { IPathUtils } from '../../../../../client/common/types'; +import { localize } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { DjangoLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Django', () => { + let fs: IFileSystem; + let workspaceService: IWorkspaceService; + let pathUtils: IPathUtils; + let provider: TestDjangoLaunchDebugConfigurationProvider; + let input: MultiStepInput; + class TestDjangoLaunchDebugConfigurationProvider extends DjangoLaunchDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public resolveVariables(pythonPath: string, resource: Uri | undefined): string { + return super.resolveVariables(pythonPath, resource); + } + // tslint:disable-next-line:no-unnecessary-override + public async getManagePyPath(folder: WorkspaceFolder): Promise { + return super.getManagePyPath(folder); + } + } + setup(() => { + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + pathUtils = mock(PathUtils); + input = mock>(MultiStepInput); + provider = new TestDjangoLaunchDebugConfigurationProvider(instance(fs), instance(workspaceService), instance(pathUtils)); + }); + test('getManagePyPath should return undefined if file doesn\'t exist', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); + when(fs.fileExists(managePyPath)).thenResolve(false); + + const file = await provider.getManagePyPath(folder); + + expect(file).to.be.equal(undefined, 'Should return undefined'); + }); + test('getManagePyPath should file path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); + + when(pathUtils.separator).thenReturn('-'); + when(fs.fileExists(managePyPath)).thenResolve(true); + + const file = await provider.getManagePyPath(folder); + + // tslint:disable-next-line:no-invalid-template-strings + expect(file).to.be.equal('${workspaceFolder}-manage.py'); + }); + test('Resolve variables (with resource)', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); + + const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); + + expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); + }); + test('Validation of path should return errors if path is undefined', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateManagePy(folder, ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateManagePy(folder, '', ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => ''; + + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path doesn\'t exist', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz'; + + when(fs.fileExists('xyz')).thenResolve(false); + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is non-python', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.txt'; + + when(fs.fileExists('xyz.txt')).thenResolve(true); + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is python', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.py'; + + when(fs.fileExists('xyz.py')).thenResolve(true); + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.equal(undefined, 'should not have errors'); + }); + test('Launch JSON with valid python path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getManagePyPath = () => Promise.resolve('xyz.py'); + when(pathUtils.separator).thenReturn('-'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.django.label', 'Python: Django')(), + type: DebuggerTypeName, + request: 'launch', + program: 'xyz.py', + args: [ + 'runserver', + '--noreload', + '--nothreading' + ], + django: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getManagePyPath = () => Promise.resolve(undefined); + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.django.label', 'Python: Django')(), + type: DebuggerTypeName, + request: 'launch', + program: 'hello', + args: [ + 'runserver', + '--noreload', + '--nothreading' + ], + django: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with default managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getManagePyPath = () => Promise.resolve(undefined); + const workspaceFolderToken = '${workspaceFolder}'; + const defaultProgram = `${workspaceFolderToken}-manage.py`; + + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.django.label', 'Python: Django')(), + type: DebuggerTypeName, + request: 'launch', + program: defaultProgram, + args: [ + 'runserver', + '--noreload', + '--nothreading' + ], + django: true + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts new file mode 100644 index 000000000000..1e06afe48dc7 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { localize } from '../../../../../client/common/utils/localize'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { FileLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; + +suite('Debugging - Configuration Provider File', () => { + let provider: FileLaunchDebugConfigurationProvider; + setup(() => { + provider = new FileLaunchDebugConfigurationProvider(); + }); + test('Launch JSON with default managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + + await provider.buildConfiguration(undefined as any, state); + + const config = { + name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), + type: DebuggerTypeName, + request: 'launch', + // tslint:disable-next-line:no-invalid-template-strings + program: '${file}', + console: 'integratedTerminal' + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts new file mode 100644 index 000000000000..ee4e1e665fc0 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { FileSystem } from '../../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../../client/common/platform/types'; +import { localize } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { FlaskLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Flask', () => { + let fs: IFileSystem; + let provider: TestFlaskLaunchDebugConfigurationProvider; + let input: MultiStepInput; + class TestFlaskLaunchDebugConfigurationProvider extends FlaskLaunchDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public async getApplicationPath(folder: WorkspaceFolder): Promise { + return super.getApplicationPath(folder); + } + } + setup(() => { + fs = mock(FileSystem); + input = mock>(MultiStepInput); + provider = new TestFlaskLaunchDebugConfigurationProvider(instance(fs)); + }); + test('getApplicationPath should return undefined if file doesn\'t exist', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const appPyPath = path.join(folder.uri.fsPath, 'app.py'); + when(fs.fileExists(appPyPath)).thenResolve(false); + + const file = await provider.getApplicationPath(folder); + + expect(file).to.be.equal(undefined, 'Should return undefined'); + }); + test('getApplicationPath should file path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const appPyPath = path.join(folder.uri.fsPath, 'app.py'); + + when(fs.fileExists(appPyPath)).thenResolve(true); + + const file = await provider.getApplicationPath(folder); + + // tslint:disable-next-line:no-invalid-template-strings + expect(file).to.be.equal('app.py'); + }); + test('Launch JSON with valid python path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getApplicationPath = () => Promise.resolve('xyz.py'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: 'xyz.py', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: [ + 'run', + '--no-debugger', + '--no-reload' + ], + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected app path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getApplicationPath = () => Promise.resolve(undefined); + + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: 'hello', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: [ + 'run', + '--no-debugger', + '--no-reload' + ], + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with default managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getApplicationPath = () => Promise.resolve(undefined); + + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: 'app.py', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: [ + 'run', + '--no-debugger', + '--no-reload' + ], + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts new file mode 100644 index 000000000000..7f8be8d55128 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { localize } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { ModuleLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Module', () => { + let provider: ModuleLaunchDebugConfigurationProvider; + setup(() => { + provider = new ModuleLaunchDebugConfigurationProvider(); + }); + test('Launch JSON with default module name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + const input = mock>(MultiStepInput); + + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.module.label', 'Python: Module')(), + type: DebuggerTypeName, + request: 'launch', + module: 'enter-your-module-name-here' + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected module name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + const input = mock>(MultiStepInput); + + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.module.label', 'Python: Module')(), + type: DebuggerTypeName, + request: 'launch', + module: 'hello' + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts b/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts new file mode 100644 index 000000000000..d1433326dc2f --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { getNamesAndValues } from '../../../../../client/common/utils/enum'; +import { DebugConfigurationProviderFactory } from '../../../../../client/debugger/extension/configuration/providers/providerFactory'; +import { IDebugConfigurationProviderFactory } from '../../../../../client/debugger/extension/configuration/types'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Factory', () => { + let mappedProviders: Map; + let factory: IDebugConfigurationProviderFactory; + setup(() => { + mappedProviders = new Map(); + getNamesAndValues(DebugConfigurationType).forEach(item => { + mappedProviders.set(item.value, item.value as any as IDebugConfigurationProvider); + }); + factory = new DebugConfigurationProviderFactory( + mappedProviders.get(DebugConfigurationType.launchFlask)!, + mappedProviders.get(DebugConfigurationType.launchDjango)!, + mappedProviders.get(DebugConfigurationType.launchModule)!, + mappedProviders.get(DebugConfigurationType.launchFile)!, + mappedProviders.get(DebugConfigurationType.launchPyramid)!, + mappedProviders.get(DebugConfigurationType.remoteAttach)! + ); + }); + getNamesAndValues(DebugConfigurationType).forEach(item => { + test(`Configuration Provider for ${item.name}`, () => { + const provider = factory.create(item.value); + expect(provider).to.equal(mappedProviders.get(item.value)); + }); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts new file mode 100644 index 000000000000..b3d93ff4b39b --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { FileSystem } from '../../../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../../../client/common/platform/pathUtils'; +import { IFileSystem } from '../../../../../client/common/platform/types'; +import { IPathUtils } from '../../../../../client/common/types'; +import { localize } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { PyramidLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Pyramid', () => { + let fs: IFileSystem; + let workspaceService: IWorkspaceService; + let pathUtils: IPathUtils; + let provider: TestPyramidLaunchDebugConfigurationProvider; + let input: MultiStepInput; + class TestPyramidLaunchDebugConfigurationProvider extends PyramidLaunchDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public resolveVariables(pythonPath: string, resource: Uri | undefined): string { + return super.resolveVariables(pythonPath, resource); + } + // tslint:disable-next-line:no-unnecessary-override + public async getDevelopmentIniPath(folder: WorkspaceFolder): Promise { + return super.getDevelopmentIniPath(folder); + } + } + setup(() => { + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + pathUtils = mock(PathUtils); + input = mock>(MultiStepInput); + provider = new TestPyramidLaunchDebugConfigurationProvider(instance(fs), instance(workspaceService), instance(pathUtils)); + }); + test('getDevelopmentIniPath should return undefined if file doesn\'t exist', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); + when(fs.fileExists(managePyPath)).thenResolve(false); + + const file = await provider.getDevelopmentIniPath(folder); + + expect(file).to.be.equal(undefined, 'Should return undefined'); + }); + test('getDevelopmentIniPath should file path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); + + when(pathUtils.separator).thenReturn('-'); + when(fs.fileExists(managePyPath)).thenResolve(true); + + const file = await provider.getDevelopmentIniPath(folder); + + // tslint:disable-next-line:no-invalid-template-strings + expect(file).to.be.equal('${workspaceFolder}-development.ini'); + }); + test('Resolve variables (with resource)', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); + + const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); + + expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); + }); + test('Validation of path should return errors if path is undefined', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateIniPath(folder, ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateIniPath(folder, '', ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => ''; + + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path doesn\'t exist', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz'; + + when(fs.fileExists('xyz')).thenResolve(false); + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is non-ini', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.txt'; + + when(fs.fileExists('xyz.txt')).thenResolve(true); + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is ini', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.ini'; + + when(fs.fileExists('xyz.ini')).thenResolve(true); + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.equal(undefined, 'should not have errors'); + }); + test('Launch JSON with valid ini path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getDevelopmentIniPath = () => Promise.resolve('xyz.ini'); + when(pathUtils.separator).thenReturn('-'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), + type: DebuggerTypeName, + request: 'launch', + args: [ + 'xyz.ini' + ], + pyramid: true, + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected ini path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getDevelopmentIniPath = () => Promise.resolve(undefined); + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), + type: DebuggerTypeName, + request: 'launch', + args: [ + 'hello' + ], + pyramid: true, + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with default ini path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getDevelopmentIniPath = () => Promise.resolve(undefined); + const workspaceFolderToken = '${workspaceFolder}'; + const defaultIni = `${workspaceFolderToken}-development.ini`; + + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), + type: DebuggerTypeName, + request: 'launch', + args: [ + defaultIni + ], + pyramid: true, + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts new file mode 100644 index 000000000000..b91f815b5e57 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { localize } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { RemoteAttachDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; +import { AttachRequestArguments } from '../../../../../client/debugger/types'; + +suite('Debugging - Configuration Provider Remote Attach', () => { + let provider: TestRemoteAttachDebugConfigurationProvider; + let input: MultiStepInput; + class TestRemoteAttachDebugConfigurationProvider extends RemoteAttachDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public async configurePort(i: MultiStepInput, config: Partial) { + return super.configurePort(i, config); + } + } + setup(() => { + input = mock>(MultiStepInput); + provider = new TestRemoteAttachDebugConfigurationProvider(); + }); + test('Configure port will display prompt', async () => { + when(input.showInputBox(anything())).thenResolve(); + + await provider.configurePort(instance(input), {}); + + verify(input.showInputBox(anything())).once(); + }); + test('Configure port will default to 5678 if entered value is not a number', async () => { + const config: { port?: number } = {}; + when(input.showInputBox(anything())).thenResolve('xyz'); + + await provider.configurePort(instance(input), config); + + verify(input.showInputBox(anything())).once(); + expect(config.port).to.equal(5678); + }); + test('Configure port will default to 5678', async () => { + const config: { port?: number } = {}; + when(input.showInputBox(anything())).thenResolve(); + + await provider.configurePort(instance(input), config); + + verify(input.showInputBox(anything())).once(); + expect(config.port).to.equal(5678); + }); + test('Configure port will use user selected value', async () => { + const config: { port?: number } = {}; + when(input.showInputBox(anything())).thenResolve('1234'); + + await provider.configurePort(instance(input), config); + + verify(input.showInputBox(anything())).once(); + expect(config.port).to.equal(1234); + }); + test('Launch JSON with default host name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + let portConfigured = false; + when(input.showInputBox(anything())).thenResolve(); + provider.configurePort = () => { + portConfigured = true; + return Promise.resolve(); + }; + + const configurePort = await provider.buildConfiguration(instance(input), state); + if (configurePort) { + await configurePort!(input, state); + } + + const config = { + name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), + type: DebuggerTypeName, + request: 'attach', + port: 5678, + host: 'localhost' + }; + + expect(state.config).to.be.deep.equal(config); + expect(portConfigured).to.be.equal(true, 'Port not configured'); + }); + test('Launch JSON with user defined host name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + let portConfigured = false; + when(input.showInputBox(anything())).thenResolve('Hello'); + provider.configurePort = (_, cfg) => { + portConfigured = true; + cfg.port = 9999; + return Promise.resolve(); + }; + + const configurePort = await provider.buildConfiguration(instance(input), state); + if (configurePort) { + await configurePort(input, state); + } + + const config = { + name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), + type: DebuggerTypeName, + request: 'attach', + port: 9999, + host: 'Hello' + }; + + expect(state.config).to.be.deep.equal(config); + expect(portConfigured).to.be.equal(true, 'Port not configured'); + }); +}); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 37740a2cd960..b2e6f0e9d8ce 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -10,14 +10,21 @@ import * as typemoq from 'typemoq'; import { DebuggerBanner } from '../../../client/debugger/extension/banner'; import { ConfigurationProviderUtils } from '../../../client/debugger/extension/configuration/configurationProviderUtils'; import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; +import { DjangoLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/djangoLaunch'; +import { FileLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/fileLaunch'; +import { FlaskLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/flaskLaunch'; +import { ModuleLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/moduleLaunch'; +import { DebugConfigurationProviderFactory } from '../../../client/debugger/extension/configuration/providers/providerFactory'; +import { PyramidLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/pyramidLaunch'; +import { RemoteAttachDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/remoteAttach'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { IConfigurationProviderUtils, IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; +import { IConfigurationProviderUtils, IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; import { ChildProcessAttachEventHandler } from '../../../client/debugger/extension/hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from '../../../client/debugger/extension/hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../../client/debugger/extension/hooks/types'; import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; -import { IDebugConfigurationService, IDebuggerBanner } from '../../../client/debugger/extension/types'; +import { DebugConfigurationType, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from '../../../client/debugger/extension/types'; import { IServiceManager } from '../../../client/ioc/types'; suite('Debugging - Service Registry', () => { @@ -31,7 +38,14 @@ suite('Debugging - Service Registry', () => { [IChildProcessAttachService, ChildProcessAttachService], [IDebugSessionEventHandlers, ChildProcessAttachEventHandler], [IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'], - [IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'] + [IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'], + [IDebugConfigurationProviderFactory, DebugConfigurationProviderFactory], + [IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider, DebugConfigurationType.launchFile], + [IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider, DebugConfigurationType.launchDjango], + [IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider, DebugConfigurationType.launchFlask], + [IDebugConfigurationProvider, RemoteAttachDebugConfigurationProvider, DebugConfigurationType.remoteAttach], + [IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider, DebugConfigurationType.launchModule], + [IDebugConfigurationProvider, PyramidLaunchDebugConfigurationProvider, DebugConfigurationType.launchPyramid] ].forEach(mapping => { if (mapping.length === 2) { serviceManager From 22ee97df20ecedee75953c01950a202ee51bc24e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Dec 2018 13:53:26 -0800 Subject: [PATCH 7/9] News entry --- news/1 Enhancements/3321.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1 Enhancements/3321.md diff --git a/news/1 Enhancements/3321.md b/news/1 Enhancements/3321.md new file mode 100644 index 000000000000..a51cefe4a567 --- /dev/null +++ b/news/1 Enhancements/3321.md @@ -0,0 +1 @@ +Prompt user to select a debug configuration when generating the `launch.json`. From c753e942293e957ee7cd0d3c6c2ede2fa8a08d75 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Dec 2018 14:56:12 -0800 Subject: [PATCH 8/9] add telemetry --- .../configuration/providers/djangoLaunch.ts | 8 +++++-- .../configuration/providers/fileLaunch.ts | 5 ++++- .../configuration/providers/flaskLaunch.ts | 5 +++++ .../configuration/providers/moduleLaunch.ts | 8 ++++++- .../configuration/providers/pyramidLaunch.ts | 7 ++++++- .../configuration/providers/remoteAttach.ts | 21 ++++++++++++------- .../extension/configuration/resolvers/base.ts | 7 ++++++- src/client/telemetry/constants.ts | 1 + src/client/telemetry/types.ts | 15 ++++++++++++- 9 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts index 06d0571c50c2..f0fc850d0876 100644 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts @@ -12,9 +12,11 @@ import { IPathUtils } from '../../../../common/types'; import { Debug, localize } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; // tslint:disable-next-line:no-invalid-template-strings const workspaceFolderToken = '${workspaceFolder}'; @@ -26,7 +28,7 @@ export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurati @inject(IPathUtils) private pathUtils: IPathUtils) { } public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { const program = await this.getManagePyPath(state.folder); - + let manuallyEnteredAValue: boolean | undefined; const defaultProgram = `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; const config: Partial = { name: localize('python.snippet.launch.django.label', 'Python: Django')(), @@ -48,10 +50,12 @@ export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurati validate: value => this.validateManagePy(state.folder, defaultProgram, value) }); if (selectedProgram) { + manuallyEnteredAValue = true; config.program = selectedProgram; } } + sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchDjango, autoDetectedDjangoManagePyPath: !!program, manuallyEnteredAValue }); Object.assign(state.config, config); } public async validateManagePy(folder: WorkspaceFolder | undefined, defaultValue: string, selected?: string): Promise { diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts index d5fdf38405c4..7718778ff24e 100644 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -6,12 +6,15 @@ import { injectable } from 'inversify'; import { localize } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { captureTelemetry } from '../../../../telemetry'; +import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; @injectable() export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + @captureTelemetry(DEBUGGER_CONFIGURATION_PROMPTS, { configurationType: DebugConfigurationType.launchFile }, false) public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { const config: Partial = { name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts index b1584c38e7c6..539b5cf13843 100644 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -9,6 +9,8 @@ import { WorkspaceFolder } from 'vscode'; import { IFileSystem } from '../../../../common/platform/types'; import { Debug, localize } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; @@ -21,6 +23,7 @@ export class FlaskLaunchDebugConfigurationProvider implements IDebugConfiguratio } public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { const application = await this.getApplicationPath(state.folder); + let manuallyEnteredAValue: boolean | undefined; const config: Partial = { name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), type: DebuggerTypeName, @@ -47,10 +50,12 @@ export class FlaskLaunchDebugConfigurationProvider implements IDebugConfiguratio validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : Debug.flaskEnterAppPathOrNamePathInvalidNameError()) }); if (selectedApp) { + manuallyEnteredAValue = true; config.env!.FLASK_APP = selectedApp; } } + sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchFlask, autoDetectedFlaskAppPyPath: !!application, manuallyEnteredAValue }); Object.assign(state.config, config); } protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise { diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts index a299dfa8b705..f5fd5c1577cc 100644 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts @@ -6,13 +6,16 @@ import { injectable } from 'inversify'; import { Debug, localize } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; @injectable() export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + let manuallyEnteredAValue: boolean | undefined; const config: Partial = { name: localize('python.snippet.launch.module.label', 'Python: Module')(), type: DebuggerTypeName, @@ -26,8 +29,11 @@ export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurati validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : Debug.moduleEnterModuleInvalidNameError()) }); if (selectedModule) { + manuallyEnteredAValue = true; config.module = selectedModule; } + + sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchModule, manuallyEnteredAValue }); Object.assign(state.config, config); } } diff --git a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts index 0353c97c9f1d..6a854319f0c3 100644 --- a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts @@ -12,9 +12,11 @@ import { IPathUtils } from '../../../../common/types'; import { Debug, localize } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; // tslint:disable-next-line:no-invalid-template-strings const workspaceFolderToken = '${workspaceFolder}'; @@ -27,6 +29,7 @@ export class PyramidLaunchDebugConfigurationProvider implements IDebugConfigurat public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { const iniPath = await this.getDevelopmentIniPath(state.folder); const defaultIni = `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; + let manuallyEnteredAValue: boolean | undefined; const config: Partial = { name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), @@ -47,10 +50,12 @@ export class PyramidLaunchDebugConfigurationProvider implements IDebugConfigurat validate: value => this.validateIniPath(state ? state.folder : undefined, defaultIni, value) }); if (selectedIniPath) { + manuallyEnteredAValue = true; config.args = [selectedIniPath]; } } + sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchPyramid, autoDetectedPyramidIniPath: !!iniPath, manuallyEnteredAValue }); Object.assign(state.config, config); } public async validateIniPath(folder: WorkspaceFolder | undefined, defaultValue: string, selected?: string): Promise { diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts index ddf532e101fd..4cbbd9281c92 100644 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ b/src/client/debugger/extension/configuration/providers/remoteAttach.ts @@ -6,9 +6,14 @@ import { injectable } from 'inversify'; import { Debug, localize } from '../../../../common/utils/localize'; import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, IDebugConfigurationProvider } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +const defaultHost = 'localhost'; +const defaultPort = 5678; @injectable() export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurationProvider { @@ -17,22 +22,23 @@ export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurati name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), type: DebuggerTypeName, request: 'attach', - port: 5678, - host: 'localhost' + port: defaultPort, + host: defaultHost }; config.host = await input.showInputBox({ title: Debug.attachRemoteHostTitle(), step: 1, totalSteps: 2, - value: config.host || 'localhost', + value: config.host || defaultHost, prompt: Debug.attachRemoteHostPrompt(), validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : Debug.attachRemoteHostValidationError()) }); if (!config.host) { - config.host = 'localhost'; + config.host = defaultHost; } + sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.remoteAttach, manuallyEnteredAValue: config.host !== defaultHost }); Object.assign(state.config, config); return _ => this.configurePort(input, state.config); } @@ -41,7 +47,7 @@ export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurati title: Debug.attachRemotePortTitle(), step: 2, totalSteps: 2, - value: (config.port || 5678).toString(), + value: (config.port || defaultPort).toString(), prompt: Debug.attachRemotePortPrompt(), validate: value => Promise.resolve((value && /^\d+$/.test(value.trim())) ? undefined : Debug.attachRemotePortValidationError()) }); @@ -49,7 +55,8 @@ export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurati config.port = parseInt(port, 10); } if (!config.port) { - config.port = 5678; + config.port = defaultPort; } + sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.remoteAttach, manuallyEnteredAValue: config.port !== defaultPort }); } } diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index d8e9ad63550f..f5b267d8668f 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -70,6 +70,7 @@ export abstract class BaseConfigurationResolver im return (debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK') ? true : false; } protected sendTelemetry(trigger: 'launch' | 'attach', debugConfiguration: Partial) { + const name = debugConfiguration.name || ''; const telemetryProps: DebuggerTelemetry = { trigger, console: debugConfiguration.console, @@ -84,7 +85,11 @@ export abstract class BaseConfigurationResolver im pyramid: !!debugConfiguration.pyramid, stopOnEntry: !!debugConfiguration.stopOnEntry, showReturnValue: !!debugConfiguration.showReturnValue, - subProcess: !!debugConfiguration.subProcess + subProcess: !!debugConfiguration.subProcess, + watson: name.toLowerCase().indexOf('watson') >= 0, + pyspark: name.toLowerCase().indexOf('pyspark') >= 0, + gevent: name.toLowerCase().indexOf('gevent') >= 0, + scrapy: (debugConfiguration.module || '').toLowerCase() === 'scrapy' }; sendTelemetryEvent(DEBUGGER, undefined, telemetryProps); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 9d512a989c2c..fbb26ff51b2a 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -29,6 +29,7 @@ export const EXECUTION_DJANGO = 'EXECUTION_DJANGO'; export const DEBUGGER = 'DEBUGGER'; export const DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS'; export const DEBUGGER_PERFORMANCE = 'DEBUGGER.PERFORMANCE'; +export const DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS'; export const UNITTEST_STOP = 'UNITTEST.STOP'; export const UNITTEST_RUN = 'UNITTEST.RUN'; export const UNITTEST_DISCOVER = 'UNITTEST.DISCOVER'; diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index 67c03e35a896..89b3eeeb8d43 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -3,6 +3,7 @@ 'use strict'; import { TerminalShellType } from '../common/terminal/types'; +import { DebugConfigurationType } from '../debugger/extension/types'; import { InterpreterType } from '../interpreter/contracts'; import { LinterId } from '../linters/types'; import { PlatformErrors } from './constants'; @@ -63,6 +64,10 @@ export type DebuggerTelemetry = { showReturnValue: boolean; pyramid: boolean; subProcess: boolean; + watson: boolean; + pyspark: boolean; + gevent: boolean; + scrapy: boolean; }; export type DebuggerPerformanceTelemetry = { duration: number; @@ -92,6 +97,13 @@ export type TerminalTelemetry = { pythonVersion?: string; interpreterType?: InterpreterType; }; +export type DebuggerConfigurationPromtpsTelemetry = { + configurationType: DebugConfigurationType; + autoDetectedDjangoManagePyPath?: boolean; + autoDetectedPyramidIniPath?: boolean; + autoDetectedFlaskAppPyPath?: boolean; + manuallyEnteredAValue?: boolean; +}; export type DiagnosticsAction = { /** * Diagnostics command executed. @@ -147,4 +159,5 @@ export type TelemetryProperties = FormatTelemetry | DiagnosticsMessages | ImportNotebook | Platform - | LanguageServePlatformSupported; + | LanguageServePlatformSupported + | DebuggerConfigurationPromtpsTelemetry; From 8ba770fa37b88e775d3f427f27021bba32111758 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 20 Dec 2018 14:57:57 -0800 Subject: [PATCH 9/9] Simplify --- .../debugger/extension/configuration/resolvers/base.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index f5b267d8668f..dffff2e0c6f5 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -71,6 +71,7 @@ export abstract class BaseConfigurationResolver im } protected sendTelemetry(trigger: 'launch' | 'attach', debugConfiguration: Partial) { const name = debugConfiguration.name || ''; + const moduleName = debugConfiguration.module || ''; const telemetryProps: DebuggerTelemetry = { trigger, console: debugConfiguration.console, @@ -79,7 +80,7 @@ export abstract class BaseConfigurationResolver im flask: this.isDebuggingFlask(debugConfiguration), hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, isLocalhost: this.isLocalHost(debugConfiguration.host), - isModule: typeof debugConfiguration.module === 'string' && debugConfiguration.module.length > 0, + isModule: moduleName.length > 0, isSudo: !!debugConfiguration.sudo, jinja: !!debugConfiguration.jinja, pyramid: !!debugConfiguration.pyramid, @@ -89,7 +90,7 @@ export abstract class BaseConfigurationResolver im watson: name.toLowerCase().indexOf('watson') >= 0, pyspark: name.toLowerCase().indexOf('pyspark') >= 0, gevent: name.toLowerCase().indexOf('gevent') >= 0, - scrapy: (debugConfiguration.module || '').toLowerCase() === 'scrapy' + scrapy: moduleName.toLowerCase() === 'scrapy' }; sendTelemetryEvent(DEBUGGER, undefined, telemetryProps); }