From 95daf89c5be3c18d96da28f88ae7e5877e6c6fef Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Wed, 18 Oct 2023 15:08:08 -0700 Subject: [PATCH 01/24] Remove python debugger from core extension --- package.json | 386 ------ src/client/common/application/debugService.ts | 7 - .../application/debugSessionTelemetry.ts | 80 -- src/client/common/application/types.ts | 12 - src/client/common/serviceRegistry.ts | 5 - src/client/debugger/constants.ts | 2 +- .../debugger/extension/adapter/activator.ts | 53 - .../debugger/extension/adapter/factory.ts | 208 --- .../debugger/extension/adapter/logging.ts | 77 -- .../adapter/outdatedDebuggerPrompt.ts | 73 - .../debugger/extension/adapter/types.ts | 10 - .../extension/attachQuickPick/factory.ts | 36 - .../extension/attachQuickPick/picker.ts | 82 -- .../extension/attachQuickPick/provider.ts | 82 -- .../attachQuickPick/psProcessParser.ts | 101 -- .../extension/attachQuickPick/types.ts | 29 - .../attachQuickPick/wmicProcessParser.ts | 82 -- .../debugConfigurationService.ts | 195 --- .../dynamicdebugConfigurationService.ts | 134 -- .../launch.json/completionProvider.ts | 83 -- .../launch.json/interpreterPathCommand.ts | 51 - .../launch.json/updaterService.ts | 28 - .../launch.json/updaterServiceHelper.ts | 151 -- .../configuration/providers/djangoLaunch.ts | 88 -- .../configuration/providers/fastapiLaunch.ts | 67 - .../configuration/providers/fileLaunch.ts | 30 - .../configuration/providers/flaskLaunch.ts | 71 - .../configuration/providers/moduleLaunch.ts | 45 - .../configuration/providers/pidAttach.ts | 29 - .../configuration/providers/pyramidLaunch.ts | 96 -- .../configuration/providers/remoteAttach.ts | 61 - .../configuration/resolvers/attach.ts | 124 -- .../extension/configuration/resolvers/base.ts | 12 +- .../configuration/utils/configuration.ts | 44 - .../debugger/extension/debugCommands.ts | 73 - .../extension/helpers/protocolParser.ts | 140 -- .../hooks/childProcessAttachHandler.ts | 47 - .../hooks/childProcessAttachService.ts | 56 - .../debugger/extension/hooks/constants.ts | 10 - .../extension/hooks/eventHandlerDispatcher.ts | 33 - src/client/debugger/extension/hooks/types.ts | 18 - .../debugger/extension/serviceRegistry.ts | 77 +- src/client/debugger/extension/types.ts | 24 +- src/client/debugger/types.ts | 27 - src/client/extensionActivation.ts | 33 +- src/client/formatters/baseFormatter.ts | 148 ++ src/client/formatters/blackFormatter.ts | 53 + .../linters/errorHandlers/notInstalled.ts | 33 + src/client/linters/linterCommands.ts | 109 ++ src/client/providers/formatProvider.ts | 126 ++ src/client/testing/common/debugLauncher.ts | 2 +- .../invalidPythonPathInDebugger.unit.test.ts | 1 - src/test/common/moduleInstaller.test.ts | 5 - .../resolvers/attach.unit.test.ts | 569 -------- .../configuration/resolvers/base.unit.test.ts | 337 ----- .../resolvers/helper.unit.test.ts | 70 - .../resolvers/launch.unit.test.ts | 1213 ----------------- .../extension/serviceRegistry.unit.test.ts | 101 -- 58 files changed, 488 insertions(+), 5451 deletions(-) delete mode 100644 src/client/common/application/debugSessionTelemetry.ts delete mode 100644 src/client/debugger/extension/adapter/activator.ts delete mode 100644 src/client/debugger/extension/adapter/factory.ts delete mode 100644 src/client/debugger/extension/adapter/logging.ts delete mode 100644 src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts delete mode 100644 src/client/debugger/extension/adapter/types.ts delete mode 100644 src/client/debugger/extension/attachQuickPick/factory.ts delete mode 100644 src/client/debugger/extension/attachQuickPick/picker.ts delete mode 100644 src/client/debugger/extension/attachQuickPick/provider.ts delete mode 100644 src/client/debugger/extension/attachQuickPick/psProcessParser.ts delete mode 100644 src/client/debugger/extension/attachQuickPick/types.ts delete mode 100644 src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts delete mode 100644 src/client/debugger/extension/configuration/debugConfigurationService.ts delete mode 100644 src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts delete mode 100644 src/client/debugger/extension/configuration/launch.json/completionProvider.ts delete mode 100644 src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts delete mode 100644 src/client/debugger/extension/configuration/launch.json/updaterService.ts delete mode 100644 src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts delete mode 100644 src/client/debugger/extension/configuration/providers/djangoLaunch.ts delete mode 100644 src/client/debugger/extension/configuration/providers/fastapiLaunch.ts delete mode 100644 src/client/debugger/extension/configuration/providers/fileLaunch.ts delete mode 100644 src/client/debugger/extension/configuration/providers/flaskLaunch.ts delete mode 100644 src/client/debugger/extension/configuration/providers/moduleLaunch.ts delete mode 100644 src/client/debugger/extension/configuration/providers/pidAttach.ts delete mode 100644 src/client/debugger/extension/configuration/providers/pyramidLaunch.ts delete mode 100644 src/client/debugger/extension/configuration/providers/remoteAttach.ts delete mode 100644 src/client/debugger/extension/configuration/resolvers/attach.ts delete mode 100644 src/client/debugger/extension/configuration/utils/configuration.ts delete mode 100644 src/client/debugger/extension/debugCommands.ts delete mode 100644 src/client/debugger/extension/helpers/protocolParser.ts delete mode 100644 src/client/debugger/extension/hooks/childProcessAttachHandler.ts delete mode 100644 src/client/debugger/extension/hooks/childProcessAttachService.ts delete mode 100644 src/client/debugger/extension/hooks/constants.ts delete mode 100644 src/client/debugger/extension/hooks/eventHandlerDispatcher.ts delete mode 100644 src/client/debugger/extension/hooks/types.ts create mode 100644 src/client/formatters/baseFormatter.ts create mode 100644 src/client/formatters/blackFormatter.ts create mode 100644 src/client/linters/errorHandlers/notInstalled.ts create mode 100644 src/client/linters/linterCommands.ts create mode 100644 src/client/providers/formatProvider.ts delete mode 100644 src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/resolvers/base.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts diff --git a/package.json b/package.json index 1976a7076f7e..c9e83bd38ae5 100644 --- a/package.json +++ b/package.json @@ -388,12 +388,6 @@ "icon": "$(play)", "title": "%python.command.python.execInDedicatedTerminal.title%" }, - { - "category": "Python", - "command": "python.debugInTerminal", - "icon": "$(debug-alt)", - "title": "%python.command.python.debugInTerminal.title%" - }, { "category": "Python", "command": "python.execSelectionInDjangoShell", @@ -793,373 +787,6 @@ "title": "Python", "type": "object" }, - "debuggers": [ - { - "configurationAttributes": { - "attach": { - "properties": { - "connect": { - "label": "Attach by connecting to debugpy over a socket.", - "properties": { - "host": { - "default": "127.0.0.1", - "description": "Hostname or IP address to connect to.", - "type": "string" - }, - "port": { - "description": "Port to connect to.", - "type": "number" - } - }, - "required": [ - "port" - ], - "type": "object" - }, - "debugAdapterPath": { - "description": "Path (fully qualified) to the python debug adapter executable.", - "type": "string" - }, - "django": { - "default": false, - "description": "Django debugging.", - "type": "boolean" - }, - "host": { - "default": "127.0.0.1", - "description": "Hostname or IP address to connect to.", - "type": "string" - }, - "jinja": { - "default": null, - "description": "Jinja template debugging (e.g. Flask).", - "enum": [ - false, - null, - true - ] - }, - "justMyCode": { - "default": true, - "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.", - "type": "boolean" - }, - "listen": { - "label": "Attach by listening for incoming socket connection from debugpy", - "properties": { - "host": { - "default": "127.0.0.1", - "description": "Hostname or IP address of the interface to listen on.", - "type": "string" - }, - "port": { - "description": "Port to listen on.", - "type": "number" - } - }, - "required": [ - "port" - ], - "type": "object" - }, - "logToFile": { - "default": false, - "description": "Enable logging of debugger events to a log file.", - "type": "boolean" - }, - "pathMappings": { - "default": [], - "items": { - "label": "Path mapping", - "properties": { - "localRoot": { - "default": "${workspaceFolder}", - "label": "Local source root.", - "type": "string" - }, - "remoteRoot": { - "default": "", - "label": "Remote source root.", - "type": "string" - } - }, - "required": [ - "localRoot", - "remoteRoot" - ], - "type": "object" - }, - "label": "Path mappings.", - "type": "array" - }, - "port": { - "description": "Port to connect to.", - "type": "number" - }, - "processId": { - "anyOf": [ - { - "default": "${command:pickProcess}", - "description": "Use process picker to select a process to attach, or Process ID as integer.", - "enum": [ - "${command:pickProcess}" - ] - }, - { - "description": "ID of the local process to attach to.", - "type": "integer" - } - ] - }, - "redirectOutput": { - "default": true, - "description": "Redirect output.", - "type": "boolean" - }, - "showReturnValue": { - "default": true, - "description": "Show return value of functions when stepping.", - "type": "boolean" - }, - "subProcess": { - "default": false, - "description": "Whether to enable Sub Process debugging", - "type": "boolean" - } - } - }, - "launch": { - "properties": { - "args": { - "default": [], - "description": "Command line arguments passed to the program.", - "items": { - "type": "string" - }, - "type": [ - "array", - "string" - ] - }, - "autoReload": { - "default": {}, - "description": "Configures automatic reload of code on edit.", - "properties": { - "enable": { - "default": false, - "description": "Automatically reload code on edit.", - "type": "boolean" - }, - "exclude": { - "default": [ - "**/.git/**", - "**/.metadata/**", - "**/__pycache__/**", - "**/node_modules/**", - "**/site-packages/**" - ], - "description": "Glob patterns of paths to exclude from auto reload.", - "items": { - "type": "string" - }, - "type": "array" - }, - "include": { - "default": [ - "**/*.py", - "**/*.pyw" - ], - "description": "Glob patterns of paths to include in auto reload.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "console": { - "default": "integratedTerminal", - "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", - "enum": [ - "externalTerminal", - "integratedTerminal", - "internalConsole" - ] - }, - "consoleTitle": { - "default": "Python Debug Console", - "description": "Display name of the debug console or terminal" - }, - "cwd": { - "default": "${workspaceFolder}", - "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", - "type": "string" - }, - "debugAdapterPath": { - "description": "Path (fully qualified) to the python debug adapter executable.", - "type": "string" - }, - "django": { - "default": false, - "description": "Django debugging.", - "type": "boolean" - }, - "env": { - "additionalProperties": { - "type": "string" - }, - "default": {}, - "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", - "type": "object" - }, - "envFile": { - "default": "${workspaceFolder}/.env", - "description": "Absolute path to a file containing environment variable definitions.", - "type": "string" - }, - "gevent": { - "default": false, - "description": "Enable debugging of gevent monkey-patched code.", - "type": "boolean" - }, - "host": { - "default": "localhost", - "description": "IP address of the of the local debug server (default is localhost).", - "type": "string" - }, - "jinja": { - "default": null, - "description": "Jinja template debugging (e.g. Flask).", - "enum": [ - false, - null, - true - ] - }, - "justMyCode": { - "default": true, - "description": "Debug only user-written code.", - "type": "boolean" - }, - "logToFile": { - "default": false, - "description": "Enable logging of debugger events to a log file.", - "type": "boolean" - }, - "module": { - "default": "", - "description": "Name of the module to be debugged.", - "type": "string" - }, - "pathMappings": { - "default": [], - "items": { - "label": "Path mapping", - "properties": { - "localRoot": { - "default": "${workspaceFolder}", - "label": "Local source root.", - "type": "string" - }, - "remoteRoot": { - "default": "", - "label": "Remote source root.", - "type": "string" - } - }, - "required": [ - "localRoot", - "remoteRoot" - ], - "type": "object" - }, - "label": "Path mappings.", - "type": "array" - }, - "port": { - "default": 0, - "description": "Debug port (default is 0, resulting in the use of a dynamic port).", - "type": "number" - }, - "program": { - "default": "${file}", - "description": "Absolute path to the program.", - "type": "string" - }, - "purpose": { - "default": [], - "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.", - "items": { - "enum": [ - "debug-test", - "debug-in-terminal" - ], - "enumDescriptions": [ - "Use this configuration while debugging tests using test view or test debug commands.", - "Use this configuration while debugging a file using debug in terminal button in the editor." - ] - }, - "type": "array" - }, - "pyramid": { - "default": false, - "description": "Whether debugging Pyramid applications", - "type": "boolean" - }, - "python": { - "default": "${command:python.interpreterPath}", - "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.", - "type": "string" - }, - "pythonArgs": { - "default": [], - "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".", - "items": { - "type": "string" - }, - "type": "array" - }, - "redirectOutput": { - "default": true, - "description": "Redirect output.", - "type": "boolean" - }, - "showReturnValue": { - "default": true, - "description": "Show return value of functions when stepping.", - "type": "boolean" - }, - "stopOnEntry": { - "default": false, - "description": "Automatically stop after launch.", - "type": "boolean" - }, - "subProcess": { - "default": false, - "description": "Whether to enable Sub Process debugging", - "type": "boolean" - }, - "sudo": { - "default": false, - "description": "Running debug program under elevated permissions (on Unix).", - "type": "boolean" - } - } - } - }, - "configurationSnippets": [], - "label": "Python", - "languages": [ - "python" - ], - "type": "python", - "variables": { - "pickProcess": "python.pickLocalProcess" - }, - "when": "!virtualWorkspace && shellExecutionSupported" - } - ], "grammars": [ { "language": "pip-requirements", @@ -1323,13 +950,6 @@ "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "false" }, - { - "category": "Python", - "command": "python.debugInTerminal", - "icon": "$(debug-alt)", - "title": "%python.command.python.debugInTerminal.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.execSelectionInDjangoShell", @@ -1462,12 +1082,6 @@ "group": "navigation@0", "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" - }, - { - "command": "python.debugInTerminal", - "group": "navigation@1", - "title": "%python.command.python.debugInTerminal.title%", - "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" } ], "explorer/context": [ diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts index d98262d88926..32895ed3f0a8 100644 --- a/src/client/common/application/debugService.ts +++ b/src/client/common/application/debugService.ts @@ -8,7 +8,6 @@ import { Breakpoint, BreakpointsChangeEvent, debug, - DebugAdapterDescriptorFactory, DebugConfiguration, DebugConsole, DebugSession, @@ -67,10 +66,4 @@ export class DebugService implements IDebugService { public removeBreakpoints(breakpoints: Breakpoint[]): void { debug.removeBreakpoints(breakpoints); } - public registerDebugAdapterDescriptorFactory( - debugType: string, - factory: DebugAdapterDescriptorFactory, - ): Disposable { - return debug.registerDebugAdapterDescriptorFactory(debugType, factory); - } } diff --git a/src/client/common/application/debugSessionTelemetry.ts b/src/client/common/application/debugSessionTelemetry.ts deleted file mode 100644 index 42b8b2651092..000000000000 --- a/src/client/common/application/debugSessionTelemetry.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; - -import { IExtensionSingleActivationService } from '../../activation/types'; -import { AttachRequestArguments, ConsoleType, LaunchRequestArguments, TriggerType } from '../../debugger/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { IDisposableRegistry } from '../types'; -import { StopWatch } from '../utils/stopWatch'; -import { IDebugService } from './types'; - -function isResponse(a: any): a is DebugProtocol.Response { - return a.type === 'response'; -} -class TelemetryTracker implements DebugAdapterTracker { - private timer = new StopWatch(); - private readonly trigger: TriggerType = 'launch'; - private readonly console: ConsoleType | undefined; - - constructor(session: DebugSession) { - this.trigger = session.configuration.request as TriggerType; - const debugConfiguration = session.configuration as Partial; - this.console = debugConfiguration.console; - } - - public onWillStartSession() { - this.sendTelemetry(EventName.DEBUG_SESSION_START); - } - - public onDidSendMessage(message: any): void { - if (isResponse(message)) { - if (message.command === 'configurationDone') { - // "configurationDone" response is sent immediately after user code starts running. - this.sendTelemetry(EventName.DEBUG_SESSION_USER_CODE_RUNNING); - } - } - } - - public onWillStopSession(): void { - this.sendTelemetry(EventName.DEBUG_SESSION_STOP); - } - - public onError?(_error: Error): void { - this.sendTelemetry(EventName.DEBUG_SESSION_ERROR); - } - - private sendTelemetry(eventName: EventName): void { - if (eventName === EventName.DEBUG_SESSION_START) { - this.timer.reset(); - } - const telemetryProps = { - trigger: this.trigger, - console: this.console, - }; - sendTelemetryEvent(eventName, this.timer.elapsedTime, telemetryProps); - } -} - -@injectable() -export class DebugSessionTelemetry implements DebugAdapterTrackerFactory, IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - constructor( - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IDebugService) debugService: IDebugService, - ) { - disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); - } - - public async activate(): Promise { - // We actually register in the constructor. Not necessary to do it here - } - - public createDebugAdapterTracker(session: DebugSession): ProviderResult { - return new TelemetryTracker(session); - } -} diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 6705331bf57d..2a6950dd1eb2 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -9,7 +9,6 @@ import { CancellationToken, CompletionItemProvider, ConfigurationChangeEvent, - DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfiguration, DebugConfigurationProvider, @@ -995,17 +994,6 @@ export interface IDebugService { */ registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable; - /** - * Register a [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) for a specific debug type. - * An extension is only allowed to register a DebugAdapterDescriptorFactory for the debug type(s) defined by the extension. Otherwise an error is thrown. - * Registering more than one DebugAdapterDescriptorFactory for a debug type results in an error. - * - * @param debugType The debug type for which the factory is registered. - * @param factory The [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) to register. - * @return A [disposable](#Disposable) that unregisters this factory when being disposed. - */ - registerDebugAdapterDescriptorFactory(debugType: string, factory: DebugAdapterDescriptorFactory): Disposable; - /** * Register a debug adapter tracker factory for the given debug type. * diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 8c872c3113ba..f230e78eb3eb 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -28,7 +28,6 @@ import { CommandManager } from './application/commandManager'; import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand'; import { DebugService } from './application/debugService'; -import { DebugSessionTelemetry } from './application/debugSessionTelemetry'; import { DocumentManager } from './application/documentManager'; import { Extensions } from './application/extensions'; import { LanguageService } from './application/languageService'; @@ -183,8 +182,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IExtensionSingleActivationService, ReportIssueCommandHandler, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); } diff --git a/src/client/debugger/constants.ts b/src/client/debugger/constants.ts index 08b8619ce03a..0746de4e23cd 100644 --- a/src/client/debugger/constants.ts +++ b/src/client/debugger/constants.ts @@ -3,4 +3,4 @@ 'use strict'; -export const DebuggerTypeName = 'python'; +export const DebuggerTypeName = 'debugpy'; diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts deleted file mode 100644 index 999c00366ed6..000000000000 --- a/src/client/debugger/extension/adapter/activator.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; -import { Uri } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IExtensionSingleActivationService } from '../../../activation/types'; -import { IDebugService } from '../../../common/application/types'; -import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; -import { ICommandManager } from '../../../common/application/types'; -import { DebuggerTypeName } from '../../constants'; -import { IAttachProcessProviderFactory } from '../attachQuickPick/types'; -import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from '../types'; - -@injectable() -export class DebugAdapterActivator implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - constructor( - @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IConfigurationService) private readonly configSettings: IConfigurationService, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(IDebugAdapterDescriptorFactory) private descriptorFactory: IDebugAdapterDescriptorFactory, - @inject(IDebugSessionLoggingFactory) private debugSessionLoggingFactory: IDebugSessionLoggingFactory, - @inject(IOutdatedDebuggerPromptFactory) private debuggerPromptFactory: IOutdatedDebuggerPromptFactory, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IAttachProcessProviderFactory) - private readonly attachProcessProviderFactory: IAttachProcessProviderFactory, - ) {} - public async activate(): Promise { - this.attachProcessProviderFactory.registerCommands(); - - this.disposables.push( - this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory), - ); - this.disposables.push( - this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory), - ); - - this.disposables.push( - this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory), - ); - this.disposables.push( - this.debugService.onDidStartDebugSession((debugSession) => { - if (this.shouldTerminalFocusOnStart(debugSession.workspaceFolder?.uri)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); - }), - ); - } - - private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { - return this.configSettings.getSettings(uri)?.terminal.focusAfterLaunch; - } -} diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts deleted file mode 100644 index ecbd8afcc287..000000000000 --- a/src/client/debugger/extension/adapter/factory.ts +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { - DebugAdapterDescriptor, - DebugAdapterExecutable, - DebugAdapterServer, - DebugSession, - l10n, - WorkspaceFolder, -} from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { IInterpreterService } from '../../../interpreter/contracts'; -import { traceLog, traceVerbose } from '../../../logging'; -import { PythonEnvironment } from '../../../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { IDebugAdapterDescriptorFactory } from '../types'; -import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; -import { Common, Interpreters } from '../../../common/utils/localize'; -import { IPersistentStateFactory } from '../../../common/types'; -import { Commands } from '../../../common/constants'; -import { ICommandManager } from '../../../common/application/types'; - -// persistent state names, exported to make use of in testing -export enum debugStateKeys { - doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain', -} - -@injectable() -export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory { - constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, - ) {} - - public async createDebugAdapterDescriptor( - session: DebugSession, - _executable: DebugAdapterExecutable | undefined, - ): Promise { - const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments; - - // There are four distinct scenarios here: - // - // 1. "launch"; - // 2. "attach" with "processId"; - // 3. "attach" with "listen"; - // 4. "attach" with "connect" (or legacy "host"/"port"); - // - // For the first three, we want to spawn the debug adapter directly. - // For the last one, the adapter is already listening on the specified socket. - // When "debugServer" is used, the standard adapter factory takes care of it - no need to check here. - - if (configuration.request === 'attach') { - if (configuration.connect !== undefined) { - traceLog( - `Connecting to DAP Server at: ${configuration.connect.host ?? '127.0.0.1'}:${ - configuration.connect.port - }`, - ); - return new DebugAdapterServer(configuration.connect.port, configuration.connect.host ?? '127.0.0.1'); - } else if (configuration.port !== undefined) { - traceLog(`Connecting to DAP Server at: ${configuration.host ?? '127.0.0.1'}:${configuration.port}`); - return new DebugAdapterServer(configuration.port, configuration.host ?? '127.0.0.1'); - } else if (configuration.listen === undefined && configuration.processId === undefined) { - throw new Error('"request":"attach" requires either "connect", "listen", or "processId"'); - } - } - - const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder); - if (command.length !== 0) { - if (configuration.request === 'attach' && configuration.processId !== undefined) { - sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); - } - - const executable = command.shift() ?? 'python'; - - // "logToFile" is not handled directly by the adapter - instead, we need to pass - // the corresponding CLI switch when spawning it. - const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : []; - - if (configuration.debugAdapterPath !== undefined) { - const args = command.concat([configuration.debugAdapterPath, ...logArgs]); - traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); - return new DebugAdapterExecutable(executable, args); - } - - const debuggerAdapterPathToUse = path.join( - EXTENSION_ROOT_DIR, - 'pythonFiles', - 'lib', - 'python', - 'debugpy', - 'adapter', - ); - - const args = command.concat([debuggerAdapterPathToUse, ...logArgs]); - traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); - sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true }); - return new DebugAdapterExecutable(executable, args); - } - - // Unlikely scenario. - throw new Error('Debug Adapter Executable not provided'); - } - - /** - * Get the python executable used to launch the Python Debug Adapter. - * In the case of `attach` scenarios, just use the workspace interpreter, else first available one. - * It is unlike user won't have a Python interpreter - * - * @private - * @param {(LaunchRequestArguments | AttachRequestArguments)} configuration - * @param {WorkspaceFolder} [workspaceFolder] - * @returns {Promise} Path to the python interpreter for this workspace. - * @memberof DebugAdapterDescriptorFactory - */ - private async getDebugAdapterPython( - configuration: LaunchRequestArguments | AttachRequestArguments, - workspaceFolder?: WorkspaceFolder, - ): Promise { - if (configuration.debugAdapterPython !== undefined) { - return this.getExecutableCommand( - await this.interpreterService.getInterpreterDetails(configuration.debugAdapterPython), - ); - } else if (configuration.pythonPath) { - return this.getExecutableCommand( - await this.interpreterService.getInterpreterDetails(configuration.pythonPath), - ); - } - - const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined; - const interpreter = await this.interpreterService.getActiveInterpreter(resourceUri); - if (interpreter) { - traceVerbose(`Selecting active interpreter as Python Executable for DA '${interpreter.path}'`); - return this.getExecutableCommand(interpreter); - } - - await this.interpreterService.hasInterpreters(); // Wait until we know whether we have an interpreter - const interpreters = this.interpreterService.getInterpreters(resourceUri); - if (interpreters.length === 0) { - this.notifySelectInterpreter().ignoreErrors(); - return []; - } - - traceVerbose(`Picking first available interpreter to launch the DA '${interpreters[0].path}'`); - return this.getExecutableCommand(interpreters[0]); - } - - private async showDeprecatedPythonMessage() { - const notificationPromptEnabled = this.persistentState.createGlobalPersistentState( - debugStateKeys.doNotShowAgain, - false, - ); - if (notificationPromptEnabled.value) { - return; - } - const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain]; - const selection = await showErrorMessage( - l10n.t('The debugger in the python extension no longer supports python versions minor than 3.7.'), - { modal: true }, - ...prompts, - ); - if (!selection) { - return; - } - if (selection == Interpreters.changePythonInterpreter) { - await this.commandManager.executeCommand(Commands.Set_Interpreter); - } - if (selection == Common.doNotShowAgain) { - // Never show the message again - await this.persistentState - .createGlobalPersistentState(debugStateKeys.doNotShowAgain, false) - .updateValue(true); - } - } - - private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { - if (interpreter) { - if ( - (interpreter.version?.major ?? 0) < 3 || - ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) <= 6) - ) { - this.showDeprecatedPythonMessage(); - } - return interpreter.path.length > 0 ? [interpreter.path] : []; - } - return []; - } - - /** - * Notify user about the requirement for Python. - * Unlikely scenario, as ex expect users to have Python in order to use the extension. - * However it is possible to ignore the warnings and continue using the extension. - * - * @private - * @memberof DebugAdapterDescriptorFactory - */ - private async notifySelectInterpreter() { - await showErrorMessage(l10n.t('Install Python or select a Python Interpreter to use the debugger.')); - } -} diff --git a/src/client/debugger/extension/adapter/logging.ts b/src/client/debugger/extension/adapter/logging.ts deleted file mode 100644 index 907b895170c6..000000000000 --- a/src/client/debugger/extension/adapter/logging.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { - DebugAdapterTracker, - DebugAdapterTrackerFactory, - DebugConfiguration, - DebugSession, - ProviderResult, -} from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; - -import { IFileSystem, WriteStream } from '../../../common/platform/types'; -import { StopWatch } from '../../../common/utils/stopWatch'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; - -class DebugSessionLoggingTracker implements DebugAdapterTracker { - private readonly enabled: boolean = false; - private stream?: WriteStream; - private timer = new StopWatch(); - - constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { - this.enabled = this.session.configuration.logToFile as boolean; - if (this.enabled) { - const fileName = `debugger.vscode_${this.session.id}.log`; - this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); - } - } - - public onWillStartSession() { - this.timer.reset(); - this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); - } - - public onWillReceiveMessage(message: DebugProtocol.Message) { - this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); - } - - public onDidSendMessage(message: DebugProtocol.Message) { - this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); - } - - public onWillStopSession() { - this.log('Stopping Session\n'); - } - - public onError(error: Error) { - this.log(`Error:\n${this.stringify(error)}\n`); - } - - public onExit(code: number | undefined, signal: string | undefined) { - this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); - this.stream?.close(); - } - - private log(message: string) { - if (this.enabled) { - this.stream!.write(`${this.timer.elapsedTime} ${message}`); // NOSONAR - } - } - - private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { - return JSON.stringify(data, null, 4); - } -} - -@injectable() -export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { - constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) {} - - public createDebugAdapterTracker(session: DebugSession): ProviderResult { - return new DebugSessionLoggingTracker(session, this.fileSystem); - } -} diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts deleted file mode 100644 index 04117e9838d1..000000000000 --- a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; -import { injectable } from 'inversify'; -import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { Common, OutdatedDebugger } from '../../../common/utils/localize'; -import { launch } from '../../../common/vscodeApis/browserApis'; -import { showInformationMessage } from '../../../common/vscodeApis/windowApis'; -import { IPromptShowState } from './types'; - -// This situation occurs when user connects to old containers or server where -// the debugger they had installed was ptvsd. We should show a prompt to ask them to update. -class OutdatedDebuggerPrompt implements DebugAdapterTracker { - constructor(private promptCheck: IPromptShowState) {} - - public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { - if (this.promptCheck.shouldShowPrompt() && this.isPtvsd(message)) { - const prompts = [Common.moreInfo]; - showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts).then((selection) => { - if (selection === prompts[0]) { - launch('https://aka.ms/migrateToDebugpy'); - } - }); - } - } - - private isPtvsd(message: DebugProtocol.ProtocolMessage) { - if (message.type === 'event') { - const eventMessage = message as DebugProtocol.Event; - if (eventMessage.event === 'output') { - const outputMessage = eventMessage as DebugProtocol.OutputEvent; - if (outputMessage.body.category === 'telemetry') { - // debugpy sends telemetry as both ptvsd and debugpy. This was done to help with - // transition from ptvsd to debugpy while analyzing usage telemetry. - if ( - outputMessage.body.output === 'ptvsd' && - !outputMessage.body.data.packageVersion.startsWith('1') - ) { - this.promptCheck.setShowPrompt(false); - return true; - } - if (outputMessage.body.output === 'debugpy') { - this.promptCheck.setShowPrompt(false); - } - } - } - } - return false; - } -} - -class OutdatedDebuggerPromptState implements IPromptShowState { - private shouldShow: boolean = true; - public shouldShowPrompt(): boolean { - return this.shouldShow; - } - public setShowPrompt(show: boolean) { - this.shouldShow = show; - } -} - -@injectable() -export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { - private readonly promptCheck: OutdatedDebuggerPromptState; - constructor() { - this.promptCheck = new OutdatedDebuggerPromptState(); - } - public createDebugAdapterTracker(_session: DebugSession): ProviderResult { - return new OutdatedDebuggerPrompt(this.promptCheck); - } -} diff --git a/src/client/debugger/extension/adapter/types.ts b/src/client/debugger/extension/adapter/types.ts deleted file mode 100644 index 6c082a801ad6..000000000000 --- a/src/client/debugger/extension/adapter/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -export const IPromptShowState = Symbol('IPromptShowState'); -export interface IPromptShowState { - shouldShowPrompt(): boolean; - setShowPrompt(show: boolean): void; -} diff --git a/src/client/debugger/extension/attachQuickPick/factory.ts b/src/client/debugger/extension/attachQuickPick/factory.ts deleted file mode 100644 index 627962106e88..000000000000 --- a/src/client/debugger/extension/attachQuickPick/factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IApplicationShell, ICommandManager } from '../../../common/application/types'; -import { Commands } from '../../../common/constants'; -import { IPlatformService } from '../../../common/platform/types'; -import { IProcessServiceFactory } from '../../../common/process/types'; -import { IDisposableRegistry } from '../../../common/types'; -import { AttachPicker } from './picker'; -import { AttachProcessProvider } from './provider'; -import { IAttachProcessProviderFactory } from './types'; - -@injectable() -export class AttachProcessProviderFactory implements IAttachProcessProviderFactory { - constructor( - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - ) {} - - public registerCommands() { - const provider = new AttachProcessProvider(this.platformService, this.processServiceFactory); - const picker = new AttachPicker(this.applicationShell, provider); - const disposable = this.commandManager.registerCommand( - Commands.PickLocalProcess, - () => picker.showQuickPick(), - this, - ); - this.disposableRegistry.push(disposable); - } -} diff --git a/src/client/debugger/extension/attachQuickPick/picker.ts b/src/client/debugger/extension/attachQuickPick/picker.ts deleted file mode 100644 index a296a9b3163a..000000000000 --- a/src/client/debugger/extension/attachQuickPick/picker.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Disposable } from 'vscode'; -import { IApplicationShell } from '../../../common/application/types'; -import { getIcon } from '../../../common/utils/icons'; -import { AttachProcess } from '../../../common/utils/localize'; -import { IAttachItem, IAttachPicker, IAttachProcessProvider, REFRESH_BUTTON_ICON } from './types'; - -@injectable() -export class AttachPicker implements IAttachPicker { - constructor( - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - private readonly attachItemsProvider: IAttachProcessProvider, - ) {} - - public showQuickPick(): Promise { - return new Promise(async (resolve, reject) => { - const processEntries = await this.attachItemsProvider.getAttachItems(); - - const refreshButton = { - iconPath: getIcon(REFRESH_BUTTON_ICON), - tooltip: AttachProcess.refreshList, - }; - - const quickPick = this.applicationShell.createQuickPick(); - quickPick.title = AttachProcess.attachTitle; - quickPick.placeholder = AttachProcess.selectProcessPlaceholder; - quickPick.canSelectMany = false; - quickPick.matchOnDescription = true; - quickPick.matchOnDetail = true; - quickPick.items = processEntries; - quickPick.buttons = [refreshButton]; - - const disposables: Disposable[] = []; - - quickPick.onDidTriggerButton( - async () => { - quickPick.busy = true; - const attachItems = await this.attachItemsProvider.getAttachItems(); - quickPick.items = attachItems; - quickPick.busy = false; - }, - this, - disposables, - ); - - quickPick.onDidAccept( - () => { - if (quickPick.selectedItems.length !== 1) { - reject(new Error(AttachProcess.noProcessSelected)); - } - - const selectedId = quickPick.selectedItems[0].id; - - disposables.forEach((item) => item.dispose()); - quickPick.dispose(); - - resolve(selectedId); - }, - undefined, - disposables, - ); - - quickPick.onDidHide( - () => { - disposables.forEach((item) => item.dispose()); - quickPick.dispose(); - - reject(new Error(AttachProcess.noProcessSelected)); - }, - undefined, - disposables, - ); - - quickPick.show(); - }); - } -} diff --git a/src/client/debugger/extension/attachQuickPick/provider.ts b/src/client/debugger/extension/attachQuickPick/provider.ts deleted file mode 100644 index 3626d8dfb8ce..000000000000 --- a/src/client/debugger/extension/attachQuickPick/provider.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { l10n } from 'vscode'; -import { IPlatformService } from '../../../common/platform/types'; -import { IProcessServiceFactory } from '../../../common/process/types'; -import { PsProcessParser } from './psProcessParser'; -import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; -import { WmicProcessParser } from './wmicProcessParser'; - -@injectable() -export class AttachProcessProvider implements IAttachProcessProvider { - constructor( - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - ) {} - - public getAttachItems(): Promise { - return this._getInternalProcessEntries().then((processEntries) => { - processEntries.sort( - ( - { processName: aprocessName, commandLine: aCommandLine }, - { processName: bProcessName, commandLine: bCommandLine }, - ) => { - const compare = (aString: string, bString: string): number => { - // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) - // We can change to localeCompare if this becomes an issue - const aLower = aString.toLowerCase(); - const bLower = bString.toLowerCase(); - - if (aLower === bLower) { - return 0; - } - - return aLower < bLower ? -1 : 1; - }; - - const aPython = aprocessName.startsWith('python'); - const bPython = bProcessName.startsWith('python'); - - if (aPython || bPython) { - if (aPython && !bPython) { - return -1; - } - if (bPython && !aPython) { - return 1; - } - - return aPython ? compare(aCommandLine!, bCommandLine!) : compare(bCommandLine!, aCommandLine!); - } - - return compare(aprocessName, bProcessName); - }, - ); - - return processEntries; - }); - } - - public async _getInternalProcessEntries(): Promise { - let processCmd: ProcessListCommand; - if (this.platformService.isMac) { - processCmd = PsProcessParser.psDarwinCommand; - } else if (this.platformService.isLinux) { - processCmd = PsProcessParser.psLinuxCommand; - } else if (this.platformService.isWindows) { - processCmd = WmicProcessParser.wmicCommand; - } else { - throw new Error(l10n.t("Operating system '{0}' not supported.", this.platformService.osType)); - } - - const processService = await this.processServiceFactory.create(); - const output = await processService.exec(processCmd.command, processCmd.args, { throwOnStdErr: true }); - - return this.platformService.isWindows - ? WmicProcessParser.parseProcesses(output.stdout) - : PsProcessParser.parseProcesses(output.stdout); - } -} diff --git a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts deleted file mode 100644 index 843369bd00c7..000000000000 --- a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IAttachItem, ProcessListCommand } from './types'; - -export namespace PsProcessParser { - const secondColumnCharacters = 50; - const commColumnTitle = ''.padStart(secondColumnCharacters, 'a'); - - // Perf numbers: - // OS X 10.10 - // | # of processes | Time (ms) | - // |----------------+-----------| - // | 272 | 52 | - // | 296 | 49 | - // | 384 | 53 | - // | 784 | 116 | - // - // Ubuntu 16.04 - // | # of processes | Time (ms) | - // |----------------+-----------| - // | 232 | 26 | - // | 336 | 34 | - // | 736 | 62 | - // | 1039 | 115 | - // | 1239 | 182 | - - // ps outputs as a table. With the option "ww", ps will use as much width as necessary. - // However, that only applies to the right-most column. Here we use a hack of setting - // the column header to 50 a's so that the second column will have at least that many - // characters. 50 was chosen because that's the maximum length of a "label" in the - // QuickPick UI in VS Code. - - // the BSD version of ps uses '-c' to have 'comm' only output the executable name and not - // the full path. The Linux version of ps has 'comm' to only display the name of the executable - // Note that comm on Linux systems is truncated to 16 characters: - // https://bugzilla.redhat.com/show_bug.cgi?id=429565 - // Since 'args' contains the full path to the executable, even if truncated, searching will work as desired. - export const psLinuxCommand: ProcessListCommand = { - command: 'ps', - args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`], - }; - export const psDarwinCommand: ProcessListCommand = { - command: 'ps', - args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c'], - }; - - export function parseProcesses(processes: string): IAttachItem[] { - const lines: string[] = processes.split('\n'); - return parseProcessesFromPsArray(lines); - } - - function parseProcessesFromPsArray(processArray: string[]): IAttachItem[] { - const processEntries: IAttachItem[] = []; - - // lines[0] is the header of the table - for (let i = 1; i < processArray.length; i += 1) { - const line = processArray[i]; - if (!line) { - continue; - } - - const processEntry = parseLineFromPs(line); - if (processEntry) { - processEntries.push(processEntry); - } - } - - return processEntries; - } - - function parseLineFromPs(line: string): IAttachItem | undefined { - // Explanation of the regex: - // - any leading whitespace - // - PID - // - whitespace - // - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character - // for the whitespace separator - // - whitespace - // - args (might be empty) - const psEntry: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.{${secondColumnCharacters - 1}})\\s+(.*)$`); - const matches = psEntry.exec(line); - - if (matches?.length === 4) { - const pid = matches[1].trim(); - const executable = matches[2].trim(); - const cmdline = matches[3].trim(); - - return { - label: executable, - description: pid, - detail: cmdline, - id: pid, - processName: executable, - commandLine: cmdline, - }; - } - } -} diff --git a/src/client/debugger/extension/attachQuickPick/types.ts b/src/client/debugger/extension/attachQuickPick/types.ts deleted file mode 100644 index 5e26c1354f9e..000000000000 --- a/src/client/debugger/extension/attachQuickPick/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { QuickPickItem } from 'vscode'; - -export type ProcessListCommand = { command: string; args: string[] }; - -export interface IAttachItem extends QuickPickItem { - id: string; - processName: string; - commandLine: string; -} - -export interface IAttachProcessProvider { - getAttachItems(): Promise; -} - -export const IAttachProcessProviderFactory = Symbol('IAttachProcessProviderFactory'); -export interface IAttachProcessProviderFactory { - registerCommands(): void; -} - -export interface IAttachPicker { - showQuickPick(): Promise; -} - -export const REFRESH_BUTTON_ICON = 'refresh.svg'; diff --git a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts deleted file mode 100644 index e1faed50fc2e..000000000000 --- a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IAttachItem, ProcessListCommand } from './types'; - -export namespace WmicProcessParser { - const wmicNameTitle = 'Name'; - const wmicCommandLineTitle = 'CommandLine'; - const wmicPidTitle = 'ProcessId'; - const defaultEmptyEntry: IAttachItem = { - label: '', - description: '', - detail: '', - id: '', - processName: '', - commandLine: '', - }; - - // Perf numbers on Win10: - // | # of processes | Time (ms) | - // |----------------+-----------| - // | 309 | 413 | - // | 407 | 463 | - // | 887 | 746 | - // | 1308 | 1132 | - export const wmicCommand: ProcessListCommand = { - command: 'wmic', - args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list'], - }; - - export function parseProcesses(processes: string): IAttachItem[] { - const lines: string[] = processes.split('\r\n'); - const processEntries: IAttachItem[] = []; - let entry = { ...defaultEmptyEntry }; - - for (const line of lines) { - if (!line.length) { - continue; - } - - parseLineFromWmic(line, entry); - - // Each entry of processes has ProcessId as the last line - if (line.lastIndexOf(wmicPidTitle, 0) === 0) { - processEntries.push(entry); - entry = { ...defaultEmptyEntry }; - } - } - - return processEntries; - } - - function parseLineFromWmic(line: string, item: IAttachItem): IAttachItem { - const splitter = line.indexOf('='); - const currentItem = item; - - if (splitter > 0) { - const key = line.slice(0, splitter).trim(); - let value = line.slice(splitter + 1).trim(); - - if (key === wmicNameTitle) { - currentItem.label = value; - currentItem.processName = value; - } else if (key === wmicPidTitle) { - currentItem.description = value; - currentItem.id = value; - } else if (key === wmicCommandLineTitle) { - const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178 - if (value.lastIndexOf(dosDevicePrefix, 0) === 0) { - value = value.slice(dosDevicePrefix.length); - } - - currentItem.detail = value; - currentItem.commandLine = value; - } - } - - return currentItem; - } -} diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts deleted file mode 100644 index 80a1e3a8a8c4..000000000000 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { cloneDeep } from 'lodash'; -import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../common/utils/localize'; -import { - IMultiStepInputFactory, - InputStep, - IQuickPickParameters, - MultiStepInput, -} from '../../../common/utils/multiStepInput'; -import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types'; -import { buildDjangoLaunchDebugConfiguration } from './providers/djangoLaunch'; -import { buildFastAPILaunchDebugConfiguration } from './providers/fastapiLaunch'; -import { buildFileLaunchDebugConfiguration } from './providers/fileLaunch'; -import { buildFlaskLaunchDebugConfiguration } from './providers/flaskLaunch'; -import { buildModuleLaunchConfiguration } from './providers/moduleLaunch'; -import { buildPidAttachConfiguration } from './providers/pidAttach'; -import { buildPyramidLaunchConfiguration } from './providers/pyramidLaunch'; -import { buildRemoteAttachConfiguration } from './providers/remoteAttach'; -import { IDebugConfigurationResolver } from './types'; - -@injectable() -export class PythonDebugConfigurationService implements IDebugConfigurationService { - private cacheDebugConfig: DebugConfiguration | undefined = undefined; - - constructor( - @inject(IDebugConfigurationResolver) - @named('attach') - private readonly attachResolver: IDebugConfigurationResolver, - @inject(IDebugConfigurationResolver) - @named('launch') - private readonly launchResolver: IDebugConfigurationResolver, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - ) {} - - public async provideDebugConfigurations( - folder: WorkspaceFolder | undefined, - token?: CancellationToken, - ): Promise { - const config: Partial = {}; - const state = { config, folder, token }; - - // Disabled until configuration issues are addressed by VS Code. See #4007 - const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => PythonDebugConfigurationService.pickDebugConfiguration(input, s), state); - - if (Object.keys(state.config).length !== 0) { - return [state.config as DebugConfiguration]; - } - return undefined; - } - - public async resolveDebugConfiguration( - folder: WorkspaceFolder | undefined, - debugConfiguration: DebugConfiguration, - token?: CancellationToken, - ): Promise { - if (debugConfiguration.request === 'attach') { - return this.attachResolver.resolveDebugConfiguration( - folder, - debugConfiguration as AttachRequestArguments, - token, - ); - } - if (debugConfiguration.request === 'test') { - // `"request": "test"` is now deprecated. But some users might have it in their - // launch config. We get here if they triggered it using F5 or start with debugger. - throw Error( - 'This configuration can only be used by the test debugging commands. `"request": "test"` is deprecated, please keep as `"request": "launch"` and add `"purpose": ["debug-test"]` instead.', - ); - } else { - if (Object.keys(debugConfiguration).length === 0) { - if (this.cacheDebugConfig) { - debugConfiguration = cloneDeep(this.cacheDebugConfig); - } else { - const configs = await this.provideDebugConfigurations(folder, token); - if (configs === undefined) { - return undefined; - } - if (Array.isArray(configs) && configs.length === 1) { - // eslint-disable-next-line prefer-destructuring - debugConfiguration = configs[0]; - } - this.cacheDebugConfig = cloneDeep(debugConfiguration); - } - } - return this.launchResolver.resolveDebugConfiguration( - folder, - debugConfiguration as LaunchRequestArguments, - token, - ); - } - } - - public async resolveDebugConfigurationWithSubstitutedVariables( - folder: WorkspaceFolder | undefined, - debugConfiguration: DebugConfiguration, - token?: CancellationToken, - ): Promise { - function resolve(resolver: IDebugConfigurationResolver) { - return resolver.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration as T, token); - } - return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver); - } - - // eslint-disable-next-line consistent-return - protected static async pickDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ): Promise | void> { - type DebugConfigurationQuickPickItemFunc = ( - input: MultiStepInput, - state: DebugConfigurationState, - ) => Promise>; - type DebugConfigurationQuickPickItem = QuickPickItem & { - type: DebugConfigurationType; - func: DebugConfigurationQuickPickItemFunc; - }; - const items: DebugConfigurationQuickPickItem[] = [ - { - func: buildFileLaunchDebugConfiguration, - label: DebugConfigStrings.file.selectConfiguration.label, - type: DebugConfigurationType.launchFile, - description: DebugConfigStrings.file.selectConfiguration.description, - }, - { - func: buildModuleLaunchConfiguration, - label: DebugConfigStrings.module.selectConfiguration.label, - type: DebugConfigurationType.launchModule, - description: DebugConfigStrings.module.selectConfiguration.description, - }, - { - func: buildRemoteAttachConfiguration, - label: DebugConfigStrings.attach.selectConfiguration.label, - type: DebugConfigurationType.remoteAttach, - description: DebugConfigStrings.attach.selectConfiguration.description, - }, - { - func: buildPidAttachConfiguration, - label: DebugConfigStrings.attachPid.selectConfiguration.label, - type: DebugConfigurationType.pidAttach, - description: DebugConfigStrings.attachPid.selectConfiguration.description, - }, - { - func: buildDjangoLaunchDebugConfiguration, - label: DebugConfigStrings.django.selectConfiguration.label, - type: DebugConfigurationType.launchDjango, - description: DebugConfigStrings.django.selectConfiguration.description, - }, - { - func: buildFastAPILaunchDebugConfiguration, - label: DebugConfigStrings.fastapi.selectConfiguration.label, - type: DebugConfigurationType.launchFastAPI, - description: DebugConfigStrings.fastapi.selectConfiguration.description, - }, - { - func: buildFlaskLaunchDebugConfiguration, - label: DebugConfigStrings.flask.selectConfiguration.label, - type: DebugConfigurationType.launchFlask, - description: DebugConfigStrings.flask.selectConfiguration.description, - }, - { - func: buildPyramidLaunchConfiguration, - label: DebugConfigStrings.pyramid.selectConfiguration.label, - type: DebugConfigurationType.launchPyramid, - description: DebugConfigStrings.pyramid.selectConfiguration.description, - }, - ]; - const debugConfigurations = new Map(); - for (const config of items) { - debugConfigurations.set(config.type, config.func); - } - - state.config = {}; - const pick = await input.showQuickPick< - DebugConfigurationQuickPickItem, - IQuickPickParameters - >({ - title: DebugConfigStrings.selectConfiguration.title, - placeholder: DebugConfigStrings.selectConfiguration.placeholder, - activeItem: items[0], - items, - }); - if (pick) { - const pickedDebugConfiguration = debugConfigurations.get(pick.type)!; - return pickedDebugConfiguration(input, state); - } - } -} diff --git a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts deleted file mode 100644 index e79f201d9367..000000000000 --- a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { injectable } from 'inversify'; -import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; -import { IDynamicDebugConfigurationService } from '../types'; -import { DebuggerTypeName } from '../../constants'; -import { asyncFilter } from '../../../common/utils/arrayUtils'; -import { replaceAll } from '../../../common/stringUtils'; - -const workspaceFolderToken = '${workspaceFolder}'; - -@injectable() -export class DynamicPythonDebugConfigurationService implements IDynamicDebugConfigurationService { - // eslint-disable-next-line class-methods-use-this - public async provideDebugConfigurations( - folder: WorkspaceFolder, - _token?: CancellationToken, - ): Promise { - const providers = []; - - providers.push({ - name: 'Python: File', - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - justMyCode: true, - }); - - const djangoManagePath = await DynamicPythonDebugConfigurationService.getDjangoPath(folder); - if (djangoManagePath) { - providers.push({ - name: 'Python: Django', - type: DebuggerTypeName, - request: 'launch', - program: `${workspaceFolderToken}${path.sep}${djangoManagePath}`, - args: ['runserver'], - django: true, - justMyCode: true, - }); - } - - const flaskPath = await DynamicPythonDebugConfigurationService.getFlaskPath(folder); - if (flaskPath) { - providers.push({ - name: 'Python: Flask', - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: path.relative(folder.uri.fsPath, flaskPath), - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }); - } - - let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder); - if (fastApiPath) { - fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPath), path.sep, '.').replace('.py', ''); - providers.push({ - name: 'Python: FastAPI', - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: [`${fastApiPath}:app`, '--reload'], - jinja: true, - justMyCode: true, - }); - } - - return providers; - } - - private static async getDjangoPath(folder: WorkspaceFolder) { - const regExpression = /execute_from_command_line\(/; - const possiblePaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['manage.py', '*/manage.py', 'app.py', '*/app.py'], - regExpression, - ); - return possiblePaths.length ? path.relative(folder.uri.fsPath, possiblePaths[0]) : null; - } - - private static async getFastApiPath(folder: WorkspaceFolder) { - const regExpression = /app\s*=\s*FastAPI\(/; - const fastApiPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['main.py', 'app.py', '*/main.py', '*/app.py', '*/*/main.py', '*/*/app.py'], - regExpression, - ); - - return fastApiPaths.length ? fastApiPaths[0] : null; - } - - private static async getFlaskPath(folder: WorkspaceFolder) { - const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/; - const flaskPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( - folder, - ['__init__.py', 'app.py', 'wsgi.py', '*/__init__.py', '*/app.py', '*/wsgi.py'], - regExpression, - ); - - return flaskPaths.length ? flaskPaths[0] : null; - } - - private static async getPossiblePaths( - folder: WorkspaceFolder, - globPatterns: string[], - regex: RegExp, - ): Promise { - const foundPathsPromises = (await Promise.allSettled( - globPatterns.map( - async (pattern): Promise => - (await fs.pathExists(path.join(folder.uri.fsPath, pattern))) - ? [path.join(folder.uri.fsPath, pattern)] - : [], - ), - )) as { status: string; value: [] }[]; - const possiblePaths: string[] = []; - foundPathsPromises.forEach((result) => possiblePaths.push(...result.value)); - const finalPaths = await asyncFilter(possiblePaths, async (possiblePath) => - regex.exec((await fs.readFile(possiblePath)).toString()), - ); - - return finalPaths; - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts deleted file mode 100644 index c3b243fe9065..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { getLocation } from 'jsonc-parser'; -import * as path from 'path'; -import { - CancellationToken, - CompletionItem, - CompletionItemKind, - CompletionItemProvider, - Position, - SnippetString, - TextDocument, -} from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ILanguageService } from '../../../../common/application/types'; -import { IDisposableRegistry } from '../../../../common/types'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; - -const configurationNodeName = 'configurations'; -enum JsonLanguages { - json = 'json', - jsonWithComments = 'jsonc', -} - -@injectable() -export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(ILanguageService) private readonly languageService: ILanguageService, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - ) {} - - public async activate(): Promise { - this.disposableRegistry.push( - this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this), - ); - this.disposableRegistry.push( - this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this), - ); - } - - // eslint-disable-next-line class-methods-use-this - public async provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - if (!LaunchJsonCompletionProvider.canProvideCompletions(document, position)) { - return []; - } - - return [ - { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description, - arguments: [document, position, token], - }, - documentation: DebugConfigStrings.launchJsonCompletions.description, - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label, - insertText: new SnippetString(), - }, - ]; - } - - public static canProvideCompletions(document: TextDocument, position: Position): boolean { - if (path.basename(document.uri.fsPath) !== 'launch.json') { - return false; - } - const location = getLocation(document.getText(), document.offsetAt(position)); - // Cursor must be inside the configurations array and not in any nested items. - // Hence path[0] = array, path[1] = array element index. - return location.path[0] === configurationNodeName && location.path.length === 2; - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts deleted file mode 100644 index 21c8d0f1147b..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ /dev/null @@ -1,51 +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 { IExtensionSingleActivationService } from '../../../../activation/types'; -import { Commands } from '../../../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IInterpreterService } from '../../../../interpreter/contracts'; - -@injectable() -export class InterpreterPathCommand implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IDisposableRegistry) private readonly disposables: IDisposable[], - ) {} - - public async activate(): Promise { - this.disposables.push( - registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), - ); - } - - public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { - // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder - // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder - let workspaceFolder; - if ('workspaceFolder' in args) { - workspaceFolder = args.workspaceFolder; - } else if (args[1]) { - const [, second] = args; - workspaceFolder = second; - } else { - workspaceFolder = undefined; - } - - let workspaceFolderUri; - try { - workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; - } catch (ex) { - workspaceFolderUri = undefined; - } - - return (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/src/client/debugger/extension/configuration/launch.json/updaterService.ts deleted file mode 100644 index b95749040f3c..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/updaterService.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IDebugConfigurationService } from '../../types'; -import { LaunchJsonUpdaterServiceHelper } from './updaterServiceHelper'; - -@injectable() -export class LaunchJsonUpdaterService implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService, - ) {} - - public async activate(): Promise { - const handler = new LaunchJsonUpdaterServiceHelper(this.configurationProvider); - this.disposableRegistry.push( - registerCommand('python.SelectAndInsertDebugConfiguration', handler.selectAndInsertDebugConfig, handler), - ); - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts b/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts deleted file mode 100644 index bc0820fa188f..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; -import { CancellationToken, DebugConfiguration, Position, Range, TextDocument, WorkspaceEdit } from 'vscode'; -import { noop } from '../../../../common/utils/misc'; -import { executeCommand } from '../../../../common/vscodeApis/commandApis'; -import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; -import { applyEdit, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; -import { captureTelemetry } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { IDebugConfigurationService } from '../../types'; - -type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; -type PositionOfComma = 'BeforeCursor'; - -export class LaunchJsonUpdaterServiceHelper { - constructor(private readonly configurationProvider: IDebugConfigurationService) {} - - @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) - public async selectAndInsertDebugConfig( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - const activeTextEditor = getActiveTextEditor(); - if (activeTextEditor && activeTextEditor.document === document) { - const folder = getWorkspaceFolder(document.uri); - const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); - - if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { - // Always use the first available debug configuration. - await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document, position, configs[0]); - } - } - } - - /** - * Inserts the debug configuration into the document. - * Invokes the document formatter to ensure JSON is formatted nicely. - * @param {TextDocument} document - * @param {Position} position - * @param {DebugConfiguration} config - * @returns {Promise} - * @memberof LaunchJsonCompletionItemProvider - */ - public static async insertDebugConfiguration( - document: TextDocument, - position: Position, - config: DebugConfiguration, - ): Promise { - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document, - position, - ); - if (!cursorPosition) { - return; - } - const commaPosition = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document, position) - ? 'BeforeCursor' - : undefined; - const formattedJson = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, cursorPosition, commaPosition); - const workspaceEdit = new WorkspaceEdit(); - workspaceEdit.insert(document.uri, position, formattedJson); - await applyEdit(workspaceEdit); - executeCommand('editor.action.formatDocument').then(noop, noop); - } - - /** - * Gets the string representation of the debug config for insertion in the document. - * Adds necessary leading or trailing commas (remember the text is added into an array). - * @param {DebugConfiguration} config - * @param {PositionOfCursor} cursorPosition - * @param {PositionOfComma} [commaPosition] - * @returns - * @memberof LaunchJsonCompletionItemProvider - */ - public static getTextForInsertion( - config: DebugConfiguration, - cursorPosition: PositionOfCursor, - commaPosition?: PositionOfComma, - ): string { - const json = JSON.stringify(config); - if (cursorPosition === 'AfterItem') { - // If we already have a comma immediatley before the cursor, then no need of adding a comma. - return commaPosition === 'BeforeCursor' ? json : `,${json}`; - } - if (cursorPosition === 'BeforeItem') { - return `${json},`; - } - return json; - } - - public static getCursorPositionInConfigurationsArray( - document: TextDocument, - position: Position, - ): PositionOfCursor | undefined { - if (LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document)) { - return 'InsideEmptyArray'; - } - const scanner = createScanner(document.getText(), true); - scanner.setPosition(document.offsetAt(position)); - const nextToken = scanner.scan(); - if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { - return 'AfterItem'; - } - if (nextToken === SyntaxKind.OpenBraceToken) { - return 'BeforeItem'; - } - return undefined; - } - - public static isConfigurationArrayEmpty(document: TextDocument): boolean { - const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { - configurations: []; - }; - return ( - !configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0 - ); - } - - public static isCommaImmediatelyBeforeCursor(document: TextDocument, position: Position): boolean { - const line = document.lineAt(position.line); - // Get text from start of line until the cursor. - const currentLine = document.getText(new Range(line.range.start, position)); - if (currentLine.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (currentLine.trim().length !== 0) { - return false; - } - - // Keep walking backwards until we hit a non-comma character or a comm character. - let startLineNumber = position.line - 1; - while (startLineNumber > 0) { - const lineText = document.lineAt(startLineNumber).text; - if (lineText.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (lineText.trim().length !== 0) { - return false; - } - startLineNumber -= 1; - } - return false; - } -} diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts deleted file mode 100644 index 4e1513ccb1ea..000000000000 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; - -const workspaceFolderToken = '${workspaceFolder}'; - -export async function buildDjangoLaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const program = await getManagePyPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const defaultProgram = `${workspaceFolderToken}${path.sep}manage.py`; - const config: Partial = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: program || defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - if (!program) { - const selectedProgram = await input.showInputBox({ - title: DebugConfigStrings.django.enterManagePyPath.title, - value: defaultProgram, - prompt: DebugConfigStrings.django.enterManagePyPath.prompt, - validate: (value) => validateManagePy(state.folder, defaultProgram, value), - }); - if (selectedProgram) { - manuallyEnteredAValue = true; - config.program = selectedProgram; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchDjango, - autoDetectedDjangoManagePyPath: !!program, - manuallyEnteredAValue, - }); - - Object.assign(state.config, config); -} - -export async function validateManagePy( - folder: vscode.WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, -): Promise { - const error = DebugConfigStrings.django.enterManagePyPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = resolveVariables(selected, undefined, folder); - if (resolvedPath) { - if (selected !== defaultValue && !(await fs.pathExists(resolvedPath))) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { - return error; - } - } - return undefined; -} - -export async function getManagePyPath(folder: vscode.WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${path.sep}manage.py`; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts deleted file mode 100644 index 38a9b7ccf1a2..000000000000 --- a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFastAPILaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const application = await getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app', '--reload'], - jinja: true, - justMyCode: true, - }; - - if (!application && config.args) { - const selectedPath = await input.showInputBox({ - title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, - value: 'main.py', - prompt: DebugConfigStrings.fastapi.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.fastapi.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedPath) { - manuallyEnteredAValue = true; - config.args[0] = `${path.basename(selectedPath, '.py').replace('/', '.')}:app`; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFastAPI, - autoDetectedFastAPIMainPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} -export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'main.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return 'main.py'; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts deleted file mode 100644 index edda7ed7e22d..000000000000 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFileLaunchDebugConfiguration( - _input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const config: Partial = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFile, - }); - Object.assign(state.config, config); -} diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts deleted file mode 100644 index d85258c800c6..000000000000 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildFlaskLaunchDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const application = await getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: application || 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - if (!application) { - const selectedApp = await input.showInputBox({ - title: DebugConfigStrings.flask.enterAppPathOrNamePath.title, - value: 'app.py', - prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 - ? undefined - : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid, - ), - }); - if (selectedApp) { - manuallyEnteredAValue = true; - config.env!.FLASK_APP = selectedApp; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFlask, - autoDetectedFlaskAppPyPath: !!application, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} -export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return 'app.py'; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts deleted file mode 100644 index 16787296ce7c..000000000000 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildModuleLaunchConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - const selectedModule = await input.showInputBox({ - title: DebugConfigStrings.module.enterModule.title, - value: config.module || DebugConfigStrings.module.enterModule.default, - prompt: DebugConfigStrings.module.enterModule.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.module.enterModule.invalid, - ), - }); - if (selectedModule) { - manuallyEnteredAValue = true; - config.module = selectedModule; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchModule, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} diff --git a/src/client/debugger/extension/configuration/providers/pidAttach.ts b/src/client/debugger/extension/configuration/providers/pidAttach.ts deleted file mode 100644 index fc0d66874470..000000000000 --- a/src/client/debugger/extension/configuration/providers/pidAttach.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -export async function buildPidAttachConfiguration( - _input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const config: Partial = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.pidAttach, - }); - Object.assign(state.config, config); -} diff --git a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts deleted file mode 100644 index 315e204e7bf8..000000000000 --- a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import { l10n, WorkspaceFolder } from 'vscode'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; - -const workspaceFolderToken = '${workspaceFolder}'; - -export async function buildPyramidLaunchConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise { - const iniPath = await getDevelopmentIniPath(state.folder); - const defaultIni = `${workspaceFolderToken}${path.sep}development.ini`; - let manuallyEnteredAValue: boolean | undefined; - - const config: Partial = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [iniPath || defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - if (!iniPath) { - const selectedIniPath = await input.showInputBox({ - title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title, - value: defaultIni, - prompt: l10n.t( - 'Enter the path to development.ini ({0} points to the root of the current workspace folder)', - workspaceFolderToken, - ), - validate: (value) => validateIniPath(state ? state.folder : undefined, defaultIni, value), - }); - if (selectedIniPath) { - manuallyEnteredAValue = true; - config.args = [selectedIniPath]; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchPyramid, - autoDetectedPyramidIniPath: !!iniPath, - manuallyEnteredAValue, - }); - Object.assign(state.config, config); -} - -export async function validateIniPath( - folder: WorkspaceFolder | undefined, - defaultValue: string, - selected?: string, -): Promise { - if (!folder) { - return undefined; - } - const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid; - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = resolveVariables(selected, undefined, folder); - if (resolvedPath) { - if (selected !== defaultValue && !fs.pathExists(resolvedPath)) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { - return error; - } - } - return undefined; -} - -export async function getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return undefined; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); - if (await fs.pathExists(defaultLocationOfManagePy)) { - return `${workspaceFolderToken}${path.sep}development.ini`; - } - return undefined; -} diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts deleted file mode 100644 index a43c48b664af..000000000000 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { configurePort } from '../utils/configuration'; - -const defaultHost = 'localhost'; -const defaultPort = 5678; - -export async function buildRemoteAttachConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, -): Promise | void> { - const config: Partial = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: defaultHost, - port: defaultPort, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - const connect = config.connect!; - connect.host = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemoteHost.title, - step: 1, - totalSteps: 2, - value: connect.host || defaultHost, - prompt: DebugConfigStrings.attach.enterRemoteHost.prompt, - validate: (value) => - Promise.resolve( - value && value.trim().length > 0 ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid, - ), - }); - if (!connect.host) { - connect.host = defaultHost; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.host !== defaultHost, - }); - Object.assign(state.config, config); - return (_) => configurePort(input, state.config); -} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts deleted file mode 100644 index bdc72680d861..000000000000 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; -import { getOSType, OSType } from '../../../../common/utils/platform'; -import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; -import { BaseConfigurationResolver } from './base'; - -@injectable() -export class AttachConfigurationResolver extends BaseConfigurationResolver { - public async resolveDebugConfigurationWithSubstitutedVariables( - folder: WorkspaceFolder | undefined, - debugConfiguration: AttachRequestArguments, - _token?: CancellationToken, - ): Promise { - const workspaceFolder = AttachConfigurationResolver.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, - ); - } - if (debugConfiguration.clientOS === undefined) { - debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; - } - return debugConfiguration; - } - - protected async provideAttachDefaults( - workspaceFolder: Uri | undefined, - debugConfiguration: AttachRequestArguments, - ): Promise { - if (!Array.isArray(debugConfiguration.debugOptions)) { - debugConfiguration.debugOptions = []; - } - if (!(debugConfiguration.connect || debugConfiguration.listen) && !debugConfiguration.host) { - // Connect and listen cannot be mixed with host property. - debugConfiguration.host = 'localhost'; - } - if (debugConfiguration.justMyCode === undefined) { - // Populate justMyCode using debugStdLib - debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; - } - debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; - // 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.justMyCode) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.DebugStdLib); - } - if (debugConfiguration.django) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Django); - } - if (debugConfiguration.jinja) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.subProcess === true) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.SubProcess); - } - if ( - debugConfiguration.pyramid && - debugOptions.indexOf(DebugOptions.Jinja) === -1 && - debugConfiguration.jinja !== false - ) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { - AttachConfigurationResolver.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 = AttachConfigurationResolver.isLocalHost(debugConfiguration.host); - if (getOSType() === OSType.Windows && isLocalHost) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.FixFilePathCase); - } - if (debugConfiguration.clientOS === undefined) { - debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; - } - if (debugConfiguration.showReturnValue) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); - } - - debugConfiguration.pathMappings = this.resolvePathMappings( - debugConfiguration.pathMappings || [], - debugConfiguration.host, - debugConfiguration.localRoot, - debugConfiguration.remoteRoot, - workspaceFolder, - ); - AttachConfigurationResolver.sendTelemetry('attach', debugConfiguration); - } - - // eslint-disable-next-line class-methods-use-this - private resolvePathMappings( - pathMappings: PathMapping[], - host?: string, - localRoot?: string, - remoteRoot?: string, - workspaceFolder?: Uri, - ) { - // This is for backwards compatibility. - if (localRoot && remoteRoot) { - pathMappings.push({ - localRoot, - remoteRoot, - }); - } - // If attaching to local host, then always map local root and remote roots. - if (AttachConfigurationResolver.isLocalHost(host)) { - pathMappings = AttachConfigurationResolver.fixUpPathMappings( - pathMappings, - workspaceFolder ? workspaceFolder.fsPath : '', - ); - } - return pathMappings.length > 0 ? pathMappings : undefined; - } -} diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index 795d06abf6d0..ac182ebc0ef0 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -16,7 +16,7 @@ import { IInterpreterService } from '../../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTelemetry } from '../../../../telemetry/types'; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; +import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; import { resolveVariables } from '../utils/common'; @@ -217,21 +217,17 @@ export abstract class BaseConfigurationResolver return pathMappings; } - protected static isDebuggingFastAPI( - debugConfiguration: Partial, - ): boolean { + protected static isDebuggingFastAPI(debugConfiguration: Partial): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } - protected static isDebuggingFlask( - debugConfiguration: Partial, - ): boolean { + protected static isDebuggingFlask(debugConfiguration: Partial): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } protected static sendTelemetry( trigger: 'launch' | 'attach' | 'test', - debugConfiguration: Partial, + debugConfiguration: Partial, ): void { const name = debugConfiguration.name || ''; const moduleName = debugConfiguration.module || ''; diff --git a/src/client/debugger/extension/configuration/utils/configuration.ts b/src/client/debugger/extension/configuration/utils/configuration.ts deleted file mode 100644 index 37fb500dbfdd..000000000000 --- a/src/client/debugger/extension/configuration/utils/configuration.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType } from '../../types'; - -const defaultPort = 5678; - -export async function configurePort( - input: MultiStepInput, - config: Partial, -): Promise { - const connect = config.connect || (config.connect = {}); - const port = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemotePort.title, - step: 2, - totalSteps: 2, - value: (connect.port || defaultPort).toString(), - prompt: DebugConfigStrings.attach.enterRemotePort.prompt, - validate: (value) => - Promise.resolve( - value && /^\d+$/.test(value.trim()) ? undefined : DebugConfigStrings.attach.enterRemotePort.invalid, - ), - }); - if (port && /^\d+$/.test(port.trim())) { - connect.port = parseInt(port, 10); - } - if (!connect.port) { - connect.port = defaultPort; - } - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.remoteAttach, - manuallyEnteredAValue: connect.port !== defaultPort, - }); -} diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts deleted file mode 100644 index b3322e8e7dd1..000000000000 --- a/src/client/debugger/extension/debugCommands.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { inject, injectable } from 'inversify'; -import { DebugConfiguration, Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { ICommandManager, IDebugService } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IDisposableRegistry } from '../../common/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { DebugPurpose, LaunchRequestArguments } from '../types'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { noop } from '../../common/utils/misc'; -import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; -import { - CreateEnvironmentCheckKind, - triggerCreateEnvironmentCheckNonBlocking, -} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; - -@injectable() -export class DebugCommands implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - ) {} - - public activate(): Promise { - this.disposables.push( - this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { - sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); - const interpreter = await this.interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); - triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); - const config = await DebugCommands.getDebugConfiguration(file); - this.debugService.startDebugging(undefined, config); - }), - ); - return Promise.resolve(); - } - - private static async getDebugConfiguration(uri?: Uri): Promise { - const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); - for (const config of configs) { - if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { - if (!config.program && !config.module && !config.code) { - // This is only needed if people reuse debug-test for debug-in-terminal - config.program = uri?.fsPath ?? '${file}'; - } - // Ensure that the purpose is cleared, this is so we can track if people accidentally - // trigger this via F5 or Start with debugger. - config.purpose = []; - return config; - } - } - return { - name: `Debug ${uri ? path.basename(uri.fsPath) : 'File'}`, - type: 'python', - request: 'launch', - program: uri?.fsPath ?? '${file}', - console: 'integratedTerminal', - }; - } -} diff --git a/src/client/debugger/extension/helpers/protocolParser.ts b/src/client/debugger/extension/helpers/protocolParser.ts deleted file mode 100644 index c0d1306a841b..000000000000 --- a/src/client/debugger/extension/helpers/protocolParser.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { EventEmitter } from 'events'; -import { injectable } from 'inversify'; -import { Readable } from 'stream'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { IProtocolParser } from '../types'; - -const PROTOCOL_START_INDENTIFIER = '\r\n\r\n'; - -type Listener = (...args: unknown[]) => void; - -/** - * Parsers the debugger Protocol messages and raises the following events: - * 1. 'data', message (for all protocol messages) - * 1. 'event_', message (for all protocol events) - * 1. 'request_', message (for all protocol requests) - * 1. 'response_', message (for all protocol responses) - * 1. '', message (for all protocol messages that are not events, requests nor responses) - * @export - * @class ProtocolParser - * @extends {EventEmitter} - * @implements {IProtocolParser} - */ -@injectable() -export class ProtocolParser implements IProtocolParser { - private rawData = Buffer.alloc(0); - - private contentLength = -1; - - private disposed = false; - - private stream?: Readable; - - private events: EventEmitter; - - constructor() { - this.events = new EventEmitter(); - } - - public dispose(): void { - if (this.stream) { - this.stream.removeListener('data', this.dataCallbackHandler); - this.stream = undefined; - } - } - - public connect(stream: Readable): void { - this.stream = stream; - stream.addListener('data', this.dataCallbackHandler); - } - - public on(event: string | symbol, listener: Listener): this { - this.events.on(event, listener); - return this; - } - - public once(event: string | symbol, listener: Listener): this { - this.events.once(event, listener); - return this; - } - - private dataCallbackHandler = (data: string | Buffer) => { - this.handleData(data as Buffer); - }; - - private dispatch(body: string): void { - const message = JSON.parse(body) as DebugProtocol.ProtocolMessage; - - switch (message.type) { - case 'event': { - const event = message as DebugProtocol.Event; - if (typeof event.event === 'string') { - this.events.emit(`${message.type}_${event.event}`, event); - } - break; - } - case 'request': { - const request = message as DebugProtocol.Request; - if (typeof request.command === 'string') { - this.events.emit(`${message.type}_${request.command}`, request); - } - break; - } - case 'response': { - const reponse = message as DebugProtocol.Response; - if (typeof reponse.command === 'string') { - this.events.emit(`${message.type}_${reponse.command}`, reponse); - } - break; - } - default: { - this.events.emit(`${message.type}`, message); - } - } - - this.events.emit('data', message); - } - - private handleData(data: Buffer): void { - if (this.disposed) { - return; - } - this.rawData = Buffer.concat([this.rawData, data]); - - // eslint-disable-next-line no-constant-condition - while (true) { - if (this.contentLength >= 0) { - if (this.rawData.length >= this.contentLength) { - const message = this.rawData.toString('utf8', 0, this.contentLength); - this.rawData = this.rawData.slice(this.contentLength); - this.contentLength = -1; - if (message.length > 0) { - this.dispatch(message); - } - // there may be more complete messages to process. - // eslint-disable-next-line no-continue - continue; - } - } else { - const idx = this.rawData.indexOf(PROTOCOL_START_INDENTIFIER); - if (idx !== -1) { - const header = this.rawData.toString('utf8', 0, idx); - const lines = header.split('\r\n'); - for (const line of lines) { - const pair = line.split(/: +/); - if (pair[0] === 'Content-Length') { - this.contentLength = +pair[1]; - } - } - this.rawData = this.rawData.slice(idx + PROTOCOL_START_INDENTIFIER.length); - // eslint-disable-next-line no-continue - continue; - } - } - break; - } - } -} diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts deleted file mode 100644 index 23602ffce086..000000000000 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { DebugConfiguration, DebugSessionCustomEvent } from 'vscode'; -import { swallowExceptions } from '../../../common/utils/decorators'; -import { AttachRequestArguments } from '../../types'; -import { DebuggerEvents } from './constants'; -import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; -import { DebuggerTypeName } from '../../constants'; - -/** - * This class is responsible for automatically attaching the debugger to any - * child processes launched. I.e. this is the class responsible for multi-proc debugging. - * @export - * @class ChildProcessAttachEventHandler - * @implements {IDebugSessionEventHandlers} - */ -@injectable() -export class ChildProcessAttachEventHandler implements IDebugSessionEventHandlers { - constructor( - @inject(IChildProcessAttachService) private readonly childProcessAttachService: IChildProcessAttachService, - ) {} - - @swallowExceptions('Handle child process launch') - public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { - if (!event || event.session.configuration.type !== DebuggerTypeName) { - return; - } - - let data: AttachRequestArguments & DebugConfiguration; - if ( - event.event === DebuggerEvents.PtvsdAttachToSubprocess || - event.event === DebuggerEvents.DebugpyAttachToSubprocess - ) { - data = event.body as AttachRequestArguments & DebugConfiguration; - } else { - return; - } - - if (Object.keys(data).length > 0) { - await this.childProcessAttachService.attach(data, event.session); - } - } -} diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts deleted file mode 100644 index 08f44bc3cea5..000000000000 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IDebugService } from '../../../common/application/types'; -import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; -import { noop } from '../../../common/utils/misc'; -import { captureTelemetry } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; -import { AttachRequestArguments } from '../../types'; -import { IChildProcessAttachService } from './types'; -import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; -import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; - -/** - * This class is responsible for attaching the debugger to any - * child processes launched. I.e. this is the class responsible for multi-proc debugging. - * @export - * @class ChildProcessAttachEventHandler - * @implements {IChildProcessAttachService} - */ -@injectable() -export class ChildProcessAttachService implements IChildProcessAttachService { - constructor(@inject(IDebugService) private readonly debugService: IDebugService) {} - - @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) - public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { - const debugConfig: AttachRequestArguments & DebugConfiguration = data; - const folder = this.getRelatedWorkspaceFolder(debugConfig); - const debugSessionOption: DebugSessionOptions = { - parentSession: parentSession, - lifecycleManagedByParent: true, - }; - const launched = await this.debugService.startDebugging(folder, debugConfig, debugSessionOption); - if (!launched) { - showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( - noop, - noop, - ); - } - } - - private getRelatedWorkspaceFolder( - config: AttachRequestArguments & DebugConfiguration, - ): WorkspaceFolder | undefined { - const workspaceFolder = config.workspaceFolder; - - const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; - if (!hasWorkspaceFolders || !workspaceFolder) { - return; - } - return getWorkspaceFolders()!.find((ws) => ws.uri.fsPath === workspaceFolder); - } -} diff --git a/src/client/debugger/extension/hooks/constants.ts b/src/client/debugger/extension/hooks/constants.ts deleted file mode 100644 index 3bd0b657281e..000000000000 --- a/src/client/debugger/extension/hooks/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -export enum DebuggerEvents { - // Event sent by PTVSD when a child process is launched and ready to be attached to for multi-proc debugging. - PtvsdAttachToSubprocess = 'ptvsd_attach', - DebugpyAttachToSubprocess = 'debugpyAttach', -} diff --git a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts deleted file mode 100644 index 7b1dd1516abd..000000000000 --- a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, multiInject } from 'inversify'; -import { IDebugService } from '../../../common/application/types'; -import { IDisposableRegistry } from '../../../common/types'; -import { IDebugSessionEventHandlers } from './types'; - -export class DebugSessionEventDispatcher { - constructor( - @multiInject(IDebugSessionEventHandlers) private readonly eventHandlers: IDebugSessionEventHandlers[], - @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - ) {} - public registerEventHandlers() { - this.disposables.push( - this.debugService.onDidReceiveDebugSessionCustomEvent((e) => { - this.eventHandlers.forEach((handler) => - handler.handleCustomEvent ? handler.handleCustomEvent(e).ignoreErrors() : undefined, - ); - }), - ); - this.disposables.push( - this.debugService.onDidTerminateDebugSession((e) => { - this.eventHandlers.forEach((handler) => - handler.handleTerminateEvent ? handler.handleTerminateEvent(e).ignoreErrors() : undefined, - ); - }), - ); - } -} diff --git a/src/client/debugger/extension/hooks/types.ts b/src/client/debugger/extension/hooks/types.ts deleted file mode 100644 index 80d393057fb4..000000000000 --- a/src/client/debugger/extension/hooks/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugConfiguration, DebugSession, DebugSessionCustomEvent } from 'vscode'; -import { AttachRequestArguments } from '../../types'; - -export const IDebugSessionEventHandlers = Symbol('IDebugSessionEventHandlers'); -export interface IDebugSessionEventHandlers { - handleCustomEvent?(e: DebugSessionCustomEvent): Promise; - handleTerminateEvent?(e: DebugSession): Promise; -} - -export const IChildProcessAttachService = Symbol('IChildProcessAttachService'); -export interface IChildProcessAttachService { - attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise; -} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index a8c5ae7bbfcc..e1eef8b7b52c 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -3,89 +3,20 @@ 'use strict'; -import { IExtensionSingleActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; -import { AttachRequestArguments, LaunchRequestArguments } from '../types'; -import { DebugAdapterActivator } from './adapter/activator'; -import { DebugAdapterDescriptorFactory } from './adapter/factory'; -import { DebugSessionLoggingFactory } from './adapter/logging'; -import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt'; -import { AttachProcessProviderFactory } from './attachQuickPick/factory'; -import { IAttachProcessProviderFactory } from './attachQuickPick/types'; -import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; -import { DynamicPythonDebugConfigurationService } from './configuration/dynamicdebugConfigurationService'; -import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; -import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; -import { AttachConfigurationResolver } from './configuration/resolvers/attach'; +import { LaunchRequestArguments } from '../types'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; import { IDebugConfigurationResolver } from './configuration/types'; -import { DebugCommands } from './debugCommands'; -import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; -import { ChildProcessAttachService } from './hooks/childProcessAttachService'; -import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; -import { - IDebugAdapterDescriptorFactory, - IDebugConfigurationService, - IDebugSessionLoggingFactory, - IDynamicDebugConfigurationService, - IOutdatedDebuggerPromptFactory, -} from './types'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService, - ); - serviceManager.addSingleton( - IDebugConfigurationService, - PythonDebugConfigurationService, - ); - serviceManager.addSingleton( - IDynamicDebugConfigurationService, - DynamicPythonDebugConfigurationService, + serviceManager.addSingleton( + IDebugEnvironmentVariablesService, + DebugEnvironmentVariablesHelper, ); - serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); - serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>( IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch', ); - serviceManager.addSingleton>( - IDebugConfigurationResolver, - AttachConfigurationResolver, - 'attach', - ); - serviceManager.addSingleton( - IDebugEnvironmentVariablesService, - DebugEnvironmentVariablesHelper, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugAdapterActivator, - ); - serviceManager.addSingleton( - IDebugAdapterDescriptorFactory, - DebugAdapterDescriptorFactory, - ); - serviceManager.addSingleton(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); - serviceManager.addSingleton( - IOutdatedDebuggerPromptFactory, - OutdatedDebuggerPromptFactory, - ); - serviceManager.addSingleton( - IAttachProcessProviderFactory, - AttachProcessProviderFactory, - ); - serviceManager.addSingleton(IExtensionSingleActivationService, DebugCommands); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 2a304efae918..5b2e9facb92c 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -4,16 +4,7 @@ 'use strict'; import { Readable } from 'stream'; -import { - CancellationToken, - DebugAdapterDescriptorFactory, - DebugAdapterTrackerFactory, - DebugConfigurationProvider, - Disposable, - WorkspaceFolder, -} from 'vscode'; - -import { DebugConfigurationArguments } from '../types'; +import { DebugAdapterTrackerFactory, DebugConfigurationProvider, Disposable } from 'vscode'; export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); export interface IDebugConfigurationService extends DebugConfigurationProvider {} @@ -21,12 +12,6 @@ export interface IDebugConfigurationService extends DebugConfigurationProvider { export const IDynamicDebugConfigurationService = Symbol('IDynamicDebugConfigurationService'); export interface IDynamicDebugConfigurationService extends DebugConfigurationProvider {} -export type DebugConfigurationState = { - config: Partial; - folder?: WorkspaceFolder; - token?: CancellationToken; -}; - export enum DebugConfigurationType { launchFile = 'launchFile', remoteAttach = 'remoteAttach', @@ -43,13 +28,6 @@ export enum PythonPathSource { settingsJson = 'settings.json', } -export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); -export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} - -export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory'); - -export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory {} - export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFactory'); export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 60e82fb04418..af4e40a4ac59 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -31,10 +31,6 @@ export type PathMapping = { localRoot: string; remoteRoot: string; }; -type Connection = { - host?: string; - port?: number; -}; export interface IAutomaticCodeReload { enable?: boolean; @@ -61,20 +57,6 @@ interface ICommonDebugArguments { pathMappings?: PathMapping[]; clientOS?: 'windows' | 'unix'; } -interface IKnownAttachDebugArguments extends ICommonDebugArguments { - workspaceFolder?: string; - customDebugger?: boolean; - // localRoot and remoteRoot are deprecated (replaced by pathMappings). - localRoot?: string; - remoteRoot?: string; - - // Internal field used to attach to subprocess using python debug adapter - subProcessId?: number; - - processId?: number | string; - connect?: Connection; - listen?: Connection; -} interface IKnownLaunchRequestArguments extends ICommonDebugArguments { sudo?: boolean; @@ -125,15 +107,6 @@ export interface LaunchRequestArguments type: typeof DebuggerTypeName; } -export interface AttachRequestArguments - extends DebugProtocol.AttachRequestArguments, - IKnownAttachDebugArguments, - DebugConfiguration { - type: typeof DebuggerTypeName; -} - -export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments {} - export type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTerminal'; export type TriggerType = 'launch' | 'attach' | 'test'; diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index c4b663fdba6d..432b63e34061 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -3,7 +3,7 @@ 'use strict'; -import { debug, DebugConfigurationProvider, DebugConfigurationProviderTriggerKind, languages, window } from 'vscode'; +import { languages, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; @@ -22,9 +22,9 @@ import { IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; -import { DebuggerTypeName } from './debugger/constants'; +// import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; -import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; +// import { IDebugConfigurationService } from './debugger/extension/types'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { ReplProvider } from './providers/replProvider'; @@ -42,11 +42,7 @@ import * as pythonEnvironments from './pythonEnvironments'; import { ActivationResult, ExtensionState } from './components'; import { Components } from './extensionInit'; import { setDefaultLanguageServer } from './activation/common/defaultlanguageServer'; -import { DebugService } from './common/application/debugService'; -import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; -import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; -import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; @@ -148,10 +144,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const interpreterManager = serviceContainer.get(IInterpreterService); interpreterManager.initialize(); if (!workspaceService.isVirtualWorkspace) { - const handlers = serviceManager.getAll(IDebugSessionEventHandlers); - const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); - dispatcher.registerEventHandlers(); - const outputChannel = serviceManager.get(ILogOutputChannel); disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); @@ -168,20 +160,11 @@ async function activateLegacy(ext: ExtensionState): Promise { terminalProvider.initialize(window.activeTerminal).ignoreErrors(); disposables.push(terminalProvider); - serviceContainer - .getAll(IDebugConfigurationService) - .forEach((debugConfigProvider) => { - disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); - }); - - // register a dynamic configuration provider for 'python' debug type - disposables.push( - debug.registerDebugConfigurationProvider( - DebuggerTypeName, - serviceContainer.get(IDynamicDebugConfigurationService), - DebugConfigurationProviderTriggerKind.Dynamic, - ), - ); + // serviceContainer + // .getAll(IDebugConfigurationService) + // .forEach((debugConfigProvider) => { + // disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); + // }); logAndNotifyOnLegacySettings(); registerCreateEnvironmentTriggers(disposables); diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts new file mode 100644 index 000000000000..bde2c6465838 --- /dev/null +++ b/src/client/formatters/baseFormatter.ts @@ -0,0 +1,148 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import '../common/extensions'; +import { isNotInstalledError } from '../common/helpers'; +import { traceError } from '../common/logger'; +import { IFileSystem } from '../common/platform/types'; +import { IPythonToolExecutionService } from '../common/process/types'; +import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../common/types'; +import { isNotebookCell } from '../common/utils/misc'; +import { IServiceContainer } from '../ioc/types'; +import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; +import { IFormatterHelper } from './types'; + +export abstract class BaseFormatter { + protected readonly outputChannel: vscode.OutputChannel; + protected readonly workspace: IWorkspaceService; + private readonly helper: IFormatterHelper; + + constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.helper = serviceContainer.get(IFormatterHelper); + this.workspace = serviceContainer.get(IWorkspaceService); + } + + public abstract formatDocument( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + range?: vscode.Range, + ): Thenable; + protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { + if (path.basename(document.uri.fsPath) === document.uri.fsPath) { + return fallbackPath; + } + return path.dirname(document.fileName); + } + protected getWorkspaceUri(document: vscode.TextDocument) { + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const folders = this.workspace.workspaceFolders; + if (Array.isArray(folders) && folders.length > 0) { + return folders[0].uri; + } + return vscode.Uri.file(__dirname); + } + protected async provideDocumentFormattingEdits( + document: vscode.TextDocument, + _options: vscode.FormattingOptions, + token: vscode.CancellationToken, + args: string[], + cwd?: string, + ): Promise { + if (typeof cwd !== 'string' || cwd.length === 0) { + cwd = this.getWorkspaceUri(document).fsPath; + } + + // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. + // However they don't support returning the diff of the formatted text when reading data from the input stream. + // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have + // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. + // Also, always create temp files for Notebook cells. + const tempFile = await this.createTempFile(document); + if (this.checkCancellation(document.fileName, tempFile, token)) { + return []; + } + + const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); + executionInfo.args.push(tempFile); + const pythonToolsExecutionService = this.serviceContainer.get( + IPythonToolExecutionService, + ); + const promise = pythonToolsExecutionService + .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) + .then((output) => output.stdout) + .then((data) => { + if (this.checkCancellation(document.fileName, tempFile, token)) { + return [] as vscode.TextEdit[]; + } + return getTextEditsFromPatch(document.getText(), data); + }) + .catch((error) => { + if (this.checkCancellation(document.fileName, tempFile, token)) { + return [] as vscode.TextEdit[]; + } + + this.handleError(this.Id, error, document.uri).catch(() => {}); + return [] as vscode.TextEdit[]; + }) + .then((edits) => { + this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); + return edits; + }); + + const appShell = this.serviceContainer.get(IApplicationShell); + const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); + const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); + disposableRegistry.push(disposable); + return promise; + } + + protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { + let customError = `Formatting with ${this.Id} failed.`; + + if (isNotInstalledError(error)) { + const installer = this.serviceContainer.get(IInstaller); + const isInstalled = await installer.isInstalled(this.product, resource); + if (!isInstalled) { + customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; + installer + .promptToInstall(this.product, resource) + .catch((ex) => traceError('Python Extension: promptToInstall', ex)); + } + } + + this.outputChannel.appendLine(`\n${customError}\n${error}`); + } + + /** + * Always create a temporary file when formatting notebook cells. + * This is because there is no physical file associated with notebook cells (they are all virtual). + */ + private async createTempFile(document: vscode.TextDocument): Promise { + const fs = this.serviceContainer.get(IFileSystem); + return document.isDirty || isNotebookCell(document) + ? getTempFileWithDocumentContents(document, fs) + : document.fileName; + } + + private deleteTempFile(originalFile: string, tempFile: string): Promise { + if (originalFile !== tempFile) { + const fs = this.serviceContainer.get(IFileSystem); + return fs.deleteFile(tempFile); + } + return Promise.resolve(); + } + + private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { + if (token && token.isCancellationRequested) { + this.deleteTempFile(originalFile, tempFile).ignoreErrors(); + return true; + } + return false; + } +} diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts new file mode 100644 index 000000000000..ddfef8fc57ca --- /dev/null +++ b/src/client/formatters/blackFormatter.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import { Product } from '../common/installer/productInstaller'; +import { IConfigurationService } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { BaseFormatter } from './baseFormatter'; + +export class BlackFormatter extends BaseFormatter { + constructor(serviceContainer: IServiceContainer) { + super('black', Product.black, serviceContainer); + } + + public async formatDocument( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + range?: vscode.Range, + ): Promise { + const stopWatch = new StopWatch(); + const settings = this.serviceContainer + .get(IConfigurationService) + .getSettings(document.uri); + const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + if (formatSelection) { + const shell = this.serviceContainer.get(IApplicationShell); + // Black does not support partial formatting on purpose. + shell.showErrorMessage('Black does not support the "Format Selection" command').then(noop, noop); + return []; + } + + const blackArgs = ['--diff', '--quiet']; + + if (path.extname(document.fileName) === '.pyi') { + blackArgs.push('--pyi'); + } + + const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); + sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); + return promise; + } +} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts new file mode 100644 index 000000000000..16871e7ee71f --- /dev/null +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -0,0 +1,33 @@ +import { OutputChannel, Uri } from 'vscode'; +import { traceError, traceWarning } from '../../common/logger'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { ExecutionInfo, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ILinterManager } from '../types'; +import { BaseErrorHandler } from './baseErrorHandler'; + +export class NotInstalledErrorHandler extends BaseErrorHandler { + constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(product, outputChannel, serviceContainer); + } + public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); + if (isModuleInstalled) { + return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; + } + + this.installer + .promptToInstall(this.product, resource) + .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); + + const linterManager = this.serviceContainer.get(ILinterManager); + const info = linterManager.getLinterInfo(execInfo.product!); + const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; + this.outputChannel.appendLine(`\n${customError}\n${error}`); + traceWarning(customError, error); + return true; + } +} diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts new file mode 100644 index 000000000000..ad3decdcef63 --- /dev/null +++ b/src/client/linters/linterCommands.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { DiagnosticCollection, Disposable, QuickPickOptions, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IDisposable } from '../common/types'; +import { Linters } from '../common/utils/localize'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ILinterManager, ILintingEngine, LinterId } from './types'; + +export class LinterCommands implements IDisposable { + private disposables: Disposable[] = []; + private linterManager: ILinterManager; + private readonly appShell: IApplicationShell; + private readonly documentManager: IDocumentManager; + + constructor(private serviceContainer: IServiceContainer) { + this.linterManager = this.serviceContainer.get(ILinterManager); + this.appShell = this.serviceContainer.get(IApplicationShell); + this.documentManager = this.serviceContainer.get(IDocumentManager); + + const commandManager = this.serviceContainer.get(ICommandManager); + commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); + commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); + commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); + } + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + public async setLinterAsync(): Promise { + const linters = this.linterManager.getAllLinterInfos(); + const suggestions = linters.map((x) => x.id).sort(); + const linterList = ['Disable Linting', ...suggestions]; + const activeLinters = await this.linterManager.getActiveLinters(this.settingsUri); + + let current: string; + switch (activeLinters.length) { + case 0: + current = 'none'; + break; + case 1: + current = activeLinters[0].id; + break; + default: + current = 'multiple selected'; + break; + } + + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${current}`, + }; + + const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); + if (selection !== undefined) { + if (selection === 'Disable Linting') { + await this.linterManager.enableLintingAsync(false); + sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); + } else { + const index = linters.findIndex((x) => x.id === selection); + if (activeLinters.length > 1) { + const response = await this.appShell.showWarningMessage( + Linters.replaceWithSelectedLinter().format(selection), + 'Yes', + 'No', + ); + if (response !== 'Yes') { + return; + } + } + await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); + sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); + } + } + } + + public async enableLintingAsync(): Promise { + const options = ['Enable', 'Disable']; + const current = (await this.linterManager.isLintingEnabled(this.settingsUri)) ? options[0] : options[1]; + + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${current}`, + }; + + const selection = await this.appShell.showQuickPick(options, quickPickOptions); + + if (selection !== undefined) { + const enable: boolean = selection === options[0]; + await this.linterManager.enableLintingAsync(enable, this.settingsUri); + } + } + + public runLinting(): Promise { + const engine = this.serviceContainer.get(ILintingEngine); + return engine.lintOpenPythonFiles(); + } + + private get settingsUri(): Uri | undefined { + return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; + } +} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts new file mode 100644 index 000000000000..0c9155ce2b3a --- /dev/null +++ b/src/client/providers/formatProvider.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { PYTHON_LANGUAGE } from '../common/constants'; +import { IConfigurationService } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; +import { BaseFormatter } from './../formatters/baseFormatter'; +import { BlackFormatter } from './../formatters/blackFormatter'; +import { DummyFormatter } from './../formatters/dummyFormatter'; +import { YapfFormatter } from './../formatters/yapfFormatter'; + +export class PythonFormattingEditProvider + implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { + private readonly config: IConfigurationService; + private readonly workspace: IWorkspaceService; + private readonly documentManager: IDocumentManager; + private readonly commands: ICommandManager; + private formatters = new Map(); + private disposables: vscode.Disposable[] = []; + + // Workaround for https://github.com/Microsoft/vscode/issues/41194 + private documentVersionBeforeFormatting = -1; + private formatterMadeChanges = false; + private saving = false; + + public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { + const yapfFormatter = new YapfFormatter(serviceContainer); + const autoPep8 = new AutoPep8Formatter(serviceContainer); + const black = new BlackFormatter(serviceContainer); + const dummy = new DummyFormatter(serviceContainer); + this.formatters.set(yapfFormatter.Id, yapfFormatter); + this.formatters.set(black.Id, black); + this.formatters.set(autoPep8.Id, autoPep8); + this.formatters.set(dummy.Id, dummy); + + this.commands = serviceContainer.get(ICommandManager); + this.workspace = serviceContainer.get(IWorkspaceService); + this.documentManager = serviceContainer.get(IDocumentManager); + this.config = serviceContainer.get(IConfigurationService); + const interpreterService = serviceContainer.get(IInterpreterService); + this.disposables.push( + this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)), + ); + this.disposables.push( + interpreterService.onDidChangeInterpreter(async () => { + if (this.documentManager.activeTextEditor) { + return this.onSaveDocument(this.documentManager.activeTextEditor.document); + } + }), + ); + } + + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + public provideDocumentFormattingEdits( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + ): Promise { + return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); + } + + public async provideDocumentRangeFormattingEdits( + document: vscode.TextDocument, + range: vscode.Range | undefined, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + ): Promise { + // Workaround for https://github.com/Microsoft/vscode/issues/41194 + // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. + // Workaround is to resolve promise to nothing here, then execute format document and force new save. + // However, we need to know if this is 'format document' or formatting on save. + + if (this.saving || document.languageId !== PYTHON_LANGUAGE) { + // We are saving after formatting (see onSaveDocument below) + // so we do not want to format again. + return []; + } + + // Remember content before formatting so we can detect if + // formatting edits have been really applied + const editorConfig = this.workspace.getConfiguration('editor', document.uri); + if (editorConfig.get('formatOnSave') === true) { + this.documentVersionBeforeFormatting = document.version; + } + + const settings = this.config.getSettings(document.uri); + const formatter = this.formatters.get(settings.formatting.provider)!; + const edits = await formatter.formatDocument(document, options, token, range); + + this.formatterMadeChanges = edits.length > 0; + return edits; + } + + private async onSaveDocument(document: vscode.TextDocument): Promise { + // Promise was rejected = formatting took too long. + // Don't format inside the event handler, do it on timeout + setTimeout(() => { + try { + if ( + this.formatterMadeChanges && + !document.isDirty && + document.version === this.documentVersionBeforeFormatting + ) { + // Formatter changes were not actually applied due to the timeout on save. + // Force formatting now and then save the document. + this.commands.executeCommand('editor.action.formatDocument').then(async () => { + this.saving = true; + await document.save(); + this.saving = false; + }); + } + } finally { + this.documentVersionBeforeFormatting = -1; + this.saving = false; + this.formatterMadeChanges = false; + } + }, 50); + } +} diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index c76557699ff2..0d132120c904 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -78,7 +78,7 @@ export class DebugLauncher implements ITestDebugLauncher { if (!debugConfig) { debugConfig = { name: 'Debug Unit Test', - type: 'python', + type: 'debugpy', request: 'test', subProcess: true, }; diff --git a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts index 8c835003ffef..b8115322ccd7 100644 --- a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ b/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts @@ -8,7 +8,6 @@ import * as path from 'path'; import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidPythonPathInDebuggerService } from '../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; import { diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 6d1d153aba94..f55d2bb00f26 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -13,7 +13,6 @@ import { CommandManager } from '../../client/common/application/commandManager'; import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; import { DebugService } from '../../client/common/application/debugService'; -import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; import { DocumentManager } from '../../client/common/application/documentManager'; import { Extensions } from '../../client/common/application/extensions'; import { @@ -256,10 +255,6 @@ suite('Module Installer', () => { IExtensionSingleActivationService, ReportIssueCommandHandler, ); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance( diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts deleted file mode 100644 index b245a0b4622f..000000000000 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ /dev/null @@ -1,569 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import * as sinon from 'sinon'; -import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; -import { IConfigurationService } from '../../../../../client/common/types'; -import { AttachConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/attach'; -import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; -import { IInterpreterService } from '../../../../../client/interpreter/contracts'; -import { getInfoPerOS } from './common'; -import * as platform from '../../../../../client/common/utils/platform'; -import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -getInfoPerOS().forEach(([osName, osType, path]) => { - if (osType === platform.OSType.Unknown) { - return; - } - - function getAvailableOptions(): string[] { - const options = [DebugOptions.RedirectOutput]; - if (osType === platform.OSType.Windows) { - options.push(DebugOptions.FixFilePathCase); - } - options.push(DebugOptions.ShowReturnValue); - - return options; - } - - suite(`Debugging - Config Resolver attach, OS = ${osName}`, () => { - let debugProvider: DebugConfigurationProvider; - let configurationService: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - let getActiveTextEditorStub: sinon.SinonStub; - let getWorkspaceFoldersStub: sinon.SinonStub; - let getOSTypeStub: sinon.SinonStub; - const debugOptionsAvailable = getAvailableOptions(); - - setup(() => { - configurationService = TypeMoq.Mock.ofType(); - interpreterService = TypeMoq.Mock.ofType(); - debugProvider = new AttachConfigurationResolver(configurationService.object, interpreterService.object); - getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); - getOSTypeStub = sinon.stub(platform, 'getOSType'); - getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); - getOSTypeStub.returns(osType); - }); - - teardown(() => { - sinon.restore(); - }); - - 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); - getActiveTextEditorStub.returns(textEditor.object); - } else { - getActiveTextEditorStub.returns(undefined); - } - } - - function getClientOS() { - return osType === platform.OSType.Windows ? 'windows' : 'unix'; - } - - function setupWorkspaces(folders: string[]) { - const workspaceFolders = folders.map(createMoqWorkspaceFolder); - getWorkspaceFoldersStub.returns(workspaceFolders); - } - - const attach: Partial = { - name: 'Python attach', - type: 'python', - request: 'attach', - }; - - async function resolveDebugConfiguration( - workspaceFolder: WorkspaceFolder | undefined, - attachConfig: Partial, - ) { - let config = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - attachConfig as DebugConfiguration, - ); - if (config === undefined || config === null) { - return config; - } - - config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); - if (config === undefined || config === null) { - return config; - } - - return config as AttachRequestArguments; - } - - 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 resolveDebugConfiguration(workspaceFolder, { - request: 'attach', - }); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - 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 resolveDebugConfiguration(undefined, { - request: 'attach', - }); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - 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 resolveDebugConfiguration(undefined, { - request: 'attach', - }); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - 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 resolveDebugConfiguration(undefined, { - request: 'attach', - }); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - 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 resolveDebugConfiguration(undefined, { - request: 'attach', - }); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); - expect(debugConfig).to.have.property('request', 'attach'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); - expect(debugConfig).to.have.property('host', 'localhost'); - }); - - test('Default host should not be added if connect is available.', async () => { - const pythonFile = 'xyz.py'; - - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await resolveDebugConfiguration(undefined, { - ...attach, - connect: { host: 'localhost', port: 5678 }, - }); - - expect(debugConfig).to.not.have.property('host', 'localhost'); - }); - - test('Default host should not be added if listen is available.', async () => { - const pythonFile = 'xyz.py'; - - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await resolveDebugConfiguration(undefined, { - ...attach, - listen: { host: 'localhost', port: 5678 }, - } as AttachRequestArguments); - - expect(debugConfig).to.not.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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - }); - - expect(debugConfig).to.have.property('localRoot', localRoot); - }); - - ['localhost', '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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - host, - }); - - expect(debugConfig).to.have.property('localRoot', localRoot); - const { pathMappings } = debugConfig as AttachRequestArguments; - 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); - }); - - test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}'`, async function () { - if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { - return this.skip(); - } - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - host, - }); - const { pathMappings } = debugConfig as AttachRequestArguments; - - const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path')).fsPath; - expect(pathMappings![0].localRoot).to.be.equal(expected); - expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); - - return undefined; - }); - - test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}'`, async function () { - if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { - return this.skip(); - } - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - host, - }); - const { pathMappings } = debugConfig as AttachRequestArguments; - - const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path')).fsPath; - expect(pathMappings![0].localRoot).to.be.equal(expected); - expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); - - return undefined; - }); - - test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async function () { - if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { - return this.skip(); - } - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugPathMappings = [ - { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, - ]; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - pathMappings: debugPathMappings, - host, - }); - const { pathMappings } = debugConfig as AttachRequestArguments; - - const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path', localRoot)).fsPath; - expect(pathMappings![0].localRoot).to.be.equal(expected); - expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); - - return undefined; - }); - - test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}' and with existing path mappings`, async function () { - if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { - return this.skip(); - } - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugPathMappings = [ - { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, - ]; - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - pathMappings: debugPathMappings, - host, - }); - const { pathMappings } = debugConfig as AttachRequestArguments; - - const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; - expect(Uri.file(pathMappings![0].localRoot).fsPath).to.be.equal(expected); - expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); - - return undefined; - }); - - test(`Ensure local path mappings are not modified when not pointing to a local drive when host is '${host}'`, async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - host, - }); - const { pathMappings } = debugConfig as AttachRequestArguments; - - 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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - host, - }); - - expect(debugConfig).to.have.property('localRoot', localRoot); - const { pathMappings } = debugConfig as AttachRequestArguments; - 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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - remoteRoot, - }); - - 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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - localRoot, - remoteRoot, - }); - - 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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - remoteRoot, - }); - - 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 resolveDebugConfiguration(workspaceFolder, { - ...attach, - port, - }); - - 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) as DebugOptions[]; - const expectedDebugOptions = debugOptions.slice(); - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - debugOptions, - }); - - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); - }); - - const testsForJustMyCode = [ - { - justMyCode: false, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: false, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false, - }, - { - justMyCode: true, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: true, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true, - }, - ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', 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) as DebugOptions[]; - - testsForJustMyCode.forEach(async (testParams) => { - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...attach, - debugOptions, - justMyCode: testParams.justMyCode, - debugStdLib: testParams.debugStdLib, - }); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); - }); - }); - }); -}); diff --git a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts deleted file mode 100644 index 4da645bc34ac..000000000000 --- a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +++ /dev/null @@ -1,337 +0,0 @@ -/* eslint-disable class-methods-use-this */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; -import { ConfigurationService } from '../../../../../client/common/configuration/service'; -import { IConfigurationService } from '../../../../../client/common/types'; -import { BaseConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/base'; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; -import { IInterpreterService } from '../../../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; -import * as helper from '../../../../../client/debugger/extension/configuration/resolvers/helper'; - -suite('Debugging - Config Resolver', () => { - class BaseResolver extends BaseConfigurationResolver { - public resolveDebugConfiguration( - _folder: WorkspaceFolder | undefined, - _debugConfiguration: DebugConfiguration, - _token?: CancellationToken, - ): Promise { - throw new Error('Not Implemented'); - } - - public resolveDebugConfigurationWithSubstitutedVariables( - _folder: WorkspaceFolder | undefined, - _debugConfiguration: DebugConfiguration, - _token?: CancellationToken, - ): Promise { - throw new Error('Not Implemented'); - } - - public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { - return BaseConfigurationResolver.getWorkspaceFolder(folder); - } - - public resolveAndUpdatePythonPath( - workspaceFolderUri: Uri | undefined, - debugConfiguration: LaunchRequestArguments, - ) { - return super.resolveAndUpdatePythonPath(workspaceFolderUri, debugConfiguration); - } - - public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { - return BaseConfigurationResolver.debugOption(debugOptions, debugOption); - } - - public isLocalHost(hostName?: string) { - return BaseConfigurationResolver.isLocalHost(hostName); - } - - public isDebuggingFastAPI(debugConfiguration: Partial) { - return BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration); - } - - public isDebuggingFlask(debugConfiguration: Partial) { - return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); - } - } - let resolver: BaseResolver; - let configurationService: IConfigurationService; - let interpreterService: IInterpreterService; - let getWorkspaceFoldersStub: sinon.SinonStub; - let getWorkspaceFolderStub: sinon.SinonStub; - let getProgramStub: sinon.SinonStub; - - setup(() => { - configurationService = mock(ConfigurationService); - interpreterService = mock(); - resolver = new BaseResolver(instance(configurationService), instance(interpreterService)); - getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); - getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - getProgramStub = sinon.stub(helper, 'getProgram'); - }); - teardown(() => { - sinon.restore(); - }); - - 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'); - - getProgramStub.returns(programPath); - getWorkspaceFoldersStub.returns(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]; - - getProgramStub.returns(undefined); - - getWorkspaceFolderStub.returns(folder); - - getWorkspaceFoldersStub.returns(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]; - - getProgramStub.returns(programPath); - - getWorkspaceFoldersStub.returns(folders); - - getWorkspaceFolderStub.returns(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]; - - getProgramStub.returns(programPath); - getWorkspaceFoldersStub.returns(folders); - - getWorkspaceFolderStub.returns(undefined); - - const uri = resolver.getWorkspaceFolder(undefined); - - expect(uri).to.be.deep.equal(undefined, 'not undefined'); - }); - test('Do nothing if debug configuration is undefined', async () => { - await resolver.resolveAndUpdatePythonPath(undefined, (undefined as unknown) as LaunchRequestArguments); - }); - test('python in debug config must point to pythonPath in settings if pythonPath in config is not set', async () => { - const config = {}; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - - expect(config).to.have.property('python', pythonPath); - }); - test('python in debug config must point to pythonPath in settings if pythonPath in config is ${command:python.interpreterPath}', async () => { - const config = { - python: '${command:python.interpreterPath}', - }; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - - expect(config.python).to.equal(pythonPath); - }); - - test('config should only contain python and not pythonPath after resolving', async () => { - const config = { pythonPath: '${command:python.interpreterPath}', python: '${command:python.interpreterPath}' }; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPath); - }); - - test('config should convert pythonPath to python, only if python is not set', async () => { - const config = { pythonPath: '${command:python.interpreterPath}', python: undefined }; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPath); - }); - - test('config should not change python if python is different than pythonPath', async () => { - const expected = path.join('1', '2', '4'); - const config = { pythonPath: '${command:python.interpreterPath}', python: expected }; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', expected); - }); - - test('config should get python from interpreter service is nothing is set', async () => { - const config = {}; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPath); - }); - - test('config should contain debugAdapterPython and debugLauncherPython', async () => { - const config = {}; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPath); - expect(config).to.have.property('debugAdapterPython', pythonPath); - expect(config).to.have.property('debugLauncherPython', pythonPath); - }); - - test('config should not change debugAdapterPython and debugLauncherPython if already set', async () => { - const debugAdapterPythonPath = path.join('1', '2', '4'); - const debugLauncherPythonPath = path.join('1', '2', '5'); - - const config = { debugAdapterPython: debugAdapterPythonPath, debugLauncherPython: debugLauncherPythonPath }; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPath); - expect(config).to.have.property('debugAdapterPython', debugAdapterPythonPath); - expect(config).to.have.property('debugLauncherPython', debugLauncherPythonPath); - }); - - test('config should not resolve debugAdapterPython and debugLauncherPython', async () => { - const config = { - debugAdapterPython: '${command:python.interpreterPath}', - debugLauncherPython: '${command:python.interpreterPath}', - }; - const pythonPath = path.join('1', '2', '3'); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ - path: pythonPath, - } as PythonEnvironment); - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPath); - expect(config).to.have.property('debugAdapterPython', pythonPath); - expect(config).to.have.property('debugLauncherPython', pythonPath); - }); - - const localHostTestMatrix: Record = { - 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 fastapi=true', () => { - const config = { module: 'fastapi' }; - const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); - expect(isFastAPI).to.equal(true, 'not fastapi'); - }); - test('Is debugging fastapi=false', () => { - const config = { module: 'fastapi2' }; - const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); - expect(isFastAPI).to.equal(false, 'fastapi'); - }); - test('Is debugging fastapi=false when not defined', () => { - const config = {}; - const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); - expect(isFastAPI).to.equal(false, 'fastapi'); - }); - test('Is debugging flask=true', () => { - const config = { module: 'flask' }; - const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); - expect(isFlask).to.equal(true, 'not flask'); - }); - test('Is debugging flask=false', () => { - const config = { module: 'flask2' }; - const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); - expect(isFlask).to.equal(false, 'flask'); - }); - test('Is debugging flask=false when not defined', () => { - const config = {}; - const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); - expect(isFlask).to.equal(false, 'flask'); - }); -}); diff --git a/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts deleted file mode 100644 index 01205fd0c87c..000000000000 --- a/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import * as typemoq from 'typemoq'; -import { TextDocument, TextEditor } from 'vscode'; -import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; -import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; -import { getProgram } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; - -suite('Debugging - Helpers', () => { - let getActiveTextEditorStub: sinon.SinonStub; - - setup(() => { - getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); - }); - teardown(() => { - sinon.restore(); - }); - - 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()); - - getActiveTextEditorStub.returns(editor.object); - - const program = 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()); - getActiveTextEditorStub.returns(editor.object); - - const program = getProgram(); - - expect(program).to.be.equal(undefined, 'Not undefined'); - }); - test('Program should return undefined if there is no active editor', () => { - getActiveTextEditorStub.returns(undefined); - - const program = getProgram(); - - expect(program).to.be.equal(undefined, 'Not undefined'); - }); -}); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts deleted file mode 100644 index 59f61f81cd85..000000000000 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ /dev/null @@ -1,1213 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import * as sinon from 'sinon'; -import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; -import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../../client/common/process/types'; -import { IConfigurationService, IPythonSettings } from '../../../../../client/common/types'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { IDebugEnvironmentVariablesService } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; -import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; -import { PythonPathSource } from '../../../../../client/debugger/extension/types'; -import { ConsoleType, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; -import { IInterpreterHelper, IInterpreterService } from '../../../../../client/interpreter/contracts'; -import { getInfoPerOS } from './common'; -import * as platform from '../../../../../client/common/utils/platform'; -import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; -import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; -import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; - -getInfoPerOS().forEach(([osName, osType, path]) => { - if (osType === platform.OSType.Unknown) { - return; - } - - suite(`Debugging - Config Resolver Launch, OS = ${osName}`, () => { - let debugProvider: DebugConfigurationProvider; - let pythonExecutionService: TypeMoq.IMock; - let helper: TypeMoq.IMock; - const envVars = { FOO: 'BAR' }; - - let diagnosticsService: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let debugEnvHelper: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - let environmentActivationService: TypeMoq.IMock; - let getActiveTextEditorStub: sinon.SinonStub; - let getOSTypeStub: sinon.SinonStub; - let getWorkspaceFolderStub: sinon.SinonStub; - let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; - - setup(() => { - getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); - getOSTypeStub = sinon.stub(platform, 'getOSType'); - getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); - getOSTypeStub.returns(osType); - triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( - triggerApis, - 'triggerCreateEnvironmentCheckNonBlocking', - ); - triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); - }); - - teardown(() => { - sinon.restore(); - }); - - function createMoqWorkspaceFolder(folderPath: string) { - const folder = TypeMoq.Mock.ofType(); - folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); - return folder.object; - } - - function getClientOS() { - return osType === platform.OSType.Windows ? 'windows' : 'unix'; - } - - function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { - environmentActivationService = TypeMoq.Mock.ofType(); - environmentActivationService - .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(envVars)); - configService = TypeMoq.Mock.ofType(); - diagnosticsService = TypeMoq.Mock.ofType(); - debugEnvHelper = TypeMoq.Mock.ofType(); - pythonExecutionService = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - const factory = TypeMoq.Mock.ofType(); - factory - .setup((f) => f.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - helper.setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - diagnosticsService - .setup((h) => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - const settings = TypeMoq.Mock.ofType(); - interpreterService = TypeMoq.Mock.ofType(); - // interpreterService - // .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - // .returns(() => Promise.resolve({ path: pythonPath } as any)); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); - if (workspaceFolder) { - settings.setup((s) => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); - } - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - debugEnvHelper - .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({})); - - debugProvider = new LaunchConfigurationResolver( - diagnosticsService.object, - configService.object, - debugEnvHelper.object, - interpreterService.object, - environmentActivationService.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); - getActiveTextEditorStub.returns(textEditor.object); - } else { - getActiveTextEditorStub.returns(undefined); - } - } - - function setupWorkspaces(folders: string[]) { - const workspaceFolders = folders.map(createMoqWorkspaceFolder); - getWorkspaceFolderStub.returns(workspaceFolders); - } - - const launch: LaunchRequestArguments = { - name: 'Python launch', - type: 'python', - request: 'launch', - }; - - async function resolveDebugConfiguration( - workspaceFolder: WorkspaceFolder | undefined, - launchConfig: Partial, - ) { - let config = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - launchConfig as DebugConfiguration, - ); - if (config === undefined || config === null) { - return config; - } - - const interpreterPath = configService.object.getSettings(workspaceFolder ? workspaceFolder.uri : undefined) - .pythonPath; - for (const key of Object.keys(config)) { - const value = config[key]; - if (typeof value === 'string') { - config[key] = value.replace('${command:python.interpreterPath}', interpreterPath); - } - } - - config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); - if (config === undefined || config === null) { - return config; - } - - return config as LaunchRequestArguments; - } - - test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, workspaceFolder); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, {}); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - - expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); - }); - - test("Defaults should be returned when an object with 'noDebug' property is passed with a Workspace Folder and active file", async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, workspaceFolder); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - noDebug: true, - }); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - - expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); - }); - - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await resolveDebugConfiguration(undefined, {}); - const filePath = Uri.file(path.dirname('')).fsPath; - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - - expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); - }); - - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - setupIoc(pythonPath); - setupActiveEditor(undefined, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await resolveDebugConfiguration(undefined, {}); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - - expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); - }); - - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.js'; - setupIoc(pythonPath); - setupActiveEditor(activeFile, 'javascript'); - setupWorkspaces([]); - - const debugConfig = await resolveDebugConfiguration(undefined, {}); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - expect(debugConfig).to.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - - expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); - }); - - test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const defaultWorkspace = path.join('usr', 'desktop'); - setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await resolveDebugConfiguration(undefined, {}); - const filePath = Uri.file(defaultWorkspace).fsPath; - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - expect(debugConfig).to.have.property('program', activeFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - - expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); - }); - - test("Ensure 'port' is left unaltered", async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const port = 12341234; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - port, - }); - - expect(debugConfig).to.have.property('port', port); - }); - - test("Ensure 'localRoot' is left unaltered", async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - localRoot, - }); - - expect(debugConfig).to.have.property('localRoot', localRoot); - }); - - test("Ensure 'remoteRoot' is left unaltered", async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - remoteRoot, - }); - - expect(debugConfig).to.have.property('remoteRoot', remoteRoot); - }); - - test("Ensure 'localRoot' and 'remoteRoot' are not used", async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', 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 resolveDebugConfiguration(workspaceFolder, { - ...launch, - localRoot, - remoteRoot, - }); - - expect(debugConfig!.pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); - }); - - test('Ensure non-empty path mappings are used', async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const expected = { - localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, - remoteRoot: `Debug_PythonPath_Remote_Root_${new Date().toString()}`, - }; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pathMappings: [expected], - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.be.deep.equal([expected]); - }); - - test('Ensure replacement in path mappings happens', async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pathMappings: [ - { - localRoot: '${workspaceFolder}/spam', - remoteRoot: '${workspaceFolder}/spam', - }, - ], - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.be.deep.equal([ - { - localRoot: `${workspaceFolder.uri.fsPath}/spam`, - remoteRoot: '${workspaceFolder}/spam', - }, - ]); - }); - - test('Ensure path mappings are not automatically added if missing', async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - localRoot, - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); - }); - - test('Ensure path mappings are not automatically added if empty', async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - localRoot, - pathMappings: [], - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); - }); - - test('Ensure path mappings are not automatically added to existing', async () => { - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - const localRoot = `Debug_PythonPath_${new Date().toString()}`; - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - localRoot, - pathMappings: [ - { - localRoot: '/spam', - remoteRoot: '.', - }, - ], - }); - - expect(debugConfig).to.have.property('localRoot', localRoot); - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.be.deep.equal([ - { - localRoot: '/spam', - remoteRoot: '.', - }, - ]); - }); - - test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async function () { - if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { - return this.skip(); - } - const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pathMappings: [ - { - localRoot, - remoteRoot: '/app/', - }, - ], - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; - expect(pathMappings).to.deep.equal([ - { - localRoot: expected, - remoteRoot: '/app/', - }, - ]); - return undefined; - }); - - test('Ensure drive letter is not lower cased for local path mappings on non-Windows when with existing path mappings', async function () { - if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { - return this.skip(); - } - const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pathMappings: [ - { - localRoot, - remoteRoot: '/app/', - }, - ], - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.deep.equal([ - { - localRoot, - remoteRoot: '/app/', - }, - ]); - return undefined; - }); - - test('Ensure local path mappings are not modified when not pointing to a local drive', async () => { - const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); - setupActiveEditor('spam.py', PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pathMappings: [ - { - localRoot: '/spam', - remoteRoot: '.', - }, - ], - }); - - const { pathMappings } = debugConfig as LaunchRequestArguments; - expect(pathMappings).to.deep.equal([ - { - localRoot: '/spam', - remoteRoot: '.', - }, - ]); - }); - - test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pythonPath: '${command:python.interpreterPath}', - }); - - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - }); - - test('Ensure `${command:python.interpreterPath}` substitution is properly handled', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - python: '${command:python.interpreterPath}', - }); - - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - }); - - test('Ensure hardcoded pythonPath is left unaltered', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - pythonPath: debugPythonPath, - }); - - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', debugPythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', debugPythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', debugPythonPath); - }); - - test('Ensure hardcoded "python" is left unaltered', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - python: debugPythonPath, - }); - - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', debugPythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - }); - - test('Ensure hardcoded "debugAdapterPython" is left unaltered', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - debugAdapterPython: debugPythonPath, - }); - - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', debugPythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); - }); - - test('Ensure hardcoded "debugLauncherPython" is left unaltered', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - debugLauncherPython: debugPythonPath, - }); - - expect(debugConfig).to.not.have.property('pythonPath'); - expect(debugConfig).to.have.property('python', pythonPath); - expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); - expect(debugConfig).to.have.property('debugLauncherPython', debugPythonPath); - }); - - test('Test defaults of debugger', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - }); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', true); - expect(debugConfig).to.have.property('debugOptions'); - const expectedOptions = [DebugOptions.ShowReturnValue]; - if (osType === platform.OSType.Windows) { - expectedOptions.push(DebugOptions.FixFilePathCase); - } - expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); - }); - - test('Test defaults of python debugger', async () => { - if (DebuggerTypeName === 'python') { - return; - } - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - }); - - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.have.property('showReturnValue', true); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal([]); - }); - - test('Test overriding defaults of debugger', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - redirectOutput: true, - justMyCode: false, - }); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - expect(debugConfig).to.have.property('clientOS', getClientOS()); - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', true); - expect(debugConfig).to.have.property('redirectOutput', true); - expect(debugConfig).to.have.property('justMyCode', false); - expect(debugConfig).to.have.property('debugOptions'); - const expectedOptions = [ - DebugOptions.DebugStdLib, - DebugOptions.ShowReturnValue, - DebugOptions.RedirectOutput, - ]; - if (osType === platform.OSType.Windows) { - expectedOptions.push(DebugOptions.FixFilePathCase); - } - expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); - }); - - const testsForJustMyCode = [ - { - justMyCode: false, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: false, - expectedResult: false, - }, - { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false, - }, - { - justMyCode: true, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: true, - expectedResult: true, - }, - { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true, - }, - { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false, - }, - { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true, - }, - ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - testsForJustMyCode.forEach(async (testParams) => { - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - debugStdLib: testParams.debugStdLib, - justMyCode: testParams.justMyCode, - }); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); - }); - }); - - const testsForRedirectOutput = [ - { - console: 'internalConsole', - redirectOutput: undefined, - expectedRedirectOutput: true, - }, - { - console: 'integratedTerminal', - redirectOutput: undefined, - expectedRedirectOutput: undefined, - }, - { - console: 'externalTerminal', - redirectOutput: undefined, - expectedRedirectOutput: undefined, - }, - { - console: 'internalConsole', - redirectOutput: false, - expectedRedirectOutput: false, - }, - { - console: 'integratedTerminal', - redirectOutput: false, - expectedRedirectOutput: false, - }, - { - console: 'externalTerminal', - redirectOutput: false, - expectedRedirectOutput: false, - }, - { - console: 'internalConsole', - redirectOutput: true, - expectedRedirectOutput: true, - }, - { - console: 'integratedTerminal', - redirectOutput: true, - expectedRedirectOutput: true, - }, - { - console: 'externalTerminal', - redirectOutput: true, - expectedRedirectOutput: true, - }, - ]; - test('Ensure redirectOutput property is correctly derived from console type', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - testsForRedirectOutput.forEach(async (testParams) => { - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - console: testParams.console as ConsoleType, - redirectOutput: testParams.redirectOutput, - }); - expect(debugConfig).to.have.property('redirectOutput', testParams.expectedRedirectOutput); - if (testParams.expectedRedirectOutput) { - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as DebugConfiguration).debugOptions).to.contain(DebugOptions.RedirectOutput); - } - }); - }); - - test('Test fixFilePathCase', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - }); - if (osType === platform.OSType.Windows) { - expect(debugConfig).to.have.property('debugOptions').contains(DebugOptions.FixFilePathCase); - } else { - expect(debugConfig).to.have.property('debugOptions').not.contains(DebugOptions.FixFilePathCase); - } - }); - - test('Jinja added for Pyramid', async () => { - const workspacePath = path.join('usr', 'development', 'wksp1'); - const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); - const workspaceFolder = createMoqWorkspaceFolder(workspacePath); - const pythonFile = 'xyz.py'; - - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - debugOptions: [DebugOptions.Pyramid], - pyramid: true, - }); - - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); - }); - - test('Auto detect flask debugging', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - module: 'flask', - }); - - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); - }); - - test('Test validation of Python Path when launching debugger (with invalid "python")', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; - const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(pythonPath), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - // Invalid - .returns(() => Promise.resolve(false)); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(debugLauncherPython), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(debugAdapterPython), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - redirectOutput: false, - python: pythonPath, - debugLauncherPython, - debugAdapterPython, - }); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.be.equal(undefined, 'Not undefined'); - }); - - test('Test validation of Python Path when launching debugger (with invalid "debugLauncherPython")', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; - const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(pythonPath), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(debugLauncherPython), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - // Invalid - .returns(() => Promise.resolve(false)); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(debugAdapterPython), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - redirectOutput: false, - python: pythonPath, - debugLauncherPython, - debugAdapterPython, - }); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.be.equal(undefined, 'Not undefined'); - }); - - test('Test validation of Python Path when launching debugger (with invalid "debugAdapterPython")', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; - const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(pythonPath), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(debugLauncherPython), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(debugAdapterPython), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - // Invalid - .returns(() => Promise.resolve(false)); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - redirectOutput: false, - python: pythonPath, - debugLauncherPython, - debugAdapterPython, - }); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.be.equal(undefined, 'Not undefined'); - }); - - test('Test validation of Python Path when launching debugger (with valid "python/debugAdapterPython/debugLauncherPython")', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup((h) => - h.validatePythonPath( - TypeMoq.It.isValue(pythonPath), - PythonPathSource.launchJson, - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - redirectOutput: false, - python: pythonPath, - }); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); - }); - - test('Resolve path to envFile', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - const sep = osType === platform.OSType.Windows ? '\\' : '/'; - const expectedEnvFilePath = `${workspaceFolder.uri.fsPath}${sep}${'wow.envFile'}`; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup((h) => - h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ) - .returns(() => Promise.resolve(true)); - - const debugConfig = await resolveDebugConfiguration(workspaceFolder, { - ...launch, - redirectOutput: false, - pythonPath, - envFile: path.join('${workspaceFolder}', 'wow.envFile'), - }); - - expect(debugConfig!.envFile).to.be.equal(expectedEnvFilePath); - }); - - async function testSetting( - requestType: 'launch' | 'attach', - settings: Record, - debugOptionName: DebugOptions, - mustHaveDebugOption: boolean, - ) { - setupIoc('pythonPath'); - let debugConfig: DebugConfiguration = { - request: requestType, - type: 'python', - name: '', - ...settings, - }; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - - debugConfig = (await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfig))!; - debugConfig = (await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!( - workspaceFolder, - debugConfig, - ))!; - - if (mustHaveDebugOption) { - expect(debugConfig.debugOptions).contains(debugOptionName); - } else { - expect(debugConfig.debugOptions).not.contains(debugOptionName); - } - } - type LaunchOrAttach = 'launch' | 'attach'; - const items: LaunchOrAttach[] = ['launch', 'attach']; - items.forEach((requestType) => { - test(`Must not contain Sub Process when not specified(${requestType})`, async () => { - await testSetting(requestType, {}, DebugOptions.SubProcess, false); - }); - test(`Must not contain Sub Process setting = false(${requestType})`, async () => { - await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); - }); - test(`Must not contain Sub Process setting = true(${requestType})`, async () => { - await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); - }); - }); - }); -}); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 43d81bbe1385..be73a0965e4b 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -5,31 +5,11 @@ import { instance, mock, verify } from 'ts-mockito'; import { IExtensionSingleActivationService } from '../../../client/activation/types'; -import { DebugAdapterActivator } from '../../../client/debugger/extension/adapter/activator'; -import { DebugAdapterDescriptorFactory } from '../../../client/debugger/extension/adapter/factory'; -import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/adapter/logging'; -import { OutdatedDebuggerPromptFactory } from '../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; -import { AttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/factory'; -import { IAttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/types'; -import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension/configuration/launch.json/completionProvider'; -import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService'; -import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; -import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; 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 { - IDebugAdapterDescriptorFactory, - IDebugConfigurationService, - IDebugSessionLoggingFactory, - IOutdatedDebuggerPromptFactory, -} from '../../../client/debugger/extension/types'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../../client/debugger/types'; import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceManager } from '../../../client/ioc/types'; @@ -41,99 +21,18 @@ suite('Debugging - Service Registry', () => { }); test('Registrations', () => { registerTypes(instance(serviceManager)); - - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - InterpreterPathCommand, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugConfigurationService, - PythonDebugConfigurationService, - ), - ).once(); verify( serviceManager.addSingleton( IChildProcessAttachService, ChildProcessAttachService, ), ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonCompletionProvider, - ), - ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - LaunchJsonUpdaterService, - ), - ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugAdapterActivator, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugAdapterDescriptorFactory, - DebugAdapterDescriptorFactory, - ), - ).once(); verify( serviceManager.addSingleton( IDebugSessionEventHandlers, ChildProcessAttachEventHandler, ), ).once(); - verify( - serviceManager.addSingleton>( - IDebugConfigurationResolver, - LaunchConfigurationResolver, - 'launch', - ), - ).once(); - verify( - serviceManager.addSingleton>( - IDebugConfigurationResolver, - AttachConfigurationResolver, - 'attach', - ), - ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugAdapterActivator, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugAdapterDescriptorFactory, - DebugAdapterDescriptorFactory, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugSessionLoggingFactory, - DebugSessionLoggingFactory, - ), - ).once(); - verify( - serviceManager.addSingleton( - IOutdatedDebuggerPromptFactory, - OutdatedDebuggerPromptFactory, - ), - ).once(); - verify( - serviceManager.addSingleton( - IAttachProcessProviderFactory, - AttachProcessProviderFactory, - ), - ).once(); verify( serviceManager.addSingleton( IExtensionSingleActivationService, From 7b42aebebe099dce0652dca1e7a7dbce4f524bb1 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Tue, 24 Oct 2023 14:24:51 -0700 Subject: [PATCH 02/24] update tests --- package.json | 1 - .../launch.json/interpreterPathCommand.ts | 51 ++ .../extension/adapter/activator.unit.test.ts | 91 ---- .../extension/adapter/factory.unit.test.ts | 316 ----------- .../extension/adapter/logging.unit.test.ts | 149 ------ .../outdatedDebuggerPrompt.unit.test.ts | 180 ------- .../attachQuickPick/factory.unit.test.ts | 51 -- .../attachQuickPick/provider.unit.test.ts | 459 ---------------- .../psProcessParser.unit.test.ts | 192 ------- .../wmicProcessParser.unit.test.ts | 215 -------- .../completionProvider.unit.test.ts | 138 ----- .../launch.json/updaterServer.unit.test.ts | 34 -- .../updaterServerHelper.unit.test.ts | 496 ------------------ .../providers/djangoLaunch.unit.test.ts | 138 ----- .../providers/fastapiLaunch.unit.test.ts | 83 --- .../providers/fileLaunch.unit.test.ts | 36 -- .../providers/flaskLaunch.unit.test.ts | 113 ---- .../providers/moduleLaunch.unit.test.ts | 55 -- .../providers/pidAttach.unit.test.ts | 32 -- .../providers/pyramidLaunch.unit.test.ts | 163 ------ .../providers/remoteAttach.unit.test.ts | 130 ----- .../extension/debugCommands.unit.test.ts | 93 ---- .../childProcessAttachHandler.unit.test.ts | 72 --- .../childProcessAttachService.unit.test.ts | 195 ------- 24 files changed, 51 insertions(+), 3432 deletions(-) create mode 100644 src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts delete mode 100644 src/test/debugger/extension/adapter/activator.unit.test.ts delete mode 100644 src/test/debugger/extension/adapter/factory.unit.test.ts delete mode 100644 src/test/debugger/extension/adapter/logging.unit.test.ts delete mode 100644 src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts delete mode 100644 src/test/debugger/extension/attachQuickPick/factory.unit.test.ts delete mode 100644 src/test/debugger/extension/attachQuickPick/provider.unit.test.ts delete mode 100644 src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts delete mode 100644 src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts delete mode 100644 src/test/debugger/extension/debugCommands.unit.test.ts delete mode 100644 src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts delete mode 100644 src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts diff --git a/package.json b/package.json index c9e83bd38ae5..fe050dee230c 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "activationEvents": [ "onDebugInitialConfigurations", "onLanguage:python", - "onDebugDynamicConfigurations:python", "onDebugResolve:python", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts new file mode 100644 index 000000000000..0335b744f6f4 --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../activation/types'; +import { Commands } from '../../../../common/constants'; +import { IDisposable, IDisposableRegistry } from '../../../../common/types'; +import { registerCommand } from '../../../../common/vscodeApis/commandApis'; +import { IInterpreterService } from '../../../../interpreter/contracts'; + +@injectable() +export class InterpreterPathCommand implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push( + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), + ); + } + + public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { + // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder + // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder + let workspaceFolder; + if ('workspaceFolder' in args) { + workspaceFolder = args.workspaceFolder; + } else if (args[1]) { + const [, second] = args; + workspaceFolder = second; + } else { + workspaceFolder = undefined; + } + + let workspaceFolderUri; + try { + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; + } catch (ex) { + workspaceFolderUri = undefined; + } + + return (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; + } +} diff --git a/src/test/debugger/extension/adapter/activator.unit.test.ts b/src/test/debugger/extension/adapter/activator.unit.test.ts deleted file mode 100644 index e8c6ef74fc2a..000000000000 --- a/src/test/debugger/extension/adapter/activator.unit.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { IExtensionSingleActivationService } from '../../../../client/activation/types'; -import { CommandManager } from '../../../../client/common/application/commandManager'; -import { DebugService } from '../../../../client/common/application/debugService'; -import { ICommandManager, IDebugService } from '../../../../client/common/application/types'; -import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; -import { DebugAdapterActivator } from '../../../../client/debugger/extension/adapter/activator'; -import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; -import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; -import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; -import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; -import { IAttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/types'; -import { - IDebugAdapterDescriptorFactory, - IDebugSessionLoggingFactory, - IOutdatedDebuggerPromptFactory, -} from '../../../../client/debugger/extension/types'; -import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { noop } from '../../../core'; - -suite('Debugging - Adapter Factory and logger Registration', () => { - let activator: IExtensionSingleActivationService; - let debugService: IDebugService; - let commandManager: ICommandManager; - let descriptorFactory: IDebugAdapterDescriptorFactory; - let loggingFactory: IDebugSessionLoggingFactory; - let debuggerPromptFactory: IOutdatedDebuggerPromptFactory; - let disposableRegistry: IDisposableRegistry; - let attachFactory: IAttachProcessProviderFactory; - let configService: IConfigurationService; - - setup(() => { - attachFactory = mock(AttachProcessProviderFactory); - - debugService = mock(DebugService); - when(debugService.onDidStartDebugSession).thenReturn(() => noop as any); - - commandManager = mock(CommandManager); - - configService = mock(ConfigurationService); - when(configService.getSettings(undefined)).thenReturn(({ - experiments: { enabled: true }, - } as any) as IPythonSettings); - - descriptorFactory = mock(DebugAdapterDescriptorFactory); - loggingFactory = mock(DebugSessionLoggingFactory); - debuggerPromptFactory = mock(OutdatedDebuggerPromptFactory); - disposableRegistry = []; - - activator = new DebugAdapterActivator( - instance(debugService), - instance(configService), - instance(commandManager), - instance(descriptorFactory), - instance(loggingFactory), - instance(debuggerPromptFactory), - disposableRegistry, - instance(attachFactory), - ); - }); - - teardown(() => { - clearTelemetryReporter(); - }); - - test('Register Debug adapter factory', async () => { - await activator.activate(); - - verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).once(); - verify(debugService.registerDebugAdapterTrackerFactory('python', instance(debuggerPromptFactory))).once(); - verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).once(); - }); - - test('Register a disposable item', async () => { - const disposable = { dispose: noop }; - when(debugService.registerDebugAdapterTrackerFactory(anything(), anything())).thenReturn(disposable); - when(debugService.registerDebugAdapterDescriptorFactory(anything(), anything())).thenReturn(disposable); - when(debugService.onDidStartDebugSession).thenReturn(() => disposable); - - await activator.activate(); - - assert.deepEqual(disposableRegistry, [disposable, disposable, disposable, disposable]); - }); -}); diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts deleted file mode 100644 index 5728bf0c34cd..000000000000 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import rewiremock from 'rewiremock'; -import { SemVer } from 'semver'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types'; -import { Architecture } from '../../../../client/common/utils/platform'; -import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../../client/debugger/extension/adapter/factory'; -import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types'; -import { IInterpreterService } from '../../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../../client/interpreter/interpreterService'; -import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; -import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { EventName } from '../../../../client/telemetry/constants'; -import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; -import { ICommandManager } from '../../../../client/common/application/types'; -import { CommandManager } from '../../../../client/common/application/commandManager'; - -use(chaiAsPromised); - -suite('Debugging - Adapter Factory', () => { - let factory: IDebugAdapterDescriptorFactory; - let interpreterService: IInterpreterService; - let stateFactory: IPersistentStateFactory; - let state: PersistentState; - let showErrorMessageStub: sinon.SinonStub; - let readJSONSyncStub: sinon.SinonStub; - let commandManager: ICommandManager; - - const nodeExecutable = undefined; - const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); - const pythonPath = path.join('path', 'to', 'python', 'interpreter'); - const interpreter = { - architecture: Architecture.Unknown, - path: pythonPath, - sysPrefix: '', - sysVersion: '', - envType: EnvironmentType.Unknown, - version: new SemVer('3.7.4-test'), - }; - const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; - const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; - - class Reporter { - public static eventNames: string[] = []; - public static properties: Record[] = []; - public static measures: {}[] = []; - public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { - Reporter.eventNames.push(eventName); - Reporter.properties.push(properties!); - Reporter.measures.push(measures!); - } - } - - setup(() => { - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; - readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); - readJSONSyncStub.returns({ enableTelemetry: true }); - rewiremock.enable(); - rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState) as PersistentState; - commandManager = mock(CommandManager); - - showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); - - when( - stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), - ).thenReturn(instance(state)); - - const configurationService = mock(ConfigurationService); - when(configurationService.getSettings(undefined)).thenReturn(({ - experiments: { enabled: true }, - } as any) as IPythonSettings); - - interpreterService = mock(InterpreterService); - - when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); - when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]); - - factory = new DebugAdapterDescriptorFactory( - instance(commandManager), - instance(interpreterService), - instance(stateFactory), - ); - }); - - teardown(() => { - process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; - process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; - Reporter.properties = []; - Reporter.eventNames = []; - Reporter.measures = []; - rewiremock.disable(); - clearTelemetryReporter(); - sinon.restore(); - }); - - function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { - return { - configuration: { name: '', request: 'launch', type: 'python', ...config }, - id: '', - name: 'python', - type: 'python', - workspaceFolder, - customRequest: () => Promise.resolve(), - getDebugProtocolBreakpoint: () => Promise.resolve(undefined), - }; - } - - test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { - const session = createSession({ pythonPath }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { - const session = createSession({}); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Return the path of the first available interpreter as the current python path, configuration.pythonPath is not defined and there is no active interpreter', async () => { - const session = createSession({}); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Display a message if no python interpreter is set', async () => { - when(interpreterService.getInterpreters(anything())).thenReturn([]); - const session = createSession({}); - - const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); - - await expect(promise).to.eventually.be.rejectedWith('Debug Adapter Executable not provided'); - sinon.assert.calledOnce(showErrorMessageStub); - }); - - test('Display a message if python version is less than 3.7', async () => { - when(interpreterService.getInterpreters(anything())).thenReturn([]); - const session = createSession({}); - const deprecatedInterpreter = { - architecture: Architecture.Unknown, - path: pythonPath, - sysPrefix: '', - sysVersion: '', - envType: EnvironmentType.Unknown, - version: new SemVer('3.6.12-test'), - }; - when(state.value).thenReturn(false); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(deprecatedInterpreter); - - await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - sinon.assert.calledOnce(showErrorMessageStub); - }); - - test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { - const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); - const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - // Interpreter not needed for host/port - verify(interpreterService.getInterpreters(anything())).never(); - assert.deepStrictEqual(descriptor, debugServer); - }); - - test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { - const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); - const debugServer = new DebugAdapterServer( - session.configuration.connect.port, - session.configuration.connect.host, - ); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - // Interpreter not needed for connect - verify(interpreterService.getInterpreters(anything())).never(); - assert.deepStrictEqual(descriptor, debugServer); - }); - - test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { - const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { - const session = createSession({ - request: 'attach', - port: undefined, - processId: undefined, - listen: undefined, - connect: undefined, - }); - - const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); - - await expect(promise).to.eventually.be.rejectedWith( - '"request":"attach" requires either "connect", "listen", or "processId"', - ); - }); - - test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { - const session = createSession({ logToFile: true }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [ - debugAdapterPath, - '--log-dir', - EXTENSION_ROOT_DIR, - ]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { - const session = createSession({}); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { - const session = createSession({ logToFile: false }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Send attach to local process telemetry if attaching to a local process', async () => { - const session = createSession({ request: 'attach', processId: 1234 }); - await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); - }); - - test("Don't send any telemetry if not attaching to a local process", async () => { - const session = createSession({}); - - await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); - }); - - test('Use "debugAdapterPath" when specified', async () => { - const customAdapterPath = 'custom/debug/adapter/path'; - const session = createSession({ debugAdapterPath: customAdapterPath }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Use "debugAdapterPython" when specified', async () => { - const session = createSession({ debugAdapterPython: '/bin/custompy' }); - const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); - const customInterpreter = { - architecture: Architecture.Unknown, - path: '/bin/custompy', - sysPrefix: '', - sysVersion: '', - envType: EnvironmentType.Unknown, - version: new SemVer('3.7.4-test'), - }; - when(interpreterService.getInterpreterDetails('/bin/custompy')).thenResolve(customInterpreter); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); - - test('Do not use "python" to spawn the debug adapter', async () => { - const session = createSession({ python: '/bin/custompy' }); - const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); - - const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.deepStrictEqual(descriptor, debugExecutable); - }); -}); diff --git a/src/test/debugger/extension/adapter/logging.unit.test.ts b/src/test/debugger/extension/adapter/logging.unit.test.ts deleted file mode 100644 index 18fbb2b66058..000000000000 --- a/src/test/debugger/extension/adapter/logging.unit.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as fs from 'fs'; -import * as path from 'path'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { DebugSession, WorkspaceFolder } from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; - -import { FileSystem } from '../../../../client/common/platform/fileSystem'; -import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; - -suite('Debugging - Session Logging', () => { - const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; - const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; - let loggerFactory: DebugSessionLoggingFactory; - let fsService: FileSystem; - let writeStream: fs.WriteStream; - - setup(() => { - fsService = mock(FileSystem); - writeStream = mock(fs.WriteStream); - - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; - - loggerFactory = new DebugSessionLoggingFactory(instance(fsService)); - }); - - teardown(() => { - process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; - process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; - }); - - function createSession(id: string, workspaceFolder?: WorkspaceFolder): DebugSession { - return { - configuration: { - name: '', - request: 'launch', - type: 'python', - }, - id: id, - name: 'python', - type: 'python', - workspaceFolder, - customRequest: () => Promise.resolve(), - getDebugProtocolBreakpoint: () => Promise.resolve(undefined), - }; - } - - function createSessionWithLogging(id: string, logToFile: boolean, workspaceFolder?: WorkspaceFolder): DebugSession { - const session = createSession(id, workspaceFolder); - session.configuration.logToFile = logToFile; - return session; - } - - class TestMessage implements DebugProtocol.ProtocolMessage { - public seq: number; - public type: string; - public id: number; - public format: string; - public variables?: { [key: string]: string }; - public sendTelemetry?: boolean; - public showUser?: boolean; - public url?: string; - public urlLabel?: string; - constructor(id: number, seq: number, type: string) { - this.id = id; - this.format = 'json'; - this.seq = seq; - this.type = type; - } - } - - test('Create logger using session without logToFile', async () => { - const session = createSession('test1'); - const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); - - await loggerFactory.createDebugAdapterTracker(session); - - verify(fsService.createWriteStream(filePath)).never(); - }); - - test('Create logger using session with logToFile set to false', async () => { - const session = createSessionWithLogging('test2', false); - const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); - - when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); - when(writeStream.write(anything())).thenReturn(true); - const logger = await loggerFactory.createDebugAdapterTracker(session); - if (logger) { - logger.onWillStartSession!(); - } - - verify(fsService.createWriteStream(filePath)).never(); - verify(writeStream.write(anything())).never(); - }); - - test('Create logger using session with logToFile set to true', async () => { - const session = createSessionWithLogging('test3', true); - const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); - const logs: string[] = []; - - when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); - when(writeStream.write(anything())).thenCall((msg) => logs.push(msg)); - - const message = new TestMessage(1, 1, 'test-message'); - const logger = await loggerFactory.createDebugAdapterTracker(session); - - if (logger) { - logger.onWillStartSession!(); - assert.ok(logs.pop()!.includes('Starting Session')); - - logger.onDidSendMessage!(message); - const sentLog = logs.pop(); - assert.ok(sentLog!.includes('Client <-- Adapter')); - assert.ok(sentLog!.includes('test-message')); - - logger.onWillReceiveMessage!(message); - const receivedLog = logs.pop(); - assert.ok(receivedLog!.includes('Client --> Adapter')); - assert.ok(receivedLog!.includes('test-message')); - - logger.onWillStopSession!(); - assert.ok(logs.pop()!.includes('Stopping Session')); - - logger.onError!(new Error('test error message')); - assert.ok(logs.pop()!.includes('Error')); - - logger.onExit!(111, '222'); - const exitLog1 = logs.pop(); - assert.ok(exitLog1!.includes('Exit-Code: 111')); - assert.ok(exitLog1!.includes('Signal: 222')); - - logger.onExit!(undefined, undefined); - const exitLog2 = logs.pop(); - assert.ok(exitLog2!.includes('Exit-Code: 0')); - assert.ok(exitLog2!.includes('Signal: none')); - } - - verify(fsService.createWriteStream(filePath)).once(); - verify(writeStream.write(anything())).times(7); - assert.deepEqual(logs, []); - }); -}); diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts deleted file mode 100644 index 0ab094119a5c..000000000000 --- a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { anyString, anything, mock, when } from 'ts-mockito'; -import { DebugSession, WorkspaceFolder } from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { createDeferred, sleep } from '../../../../client/common/utils/async'; -import { Common } from '../../../../client/common/utils/localize'; -import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; -import { clearTelemetryReporter } from '../../../../client/telemetry'; -import * as browserApis from '../../../../client/common/vscodeApis/browserApis'; -import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { IPythonSettings } from '../../../../client/common/types'; - -suite('Debugging - Outdated Debugger Prompt tests.', () => { - let promptFactory: OutdatedDebuggerPromptFactory; - let showInformationMessageStub: sinon.SinonStub; - let browserLaunchStub: sinon.SinonStub; - - const ptvsdOutputEvent: DebugProtocol.OutputEvent = { - seq: 1, - type: 'event', - event: 'output', - body: { category: 'telemetry', output: 'ptvsd', data: { packageVersion: '4.3.2' } }, - }; - - const debugpyOutputEvent: DebugProtocol.OutputEvent = { - seq: 1, - type: 'event', - event: 'output', - body: { category: 'telemetry', output: 'debugpy', data: { packageVersion: '1.0.0' } }, - }; - - setup(() => { - const configurationService = mock(ConfigurationService); - when(configurationService.getSettings(undefined)).thenReturn(({ - experiments: { enabled: true }, - } as any) as IPythonSettings); - - showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); - browserLaunchStub = sinon.stub(browserApis, 'launch'); - - promptFactory = new OutdatedDebuggerPromptFactory(); - }); - - teardown(() => { - sinon.restore(); - clearTelemetryReporter(); - }); - - function createSession(workspaceFolder?: WorkspaceFolder): DebugSession { - return { - configuration: { - name: '', - request: 'launch', - type: 'python', - }, - id: 'test1', - name: 'python', - type: 'python', - workspaceFolder, - customRequest: () => Promise.resolve(), - getDebugProtocolBreakpoint: () => Promise.resolve(undefined), - }; - } - - test('Show prompt when attaching to ptvsd, more info is NOT clicked', async () => { - showInformationMessageStub.returns(Promise.resolve(undefined)); - const session = createSession(); - const prompter = await promptFactory.createDebugAdapterTracker(session); - if (prompter) { - prompter.onDidSendMessage!(ptvsdOutputEvent); - } - - browserLaunchStub.neverCalledWith(anyString()); - - // First call should show info once - - sinon.assert.calledOnce(showInformationMessageStub); - assert(prompter); - - prompter!.onDidSendMessage!(ptvsdOutputEvent); - // Can't use deferred promise here - await sleep(1); - - browserLaunchStub.neverCalledWith(anyString()); - // Second time it should not be called, so overall count is one. - sinon.assert.calledOnce(showInformationMessageStub); - }); - - test('Show prompt when attaching to ptvsd, more info is clicked', async () => { - showInformationMessageStub.returns(Promise.resolve(Common.moreInfo)); - - const deferred = createDeferred(); - browserLaunchStub.callsFake(() => deferred.resolve()); - browserLaunchStub.onCall(1).callsFake(() => { - return new Promise(() => deferred.resolve()); - }); - - const session = createSession(); - const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); - - prompter!.onDidSendMessage!(ptvsdOutputEvent); - await deferred.promise; - - sinon.assert.calledOnce(browserLaunchStub); - - // First call should show info once - sinon.assert.calledOnce(showInformationMessageStub); - - prompter!.onDidSendMessage!(ptvsdOutputEvent); - // The second call does not go through the same path. So we just give enough time for the - // operation to complete. - await sleep(1); - - sinon.assert.calledOnce(browserLaunchStub); - - // Second time it should not be called, so overall count is one. - sinon.assert.calledOnce(showInformationMessageStub); - }); - - test("Don't show prompt attaching to debugpy", async () => { - showInformationMessageStub.returns(Promise.resolve(undefined)); - - const session = createSession(); - const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); - - prompter!.onDidSendMessage!(debugpyOutputEvent); - // Can't use deferred promise here - await sleep(1); - - showInformationMessageStub.neverCalledWith(anything(), anything()); - }); - - const someRequest: DebugProtocol.RunInTerminalRequest = { - seq: 1, - type: 'request', - command: 'runInTerminal', - arguments: { - cwd: '', - args: [''], - }, - }; - const someEvent: DebugProtocol.ContinuedEvent = { - seq: 1, - type: 'event', - event: 'continued', - body: { threadId: 1, allThreadsContinued: true }, - }; - // Notice that this is stdout, not telemetry event. - const someOutputEvent: DebugProtocol.OutputEvent = { - seq: 1, - type: 'event', - event: 'output', - body: { category: 'stdout', output: 'ptvsd' }, - }; - - [someRequest, someEvent, someOutputEvent].forEach((message) => { - test(`Don't show prompt when non-telemetry events are seen: ${JSON.stringify(message)}`, async () => { - showInformationMessageStub.returns(Promise.resolve(undefined)); - - const session = createSession(); - const prompter = await promptFactory.createDebugAdapterTracker(session); - assert(prompter); - - prompter!.onDidSendMessage!(message); - // Can't use deferred promise here - await sleep(1); - - showInformationMessageStub.neverCalledWith(anything(), anything()); - }); - }); -}); diff --git a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts deleted file mode 100644 index 4c4deb3cb9ad..000000000000 --- a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify } from 'ts-mockito'; -import { Disposable } from 'vscode'; -import { ApplicationShell } from '../../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types'; -import { Commands } from '../../../../client/common/constants'; -import { PlatformService } from '../../../../client/common/platform/platformService'; -import { IPlatformService } from '../../../../client/common/platform/types'; -import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; -import { IProcessServiceFactory } from '../../../../client/common/process/types'; -import { IDisposableRegistry } from '../../../../client/common/types'; -import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; - -suite('Attach to process - attach process provider factory', () => { - let applicationShell: IApplicationShell; - let commandManager: ICommandManager; - let platformService: IPlatformService; - let processServiceFactory: IProcessServiceFactory; - let disposableRegistry: IDisposableRegistry; - - let factory: AttachProcessProviderFactory; - - setup(() => { - applicationShell = mock(ApplicationShell); - commandManager = mock(CommandManager); - platformService = mock(PlatformService); - processServiceFactory = mock(ProcessServiceFactory); - disposableRegistry = []; - - factory = new AttachProcessProviderFactory( - instance(applicationShell), - instance(commandManager), - instance(platformService), - instance(processServiceFactory), - disposableRegistry, - ); - }); - - test('Register commands should not fail', () => { - factory.registerCommands(); - - verify(commandManager.registerCommand(Commands.PickLocalProcess, anything(), anything())).once(); - assert.strictEqual((disposableRegistry as Disposable[]).length, 1); - }); -}); diff --git a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts deleted file mode 100644 index 64d9103f3c5d..000000000000 --- a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts +++ /dev/null @@ -1,459 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { PlatformService } from '../../../../client/common/platform/platformService'; -import { IPlatformService } from '../../../../client/common/platform/types'; -import { ProcessService } from '../../../../client/common/process/proc'; -import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; -import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; -import { OSType } from '../../../../client/common/utils/platform'; -import { AttachProcessProvider } from '../../../../client/debugger/extension/attachQuickPick/provider'; -import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; -import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; -import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; - -suite('Attach to process - process provider', () => { - let platformService: IPlatformService; - let processService: IProcessService; - let processServiceFactory: IProcessServiceFactory; - - let provider: AttachProcessProvider; - - setup(() => { - platformService = mock(PlatformService); - processService = mock(ProcessService); - processServiceFactory = mock(ProcessServiceFactory); - when(processServiceFactory.create()).thenResolve(instance(processService)); - - provider = new AttachProcessProvider(instance(platformService), instance(processServiceFactory)); - }); - - test('The Linux process list command should be called if the platform is Linux', async () => { - when(platformService.isMac).thenReturn(false); - when(platformService.isLinux).thenReturn(true); - const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -1 launchd launchd -41 syslogd syslogd -146 kextd kextd -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - ]; - when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput, - }); - - const attachItems = await provider._getInternalProcessEntries(); - - verify( - processService.exec( - PsProcessParser.psLinuxCommand.command, - PsProcessParser.psLinuxCommand.args, - anything(), - ), - ).once(); - assert.deepEqual(attachItems, expectedOutput); - }); - - test('The macOS process list command should be called if the platform is macOS', async () => { - when(platformService.isMac).thenReturn(true); - const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -1 launchd launchd -41 syslogd syslogd -146 kextd kextd -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - ]; - when(processService.exec(PsProcessParser.psDarwinCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput, - }); - - const attachItems = await provider._getInternalProcessEntries(); - - verify( - processService.exec( - PsProcessParser.psDarwinCommand.command, - PsProcessParser.psDarwinCommand.args, - anything(), - ), - ).once(); - assert.deepEqual(attachItems, expectedOutput); - }); - - test('The Windows process list command should be called if the platform is Windows', async () => { - const windowsOutput = `CommandLine=\r -Name=System\r -ProcessId=4\r -\r -\r -CommandLine=sihost.exe\r -Name=sihost.exe\r -ProcessId=5728\r -\r -\r -CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r -Name=svchost.exe\r -ProcessId=5912\r -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'System', - description: '4', - detail: '', - id: '4', - processName: 'System', - commandLine: '', - }, - { - label: 'sihost.exe', - description: '5728', - detail: 'sihost.exe', - id: '5728', - processName: 'sihost.exe', - commandLine: 'sihost.exe', - }, - { - label: 'svchost.exe', - description: '5912', - detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - id: '5912', - processName: 'svchost.exe', - commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - }, - ]; - when(platformService.isMac).thenReturn(false); - when(platformService.isLinux).thenReturn(false); - when(platformService.isWindows).thenReturn(true); - when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ - stdout: windowsOutput, - }); - - const attachItems = await provider._getInternalProcessEntries(); - - verify( - processService.exec(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, anything()), - ).once(); - assert.deepEqual(attachItems, expectedOutput); - }); - - test('An error should be thrown if the platform is neither Linux, macOS or Windows', async () => { - when(platformService.isMac).thenReturn(false); - when(platformService.isLinux).thenReturn(false); - when(platformService.isWindows).thenReturn(false); - when(platformService.osType).thenReturn(OSType.Unknown); - - const promise = provider._getInternalProcessEntries(); - - await expect(promise).to.eventually.be.rejectedWith(`Operating system '${OSType.Unknown}' not supported.`); - }); - - suite('POSIX getAttachItems (Linux)', () => { - setup(() => { - when(platformService.isMac).thenReturn(false); - when(platformService.isLinux).thenReturn(true); - }); - - test('Items returned by getAttachItems should be sorted alphabetically', async () => { - const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - 1 launchd launchd - 41 syslogd syslogd - 146 kextd kextd -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - ]; - when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput, - }); - - const output = await provider.getAttachItems(); - - assert.deepEqual(output, expectedOutput); - }); - - test('Python processes should be at the top of the list returned by getAttachItems', async () => { - const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - 1 launchd launchd - 41 syslogd syslogd - 96 python python - 146 kextd kextd - 31896 python python script.py -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'python', - description: '96', - detail: 'python', - id: '96', - processName: 'python', - commandLine: 'python', - }, - { - label: 'python', - description: '31896', - detail: 'python script.py', - id: '31896', - processName: 'python', - commandLine: 'python script.py', - }, - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - ]; - when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ - stdout: psOutput, - }); - - const output = await provider.getAttachItems(); - - assert.deepEqual(output, expectedOutput); - }); - }); - - suite('Windows getAttachItems', () => { - setup(() => { - when(platformService.isMac).thenReturn(false); - when(platformService.isLinux).thenReturn(false); - when(platformService.isWindows).thenReturn(true); - }); - - test('Items returned by getAttachItems should be sorted alphabetically', async () => { - const windowsOutput = `CommandLine=\r -Name=System\r -ProcessId=4\r -\r -\r -CommandLine=\r -Name=svchost.exe\r -ProcessId=5372\r -\r -\r -CommandLine=sihost.exe\r -Name=sihost.exe\r -ProcessId=5728\r -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'sihost.exe', - description: '5728', - detail: 'sihost.exe', - id: '5728', - processName: 'sihost.exe', - commandLine: 'sihost.exe', - }, - { - label: 'svchost.exe', - description: '5372', - detail: '', - id: '5372', - processName: 'svchost.exe', - commandLine: '', - }, - { - label: 'System', - description: '4', - detail: '', - id: '4', - processName: 'System', - commandLine: '', - }, - ]; - when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ - stdout: windowsOutput, - }); - - const output = await provider.getAttachItems(); - - assert.deepEqual(output, expectedOutput); - }); - - test('Python processes should be at the top of the list returned by getAttachItems', async () => { - const windowsOutput = `CommandLine=\r -Name=System\r -ProcessId=4\r -\r -\r -CommandLine=\r -Name=svchost.exe\r -ProcessId=5372\r -\r -\r -CommandLine=sihost.exe\r -Name=sihost.exe\r -ProcessId=5728\r -\r -\r -CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r -Name=svchost.exe\r -ProcessId=5912\r -\r -\r -CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r -Name=python.exe\r -ProcessId=6028\r -\r -\r -CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py\r -Name=python.exe\r -ProcessId=8026\r - `; - const expectedOutput: IAttachItem[] = [ - { - label: 'python.exe', - description: '8026', - detail: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', - id: '8026', - processName: 'python.exe', - commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', - }, - { - label: 'python.exe', - description: '6028', - detail: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - id: '6028', - processName: 'python.exe', - commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - }, - { - label: 'sihost.exe', - description: '5728', - detail: 'sihost.exe', - id: '5728', - processName: 'sihost.exe', - commandLine: 'sihost.exe', - }, - { - label: 'svchost.exe', - description: '5372', - detail: '', - id: '5372', - processName: 'svchost.exe', - commandLine: '', - }, - { - label: 'svchost.exe', - description: '5912', - detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - id: '5912', - processName: 'svchost.exe', - commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - }, - { - label: 'System', - description: '4', - detail: '', - id: '4', - processName: 'System', - commandLine: '', - }, - ]; - when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ - stdout: windowsOutput, - }); - - const output = await provider.getAttachItems(); - - assert.deepEqual(output, expectedOutput); - }); - }); -}); diff --git a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts deleted file mode 100644 index 160c53a60c40..000000000000 --- a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; -import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; - -suite('Attach to process - ps process parser (POSIX)', () => { - test('Processes should be parsed correctly if it is valid input', () => { - const input = `\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ - 1 launchd launchd\n\ - 41 syslogd syslogd\n\ - 42 UserEventAgent UserEventAgent (System)\n\ - 45 uninstalld uninstalld\n\ - 146 kextd kextd\n\ -31896 python python script.py\ -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - { - label: 'UserEventAgent', - description: '42', - detail: 'UserEventAgent (System)', - id: '42', - processName: 'UserEventAgent', - commandLine: 'UserEventAgent (System)', - }, - { - label: 'uninstalld', - description: '45', - detail: 'uninstalld', - id: '45', - processName: 'uninstalld', - commandLine: 'uninstalld', - }, - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - { - label: 'python', - description: '31896', - detail: 'python script.py', - id: '31896', - processName: 'python', - commandLine: 'python script.py', - }, - ]; - - const output = PsProcessParser.parseProcesses(input); - - assert.deepEqual(output, expectedOutput); - }); - - test('Empty lines should be skipped when parsing process list input', () => { - const input = `\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ - 1 launchd launchd\n\ - 41 syslogd syslogd\n\ - 42 UserEventAgent UserEventAgent (System)\n\ -\n\ - 146 kextd kextd\n\ - 31896 python python script.py\ -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - { - label: 'UserEventAgent', - description: '42', - detail: 'UserEventAgent (System)', - id: '42', - processName: 'UserEventAgent', - commandLine: 'UserEventAgent (System)', - }, - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - { - label: 'python', - description: '31896', - detail: 'python script.py', - id: '31896', - processName: 'python', - commandLine: 'python script.py', - }, - ]; - - const output = PsProcessParser.parseProcesses(input); - - assert.deepEqual(output, expectedOutput); - }); - - test('Incorrectly formatted lines should be skipped when parsing process list input', () => { - const input = `\ - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ - 1 launchd launchd\n\ - 41 syslogd syslogd\n\ - 42 UserEventAgent UserEventAgent (System)\n\ - 45 uninstalld uninstalld\n\ - 146 kextd kextd\n\ - 31896 python python script.py\ -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'launchd', - description: '1', - detail: 'launchd', - id: '1', - processName: 'launchd', - commandLine: 'launchd', - }, - { - label: 'syslogd', - description: '41', - detail: 'syslogd', - id: '41', - processName: 'syslogd', - commandLine: 'syslogd', - }, - { - label: 'UserEventAgent', - description: '42', - detail: 'UserEventAgent (System)', - id: '42', - processName: 'UserEventAgent', - commandLine: 'UserEventAgent (System)', - }, - { - label: 'kextd', - description: '146', - detail: 'kextd', - id: '146', - processName: 'kextd', - commandLine: 'kextd', - }, - { - label: 'python', - description: '31896', - detail: 'python script.py', - id: '31896', - processName: 'python', - commandLine: 'python script.py', - }, - ]; - - const output = PsProcessParser.parseProcesses(input); - - assert.deepEqual(output, expectedOutput); - }); -}); diff --git a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts deleted file mode 100644 index e29490c47926..000000000000 --- a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; -import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; - -suite('Attach to process - wmic process parser (Windows)', () => { - test('Processes should be parsed correctly if it is valid input', () => { - const input = ` -CommandLine=\r\n\ -Name=System\r\n\ -ProcessId=4\r\n\ -\r\n\ -\r\n\ -CommandLine=\r\n\ -Name=svchost.exe\r\n\ -ProcessId=5372\r\n\ -\r\n\ -\r\n\ -CommandLine=sihost.exe\r\n\ -Name=sihost.exe\r\n\ -ProcessId=5728\r\n\ -\r\n\ -\r\n\ -CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ -Name=svchost.exe\r\n\ -ProcessId=5912\r\n\ -\r\n\ -\r\n\ -CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ -Name=python.exe\r\n\ -ProcessId=6028\r\n\ -`; - const expectedOutput: IAttachItem[] = [ - { - label: 'System', - description: '4', - detail: '', - id: '4', - processName: 'System', - commandLine: '', - }, - { - label: 'svchost.exe', - description: '5372', - detail: '', - id: '5372', - processName: 'svchost.exe', - commandLine: '', - }, - { - label: 'sihost.exe', - description: '5728', - detail: 'sihost.exe', - id: '5728', - processName: 'sihost.exe', - commandLine: 'sihost.exe', - }, - { - label: 'svchost.exe', - description: '5912', - detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - id: '5912', - processName: 'svchost.exe', - commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - }, - { - label: 'python.exe', - description: '6028', - detail: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - id: '6028', - processName: 'python.exe', - commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - }, - ]; - - const output = WmicProcessParser.parseProcesses(input); - - assert.deepEqual(output, expectedOutput); - }); - - test('Incorrectly formatted lines should be skipped when parsing process list input', () => { - const input = ` -CommandLine=\r\n\ -Name=System\r\n\ -ProcessId=4\r\n\ -\r\n\ -\r\n\ -CommandLine=\r\n\ -Name=svchost.exe\r\n\ -ProcessId=5372\r\n\ -\r\n\ -\r\n\ -CommandLine=sihost.exe\r\n\ -Name=sihost.exe\r\n\ -ProcessId=5728\r\n\ -\r\n\ -\r\n\ -CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ -Name=svchost.exe\r\n\ -IncorrectKey=shouldnt.be.here\r\n\ -ProcessId=5912\r\n\ -\r\n\ -\r\n\ -CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ -Name=python.exe\r\n\ -ProcessId=6028\r\n\ -`; - - const expectedOutput: IAttachItem[] = [ - { - label: 'System', - description: '4', - detail: '', - id: '4', - processName: 'System', - commandLine: '', - }, - { - label: 'svchost.exe', - description: '5372', - detail: '', - id: '5372', - processName: 'svchost.exe', - commandLine: '', - }, - { - label: 'sihost.exe', - description: '5728', - detail: 'sihost.exe', - id: '5728', - processName: 'sihost.exe', - commandLine: 'sihost.exe', - }, - { - label: 'svchost.exe', - description: '5912', - detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - id: '5912', - processName: 'svchost.exe', - commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', - }, - { - label: 'python.exe', - description: '6028', - detail: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - id: '6028', - processName: 'python.exe', - commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - }, - ]; - - const output = WmicProcessParser.parseProcesses(input); - - assert.deepEqual(output, expectedOutput); - }); - - test('Command lines starting with a DOS device path prefix should be parsed correctly', () => { - const input = ` -CommandLine=\r\n\ -Name=System\r\n\ -ProcessId=4\r\n\ -\r\n\ -\r\n\ -CommandLine=\\??\\C:\\WINDOWS\\system32\\conhost.exe\r\n\ -Name=conhost.exe\r\n\ -ProcessId=5912\r\n\ -\r\n\ -\r\n\ -CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ -Name=python.exe\r\n\ -ProcessId=6028\r\n\ -`; - - const expectedOutput: IAttachItem[] = [ - { - label: 'System', - description: '4', - detail: '', - id: '4', - processName: 'System', - commandLine: '', - }, - { - label: 'conhost.exe', - description: '5912', - detail: 'C:\\WINDOWS\\system32\\conhost.exe', - id: '5912', - processName: 'conhost.exe', - commandLine: 'C:\\WINDOWS\\system32\\conhost.exe', - }, - { - label: 'python.exe', - description: '6028', - detail: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - id: '6028', - processName: 'python.exe', - commandLine: - 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', - }, - ]; - - const output = WmicProcessParser.parseProcesses(input); - - assert.deepEqual(output, expectedOutput); - }); -}); diff --git a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts deleted file mode 100644 index a850d50150ae..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { deepEqual, instance, mock, verify } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { - CancellationTokenSource, - CompletionItem, - CompletionItemKind, - Position, - SnippetString, - TextDocument, - Uri, -} from 'vscode'; -import { LanguageService } from '../../../../../client/common/application/languageService'; -import { ILanguageService } from '../../../../../client/common/application/types'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { LaunchJsonCompletionProvider } from '../../../../../client/debugger/extension/configuration/launch.json/completionProvider'; - -suite('Debugging - launch.json Completion Provider', () => { - let completionProvider: LaunchJsonCompletionProvider; - let languageService: ILanguageService; - - setup(() => { - languageService = mock(LanguageService); - completionProvider = new LaunchJsonCompletionProvider(instance(languageService), []); - }); - test('Activation will register the completion provider', async () => { - await completionProvider.activate(); - verify( - languageService.registerCompletionItemProvider(deepEqual({ language: 'json' }), completionProvider), - ).once(); - verify( - languageService.registerCompletionItemProvider(deepEqual({ language: 'jsonc' }), completionProvider), - ).once(); - }); - test('Cannot provide completions for non launch.json files', () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - document.setup((doc) => doc.uri).returns(() => Uri.file(__filename)); - assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); - - document.reset(); - document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); - }); - function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); - const canProvideCompletions = LaunchJsonCompletionProvider.canProvideCompletions(document.object, position); - assert.strictEqual(canProvideCompletions, expectedValue); - } - test('Cannot provide completions when there is no configurations section in json', () => { - const position = new Position(0, 0); - const config = `{ - "version": "0.1.0" -}`; - testCanProvideCompletions(position, 1, config as string, false); - }); - test('Cannot provide completions when cursor position is not in configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [] -}`; - testCanProvideCompletions(position, 10, json, false); - }); - test('Cannot provide completions when cursor position is in an empty configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - testCanProvideCompletions(position, json.indexOf('# Cursor Position'), json, true); - }); - test('No Completions for non launch.json', async () => { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - const { token } = new CancellationTokenSource(); - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 0); - }); - test('No Completions for files ending with launch.json', async () => { - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.uri).returns(() => Uri.file('x-launch.json')); - const { token } = new CancellationTokenSource(); - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 0); - }); - test('Get Completions', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.strictEqual(completions.length, 1); - - const expectedCompletionItem: CompletionItem = { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description, - arguments: [document.object, position, token], - }, - documentation: DebugConfigStrings.launchJsonCompletions.description, - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label, - insertText: new SnippetString(), - }; - - assert.deepEqual(completions[0], expectedCompletionItem); - }); -}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts deleted file mode 100644 index b2addd24267b..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { CommandManager } from '../../../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../../../client/common/application/types'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterService } from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; -import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let commandManager: ICommandManager; - let debugConfigService: IDebugConfigurationService; - setup(() => { - commandManager = mock(CommandManager); - debugConfigService = mock(PythonDebugConfigurationService); - helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); - }); - test('Activation will register the required commands', async () => { - const service = new LaunchJsonUpdaterService([], instance(debugConfigService)); - await service.activate(); - verify( - commandManager.registerCommand( - 'python.SelectAndInsertDebugConfiguration', - helper.selectAndInsertDebugConfig, - helper, - ), - ); - }); -}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts deleted file mode 100644 index 53118d68025e..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts +++ /dev/null @@ -1,496 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { - CancellationTokenSource, - DebugConfiguration, - Position, - Range, - TextDocument, - TextEditor, - TextLine, - Uri, -} from 'vscode'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; -import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; -import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; - -type LaunchJsonSchema = { - version: string; - configurations: DebugConfiguration[]; -}; - -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let getWorkspaceFolderStub: sinon.SinonStub; - let getActiveTextEditorStub: sinon.SinonStub; - let applyEditStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let debugConfigService: IDebugConfigurationService; - - const sandbox = sinon.createSandbox(); - setup(() => { - getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); - applyEditStub = sinon.stub(workspaceApis, 'applyEdit'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - debugConfigService = mock(PythonDebugConfigurationService); - sandbox.stub(LaunchJsonUpdaterServiceHelper, 'isCommaImmediatelyBeforeCursor').returns(false); - helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); - }); - teardown(() => { - sandbox.restore(); - sinon.restore(); - }); - - test('Configuration Array is detected as being empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, true); - }); - test('Configuration Array is not empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, false); - }); - test('Cursor is not positioned in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, undefined); - }); - test('Cursor is positioned in the empty configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] - }`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'InsideEmptyArray'); - }); - test('Cursor is positioned before an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned before an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned after an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - }] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Cursor is positioned after an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - - const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( - document.object, - new Position(0, 0), - ); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Text to be inserted must be prefixed with a comma', async () => { - const config = {} as DebugConfiguration; - const expectedText = `,${JSON.stringify(config)}`; - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed with a comma (as a comma already exists)', async () => { - const config = {} as DebugConfiguration; - const expectedText = JSON.stringify(config); - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem', 'BeforeCursor'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must be suffixed with a comma', async () => { - const config = {} as DebugConfiguration; - const expectedText = `${JSON.stringify(config)},`; - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'BeforeItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { - const config = {} as DebugConfiguration; - const expectedText = JSON.stringify(config); - - const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'InsideEmptyArray'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('When inserting the debug config into the json file format the document', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - const config = {} as DebugConfiguration; - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - applyEditStub.returns(undefined); - executeCommandStub.withArgs('editor.action.formatDocument').resolves(); - - await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document.object, new Position(0, 0), config); - - sinon.assert.calledOnce(applyEditStub); - sinon.assert.calledOnce(executeCommandStub.withArgs('editor.action.formatDocument')); - }); - test('No changes to configuration if there is not active document', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - getActiveTextEditorStub.returns(undefined); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.notCalled(getWorkspaceFolderStub); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if the active document is not same as the document passed in', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const { token } = new CancellationTokenSource(); - const textEditor = typemoq.Mock.ofType(); - textEditor - .setup((t) => t.document) - .returns(() => ('x' as unknown) as TextDocument) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.notCalled(getWorkspaceFolderStub); - textEditor.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if cancellation token has been cancelled', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.returns(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([''] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if no configuration items are returned', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.returns(folder); - - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.calledOnce(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('Changes are made to the configuration', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const { token } = tokenSource; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - getActiveTextEditorStub.returns(textEditor.object); - getWorkspaceFolderStub.withArgs(docUri).returns(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([ - 'config', - ] as unknown) as void); - let debugConfigInserted = false; - LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - sinon.assert.called(getActiveTextEditorStub); - sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, true); - }); - test('If cursor is at the begining of line 1 then there is no comma before cursor', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(1, 0); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 1) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after some text (not a comma) then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 1, 5) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => 'Hello') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after a comma then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3) } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '}, ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line ends with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '}, ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line does not end with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '} ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts deleted file mode 100644 index 8a5898611c82..000000000000 --- a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Uri } from 'vscode'; -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as djangoLaunch from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -suite('Debugging - Configuration Provider Django', () => { - let pathExistsStub: sinon.SinonStub; - let pathSeparatorStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - let input: MultiStepInput; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - }); - teardown(() => { - sinon.restore(); - }); - test("getManagePyPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - pathExistsStub.withArgs(managePyPath).resolves(false); - const file = await djangoLaunch.getManagePyPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getManagePyPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - pathExistsStub.withArgs(managePyPath).resolves(true); - pathSeparatorStub.value('-'); - const file = await djangoLaunch.getManagePyPath(folder); - - expect(file).to.be.equal('${workspaceFolder}-manage.py'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test("Validation of path should return errors if resolved path doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz').resolves(false); - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.txt').resolves(true); - const error = await djangoLaunch.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.py').resolves(true); - const error = await djangoLaunch.validateManagePy(folder, '', 'xyz.py'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with selected managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: 'hello', - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const workspaceFolderToken = '${workspaceFolder}'; - const defaultProgram = `${workspaceFolderToken}-manage.py`; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve(); - await djangoLaunch.buildDjangoLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: defaultProgram, - args: ['runserver'], - django: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts deleted file mode 100644 index 80ce37167024..000000000000 --- a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import * as fastApiLaunch from '../../../../../client/debugger/extension/configuration/providers/fastapiLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider FastAPI', () => { - let input: MultiStepInput; - let pathExistsStub: sinon.SinonStub; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - }); - teardown(() => { - sinon.restore(); - }); - test("getApplicationPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - pathExistsStub.withArgs(appPyPath).resolves(false); - const file = await fastApiLaunch.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should find path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'main.py'); - pathExistsStub.withArgs(appPyPath).resolves(true); - const file = await fastApiLaunch.getApplicationPath(folder); - - expect(file).to.be.equal('main.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app', '--reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - when(input.showInputBox(anything())).thenResolve('main'); - - await fastApiLaunch.buildFastAPILaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.fastapi.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'uvicorn', - args: ['main:app', '--reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts deleted file mode 100644 index f627c7558c51..000000000000 --- a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildFileLaunchDebugConfiguration } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider File', () => { - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await buildFileLaunchDebugConfiguration( - (undefined as unknown) as MultiStepInput, - state, - ); - - const config = { - name: DebugConfigStrings.file.snippet.name, - type: DebuggerTypeName, - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts deleted file mode 100644 index 08fb5259b282..000000000000 --- a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import * as flaskLaunch from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; - -suite('Debugging - Configuration Provider Flask', () => { - let pathExistsStub: sinon.SinonStub; - let input: MultiStepInput; - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - }); - teardown(() => { - sinon.restore(); - }); - test("getApplicationPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - pathExistsStub.withArgs(appPyPath).resolves(false); - const file = await flaskLaunch.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - pathExistsStub.withArgs(appPyPath).resolves(true); - const file = await flaskLaunch.getApplicationPath(folder); - - expect(file).to.be.equal('app.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - when(input.showInputBox(anything())).thenResolve('hello'); - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'hello', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - when(input.showInputBox(anything())).thenResolve(); - - await flaskLaunch.buildFlaskLaunchDebugConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_DEBUG: '1', - }, - args: ['run', '--no-debugger', '--no-reload'], - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts deleted file mode 100644 index 2508db506ca2..000000000000 --- a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildModuleLaunchConfiguration } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Module', () => { - test('Launch JSON with default module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve(); - - await buildModuleLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve('hello'); - - await buildModuleLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'hello', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts deleted file mode 100644 index 8217e150aa01..000000000000 --- a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { buildPidAttachConfiguration } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider File', () => { - test('Launch JSON with default process id', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await buildPidAttachConfiguration((undefined as unknown) as MultiStepInput, state); - - const config = { - name: DebugConfigStrings.attachPid.snippet.name, - type: DebuggerTypeName, - request: 'attach', - processId: '${command:pickProcess}', - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts deleted file mode 100644 index 688215259a2f..000000000000 --- a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as pyramidLaunch from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; - -suite('Debugging - Configuration Provider Pyramid', () => { - let input: MultiStepInput; - let pathExistsStub: sinon.SinonStub; - let pathSeparatorStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - - setup(() => { - input = mock>(MultiStepInput); - pathExistsStub = sinon.stub(fs, 'pathExists'); - pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); - }); - teardown(() => { - sinon.restore(); - }); - test("getDevelopmentIniPath should return undefined if file doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - pathExistsStub.withArgs(managePyPath).resolves(false); - const file = await pyramidLaunch.getDevelopmentIniPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getDevelopmentIniPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - pathSeparatorStub.value('-'); - pathExistsStub.withArgs(managePyPath).resolves(true); - const file = await pyramidLaunch.getDevelopmentIniPath(folder); - - expect(file).to.be.equal('${workspaceFolder}-development.ini'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test("Validation of path should return errors if resolved path doesn't exist", async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz').resolves(false); - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.txt').resolves(true); - const error = await pyramidLaunch.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should not return errors if resolved path is ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - pathExistsStub.withArgs('xyz.ini').resolves(true); - const error = await pyramidLaunch.validateIniPath(folder, '', 'xyz.ini'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with valid ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: ['${workspaceFolder}-development.ini'], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: ['hello'], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const workspaceFolderToken = '${workspaceFolder}'; - const defaultIni = `${workspaceFolderToken}-development.ini`; - - pathSeparatorStub.value('-'); - when(input.showInputBox(anything())).thenResolve(); - - await pyramidLaunch.buildPyramidLaunchConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name, - type: DebuggerTypeName, - request: 'launch', - module: 'pyramid.scripts.pserve', - args: [defaultIni], - pyramid: true, - jinja: true, - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts deleted file mode 100644 index 323cda94a1eb..000000000000 --- a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import * as configuration from '../../../../../client/debugger/extension/configuration/utils/configuration'; -import * as remoteAttach from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Remote Attach', () => { - let input: MultiStepInput; - - setup(() => { - input = mock>(MultiStepInput); - }); - teardown(() => { - sinon.restore(); - }); - test('Configure port will display prompt', async () => { - when(input.showInputBox(anything())).thenResolve(); - - await configuration.configurePort(instance(input), {}); - - verify(input.showInputBox(anything())).once(); - }); - test('Configure port will default to 5678 if entered value is not a number', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve('xyz'); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 5678 } }); - }); - test('Configure port will default to 5678', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve(); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 5678 } }); - }); - test('Configure port will use user selected value', async () => { - const config: { connect?: { port?: number } } = {}; - when(input.showInputBox(anything())).thenResolve('1234'); - - await configuration.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config).to.be.deep.equal({ connect: { port: 1234 } }); - }); - test('Launch JSON with default host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve(); - - sinon.stub(configuration, 'configurePort').callsFake(async () => { - portConfigured = true; - }); - - const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); - if (configurePort) { - await configurePort!(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: 'localhost', - port: 5678, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); - test('Launch JSON with user defined host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve('Hello'); - sinon.stub(configuration, 'configurePort').callsFake(async (_, cfg) => { - portConfigured = true; - cfg.connect!.port = 9999; - }); - const configurePort = await remoteAttach.buildRemoteAttachConfiguration(instance(input), state); - if (configurePort) { - await configurePort(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name, - type: DebuggerTypeName, - request: 'attach', - connect: { - host: 'Hello', - port: 9999, - }, - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.', - }, - ], - justMyCode: true, - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); -}); diff --git a/src/test/debugger/extension/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts deleted file mode 100644 index 7d2463072f06..000000000000 --- a/src/test/debugger/extension/debugCommands.unit.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../../client/activation/types'; -import { ICommandManager, IDebugService } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import { IDisposableRegistry } from '../../../client/common/types'; -import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; -import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; -import * as telemetry from '../../../client/telemetry'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; - -suite('Debugging - commands', () => { - let commandManager: typemoq.IMock; - let debugService: typemoq.IMock; - let disposables: typemoq.IMock; - let interpreterService: typemoq.IMock; - let debugCommands: IExtensionSingleActivationService; - let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; - - setup(() => { - commandManager = typemoq.Mock.ofType(); - commandManager - .setup((c) => c.executeCommand(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve()); - debugService = typemoq.Mock.ofType(); - disposables = typemoq.Mock.ofType(); - interpreterService = typemoq.Mock.ofType(); - interpreterService - .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); - sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { - /** noop */ - }); - triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( - triggerApis, - 'triggerCreateEnvironmentCheckNonBlocking', - ); - triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); - }); - teardown(() => { - sinon.restore(); - }); - test('Test registering debug file command', async () => { - commandManager - .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) - .returns(() => ({ - dispose: () => { - /* noop */ - }, - })) - .verifiable(typemoq.Times.once()); - - debugCommands = new DebugCommands( - commandManager.object, - debugService.object, - disposables.object, - interpreterService.object, - ); - await debugCommands.activate(); - commandManager.verifyAll(); - }); - test('Test running debug file command', async () => { - let callback: (f: Uri) => Promise = (_f: Uri) => Promise.resolve(); - commandManager - .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) - .callback((_name, cb) => { - callback = cb; - }); - debugService - .setup((d) => d.startDebugging(undefined, typemoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - - debugCommands = new DebugCommands( - commandManager.object, - debugService.object, - disposables.object, - interpreterService.object, - ); - await debugCommands.activate(); - - await callback(Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'test.py'))); - commandManager.verifyAll(); - debugService.verifyAll(); - }); -}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts deleted file mode 100644 index b1053def2eba..000000000000 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import { ChildProcessAttachEventHandler } from '../../../../client/debugger/extension/hooks/childProcessAttachHandler'; -import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; -import { DebuggerEvents } from '../../../../client/debugger/extension/hooks/constants'; -import { AttachRequestArguments } from '../../../../client/debugger/types'; -import { DebuggerTypeName } from '../../../../client/debugger/constants'; - -suite('Debug - Child Process', () => { - test('Do not attach if the event is undefined', async () => { - const attachService = mock(ChildProcessAttachService); - const handler = new ChildProcessAttachEventHandler(instance(attachService)); - await handler.handleCustomEvent(undefined as any); - verify(attachService.attach(anything(), anything())).never(); - }); - test('Do not attach to child process if event is invalid', async () => { - const attachService = mock(ChildProcessAttachService); - const handler = new ChildProcessAttachEventHandler(instance(attachService)); - const body: any = {}; - const session: any = { configuration: { type: DebuggerTypeName } }; - await handler.handleCustomEvent({ event: 'abc', body, session }); - verify(attachService.attach(body, session)).never(); - }); - test('Do not attach to child process if debugger type is different', async () => { - const attachService = mock(ChildProcessAttachService); - const handler = new ChildProcessAttachEventHandler(instance(attachService)); - const body: any = {}; - const session: any = { configuration: { type: 'other-type' } }; - await handler.handleCustomEvent({ event: 'abc', body, session }); - verify(attachService.attach(body, session)).never(); - }); - test('Do not attach to child process if ptvsd_attach event is invalid', async () => { - const attachService = mock(ChildProcessAttachService); - const handler = new ChildProcessAttachEventHandler(instance(attachService)); - const body: any = {}; - const session: any = { configuration: { type: DebuggerTypeName } }; - await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); - verify(attachService.attach(body, session)).never(); - }); - test('Do not attach to child process if debugpy_attach event is invalid', async () => { - const attachService = mock(ChildProcessAttachService); - const handler = new ChildProcessAttachEventHandler(instance(attachService)); - const body: any = {}; - const session: any = { configuration: { type: DebuggerTypeName } }; - await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); - verify(attachService.attach(body, session)).never(); - }); - test('Exceptions are not bubbled up if exceptions are thrown', async () => { - const attachService = mock(ChildProcessAttachService); - const handler = new ChildProcessAttachEventHandler(instance(attachService)); - const body: AttachRequestArguments = { - name: 'Attach', - type: 'python', - request: 'attach', - port: 1234, - subProcessId: 2, - }; - const session: any = { - configuration: { type: DebuggerTypeName }, - }; - when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); - await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); - verify(attachService.attach(body, anything())).once(); - const [, secondArg] = capture(attachService.attach).last(); - expect(secondArg).to.deep.equal(session); - }); -}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts deleted file mode 100644 index 118efe416e94..000000000000 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { DebugService } from '../../../../client/common/application/debugService'; -import { IDebugService } from '../../../../client/common/application/types'; -import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; -import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; -import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; - -suite('Debug - Attach to Child Process', () => { - let debugService: IDebugService; - let attachService: ChildProcessAttachService; - let getWorkspaceFoldersStub: sinon.SinonStub; - let showErrorMessageStub: sinon.SinonStub; - - setup(() => { - debugService = mock(DebugService); - attachService = new ChildProcessAttachService(instance(debugService)); - getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); - showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); - }); - teardown(() => { - sinon.restore(); - }); - - test('Message is not displayed if debugger is launched', async () => { - const data: AttachRequestArguments = { - name: 'Attach', - type: 'python', - request: 'attach', - port: 1234, - subProcessId: 2, - }; - const session: any = {}; - getWorkspaceFoldersStub.returns(undefined); - when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); - showErrorMessageStub.returns(undefined); - - await attachService.attach(data, session); - - sinon.assert.calledOnce(getWorkspaceFoldersStub); - verify(debugService.startDebugging(anything(), anything(), anything())).once(); - sinon.assert.notCalled(showErrorMessageStub); - }); - test('Message is displayed if debugger is not launched', async () => { - const data: AttachRequestArguments = { - name: 'Attach', - type: 'python', - request: 'attach', - port: 1234, - subProcessId: 2, - }; - - const session: any = {}; - getWorkspaceFoldersStub.returns(undefined); - when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); - showErrorMessageStub.resolves(() => {}); - - await attachService.attach(data, session); - - sinon.assert.calledOnce(getWorkspaceFoldersStub); - verify(debugService.startDebugging(anything(), anything(), anything())).once(); - sinon.assert.calledOnce(showErrorMessageStub); - }); - test('Use correct workspace folder', async () => { - const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; - const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; - const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; - - const data: AttachRequestArguments = { - name: 'Attach', - type: 'python', - request: 'attach', - port: 1234, - subProcessId: 2, - workspaceFolder: rightWorkspaceFolder.uri.fsPath, - }; - - const session: any = {}; - getWorkspaceFoldersStub.returns([wkspace1, rightWorkspaceFolder, wkspace2]); - when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); - - await attachService.attach(data, session); - - sinon.assert.called(getWorkspaceFoldersStub); - verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); - sinon.assert.notCalled(showErrorMessageStub); - }); - test('Use empty workspace folder if right one is not found', async () => { - const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; - const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; - const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; - - const data: AttachRequestArguments = { - name: 'Attach', - type: 'python', - request: 'attach', - port: 1234, - subProcessId: 2, - workspaceFolder: rightWorkspaceFolder.uri.fsPath, - }; - - const session: any = {}; - getWorkspaceFoldersStub.returns([wkspace1, wkspace2]); - when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - - await attachService.attach(data, session); - - sinon.assert.called(getWorkspaceFoldersStub); - verify(debugService.startDebugging(undefined, anything(), anything())).once(); - sinon.assert.notCalled(showErrorMessageStub); - }); - test('Validate debug config is passed with the correct params', async () => { - const data: LaunchRequestArguments | AttachRequestArguments = { - request: 'attach', - type: 'python', - name: 'Attach', - port: 1234, - subProcessId: 2, - host: 'localhost', - }; - - const debugConfig = JSON.parse(JSON.stringify(data)); - debugConfig.host = 'localhost'; - const session: any = {}; - - getWorkspaceFoldersStub.returns(undefined); - when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - - await attachService.attach(data, session); - - sinon.assert.calledOnce(getWorkspaceFoldersStub); - verify(debugService.startDebugging(undefined, anything(), anything())).once(); - const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); - expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); - sinon.assert.notCalled(showErrorMessageStub); - }); - test('Pass data as is if data is attach debug configuration', async () => { - const data: AttachRequestArguments = { - type: 'python', - request: 'attach', - name: '', - }; - const session: any = {}; - const debugConfig = JSON.parse(JSON.stringify(data)); - - getWorkspaceFoldersStub.returns(undefined); - when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - - await attachService.attach(data, session); - - sinon.assert.calledOnce(getWorkspaceFoldersStub); - verify(debugService.startDebugging(undefined, anything(), anything())).once(); - const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); - expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); - sinon.assert.notCalled(showErrorMessageStub); - }); - test('Validate debug config when parent/root parent was attached', async () => { - const data: AttachRequestArguments = { - request: 'attach', - type: 'python', - name: 'Attach', - host: '123.123.123.123', - port: 1234, - subProcessId: 2, - }; - - const debugConfig = JSON.parse(JSON.stringify(data)); - debugConfig.host = data.host; - debugConfig.port = data.port; - debugConfig.request = 'attach'; - const session: any = {}; - - getWorkspaceFoldersStub.returns(undefined); - when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - - await attachService.attach(data, session); - - sinon.assert.calledOnce(getWorkspaceFoldersStub); - verify(debugService.startDebugging(undefined, anything(), anything())).once(); - const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); - expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); - sinon.assert.notCalled(showErrorMessageStub); - }); -}); From f3fde6acfa20ce8d1c8cbcd5458fa7e808abba2d Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 12:27:30 -0500 Subject: [PATCH 03/24] Fix merge --- src/client/formatters/baseFormatter.ts | 148 ------------------ src/client/formatters/blackFormatter.ts | 53 ------- .../linters/errorHandlers/notInstalled.ts | 33 ---- src/client/linters/linterCommands.ts | 109 ------------- src/client/providers/formatProvider.ts | 126 --------------- 5 files changed, 469 deletions(-) delete mode 100644 src/client/formatters/baseFormatter.ts delete mode 100644 src/client/formatters/blackFormatter.ts delete mode 100644 src/client/linters/errorHandlers/notInstalled.ts delete mode 100644 src/client/linters/linterCommands.ts delete mode 100644 src/client/providers/formatProvider.ts diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts deleted file mode 100644 index bde2c6465838..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,148 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { traceError } from '../common/logger'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../common/types'; -import { isNotebookCell } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; -import { IFormatterHelper } from './types'; - -export abstract class BaseFormatter { - protected readonly outputChannel: vscode.OutputChannel; - protected readonly workspace: IWorkspaceService; - private readonly helper: IFormatterHelper; - - constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { - this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.helper = serviceContainer.get(IFormatterHelper); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public abstract formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable; - protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { - if (path.basename(document.uri.fsPath) === document.uri.fsPath) { - return fallbackPath; - } - return path.dirname(document.fileName); - } - protected getWorkspaceUri(document: vscode.TextDocument) { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const folders = this.workspace.workspaceFolders; - if (Array.isArray(folders) && folders.length > 0) { - return folders[0].uri; - } - return vscode.Uri.file(__dirname); - } - protected async provideDocumentFormattingEdits( - document: vscode.TextDocument, - _options: vscode.FormattingOptions, - token: vscode.CancellationToken, - args: string[], - cwd?: string, - ): Promise { - if (typeof cwd !== 'string' || cwd.length === 0) { - cwd = this.getWorkspaceUri(document).fsPath; - } - - // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. - // However they don't support returning the diff of the formatted text when reading data from the input stream. - // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. - // Also, always create temp files for Notebook cells. - const tempFile = await this.createTempFile(document); - if (this.checkCancellation(document.fileName, tempFile, token)) { - return []; - } - - const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); - executionInfo.args.push(tempFile); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - const promise = pythonToolsExecutionService - .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) - .then((output) => output.stdout) - .then((data) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - return getTextEditsFromPatch(document.getText(), data); - }) - .catch((error) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - - this.handleError(this.Id, error, document.uri).catch(() => {}); - return [] as vscode.TextEdit[]; - }) - .then((edits) => { - this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); - return edits; - }); - - const appShell = this.serviceContainer.get(IApplicationShell); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - disposableRegistry.push(disposable); - return promise; - } - - protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - let customError = `Formatting with ${this.Id} failed.`; - - if (isNotInstalledError(error)) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled) { - customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('Python Extension: promptToInstall', ex)); - } - } - - this.outputChannel.appendLine(`\n${customError}\n${error}`); - } - - /** - * Always create a temporary file when formatting notebook cells. - * This is because there is no physical file associated with notebook cells (they are all virtual). - */ - private async createTempFile(document: vscode.TextDocument): Promise { - const fs = this.serviceContainer.get(IFileSystem); - return document.isDirty || isNotebookCell(document) - ? getTempFileWithDocumentContents(document, fs) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise { - if (originalFile !== tempFile) { - const fs = this.serviceContainer.get(IFileSystem); - return fs.deleteFile(tempFile); - } - return Promise.resolve(); - } - - private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { - if (token && token.isCancellationRequested) { - this.deleteTempFile(originalFile, tempFile).ignoreErrors(); - return true; - } - return false; - } -} diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts deleted file mode 100644 index ddfef8fc57ca..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public async formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Promise { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - if (formatSelection) { - const shell = this.serviceContainer.get(IApplicationShell); - // Black does not support partial formatting on purpose. - shell.showErrorMessage('Black does not support the "Format Selection" command').then(noop, noop); - return []; - } - - const blackArgs = ['--diff', '--quiet']; - - if (path.extname(document.fileName) === '.pyi') { - blackArgs.push('--pyi'); - } - - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 16871e7ee71f..000000000000 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { OutputChannel, Uri } from 'vscode'; -import { traceError, traceWarning } from '../../common/logger'; -import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { ILinterManager } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class NotInstalledErrorHandler extends BaseErrorHandler { - constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(product, outputChannel, serviceContainer); - } - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer - .get(IPythonExecutionFactory) - .create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - this.outputChannel.appendLine(`\n${customError}\n${error}`); - traceWarning(customError, error); - return true; - } -} diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts deleted file mode 100644 index ad3decdcef63..000000000000 --- a/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { DiagnosticCollection, Disposable, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IDisposable } from '../common/types'; -import { Linters } from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { ILinterManager, ILintingEngine, LinterId } from './types'; - -export class LinterCommands implements IDisposable { - private disposables: Disposable[] = []; - private linterManager: ILinterManager; - private readonly appShell: IApplicationShell; - private readonly documentManager: IDocumentManager; - - constructor(private serviceContainer: IServiceContainer) { - this.linterManager = this.serviceContainer.get(ILinterManager); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.documentManager = this.serviceContainer.get(IDocumentManager); - - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); - commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); - commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); - } - public dispose() { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - public async setLinterAsync(): Promise { - const linters = this.linterManager.getAllLinterInfos(); - const suggestions = linters.map((x) => x.id).sort(); - const linterList = ['Disable Linting', ...suggestions]; - const activeLinters = await this.linterManager.getActiveLinters(this.settingsUri); - - let current: string; - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); - if (selection !== undefined) { - if (selection === 'Disable Linting') { - await this.linterManager.enableLintingAsync(false); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); - } else { - const index = linters.findIndex((x) => x.id === selection); - if (activeLinters.length > 1) { - const response = await this.appShell.showWarningMessage( - Linters.replaceWithSelectedLinter().format(selection), - 'Yes', - 'No', - ); - if (response !== 'Yes') { - return; - } - } - await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); - } - } - } - - public async enableLintingAsync(): Promise { - const options = ['Enable', 'Disable']; - const current = (await this.linterManager.isLintingEnabled(this.settingsUri)) ? options[0] : options[1]; - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(options, quickPickOptions); - - if (selection !== undefined) { - const enable: boolean = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles(); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts deleted file mode 100644 index 0c9155ce2b3a..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_LANGUAGE } from '../common/constants'; -import { IConfigurationService } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; -import { BaseFormatter } from './../formatters/baseFormatter'; -import { BlackFormatter } from './../formatters/blackFormatter'; -import { DummyFormatter } from './../formatters/dummyFormatter'; -import { YapfFormatter } from './../formatters/yapfFormatter'; - -export class PythonFormattingEditProvider - implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { - private readonly config: IConfigurationService; - private readonly workspace: IWorkspaceService; - private readonly documentManager: IDocumentManager; - private readonly commands: ICommandManager; - private formatters = new Map(); - private disposables: vscode.Disposable[] = []; - - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - private documentVersionBeforeFormatting = -1; - private formatterMadeChanges = false; - private saving = false; - - public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { - const yapfFormatter = new YapfFormatter(serviceContainer); - const autoPep8 = new AutoPep8Formatter(serviceContainer); - const black = new BlackFormatter(serviceContainer); - const dummy = new DummyFormatter(serviceContainer); - this.formatters.set(yapfFormatter.Id, yapfFormatter); - this.formatters.set(black.Id, black); - this.formatters.set(autoPep8.Id, autoPep8); - this.formatters.set(dummy.Id, dummy); - - this.commands = serviceContainer.get(ICommandManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); - this.config = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - this.disposables.push( - this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)), - ); - this.disposables.push( - interpreterService.onDidChangeInterpreter(async () => { - if (this.documentManager.activeTextEditor) { - return this.onSaveDocument(this.documentManager.activeTextEditor.document); - } - }), - ); - } - - public dispose() { - this.disposables.forEach((d) => d.dispose()); - } - - public provideDocumentFormattingEdits( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits( - document: vscode.TextDocument, - range: vscode.Range | undefined, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. - // Workaround is to resolve promise to nothing here, then execute format document and force new save. - // However, we need to know if this is 'format document' or formatting on save. - - if (this.saving || document.languageId !== PYTHON_LANGUAGE) { - // We are saving after formatting (see onSaveDocument below) - // so we do not want to format again. - return []; - } - - // Remember content before formatting so we can detect if - // formatting edits have been really applied - const editorConfig = this.workspace.getConfiguration('editor', document.uri); - if (editorConfig.get('formatOnSave') === true) { - this.documentVersionBeforeFormatting = document.version; - } - - const settings = this.config.getSettings(document.uri); - const formatter = this.formatters.get(settings.formatting.provider)!; - const edits = await formatter.formatDocument(document, options, token, range); - - this.formatterMadeChanges = edits.length > 0; - return edits; - } - - private async onSaveDocument(document: vscode.TextDocument): Promise { - // Promise was rejected = formatting took too long. - // Don't format inside the event handler, do it on timeout - setTimeout(() => { - try { - if ( - this.formatterMadeChanges && - !document.isDirty && - document.version === this.documentVersionBeforeFormatting - ) { - // Formatter changes were not actually applied due to the timeout on save. - // Force formatting now and then save the document. - this.commands.executeCommand('editor.action.formatDocument').then(async () => { - this.saving = true; - await document.save(); - this.saving = false; - }); - } - } finally { - this.documentVersionBeforeFormatting = -1; - this.saving = false; - this.formatterMadeChanges = false; - } - }, 50); - } -} From 4d8f01fb667a425962830e21e36767f05e4e2faa Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 13:30:08 -0500 Subject: [PATCH 04/24] Fix lint --- .../checks/invalidLaunchJsonDebugger.ts | 206 -------- .../diagnostics/serviceRegistry.ts | 18 - .../invalidLaunchJsonDebugger.unit.test.ts | 462 ------------------ .../invalidPythonPathInDebugger.unit.test.ts | 416 ---------------- .../checks/pythonInterpreter.unit.test.ts | 34 -- .../diagnostics/serviceRegistry.unit.test.ts | 10 +- .../debugConfigurationService.unit.test.ts | 181 ------- .../extension/serviceRegistry.unit.test.ts | 43 -- 8 files changed, 5 insertions(+), 1365 deletions(-) delete mode 100644 src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts delete mode 100644 src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts delete mode 100644 src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts delete mode 100644 src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts delete mode 100644 src/test/debugger/extension/serviceRegistry.unit.test.ts diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts deleted file mode 100644 index 440ff16856d3..000000000000 --- a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// eslint-disable-next-line max-classes-per-file -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import '../../../common/extensions'; -import { IFileSystem } from '../../../common/platform/types'; -import { IDisposableRegistry, Resource } from '../../../common/types'; -import { Common, Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -const messages = { - [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic, - [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic, - [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic, - [DiagnosticCodes.ConfigPythonPathDiagnostic]: '', -}; - -export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { - constructor( - code: - | DiagnosticCodes.InvalidDebuggerTypeDiagnostic - | DiagnosticCodes.JustMyCodeDiagnostic - | DiagnosticCodes.ConsoleTypeDiagnostic - | DiagnosticCodes.ConfigPythonPathDiagnostic, - resource: Resource, - shouldShowPrompt = true, - ) { - super( - code, - messages[code], - DiagnosticSeverity.Error, - DiagnosticScope.WorkspaceFolder, - resource, - shouldShowPrompt, - ); - } -} - -export const InvalidLaunchJsonDebuggerServiceId = 'InvalidLaunchJsonDebuggerServiceId'; - -@injectable() -export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - private readonly messageService: IDiagnosticHandlerService, - ) { - super( - [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - DiagnosticCodes.ConfigPythonPathDiagnostic, - ], - serviceContainer, - disposableRegistry, - true, - ); - } - - public async diagnose(resource: Resource): Promise { - const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; - if (!hasWorkspaceFolders) { - return []; - } - const workspaceFolder = resource - ? this.workspaceService.getWorkspaceFolder(resource)! - : this.workspaceService.workspaceFolders![0]; - return this.diagnoseWorkspace(workspaceFolder, resource); - } - - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); - } - - protected async fixLaunchJson(code: DiagnosticCodes): Promise { - const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; - if (!hasWorkspaceFolders) { - return; - } - - await Promise.all( - (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => - this.fixLaunchJsonInWorkspace(code, workspaceFolder), - ), - ); - } - - private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { - const launchJson = getLaunchJsonFile(workspaceFolder); - if (!(await this.fs.fileExists(launchJson))) { - return []; - } - - const fileContents = await this.fs.readFile(launchJson); - const diagnostics: IDiagnostic[] = []; - if (fileContents.indexOf('"pythonExperimental"') > 0) { - diagnostics.push( - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), - ); - } - if (fileContents.indexOf('"debugStdLib"') > 0) { - diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, resource)); - } - if (fileContents.indexOf('"console": "none"') > 0) { - diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource)); - } - if ( - fileContents.indexOf('"pythonPath":') > 0 || - fileContents.indexOf('{config:python.pythonPath}') > 0 || - fileContents.indexOf('{config:python.interpreterPath}') > 0 - ) { - diagnostics.push( - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false), - ); - } - return diagnostics; - } - - private async handleDiagnostic(diagnostic: IDiagnostic): Promise { - if (!diagnostic.shouldShowPrompt) { - await this.fixLaunchJson(diagnostic.code); - return; - } - const commandPrompts = [ - { - prompt: Diagnostics.yesUpdateLaunch, - command: { - diagnostic, - invoke: async (): Promise => { - await this.fixLaunchJson(diagnostic.code); - }, - }, - }, - { - prompt: Common.noIWillDoItLater, - }, - ]; - - await this.messageService.handle(diagnostic, { commandPrompts }); - } - - private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { - if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { - return; - } - const launchJson = getLaunchJsonFile(workspaceFolder); - let fileContents = await this.fs.readFile(launchJson); - switch (code) { - case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { - fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); - fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); - break; - } - case DiagnosticCodes.JustMyCodeDiagnostic: { - fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); - fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); - break; - } - case DiagnosticCodes.ConsoleTypeDiagnostic: { - fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); - break; - } - case DiagnosticCodes.ConfigPythonPathDiagnostic: { - fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); - fileContents = findAndReplace( - fileContents, - '{config:python.pythonPath}', - '{command:python.interpreterPath}', - ); - fileContents = findAndReplace( - fileContents, - '{config:python.interpreterPath}', - '{command:python.interpreterPath}', - ); - break; - } - default: { - return; - } - } - - await this.fs.writeFile(launchJson, fileContents); - } -} - -function findAndReplace(fileContents: string, search: string, replace: string) { - const searchRegex = new RegExp(search, 'g'); - return fileContents.replace(searchRegex, replace); -} - -function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); -} diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index 8d9b765939c9..bfffb809ef55 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -11,14 +11,6 @@ import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, } from './checks/envPathVariable'; -import { - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, -} from './checks/invalidLaunchJsonDebugger'; -import { - InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId, -} from './checks/invalidPythonPathInDebugger'; import { JediPython27NotSupportedDiagnosticService, JediPython27NotSupportedDiagnosticServiceId, @@ -59,11 +51,6 @@ export function registerTypes(serviceManager: IServiceManager): void { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, ); - serviceManager.addSingleton( - IDiagnosticsService, - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, - ); serviceManager.addSingleton( IDiagnosticsService, InvalidPythonInterpreterService, @@ -73,11 +60,6 @@ export function registerTypes(serviceManager: IServiceManager): void { IExtensionSingleActivationService, InvalidPythonInterpreterService, ); - serviceManager.addSingleton( - IDiagnosticsService, - InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId, - ); serviceManager.addSingleton( IDiagnosticsService, PowerShellActivationHackDiagnosticsService, diff --git a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts deleted file mode 100644 index d4eefd69dd5f..000000000000 --- a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { - InvalidLaunchJsonDebuggerDiagnostic, - InvalidLaunchJsonDebuggerService, -} from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; -import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticHandlerService, - IDiagnosticsService, -} from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IFileSystem } from '../../../../client/common/platform/types'; -import { Diagnostics } from '../../../../client/common/utils/localize'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks if launch.json is invalid', () => { - let serviceContainer: TypeMoq.IMock; - let diagnosticService: IDiagnosticsService; - let commandFactory: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let baseWorkspaceService: TypeMoq.IMock; - let messageHandler: TypeMoq.IMock>; - let workspaceFolder: WorkspaceFolder; - - setup(() => { - workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - serviceContainer = TypeMoq.Mock.ofType(); - commandFactory = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - messageHandler = TypeMoq.Mock.ofType>(); - workspaceService = TypeMoq.Mock.ofType(); - baseWorkspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => baseWorkspaceService.object); - - diagnosticService = new (class extends InvalidLaunchJsonDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - public async fixLaunchJson(code: DiagnosticCodes) { - await super.fixLaunchJson(code); - } - })(serviceContainer.object, fs.object, [], workspaceService.object, messageHandler.object); - (diagnosticService as any)._clear(); - }); - - test('Can handle all InvalidLaunchJsonDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - - test('Can not handle non-InvalidLaunchJsonDebugger diagnostics', async () => { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - - test('Should return empty diagnostics if there are no workspace folders', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not exist', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup((w) => w.getWorkspaceFolder(undefined)) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not contain strings "pythonExperimental" and "debugStdLib" ', async () => { - const fileContents = 'Hello I am launch.json, although I am not very jsony'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return InvalidDebuggerTypeDiagnostic if file launch.json contains string "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "pythonExperimental"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return JustMyCodeDiagnostic if file launch.json contains string "debugStdLib"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "debugStdLib"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.pythonPath}"', async () => { - const fileContents = 'Hello I am launch.json, I contain string {config:python.pythonPath}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.interpreterPath}"', async () => { - const fileContents = 'Hello I am launch.json, I contain string {config:python.interpreterPath}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return both diagnostics if file launch.json contains string "debugStdLib" and "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain both "debugStdLib" and "pythonExperimental"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal( - [ - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined), - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined), - ], - 'Diagnostics returned are not as expected', - ); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `true` should display a prompt with 2 buttons where clicking the first button will invoke a command', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.shouldShowPrompt) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(TypeMoq.Times.atLeastOnce()); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(2); - expect(options!.commandPrompts[0].prompt).to.be.equal(Diagnostics.yesUpdateLaunch); - expect(options!.commandPrompts[0].command).not.to.be.equal(undefined, 'Command not set'); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `false` should directly fix launch.json', async () => { - for (const code of [DiagnosticCodes.ConfigPythonPathDiagnostic]) { - let called = false; - (diagnosticService as any).fixLaunchJson = () => { - called = true; - }; - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.shouldShowPrompt) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.never()); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(called).to.equal(true, ''); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics should display message twice if invoked twice', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]) { - const diagnostic = TypeMoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'always') - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler.reset(); - messageHandler - .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.exactly(2)); - baseWorkspaceService - .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.never()); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - } - }); - - const codes = [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - ]; - - codes.forEach((code) => { - test('Function fixLaunchJson() returns if there are no workspace folders', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - }); - - test('Function fixLaunchJson() returns if file launch.json does not exist', async () => { - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve('')) - .verifiable(TypeMoq.Times.never()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - }); - - test('File launch.json is fixed correctly when code equals JustMyCodeDiagnostic', async () => { - const launchJson = '{"debugStdLib": true, "debugStdLib": false}'; - const correctedlaunchJson = '{"justMyCode": false, "justMyCode": true}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.JustMyCodeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals InvalidDebuggerTypeDiagnostic', async () => { - const launchJson = '{"Python Experimental: task" "pythonExperimental"}'; - const correctedlaunchJson = '{"Python: task" "python"}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.InvalidDebuggerTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConsoleTypeDiagnostic', async () => { - const launchJson = '{"console": "none"}'; - const correctedlaunchJson = '{"console": "internalConsole"}'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConsoleTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConfigPythonPathDiagnostic', async () => { - const launchJson = '"pythonPath": "{config:python.pythonPath}{config:python.interpreterPath}"'; - const correctedlaunchJson = '"python": "{command:python.interpreterPath}{command:python.interpreterPath}"'; - workspaceService - .setup((w) => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup((w) => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConfigPythonPathDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); -}); diff --git a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts deleted file mode 100644 index b8115322ccd7..000000000000 --- a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { - DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt, -} from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticCommand, - IDiagnosticHandlerService, - IInvalidPythonPathInDebuggerService, -} from '../../../../client/application/diagnostics/types'; -import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; -import { IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; -import { PythonPathSource } from '../../../../client/debugger/extension/types'; -import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks Python Path in debugger', () => { - let diagnosticService: IInvalidPythonPathInDebuggerService; - let messageHandler: typemoq.IMock>; - let commandFactory: typemoq.IMock; - let configService: typemoq.IMock; - let helper: typemoq.IMock; - let workspaceService: typemoq.IMock; - let docMgr: typemoq.IMock; - setup(() => { - const serviceContainer = typemoq.Mock.ofType(); - messageHandler = typemoq.Mock.ofType>(); - serviceContainer - .setup((s) => - s.get( - typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), - ), - ) - .returns(() => messageHandler.object); - commandFactory = typemoq.Mock.ofType(); - docMgr = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) - .returns(() => commandFactory.object); - configService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - helper = typemoq.Mock.ofType(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - workspaceService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - - diagnosticService = new (class extends InvalidPythonPathInDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - })( - serviceContainer.object, - workspaceService.object, - commandFactory.object, - helper.object, - docMgr.object, - configService.object, - [], - messageHandler.object, - ); - (diagnosticService as any)._clear(); - }); - - test('Can handle InvalidPythonPathInDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, - ]) { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => code) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - test('Can not handle non-InvalidPythonPathInDebugger diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Should return empty diagnostics', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.once()); - messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message once if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'default') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(1)); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(1)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message twice if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup((d) => d.invokeHandler) - .returns(() => 'always') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType(); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(2)); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(2)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerLaunch diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(1); - expect(options!.commandPrompts[0].prompt).to.be.equal('Open launch.json'); - }); - test('Ensure we get python path from config when path = ${command:python.interpreterPath}', async () => { - const pythonPath = '${command:python.interpreterPath}'; - - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${workspaceFolder} is not expanded when a resource is not passed', async () => { - const pythonPath = '${workspaceFolder}/venv/bin/python'; - - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.never()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isAny())) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - }); - test('Ensure ${workspaceFolder} is expanded', async () => { - const pythonPath = '${workspaceFolder}/venv/bin/python'; - - const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - const expectedPath = `${workspaceFolder.uri.fsPath}/venv/bin/python`; - - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath( - pythonPath, - PythonPathSource.settingsJson, - Uri.parse('something'), - ); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${env:XYZ123} is expanded', async () => { - const pythonPath = '${env:XYZ123}/venv/bin/python'; - - process.env.XYZ123 = 'something/else'; - const expectedPath = `${process.env.XYZ123}/venv/bin/python`; - workspaceService - .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we get python path from config when path = undefined', async () => { - const pythonPath = undefined; - - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we do not get python path from config when path is provided', async () => { - const pythonPath = path.join('a', 'b'); - - const settings = typemoq.Mock.ofType(); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure InvalidPythonPathInDebuggerLaunch diagnostic is handled when path is invalid in launch.json', async () => { - const pythonPath = path.join('a', 'b'); - const settings = typemoq.Mock.ofType(); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - let handleInvoked = false; - diagnosticService.handle = (diagnostics) => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.launchJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); - test('Ensure InvalidPythonPathInDebuggerSettings diagnostic is handled when path is invalid in settings.json', async () => { - const pythonPath = undefined; - const settings = typemoq.Mock.ofType(); - settings - .setup((s) => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup((c) => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - let handleInvoked = false; - diagnosticService.handle = (diagnostics) => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.settingsJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); -}); diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index 2397743274c1..2eecf052e433 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -7,7 +7,6 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { EventEmitter, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidLaunchJsonDebuggerDiagnostic } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; import { DefaultShellDiagnostic, InvalidPythonInterpreterDiagnostic, @@ -586,39 +585,6 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { await diagnosticServiceMock.object.handle([diagnostic]); - messageHandler.verifyAll(); - commandFactory.verifyAll(); - }); - test('Getting command prompts for an unsupported diagnostic code should throw an error', async () => { - const diagnostic = new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined); - const cmd = ({} as any) as IDiagnosticCommand; - - messageHandler - .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.never()); - commandFactory - .setup((f) => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith>({ - type: 'executeVSCCommand', - }), - ), - ) - .returns(() => cmd) - .verifiable(typemoq.Times.never()); - - try { - await diagnosticService.handle([diagnostic]); - } catch (err) { - expect((err as Error).message).to.be.equal( - "Invalid diagnostic for 'InvalidPythonInterpreterService'", - 'Error message is different', - ); - } - messageHandler.verifyAll(); commandFactory.verifyAll(); }); diff --git a/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/src/test/application/diagnostics/serviceRegistry.unit.test.ts index dcff47b2b7e7..ecc2ae1c5c83 100644 --- a/src/test/application/diagnostics/serviceRegistry.unit.test.ts +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -11,9 +11,9 @@ import { EnvironmentPathVariableDiagnosticsServiceId, } from '../../../client/application/diagnostics/checks/envPathVariable'; import { - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, -} from '../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, +} from '../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; import { JediPython27NotSupportedDiagnosticService, JediPython27NotSupportedDiagnosticServiceId, @@ -80,8 +80,8 @@ suite('Application Diagnostics - Register classes in IOC Container', () => { verify( serviceManager.addSingleton( IDiagnosticsService, - InvalidLaunchJsonDebuggerService, - InvalidLaunchJsonDebuggerServiceId, + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, ), ); verify( diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts deleted file mode 100644 index 7c7977ab8480..000000000000 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as typemoq from 'typemoq'; -import { DebugConfiguration, Uri } from 'vscode'; -import { IMultiStepInputFactory, MultiStepInput } from '../../../../client/common/utils/multiStepInput'; -import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; -import { DebugConfigurationState } from '../../../../client/debugger/extension/types'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; - -suite('Debugging - Configuration Service', () => { - let attachResolver: typemoq.IMock>; - let launchResolver: typemoq.IMock>; - let configService: TestPythonDebugConfigurationService; - let multiStepFactory: typemoq.IMock; - - class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { - public static async pickDebugConfiguration( - input: MultiStepInput, - state: DebugConfigurationState, - ) { - return PythonDebugConfigurationService.pickDebugConfiguration(input, state); - } - } - setup(() => { - attachResolver = typemoq.Mock.ofType>(); - launchResolver = typemoq.Mock.ofType>(); - multiStepFactory = typemoq.Mock.ofType(); - - configService = new TestPythonDebugConfigurationService( - attachResolver.object, - launchResolver.object, - multiStepFactory.object, - ); - }); - test('Should use attach resolver when passing attach config', async () => { - const config = ({ - request: 'attach', - } as DebugConfiguration) 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 unknown) as AttachRequestArguments)) - .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 configService.resolveDebugConfiguration(folder, config as DebugConfiguration); - - 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 DebugConfiguration) as LaunchRequestArguments), - typemoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve((expectedConfig as unknown) as LaunchRequestArguments)) - .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 configService.resolveDebugConfiguration(folder, config as DebugConfiguration); - - expect(resolvedConfig).to.deep.equal(expectedConfig); - attachResolver.verifyAll(); - launchResolver.verifyAll(); - }); - }); - test('Picker should be displayed', async () => { - const state = ({ configs: [], folder: {}, token: undefined } as unknown) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - }); - test('Existing Configuration items must be removed before displaying picker', async () => { - const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as unknown) as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - expect(Object.keys(state.config)).to.be.lengthOf(0); - }); - test('Ensure generated config is returned', async () => { - const expectedConfig = { yes: 'Updated' }; - const multiStepInput = { - run: (_: unknown, state: DebugConfiguration) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }, - }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); - TestPythonDebugConfigurationService.pickDebugConfiguration = (_, state) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }; - const config = await configService.provideDebugConfigurations!(({} as unknown) as undefined); - - multiStepFactory.verifyAll(); - expect(config).to.deep.equal([expectedConfig]); - }); - test('Ensure `undefined` is returned if QuickPick is cancelled', async () => { - const multiStepInput = { - run: (_: unknown, _state: DebugConfiguration) => Promise.resolve(), - }; - const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); - const config = await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - - multiStepFactory.verifyAll(); - - expect(config).to.equal(undefined, `Config should be undefined`); - }); - test('Use cached debug configuration', async () => { - const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; - const expectedConfig = { - name: 'File', - type: 'python', - request: 'launch', - program: '${file}', - console: 'integratedTerminal', - }; - const multiStepInput = { - run: (_: unknown, state: DebugConfiguration) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }, - }; - multiStepFactory - .setup((f) => f.create()) - .returns(() => multiStepInput as MultiStepInput) - .verifiable(typemoq.Times.once()); // this should be called only once. - - launchResolver - .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(expectedConfig as LaunchRequestArguments)) - .verifiable(typemoq.Times.exactly(2)); // this should be called twice with the same config. - - await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); - - multiStepFactory.verifyAll(); - launchResolver.verifyAll(); - }); -}); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts deleted file mode 100644 index be73a0965e4b..000000000000 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { IExtensionSingleActivationService } from '../../../client/activation/types'; -import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; -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 { ServiceManager } from '../../../client/ioc/serviceManager'; -import { IServiceManager } from '../../../client/ioc/types'; - -suite('Debugging - Service Registry', () => { - let serviceManager: IServiceManager; - - setup(() => { - serviceManager = mock(ServiceManager); - }); - test('Registrations', () => { - registerTypes(instance(serviceManager)); - verify( - serviceManager.addSingleton( - IChildProcessAttachService, - ChildProcessAttachService, - ), - ).once(); - verify( - serviceManager.addSingleton( - IDebugSessionEventHandlers, - ChildProcessAttachEventHandler, - ), - ).once(); - verify( - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugCommands, - ), - ).once(); - }); -}); From 600246aee5f4fee9025c61921124a39313621f55 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 15:41:08 -0500 Subject: [PATCH 05/24] Fix Service Registry --- .../diagnostics/serviceRegistry.ts | 9 +++ .../debugger/common/protocolparser.test.ts | 70 ------------------- 2 files changed, 9 insertions(+), 70 deletions(-) delete mode 100644 src/test/debugger/common/protocolparser.test.ts diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index bfffb809ef55..acf460b88625 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -11,6 +11,10 @@ import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId, } from './checks/envPathVariable'; +import { + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, +} from './checks/invalidPythonPathInDebugger'; import { JediPython27NotSupportedDiagnosticService, JediPython27NotSupportedDiagnosticServiceId, @@ -60,6 +64,11 @@ export function registerTypes(serviceManager: IServiceManager): void { IExtensionSingleActivationService, InvalidPythonInterpreterService, ); + serviceManager.addSingleton( + IDiagnosticsService, + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, + ); serviceManager.addSingleton( IDiagnosticsService, PowerShellActivationHackDiagnosticsService, diff --git a/src/test/debugger/common/protocolparser.test.ts b/src/test/debugger/common/protocolparser.test.ts deleted file mode 100644 index 117a58a7bc66..000000000000 --- a/src/test/debugger/common/protocolparser.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { PassThrough } from 'stream'; -import { createDeferred } from '../../../client/common/utils/async'; -import { ProtocolParser } from '../../../client/debugger/extension/helpers/protocolParser'; -import { sleep } from '../../common'; - -suite('Debugging - Protocol Parser', () => { - test('Test request, response and event messages', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => (messagesDetected += 1)); - const requestDetected = new Promise((resolve) => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - const responseDetected = new Promise((resolve) => { - protocolParser.on('response_initialize', () => resolve(true)); - }); - const eventDetected = new Promise((resolve) => { - protocolParser.on('event_initialized', () => resolve(true)); - }); - - stream.write( - 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}', - ); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - stream.write( - 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}', - ); - await expect(responseDetected).to.eventually.equal(true, 'response not parsed'); - - stream.write('Content-Length: 63\r\n\r\n{"type": "event", "seq": 1, "event": "initialized", "body": {}}'); - await expect(eventDetected).to.eventually.equal(true, 'event not parsed'); - - expect(messagesDetected).to.be.equal(3, 'incorrect number of protocol messages'); - }); - test('Ensure messages are not received after disposing the parser', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => (messagesDetected += 1)); - const requestDetected = new Promise((resolve) => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - stream.write( - 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}', - ); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - protocolParser.dispose(); - - const responseDetected = createDeferred(); - protocolParser.on('response_initialize', () => responseDetected.resolve(true)); - - stream.write( - 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}', - ); - // Wait for messages to go through and get parsed (unnecenssary, but add for testing edge cases). - await sleep(1000); - expect(responseDetected.completed).to.be.equal(false, 'Promise should not have resolved'); - }); -}); From 1a783a89a637ea12f297fd43adcfa6c871bac076 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 16:52:25 -0500 Subject: [PATCH 06/24] clean code --- src/client/debugger/extension/types.ts | 8 +------- src/client/extensionActivation.ts | 8 -------- src/client/testing/common/debugLauncher.ts | 3 +-- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 5b2e9facb92c..2321dce30c14 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -4,13 +4,7 @@ 'use strict'; import { Readable } from 'stream'; -import { DebugAdapterTrackerFactory, DebugConfigurationProvider, Disposable } from 'vscode'; - -export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); -export interface IDebugConfigurationService extends DebugConfigurationProvider {} - -export const IDynamicDebugConfigurationService = Symbol('IDynamicDebugConfigurationService'); -export interface IDynamicDebugConfigurationService extends DebugConfigurationProvider {} +import { DebugAdapterTrackerFactory, Disposable } from 'vscode'; export enum DebugConfigurationType { launchFile = 'launchFile', diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 432b63e34061..af1ef68bc285 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -22,9 +22,7 @@ import { IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; -// import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; -// import { IDebugConfigurationService } from './debugger/extension/types'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { ReplProvider } from './providers/replProvider'; @@ -160,12 +158,6 @@ async function activateLegacy(ext: ExtensionState): Promise { terminalProvider.initialize(window.activeTerminal).ignoreErrors(); disposables.push(terminalProvider); - // serviceContainer - // .getAll(IDebugConfigurationService) - // .forEach((debugConfigProvider) => { - // disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); - // }); - logAndNotifyOnLegacySettings(); registerCreateEnvironmentTriggers(disposables); initializePersistentStateForTriggers(ext.context); diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 0d132120c904..5c811dfc5ad7 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,6 +1,6 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, l10n, Uri, workspace, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IDebugService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; @@ -186,7 +186,6 @@ export class DebugLauncher implements ITestDebugLauncher { configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. - let launchArgs = await this.launchResolver.resolveDebugConfiguration( workspaceFolder, configArgs, From 447569a00dfa1130da1bd22c5acc500698e62bbe Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 17:03:38 -0500 Subject: [PATCH 07/24] fix lint --- src/client/testing/common/debugLauncher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 5c811dfc5ad7..37617e7c8a74 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,6 +1,6 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import { DebugConfiguration, l10n, Uri, workspace, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IDebugService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; From 54aa4f824d2a1d9871e9d5927ecd76b1ab619deb Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 20:03:12 -0500 Subject: [PATCH 08/24] fix tests --- src/test/testing/common/debugLauncher.unit.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index bbb65f0b2e2a..313b3231cae6 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -124,6 +124,8 @@ suite('Unit Tests - Debug Launcher', () => { debugEnvHelper .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(expected.env)); + console.log(workspaceFolder) + console.log(expected) debugService .setup((d) => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) @@ -578,7 +580,7 @@ suite('Unit Tests - Debug Launcher', () => { debugService.verifyAll(); }); - test('Handles comments', async () => { + test.only('Handles comments', async () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], @@ -599,7 +601,7 @@ suite('Unit Tests - Debug Launcher', () => { { \n\ // "test" debug config \n\ "name": "spam", /* non-empty */ \n\ - "type": "python", /* must be "python" */ \n\ + "type": "debugpy", /* must be "debugpy" */ \n\ "request": "test", /* must be "test" */ \n\ // extra stuff here: \n\ "stopOnEntry": true \n\ @@ -608,6 +610,7 @@ suite('Unit Tests - Debug Launcher', () => { } \n\ ', ); + console.log("options: ", options) await debugLauncher.launchDebugger(options); From 53209be4c3eda59bba07c39bc2c0d611f762d893 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 20:04:59 -0500 Subject: [PATCH 09/24] clean code --- src/test/testing/common/debugLauncher.unit.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 313b3231cae6..4b11a9837e65 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -124,8 +124,6 @@ suite('Unit Tests - Debug Launcher', () => { debugEnvHelper .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(expected.env)); - console.log(workspaceFolder) - console.log(expected) debugService .setup((d) => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) @@ -580,7 +578,7 @@ suite('Unit Tests - Debug Launcher', () => { debugService.verifyAll(); }); - test.only('Handles comments', async () => { + test('Handles comments', async () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], @@ -610,7 +608,6 @@ suite('Unit Tests - Debug Launcher', () => { } \n\ ', ); - console.log("options: ", options) await debugLauncher.launchDebugger(options); From 7b84e49c9f0f40f6d0f2d58bf624196e3bf73f0f Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 21:26:27 -0500 Subject: [PATCH 10/24] Move debugger for testing to the testing part --- .../debugger/extension/serviceRegistry.ts | 22 ------------ src/client/debugger/types.ts | 2 +- src/client/extensionActivation.ts | 2 -- .../common}/debugger/constants.ts | 0 .../common/{ => debugger}/debugLauncher.ts | 34 +++++++++---------- .../common/debugger}/launchJsonReader.ts | 4 +-- .../common/debugger}/resolvers/base.ts | 6 ++-- .../common/debugger}/resolvers/helper.ts | 2 +- .../common/debugger}/resolvers/launch.ts | 4 +-- .../common/debugger/resolvers}/types.ts | 0 .../common/debugger}/utils/common.ts | 0 src/client/testing/serviceRegistry.ts | 18 ++++++++-- src/test/debugger/common/constants.ts | 7 ---- .../testing/common/debugLauncher.unit.test.ts | 8 ++--- .../common}/debugger/envVars.test.ts | 16 ++++----- .../resolvers}/launchJsonReader.unit.test.ts | 2 +- 16 files changed, 55 insertions(+), 72 deletions(-) delete mode 100644 src/client/debugger/extension/serviceRegistry.ts rename src/client/{ => testing/common}/debugger/constants.ts (100%) rename src/client/testing/common/{ => debugger}/debugLauncher.ts (88%) rename src/client/{debugger/extension/configuration/launch.json => testing/common/debugger}/launchJsonReader.ts (92%) rename src/client/{debugger/extension/configuration => testing/common/debugger}/resolvers/base.ts (98%) rename src/client/{debugger/extension/configuration => testing/common/debugger}/resolvers/helper.ts (98%) rename src/client/{debugger/extension/configuration => testing/common/debugger}/resolvers/launch.ts (99%) rename src/client/{debugger/extension/configuration => testing/common/debugger/resolvers}/types.ts (100%) rename src/client/{debugger/extension/configuration => testing/common/debugger}/utils/common.ts (100%) delete mode 100644 src/test/debugger/common/constants.ts rename src/test/{ => testing/common}/debugger/envVars.test.ts (95%) rename src/test/{debugger/extension/configuration/launch.json => testing/common/debugger/resolvers}/launchJsonReader.unit.test.ts (97%) diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts deleted file mode 100644 index e1eef8b7b52c..000000000000 --- a/src/client/debugger/extension/serviceRegistry.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IServiceManager } from '../../ioc/types'; -import { LaunchRequestArguments } from '../types'; -import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; -import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; -import { IDebugConfigurationResolver } from './configuration/types'; - -export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton( - IDebugEnvironmentVariablesService, - DebugEnvironmentVariablesHelper, - ); - serviceManager.addSingleton>( - IDebugConfigurationResolver, - LaunchConfigurationResolver, - 'launch', - ); -} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index af4e40a4ac59..e00637fc7102 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -5,7 +5,7 @@ import { DebugConfiguration } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { DebuggerTypeName } from './constants'; +import { DebuggerTypeName } from '../testing/common/debugger/constants'; export enum DebugOptions { RedirectOutput = 'RedirectOutput', diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index af1ef68bc285..c20ded3ee536 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -22,7 +22,6 @@ import { IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; -import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { ReplProvider } from './providers/replProvider'; @@ -117,7 +116,6 @@ async function activateLegacy(ext: ExtensionState): Promise { unitTestsRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); - debugConfigurationRegisterTypes(serviceManager); tensorBoardRegisterTypes(serviceManager); const extensions = serviceContainer.get(IExtensions); diff --git a/src/client/debugger/constants.ts b/src/client/testing/common/debugger/constants.ts similarity index 100% rename from src/client/debugger/constants.ts rename to src/client/testing/common/debugger/constants.ts diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugger/debugLauncher.ts similarity index 88% rename from src/client/testing/common/debugLauncher.ts rename to src/client/testing/common/debugger/debugLauncher.ts index 37617e7c8a74..190375a79bbc 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugger/debugLauncher.ts @@ -1,23 +1,23 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService } from '../../common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import * as internalScripts from '../../common/process/internal/scripts'; -import { IConfigurationService, IPythonSettings } from '../../common/types'; -import { DebuggerTypeName } from '../../debugger/constants'; -import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; -import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; -import { IServiceContainer } from '../../ioc/types'; -import { traceError } from '../../logging'; -import { TestProvider } from '../types'; -import { ITestDebugLauncher, LaunchOptions } from './types'; -import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; -import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; -import { showErrorMessage } from '../../common/vscodeApis/windowApis'; -import { createDeferred } from '../../common/utils/async'; -import { pythonTestAdapterRewriteEnabled } from '../testController/common/utils'; -import { addPathToPythonpath } from './helpers'; +import { IApplicationShell, IDebugService } from '../../../common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import * as internalScripts from '../../../common/process/internal/scripts'; +import { IConfigurationService, IPythonSettings } from '../../../common/types'; +import { DebuggerTypeName } from './constants'; +import { IDebugConfigurationResolver } from './resolvers/types'; +import { DebugPurpose, LaunchRequestArguments } from '../../../debugger/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { traceError } from '../../../logging'; +import { TestProvider } from '../../types'; +import { ITestDebugLauncher, LaunchOptions } from '../types'; +import { getConfigurationsForWorkspace } from './launchJsonReader'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../../common/utils/async'; +import { pythonTestAdapterRewriteEnabled } from '../../testController/common/utils'; +import { addPathToPythonpath } from '../helpers'; @injectable() export class DebugLauncher implements ITestDebugLauncher { diff --git a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts b/src/client/testing/common/debugger/launchJsonReader.ts similarity index 92% rename from src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts rename to src/client/testing/common/debugger/launchJsonReader.ts index 1d76e3b8cd26..21b838c89dce 100644 --- a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts +++ b/src/client/testing/common/debugger/launchJsonReader.ts @@ -5,8 +5,8 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { parse } from 'jsonc-parser'; import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; -import { traceLog } from '../../../../logging'; +import { getConfiguration, getWorkspaceFolder } from '../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../logging'; export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/testing/common/debugger/resolvers/base.ts similarity index 98% rename from src/client/debugger/extension/configuration/resolvers/base.ts rename to src/client/testing/common/debugger/resolvers/base.ts index ac182ebc0ef0..a9e2611bd7f1 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/testing/common/debugger/resolvers/base.ts @@ -16,9 +16,9 @@ import { IInterpreterService } from '../../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTelemetry } from '../../../../telemetry/types'; -import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; -import { PythonPathSource } from '../../types'; -import { IDebugConfigurationResolver } from '../types'; +import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../../debugger/types'; +import { PythonPathSource } from '../../../../debugger/extension/types'; +import { IDebugConfigurationResolver } from './types'; import { resolveVariables } from '../utils/common'; import { getProgram } from './helper'; diff --git a/src/client/debugger/extension/configuration/resolvers/helper.ts b/src/client/testing/common/debugger/resolvers/helper.ts similarity index 98% rename from src/client/debugger/extension/configuration/resolvers/helper.ts rename to src/client/testing/common/debugger/resolvers/helper.ts index 15be5f97538e..11b3eacfb127 100644 --- a/src/client/debugger/extension/configuration/resolvers/helper.ts +++ b/src/client/testing/common/debugger/resolvers/helper.ts @@ -6,7 +6,7 @@ import { inject, injectable } from 'inversify'; import { ICurrentProcess } from '../../../../common/types'; import { EnvironmentVariables, IEnvironmentVariablesService } from '../../../../common/variables/types'; -import { LaunchRequestArguments } from '../../../types'; +import { LaunchRequestArguments } from '../../../../debugger/types'; import { PYTHON_LANGUAGE } from '../../../../common/constants'; import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; import { getSearchPathEnvVarNames } from '../../../../common/utils/exec'; diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/testing/common/debugger/resolvers/launch.ts similarity index 99% rename from src/client/debugger/extension/configuration/resolvers/launch.ts rename to src/client/testing/common/debugger/resolvers/launch.ts index 274758797eb9..73a7cf6ded9d 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/testing/common/debugger/resolvers/launch.ts @@ -12,8 +12,8 @@ import { getOSType, OSType } from '../../../../common/utils/platform'; import { EnvironmentVariables } from '../../../../common/variables/types'; import { IEnvironmentActivationService } from '../../../../interpreter/activation/types'; import { IInterpreterService } from '../../../../interpreter/contracts'; -import { DebuggerTypeName } from '../../../constants'; -import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; +import { DebuggerTypeName } from '../constants'; +import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../../debugger/types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; import { diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/testing/common/debugger/resolvers/types.ts similarity index 100% rename from src/client/debugger/extension/configuration/types.ts rename to src/client/testing/common/debugger/resolvers/types.ts diff --git a/src/client/debugger/extension/configuration/utils/common.ts b/src/client/testing/common/debugger/utils/common.ts similarity index 100% rename from src/client/debugger/extension/configuration/utils/common.ts rename to src/client/testing/common/debugger/utils/common.ts diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 6a7b4b5a1640..59e30a2a895d 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -3,7 +3,7 @@ import { IExtensionActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; -import { DebugLauncher } from './common/debugLauncher'; +import { DebugLauncher } from './common/debugger/debugLauncher'; import { TestRunner } from './common/runner'; import { TestConfigSettingsService } from './common/configSettingService'; import { TestsHelper } from './common/testUtils'; @@ -22,6 +22,10 @@ import { TestingService, UnitTestManagementService } from './main'; import { ITestingService } from './types'; import { UnitTestSocketServer } from './common/socketServer'; import { registerTestControllerTypes } from './testController/serviceRegistry'; +import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './common/debugger/resolvers/helper'; +import { IDebugConfigurationResolver } from './common/debugger/resolvers/types'; +import { LaunchConfigurationResolver } from './common/debugger/resolvers/launch'; +import { LaunchRequestArguments } from '../debugger/types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ITestDebugLauncher, DebugLauncher); @@ -40,6 +44,16 @@ export function registerTypes(serviceManager: IServiceManager) { TestConfigurationManagerFactory, ); serviceManager.addSingleton(IExtensionActivationService, UnitTestManagementService); - + + serviceManager.addSingleton( + IDebugEnvironmentVariablesService, + DebugEnvironmentVariablesHelper, + ); + serviceManager.addSingleton>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ); + registerTestControllerTypes(serviceManager); } diff --git a/src/test/debugger/common/constants.ts b/src/test/debugger/common/constants.ts deleted file mode 100644 index a9bcc64f1a24..000000000000 --- a/src/test/debugger/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// Sometimes PTVSD can take a while for thread & other events to be reported. -export const DEBUGGER_TIMEOUT = 20000; diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 4b11a9837e65..a5328fa229ac 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -16,14 +16,14 @@ import { IApplicationShell, IDebugService } from '../../../client/common/applica import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import '../../../client/common/extensions'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { DebuggerTypeName } from '../../../client/debugger/constants'; -import { IDebugEnvironmentVariablesService } from '../../../client/debugger/extension/configuration/resolvers/helper'; -import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; +import { DebuggerTypeName } from '../../../client/testing/common/debugger/constants'; +import { IDebugEnvironmentVariablesService } from '../../../client/testing/common/debugger/resolvers/helper'; +import { LaunchConfigurationResolver } from '../../../client/testing/common/debugger/resolvers/launch'; import { DebugOptions } from '../../../client/debugger/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { DebugLauncher } from '../../../client/testing/common/debugLauncher'; +import { DebugLauncher } from '../../../client/testing/common/debugger/debugLauncher'; import { LaunchOptions } from '../../../client/testing/common/types'; import { ITestingSettings } from '../../../client/testing/configuration/types'; import { TestProvider } from '../../../client/testing/types'; diff --git a/src/test/debugger/envVars.test.ts b/src/test/testing/common/debugger/envVars.test.ts similarity index 95% rename from src/test/debugger/envVars.test.ts rename to src/test/testing/common/debugger/envVars.test.ts index c043146fe53d..3d610c715716 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/testing/common/debugger/envVars.test.ts @@ -5,17 +5,17 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import * as shortid from 'shortid'; -import { ICurrentProcess, IPathUtils } from '../../client/common/types'; -import { IEnvironmentVariablesService } from '../../client/common/variables/types'; +import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; +import { IEnvironmentVariablesService } from '../../../../client/common/variables/types'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService, -} from '../../client/debugger/extension/configuration/resolvers/helper'; -import { ConsoleType, LaunchRequestArguments } from '../../client/debugger/types'; -import { isOs, OSType } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { normCase } from '../../client/common/platform/fs-paths'; +} from '../../../../client/testing/common/debugger/resolvers/helper'; +import { ConsoleType, LaunchRequestArguments } from '../../../../client/debugger/types'; +import { isOs, OSType } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../../../initialize'; +import { UnitTestIocContainer } from '../../serviceRegistry'; +import { normCase } from '../../../../client/common/platform/fs-paths'; use(chaiAsPromised); diff --git a/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts b/src/test/testing/common/debugger/resolvers/launchJsonReader.unit.test.ts similarity index 97% rename from src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts rename to src/test/testing/common/debugger/resolvers/launchJsonReader.unit.test.ts index 8ed19dc254aa..2df58d10d8b4 100644 --- a/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts +++ b/src/test/testing/common/debugger/resolvers/launchJsonReader.unit.test.ts @@ -8,7 +8,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { Uri } from 'vscode'; import { assert } from 'chai'; -import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; +import { getConfigurationsForWorkspace } from '../../../../../client/testing/common/debugger/launchJsonReader'; import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; suite('Launch Json Reader', () => { From ae4f97e497d977f7eb6fa59cb99823452fa96c9f Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 21:47:54 -0500 Subject: [PATCH 11/24] Move and register interpreter path command --- .eslintignore | 3 +-- .../interpreterPathCommand.ts | 10 +++++----- src/client/interpreter/serviceRegistry.ts | 5 +++++ .../interpreterPathCommand.unit.test.ts | 10 +++++----- src/test/interpreters/serviceRegistry.unit.test.ts | 2 ++ 5 files changed, 18 insertions(+), 12 deletions(-) rename src/client/{debugger/extension/configuration/launch.json => interpreter}/interpreterPathCommand.ts (82%) rename src/test/{debugger/extension/configuration/launch.json => interpreters}/interpreterPathCommand.unit.test.ts (85%) diff --git a/.eslintignore b/.eslintignore index 7f6bb48d6c8e..7d0938f16db1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -47,10 +47,10 @@ src/test/api.functional.test.ts src/test/testing/mocks.ts src/test/testing/common/debugLauncher.unit.test.ts src/test/testing/common/services/configSettingService.unit.test.ts +src/test/testing/common/debugger/envVars.test.ts src/test/common/exitCIAfterTestReporter.ts - src/test/common/terminals/activator/index.unit.test.ts src/test/common/terminals/activator/base.unit.test.ts src/test/common/terminals/shellDetector.unit.test.ts @@ -116,7 +116,6 @@ src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts src/test/debugger/utils.ts src/test/debugger/common/protocolparser.test.ts -src/test/debugger/envVars.test.ts src/test/telemetry/index.unit.test.ts src/test/telemetry/envFileTelemetry.unit.test.ts diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/interpreter/interpreterPathCommand.ts similarity index 82% rename from src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts rename to src/client/interpreter/interpreterPathCommand.ts index 0335b744f6f4..8402374d50d6 100644 --- a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/src/client/interpreter/interpreterPathCommand.ts @@ -5,11 +5,11 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { Commands } from '../../../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../../../common/types'; -import { registerCommand } from '../../../../common/vscodeApis/commandApis'; -import { IInterpreterService } from '../../../../interpreter/contracts'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { Commands } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { IInterpreterService } from './contracts'; @injectable() export class InterpreterPathCommand implements IExtensionSingleActivationService { diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 422776bd5e43..fa44038ec717 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -29,6 +29,7 @@ import { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, I import { InterpreterDisplay } from './display'; import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; import { InterpreterHelper } from './helpers'; +import { InterpreterPathCommand } from './interpreterPathCommand'; import { InterpreterService } from './interpreterService'; import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; @@ -108,4 +109,8 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterPathCommand, + ); } diff --git a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/src/test/interpreters/interpreterPathCommand.unit.test.ts similarity index 85% rename from src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts rename to src/test/interpreters/interpreterPathCommand.unit.test.ts index 77077ad945fb..7001453100ec 100644 --- a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/src/test/interpreters/interpreterPathCommand.unit.test.ts @@ -8,11 +8,11 @@ import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; -import { IDisposable } from '../../../../../client/common/types'; -import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; -import { InterpreterPathCommand } from '../../../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { IInterpreterService } from '../../../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; +import { IDisposable } from '../../client/common/types'; +import * as commandApis from '../../client/common/vscodeApis/commandApis'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; suite('Interpreter Path Command', () => { let interpreterService: IInterpreterService; diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 00090eb4b6e9..bb488a49307d 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -43,6 +43,7 @@ import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; import { VirtualEnvironmentPrompt } from '../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; suite('Interpreters - Service Registry', () => { test('Registrations', () => { @@ -74,6 +75,7 @@ suite('Interpreters - Service Registry', () => { [EnvironmentActivationService, EnvironmentActivationService], [IEnvironmentActivationService, EnvironmentActivationService], + [IExtensionSingleActivationService, InterpreterPathCommand], [IExtensionActivationService, CondaInheritEnvPrompt], [IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch], ].forEach((mapping) => { From 5f84b74b132553897d2cfe3d09cd86da38d47339 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 22:47:46 -0500 Subject: [PATCH 12/24] Keep debug files in debuger folder --- .eslintignore | 3 +- .../common => }/debugger/constants.ts | 0 .../launch.json}/launchJsonReader.ts | 4 +- .../configuration}/resolvers/base.ts | 2 +- .../configuration}/resolvers/helper.ts | 2 +- .../configuration}/resolvers/launch.ts | 4 +- .../configuration}/resolvers/types.ts | 0 .../extension/configuration}/utils/common.ts | 0 .../debugger/extension/serviceRegistry.ts | 20 ++ src/client/debugger/extension/types.ts | 25 -- src/client/debugger/types.ts | 2 +- src/client/extensionActivation.ts | 2 + src/client/telemetry/constants.ts | 10 - src/client/telemetry/index.ts | 219 +----------------- .../common/{debugger => }/debugLauncher.ts | 34 +-- src/client/testing/serviceRegistry.ts | 8 +- .../common => }/debugger/envVars.test.ts | 16 +- .../launchJsonReader.unit.test.ts | 2 +- .../extension/serviceRegistry.unit.test.ts | 31 +++ .../testing/common/debugLauncher.unit.test.ts | 8 +- 20 files changed, 98 insertions(+), 294 deletions(-) rename src/client/{testing/common => }/debugger/constants.ts (100%) rename src/client/{testing/common/debugger => debugger/extension/configuration/launch.json}/launchJsonReader.ts (92%) rename src/client/{testing/common/debugger => debugger/extension/configuration}/resolvers/base.ts (99%) rename src/client/{testing/common/debugger => debugger/extension/configuration}/resolvers/helper.ts (98%) rename src/client/{testing/common/debugger => debugger/extension/configuration}/resolvers/launch.ts (99%) rename src/client/{testing/common/debugger => debugger/extension/configuration}/resolvers/types.ts (100%) rename src/client/{testing/common/debugger => debugger/extension/configuration}/utils/common.ts (100%) create mode 100644 src/client/debugger/extension/serviceRegistry.ts rename src/client/testing/common/{debugger => }/debugLauncher.ts (88%) rename src/test/{testing/common => }/debugger/envVars.test.ts (95%) rename src/test/{testing/common/debugger/resolvers => debugger/extension/configuration/launch.json}/launchJsonReader.unit.test.ts (97%) create mode 100644 src/test/debugger/extension/serviceRegistry.unit.test.ts diff --git a/.eslintignore b/.eslintignore index 7d0938f16db1..7f6bb48d6c8e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -47,10 +47,10 @@ src/test/api.functional.test.ts src/test/testing/mocks.ts src/test/testing/common/debugLauncher.unit.test.ts src/test/testing/common/services/configSettingService.unit.test.ts -src/test/testing/common/debugger/envVars.test.ts src/test/common/exitCIAfterTestReporter.ts + src/test/common/terminals/activator/index.unit.test.ts src/test/common/terminals/activator/base.unit.test.ts src/test/common/terminals/shellDetector.unit.test.ts @@ -116,6 +116,7 @@ src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts src/test/debugger/utils.ts src/test/debugger/common/protocolparser.test.ts +src/test/debugger/envVars.test.ts src/test/telemetry/index.unit.test.ts src/test/telemetry/envFileTelemetry.unit.test.ts diff --git a/src/client/testing/common/debugger/constants.ts b/src/client/debugger/constants.ts similarity index 100% rename from src/client/testing/common/debugger/constants.ts rename to src/client/debugger/constants.ts diff --git a/src/client/testing/common/debugger/launchJsonReader.ts b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts similarity index 92% rename from src/client/testing/common/debugger/launchJsonReader.ts rename to src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts index 21b838c89dce..1d76e3b8cd26 100644 --- a/src/client/testing/common/debugger/launchJsonReader.ts +++ b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -5,8 +5,8 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { parse } from 'jsonc-parser'; import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { getConfiguration, getWorkspaceFolder } from '../../../common/vscodeApis/workspaceApis'; -import { traceLog } from '../../../logging'; +import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../../logging'; export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); diff --git a/src/client/testing/common/debugger/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts similarity index 99% rename from src/client/testing/common/debugger/resolvers/base.ts rename to src/client/debugger/extension/configuration/resolvers/base.ts index a9e2611bd7f1..d6f38dc4cf1f 100644 --- a/src/client/testing/common/debugger/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -169,7 +169,7 @@ export abstract class BaseConfigurationResolver const LocalHosts = ['localhost', '127.0.0.1', '::1']; return !!(hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0); } - + protected static fixUpPathMappings( pathMappings: PathMapping[], defaultLocalRoot?: string, diff --git a/src/client/testing/common/debugger/resolvers/helper.ts b/src/client/debugger/extension/configuration/resolvers/helper.ts similarity index 98% rename from src/client/testing/common/debugger/resolvers/helper.ts rename to src/client/debugger/extension/configuration/resolvers/helper.ts index 11b3eacfb127..15be5f97538e 100644 --- a/src/client/testing/common/debugger/resolvers/helper.ts +++ b/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -6,7 +6,7 @@ import { inject, injectable } from 'inversify'; import { ICurrentProcess } from '../../../../common/types'; import { EnvironmentVariables, IEnvironmentVariablesService } from '../../../../common/variables/types'; -import { LaunchRequestArguments } from '../../../../debugger/types'; +import { LaunchRequestArguments } from '../../../types'; import { PYTHON_LANGUAGE } from '../../../../common/constants'; import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; import { getSearchPathEnvVarNames } from '../../../../common/utils/exec'; diff --git a/src/client/testing/common/debugger/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts similarity index 99% rename from src/client/testing/common/debugger/resolvers/launch.ts rename to src/client/debugger/extension/configuration/resolvers/launch.ts index 73a7cf6ded9d..274758797eb9 100644 --- a/src/client/testing/common/debugger/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -12,8 +12,8 @@ import { getOSType, OSType } from '../../../../common/utils/platform'; import { EnvironmentVariables } from '../../../../common/variables/types'; import { IEnvironmentActivationService } from '../../../../interpreter/activation/types'; import { IInterpreterService } from '../../../../interpreter/contracts'; -import { DebuggerTypeName } from '../constants'; -import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../../debugger/types'; +import { DebuggerTypeName } from '../../../constants'; +import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; import { diff --git a/src/client/testing/common/debugger/resolvers/types.ts b/src/client/debugger/extension/configuration/resolvers/types.ts similarity index 100% rename from src/client/testing/common/debugger/resolvers/types.ts rename to src/client/debugger/extension/configuration/resolvers/types.ts diff --git a/src/client/testing/common/debugger/utils/common.ts b/src/client/debugger/extension/configuration/utils/common.ts similarity index 100% rename from src/client/testing/common/debugger/utils/common.ts rename to src/client/debugger/extension/configuration/utils/common.ts diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts new file mode 100644 index 000000000000..b536007853e9 --- /dev/null +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceManager } from '../../ioc/types'; +import { LaunchRequestArguments } from '../types'; +import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; +import { IDebugConfigurationResolver } from './configuration/resolvers/types'; +import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; + +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ); + serviceManager.addSingleton( + IDebugEnvironmentVariablesService, + DebugEnvironmentVariablesHelper, + ); +} \ No newline at end of file diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 2321dce30c14..a9604efad68f 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,32 +3,7 @@ 'use strict'; -import { Readable } from 'stream'; -import { DebugAdapterTrackerFactory, Disposable } from 'vscode'; - -export enum DebugConfigurationType { - launchFile = 'launchFile', - remoteAttach = 'remoteAttach', - launchDjango = 'launchDjango', - launchFastAPI = 'launchFastAPI', - launchFlask = 'launchFlask', - launchModule = 'launchModule', - launchPyramid = 'launchPyramid', - pidAttach = 'pidAttach', -} - export enum PythonPathSource { launchJson = 'launch.json', settingsJson = 'settings.json', } - -export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFactory'); - -export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} - -export const IProtocolParser = Symbol('IProtocolParser'); -export interface IProtocolParser extends Disposable { - connect(stream: Readable): void; - once(event: string | symbol, listener: (...args: unknown[]) => void): this; - on(event: string | symbol, listener: (...args: unknown[]) => void): this; -} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index e00637fc7102..af4e40a4ac59 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -5,7 +5,7 @@ import { DebugConfiguration } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { DebuggerTypeName } from '../testing/common/debugger/constants'; +import { DebuggerTypeName } from './constants'; export enum DebugOptions { RedirectOutput = 'RedirectOutput', diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index c20ded3ee536..af1ef68bc285 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -22,6 +22,7 @@ import { IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; +import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { ReplProvider } from './providers/replProvider'; @@ -116,6 +117,7 @@ async function activateLegacy(ext: ExtensionState): Promise { unitTestsRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); tensorBoardRegisterTypes(serviceManager); const extensions = serviceContainer.get(IExtensions); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 9e29ef808d0d..c35b2501ea06 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -34,18 +34,8 @@ export enum EventName { ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', EXECUTION_CODE = 'EXECUTION_CODE', EXECUTION_DJANGO = 'EXECUTION_DJANGO', - DEBUG_IN_TERMINAL_BUTTON = 'DEBUG.IN_TERMINAL', - DEBUG_ADAPTER_USING_WHEELS_PATH = 'DEBUG_ADAPTER.USING_WHEELS_PATH', - DEBUG_SESSION_ERROR = 'DEBUG_SESSION.ERROR', - DEBUG_SESSION_START = 'DEBUG_SESSION.START', - DEBUG_SESSION_STOP = 'DEBUG_SESSION.STOP', - DEBUG_SESSION_USER_CODE_RUNNING = 'DEBUG_SESSION.USER_CODE_RUNNING', DEBUGGER = 'DEBUGGER', - DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', - DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS', - DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', - DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', // Python testing specific telemetry UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f9ed98eb3764..ebb90485e76b 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -11,8 +11,6 @@ import { AppinsightsKey, EXTENSION_ROOT_DIR, isTestExecution, isUnitTestExecutio import type { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; -import { DebugConfigurationType } from '../debugger/extension/types'; -import { ConsoleType, TriggerType } from '../debugger/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; import { TensorBoardPromptSelection, @@ -22,6 +20,7 @@ import { } from '../tensorBoard/constants'; import { EventName } from './constants'; import type { TestTool } from './types'; +import { ConsoleType, TriggerType } from '../debugger/types'; /** * Checks whether telemetry is supported. @@ -311,145 +310,7 @@ export interface IEventNamePropertyMapping { "debug_in_terminal_button" : { "owner": "paulacamargo25" } */ [EventName.DEBUG_IN_TERMINAL_BUTTON]: never | undefined; - /** - * Telemetry event captured when debug adapter executable is created - */ - /* __GDPR__ - "debug_adapter.using_wheels_path" : { - "usingwheels" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } - } - */ - - [EventName.DEBUG_ADAPTER_USING_WHEELS_PATH]: { - /** - * Carries boolean - * - `true` if path used for the adapter is the debugger with wheels. - * - `false` if path used for the adapter is the source only version of the debugger. - */ - usingWheels: boolean; - }; - /** - * Telemetry captured before starting debug session. - */ - /* __GDPR__ - "debug_session.start" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_START]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured when debug session runs into an error. - */ - /* __GDPR__ - "debug_session.error" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - - } - */ - [EventName.DEBUG_SESSION_ERROR]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured after stopping debug session. - */ - /* __GDPR__ - "debug_session.stop" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_STOP]: { /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured when user code starts running after loading the debugger. - */ - /* __GDPR__ - "debug_session.user_code_running" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_USER_CODE_RUNNING]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** * Telemetry captured when starting the debugger. */ /* __GDPR__ @@ -475,7 +336,7 @@ export interface IEventNamePropertyMapping { "scrapy": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } } */ - [EventName.DEBUGGER]: { + [EventName.DEBUGGER]: { /** * Trigger for starting the debugger. * - `launch`: Launch/start new code and debug it. @@ -599,82 +460,6 @@ export interface IEventNamePropertyMapping { */ scrapy: boolean; }; - /** - * Telemetry event sent when attaching to child process - */ - /* __GDPR__ - "debugger.attach_to_child_process" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "paulacamargo25" } - } - */ - [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; - /** - * Telemetry event sent when attaching to a local process. - */ - /* __GDPR__ - "debugger.attach_to_local_process" : { "owner": "paulacamargo25" } - */ - [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; - /** - * Telemetry sent after building configuration for debugger - */ - /* __GDPR__ - "debugger.configuration.prompts" : { - "configurationtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetecteddjangomanagepypath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetectedpyramidinipath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetectedfastapimainpypath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "autodetectedflaskapppypath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "manuallyenteredavalue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } - } - */ - - [EventName.DEBUGGER_CONFIGURATION_PROMPTS]: { - /** - * The type of debug configuration to build configuration for - * - * @type {DebugConfigurationType} - */ - configurationType: DebugConfigurationType; - /** - * Carries `true` if we are able to auto-detect manage.py path for Django, `false` otherwise - * - * @type {boolean} - */ - autoDetectedDjangoManagePyPath?: boolean; - /** - * Carries `true` if we are able to auto-detect .ini file path for Pyramid, `false` otherwise - * - * @type {boolean} - */ - autoDetectedPyramidIniPath?: boolean; - /** - * Carries `true` if we are able to auto-detect main.py path for FastAPI, `false` otherwise - * - * @type {boolean} - */ - autoDetectedFastAPIMainPyPath?: boolean; - /** - * Carries `true` if we are able to auto-detect app.py path for Flask, `false` otherwise - * - * @type {boolean} - */ - autoDetectedFlaskAppPyPath?: boolean; - /** - * Carries `true` if user manually entered the required path for the app - * (path to `manage.py` for Django, path to `.ini` for Pyramid, path to `app.py` for Flask), `false` otherwise - * - * @type {boolean} - */ - manuallyEnteredAValue?: boolean; - }; - /** - * Telemetry event sent when providing completion provider in launch.json. It is sent just *after* inserting the completion. - */ - /* __GDPR__ - "debugger.configuration.prompts.in.launch.json" : { "owner": "paulacamargo25" } - */ - [EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON]: never | undefined; /** * Telemetry event sent with details of actions when invoking a diagnostic command */ diff --git a/src/client/testing/common/debugger/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts similarity index 88% rename from src/client/testing/common/debugger/debugLauncher.ts rename to src/client/testing/common/debugLauncher.ts index 190375a79bbc..9119b0f4d1d4 100644 --- a/src/client/testing/common/debugger/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,23 +1,23 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService } from '../../../common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import * as internalScripts from '../../../common/process/internal/scripts'; -import { IConfigurationService, IPythonSettings } from '../../../common/types'; -import { DebuggerTypeName } from './constants'; -import { IDebugConfigurationResolver } from './resolvers/types'; -import { DebugPurpose, LaunchRequestArguments } from '../../../debugger/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { traceError } from '../../../logging'; -import { TestProvider } from '../../types'; -import { ITestDebugLauncher, LaunchOptions } from '../types'; -import { getConfigurationsForWorkspace } from './launchJsonReader'; -import { getWorkspaceFolder, getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; -import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; -import { createDeferred } from '../../../common/utils/async'; -import { pythonTestAdapterRewriteEnabled } from '../../testController/common/utils'; -import { addPathToPythonpath } from '../helpers'; +import { IApplicationShell, IDebugService } from '../../common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { IConfigurationService, IPythonSettings } from '../../common/types'; +import { DebuggerTypeName } from '../../debugger/constants'; +import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/resolvers/types'; +import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; +import { IServiceContainer } from '../../ioc/types'; +import { traceError } from '../../logging'; +import { TestProvider } from '../types'; +import { ITestDebugLauncher, LaunchOptions } from './types'; +import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { showErrorMessage } from '../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../common/utils/async'; +import { pythonTestAdapterRewriteEnabled } from '../testController/common/utils'; +import { addPathToPythonpath } from './helpers'; @injectable() export class DebugLauncher implements ITestDebugLauncher { diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 59e30a2a895d..6369c2536f13 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -3,7 +3,7 @@ import { IExtensionActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; -import { DebugLauncher } from './common/debugger/debugLauncher'; +import { DebugLauncher } from './common/debugLauncher'; import { TestRunner } from './common/runner'; import { TestConfigSettingsService } from './common/configSettingService'; import { TestsHelper } from './common/testUtils'; @@ -22,9 +22,9 @@ import { TestingService, UnitTestManagementService } from './main'; import { ITestingService } from './types'; import { UnitTestSocketServer } from './common/socketServer'; import { registerTestControllerTypes } from './testController/serviceRegistry'; -import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './common/debugger/resolvers/helper'; -import { IDebugConfigurationResolver } from './common/debugger/resolvers/types'; -import { LaunchConfigurationResolver } from './common/debugger/resolvers/launch'; +import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from '../debugger/extension/configuration/resolvers/helper'; +import { IDebugConfigurationResolver } from '../debugger/extension/configuration/resolvers/types'; +import { LaunchConfigurationResolver } from '../debugger/extension/configuration/resolvers/launch'; import { LaunchRequestArguments } from '../debugger/types'; export function registerTypes(serviceManager: IServiceManager) { diff --git a/src/test/testing/common/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts similarity index 95% rename from src/test/testing/common/debugger/envVars.test.ts rename to src/test/debugger/envVars.test.ts index 3d610c715716..c043146fe53d 100644 --- a/src/test/testing/common/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -5,17 +5,17 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import * as shortid from 'shortid'; -import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; -import { IEnvironmentVariablesService } from '../../../../client/common/variables/types'; +import { ICurrentProcess, IPathUtils } from '../../client/common/types'; +import { IEnvironmentVariablesService } from '../../client/common/variables/types'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService, -} from '../../../../client/testing/common/debugger/resolvers/helper'; -import { ConsoleType, LaunchRequestArguments } from '../../../../client/debugger/types'; -import { isOs, OSType } from '../../../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../../../initialize'; -import { UnitTestIocContainer } from '../../serviceRegistry'; -import { normCase } from '../../../../client/common/platform/fs-paths'; +} from '../../client/debugger/extension/configuration/resolvers/helper'; +import { ConsoleType, LaunchRequestArguments } from '../../client/debugger/types'; +import { isOs, OSType } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { normCase } from '../../client/common/platform/fs-paths'; use(chaiAsPromised); diff --git a/src/test/testing/common/debugger/resolvers/launchJsonReader.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts similarity index 97% rename from src/test/testing/common/debugger/resolvers/launchJsonReader.unit.test.ts rename to src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts index 2df58d10d8b4..8ed19dc254aa 100644 --- a/src/test/testing/common/debugger/resolvers/launchJsonReader.unit.test.ts +++ b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts @@ -8,7 +8,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { Uri } from 'vscode'; import { assert } from 'chai'; -import { getConfigurationsForWorkspace } from '../../../../../client/testing/common/debugger/launchJsonReader'; +import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; suite('Launch Json Reader', () => { 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..d7e972b8723b --- /dev/null +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; +import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; +import { LaunchRequestArguments } from '../../../client/debugger/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; +import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/types'; + +suite('Debugging - Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + test('Registrations', () => { + registerTypes(instance(serviceManager)); + + verify( + serviceManager.addSingleton>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ), + ).once(); + }); +}); \ No newline at end of file diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index a5328fa229ac..4b11a9837e65 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -16,14 +16,14 @@ import { IApplicationShell, IDebugService } from '../../../client/common/applica import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import '../../../client/common/extensions'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { DebuggerTypeName } from '../../../client/testing/common/debugger/constants'; -import { IDebugEnvironmentVariablesService } from '../../../client/testing/common/debugger/resolvers/helper'; -import { LaunchConfigurationResolver } from '../../../client/testing/common/debugger/resolvers/launch'; +import { DebuggerTypeName } from '../../../client/debugger/constants'; +import { IDebugEnvironmentVariablesService } from '../../../client/debugger/extension/configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; import { DebugOptions } from '../../../client/debugger/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { DebugLauncher } from '../../../client/testing/common/debugger/debugLauncher'; +import { DebugLauncher } from '../../../client/testing/common/debugLauncher'; import { LaunchOptions } from '../../../client/testing/common/types'; import { ITestingSettings } from '../../../client/testing/configuration/types'; import { TestProvider } from '../../../client/testing/types'; From ca53dc2c2dab92f0ad64e3cd2e69c6c222eecea2 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 22:53:05 -0500 Subject: [PATCH 13/24] update types --- src/client/debugger/extension/configuration/resolvers/base.ts | 2 +- .../debugger/extension/configuration/{resolvers => }/types.ts | 0 src/client/debugger/extension/serviceRegistry.ts | 2 +- src/client/testing/common/debugLauncher.ts | 2 +- src/client/testing/serviceRegistry.ts | 2 +- src/test/debugger/extension/serviceRegistry.unit.test.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/client/debugger/extension/configuration/{resolvers => }/types.ts (100%) diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index d6f38dc4cf1f..9074922746de 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -18,7 +18,7 @@ import { EventName } from '../../../../telemetry/constants'; import { DebuggerTelemetry } from '../../../../telemetry/types'; import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../../debugger/types'; import { PythonPathSource } from '../../../../debugger/extension/types'; -import { IDebugConfigurationResolver } from './types'; +import { IDebugConfigurationResolver } from '../types'; import { resolveVariables } from '../utils/common'; import { getProgram } from './helper'; diff --git a/src/client/debugger/extension/configuration/resolvers/types.ts b/src/client/debugger/extension/configuration/types.ts similarity index 100% rename from src/client/debugger/extension/configuration/resolvers/types.ts rename to src/client/debugger/extension/configuration/types.ts diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index b536007853e9..29d8b2095be5 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -4,7 +4,7 @@ import { IServiceManager } from '../../ioc/types'; import { LaunchRequestArguments } from '../types'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; -import { IDebugConfigurationResolver } from './configuration/resolvers/types'; +import { IDebugConfigurationResolver } from './configuration/types'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; export function registerTypes(serviceManager: IServiceManager): void { diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 9119b0f4d1d4..37617e7c8a74 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -6,7 +6,7 @@ import { EXTENSION_ROOT_DIR } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IConfigurationService, IPythonSettings } from '../../common/types'; import { DebuggerTypeName } from '../../debugger/constants'; -import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/resolvers/types'; +import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; import { IServiceContainer } from '../../ioc/types'; import { traceError } from '../../logging'; diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 6369c2536f13..44aa724e6357 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -23,7 +23,7 @@ import { ITestingService } from './types'; import { UnitTestSocketServer } from './common/socketServer'; import { registerTestControllerTypes } from './testController/serviceRegistry'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from '../debugger/extension/configuration/resolvers/helper'; -import { IDebugConfigurationResolver } from '../debugger/extension/configuration/resolvers/types'; +import { IDebugConfigurationResolver } from '../debugger/extension/configuration/types'; import { LaunchConfigurationResolver } from '../debugger/extension/configuration/resolvers/launch'; import { LaunchRequestArguments } from '../debugger/types'; diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index d7e972b8723b..8fc8993e83ce 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -9,7 +9,7 @@ import { registerTypes } from '../../../client/debugger/extension/serviceRegistr import { LaunchRequestArguments } from '../../../client/debugger/types'; import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceManager } from '../../../client/ioc/types'; -import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/types'; +import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; suite('Debugging - Service Registry', () => { let serviceManager: IServiceManager; From 0d912e54e9bb7e070c6d7607c696c54cb22defdd Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 22:54:24 -0500 Subject: [PATCH 14/24] fix lint --- .../debugger/extension/configuration/resolvers/base.ts | 2 +- src/client/debugger/extension/serviceRegistry.ts | 2 +- src/client/telemetry/index.ts | 4 ++-- src/client/testing/serviceRegistry.ts | 9 ++++++--- src/test/debugger/extension/serviceRegistry.unit.test.ts | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index 9074922746de..24f45c786d69 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -169,7 +169,7 @@ export abstract class BaseConfigurationResolver const LocalHosts = ['localhost', '127.0.0.1', '::1']; return !!(hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0); } - + protected static fixUpPathMappings( pathMappings: PathMapping[], defaultLocalRoot?: string, diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 29d8b2095be5..3158bdf4c630 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -17,4 +17,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper, ); -} \ No newline at end of file +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index ebb90485e76b..676d018c5925 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -310,7 +310,7 @@ export interface IEventNamePropertyMapping { "debug_in_terminal_button" : { "owner": "paulacamargo25" } */ [EventName.DEBUG_IN_TERMINAL_BUTTON]: never | undefined; - /** + /** * Telemetry captured when starting the debugger. */ /* __GDPR__ @@ -336,7 +336,7 @@ export interface IEventNamePropertyMapping { "scrapy": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } } */ - [EventName.DEBUGGER]: { + [EventName.DEBUGGER]: { /** * Trigger for starting the debugger. * - `launch`: Launch/start new code and debug it. diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 44aa724e6357..7c013e6e433d 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -22,7 +22,10 @@ import { TestingService, UnitTestManagementService } from './main'; import { ITestingService } from './types'; import { UnitTestSocketServer } from './common/socketServer'; import { registerTestControllerTypes } from './testController/serviceRegistry'; -import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from '../debugger/extension/configuration/resolvers/helper'; +import { + DebugEnvironmentVariablesHelper, + IDebugEnvironmentVariablesService, +} from '../debugger/extension/configuration/resolvers/helper'; import { IDebugConfigurationResolver } from '../debugger/extension/configuration/types'; import { LaunchConfigurationResolver } from '../debugger/extension/configuration/resolvers/launch'; import { LaunchRequestArguments } from '../debugger/types'; @@ -44,7 +47,7 @@ export function registerTypes(serviceManager: IServiceManager) { TestConfigurationManagerFactory, ); serviceManager.addSingleton(IExtensionActivationService, UnitTestManagementService); - + serviceManager.addSingleton( IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper, @@ -54,6 +57,6 @@ export function registerTypes(serviceManager: IServiceManager) { LaunchConfigurationResolver, 'launch', ); - + registerTestControllerTypes(serviceManager); } diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 8fc8993e83ce..5557415f923c 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -28,4 +28,4 @@ suite('Debugging - Service Registry', () => { ), ).once(); }); -}); \ No newline at end of file +}); From b4377e19cc2c3c3185b8d51b884b731bffe4d1ba Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 23:00:30 -0500 Subject: [PATCH 15/24] fix lint --- src/client/debugger/extension/configuration/resolvers/base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index 24f45c786d69..ac182ebc0ef0 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -16,8 +16,8 @@ import { IInterpreterService } from '../../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTelemetry } from '../../../../telemetry/types'; -import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../../debugger/types'; -import { PythonPathSource } from '../../../../debugger/extension/types'; +import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; +import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; import { resolveVariables } from '../utils/common'; import { getProgram } from './helper'; From 852ded9f801fb08bf497541bcd21543cff6dee38 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 20 Nov 2023 23:05:23 -0500 Subject: [PATCH 16/24] remove extra registry --- src/client/testing/common/debugLauncher.ts | 1 + src/client/testing/serviceRegistry.ts | 17 ----------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 37617e7c8a74..0d132120c904 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -186,6 +186,7 @@ export class DebugLauncher implements ITestDebugLauncher { configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. + let launchArgs = await this.launchResolver.resolveDebugConfiguration( workspaceFolder, configArgs, diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 7c013e6e433d..6a7b4b5a1640 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -22,13 +22,6 @@ import { TestingService, UnitTestManagementService } from './main'; import { ITestingService } from './types'; import { UnitTestSocketServer } from './common/socketServer'; import { registerTestControllerTypes } from './testController/serviceRegistry'; -import { - DebugEnvironmentVariablesHelper, - IDebugEnvironmentVariablesService, -} from '../debugger/extension/configuration/resolvers/helper'; -import { IDebugConfigurationResolver } from '../debugger/extension/configuration/types'; -import { LaunchConfigurationResolver } from '../debugger/extension/configuration/resolvers/launch'; -import { LaunchRequestArguments } from '../debugger/types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ITestDebugLauncher, DebugLauncher); @@ -48,15 +41,5 @@ export function registerTypes(serviceManager: IServiceManager) { ); serviceManager.addSingleton(IExtensionActivationService, UnitTestManagementService); - serviceManager.addSingleton( - IDebugEnvironmentVariablesService, - DebugEnvironmentVariablesHelper, - ); - serviceManager.addSingleton>( - IDebugConfigurationResolver, - LaunchConfigurationResolver, - 'launch', - ); - registerTestControllerTypes(serviceManager); } From e9bd343a6b266db28a3d856acacf6a11a7828db4 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 02:38:10 -0500 Subject: [PATCH 17/24] revert launch and attach files --- package.json | 367 ++++++++++++++++++ .../checks/invalidLaunchJsonDebugger.ts | 206 ++++++++++ src/client/common/application/debugService.ts | 7 + .../application/debugSessionTelemetry.ts | 80 ++++ src/client/common/application/types.ts | 12 + src/client/debugger/constants.ts | 2 +- .../debugger/extension/adapter/activator.ts | 53 +++ .../debugger/extension/adapter/factory.ts | 208 ++++++++++ .../debugger/extension/adapter/logging.ts | 77 ++++ .../adapter/outdatedDebuggerPrompt.ts | 83 ++++ .../debugger/extension/adapter/types.ts | 10 + .../extension/attachQuickPick/factory.ts | 36 ++ .../extension/attachQuickPick/picker.ts | 82 ++++ .../extension/attachQuickPick/provider.ts | 82 ++++ .../attachQuickPick/psProcessParser.ts | 101 +++++ .../extension/attachQuickPick/types.ts | 29 ++ .../attachQuickPick/wmicProcessParser.ts | 82 ++++ .../configuration/resolvers/attach.ts | 124 ++++++ .../extension/configuration/resolvers/base.ts | 10 +- .../debugger/extension/debugCommands.ts | 73 ++++ .../hooks/childProcessAttachHandler.ts | 48 +++ .../hooks/childProcessAttachService.ts | 56 +++ .../debugger/extension/hooks/constants.ts | 10 + .../extension/hooks/eventHandlerDispatcher.ts | 33 ++ src/client/debugger/extension/hooks/types.ts | 18 + .../debugger/extension/serviceRegistry.ts | 32 ++ src/client/debugger/extension/types.ts | 13 + src/client/debugger/types.ts | 28 ++ src/client/extensionActivation.ts | 7 +- src/client/telemetry/constants.ts | 7 + src/client/telemetry/index.ts | 153 ++++++++ src/client/testing/common/debugLauncher.ts | 2 +- 32 files changed, 2124 insertions(+), 7 deletions(-) create mode 100644 src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts create mode 100644 src/client/common/application/debugSessionTelemetry.ts create mode 100644 src/client/debugger/extension/adapter/activator.ts create mode 100644 src/client/debugger/extension/adapter/factory.ts create mode 100644 src/client/debugger/extension/adapter/logging.ts create mode 100644 src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts create mode 100644 src/client/debugger/extension/adapter/types.ts create mode 100644 src/client/debugger/extension/attachQuickPick/factory.ts create mode 100644 src/client/debugger/extension/attachQuickPick/picker.ts create mode 100644 src/client/debugger/extension/attachQuickPick/provider.ts create mode 100644 src/client/debugger/extension/attachQuickPick/psProcessParser.ts create mode 100644 src/client/debugger/extension/attachQuickPick/types.ts create mode 100644 src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts create mode 100644 src/client/debugger/extension/configuration/resolvers/attach.ts create mode 100644 src/client/debugger/extension/debugCommands.ts create mode 100644 src/client/debugger/extension/hooks/childProcessAttachHandler.ts create mode 100644 src/client/debugger/extension/hooks/childProcessAttachService.ts create mode 100644 src/client/debugger/extension/hooks/constants.ts create mode 100644 src/client/debugger/extension/hooks/eventHandlerDispatcher.ts create mode 100644 src/client/debugger/extension/hooks/types.ts diff --git a/package.json b/package.json index fe050dee230c..a00c68f077a8 100644 --- a/package.json +++ b/package.json @@ -786,6 +786,373 @@ "title": "Python", "type": "object" }, + "debuggers": [ + { + "configurationAttributes": { + "attach": { + "properties": { + "connect": { + "label": "Attach by connecting to debugpy over a socket.", + "properties": { + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address to connect to.", + "type": "string" + }, + "port": { + "description": "Port to connect to.", + "type": "number" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" + }, + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" + }, + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address to connect to.", + "type": "string" + }, + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] + }, + "justMyCode": { + "default": true, + "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.", + "type": "boolean" + }, + "listen": { + "label": "Attach by listening for incoming socket connection from debugpy", + "properties": { + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address of the interface to listen on.", + "type": "string" + }, + "port": { + "description": "Port to listen on.", + "type": "number" + } + }, + "required": [ + "port" + ], + "type": "object" + }, + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" + }, + "pathMappings": { + "default": [], + "items": { + "label": "Path mapping", + "properties": { + "localRoot": { + "default": "${workspaceFolder}", + "label": "Local source root.", + "type": "string" + }, + "remoteRoot": { + "default": "", + "label": "Remote source root.", + "type": "string" + } + }, + "required": [ + "localRoot", + "remoteRoot" + ], + "type": "object" + }, + "label": "Path mappings.", + "type": "array" + }, + "port": { + "description": "Port to connect to.", + "type": "number" + }, + "processId": { + "anyOf": [ + { + "default": "${command:pickProcess}", + "description": "Use process picker to select a process to attach, or Process ID as integer.", + "enum": [ + "${command:pickProcess}" + ] + }, + { + "description": "ID of the local process to attach to.", + "type": "integer" + } + ] + }, + "redirectOutput": { + "default": true, + "description": "Redirect output.", + "type": "boolean" + }, + "showReturnValue": { + "default": true, + "description": "Show return value of functions when stepping.", + "type": "boolean" + }, + "subProcess": { + "default": false, + "description": "Whether to enable Sub Process debugging", + "type": "boolean" + } + } + }, + "launch": { + "properties": { + "args": { + "default": [], + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "type": [ + "array", + "string" + ] + }, + "autoReload": { + "default": {}, + "description": "Configures automatic reload of code on edit.", + "properties": { + "enable": { + "default": false, + "description": "Automatically reload code on edit.", + "type": "boolean" + }, + "exclude": { + "default": [ + "**/.git/**", + "**/.metadata/**", + "**/__pycache__/**", + "**/node_modules/**", + "**/site-packages/**" + ], + "description": "Glob patterns of paths to exclude from auto reload.", + "items": { + "type": "string" + }, + "type": "array" + }, + "include": { + "default": [ + "**/*.py", + "**/*.pyw" + ], + "description": "Glob patterns of paths to include in auto reload.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "console": { + "default": "integratedTerminal", + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "enum": [ + "externalTerminal", + "integratedTerminal", + "internalConsole" + ] + }, + "consoleTitle": { + "default": "Python Debug Console", + "description": "Display name of the debug console or terminal" + }, + "cwd": { + "default": "${workspaceFolder}", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "type": "string" + }, + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" + }, + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "default": {}, + "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", + "type": "object" + }, + "envFile": { + "default": "${workspaceFolder}/.env", + "description": "Absolute path to a file containing environment variable definitions.", + "type": "string" + }, + "gevent": { + "default": false, + "description": "Enable debugging of gevent monkey-patched code.", + "type": "boolean" + }, + "host": { + "default": "localhost", + "description": "IP address of the of the local debug server (default is localhost).", + "type": "string" + }, + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] + }, + "justMyCode": { + "default": true, + "description": "Debug only user-written code.", + "type": "boolean" + }, + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" + }, + "module": { + "default": "", + "description": "Name of the module to be debugged.", + "type": "string" + }, + "pathMappings": { + "default": [], + "items": { + "label": "Path mapping", + "properties": { + "localRoot": { + "default": "${workspaceFolder}", + "label": "Local source root.", + "type": "string" + }, + "remoteRoot": { + "default": "", + "label": "Remote source root.", + "type": "string" + } + }, + "required": [ + "localRoot", + "remoteRoot" + ], + "type": "object" + }, + "label": "Path mappings.", + "type": "array" + }, + "port": { + "default": 0, + "description": "Debug port (default is 0, resulting in the use of a dynamic port).", + "type": "number" + }, + "program": { + "default": "${file}", + "description": "Absolute path to the program.", + "type": "string" + }, + "purpose": { + "default": [], + "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.", + "items": { + "enum": [ + "debug-test", + "debug-in-terminal" + ], + "enumDescriptions": [ + "Use this configuration while debugging tests using test view or test debug commands.", + "Use this configuration while debugging a file using debug in terminal button in the editor." + ] + }, + "type": "array" + }, + "pyramid": { + "default": false, + "description": "Whether debugging Pyramid applications", + "type": "boolean" + }, + "python": { + "default": "${command:python.interpreterPath}", + "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.", + "type": "string" + }, + "pythonArgs": { + "default": [], + "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".", + "items": { + "type": "string" + }, + "type": "array" + }, + "redirectOutput": { + "default": true, + "description": "Redirect output.", + "type": "boolean" + }, + "showReturnValue": { + "default": true, + "description": "Show return value of functions when stepping.", + "type": "boolean" + }, + "stopOnEntry": { + "default": false, + "description": "Automatically stop after launch.", + "type": "boolean" + }, + "subProcess": { + "default": false, + "description": "Whether to enable Sub Process debugging", + "type": "boolean" + }, + "sudo": { + "default": false, + "description": "Running debug program under elevated permissions (on Unix).", + "type": "boolean" + } + } + } + }, + "configurationSnippets": [], + "label": "Python", + "languages": [ + "python" + ], + "type": "python", + "variables": { + "pickProcess": "python.`pickLocalProcess`" + }, + "when": "!virtualWorkspace && shellExecutionSupported" + } + ], "grammars": [ { "language": "pip-requirements", diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts new file mode 100644 index 000000000000..9b0038d9ecd0 --- /dev/null +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; +import { IFileSystem } from '../../../common/platform/types'; +import { IDisposableRegistry, Resource } from '../../../common/types'; +import { Common, Diagnostics } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +const messages = { + [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic, + [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic, + [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic, + [DiagnosticCodes.ConfigPythonPathDiagnostic]: '', +}; + +export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { + constructor( + code: + | DiagnosticCodes.InvalidDebuggerTypeDiagnostic + | DiagnosticCodes.JustMyCodeDiagnostic + | DiagnosticCodes.ConsoleTypeDiagnostic + | DiagnosticCodes.ConfigPythonPathDiagnostic, + resource: Resource, + shouldShowPrompt = true, + ) { + super( + code, + messages[code], + DiagnosticSeverity.Error, + DiagnosticScope.WorkspaceFolder, + resource, + shouldShowPrompt, + ); + } +} + +export const InvalidLaunchJsonDebuggerServiceId = 'InvalidLaunchJsonDebuggerServiceId'; + +@injectable() +export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + private readonly messageService: IDiagnosticHandlerService, + ) { + super( + [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic, + DiagnosticCodes.ConfigPythonPathDiagnostic, + ], + serviceContainer, + disposableRegistry, + true, + ); + } + + public async diagnose(resource: Resource): Promise { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + return []; + } + const workspaceFolder = resource + ? this.workspaceService.getWorkspaceFolder(resource)! + : this.workspaceService.workspaceFolders![0]; + return this.diagnoseWorkspace(workspaceFolder, resource); + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); + } + + protected async fixLaunchJson(code: DiagnosticCodes): Promise { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + return; + } + + await Promise.all( + (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => + this.fixLaunchJsonInWorkspace(code, workspaceFolder), + ), + ); + } + + private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { + const launchJson = getLaunchJsonFile(workspaceFolder); + if (!(await this.fs.fileExists(launchJson))) { + return []; + } + + const fileContents = await this.fs.readFile(launchJson); + const diagnostics: IDiagnostic[] = []; + if (fileContents.indexOf('"pythonExperimental"') > 0) { + diagnostics.push( + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), + ); + } + if (fileContents.indexOf('"debugStdLib"') > 0) { + diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, resource)); + } + if (fileContents.indexOf('"console": "none"') > 0) { + diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource)); + } + if ( + fileContents.indexOf('"pythonPath":') > 0 || + fileContents.indexOf('{config:python.pythonPath}') > 0 || + fileContents.indexOf('{config:python.interpreterPath}') > 0 + ) { + diagnostics.push( + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false), + ); + } + return diagnostics; + } + + private async handleDiagnostic(diagnostic: IDiagnostic): Promise { + if (!diagnostic.shouldShowPrompt) { + await this.fixLaunchJson(diagnostic.code); + return; + } + const commandPrompts = [ + { + prompt: Diagnostics.yesUpdateLaunch, + command: { + diagnostic, + invoke: async (): Promise => { + await this.fixLaunchJson(diagnostic.code); + }, + }, + }, + { + prompt: Common.noIWillDoItLater, + }, + ]; + + await this.messageService.handle(diagnostic, { commandPrompts }); + } + + private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { + if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { + return; + } + const launchJson = getLaunchJsonFile(workspaceFolder); + let fileContents = await this.fs.readFile(launchJson); + switch (code) { + case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); + fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); + break; + } + case DiagnosticCodes.JustMyCodeDiagnostic: { + fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); + fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); + break; + } + case DiagnosticCodes.ConsoleTypeDiagnostic: { + fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); + break; + } + case DiagnosticCodes.ConfigPythonPathDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); + fileContents = findAndReplace( + fileContents, + '{config:python.pythonPath}', + '{command:python.interpreterPath}', + ); + fileContents = findAndReplace( + fileContents, + '{config:python.interpreterPath}', + '{command:python.interpreterPath}', + ); + break; + } + default: { + return; + } + } + + await this.fs.writeFile(launchJson, fileContents); + } +} + +function findAndReplace(fileContents: string, search: string, replace: string) { + const searchRegex = new RegExp(search, 'g'); + return fileContents.replace(searchRegex, replace); +} + +function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { + return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); +} diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts index 32895ed3f0a8..d98262d88926 100644 --- a/src/client/common/application/debugService.ts +++ b/src/client/common/application/debugService.ts @@ -8,6 +8,7 @@ import { Breakpoint, BreakpointsChangeEvent, debug, + DebugAdapterDescriptorFactory, DebugConfiguration, DebugConsole, DebugSession, @@ -66,4 +67,10 @@ export class DebugService implements IDebugService { public removeBreakpoints(breakpoints: Breakpoint[]): void { debug.removeBreakpoints(breakpoints); } + public registerDebugAdapterDescriptorFactory( + debugType: string, + factory: DebugAdapterDescriptorFactory, + ): Disposable { + return debug.registerDebugAdapterDescriptorFactory(debugType, factory); + } } diff --git a/src/client/common/application/debugSessionTelemetry.ts b/src/client/common/application/debugSessionTelemetry.ts new file mode 100644 index 000000000000..6117c3eb0fab --- /dev/null +++ b/src/client/common/application/debugSessionTelemetry.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IExtensionSingleActivationService } from '../../activation/types'; +import { AttachRequestArguments, ConsoleType, LaunchRequestArguments, TriggerType } from '../../debugger/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IDisposableRegistry } from '../types'; +import { StopWatch } from '../utils/stopWatch'; +import { IDebugService } from './types'; + +function isResponse(a: any): a is DebugProtocol.Response { + return a.type === 'response'; +} +class TelemetryTracker implements DebugAdapterTracker { + private timer = new StopWatch(); + private readonly trigger: TriggerType = 'launch'; + private readonly console: ConsoleType | undefined; + + constructor(session: DebugSession) { + this.trigger = session.configuration.request as TriggerType; + const debugConfiguration = session.configuration as Partial; + this.console = debugConfiguration.console; + } + + public onWillStartSession() { + this.sendTelemetry(EventName.DEBUG_SESSION_START); + } + + public onDidSendMessage(message: any): void { + if (isResponse(message)) { + if (message.command === 'configurationDone') { + // "configurationDone" response is sent immediately after user code starts running. + this.sendTelemetry(EventName.DEBUG_SESSION_USER_CODE_RUNNING); + } + } + } + + public onWillStopSession(): void { + this.sendTelemetry(EventName.DEBUG_SESSION_STOP); + } + + public onError?(_error: Error): void { + this.sendTelemetry(EventName.DEBUG_SESSION_ERROR); + } + + private sendTelemetry(eventName: EventName): void { + if (eventName === EventName.DEBUG_SESSION_START) { + this.timer.reset(); + } + const telemetryProps = { + trigger: this.trigger, + console: this.console, + }; + sendTelemetryEvent(eventName, this.timer.elapsedTime, telemetryProps); + } +} + +@injectable() +export class DebugSessionTelemetry implements DebugAdapterTrackerFactory, IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + constructor( + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IDebugService) debugService: IDebugService, + ) { + disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); + } + + public async activate(): Promise { + // We actually register in the constructor. Not necessary to do it here + } + + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + return new TelemetryTracker(session); + } +} \ No newline at end of file diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 2a6950dd1eb2..6705331bf57d 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -9,6 +9,7 @@ import { CancellationToken, CompletionItemProvider, ConfigurationChangeEvent, + DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfiguration, DebugConfigurationProvider, @@ -994,6 +995,17 @@ export interface IDebugService { */ registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable; + /** + * Register a [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) for a specific debug type. + * An extension is only allowed to register a DebugAdapterDescriptorFactory for the debug type(s) defined by the extension. Otherwise an error is thrown. + * Registering more than one DebugAdapterDescriptorFactory for a debug type results in an error. + * + * @param debugType The debug type for which the factory is registered. + * @param factory The [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) to register. + * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + */ + registerDebugAdapterDescriptorFactory(debugType: string, factory: DebugAdapterDescriptorFactory): Disposable; + /** * Register a debug adapter tracker factory for the given debug type. * diff --git a/src/client/debugger/constants.ts b/src/client/debugger/constants.ts index 0746de4e23cd..08b8619ce03a 100644 --- a/src/client/debugger/constants.ts +++ b/src/client/debugger/constants.ts @@ -3,4 +3,4 @@ 'use strict'; -export const DebuggerTypeName = 'debugpy'; +export const DebuggerTypeName = 'python'; diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts new file mode 100644 index 000000000000..82750fc6f204 --- /dev/null +++ b/src/client/debugger/extension/adapter/activator.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import { Uri } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { IDebugService } from '../../../common/application/types'; +import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; +import { ICommandManager } from '../../../common/application/types'; +import { DebuggerTypeName } from '../../constants'; +import { IAttachProcessProviderFactory } from '../attachQuickPick/types'; +import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from '../types'; + +@injectable() +export class DebugAdapterActivator implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + constructor( + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IConfigurationService) private readonly configSettings: IConfigurationService, + @inject(ICommandManager) private commandManager: ICommandManager, + @inject(IDebugAdapterDescriptorFactory) private descriptorFactory: IDebugAdapterDescriptorFactory, + @inject(IDebugSessionLoggingFactory) private debugSessionLoggingFactory: IDebugSessionLoggingFactory, + @inject(IOutdatedDebuggerPromptFactory) private debuggerPromptFactory: IOutdatedDebuggerPromptFactory, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IAttachProcessProviderFactory) + private readonly attachProcessProviderFactory: IAttachProcessProviderFactory, + ) {} + public async activate(): Promise { + this.attachProcessProviderFactory.registerCommands(); + + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory), + ); + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory), + ); + + this.disposables.push( + this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory), + ); + this.disposables.push( + this.debugService.onDidStartDebugSession((debugSession) => { + if (this.shouldTerminalFocusOnStart(debugSession.workspaceFolder?.uri)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }), + ); + } + + private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { + return this.configSettings.getSettings(uri)?.terminal.focusAfterLaunch; + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts new file mode 100644 index 000000000000..c5cfdfb4cb21 --- /dev/null +++ b/src/client/debugger/extension/adapter/factory.ts @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterDescriptor, + DebugAdapterExecutable, + DebugAdapterServer, + DebugSession, + l10n, + WorkspaceFolder, +} from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { traceLog, traceVerbose } from '../../../logging'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugAdapterDescriptorFactory } from '../types'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { IPersistentStateFactory } from '../../../common/types'; +import { Commands } from '../../../common/constants'; +import { ICommandManager } from '../../../common/application/types'; + +// persistent state names, exported to make use of in testing +export enum debugStateKeys { + doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain', +} + +@injectable() +export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory { + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + ) {} + + public async createDebugAdapterDescriptor( + session: DebugSession, + _executable: DebugAdapterExecutable | undefined, + ): Promise { + const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments; + + // There are four distinct scenarios here: + // + // 1. "launch"; + // 2. "attach" with "processId"; + // 3. "attach" with "listen"; + // 4. "attach" with "connect" (or legacy "host"/"port"); + // + // For the first three, we want to spawn the debug adapter directly. + // For the last one, the adapter is already listening on the specified socket. + // When "debugServer" is used, the standard adapter factory takes care of it - no need to check here. + + if (configuration.request === 'attach') { + if (configuration.connect !== undefined) { + traceLog( + `Connecting to DAP Server at: ${configuration.connect.host ?? '127.0.0.1'}:${ + configuration.connect.port + }`, + ); + return new DebugAdapterServer(configuration.connect.port, configuration.connect.host ?? '127.0.0.1'); + } else if (configuration.port !== undefined) { + traceLog(`Connecting to DAP Server at: ${configuration.host ?? '127.0.0.1'}:${configuration.port}`); + return new DebugAdapterServer(configuration.port, configuration.host ?? '127.0.0.1'); + } else if (configuration.listen === undefined && configuration.processId === undefined) { + throw new Error('"request":"attach" requires either "connect", "listen", or "processId"'); + } + } + + const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder); + if (command.length !== 0) { + if (configuration.request === 'attach' && configuration.processId !== undefined) { + sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); + } + + const executable = command.shift() ?? 'python'; + + // "logToFile" is not handled directly by the adapter - instead, we need to pass + // the corresponding CLI switch when spawning it. + const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : []; + + if (configuration.debugAdapterPath !== undefined) { + const args = command.concat([configuration.debugAdapterPath, ...logArgs]); + traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); + return new DebugAdapterExecutable(executable, args); + } + + const debuggerAdapterPathToUse = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'lib', + 'python', + 'debugpy', + 'adapter', + ); + + const args = command.concat([debuggerAdapterPathToUse, ...logArgs]); + traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); + sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true }); + return new DebugAdapterExecutable(executable, args); + } + + // Unlikely scenario. + throw new Error('Debug Adapter Executable not provided'); + } + + /** + * Get the python executable used to launch the Python Debug Adapter. + * In the case of `attach` scenarios, just use the workspace interpreter, else first available one. + * It is unlike user won't have a Python interpreter + * + * @private + * @param {(LaunchRequestArguments | AttachRequestArguments)} configuration + * @param {WorkspaceFolder} [workspaceFolder] + * @returns {Promise} Path to the python interpreter for this workspace. + * @memberof DebugAdapterDescriptorFactory + */ + private async getDebugAdapterPython( + configuration: LaunchRequestArguments | AttachRequestArguments, + workspaceFolder?: WorkspaceFolder, + ): Promise { + if (configuration.debugAdapterPython !== undefined) { + return this.getExecutableCommand( + await this.interpreterService.getInterpreterDetails(configuration.debugAdapterPython), + ); + } else if (configuration.pythonPath) { + return this.getExecutableCommand( + await this.interpreterService.getInterpreterDetails(configuration.pythonPath), + ); + } + + const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined; + const interpreter = await this.interpreterService.getActiveInterpreter(resourceUri); + if (interpreter) { + traceVerbose(`Selecting active interpreter as Python Executable for DA '${interpreter.path}'`); + return this.getExecutableCommand(interpreter); + } + + await this.interpreterService.hasInterpreters(); // Wait until we know whether we have an interpreter + const interpreters = this.interpreterService.getInterpreters(resourceUri); + if (interpreters.length === 0) { + this.notifySelectInterpreter().ignoreErrors(); + return []; + } + + traceVerbose(`Picking first available interpreter to launch the DA '${interpreters[0].path}'`); + return this.getExecutableCommand(interpreters[0]); + } + + private async showDeprecatedPythonMessage() { + const notificationPromptEnabled = this.persistentState.createGlobalPersistentState( + debugStateKeys.doNotShowAgain, + false, + ); + if (notificationPromptEnabled.value) { + return; + } + const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain]; + const selection = await showErrorMessage( + l10n.t('The debugger in the python extension no longer supports python versions minor than 3.7.'), + { modal: true }, + ...prompts, + ); + if (!selection) { + return; + } + if (selection == Interpreters.changePythonInterpreter) { + await this.commandManager.executeCommand(Commands.Set_Interpreter); + } + if (selection == Common.doNotShowAgain) { + // Never show the message again + await this.persistentState + .createGlobalPersistentState(debugStateKeys.doNotShowAgain, false) + .updateValue(true); + } + } + + private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { + if (interpreter) { + if ( + (interpreter.version?.major ?? 0) < 3 || + ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) <= 6) + ) { + this.showDeprecatedPythonMessage(); + } + return interpreter.path.length > 0 ? [interpreter.path] : []; + } + return []; + } + + /** + * Notify user about the requirement for Python. + * Unlikely scenario, as ex expect users to have Python in order to use the extension. + * However it is possible to ignore the warnings and continue using the extension. + * + * @private + * @memberof DebugAdapterDescriptorFactory + */ + private async notifySelectInterpreter() { + await showErrorMessage(l10n.t('Install Python or select a Python Interpreter to use the debugger.')); + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/adapter/logging.ts b/src/client/debugger/extension/adapter/logging.ts new file mode 100644 index 000000000000..9ab5fcf70113 --- /dev/null +++ b/src/client/debugger/extension/adapter/logging.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugSession, + ProviderResult, +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IFileSystem, WriteStream } from '../../../common/platform/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; + +class DebugSessionLoggingTracker implements DebugAdapterTracker { + private readonly enabled: boolean = false; + private stream?: WriteStream; + private timer = new StopWatch(); + + constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { + this.enabled = this.session.configuration.logToFile as boolean; + if (this.enabled) { + const fileName = `debugger.vscode_${this.session.id}.log`; + this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); + } + } + + public onWillStartSession() { + this.timer.reset(); + this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); + } + + public onWillReceiveMessage(message: DebugProtocol.Message) { + this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); + } + + public onDidSendMessage(message: DebugProtocol.Message) { + this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); + } + + public onWillStopSession() { + this.log('Stopping Session\n'); + } + + public onError(error: Error) { + this.log(`Error:\n${this.stringify(error)}\n`); + } + + public onExit(code: number | undefined, signal: string | undefined) { + this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); + this.stream?.close(); + } + + private log(message: string) { + if (this.enabled) { + this.stream!.write(`${this.timer.elapsedTime} ${message}`); // NOSONAR + } + } + + private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { + return JSON.stringify(data, null, 4); + } +} + +@injectable() +export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { + constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) {} + + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + return new DebugSessionLoggingTracker(session, this.fileSystem); + } +} diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts new file mode 100644 index 000000000000..c86f3a9ef206 --- /dev/null +++ b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IApplicationShell } from '../../../common/application/types'; +import { IBrowserService } from '../../../common/types'; +import { Common, OutdatedDebugger } from '../../../common/utils/localize'; +import { IPromptShowState } from './types'; + +// This situation occurs when user connects to old containers or server where +// the debugger they had installed was ptvsd. We should show a prompt to ask them to update. +class OutdatedDebuggerPrompt implements DebugAdapterTracker { + constructor( + private promptCheck: IPromptShowState, + private appShell: IApplicationShell, + private browserService: IBrowserService, + ) {} + + public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { + if (this.promptCheck.shouldShowPrompt() && this.isPtvsd(message)) { + const prompts = [Common.moreInfo]; + this.appShell + .showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts) + .then((selection) => { + if (selection === prompts[0]) { + this.browserService.launch('https://aka.ms/migrateToDebugpy'); + } + }); + } + } + + private isPtvsd(message: DebugProtocol.ProtocolMessage) { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'output') { + const outputMessage = eventMessage as DebugProtocol.OutputEvent; + if (outputMessage.body.category === 'telemetry') { + // debugpy sends telemetry as both ptvsd and debugpy. This was done to help with + // transition from ptvsd to debugpy while analyzing usage telemetry. + if ( + outputMessage.body.output === 'ptvsd' && + !outputMessage.body.data.packageVersion.startsWith('1') + ) { + this.promptCheck.setShowPrompt(false); + return true; + } + if (outputMessage.body.output === 'debugpy') { + this.promptCheck.setShowPrompt(false); + } + } + } + } + return false; + } +} + +class OutdatedDebuggerPromptState implements IPromptShowState { + private shouldShow: boolean = true; + public shouldShowPrompt(): boolean { + return this.shouldShow; + } + public setShowPrompt(show: boolean) { + this.shouldShow = show; + } +} + +@injectable() +export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { + private readonly promptCheck: OutdatedDebuggerPromptState; + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IBrowserService) private browserService: IBrowserService, + ) { + this.promptCheck = new OutdatedDebuggerPromptState(); + } + public createDebugAdapterTracker(_session: DebugSession): ProviderResult { + return new OutdatedDebuggerPrompt(this.promptCheck, this.appShell, this.browserService); + } +} diff --git a/src/client/debugger/extension/adapter/types.ts b/src/client/debugger/extension/adapter/types.ts new file mode 100644 index 000000000000..099260376ddb --- /dev/null +++ b/src/client/debugger/extension/adapter/types.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const IPromptShowState = Symbol('IPromptShowState'); +export interface IPromptShowState { + shouldShowPrompt(): boolean; + setShowPrompt(show: boolean): void; +} \ No newline at end of file diff --git a/src/client/debugger/extension/attachQuickPick/factory.ts b/src/client/debugger/extension/attachQuickPick/factory.ts new file mode 100644 index 000000000000..90e226e587db --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/factory.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IApplicationShell, ICommandManager } from '../../../common/application/types'; +import { Commands } from '../../../common/constants'; +import { IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { AttachPicker } from './picker'; +import { AttachProcessProvider } from './provider'; +import { IAttachProcessProviderFactory } from './types'; + +@injectable() +export class AttachProcessProviderFactory implements IAttachProcessProviderFactory { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + ) {} + + public registerCommands() { + const provider = new AttachProcessProvider(this.platformService, this.processServiceFactory); + const picker = new AttachPicker(this.applicationShell, provider); + const disposable = this.commandManager.registerCommand( + Commands.PickLocalProcess, + () => picker.showQuickPick(), + this, + ); + this.disposableRegistry.push(disposable); + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/attachQuickPick/picker.ts b/src/client/debugger/extension/attachQuickPick/picker.ts new file mode 100644 index 000000000000..2107e6d1e244 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/picker.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { getIcon } from '../../../common/utils/icons'; +import { AttachProcess } from '../../../common/utils/localize'; +import { IAttachItem, IAttachPicker, IAttachProcessProvider, REFRESH_BUTTON_ICON } from './types'; + +@injectable() +export class AttachPicker implements IAttachPicker { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + private readonly attachItemsProvider: IAttachProcessProvider, + ) {} + + public showQuickPick(): Promise { + return new Promise(async (resolve, reject) => { + const processEntries = await this.attachItemsProvider.getAttachItems(); + + const refreshButton = { + iconPath: getIcon(REFRESH_BUTTON_ICON), + tooltip: AttachProcess.refreshList, + }; + + const quickPick = this.applicationShell.createQuickPick(); + quickPick.title = AttachProcess.attachTitle; + quickPick.placeholder = AttachProcess.selectProcessPlaceholder; + quickPick.canSelectMany = false; + quickPick.matchOnDescription = true; + quickPick.matchOnDetail = true; + quickPick.items = processEntries; + quickPick.buttons = [refreshButton]; + + const disposables: Disposable[] = []; + + quickPick.onDidTriggerButton( + async () => { + quickPick.busy = true; + const attachItems = await this.attachItemsProvider.getAttachItems(); + quickPick.items = attachItems; + quickPick.busy = false; + }, + this, + disposables, + ); + + quickPick.onDidAccept( + () => { + if (quickPick.selectedItems.length !== 1) { + reject(new Error(AttachProcess.noProcessSelected)); + } + + const selectedId = quickPick.selectedItems[0].id; + + disposables.forEach((item) => item.dispose()); + quickPick.dispose(); + + resolve(selectedId); + }, + undefined, + disposables, + ); + + quickPick.onDidHide( + () => { + disposables.forEach((item) => item.dispose()); + quickPick.dispose(); + + reject(new Error(AttachProcess.noProcessSelected)); + }, + undefined, + disposables, + ); + + quickPick.show(); + }); + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/attachQuickPick/provider.ts b/src/client/debugger/extension/attachQuickPick/provider.ts new file mode 100644 index 000000000000..d7da60a8c8de --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/provider.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { l10n } from 'vscode'; +import { IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { PsProcessParser } from './psProcessParser'; +import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; +import { WmicProcessParser } from './wmicProcessParser'; + +@injectable() +export class AttachProcessProvider implements IAttachProcessProvider { + constructor( + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + ) {} + + public getAttachItems(): Promise { + return this._getInternalProcessEntries().then((processEntries) => { + processEntries.sort( + ( + { processName: aprocessName, commandLine: aCommandLine }, + { processName: bProcessName, commandLine: bCommandLine }, + ) => { + const compare = (aString: string, bString: string): number => { + // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) + // We can change to localeCompare if this becomes an issue + const aLower = aString.toLowerCase(); + const bLower = bString.toLowerCase(); + + if (aLower === bLower) { + return 0; + } + + return aLower < bLower ? -1 : 1; + }; + + const aPython = aprocessName.startsWith('python'); + const bPython = bProcessName.startsWith('python'); + + if (aPython || bPython) { + if (aPython && !bPython) { + return -1; + } + if (bPython && !aPython) { + return 1; + } + + return aPython ? compare(aCommandLine!, bCommandLine!) : compare(bCommandLine!, aCommandLine!); + } + + return compare(aprocessName, bProcessName); + }, + ); + + return processEntries; + }); + } + + public async _getInternalProcessEntries(): Promise { + let processCmd: ProcessListCommand; + if (this.platformService.isMac) { + processCmd = PsProcessParser.psDarwinCommand; + } else if (this.platformService.isLinux) { + processCmd = PsProcessParser.psLinuxCommand; + } else if (this.platformService.isWindows) { + processCmd = WmicProcessParser.wmicCommand; + } else { + throw new Error(l10n.t("Operating system '{0}' not supported.", this.platformService.osType)); + } + + const processService = await this.processServiceFactory.create(); + const output = await processService.exec(processCmd.command, processCmd.args, { throwOnStdErr: true }); + + return this.platformService.isWindows + ? WmicProcessParser.parseProcesses(output.stdout) + : PsProcessParser.parseProcesses(output.stdout); + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts new file mode 100644 index 000000000000..1666f4345f21 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace PsProcessParser { + const secondColumnCharacters = 50; + const commColumnTitle = ''.padStart(secondColumnCharacters, 'a'); + + // Perf numbers: + // OS X 10.10 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 272 | 52 | + // | 296 | 49 | + // | 384 | 53 | + // | 784 | 116 | + // + // Ubuntu 16.04 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 232 | 26 | + // | 336 | 34 | + // | 736 | 62 | + // | 1039 | 115 | + // | 1239 | 182 | + + // ps outputs as a table. With the option "ww", ps will use as much width as necessary. + // However, that only applies to the right-most column. Here we use a hack of setting + // the column header to 50 a's so that the second column will have at least that many + // characters. 50 was chosen because that's the maximum length of a "label" in the + // QuickPick UI in VS Code. + + // the BSD version of ps uses '-c' to have 'comm' only output the executable name and not + // the full path. The Linux version of ps has 'comm' to only display the name of the executable + // Note that comm on Linux systems is truncated to 16 characters: + // https://bugzilla.redhat.com/show_bug.cgi?id=429565 + // Since 'args' contains the full path to the executable, even if truncated, searching will work as desired. + export const psLinuxCommand: ProcessListCommand = { + command: 'ps', + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`], + }; + export const psDarwinCommand: ProcessListCommand = { + command: 'ps', + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c'], + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const lines: string[] = processes.split('\n'); + return parseProcessesFromPsArray(lines); + } + + function parseProcessesFromPsArray(processArray: string[]): IAttachItem[] { + const processEntries: IAttachItem[] = []; + + // lines[0] is the header of the table + for (let i = 1; i < processArray.length; i += 1) { + const line = processArray[i]; + if (!line) { + continue; + } + + const processEntry = parseLineFromPs(line); + if (processEntry) { + processEntries.push(processEntry); + } + } + + return processEntries; + } + + function parseLineFromPs(line: string): IAttachItem | undefined { + // Explanation of the regex: + // - any leading whitespace + // - PID + // - whitespace + // - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character + // for the whitespace separator + // - whitespace + // - args (might be empty) + const psEntry: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.{${secondColumnCharacters - 1}})\\s+(.*)$`); + const matches = psEntry.exec(line); + + if (matches?.length === 4) { + const pid = matches[1].trim(); + const executable = matches[2].trim(); + const cmdline = matches[3].trim(); + + return { + label: executable, + description: pid, + detail: cmdline, + id: pid, + processName: executable, + commandLine: cmdline, + }; + } + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/attachQuickPick/types.ts b/src/client/debugger/extension/attachQuickPick/types.ts new file mode 100644 index 000000000000..eda7d2c575d7 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/types.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem } from 'vscode'; + +export type ProcessListCommand = { command: string; args: string[] }; + +export interface IAttachItem extends QuickPickItem { + id: string; + processName: string; + commandLine: string; +} + +export interface IAttachProcessProvider { + getAttachItems(): Promise; +} + +export const IAttachProcessProviderFactory = Symbol('IAttachProcessProviderFactory'); +export interface IAttachProcessProviderFactory { + registerCommands(): void; +} + +export interface IAttachPicker { + showQuickPick(): Promise; +} + +export const REFRESH_BUTTON_ICON = 'refresh.svg'; \ No newline at end of file diff --git a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts new file mode 100644 index 000000000000..1bd2e644c364 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace WmicProcessParser { + const wmicNameTitle = 'Name'; + const wmicCommandLineTitle = 'CommandLine'; + const wmicPidTitle = 'ProcessId'; + const defaultEmptyEntry: IAttachItem = { + label: '', + description: '', + detail: '', + id: '', + processName: '', + commandLine: '', + }; + + // Perf numbers on Win10: + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 309 | 413 | + // | 407 | 463 | + // | 887 | 746 | + // | 1308 | 1132 | + export const wmicCommand: ProcessListCommand = { + command: 'wmic', + args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list'], + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const lines: string[] = processes.split('\r\n'); + const processEntries: IAttachItem[] = []; + let entry = { ...defaultEmptyEntry }; + + for (const line of lines) { + if (!line.length) { + continue; + } + + parseLineFromWmic(line, entry); + + // Each entry of processes has ProcessId as the last line + if (line.lastIndexOf(wmicPidTitle, 0) === 0) { + processEntries.push(entry); + entry = { ...defaultEmptyEntry }; + } + } + + return processEntries; + } + + function parseLineFromWmic(line: string, item: IAttachItem): IAttachItem { + const splitter = line.indexOf('='); + const currentItem = item; + + if (splitter > 0) { + const key = line.slice(0, splitter).trim(); + let value = line.slice(splitter + 1).trim(); + + if (key === wmicNameTitle) { + currentItem.label = value; + currentItem.processName = value; + } else if (key === wmicPidTitle) { + currentItem.description = value; + currentItem.id = value; + } else if (key === wmicCommandLineTitle) { + const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178 + if (value.lastIndexOf(dosDevicePrefix, 0) === 0) { + value = value.slice(dosDevicePrefix.length); + } + + currentItem.detail = value; + currentItem.commandLine = value; + } + } + + return currentItem; + } +} \ No newline at end of file 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..a1356c3b10d1 --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { getOSType, OSType } from '../../../../common/utils/platform'; +import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; +import { BaseConfigurationResolver } from './base'; + +@injectable() +export class AttachConfigurationResolver extends BaseConfigurationResolver { + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: AttachRequestArguments, + _token?: CancellationToken, + ): Promise { + const workspaceFolder = AttachConfigurationResolver.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, + ); + } + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; + } + return debugConfiguration; + } + + protected async provideAttachDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: AttachRequestArguments, + ): Promise { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + if (!(debugConfiguration.connect || debugConfiguration.listen) && !debugConfiguration.host) { + // Connect and listen cannot be mixed with host property. + debugConfiguration.host = 'localhost'; + } + if (debugConfiguration.justMyCode === undefined) { + // Populate justMyCode using debugStdLib + debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; + } + debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; + // 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.justMyCode) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.django) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.subProcess === true) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.SubProcess); + } + if ( + debugConfiguration.pyramid && + debugOptions.indexOf(DebugOptions.Jinja) === -1 && + debugConfiguration.jinja !== false + ) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + AttachConfigurationResolver.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 = AttachConfigurationResolver.isLocalHost(debugConfiguration.host); + if (getOSType() === OSType.Windows && isLocalHost) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; + } + if (debugConfiguration.showReturnValue) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); + } + + debugConfiguration.pathMappings = this.resolvePathMappings( + debugConfiguration.pathMappings || [], + debugConfiguration.host, + debugConfiguration.localRoot, + debugConfiguration.remoteRoot, + workspaceFolder, + ); + AttachConfigurationResolver.sendTelemetry('attach', debugConfiguration); + } + + // eslint-disable-next-line class-methods-use-this + private resolvePathMappings( + pathMappings: PathMapping[], + host?: string, + localRoot?: string, + remoteRoot?: string, + workspaceFolder?: Uri, + ) { + // This is for backwards compatibility. + if (localRoot && remoteRoot) { + pathMappings.push({ + localRoot, + remoteRoot, + }); + } + // If attaching to local host, then always map local root and remote roots. + if (AttachConfigurationResolver.isLocalHost(host)) { + pathMappings = AttachConfigurationResolver.fixUpPathMappings( + pathMappings, + workspaceFolder ? workspaceFolder.fsPath : '', + ); + } + return pathMappings.length > 0 ? pathMappings : undefined; + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index ac182ebc0ef0..e36e37394b18 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -16,7 +16,7 @@ import { IInterpreterService } from '../../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTelemetry } from '../../../../telemetry/types'; -import { DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; import { resolveVariables } from '../utils/common'; @@ -217,17 +217,19 @@ export abstract class BaseConfigurationResolver return pathMappings; } - protected static isDebuggingFastAPI(debugConfiguration: Partial): boolean { + protected static isDebuggingFastAPI(debugConfiguration: Partial, + ): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } - protected static isDebuggingFlask(debugConfiguration: Partial): boolean { + protected static isDebuggingFlask(debugConfiguration: Partial, + ): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } protected static sendTelemetry( trigger: 'launch' | 'attach' | 'test', - debugConfiguration: Partial, + debugConfiguration: Partial, ): void { const name = debugConfiguration.name || ''; const moduleName = debugConfiguration.module || ''; diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts new file mode 100644 index 000000000000..c01bdf8406c1 --- /dev/null +++ b/src/client/debugger/extension/debugCommands.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IDebugService } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { DebugPurpose, LaunchRequestArguments } from '../types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { noop } from '../../common/utils/misc'; +import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; + +@injectable() +export class DebugCommands implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} + + public activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { + sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); + const interpreter = await this.interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + const config = await DebugCommands.getDebugConfiguration(file); + this.debugService.startDebugging(undefined, config); + }), + ); + return Promise.resolve(); + } + + private static async getDebugConfiguration(uri?: Uri): Promise { + const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); + for (const config of configs) { + if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { + if (!config.program && !config.module && !config.code) { + // This is only needed if people reuse debug-test for debug-in-terminal + config.program = uri?.fsPath ?? '${file}'; + } + // Ensure that the purpose is cleared, this is so we can track if people accidentally + // trigger this via F5 or Start with debugger. + config.purpose = []; + return config; + } + } + return { + name: `Debug ${uri ? path.basename(uri.fsPath) : 'File'}`, + type: 'python', + request: 'launch', + program: uri?.fsPath ?? '${file}', + console: 'integratedTerminal', + }; + } +} diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts new file mode 100644 index 000000000000..22138dda9005 --- /dev/null +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, DebugSessionCustomEvent } from 'vscode'; +import { swallowExceptions } from '../../../common/utils/decorators'; +import { AttachRequestArguments } from '../../types'; +import { DebuggerEvents } from './constants'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; +import { DebuggerTypeName } from '../../constants'; + +/** + * This class is responsible for automatically attaching the debugger to any + * child processes launched. I.e. this is the class responsible for multi-proc debugging. + * @export + * @class ChildProcessAttachEventHandler + * @implements {IDebugSessionEventHandlers} + */ +@injectable() +export class ChildProcessAttachEventHandler implements IDebugSessionEventHandlers { + constructor( + @inject(IChildProcessAttachService) private readonly childProcessAttachService: IChildProcessAttachService, + ) {} + + @swallowExceptions('Handle child process launch') + public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { + if (!event || event.session.configuration.type !== DebuggerTypeName) { + return; + } + + let data: AttachRequestArguments & DebugConfiguration; + if ( + event.event === DebuggerEvents.PtvsdAttachToSubprocess || + event.event === DebuggerEvents.DebugpyAttachToSubprocess + ) { + data = event.body as AttachRequestArguments & DebugConfiguration; + } else { + return; + } + + if (Object.keys(data).length > 0) { + await this.childProcessAttachService.attach(data, event.session); + } + } +} + \ No newline at end of file diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts new file mode 100644 index 000000000000..781d39edc955 --- /dev/null +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IDebugService } from '../../../common/application/types'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; +import { noop } from '../../../common/utils/misc'; +import { captureTelemetry } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { AttachRequestArguments } from '../../types'; +import { IChildProcessAttachService } from './types'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; + +/** + * This class is responsible for attaching the debugger to any + * child processes launched. I.e. this is the class responsible for multi-proc debugging. + * @export + * @class ChildProcessAttachEventHandler + * @implements {IChildProcessAttachService} + */ +@injectable() +export class ChildProcessAttachService implements IChildProcessAttachService { + constructor(@inject(IDebugService) private readonly debugService: IDebugService) {} + + @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) + public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { + const debugConfig: AttachRequestArguments & DebugConfiguration = data; + const folder = this.getRelatedWorkspaceFolder(debugConfig); + const debugSessionOption: DebugSessionOptions = { + parentSession: parentSession, + lifecycleManagedByParent: true, + }; + const launched = await this.debugService.startDebugging(folder, debugConfig, debugSessionOption); + if (!launched) { + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( + noop, + noop, + ); + } + } + + private getRelatedWorkspaceFolder( + config: AttachRequestArguments & DebugConfiguration, + ): WorkspaceFolder | undefined { + const workspaceFolder = config.workspaceFolder; + + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; + if (!hasWorkspaceFolders || !workspaceFolder) { + return; + } + return getWorkspaceFolders()!.find((ws) => ws.uri.fsPath === workspaceFolder); + } +} \ No newline at end of file diff --git a/src/client/debugger/extension/hooks/constants.ts b/src/client/debugger/extension/hooks/constants.ts new file mode 100644 index 000000000000..c69d922e7c0c --- /dev/null +++ b/src/client/debugger/extension/hooks/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export enum DebuggerEvents { + // Event sent by PTVSD when a child process is launched and ready to be attached to for multi-proc debugging. + PtvsdAttachToSubprocess = 'ptvsd_attach', + DebugpyAttachToSubprocess = 'debugpyAttach', +} \ No newline at end of file diff --git a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts new file mode 100644 index 000000000000..7b1dd1516abd --- /dev/null +++ b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, multiInject } from 'inversify'; +import { IDebugService } from '../../../common/application/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { IDebugSessionEventHandlers } from './types'; + +export class DebugSessionEventDispatcher { + constructor( + @multiInject(IDebugSessionEventHandlers) private readonly eventHandlers: IDebugSessionEventHandlers[], + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + public registerEventHandlers() { + this.disposables.push( + this.debugService.onDidReceiveDebugSessionCustomEvent((e) => { + this.eventHandlers.forEach((handler) => + handler.handleCustomEvent ? handler.handleCustomEvent(e).ignoreErrors() : undefined, + ); + }), + ); + this.disposables.push( + this.debugService.onDidTerminateDebugSession((e) => { + this.eventHandlers.forEach((handler) => + handler.handleTerminateEvent ? handler.handleTerminateEvent(e).ignoreErrors() : undefined, + ); + }), + ); + } +} diff --git a/src/client/debugger/extension/hooks/types.ts b/src/client/debugger/extension/hooks/types.ts new file mode 100644 index 000000000000..72865da0e618 --- /dev/null +++ b/src/client/debugger/extension/hooks/types.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DebugConfiguration, DebugSession, DebugSessionCustomEvent } from 'vscode'; +import { AttachRequestArguments } from '../../types'; + +export const IDebugSessionEventHandlers = Symbol('IDebugSessionEventHandlers'); +export interface IDebugSessionEventHandlers { + handleCustomEvent?(e: DebugSessionCustomEvent): Promise; + handleTerminateEvent?(e: DebugSession): Promise; +} + +export const IChildProcessAttachService = Symbol('IChildProcessAttachService'); +export interface IChildProcessAttachService { + attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise; +} \ No newline at end of file diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 3158bdf4c630..a79f8482e50d 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -6,8 +6,22 @@ import { LaunchRequestArguments } from '../types'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; import { IDebugConfigurationResolver } from './configuration/types'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; +import { ChildProcessAttachService } from './hooks/childProcessAttachService'; +import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { DebugAdapterActivator } from './adapter/activator'; +import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from './types'; +import { DebugAdapterDescriptorFactory } from './adapter/factory'; +import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt'; +import { IAttachProcessProviderFactory } from './attachQuickPick/types'; +import { AttachProcessProviderFactory } from './attachQuickPick/factory'; +import { DebugCommands } from './debugCommands'; +import { DebugSessionLoggingFactory } from './adapter/logging'; export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); + serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); serviceManager.addSingleton>( IDebugConfigurationResolver, LaunchConfigurationResolver, @@ -17,4 +31,22 @@ export function registerTypes(serviceManager: IServiceManager): void { IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator, + ); + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ); + serviceManager.addSingleton(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); + serviceManager.addSingleton( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory, + ); + serviceManager.addSingleton( + IAttachProcessProviderFactory, + AttachProcessProviderFactory, + ); + serviceManager.addSingleton(IExtensionSingleActivationService, DebugCommands); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index a9604efad68f..cddbc748ad01 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,6 +3,19 @@ 'use strict'; +import { DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider } from 'vscode'; + +export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); +export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} + +export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory'); + +export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory {} + +export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFactory'); + +export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} + export enum PythonPathSource { launchJson = 'launch.json', settingsJson = 'settings.json', diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index af4e40a4ac59..e7cf53f656c3 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -32,6 +32,11 @@ export type PathMapping = { remoteRoot: string; }; +type Connection = { + host?: string; + port?: number; +}; + export interface IAutomaticCodeReload { enable?: boolean; exclude?: string[]; @@ -99,6 +104,20 @@ interface IKnownLaunchRequestArguments extends ICommonDebugArguments { // Defines where the purpose where the config should be used. purpose?: DebugPurpose[]; } +interface IKnownAttachDebugArguments extends ICommonDebugArguments { + workspaceFolder?: string; + customDebugger?: boolean; + // localRoot and remoteRoot are deprecated (replaced by pathMappings). + localRoot?: string; + remoteRoot?: string; + + // Internal field used to attach to subprocess using python debug adapter + subProcessId?: number; + + processId?: number | string; + connect?: Connection; + listen?: Connection; +} export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, @@ -107,6 +126,15 @@ export interface LaunchRequestArguments type: typeof DebuggerTypeName; } +export interface AttachRequestArguments + extends DebugProtocol.AttachRequestArguments, + IKnownAttachDebugArguments, + DebugConfiguration { + type: typeof DebuggerTypeName; +} + +export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments {} + export type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTerminal'; export type TriggerType = 'launch' | 'attach' | 'test'; diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index af1ef68bc285..2d81cb3b186c 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -4,7 +4,6 @@ 'use strict'; import { languages, window } from 'vscode'; - import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; @@ -46,6 +45,9 @@ import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creat import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; import { logAndNotifyOnLegacySettings } from './logging/settingLogs'; +import { DebugService } from './common/application/debugService'; +import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; +import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -142,6 +144,9 @@ async function activateLegacy(ext: ExtensionState): Promise { const interpreterManager = serviceContainer.get(IInterpreterService); interpreterManager.initialize(); if (!workspaceService.isVirtualWorkspace) { + const handlers = serviceManager.getAll(IDebugSessionEventHandlers); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); const outputChannel = serviceManager.get(ILogOutputChannel); disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index c35b2501ea06..dae41e15d784 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -35,7 +35,14 @@ export enum EventName { EXECUTION_CODE = 'EXECUTION_CODE', EXECUTION_DJANGO = 'EXECUTION_DJANGO', DEBUG_IN_TERMINAL_BUTTON = 'DEBUG.IN_TERMINAL', + DEBUG_ADAPTER_USING_WHEELS_PATH = 'DEBUG_ADAPTER.USING_WHEELS_PATH', + DEBUG_SESSION_ERROR = 'DEBUG_SESSION.ERROR', + DEBUG_SESSION_START = 'DEBUG_SESSION.START', + DEBUG_SESSION_STOP = 'DEBUG_SESSION.STOP', + DEBUG_SESSION_USER_CODE_RUNNING = 'DEBUG_SESSION.USER_CODE_RUNNING', DEBUGGER = 'DEBUGGER', + DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', + DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS', // Python testing specific telemetry UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 676d018c5925..155454e0df56 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -310,6 +310,143 @@ export interface IEventNamePropertyMapping { "debug_in_terminal_button" : { "owner": "paulacamargo25" } */ [EventName.DEBUG_IN_TERMINAL_BUTTON]: never | undefined; + /** + * Telemetry event captured when debug adapter executable is created + */ + /* __GDPR__ + "debug_adapter.using_wheels_path" : { + "usingwheels" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } + } + */ + + [EventName.DEBUG_ADAPTER_USING_WHEELS_PATH]: { + /** + * Carries boolean + * - `true` if path used for the adapter is the debugger with wheels. + * - `false` if path used for the adapter is the source only version of the debugger. + */ + usingWheels: boolean; + }; + /** + * Telemetry captured before starting debug session. + */ + /* __GDPR__ + "debug_session.start" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, + "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } + } + */ + [EventName.DEBUG_SESSION_START]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured when debug session runs into an error. + */ + /* __GDPR__ + "debug_session.error" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, + "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } + } + */ + [EventName.DEBUG_SESSION_ERROR]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured after stopping debug session. + */ + /* __GDPR__ + "debug_session.stop" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, + "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } + } + */ + [EventName.DEBUG_SESSION_STOP]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured when user code starts running after loading the debugger. + */ + /* __GDPR__ + "debug_session.user_code_running" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, + "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } + } + */ + [EventName.DEBUG_SESSION_USER_CODE_RUNNING]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; /** * Telemetry captured when starting the debugger. */ @@ -460,6 +597,22 @@ export interface IEventNamePropertyMapping { */ scrapy: boolean; }; + /** + * Telemetry event sent when attaching to child process + */ + /* __GDPR__ + "debugger.attach_to_child_process" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "paulacamargo25" } + } + */ + [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; + /** + * Telemetry event sent when attaching to a local process. + */ + /* __GDPR__ + "debugger.attach_to_local_process" : { "owner": "paulacamargo25" } + */ + [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; /** * Telemetry event sent with details of actions when invoking a diagnostic command */ diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 0d132120c904..c76557699ff2 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -78,7 +78,7 @@ export class DebugLauncher implements ITestDebugLauncher { if (!debugConfig) { debugConfig = { name: 'Debug Unit Test', - type: 'debugpy', + type: 'python', request: 'test', subProcess: true, }; From 0a384d5ffe078826349caa10f82fb53c2a659ecf Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 02:38:18 -0500 Subject: [PATCH 18/24] fix lint --- .../checks/invalidLaunchJsonDebugger.ts | 412 +++++++++--------- .../application/debugSessionTelemetry.ts | 2 +- .../debugger/extension/adapter/activator.ts | 2 +- .../debugger/extension/adapter/factory.ts | 2 +- .../debugger/extension/adapter/logging.ts | 126 +++--- .../debugger/extension/adapter/types.ts | 2 +- .../extension/attachQuickPick/factory.ts | 2 +- .../extension/attachQuickPick/picker.ts | 2 +- .../extension/attachQuickPick/provider.ts | 2 +- .../attachQuickPick/psProcessParser.ts | 2 +- .../extension/attachQuickPick/types.ts | 2 +- .../attachQuickPick/wmicProcessParser.ts | 2 +- .../configuration/resolvers/attach.ts | 230 +++++----- .../extension/configuration/resolvers/base.ts | 10 +- .../debugger/extension/debugCommands.ts | 136 +++--- .../hooks/childProcessAttachHandler.ts | 1 - .../hooks/childProcessAttachService.ts | 2 +- .../debugger/extension/hooks/constants.ts | 2 +- src/client/debugger/extension/hooks/types.ts | 2 +- src/client/telemetry/index.ts | 12 +- 20 files changed, 477 insertions(+), 476 deletions(-) diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts index 9b0038d9ecd0..440ff16856d3 100644 --- a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -1,206 +1,206 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// eslint-disable-next-line max-classes-per-file -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import '../../../common/extensions'; -import { IFileSystem } from '../../../common/platform/types'; -import { IDisposableRegistry, Resource } from '../../../common/types'; -import { Common, Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -const messages = { - [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic, - [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic, - [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic, - [DiagnosticCodes.ConfigPythonPathDiagnostic]: '', -}; - -export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { - constructor( - code: - | DiagnosticCodes.InvalidDebuggerTypeDiagnostic - | DiagnosticCodes.JustMyCodeDiagnostic - | DiagnosticCodes.ConsoleTypeDiagnostic - | DiagnosticCodes.ConfigPythonPathDiagnostic, - resource: Resource, - shouldShowPrompt = true, - ) { - super( - code, - messages[code], - DiagnosticSeverity.Error, - DiagnosticScope.WorkspaceFolder, - resource, - shouldShowPrompt, - ); - } -} - -export const InvalidLaunchJsonDebuggerServiceId = 'InvalidLaunchJsonDebuggerServiceId'; - -@injectable() -export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - private readonly messageService: IDiagnosticHandlerService, - ) { - super( - [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic, - DiagnosticCodes.ConfigPythonPathDiagnostic, - ], - serviceContainer, - disposableRegistry, - true, - ); - } - - public async diagnose(resource: Resource): Promise { - const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; - if (!hasWorkspaceFolders) { - return []; - } - const workspaceFolder = resource - ? this.workspaceService.getWorkspaceFolder(resource)! - : this.workspaceService.workspaceFolders![0]; - return this.diagnoseWorkspace(workspaceFolder, resource); - } - - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); - } - - protected async fixLaunchJson(code: DiagnosticCodes): Promise { - const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; - if (!hasWorkspaceFolders) { - return; - } - - await Promise.all( - (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => - this.fixLaunchJsonInWorkspace(code, workspaceFolder), - ), - ); - } - - private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { - const launchJson = getLaunchJsonFile(workspaceFolder); - if (!(await this.fs.fileExists(launchJson))) { - return []; - } - - const fileContents = await this.fs.readFile(launchJson); - const diagnostics: IDiagnostic[] = []; - if (fileContents.indexOf('"pythonExperimental"') > 0) { - diagnostics.push( - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), - ); - } - if (fileContents.indexOf('"debugStdLib"') > 0) { - diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, resource)); - } - if (fileContents.indexOf('"console": "none"') > 0) { - diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource)); - } - if ( - fileContents.indexOf('"pythonPath":') > 0 || - fileContents.indexOf('{config:python.pythonPath}') > 0 || - fileContents.indexOf('{config:python.interpreterPath}') > 0 - ) { - diagnostics.push( - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false), - ); - } - return diagnostics; - } - - private async handleDiagnostic(diagnostic: IDiagnostic): Promise { - if (!diagnostic.shouldShowPrompt) { - await this.fixLaunchJson(diagnostic.code); - return; - } - const commandPrompts = [ - { - prompt: Diagnostics.yesUpdateLaunch, - command: { - diagnostic, - invoke: async (): Promise => { - await this.fixLaunchJson(diagnostic.code); - }, - }, - }, - { - prompt: Common.noIWillDoItLater, - }, - ]; - - await this.messageService.handle(diagnostic, { commandPrompts }); - } - - private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { - if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { - return; - } - const launchJson = getLaunchJsonFile(workspaceFolder); - let fileContents = await this.fs.readFile(launchJson); - switch (code) { - case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { - fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); - fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); - break; - } - case DiagnosticCodes.JustMyCodeDiagnostic: { - fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); - fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); - break; - } - case DiagnosticCodes.ConsoleTypeDiagnostic: { - fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); - break; - } - case DiagnosticCodes.ConfigPythonPathDiagnostic: { - fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); - fileContents = findAndReplace( - fileContents, - '{config:python.pythonPath}', - '{command:python.interpreterPath}', - ); - fileContents = findAndReplace( - fileContents, - '{config:python.interpreterPath}', - '{command:python.interpreterPath}', - ); - break; - } - default: { - return; - } - } - - await this.fs.writeFile(launchJson, fileContents); - } -} - -function findAndReplace(fileContents: string, search: string, replace: string) { - const searchRegex = new RegExp(search, 'g'); - return fileContents.replace(searchRegex, replace); -} - -function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; +import { IFileSystem } from '../../../common/platform/types'; +import { IDisposableRegistry, Resource } from '../../../common/types'; +import { Common, Diagnostics } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +const messages = { + [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic, + [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic, + [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic, + [DiagnosticCodes.ConfigPythonPathDiagnostic]: '', +}; + +export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { + constructor( + code: + | DiagnosticCodes.InvalidDebuggerTypeDiagnostic + | DiagnosticCodes.JustMyCodeDiagnostic + | DiagnosticCodes.ConsoleTypeDiagnostic + | DiagnosticCodes.ConfigPythonPathDiagnostic, + resource: Resource, + shouldShowPrompt = true, + ) { + super( + code, + messages[code], + DiagnosticSeverity.Error, + DiagnosticScope.WorkspaceFolder, + resource, + shouldShowPrompt, + ); + } +} + +export const InvalidLaunchJsonDebuggerServiceId = 'InvalidLaunchJsonDebuggerServiceId'; + +@injectable() +export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + private readonly messageService: IDiagnosticHandlerService, + ) { + super( + [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic, + DiagnosticCodes.ConfigPythonPathDiagnostic, + ], + serviceContainer, + disposableRegistry, + true, + ); + } + + public async diagnose(resource: Resource): Promise { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + return []; + } + const workspaceFolder = resource + ? this.workspaceService.getWorkspaceFolder(resource)! + : this.workspaceService.workspaceFolders![0]; + return this.diagnoseWorkspace(workspaceFolder, resource); + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); + } + + protected async fixLaunchJson(code: DiagnosticCodes): Promise { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + return; + } + + await Promise.all( + (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => + this.fixLaunchJsonInWorkspace(code, workspaceFolder), + ), + ); + } + + private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { + const launchJson = getLaunchJsonFile(workspaceFolder); + if (!(await this.fs.fileExists(launchJson))) { + return []; + } + + const fileContents = await this.fs.readFile(launchJson); + const diagnostics: IDiagnostic[] = []; + if (fileContents.indexOf('"pythonExperimental"') > 0) { + diagnostics.push( + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), + ); + } + if (fileContents.indexOf('"debugStdLib"') > 0) { + diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, resource)); + } + if (fileContents.indexOf('"console": "none"') > 0) { + diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource)); + } + if ( + fileContents.indexOf('"pythonPath":') > 0 || + fileContents.indexOf('{config:python.pythonPath}') > 0 || + fileContents.indexOf('{config:python.interpreterPath}') > 0 + ) { + diagnostics.push( + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false), + ); + } + return diagnostics; + } + + private async handleDiagnostic(diagnostic: IDiagnostic): Promise { + if (!diagnostic.shouldShowPrompt) { + await this.fixLaunchJson(diagnostic.code); + return; + } + const commandPrompts = [ + { + prompt: Diagnostics.yesUpdateLaunch, + command: { + diagnostic, + invoke: async (): Promise => { + await this.fixLaunchJson(diagnostic.code); + }, + }, + }, + { + prompt: Common.noIWillDoItLater, + }, + ]; + + await this.messageService.handle(diagnostic, { commandPrompts }); + } + + private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { + if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { + return; + } + const launchJson = getLaunchJsonFile(workspaceFolder); + let fileContents = await this.fs.readFile(launchJson); + switch (code) { + case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); + fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); + break; + } + case DiagnosticCodes.JustMyCodeDiagnostic: { + fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); + fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); + break; + } + case DiagnosticCodes.ConsoleTypeDiagnostic: { + fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); + break; + } + case DiagnosticCodes.ConfigPythonPathDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); + fileContents = findAndReplace( + fileContents, + '{config:python.pythonPath}', + '{command:python.interpreterPath}', + ); + fileContents = findAndReplace( + fileContents, + '{config:python.interpreterPath}', + '{command:python.interpreterPath}', + ); + break; + } + default: { + return; + } + } + + await this.fs.writeFile(launchJson, fileContents); + } +} + +function findAndReplace(fileContents: string, search: string, replace: string) { + const searchRegex = new RegExp(search, 'g'); + return fileContents.replace(searchRegex, replace); +} + +function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { + return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); +} diff --git a/src/client/common/application/debugSessionTelemetry.ts b/src/client/common/application/debugSessionTelemetry.ts index 6117c3eb0fab..42b8b2651092 100644 --- a/src/client/common/application/debugSessionTelemetry.ts +++ b/src/client/common/application/debugSessionTelemetry.ts @@ -77,4 +77,4 @@ export class DebugSessionTelemetry implements DebugAdapterTrackerFactory, IExten public createDebugAdapterTracker(session: DebugSession): ProviderResult { return new TelemetryTracker(session); } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts index 82750fc6f204..999c00366ed6 100644 --- a/src/client/debugger/extension/adapter/activator.ts +++ b/src/client/debugger/extension/adapter/activator.ts @@ -50,4 +50,4 @@ export class DebugAdapterActivator implements IExtensionSingleActivationService private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { return this.configSettings.getSettings(uri)?.terminal.focusAfterLaunch; } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index c5cfdfb4cb21..ecbd8afcc287 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -205,4 +205,4 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac private async notifySelectInterpreter() { await showErrorMessage(l10n.t('Install Python or select a Python Interpreter to use the debugger.')); } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/adapter/logging.ts b/src/client/debugger/extension/adapter/logging.ts index 9ab5fcf70113..907b895170c6 100644 --- a/src/client/debugger/extension/adapter/logging.ts +++ b/src/client/debugger/extension/adapter/logging.ts @@ -1,77 +1,77 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { - DebugAdapterTracker, - DebugAdapterTrackerFactory, - DebugConfiguration, - DebugSession, - ProviderResult, -} from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugSession, + ProviderResult, +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; -import { IFileSystem, WriteStream } from '../../../common/platform/types'; -import { StopWatch } from '../../../common/utils/stopWatch'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IFileSystem, WriteStream } from '../../../common/platform/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; -class DebugSessionLoggingTracker implements DebugAdapterTracker { - private readonly enabled: boolean = false; - private stream?: WriteStream; - private timer = new StopWatch(); +class DebugSessionLoggingTracker implements DebugAdapterTracker { + private readonly enabled: boolean = false; + private stream?: WriteStream; + private timer = new StopWatch(); - constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { - this.enabled = this.session.configuration.logToFile as boolean; - if (this.enabled) { - const fileName = `debugger.vscode_${this.session.id}.log`; - this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); - } - } + constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { + this.enabled = this.session.configuration.logToFile as boolean; + if (this.enabled) { + const fileName = `debugger.vscode_${this.session.id}.log`; + this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); + } + } - public onWillStartSession() { - this.timer.reset(); - this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); - } + public onWillStartSession() { + this.timer.reset(); + this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); + } - public onWillReceiveMessage(message: DebugProtocol.Message) { - this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); - } + public onWillReceiveMessage(message: DebugProtocol.Message) { + this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); + } - public onDidSendMessage(message: DebugProtocol.Message) { - this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); - } + public onDidSendMessage(message: DebugProtocol.Message) { + this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); + } - public onWillStopSession() { - this.log('Stopping Session\n'); - } + public onWillStopSession() { + this.log('Stopping Session\n'); + } - public onError(error: Error) { - this.log(`Error:\n${this.stringify(error)}\n`); - } + public onError(error: Error) { + this.log(`Error:\n${this.stringify(error)}\n`); + } - public onExit(code: number | undefined, signal: string | undefined) { - this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); - this.stream?.close(); - } + public onExit(code: number | undefined, signal: string | undefined) { + this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); + this.stream?.close(); + } - private log(message: string) { - if (this.enabled) { - this.stream!.write(`${this.timer.elapsedTime} ${message}`); // NOSONAR - } - } + private log(message: string) { + if (this.enabled) { + this.stream!.write(`${this.timer.elapsedTime} ${message}`); // NOSONAR + } + } - private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { - return JSON.stringify(data, null, 4); - } -} + private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { + return JSON.stringify(data, null, 4); + } +} -@injectable() -export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { - constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) {} +@injectable() +export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { + constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) {} - public createDebugAdapterTracker(session: DebugSession): ProviderResult { - return new DebugSessionLoggingTracker(session, this.fileSystem); - } -} + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + return new DebugSessionLoggingTracker(session, this.fileSystem); + } +} diff --git a/src/client/debugger/extension/adapter/types.ts b/src/client/debugger/extension/adapter/types.ts index 099260376ddb..6c082a801ad6 100644 --- a/src/client/debugger/extension/adapter/types.ts +++ b/src/client/debugger/extension/adapter/types.ts @@ -7,4 +7,4 @@ export const IPromptShowState = Symbol('IPromptShowState'); export interface IPromptShowState { shouldShowPrompt(): boolean; setShowPrompt(show: boolean): void; -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/attachQuickPick/factory.ts b/src/client/debugger/extension/attachQuickPick/factory.ts index 90e226e587db..627962106e88 100644 --- a/src/client/debugger/extension/attachQuickPick/factory.ts +++ b/src/client/debugger/extension/attachQuickPick/factory.ts @@ -33,4 +33,4 @@ export class AttachProcessProviderFactory implements IAttachProcessProviderFacto ); this.disposableRegistry.push(disposable); } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/attachQuickPick/picker.ts b/src/client/debugger/extension/attachQuickPick/picker.ts index 2107e6d1e244..a296a9b3163a 100644 --- a/src/client/debugger/extension/attachQuickPick/picker.ts +++ b/src/client/debugger/extension/attachQuickPick/picker.ts @@ -79,4 +79,4 @@ export class AttachPicker implements IAttachPicker { quickPick.show(); }); } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/attachQuickPick/provider.ts b/src/client/debugger/extension/attachQuickPick/provider.ts index d7da60a8c8de..3626d8dfb8ce 100644 --- a/src/client/debugger/extension/attachQuickPick/provider.ts +++ b/src/client/debugger/extension/attachQuickPick/provider.ts @@ -79,4 +79,4 @@ export class AttachProcessProvider implements IAttachProcessProvider { ? WmicProcessParser.parseProcesses(output.stdout) : PsProcessParser.parseProcesses(output.stdout); } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts index 1666f4345f21..843369bd00c7 100644 --- a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts +++ b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts @@ -98,4 +98,4 @@ export namespace PsProcessParser { }; } } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/attachQuickPick/types.ts b/src/client/debugger/extension/attachQuickPick/types.ts index eda7d2c575d7..5e26c1354f9e 100644 --- a/src/client/debugger/extension/attachQuickPick/types.ts +++ b/src/client/debugger/extension/attachQuickPick/types.ts @@ -26,4 +26,4 @@ export interface IAttachPicker { showQuickPick(): Promise; } -export const REFRESH_BUTTON_ICON = 'refresh.svg'; \ No newline at end of file +export const REFRESH_BUTTON_ICON = 'refresh.svg'; diff --git a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts index 1bd2e644c364..e1faed50fc2e 100644 --- a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts +++ b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts @@ -79,4 +79,4 @@ export namespace WmicProcessParser { return currentItem; } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts index a1356c3b10d1..bdc72680d861 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -1,124 +1,124 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -'use strict'; +'use strict'; -import { injectable } from 'inversify'; -import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; -import { getOSType, OSType } from '../../../../common/utils/platform'; -import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; -import { BaseConfigurationResolver } from './base'; +import { injectable } from 'inversify'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { getOSType, OSType } from '../../../../common/utils/platform'; +import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; +import { BaseConfigurationResolver } from './base'; -@injectable() -export class AttachConfigurationResolver extends BaseConfigurationResolver { - public async resolveDebugConfigurationWithSubstitutedVariables( - folder: WorkspaceFolder | undefined, - debugConfiguration: AttachRequestArguments, - _token?: CancellationToken, - ): Promise { - const workspaceFolder = AttachConfigurationResolver.getWorkspaceFolder(folder); +@injectable() +export class AttachConfigurationResolver extends BaseConfigurationResolver { + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: AttachRequestArguments, + _token?: CancellationToken, + ): Promise { + const workspaceFolder = AttachConfigurationResolver.getWorkspaceFolder(folder); - await this.provideAttachDefaults(workspaceFolder, debugConfiguration as AttachRequestArguments); + 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, - ); - } - if (debugConfiguration.clientOS === undefined) { - debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; - } - return debugConfiguration; - } + const dbgConfig = debugConfiguration; + if (Array.isArray(dbgConfig.debugOptions)) { + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter( + (item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos, + ); + } + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; + } + return debugConfiguration; + } - protected async provideAttachDefaults( - workspaceFolder: Uri | undefined, - debugConfiguration: AttachRequestArguments, - ): Promise { - if (!Array.isArray(debugConfiguration.debugOptions)) { - debugConfiguration.debugOptions = []; - } - if (!(debugConfiguration.connect || debugConfiguration.listen) && !debugConfiguration.host) { - // Connect and listen cannot be mixed with host property. - debugConfiguration.host = 'localhost'; - } - if (debugConfiguration.justMyCode === undefined) { - // Populate justMyCode using debugStdLib - debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; - } - debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; - // 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.justMyCode) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.DebugStdLib); - } - if (debugConfiguration.django) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Django); - } - if (debugConfiguration.jinja) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.subProcess === true) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.SubProcess); - } - if ( - debugConfiguration.pyramid && - debugOptions.indexOf(DebugOptions.Jinja) === -1 && - debugConfiguration.jinja !== false - ) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.RedirectOutput); - } + protected async provideAttachDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: AttachRequestArguments, + ): Promise { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + if (!(debugConfiguration.connect || debugConfiguration.listen) && !debugConfiguration.host) { + // Connect and listen cannot be mixed with host property. + debugConfiguration.host = 'localhost'; + } + if (debugConfiguration.justMyCode === undefined) { + // Populate justMyCode using debugStdLib + debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; + } + debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; + // 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.justMyCode) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.django) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.subProcess === true) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.SubProcess); + } + if ( + debugConfiguration.pyramid && + debugOptions.indexOf(DebugOptions.Jinja) === -1 && + debugConfiguration.jinja !== false + ) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + AttachConfigurationResolver.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 = AttachConfigurationResolver.isLocalHost(debugConfiguration.host); - if (getOSType() === OSType.Windows && isLocalHost) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.FixFilePathCase); - } - if (debugConfiguration.clientOS === undefined) { - debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; - } - if (debugConfiguration.showReturnValue) { - AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); - } + // 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 = AttachConfigurationResolver.isLocalHost(debugConfiguration.host); + if (getOSType() === OSType.Windows && isLocalHost) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; + } + if (debugConfiguration.showReturnValue) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); + } - debugConfiguration.pathMappings = this.resolvePathMappings( - debugConfiguration.pathMappings || [], - debugConfiguration.host, - debugConfiguration.localRoot, - debugConfiguration.remoteRoot, - workspaceFolder, - ); - AttachConfigurationResolver.sendTelemetry('attach', debugConfiguration); - } + debugConfiguration.pathMappings = this.resolvePathMappings( + debugConfiguration.pathMappings || [], + debugConfiguration.host, + debugConfiguration.localRoot, + debugConfiguration.remoteRoot, + workspaceFolder, + ); + AttachConfigurationResolver.sendTelemetry('attach', debugConfiguration); + } - // eslint-disable-next-line class-methods-use-this - private resolvePathMappings( - pathMappings: PathMapping[], - host?: string, - localRoot?: string, - remoteRoot?: string, - workspaceFolder?: Uri, - ) { - // This is for backwards compatibility. - if (localRoot && remoteRoot) { - pathMappings.push({ - localRoot, - remoteRoot, - }); - } - // If attaching to local host, then always map local root and remote roots. - if (AttachConfigurationResolver.isLocalHost(host)) { - pathMappings = AttachConfigurationResolver.fixUpPathMappings( - pathMappings, - workspaceFolder ? workspaceFolder.fsPath : '', - ); - } - return pathMappings.length > 0 ? pathMappings : undefined; - } -} + // eslint-disable-next-line class-methods-use-this + private resolvePathMappings( + pathMappings: PathMapping[], + host?: string, + localRoot?: string, + remoteRoot?: string, + workspaceFolder?: Uri, + ) { + // This is for backwards compatibility. + if (localRoot && remoteRoot) { + pathMappings.push({ + localRoot, + remoteRoot, + }); + } + // If attaching to local host, then always map local root and remote roots. + if (AttachConfigurationResolver.isLocalHost(host)) { + pathMappings = AttachConfigurationResolver.fixUpPathMappings( + pathMappings, + workspaceFolder ? workspaceFolder.fsPath : '', + ); + } + return pathMappings.length > 0 ? pathMappings : undefined; + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index e36e37394b18..795d06abf6d0 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -217,13 +217,15 @@ export abstract class BaseConfigurationResolver return pathMappings; } - protected static isDebuggingFastAPI(debugConfiguration: Partial, - ): boolean { + protected static isDebuggingFastAPI( + debugConfiguration: Partial, + ): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } - protected static isDebuggingFlask(debugConfiguration: Partial, - ): boolean { + protected static isDebuggingFlask( + debugConfiguration: Partial, + ): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts index c01bdf8406c1..b3322e8e7dd1 100644 --- a/src/client/debugger/extension/debugCommands.ts +++ b/src/client/debugger/extension/debugCommands.ts @@ -1,73 +1,73 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -import * as path from 'path'; -import { inject, injectable } from 'inversify'; -import { DebugConfiguration, Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { ICommandManager, IDebugService } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IDisposableRegistry } from '../../common/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { DebugPurpose, LaunchRequestArguments } from '../types'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { noop } from '../../common/utils/misc'; -import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; -import { - CreateEnvironmentCheckKind, - triggerCreateEnvironmentCheckNonBlocking, -} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IDebugService } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { DebugPurpose, LaunchRequestArguments } from '../types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { noop } from '../../common/utils/misc'; +import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; -@injectable() -export class DebugCommands implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; +@injectable() +export class DebugCommands implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - ) {} + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} - public activate(): Promise { - this.disposables.push( - this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { - sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); - const interpreter = await this.interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); - triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); - const config = await DebugCommands.getDebugConfiguration(file); - this.debugService.startDebugging(undefined, config); - }), - ); - return Promise.resolve(); - } + public activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { + sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); + const interpreter = await this.interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + const config = await DebugCommands.getDebugConfiguration(file); + this.debugService.startDebugging(undefined, config); + }), + ); + return Promise.resolve(); + } - private static async getDebugConfiguration(uri?: Uri): Promise { - const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); - for (const config of configs) { - if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { - if (!config.program && !config.module && !config.code) { - // This is only needed if people reuse debug-test for debug-in-terminal - config.program = uri?.fsPath ?? '${file}'; - } - // Ensure that the purpose is cleared, this is so we can track if people accidentally - // trigger this via F5 or Start with debugger. - config.purpose = []; - return config; - } - } - return { - name: `Debug ${uri ? path.basename(uri.fsPath) : 'File'}`, - type: 'python', - request: 'launch', - program: uri?.fsPath ?? '${file}', - console: 'integratedTerminal', - }; - } -} + private static async getDebugConfiguration(uri?: Uri): Promise { + const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); + for (const config of configs) { + if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { + if (!config.program && !config.module && !config.code) { + // This is only needed if people reuse debug-test for debug-in-terminal + config.program = uri?.fsPath ?? '${file}'; + } + // Ensure that the purpose is cleared, this is so we can track if people accidentally + // trigger this via F5 or Start with debugger. + config.purpose = []; + return config; + } + } + return { + name: `Debug ${uri ? path.basename(uri.fsPath) : 'File'}`, + type: 'python', + request: 'launch', + program: uri?.fsPath ?? '${file}', + console: 'integratedTerminal', + }; + } +} diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts index 22138dda9005..23602ffce086 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -45,4 +45,3 @@ export class ChildProcessAttachEventHandler implements IDebugSessionEventHandler } } } - \ No newline at end of file diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index 781d39edc955..08f44bc3cea5 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -53,4 +53,4 @@ export class ChildProcessAttachService implements IChildProcessAttachService { } return getWorkspaceFolders()!.find((ws) => ws.uri.fsPath === workspaceFolder); } -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/hooks/constants.ts b/src/client/debugger/extension/hooks/constants.ts index c69d922e7c0c..3bd0b657281e 100644 --- a/src/client/debugger/extension/hooks/constants.ts +++ b/src/client/debugger/extension/hooks/constants.ts @@ -7,4 +7,4 @@ export enum DebuggerEvents { // Event sent by PTVSD when a child process is launched and ready to be attached to for multi-proc debugging. PtvsdAttachToSubprocess = 'ptvsd_attach', DebugpyAttachToSubprocess = 'debugpyAttach', -} \ No newline at end of file +} diff --git a/src/client/debugger/extension/hooks/types.ts b/src/client/debugger/extension/hooks/types.ts index 72865da0e618..80d393057fb4 100644 --- a/src/client/debugger/extension/hooks/types.ts +++ b/src/client/debugger/extension/hooks/types.ts @@ -15,4 +15,4 @@ export interface IDebugSessionEventHandlers { export const IChildProcessAttachService = Symbol('IChildProcessAttachService'); export interface IChildProcessAttachService { attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise; -} \ No newline at end of file +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 155454e0df56..168ea18bf722 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -319,7 +319,7 @@ export interface IEventNamePropertyMapping { } */ - [EventName.DEBUG_ADAPTER_USING_WHEELS_PATH]: { + [EventName.DEBUG_ADAPTER_USING_WHEELS_PATH]: { /** * Carries boolean * - `true` if path used for the adapter is the debugger with wheels. @@ -337,7 +337,7 @@ export interface IEventNamePropertyMapping { "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } } */ - [EventName.DEBUG_SESSION_START]: { + [EventName.DEBUG_SESSION_START]: { /** * Trigger for starting the debugger. * - `launch`: Launch/start new code and debug it. @@ -427,7 +427,7 @@ export interface IEventNamePropertyMapping { "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } } */ - [EventName.DEBUG_SESSION_USER_CODE_RUNNING]: { + [EventName.DEBUG_SESSION_USER_CODE_RUNNING]: { /** * Trigger for starting the debugger. * - `launch`: Launch/start new code and debug it. @@ -605,14 +605,14 @@ export interface IEventNamePropertyMapping { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "paulacamargo25" } } */ - [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; - /** + [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; + /** * Telemetry event sent when attaching to a local process. */ /* __GDPR__ "debugger.attach_to_local_process" : { "owner": "paulacamargo25" } */ - [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; + [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; /** * Telemetry event sent with details of actions when invoking a diagnostic command */ From c13200b42f5526b6a44a1e91c2091f97626821f3 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 09:35:16 -0500 Subject: [PATCH 19/24] add tests --- .../adapter/outdatedDebuggerPrompt.ts | 34 +- src/client/debugger/extension/types.ts | 2 +- .../extension/adapter/factory.unit.test.ts | 316 +++++ .../extension/adapter/logging.unit.test.ts | 149 ++ .../outdatedDebuggerPrompt.unit.test.ts | 180 +++ .../attachQuickPick/factory.unit.test.ts | 51 + .../attachQuickPick/provider.unit.test.ts | 459 +++++++ .../psProcessParser.unit.test.ts | 192 +++ .../wmicProcessParser.unit.test.ts | 215 +++ .../resolvers/attach.unit.test.ts | 570 ++++++++ .../configuration/resolvers/base.unit.test.ts | 337 +++++ .../configuration/resolvers/common.ts | 2 +- .../resolvers/helper.unit.test.ts | 70 + .../resolvers/launch.unit.test.ts | 1213 +++++++++++++++++ .../extension/debugCommands.unit.test.ts | 93 ++ .../childProcessAttachHandler.unit.test.ts | 72 + .../childProcessAttachService.unit.test.ts | 195 +++ .../extension/serviceRegistry.unit.test.ts | 92 +- .../testing/common/debugLauncher.unit.test.ts | 2 +- 19 files changed, 4214 insertions(+), 30 deletions(-) create mode 100644 src/test/debugger/extension/adapter/factory.unit.test.ts create mode 100644 src/test/debugger/extension/adapter/logging.unit.test.ts create mode 100644 src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts create mode 100644 src/test/debugger/extension/attachQuickPick/factory.unit.test.ts create mode 100644 src/test/debugger/extension/attachQuickPick/provider.unit.test.ts create mode 100644 src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts create mode 100644 src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/resolvers/base.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts create mode 100644 src/test/debugger/extension/debugCommands.unit.test.ts create mode 100644 src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts create mode 100644 src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts index c86f3a9ef206..0ece567c7a84 100644 --- a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts +++ b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -2,34 +2,27 @@ // Licensed under the MIT License. 'use strict'; - -import { inject, injectable } from 'inversify'; +import { injectable } from 'inversify'; import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { IApplicationShell } from '../../../common/application/types'; -import { IBrowserService } from '../../../common/types'; import { Common, OutdatedDebugger } from '../../../common/utils/localize'; +import { launch } from '../../../common/vscodeApis/browserApis'; +import { showInformationMessage } from '../../../common/vscodeApis/windowApis'; import { IPromptShowState } from './types'; // This situation occurs when user connects to old containers or server where // the debugger they had installed was ptvsd. We should show a prompt to ask them to update. class OutdatedDebuggerPrompt implements DebugAdapterTracker { - constructor( - private promptCheck: IPromptShowState, - private appShell: IApplicationShell, - private browserService: IBrowserService, - ) {} + constructor(private promptCheck: IPromptShowState) {} public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { if (this.promptCheck.shouldShowPrompt() && this.isPtvsd(message)) { const prompts = [Common.moreInfo]; - this.appShell - .showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts) - .then((selection) => { - if (selection === prompts[0]) { - this.browserService.launch('https://aka.ms/migrateToDebugpy'); - } - }); + showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts).then((selection) => { + if (selection === prompts[0]) { + launch('https://aka.ms/migrateToDebugpy'); + } + }); } } @@ -71,13 +64,10 @@ class OutdatedDebuggerPromptState implements IPromptShowState { @injectable() export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { private readonly promptCheck: OutdatedDebuggerPromptState; - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IBrowserService) private browserService: IBrowserService, - ) { + constructor() { this.promptCheck = new OutdatedDebuggerPromptState(); } public createDebugAdapterTracker(_session: DebugSession): ProviderResult { - return new OutdatedDebuggerPrompt(this.promptCheck, this.appShell, this.browserService); + return new OutdatedDebuggerPrompt(this.promptCheck); } -} +} \ No newline at end of file diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index cddbc748ad01..7fe77b26f731 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,7 +3,7 @@ 'use strict'; -import { DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider } from 'vscode'; +import { DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory } from 'vscode'; export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts new file mode 100644 index 000000000000..8ab1d71e2740 --- /dev/null +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import rewiremock from 'rewiremock'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../../client/debugger/extension/adapter/factory'; +import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; +import { clearTelemetryReporter } from '../../../../client/telemetry'; +import { EventName } from '../../../../client/telemetry/constants'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { CommandManager } from '../../../../client/common/application/commandManager'; + +use(chaiAsPromised); + +suite('Debugging - Adapter Factory', () => { + let factory: IDebugAdapterDescriptorFactory; + let interpreterService: IInterpreterService; + let stateFactory: IPersistentStateFactory; + let state: PersistentState; + let showErrorMessageStub: sinon.SinonStub; + let readJSONSyncStub: sinon.SinonStub; + let commandManager: ICommandManager; + + const nodeExecutable = undefined; + const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); + const pythonPath = path.join('path', 'to', 'python', 'interpreter'); + const interpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.7.4-test'), + }; + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + class Reporter { + public static eventNames: string[] = []; + public static properties: Record[] = []; + public static measures: {}[] = []; + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); + rewiremock.enable(); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState) as PersistentState; + commandManager = mock(CommandManager); + + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + + when( + stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); + + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true }, + } as any) as IPythonSettings); + + interpreterService = mock(InterpreterService); + + when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); + when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]); + + factory = new DebugAdapterDescriptorFactory( + instance(commandManager), + instance(interpreterService), + instance(stateFactory), + ); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + Reporter.properties = []; + Reporter.eventNames = []; + Reporter.measures = []; + rewiremock.disable(); + clearTelemetryReporter(); + sinon.restore(); + }); + + function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { name: '', request: 'launch', type: 'python', ...config }, + id: '', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { + const session = createSession({ pythonPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Return the path of the first available interpreter as the current python path, configuration.pythonPath is not defined and there is no active interpreter', async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Display a message if no python interpreter is set', async () => { + when(interpreterService.getInterpreters(anything())).thenReturn([]); + const session = createSession({}); + + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith('Debug Adapter Executable not provided'); + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('Display a message if python version is less than 3.7', async () => { + when(interpreterService.getInterpreters(anything())).thenReturn([]); + const session = createSession({}); + const deprecatedInterpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.6.12-test'), + }; + when(state.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(deprecatedInterpreter); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { + const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); + const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for host/port + verify(interpreterService.getInterpreters(anything())).never(); + assert.deepStrictEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { + const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); + const debugServer = new DebugAdapterServer( + session.configuration.connect.port, + session.configuration.connect.host, + ); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for connect + verify(interpreterService.getInterpreters(anything())).never(); + assert.deepStrictEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { + const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { + const session = createSession({ + request: 'attach', + port: undefined, + processId: undefined, + listen: undefined, + connect: undefined, + }); + + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith( + '"request":"attach" requires either "connect", "listen", or "processId"', + ); + }); + + test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { + const session = createSession({ logToFile: true }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [ + debugAdapterPath, + '--log-dir', + EXTENSION_ROOT_DIR, + ]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { + const session = createSession({ logToFile: false }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Send attach to local process telemetry if attaching to a local process', async () => { + const session = createSession({ request: 'attach', processId: 1234 }); + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); + }); + + test("Don't send any telemetry if not attaching to a local process", async () => { + const session = createSession({}); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); + }); + + test('Use "debugAdapterPath" when specified', async () => { + const customAdapterPath = 'custom/debug/adapter/path'; + const session = createSession({ debugAdapterPath: customAdapterPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Use "debugAdapterPython" when specified', async () => { + const session = createSession({ debugAdapterPython: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); + const customInterpreter = { + architecture: Architecture.Unknown, + path: '/bin/custompy', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.7.4-test'), + }; + when(interpreterService.getInterpreterDetails('/bin/custompy')).thenResolve(customInterpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Do not use "python" to spawn the debug adapter', async () => { + const session = createSession({ python: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/adapter/logging.unit.test.ts b/src/test/debugger/extension/adapter/logging.unit.test.ts new file mode 100644 index 000000000000..cea32d449497 --- /dev/null +++ b/src/test/debugger/extension/adapter/logging.unit.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; + +suite('Debugging - Session Logging', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let loggerFactory: DebugSessionLoggingFactory; + let fsService: FileSystem; + let writeStream: fs.WriteStream; + + setup(() => { + fsService = mock(FileSystem); + writeStream = mock(fs.WriteStream); + + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + + loggerFactory = new DebugSessionLoggingFactory(instance(fsService)); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + }); + + function createSession(id: string, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python', + }, + id: id, + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + function createSessionWithLogging(id: string, logToFile: boolean, workspaceFolder?: WorkspaceFolder): DebugSession { + const session = createSession(id, workspaceFolder); + session.configuration.logToFile = logToFile; + return session; + } + + class TestMessage implements DebugProtocol.ProtocolMessage { + public seq: number; + public type: string; + public id: number; + public format: string; + public variables?: { [key: string]: string }; + public sendTelemetry?: boolean; + public showUser?: boolean; + public url?: string; + public urlLabel?: string; + constructor(id: number, seq: number, type: string) { + this.id = id; + this.format = 'json'; + this.seq = seq; + this.type = type; + } + } + + test('Create logger using session without logToFile', async () => { + const session = createSession('test1'); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + await loggerFactory.createDebugAdapterTracker(session); + + verify(fsService.createWriteStream(filePath)).never(); + }); + + test('Create logger using session with logToFile set to false', async () => { + const session = createSessionWithLogging('test2', false); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenReturn(true); + const logger = await loggerFactory.createDebugAdapterTracker(session); + if (logger) { + logger.onWillStartSession!(); + } + + verify(fsService.createWriteStream(filePath)).never(); + verify(writeStream.write(anything())).never(); + }); + + test('Create logger using session with logToFile set to true', async () => { + const session = createSessionWithLogging('test3', true); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + const logs: string[] = []; + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenCall((msg) => logs.push(msg)); + + const message = new TestMessage(1, 1, 'test-message'); + const logger = await loggerFactory.createDebugAdapterTracker(session); + + if (logger) { + logger.onWillStartSession!(); + assert.ok(logs.pop()!.includes('Starting Session')); + + logger.onDidSendMessage!(message); + const sentLog = logs.pop(); + assert.ok(sentLog!.includes('Client <-- Adapter')); + assert.ok(sentLog!.includes('test-message')); + + logger.onWillReceiveMessage!(message); + const receivedLog = logs.pop(); + assert.ok(receivedLog!.includes('Client --> Adapter')); + assert.ok(receivedLog!.includes('test-message')); + + logger.onWillStopSession!(); + assert.ok(logs.pop()!.includes('Stopping Session')); + + logger.onError!(new Error('test error message')); + assert.ok(logs.pop()!.includes('Error')); + + logger.onExit!(111, '222'); + const exitLog1 = logs.pop(); + assert.ok(exitLog1!.includes('Exit-Code: 111')); + assert.ok(exitLog1!.includes('Signal: 222')); + + logger.onExit!(undefined, undefined); + const exitLog2 = logs.pop(); + assert.ok(exitLog2!.includes('Exit-Code: 0')); + assert.ok(exitLog2!.includes('Signal: none')); + } + + verify(fsService.createWriteStream(filePath)).once(); + verify(writeStream.write(anything())).times(7); + assert.deepEqual(logs, []); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts new file mode 100644 index 000000000000..a13d4a103208 --- /dev/null +++ b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { anyString, anything, mock, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { createDeferred, sleep } from '../../../../client/common/utils/async'; +import { Common } from '../../../../client/common/utils/localize'; +import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; +import { clearTelemetryReporter } from '../../../../client/telemetry'; +import * as browserApis from '../../../../client/common/vscodeApis/browserApis'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { IPythonSettings } from '../../../../client/common/types'; + +suite('Debugging - Outdated Debugger Prompt tests.', () => { + let promptFactory: OutdatedDebuggerPromptFactory; + let showInformationMessageStub: sinon.SinonStub; + let browserLaunchStub: sinon.SinonStub; + + const ptvsdOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'telemetry', output: 'ptvsd', data: { packageVersion: '4.3.2' } }, + }; + + const debugpyOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'telemetry', output: 'debugpy', data: { packageVersion: '1.0.0' } }, + }; + + setup(() => { + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true }, + } as any) as IPythonSettings); + + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + browserLaunchStub = sinon.stub(browserApis, 'launch'); + + promptFactory = new OutdatedDebuggerPromptFactory(); + }); + + teardown(() => { + sinon.restore(); + clearTelemetryReporter(); + }); + + function createSession(workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python', + }, + id: 'test1', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + test('Show prompt when attaching to ptvsd, more info is NOT clicked', async () => { + showInformationMessageStub.returns(Promise.resolve(undefined)); + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + if (prompter) { + prompter.onDidSendMessage!(ptvsdOutputEvent); + } + + browserLaunchStub.neverCalledWith(anyString()); + + // First call should show info once + + sinon.assert.calledOnce(showInformationMessageStub); + assert(prompter); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + // Can't use deferred promise here + await sleep(1); + + browserLaunchStub.neverCalledWith(anyString()); + // Second time it should not be called, so overall count is one. + sinon.assert.calledOnce(showInformationMessageStub); + }); + + test('Show prompt when attaching to ptvsd, more info is clicked', async () => { + showInformationMessageStub.returns(Promise.resolve(Common.moreInfo)); + + const deferred = createDeferred(); + browserLaunchStub.callsFake(() => deferred.resolve()); + browserLaunchStub.onCall(1).callsFake(() => { + return new Promise(() => deferred.resolve()); + }); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert(prompter); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + await deferred.promise; + + sinon.assert.calledOnce(browserLaunchStub); + + // First call should show info once + sinon.assert.calledOnce(showInformationMessageStub); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + // The second call does not go through the same path. So we just give enough time for the + // operation to complete. + await sleep(1); + + sinon.assert.calledOnce(browserLaunchStub); + + // Second time it should not be called, so overall count is one. + sinon.assert.calledOnce(showInformationMessageStub); + }); + + test("Don't show prompt attaching to debugpy", async () => { + showInformationMessageStub.returns(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert(prompter); + + prompter!.onDidSendMessage!(debugpyOutputEvent); + // Can't use deferred promise here + await sleep(1); + + showInformationMessageStub.neverCalledWith(anything(), anything()); + }); + + const someRequest: DebugProtocol.RunInTerminalRequest = { + seq: 1, + type: 'request', + command: 'runInTerminal', + arguments: { + cwd: '', + args: [''], + }, + }; + const someEvent: DebugProtocol.ContinuedEvent = { + seq: 1, + type: 'event', + event: 'continued', + body: { threadId: 1, allThreadsContinued: true }, + }; + // Notice that this is stdout, not telemetry event. + const someOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'stdout', output: 'ptvsd' }, + }; + + [someRequest, someEvent, someOutputEvent].forEach((message) => { + test(`Don't show prompt when non-telemetry events are seen: ${JSON.stringify(message)}`, async () => { + showInformationMessageStub.returns(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert(prompter); + + prompter!.onDidSendMessage!(message); + // Can't use deferred promise here + await sleep(1); + + showInformationMessageStub.neverCalledWith(anything(), anything()); + }); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts new file mode 100644 index 000000000000..8f3f80fe20c6 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify } from 'ts-mockito'; +import { Disposable } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; +import { IProcessServiceFactory } from '../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; + +suite('Attach to process - attach process provider factory', () => { + let applicationShell: IApplicationShell; + let commandManager: ICommandManager; + let platformService: IPlatformService; + let processServiceFactory: IProcessServiceFactory; + let disposableRegistry: IDisposableRegistry; + + let factory: AttachProcessProviderFactory; + + setup(() => { + applicationShell = mock(ApplicationShell); + commandManager = mock(CommandManager); + platformService = mock(PlatformService); + processServiceFactory = mock(ProcessServiceFactory); + disposableRegistry = []; + + factory = new AttachProcessProviderFactory( + instance(applicationShell), + instance(commandManager), + instance(platformService), + instance(processServiceFactory), + disposableRegistry, + ); + }); + + test('Register commands should not fail', () => { + factory.registerCommands(); + + verify(commandManager.registerCommand(Commands.PickLocalProcess, anything(), anything())).once(); + assert.strictEqual((disposableRegistry as Disposable[]).length, 1); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts new file mode 100644 index 000000000000..b2f044eb59c0 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ProcessService } from '../../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { OSType } from '../../../../client/common/utils/platform'; +import { AttachProcessProvider } from '../../../../client/debugger/extension/attachQuickPick/provider'; +import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; + +suite('Attach to process - process provider', () => { + let platformService: IPlatformService; + let processService: IProcessService; + let processServiceFactory: IProcessServiceFactory; + + let provider: AttachProcessProvider; + + setup(() => { + platformService = mock(PlatformService); + processService = mock(ProcessService); + processServiceFactory = mock(ProcessServiceFactory); + when(processServiceFactory.create()).thenResolve(instance(processService)); + + provider = new AttachProcessProvider(instance(platformService), instance(processServiceFactory)); + }); + + test('The Linux process list command should be called if the platform is Linux', async () => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +1 launchd launchd +41 syslogd syslogd +146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec( + PsProcessParser.psLinuxCommand.command, + PsProcessParser.psLinuxCommand.args, + anything(), + ), + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('The macOS process list command should be called if the platform is macOS', async () => { + when(platformService.isMac).thenReturn(true); + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +1 launchd launchd +41 syslogd syslogd +146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + ]; + when(processService.exec(PsProcessParser.psDarwinCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec( + PsProcessParser.psDarwinCommand.command, + PsProcessParser.psDarwinCommand.args, + anything(), + ), + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('The Windows process list command should be called if the platform is Windows', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + ]; + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(true); + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput, + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, anything()), + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('An error should be thrown if the platform is neither Linux, macOS or Windows', async () => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(false); + when(platformService.osType).thenReturn(OSType.Unknown); + + const promise = provider._getInternalProcessEntries(); + + await expect(promise).to.eventually.be.rejectedWith(`Operating system '${OSType.Unknown}' not supported.`); + }); + + suite('POSIX getAttachItems (Linux)', () => { + setup(() => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + }); + + test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + 1 launchd launchd + 41 syslogd syslogd + 146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + 1 launchd launchd + 41 syslogd syslogd + 96 python python + 146 kextd kextd + 31896 python python script.py +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'python', + description: '96', + detail: 'python', + id: '96', + processName: 'python', + commandLine: 'python', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); + + suite('Windows getAttachItems', () => { + setup(() => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(true); + }); + + test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=\r +Name=svchost.exe\r +ProcessId=5372\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=\r +Name=svchost.exe\r +ProcessId=5372\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +\r +\r +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r +Name=python.exe\r +ProcessId=6028\r +\r +\r +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py\r +Name=python.exe\r +ProcessId=8026\r + `; + const expectedOutput: IAttachItem[] = [ + { + label: 'python.exe', + description: '8026', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + id: '8026', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts new file mode 100644 index 000000000000..f552598e7930 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; + +suite('Attach to process - ps process parser (POSIX)', () => { + test('Processes should be parsed correctly if it is valid input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ + 45 uninstalld uninstalld\n\ + 146 kextd kextd\n\ +31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)', + }, + { + label: 'uninstalld', + description: '45', + detail: 'uninstalld', + id: '45', + processName: 'uninstalld', + commandLine: 'uninstalld', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Empty lines should be skipped when parsing process list input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ +\n\ + 146 kextd kextd\n\ + 31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Incorrectly formatted lines should be skipped when parsing process list input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ + 45 uninstalld uninstalld\n\ + 146 kextd kextd\n\ + 31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts new file mode 100644 index 000000000000..de248d792dfe --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; + +suite('Attach to process - wmic process parser (Windows)', () => { + test('Processes should be parsed correctly if it is valid input', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5372\r\n\ +\r\n\ +\r\n\ +CommandLine=sihost.exe\r\n\ +Name=sihost.exe\r\n\ +ProcessId=5728\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Incorrectly formatted lines should be skipped when parsing process list input', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5372\r\n\ +\r\n\ +\r\n\ +CommandLine=sihost.exe\r\n\ +Name=sihost.exe\r\n\ +ProcessId=5728\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ +Name=svchost.exe\r\n\ +IncorrectKey=shouldnt.be.here\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Command lines starting with a DOS device path prefix should be parsed correctly', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\\??\\C:\\WINDOWS\\system32\\conhost.exe\r\n\ +Name=conhost.exe\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'conhost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\conhost.exe', + id: '5912', + processName: 'conhost.exe', + commandLine: 'C:\\WINDOWS\\system32\\conhost.exe', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); +}); \ No newline at end of file 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..41570e34ee1b --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -0,0 +1,570 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IConfigurationService } from '../../../../../client/common/types'; +import { AttachConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/attach'; +import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; +import { IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { getInfoPerOS } from './common'; +import * as platform from '../../../../../client/common/utils/platform'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +getInfoPerOS().forEach(([osName, osType, path]) => { + if (osType === platform.OSType.Unknown) { + return; + } + + function getAvailableOptions(): string[] { + const options = [DebugOptions.RedirectOutput]; + if (osType === platform.OSType.Windows) { + options.push(DebugOptions.FixFilePathCase); + } + options.push(DebugOptions.ShowReturnValue); + + return options; + } + + suite(`Debugging - Config Resolver attach, OS = ${osName}`, () => { + let debugProvider: DebugConfigurationProvider; + let configurationService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let getActiveTextEditorStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + const debugOptionsAvailable = getAvailableOptions(); + + setup(() => { + configurationService = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + debugProvider = new AttachConfigurationResolver(configurationService.object, interpreterService.object); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + getOSTypeStub = sinon.stub(platform, 'getOSType'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getOSTypeStub.returns(osType); + }); + + teardown(() => { + sinon.restore(); + }); + + 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); + getActiveTextEditorStub.returns(textEditor.object); + } else { + getActiveTextEditorStub.returns(undefined); + } + } + + function getClientOS() { + return osType === platform.OSType.Windows ? 'windows' : 'unix'; + } + + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + getWorkspaceFoldersStub.returns(workspaceFolders); + } + + const attach: Partial = { + name: 'Python attach', + type: 'python', + request: 'attach', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + attachConfig: Partial, + ) { + let config = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + attachConfig as DebugConfiguration, + ); + if (config === undefined || config === null) { + return config; + } + + config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); + if (config === undefined || config === null) { + return config; + } + + return config as AttachRequestArguments; + } + + 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 resolveDebugConfiguration(workspaceFolder, { + request: 'attach', + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + 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 resolveDebugConfiguration(undefined, { + request: 'attach', + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + 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 resolveDebugConfiguration(undefined, { + request: 'attach', + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + 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 resolveDebugConfiguration(undefined, { + request: 'attach', + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + 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 resolveDebugConfiguration(undefined, { + request: 'attach', + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + + test('Default host should not be added if connect is available.', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, { + ...attach, + connect: { host: 'localhost', port: 5678 }, + }); + + expect(debugConfig).to.not.have.property('host', 'localhost'); + }); + + test('Default host should not be added if listen is available.', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, { + ...attach, + listen: { host: 'localhost', port: 5678 }, + } as AttachRequestArguments); + + expect(debugConfig).to.not.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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + + ['localhost', '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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const { pathMappings } = debugConfig as AttachRequestArguments; + 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); + }); + + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}'`, async function () { + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + const { pathMappings } = debugConfig as AttachRequestArguments; + + const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path')).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + + return undefined; + }); + + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}'`, async function () { + if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + const { pathMappings } = debugConfig as AttachRequestArguments; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path')).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + + return undefined; + }); + + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async function () { + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, + ]; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + pathMappings: debugPathMappings, + host, + }); + const { pathMappings } = debugConfig as AttachRequestArguments; + + const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path', localRoot)).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + + return undefined; + }); + + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}' and with existing path mappings`, async function () { + if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, + ]; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + pathMappings: debugPathMappings, + host, + }); + const { pathMappings } = debugConfig as AttachRequestArguments; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; + expect(Uri.file(pathMappings![0].localRoot).fsPath).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + + return undefined; + }); + + test(`Ensure local path mappings are not modified when not pointing to a local drive when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + const { pathMappings } = debugConfig as AttachRequestArguments; + + 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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const { pathMappings } = debugConfig as AttachRequestArguments; + 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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + remoteRoot, + }); + + 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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + remoteRoot, + }); + + 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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + remoteRoot, + }); + + 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 resolveDebugConfiguration(workspaceFolder, { + ...attach, + port, + }); + + 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) as DebugOptions[]; + const expectedDebugOptions = debugOptions.slice(); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + debugOptions, + }); + + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); + }); + + const testsForJustMyCode = [ + { + justMyCode: false, + debugStdLib: true, + expectedResult: false, + }, + { + justMyCode: false, + debugStdLib: false, + expectedResult: false, + }, + { + justMyCode: false, + debugStdLib: undefined, + expectedResult: false, + }, + { + justMyCode: true, + debugStdLib: false, + expectedResult: true, + }, + { + justMyCode: true, + debugStdLib: true, + expectedResult: true, + }, + { + justMyCode: true, + debugStdLib: undefined, + expectedResult: true, + }, + { + justMyCode: undefined, + debugStdLib: false, + expectedResult: true, + }, + { + justMyCode: undefined, + debugStdLib: true, + expectedResult: false, + }, + { + justMyCode: undefined, + debugStdLib: undefined, + expectedResult: true, + }, + ]; + test('Ensure justMyCode property is correctly derived from debugStdLib', 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) as DebugOptions[]; + + testsForJustMyCode.forEach(async (testParams) => { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + debugOptions, + justMyCode: testParams.justMyCode, + debugStdLib: testParams.debugStdLib, + }); + expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + }); + }); + }); +}); + \ No newline at end of file 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..aa20d805fae5 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -0,0 +1,337 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { ConfigurationService } from '../../../../../client/common/configuration/service'; +import { IConfigurationService } from '../../../../../client/common/types'; +import { BaseConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/base'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; +import { IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; +import * as helper from '../../../../../client/debugger/extension/configuration/resolvers/helper'; + +suite('Debugging - Config Resolver', () => { + class BaseResolver extends BaseConfigurationResolver { + public resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + throw new Error('Not Implemented'); + } + + public resolveDebugConfigurationWithSubstitutedVariables( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + throw new Error('Not Implemented'); + } + + public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + return BaseConfigurationResolver.getWorkspaceFolder(folder); + } + + public resolveAndUpdatePythonPath( + workspaceFolderUri: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ) { + return super.resolveAndUpdatePythonPath(workspaceFolderUri, debugConfiguration); + } + + public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + return BaseConfigurationResolver.debugOption(debugOptions, debugOption); + } + + public isLocalHost(hostName?: string) { + return BaseConfigurationResolver.isLocalHost(hostName); + } + + public isDebuggingFastAPI(debugConfiguration: Partial) { + return BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration); + } + + public isDebuggingFlask(debugConfiguration: Partial) { + return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); + } + } + let resolver: BaseResolver; + let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let getProgramStub: sinon.SinonStub; + + setup(() => { + configurationService = mock(ConfigurationService); + interpreterService = mock(); + resolver = new BaseResolver(instance(configurationService), instance(interpreterService)); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getProgramStub = sinon.stub(helper, 'getProgram'); + }); + teardown(() => { + sinon.restore(); + }); + + 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'); + + getProgramStub.returns(programPath); + getWorkspaceFoldersStub.returns(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]; + + getProgramStub.returns(undefined); + + getWorkspaceFolderStub.returns(folder); + + getWorkspaceFoldersStub.returns(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]; + + getProgramStub.returns(programPath); + + getWorkspaceFoldersStub.returns(folders); + + getWorkspaceFolderStub.returns(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]; + + getProgramStub.returns(programPath); + getWorkspaceFoldersStub.returns(folders); + + getWorkspaceFolderStub.returns(undefined); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri).to.be.deep.equal(undefined, 'not undefined'); + }); + test('Do nothing if debug configuration is undefined', async () => { + await resolver.resolveAndUpdatePythonPath(undefined, (undefined as unknown) as LaunchRequestArguments); + }); + test('python in debug config must point to pythonPath in settings if pythonPath in config is not set', async () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.have.property('python', pythonPath); + }); + test('python in debug config must point to pythonPath in settings if pythonPath in config is ${command:python.interpreterPath}', async () => { + const config = { + python: '${command:python.interpreterPath}', + }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config.python).to.equal(pythonPath); + }); + + test('config should only contain python and not pythonPath after resolving', async () => { + const config = { pythonPath: '${command:python.interpreterPath}', python: '${command:python.interpreterPath}' }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + }); + + test('config should convert pythonPath to python, only if python is not set', async () => { + const config = { pythonPath: '${command:python.interpreterPath}', python: undefined }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + }); + + test('config should not change python if python is different than pythonPath', async () => { + const expected = path.join('1', '2', '4'); + const config = { pythonPath: '${command:python.interpreterPath}', python: expected }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', expected); + }); + + test('config should get python from interpreter service is nothing is set', async () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + }); + + test('config should contain debugAdapterPython and debugLauncherPython', async () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + expect(config).to.have.property('debugAdapterPython', pythonPath); + expect(config).to.have.property('debugLauncherPython', pythonPath); + }); + + test('config should not change debugAdapterPython and debugLauncherPython if already set', async () => { + const debugAdapterPythonPath = path.join('1', '2', '4'); + const debugLauncherPythonPath = path.join('1', '2', '5'); + + const config = { debugAdapterPython: debugAdapterPythonPath, debugLauncherPython: debugLauncherPythonPath }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + expect(config).to.have.property('debugAdapterPython', debugAdapterPythonPath); + expect(config).to.have.property('debugLauncherPython', debugLauncherPythonPath); + }); + + test('config should not resolve debugAdapterPython and debugLauncherPython', async () => { + const config = { + debugAdapterPython: '${command:python.interpreterPath}', + debugLauncherPython: '${command:python.interpreterPath}', + }; + const pythonPath = path.join('1', '2', '3'); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPath); + expect(config).to.have.property('debugAdapterPython', pythonPath); + expect(config).to.have.property('debugLauncherPython', pythonPath); + }); + + const localHostTestMatrix: Record = { + 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 fastapi=true', () => { + const config = { module: 'fastapi' }; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(true, 'not fastapi'); + }); + test('Is debugging fastapi=false', () => { + const config = { module: 'fastapi2' }; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(false, 'fastapi'); + }); + test('Is debugging fastapi=false when not defined', () => { + const config = {}; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(false, 'fastapi'); + }); + test('Is debugging flask=true', () => { + const config = { module: 'flask' }; + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); + expect(isFlask).to.equal(true, 'not flask'); + }); + test('Is debugging flask=false', () => { + const config = { module: 'flask2' }; + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); + expect(isFlask).to.equal(false, 'flask'); + }); + test('Is debugging flask=false when not defined', () => { + const config = {}; + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); + expect(isFlask).to.equal(false, 'flask'); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/configuration/resolvers/common.ts b/src/test/debugger/extension/configuration/resolvers/common.ts index 24c0599a04a6..176318305963 100644 --- a/src/test/debugger/extension/configuration/resolvers/common.ts +++ b/src/test/debugger/extension/configuration/resolvers/common.ts @@ -41,4 +41,4 @@ function getPathModuleForOS(osType: OSType): IPathModule { // We are testing a different OS from the native one. // So use a "path" module matching the target OS. return osType === OSType.Windows ? path.win32 : path.posix; -} +} \ No newline at end of file diff --git a/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts new file mode 100644 index 000000000000..a52cadaa59d3 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TextDocument, TextEditor } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import { getProgram } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; + +suite('Debugging - Helpers', () => { + let getActiveTextEditorStub: sinon.SinonStub; + + setup(() => { + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + }); + teardown(() => { + sinon.restore(); + }); + + 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()); + + getActiveTextEditorStub.returns(editor.object); + + const program = 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()); + getActiveTextEditorStub.returns(editor.object); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Program should return undefined if there is no active editor', () => { + getActiveTextEditorStub.returns(undefined); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts new file mode 100644 index 000000000000..4b789b118bdc --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -0,0 +1,1213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../../../client/common/types'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { IDebugEnvironmentVariablesService } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; +import { PythonPathSource } from '../../../../../client/debugger/extension/types'; +import { ConsoleType, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; +import { IInterpreterHelper, IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { getInfoPerOS } from './common'; +import * as platform from '../../../../../client/common/utils/platform'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; +import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; +import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; + +getInfoPerOS().forEach(([osName, osType, path]) => { + if (osType === platform.OSType.Unknown) { + return; + } + + suite(`Debugging - Config Resolver Launch, OS = ${osName}`, () => { + let debugProvider: DebugConfigurationProvider; + let pythonExecutionService: TypeMoq.IMock; + let helper: TypeMoq.IMock; + const envVars = { FOO: 'BAR' }; + + let diagnosticsService: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let debugEnvHelper: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let environmentActivationService: TypeMoq.IMock; + let getActiveTextEditorStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + getOSTypeStub = sinon.stub(platform, 'getOSType'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType(); + folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + + function getClientOS() { + return osType === platform.OSType.Windows ? 'windows' : 'unix'; + } + + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { + environmentActivationService = TypeMoq.Mock.ofType(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + configService = TypeMoq.Mock.ofType(); + diagnosticsService = TypeMoq.Mock.ofType(); + debugEnvHelper = TypeMoq.Mock.ofType(); + pythonExecutionService = TypeMoq.Mock.ofType(); + helper = TypeMoq.Mock.ofType(); + const factory = TypeMoq.Mock.ofType(); + factory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + helper.setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + diagnosticsService + .setup((h) => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + + const settings = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + // interpreterService + // .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + // .returns(() => Promise.resolve({ path: pythonPath } as any)); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + if (workspaceFolder) { + settings.setup((s) => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); + } + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + debugProvider = new LaunchConfigurationResolver( + diagnosticsService.object, + configService.object, + debugEnvHelper.object, + interpreterService.object, + environmentActivationService.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); + getActiveTextEditorStub.returns(textEditor.object); + } else { + getActiveTextEditorStub.returns(undefined); + } + } + + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + getWorkspaceFolderStub.returns(workspaceFolders); + } + + const launch: LaunchRequestArguments = { + name: 'Python launch', + type: 'python', + request: 'launch', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + launchConfig: Partial, + ) { + let config = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + launchConfig as DebugConfiguration, + ); + if (config === undefined || config === null) { + return config; + } + + const interpreterPath = configService.object.getSettings(workspaceFolder ? workspaceFolder.uri : undefined) + .pythonPath; + for (const key of Object.keys(config)) { + const value = config[key]; + if (typeof value === 'string') { + config[key] = value.replace('${command:python.interpreterPath}', interpreterPath); + } + } + + config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); + if (config === undefined || config === null) { + return config; + } + + return config as LaunchRequestArguments; + } + + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, {}); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + expect(debugConfig).to.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); + }); + + test("Defaults should be returned when an object with 'noDebug' property is passed with a Workspace Folder and active file", async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + noDebug: true, + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + expect(debugConfig).to.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); + }); + + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, {}); + const filePath = Uri.file(path.dirname('')).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + expect(debugConfig).to.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); + }); + + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + setupIoc(pythonPath); + setupActiveEditor(undefined, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, {}); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', ''); + expect(debugConfig).not.to.have.property('cwd'); + expect(debugConfig).not.to.have.property('envFile'); + expect(debugConfig).to.have.property('env'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); + }); + + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.js'; + setupIoc(pythonPath); + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, {}); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + expect(debugConfig).to.have.property('program', ''); + expect(debugConfig).not.to.have.property('cwd'); + expect(debugConfig).not.to.have.property('envFile'); + expect(debugConfig).to.have.property('env'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); + }); + + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const defaultWorkspace = path.join('usr', 'desktop'); + setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await resolveDebugConfiguration(undefined, {}); + const filePath = Uri.file(defaultWorkspace).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + expect(debugConfig).to.have.property('program', activeFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); + }); + + test("Ensure 'port' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + port, + }); + + expect(debugConfig).to.have.property('port', port); + }); + + test("Ensure 'localRoot' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + + test("Ensure 'remoteRoot' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + remoteRoot, + }); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + + test("Ensure 'localRoot' and 'remoteRoot' are not used", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + remoteRoot, + }); + + expect(debugConfig!.pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + + test('Ensure non-empty path mappings are used', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const expected = { + localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, + remoteRoot: `Debug_PythonPath_Remote_Root_${new Date().toString()}`, + }; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [expected], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.deep.equal([expected]); + }); + + test('Ensure replacement in path mappings happens', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot: '${workspaceFolder}/spam', + remoteRoot: '${workspaceFolder}/spam', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.deep.equal([ + { + localRoot: `${workspaceFolder.uri.fsPath}/spam`, + remoteRoot: '${workspaceFolder}/spam', + }, + ]); + }); + + test('Ensure path mappings are not automatically added if missing', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + + test('Ensure path mappings are not automatically added if empty', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + pathMappings: [], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + + test('Ensure path mappings are not automatically added to existing', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + pathMappings: [ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ], + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.deep.equal([ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ]); + }); + + test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async function () { + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot, + remoteRoot: '/app/', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; + expect(pathMappings).to.deep.equal([ + { + localRoot: expected, + remoteRoot: '/app/', + }, + ]); + return undefined; + }); + + test('Ensure drive letter is not lower cased for local path mappings on non-Windows when with existing path mappings', async function () { + if (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot, + remoteRoot: '/app/', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.deep.equal([ + { + localRoot, + remoteRoot: '/app/', + }, + ]); + return undefined; + }); + + test('Ensure local path mappings are not modified when not pointing to a local drive', async () => { + const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.deep.equal([ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ]); + }); + + test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pythonPath: '${command:python.interpreterPath}', + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure `${command:python.interpreterPath}` substitution is properly handled', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + python: '${command:python.interpreterPath}', + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure hardcoded pythonPath is left unaltered', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pythonPath: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', debugPythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', debugPythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', debugPythonPath); + }); + + test('Ensure hardcoded "python" is left unaltered', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + python: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', debugPythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure hardcoded "debugAdapterPython" is left unaltered', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugAdapterPython: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', debugPythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', pythonPath); + }); + + test('Ensure hardcoded "debugLauncherPython" is left unaltered', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugLauncherPython: debugPythonPath, + }); + + expect(debugConfig).to.not.have.property('pythonPath'); + expect(debugConfig).to.have.property('python', pythonPath); + expect(debugConfig).to.have.property('debugAdapterPython', pythonPath); + expect(debugConfig).to.have.property('debugLauncherPython', debugPythonPath); + }); + + test('Test defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('debugOptions'); + const expectedOptions = [DebugOptions.ShowReturnValue]; + if (osType === platform.OSType.Windows) { + expectedOptions.push(DebugOptions.FixFilePathCase); + } + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); + }); + + test('Test defaults of python debugger', async () => { + if (DebuggerTypeName === 'python') { + return; + } + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal([]); + }); + + test('Test overriding defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: true, + justMyCode: false, + }); + + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('redirectOutput', true); + expect(debugConfig).to.have.property('justMyCode', false); + expect(debugConfig).to.have.property('debugOptions'); + const expectedOptions = [ + DebugOptions.DebugStdLib, + DebugOptions.ShowReturnValue, + DebugOptions.RedirectOutput, + ]; + if (osType === platform.OSType.Windows) { + expectedOptions.push(DebugOptions.FixFilePathCase); + } + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); + }); + + const testsForJustMyCode = [ + { + justMyCode: false, + debugStdLib: true, + expectedResult: false, + }, + { + justMyCode: false, + debugStdLib: false, + expectedResult: false, + }, + { + justMyCode: false, + debugStdLib: undefined, + expectedResult: false, + }, + { + justMyCode: true, + debugStdLib: false, + expectedResult: true, + }, + { + justMyCode: true, + debugStdLib: true, + expectedResult: true, + }, + { + justMyCode: true, + debugStdLib: undefined, + expectedResult: true, + }, + { + justMyCode: undefined, + debugStdLib: false, + expectedResult: true, + }, + { + justMyCode: undefined, + debugStdLib: true, + expectedResult: false, + }, + { + justMyCode: undefined, + debugStdLib: undefined, + expectedResult: true, + }, + ]; + test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + testsForJustMyCode.forEach(async (testParams) => { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugStdLib: testParams.debugStdLib, + justMyCode: testParams.justMyCode, + }); + expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + }); + }); + + const testsForRedirectOutput = [ + { + console: 'internalConsole', + redirectOutput: undefined, + expectedRedirectOutput: true, + }, + { + console: 'integratedTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined, + }, + { + console: 'externalTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined, + }, + { + console: 'internalConsole', + redirectOutput: false, + expectedRedirectOutput: false, + }, + { + console: 'integratedTerminal', + redirectOutput: false, + expectedRedirectOutput: false, + }, + { + console: 'externalTerminal', + redirectOutput: false, + expectedRedirectOutput: false, + }, + { + console: 'internalConsole', + redirectOutput: true, + expectedRedirectOutput: true, + }, + { + console: 'integratedTerminal', + redirectOutput: true, + expectedRedirectOutput: true, + }, + { + console: 'externalTerminal', + redirectOutput: true, + expectedRedirectOutput: true, + }, + ]; + test('Ensure redirectOutput property is correctly derived from console type', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + testsForRedirectOutput.forEach(async (testParams) => { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + console: testParams.console as ConsoleType, + redirectOutput: testParams.redirectOutput, + }); + expect(debugConfig).to.have.property('redirectOutput', testParams.expectedRedirectOutput); + if (testParams.expectedRedirectOutput) { + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as DebugConfiguration).debugOptions).to.contain(DebugOptions.RedirectOutput); + } + }); + }); + + test('Test fixFilePathCase', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + if (osType === platform.OSType.Windows) { + expect(debugConfig).to.have.property('debugOptions').contains(DebugOptions.FixFilePathCase); + } else { + expect(debugConfig).to.have.property('debugOptions').not.contains(DebugOptions.FixFilePathCase); + } + }); + + test('Jinja added for Pyramid', async () => { + const workspacePath = path.join('usr', 'development', 'wksp1'); + const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); + const workspaceFolder = createMoqWorkspaceFolder(workspacePath); + const pythonFile = 'xyz.py'; + + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugOptions: [DebugOptions.Pyramid], + pyramid: true, + }); + + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); + }); + + test('Auto detect flask debugging', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + module: 'flask', + }); + + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); + }); + + test('Test validation of Python Path when launching debugger (with invalid "python")', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; + const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(pythonPath), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + // Invalid + .returns(() => Promise.resolve(false)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugLauncherPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugAdapterPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + python: pythonPath, + debugLauncherPython, + debugAdapterPython, + }); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); + }); + + test('Test validation of Python Path when launching debugger (with invalid "debugLauncherPython")', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; + const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(pythonPath), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugLauncherPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + // Invalid + .returns(() => Promise.resolve(false)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugAdapterPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + python: pythonPath, + debugLauncherPython, + debugAdapterPython, + }); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); + }); + + test('Test validation of Python Path when launching debugger (with invalid "debugAdapterPython")', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const debugLauncherPython = `DebugLauncherPythonPath_${new Date().toString()}`; + const debugAdapterPython = `DebugAdapterPythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(pythonPath), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugLauncherPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(debugAdapterPython), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + // Invalid + .returns(() => Promise.resolve(false)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + python: pythonPath, + debugLauncherPython, + debugAdapterPython, + }); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); + }); + + test('Test validation of Python Path when launching debugger (with valid "python/debugAdapterPython/debugLauncherPython")', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath( + TypeMoq.It.isValue(pythonPath), + PythonPathSource.launchJson, + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + python: pythonPath, + }); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); + }); + + test('Resolve path to envFile', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + const sep = osType === platform.OSType.Windows ? '\\' : '/'; + const expectedEnvFilePath = `${workspaceFolder.uri.fsPath}${sep}${'wow.envFile'}`; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => Promise.resolve(true)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + pythonPath, + envFile: path.join('${workspaceFolder}', 'wow.envFile'), + }); + + expect(debugConfig!.envFile).to.be.equal(expectedEnvFilePath); + }); + + async function testSetting( + requestType: 'launch' | 'attach', + settings: Record, + debugOptionName: DebugOptions, + mustHaveDebugOption: boolean, + ) { + setupIoc('pythonPath'); + let debugConfig: DebugConfiguration = { + request: requestType, + type: 'python', + name: '', + ...settings, + }; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + + debugConfig = (await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfig))!; + debugConfig = (await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!( + workspaceFolder, + debugConfig, + ))!; + + if (mustHaveDebugOption) { + expect(debugConfig.debugOptions).contains(debugOptionName); + } else { + expect(debugConfig.debugOptions).not.contains(debugOptionName); + } + } + type LaunchOrAttach = 'launch' | 'attach'; + const items: LaunchOrAttach[] = ['launch', 'attach']; + items.forEach((requestType) => { + test(`Must not contain Sub Process when not specified(${requestType})`, async () => { + await testSetting(requestType, {}, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting = false(${requestType})`, async () => { + await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting = true(${requestType})`, async () => { + await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts new file mode 100644 index 000000000000..71916baee0c7 --- /dev/null +++ b/src/test/debugger/extension/debugCommands.unit.test.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { ICommandManager, IDebugService } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import * as telemetry from '../../../client/telemetry'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; + +suite('Debugging - commands', () => { + let commandManager: typemoq.IMock; + let debugService: typemoq.IMock; + let disposables: typemoq.IMock; + let interpreterService: typemoq.IMock; + let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + commandManager = typemoq.Mock.ofType(); + commandManager + .setup((c) => c.executeCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve()); + debugService = typemoq.Mock.ofType(); + disposables = typemoq.Mock.ofType(); + interpreterService = typemoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { + /** noop */ + }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); + }); + teardown(() => { + sinon.restore(); + }); + test('Test registering debug file command', async () => { + commandManager + .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* noop */ + }, + })) + .verifiable(typemoq.Times.once()); + + debugCommands = new DebugCommands( + commandManager.object, + debugService.object, + disposables.object, + interpreterService.object, + ); + await debugCommands.activate(); + commandManager.verifyAll(); + }); + test('Test running debug file command', async () => { + let callback: (f: Uri) => Promise = (_f: Uri) => Promise.resolve(); + commandManager + .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) + .callback((_name, cb) => { + callback = cb; + }); + debugService + .setup((d) => d.startDebugging(undefined, typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + debugCommands = new DebugCommands( + commandManager.object, + debugService.object, + disposables.object, + interpreterService.object, + ); + await debugCommands.activate(); + + await callback(Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'test.py'))); + commandManager.verifyAll(); + debugService.verifyAll(); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts new file mode 100644 index 000000000000..d361b4f2d049 --- /dev/null +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { ChildProcessAttachEventHandler } from '../../../../client/debugger/extension/hooks/childProcessAttachHandler'; +import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; +import { DebuggerEvents } from '../../../../client/debugger/extension/hooks/constants'; +import { AttachRequestArguments } from '../../../../client/debugger/types'; +import { DebuggerTypeName } from '../../../../client/debugger/constants'; + +suite('Debug - Child Process', () => { + test('Do not attach if the event is undefined', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + await handler.handleCustomEvent(undefined as any); + verify(attachService.attach(anything(), anything())).never(); + }); + test('Do not attach to child process if event is invalid', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugger type is different', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: 'other-type' } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if ptvsd_attach event is invalid', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugpy_attach event is invalid', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Exceptions are not bubbled up if exceptions are thrown', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + }; + const session: any = { + configuration: { type: DebuggerTypeName }, + }; + when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); + verify(attachService.attach(body, anything())).once(); + const [, secondArg] = capture(attachService.attach).last(); + expect(secondArg).to.deep.equal(session); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts new file mode 100644 index 000000000000..11f3a3fb7ae6 --- /dev/null +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { DebugService } from '../../../../client/common/application/debugService'; +import { IDebugService } from '../../../../client/common/application/types'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; + +suite('Debug - Attach to Child Process', () => { + let debugService: IDebugService; + let attachService: ChildProcessAttachService; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + + setup(() => { + debugService = mock(DebugService); + attachService = new ChildProcessAttachService(instance(debugService)); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + }); + teardown(() => { + sinon.restore(); + }); + + test('Message is not displayed if debugger is launched', async () => { + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + }; + const session: any = {}; + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); + showErrorMessageStub.returns(undefined); + + await attachService.attach(data, session); + + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(anything(), anything(), anything())).once(); + sinon.assert.notCalled(showErrorMessageStub); + }); + test('Message is displayed if debugger is not launched', async () => { + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + }; + + const session: any = {}; + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); + showErrorMessageStub.resolves(() => {}); + + await attachService.attach(data, session); + + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(anything(), anything(), anything())).once(); + sinon.assert.calledOnce(showErrorMessageStub); + }); + test('Use correct workspace folder', async () => { + const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; + const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; + const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; + + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + workspaceFolder: rightWorkspaceFolder.uri.fsPath, + }; + + const session: any = {}; + getWorkspaceFoldersStub.returns([wkspace1, rightWorkspaceFolder, wkspace2]); + when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + sinon.assert.called(getWorkspaceFoldersStub); + verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); + sinon.assert.notCalled(showErrorMessageStub); + }); + test('Use empty workspace folder if right one is not found', async () => { + const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; + const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; + const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; + + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + workspaceFolder: rightWorkspaceFolder.uri.fsPath, + }; + + const session: any = {}; + getWorkspaceFoldersStub.returns([wkspace1, wkspace2]); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + sinon.assert.called(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + sinon.assert.notCalled(showErrorMessageStub); + }); + test('Validate debug config is passed with the correct params', async () => { + const data: LaunchRequestArguments | AttachRequestArguments = { + request: 'attach', + type: 'python', + name: 'Attach', + port: 1234, + subProcessId: 2, + host: 'localhost', + }; + + const debugConfig = JSON.parse(JSON.stringify(data)); + debugConfig.host = 'localhost'; + const session: any = {}; + + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); + expect(secondArg).to.deep.equal(debugConfig); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); + }); + test('Pass data as is if data is attach debug configuration', async () => { + const data: AttachRequestArguments = { + type: 'python', + request: 'attach', + name: '', + }; + const session: any = {}; + const debugConfig = JSON.parse(JSON.stringify(data)); + + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); + expect(secondArg).to.deep.equal(debugConfig); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); + }); + test('Validate debug config when parent/root parent was attached', async () => { + const data: AttachRequestArguments = { + request: 'attach', + type: 'python', + name: 'Attach', + host: '123.123.123.123', + port: 1234, + subProcessId: 2, + }; + + const debugConfig = JSON.parse(JSON.stringify(data)); + debugConfig.host = data.host; + debugConfig.port = data.port; + debugConfig.request = 'attach'; + const session: any = {}; + + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); + expect(secondArg).to.deep.equal(debugConfig); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); + }); +}); \ No newline at end of file diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 5557415f923c..7e5b1f41a7ff 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -1,25 +1,64 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - 'use strict'; import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { DebugAdapterActivator } from '../../../client/debugger/extension/adapter/activator'; +import { DebugAdapterDescriptorFactory } from '../../../client/debugger/extension/adapter/factory'; +import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/adapter/logging'; +import { OutdatedDebuggerPromptFactory } from '../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; +import { AttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/types'; +import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; +import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; +import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; +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 { LaunchRequestArguments } from '../../../client/debugger/types'; +import { + IDebugAdapterDescriptorFactory, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory, +} from '../../../client/debugger/extension/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../client/debugger/types'; import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceManager } from '../../../client/ioc/types'; -import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; suite('Debugging - Service Registry', () => { let serviceManager: IServiceManager; - setup(() => { serviceManager = mock(ServiceManager); }); test('Registrations', () => { registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton( + IChildProcessAttachService, + ChildProcessAttachService, + ), + ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugSessionEventHandlers, + ChildProcessAttachEventHandler, + ), + ).once(); verify( serviceManager.addSingleton>( IDebugConfigurationResolver, @@ -27,5 +66,48 @@ suite('Debugging - Service Registry', () => { 'launch', ), ).once(); + verify( + serviceManager.addSingleton>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach', + ), + ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugSessionLoggingFactory, + DebugSessionLoggingFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IAttachProcessProviderFactory, + AttachProcessProviderFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugCommands, + ), + ).once(); }); -}); +}); \ No newline at end of file diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 4b11a9837e65..bbb65f0b2e2a 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -599,7 +599,7 @@ suite('Unit Tests - Debug Launcher', () => { { \n\ // "test" debug config \n\ "name": "spam", /* non-empty */ \n\ - "type": "debugpy", /* must be "debugpy" */ \n\ + "type": "python", /* must be "python" */ \n\ "request": "test", /* must be "test" */ \n\ // extra stuff here: \n\ "stopOnEntry": true \n\ From 72ea2e358ce166b3b720e1104c4bb0a40713ab6b Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 09:36:04 -0500 Subject: [PATCH 20/24] fix lint --- src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts | 2 +- src/test/debugger/extension/adapter/factory.unit.test.ts | 2 +- src/test/debugger/extension/adapter/logging.unit.test.ts | 2 +- .../extension/adapter/outdatedDebuggerPrompt.unit.test.ts | 2 +- .../debugger/extension/attachQuickPick/factory.unit.test.ts | 2 +- .../debugger/extension/attachQuickPick/provider.unit.test.ts | 2 +- .../extension/attachQuickPick/psProcessParser.unit.test.ts | 2 +- .../extension/attachQuickPick/wmicProcessParser.unit.test.ts | 2 +- .../extension/configuration/resolvers/attach.unit.test.ts | 1 - .../extension/configuration/resolvers/base.unit.test.ts | 2 +- src/test/debugger/extension/configuration/resolvers/common.ts | 2 +- .../extension/configuration/resolvers/helper.unit.test.ts | 2 +- .../extension/configuration/resolvers/launch.unit.test.ts | 2 +- src/test/debugger/extension/debugCommands.unit.test.ts | 2 +- .../extension/hooks/childProcessAttachHandler.unit.test.ts | 2 +- .../extension/hooks/childProcessAttachService.unit.test.ts | 2 +- src/test/debugger/extension/serviceRegistry.unit.test.ts | 2 +- 17 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts index 0ece567c7a84..04117e9838d1 100644 --- a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts +++ b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -70,4 +70,4 @@ export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory public createDebugAdapterTracker(_session: DebugSession): ProviderResult { return new OutdatedDebuggerPrompt(this.promptCheck); } -} \ No newline at end of file +} diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts index 8ab1d71e2740..5728bf0c34cd 100644 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -313,4 +313,4 @@ suite('Debugging - Adapter Factory', () => { assert.deepStrictEqual(descriptor, debugExecutable); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/adapter/logging.unit.test.ts b/src/test/debugger/extension/adapter/logging.unit.test.ts index cea32d449497..18fbb2b66058 100644 --- a/src/test/debugger/extension/adapter/logging.unit.test.ts +++ b/src/test/debugger/extension/adapter/logging.unit.test.ts @@ -146,4 +146,4 @@ suite('Debugging - Session Logging', () => { verify(writeStream.write(anything())).times(7); assert.deepEqual(logs, []); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts index a13d4a103208..0ab094119a5c 100644 --- a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts +++ b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -177,4 +177,4 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { showInformationMessageStub.neverCalledWith(anything(), anything()); }); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts index 8f3f80fe20c6..4c4deb3cb9ad 100644 --- a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts @@ -48,4 +48,4 @@ suite('Attach to process - attach process provider factory', () => { verify(commandManager.registerCommand(Commands.PickLocalProcess, anything(), anything())).once(); assert.strictEqual((disposableRegistry as Disposable[]).length, 1); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts index b2f044eb59c0..64d9103f3c5d 100644 --- a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts @@ -456,4 +456,4 @@ ProcessId=8026\r assert.deepEqual(output, expectedOutput); }); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts index f552598e7930..160c53a60c40 100644 --- a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts @@ -189,4 +189,4 @@ suite('Attach to process - ps process parser (POSIX)', () => { assert.deepEqual(output, expectedOutput); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts index de248d792dfe..e29490c47926 100644 --- a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts +++ b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts @@ -212,4 +212,4 @@ ProcessId=6028\r\n\ assert.deepEqual(output, expectedOutput); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts index 41570e34ee1b..b245a0b4622f 100644 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -567,4 +567,3 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); }); }); - \ No newline at end of file diff --git a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts index aa20d805fae5..4da645bc34ac 100644 --- a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -334,4 +334,4 @@ suite('Debugging - Config Resolver', () => { const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); expect(isFlask).to.equal(false, 'flask'); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/configuration/resolvers/common.ts b/src/test/debugger/extension/configuration/resolvers/common.ts index 176318305963..24c0599a04a6 100644 --- a/src/test/debugger/extension/configuration/resolvers/common.ts +++ b/src/test/debugger/extension/configuration/resolvers/common.ts @@ -41,4 +41,4 @@ function getPathModuleForOS(osType: OSType): IPathModule { // We are testing a different OS from the native one. // So use a "path" module matching the target OS. return osType === OSType.Windows ? path.win32 : path.posix; -} \ No newline at end of file +} diff --git a/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts index a52cadaa59d3..01205fd0c87c 100644 --- a/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts @@ -67,4 +67,4 @@ suite('Debugging - Helpers', () => { expect(program).to.be.equal(undefined, 'Not undefined'); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 4b789b118bdc..59f61f81cd85 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -1210,4 +1210,4 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); }); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts index 71916baee0c7..7d2463072f06 100644 --- a/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/src/test/debugger/extension/debugCommands.unit.test.ts @@ -90,4 +90,4 @@ suite('Debugging - commands', () => { commandManager.verifyAll(); debugService.verifyAll(); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts index d361b4f2d049..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -69,4 +69,4 @@ suite('Debug - Child Process', () => { const [, secondArg] = capture(attachService.attach).last(); expect(secondArg).to.deep.equal(session); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts index 11f3a3fb7ae6..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -192,4 +192,4 @@ suite('Debug - Attach to Child Process', () => { expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); -}); \ No newline at end of file +}); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 7e5b1f41a7ff..1e9660574f0e 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -110,4 +110,4 @@ suite('Debugging - Service Registry', () => { ), ).once(); }); -}); \ No newline at end of file +}); From 88e1a51abff2ae4c5bfd722ec6e9c5ace5968a3c Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 09:57:28 -0500 Subject: [PATCH 21/24] update imports --- package.json | 2 +- src/client/common/serviceRegistry.ts | 5 ++++ src/client/debugger/types.ts | 30 +++++++++---------- src/client/extensionActivation.ts | 7 +++-- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 2 +- .../diagnostics/serviceRegistry.unit.test.ts | 10 +++---- src/test/common/moduleInstaller.test.ts | 5 ++++ .../extension/serviceRegistry.unit.test.ts | 1 + 9 files changed, 38 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index a00c68f077a8..6ca8fefc79a5 100644 --- a/package.json +++ b/package.json @@ -1148,7 +1148,7 @@ ], "type": "python", "variables": { - "pickProcess": "python.`pickLocalProcess`" + "pickProcess": "python.pickLocalProcess" }, "when": "!virtualWorkspace && shellExecutionSupported" } diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index f230e78eb3eb..8c872c3113ba 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -28,6 +28,7 @@ import { CommandManager } from './application/commandManager'; import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand'; import { DebugService } from './application/debugService'; +import { DebugSessionTelemetry } from './application/debugSessionTelemetry'; import { DocumentManager } from './application/documentManager'; import { Extensions } from './application/extensions'; import { LanguageService } from './application/languageService'; @@ -182,4 +183,8 @@ export function registerTypes(serviceManager: IServiceManager): void { IExtensionSingleActivationService, ReportIssueCommandHandler, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugSessionTelemetry, + ); } diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index e7cf53f656c3..3e884cf8f64f 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -31,7 +31,6 @@ export type PathMapping = { localRoot: string; remoteRoot: string; }; - type Connection = { host?: string; port?: number; @@ -63,6 +62,21 @@ interface ICommonDebugArguments { clientOS?: 'windows' | 'unix'; } +interface IKnownAttachDebugArguments extends ICommonDebugArguments { + workspaceFolder?: string; + customDebugger?: boolean; + // localRoot and remoteRoot are deprecated (replaced by pathMappings). + localRoot?: string; + remoteRoot?: string; + + // Internal field used to attach to subprocess using python debug adapter + subProcessId?: number; + + processId?: number | string; + connect?: Connection; + listen?: Connection; +} + interface IKnownLaunchRequestArguments extends ICommonDebugArguments { sudo?: boolean; pyramid?: boolean; @@ -104,20 +118,6 @@ interface IKnownLaunchRequestArguments extends ICommonDebugArguments { // Defines where the purpose where the config should be used. purpose?: DebugPurpose[]; } -interface IKnownAttachDebugArguments extends ICommonDebugArguments { - workspaceFolder?: string; - customDebugger?: boolean; - // localRoot and remoteRoot are deprecated (replaced by pathMappings). - localRoot?: string; - remoteRoot?: string; - - // Internal field used to attach to subprocess using python debug adapter - subProcessId?: number; - - processId?: number | string; - connect?: Connection; - listen?: Connection; -} export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 2d81cb3b186c..7383129ef135 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -4,6 +4,7 @@ 'use strict'; import { languages, window } from 'vscode'; + import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; @@ -39,15 +40,15 @@ import * as pythonEnvironments from './pythonEnvironments'; import { ActivationResult, ExtensionState } from './components'; import { Components } from './extensionInit'; import { setDefaultLanguageServer } from './activation/common/defaultlanguageServer'; +import { DebugService } from './common/application/debugService'; +import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; +import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; import { logAndNotifyOnLegacySettings } from './logging/settingLogs'; -import { DebugService } from './common/application/debugService'; -import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; -import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; export async function activateComponents( // `ext` is passed to any extra activation funcs. diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index dae41e15d784..072537619224 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -34,6 +34,7 @@ export enum EventName { ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', EXECUTION_CODE = 'EXECUTION_CODE', EXECUTION_DJANGO = 'EXECUTION_DJANGO', + DEBUG_IN_TERMINAL_BUTTON = 'DEBUG.IN_TERMINAL', DEBUG_ADAPTER_USING_WHEELS_PATH = 'DEBUG_ADAPTER.USING_WHEELS_PATH', DEBUG_SESSION_ERROR = 'DEBUG_SESSION.ERROR', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 168ea18bf722..42a73fb06e07 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -11,6 +11,7 @@ import { AppinsightsKey, EXTENSION_ROOT_DIR, isTestExecution, isUnitTestExecutio import type { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; +import { ConsoleType, TriggerType } from '../debugger/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; import { TensorBoardPromptSelection, @@ -20,7 +21,6 @@ import { } from '../tensorBoard/constants'; import { EventName } from './constants'; import type { TestTool } from './types'; -import { ConsoleType, TriggerType } from '../debugger/types'; /** * Checks whether telemetry is supported. diff --git a/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/src/test/application/diagnostics/serviceRegistry.unit.test.ts index ecc2ae1c5c83..dcff47b2b7e7 100644 --- a/src/test/application/diagnostics/serviceRegistry.unit.test.ts +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -11,9 +11,9 @@ import { EnvironmentPathVariableDiagnosticsServiceId, } from '../../../client/application/diagnostics/checks/envPathVariable'; import { - InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId, -} from '../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; + InvalidLaunchJsonDebuggerService, + InvalidLaunchJsonDebuggerServiceId, +} from '../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; import { JediPython27NotSupportedDiagnosticService, JediPython27NotSupportedDiagnosticServiceId, @@ -80,8 +80,8 @@ suite('Application Diagnostics - Register classes in IOC Container', () => { verify( serviceManager.addSingleton( IDiagnosticsService, - InvalidPythonPathInDebuggerService, - InvalidPythonPathInDebuggerServiceId, + InvalidLaunchJsonDebuggerService, + InvalidLaunchJsonDebuggerServiceId, ), ); verify( diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index f55d2bb00f26..6d1d153aba94 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -13,6 +13,7 @@ import { CommandManager } from '../../client/common/application/commandManager'; import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; import { DebugService } from '../../client/common/application/debugService'; +import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; import { DocumentManager } from '../../client/common/application/documentManager'; import { Extensions } from '../../client/common/application/extensions'; import { @@ -255,6 +256,10 @@ suite('Module Installer', () => { IExtensionSingleActivationService, ReportIssueCommandHandler, ); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugSessionTelemetry, + ); } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance( diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts index 1e9660574f0e..056d722c7e0e 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; import { instance, mock, verify } from 'ts-mockito'; From 6c5413d8d40d16ce7302616edcd84badb8b66208 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 10:09:04 -0500 Subject: [PATCH 22/24] fix tests --- .../debugger/extension/serviceRegistry.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index a79f8482e50d..fe4f91e14116 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -1,23 +1,30 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IServiceManager } from '../../ioc/types'; -import { LaunchRequestArguments } from '../types'; -import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; -import { IDebugConfigurationResolver } from './configuration/types'; -import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; -import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; -import { ChildProcessAttachService } from './hooks/childProcessAttachService'; -import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; +'use strict'; + import { IExtensionSingleActivationService } from '../../activation/types'; +import { IServiceManager } from '../../ioc/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../types'; import { DebugAdapterActivator } from './adapter/activator'; -import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from './types'; import { DebugAdapterDescriptorFactory } from './adapter/factory'; +import { DebugSessionLoggingFactory } from './adapter/logging'; import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt'; -import { IAttachProcessProviderFactory } from './attachQuickPick/types'; import { AttachProcessProviderFactory } from './attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from './attachQuickPick/types'; +import { AttachConfigurationResolver } from './configuration/resolvers/attach'; +import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; +import { IDebugConfigurationResolver } from './configuration/types'; import { DebugCommands } from './debugCommands'; -import { DebugSessionLoggingFactory } from './adapter/logging'; +import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; +import { ChildProcessAttachService } from './hooks/childProcessAttachService'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; +import { + IDebugAdapterDescriptorFactory, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory, +} from './types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); @@ -27,6 +34,11 @@ export function registerTypes(serviceManager: IServiceManager): void { LaunchConfigurationResolver, 'launch', ); + serviceManager.addSingleton>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach', + ); serviceManager.addSingleton( IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper, From 622932f311853c97f85271f4c29457a356c5ce58 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 10:13:30 -0500 Subject: [PATCH 23/24] run prettier --- src/client/debugger/extension/serviceRegistry.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index fe4f91e14116..b4baca5d9ce7 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -20,11 +20,7 @@ import { DebugCommands } from './debugCommands'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; -import { - IDebugAdapterDescriptorFactory, - IDebugSessionLoggingFactory, - IOutdatedDebuggerPromptFactory, -} from './types'; +import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from './types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); From 14ec44f267c69de2c4066b15048d08d46b1c8055 Mon Sep 17 00:00:00 2001 From: Paula Camargo Date: Mon, 8 Jan 2024 10:42:54 -0500 Subject: [PATCH 24/24] add python debugger in extension pack --- README.md | 2 +- gulpfile.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8029aa096587..3a56a09f73ed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python extension for Visual Studio Code -A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the language: >=3.7), including features such as IntelliSense (Pylance), linting, debugging, code navigation, code formatting, refactoring, variable explorer, test explorer, and more! +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the language: >=3.7), including features such as IntelliSense (Pylance), linting, debugging (Python Debugger), code navigation, code formatting, refactoring, variable explorer, test explorer, and more! ## Support for [vscode.dev](https://vscode.dev/) diff --git a/gulpfile.js b/gulpfile.js index 5336deafee29..da46943f7335 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -98,7 +98,7 @@ async function addExtensionPackDependencies() { // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - packageJson.extensionPack = ['ms-python.vscode-pylance'].concat( + packageJson.extensionPack = ['ms-python.vscode-pylance', 'ms-python.debugpy'].concat( packageJson.extensionPack ? packageJson.extensionPack : [], ); // Remove potential duplicates. diff --git a/package.json b/package.json index 6ca8fefc79a5..fddd5974d0f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "python", "displayName": "Python", - "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", + "description": "IntelliSense (Pylance), Linting, Debugging (Python Debugger), code formatting, refactoring, unit tests, and more.", "version": "2023.23.0-dev", "featureFlags": { "usingNewInterpreterStorage": true