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/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/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/configProviders/baseProvider.ts b/src/client/debugger/extension/configProviders/baseProvider.ts deleted file mode 100644 index b2f65c416026..000000000000 --- a/src/client/debugger/extension/configProviders/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/configProviders/pythonV2Provider.ts b/src/client/debugger/extension/configProviders/pythonV2Provider.ts deleted file mode 100644 index 79db5b12ece2..000000000000 --- a/src/client/debugger/extension/configProviders/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/configProviders/types.ts b/src/client/debugger/extension/configProviders/types.ts deleted file mode 100644 index 8ece886ab352..000000000000 --- a/src/client/debugger/extension/configProviders/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Uri } from 'vscode'; - -export const IConfigurationProviderUtils = Symbol('IConfigurationProviderUtils'); - -export interface IConfigurationProviderUtils { - getPyramidStartupScriptFilePath(resource?: Uri): Promise; -} diff --git a/src/client/debugger/extension/configProviders/configurationProviderUtils.ts b/src/client/debugger/extension/configuration/configurationProviderUtils.ts similarity index 63% rename from src/client/debugger/extension/configProviders/configurationProviderUtils.ts rename to src/client/debugger/extension/configuration/configurationProviderUtils.ts index b13ca708b090..8a8c56103e3d 100644 --- a/src/client/debugger/extension/configProviders/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/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..4fd6feb4efc8 --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -0,0 +1,129 @@ +// 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 { 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'; +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(InvalidPythonPathInDebuggerServiceId) 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 new file mode 100644 index 000000000000..db8ba18046c4 --- /dev/null +++ b/src/client/debugger/extension/configuration/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +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 7d669c53ff70..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 './configProviders/configurationProviderUtils'; -import { PythonV2DebugConfigurationProvider } from './configProviders/pythonV2Provider'; -import { IConfigurationProviderUtils } from './configProviders/types'; +import { ConfigurationProviderUtils } from './configuration/configurationProviderUtils'; +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 65b2d490039b..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/configProviders/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 bde71cbcba07..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/configProviders/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 407c40f6ce68..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/configProviders/configurationProviderUtils'; -import { PythonV2DebugConfigurationProvider } from '../../../../client/debugger/extension/configProviders/pythonV2Provider'; -import { IConfigurationProviderUtils } from '../../../../client/debugger/extension/configProviders/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(); + }); +});