From 0386fed1205115993f2442ecd337797303d3f24b Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Mon, 9 Nov 2020 12:22:46 -0800 Subject: [PATCH] Fix #14674: Enable overriding "pythonPath" in the launcher Fix #12462: Update launch.json schema to add "python" and remove "pythonPath" Split the "pythonPath" debug property into "python", "debugAdapterPython", and "debugLauncherPython". Do most debug config validation on fully expanded property values via resolveDebugConfigurationWithSubstitutedVariables(). Add fixups for legacy launch.json with "pythonPath". --- news/1 Enhancements/12462.md | 1 + package.json | 6 +- .../checks/invalidLaunchJsonDebugger.ts | 10 + .../debugger/extension/adapter/factory.ts | 10 +- .../debugConfigurationService.ts | 14 + .../configuration/resolvers/attach.ts | 4 +- .../extension/configuration/resolvers/base.ts | 30 +- .../configuration/resolvers/launch.ts | 76 +++- .../debugger/extension/configuration/types.ts | 6 + src/client/debugger/types.ts | 24 +- src/client/testing/common/debugLauncher.ts | 12 +- .../invalidLaunchJsonDebugger.unit.test.ts | 5 +- .../extension/adapter/factory.unit.test.ts | 20 +- .../resolvers/attach.unit.test.ts | 197 +++++---- .../configuration/resolvers/base.unit.test.ts | 18 +- .../resolvers/launch.unit.test.ts | 408 +++++++++++++----- .../testing/common/debugLauncher.unit.test.ts | 17 +- 17 files changed, 626 insertions(+), 232 deletions(-) create mode 100644 news/1 Enhancements/12462.md diff --git a/news/1 Enhancements/12462.md b/news/1 Enhancements/12462.md new file mode 100644 index 000000000000..38f2302b84fb --- /dev/null +++ b/news/1 Enhancements/12462.md @@ -0,0 +1 @@ +Replaced "pythonPath" debug configuration property with "python". \ No newline at end of file diff --git a/package.json b/package.json index b48a3c400202..1583a2496294 100644 --- a/package.json +++ b/package.json @@ -1571,9 +1571,9 @@ "description": "Absolute path to the program.", "default": "${file}" }, - "pythonPath": { + "python": { "type": "string", - "description": "Path (fully qualified) to python executable. Defaults to the value in settings", + "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.", "default": "${command:python.interpreterPath}" }, "pythonArgs": { @@ -3685,4 +3685,4 @@ "publisherDisplayName": "Microsoft", "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} \ No newline at end of file +} diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts index a545764d26ec..048ad7cef142 100644 --- a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -71,6 +71,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { true ); } + public async diagnose(resource: Resource): Promise { if (!this.workspaceService.hasWorkspaceFolders) { return []; @@ -80,9 +81,11 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { : 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) { if (!this.workspaceService.hasWorkspaceFolders) { return; @@ -94,6 +97,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { ) ); } + private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { const launchJson = this.getLaunchJsonFile(workspaceFolder); if (!(await this.fs.fileExists(launchJson))) { @@ -114,6 +118,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource)); } if ( + fileContents.indexOf('"pythonPath":') > 0 || fileContents.indexOf('{config:python.pythonPath}') > 0 || fileContents.indexOf('{config:python.interpreterPath}') > 0 ) { @@ -123,6 +128,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { } return diagnostics; } + private async handleDiagnostic(diagnostic: IDiagnostic): Promise { if (!this.canHandle(diagnostic)) { return; @@ -147,6 +153,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { await this.messageService.handle(diagnostic, { commandPrompts }); } + private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { return; @@ -169,6 +176,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { break; } case DiagnosticCodes.ConfigPythonPathDiagnostic: { + fileContents = this.findAndReplace(fileContents, '"pythonPath":', '"python":'); fileContents = this.findAndReplace( fileContents, '{config:python.pythonPath}', @@ -188,10 +196,12 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { await this.fs.writeFile(launchJson, fileContents); } + private findAndReplace(fileContents: string, search: string, replace: string) { const searchRegex = new RegExp(search, 'g'); return fileContents.replace(searchRegex, replace); } + private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); } diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 6379874d8e47..58149bdf9e91 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -27,6 +27,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IApplicationShell) private readonly appShell: IApplicationShell ) {} + public async createDebugAdapterDescriptor( session: DebugSession, _executable: DebugAdapterExecutable | undefined @@ -54,7 +55,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac } } - const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder); + const pythonPath = await this.getDebugAdapterPython(configuration, session.workspaceFolder); if (pythonPath.length !== 0) { if (configuration.request === 'attach' && configuration.processId !== undefined) { sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); @@ -96,13 +97,16 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @returns {Promise} Path to the python interpreter for this workspace. * @memberof DebugAdapterDescriptorFactory */ - private async getPythonPath( + private async getDebugAdapterPython( configuration: LaunchRequestArguments | AttachRequestArguments, workspaceFolder?: WorkspaceFolder ): Promise { - if (configuration.pythonPath) { + if (configuration.debugAdapterPython !== undefined) { + return configuration.debugAdapterPython; + } else if (configuration.pythonPath) { return configuration.pythonPath; } + const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined; const interpreter = await this.interpreterService.getActiveInterpreter(resourceUri); if (interpreter) { diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index 655de8b6a5df..baabb3026dfc 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -29,6 +29,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi private readonly providerFactory: IDebugConfigurationProviderFactory, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory ) {} + public async provideDebugConfigurations( folder: WorkspaceFolder | undefined, token?: CancellationToken @@ -46,6 +47,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi return [state.config as DebugConfiguration]; } } + public async resolveDebugConfiguration( folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, @@ -76,6 +78,18 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi ); } } + + 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); + } + protected async pickDebugConfiguration( input: IMultiStepInput, state: DebugConfigurationState diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts index bb8fdf913bed..2a641502d865 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -21,7 +21,8 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver implements IDebugConfigurationResolver { protected pythonPathSource: PythonPathSource = PythonPathSource.launchJson; + constructor( protected readonly workspaceService: IWorkspaceService, protected readonly documentManager: IDocumentManager, protected readonly platformService: IPlatformService, protected readonly configurationService: IConfigurationService ) {} - public abstract resolveDebugConfiguration( + + // This is a legacy hook used solely for backwards-compatible manual substitution + // of ${command:python.interpreterPath} in "pythonPath", for the sake of other + // existing implementations of resolveDebugConfiguration() that may rely on it. + // + // For all future config variables, expansion should be performed by VSCode itself, + // and validation of debug configuration in derived classes should be performed in + // resolveDebugConfigurationWithSubstitutedVariables() instead, where all variables + // are already substituted. + public async resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + _token?: CancellationToken + ): Promise { + return debugConfiguration as T; + } + + public abstract resolveDebugConfigurationWithSubstitutedVariables( folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken ): Promise; + protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { if (folder) { return folder.uri; @@ -56,12 +75,14 @@ export abstract class BaseConfigurationResolver } } } + protected getProgram(): string | undefined { const editor = this.documentManager.activeTextEditor; if (editor && editor.document.languageId === PYTHON_LANGUAGE) { return editor.document.fileName; } } + protected resolveAndUpdatePaths( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments @@ -69,6 +90,7 @@ export abstract class BaseConfigurationResolver this.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); } + protected resolveAndUpdateEnvFilePath( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments @@ -84,6 +106,7 @@ export abstract class BaseConfigurationResolver debugConfiguration.envFile = systemVariables.resolveAny(debugConfiguration.envFile); } } + protected resolveAndUpdatePythonPath( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments @@ -99,16 +122,19 @@ export abstract class BaseConfigurationResolver this.pythonPathSource = PythonPathSource.launchJson; } } + protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { if (debugOptions.indexOf(debugOption) >= 0) { return; } debugOptions.push(debugOption); } + protected isLocalHost(hostName?: string) { const LocalHosts = ['localhost', '127.0.0.1', '::1']; return hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0 ? true : false; } + protected fixUpPathMappings( pathMappings: PathMapping[], defaultLocalRoot?: string, @@ -153,9 +179,11 @@ export abstract class BaseConfigurationResolver return pathMappings; } + protected isDebuggingFlask(debugConfiguration: Partial) { return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK' ? true : false; } + protected sendTelemetry( trigger: 'launch' | 'attach' | 'test', debugConfiguration: Partial diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index 9d78188207df..a019cef5f71d 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -29,47 +29,69 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - const workspaceFolder = this.getWorkspaceFolder(folder); - - const config = debugConfiguration as LaunchRequestArguments; - const numberOfSettings = Object.keys(config); - - if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { + if ( + debugConfiguration.name === undefined && + debugConfiguration.type === undefined && + debugConfiguration.request === undefined && + debugConfiguration.program === undefined && + debugConfiguration.env === undefined + ) { const defaultProgram = this.getProgram(); - - config.name = 'Launch'; - config.type = DebuggerTypeName; - config.request = 'launch'; - config.program = defaultProgram ? defaultProgram : ''; - config.env = {}; + debugConfiguration.name = 'Launch'; + debugConfiguration.type = DebuggerTypeName; + debugConfiguration.request = 'launch'; + debugConfiguration.program = defaultProgram ?? ''; + debugConfiguration.env = {}; } - await this.provideLaunchDefaults(workspaceFolder, config); + const workspaceFolder = this.getWorkspaceFolder(folder); + this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration); + return debugConfiguration; + } - const isValid = await this.validateLaunchConfiguration(folder, config); + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken + ): Promise { + const workspaceFolder = this.getWorkspaceFolder(folder); + await this.provideLaunchDefaults(workspaceFolder, debugConfiguration); + + const isValid = await this.validateLaunchConfiguration(folder, debugConfiguration); if (!isValid) { return; } - const dbgConfig = debugConfiguration; - if (Array.isArray(dbgConfig.debugOptions)) { - dbgConfig.debugOptions = dbgConfig.debugOptions!.filter( - (item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos + if (Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = debugConfiguration.debugOptions!.filter( + (item, pos) => debugConfiguration.debugOptions!.indexOf(item) === pos ); } return debugConfiguration; } + // tslint:disable-next-line:cyclomatic-complexity protected async provideLaunchDefaults( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments ): Promise { - this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration); + if (debugConfiguration.python === undefined) { + debugConfiguration.python = debugConfiguration.pythonPath; + } + if (debugConfiguration.debugAdapterPython === undefined) { + debugConfiguration.debugAdapterPython = debugConfiguration.pythonPath; + } + if (debugConfiguration.debugLauncherPython === undefined) { + debugConfiguration.debugLauncherPython = debugConfiguration.pythonPath; + } + delete debugConfiguration.pythonPath; + if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { debugConfiguration.cwd = workspaceFolder.fsPath; } @@ -160,10 +182,18 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { const diagnosticService = this.invalidPythonPathInDebuggerService; - return diagnosticService.validatePythonPath( - debugConfiguration.pythonPath, - this.pythonPathSource, - folder ? folder.uri : undefined + return ( + diagnosticService.validatePythonPath(debugConfiguration.python, this.pythonPathSource, folder?.uri) && + diagnosticService.validatePythonPath( + debugConfiguration.debugAdapterPython, + this.pythonPathSource, + folder?.uri + ) && + diagnosticService.validatePythonPath( + debugConfiguration.debugLauncherPython, + this.pythonPathSource, + folder?.uri + ) ); } } diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts index 1331ea39551a..947074f08292 100644 --- a/src/client/debugger/extension/configuration/types.ts +++ b/src/client/debugger/extension/configuration/types.ts @@ -13,6 +13,12 @@ export interface IDebugConfigurationResolver { debugConfiguration: T, token?: CancellationToken ): Promise; + + resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken + ): Promise; } export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 68ad0e49a149..eb6e9b19ab9e 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -70,19 +70,35 @@ export interface IKnownLaunchRequestArguments extends ICommonDebugArguments { // An absolute path to the program to debug. module?: string; program?: string; - pythonPath: string; + python?: string; // Automatically stop target after launch. If not specified, target does not stop. stopOnEntry?: boolean; - args: string[]; + args?: string[]; cwd?: string; debugOptions?: DebugOptions[]; env?: Record; - envFile: string; + envFile?: string; console?: ConsoleType; - // Internal field used to set custom python debug adapter (for testing) + // The following are all internal properties that are not publicly documented or + // exposed in launch.json schema for the extension. + + // Python interpreter used by the extension to spawn the debug adapter. + debugAdapterPython?: string; + + // Debug adapter to use in lieu of the one bundled with the extension. + // This must be a full path that is executable with "python "; + // for debugpy, this is ".../src/debugpy/adapter". debugAdapterPath?: string; + + // Python interpreter used by the debug adapter to spawn the debug launcher. + debugLauncherPython?: string; + + // Legacy interpreter setting. Equivalent to setting "python", "debugAdapterPython", + // and "debugLauncherPython" all at once. + pythonPath?: string; } + // tslint:disable-next-line:interface-name export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 969c02e19eb7..23e475313833 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -164,7 +164,7 @@ export class DebugLauncher implements ITestDebugLauncher { configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. - const launchArgs = await this.launchResolver.resolveDebugConfiguration( + let launchArgs = await this.launchResolver.resolveDebugConfiguration( workspaceFolder, configArgs, options.token @@ -172,9 +172,17 @@ export class DebugLauncher implements ITestDebugLauncher { if (!launchArgs) { throw Error(`Invalid debug config "${debugConfig.name}"`); } + launchArgs = await this.launchResolver.resolveDebugConfigurationWithSubstitutedVariables( + workspaceFolder, + launchArgs, + options.token + ); + if (!launchArgs) { + throw Error(`Invalid debug config "${debugConfig.name}"`); + } launchArgs.request = 'launch'; - return launchArgs!; + return launchArgs; } private fixArgs(args: string[], testProvider: TestProvider): string[] { diff --git a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts index 3c27c5561935..b9e7a61eabc2 100644 --- a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +++ b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts @@ -496,9 +496,8 @@ suite('Application Diagnostics - Checks if launch.json is invalid', () => { }); test('File launch.json is fixed correctly when code equals ConfigPythonPathDiagnostic ', async () => { - const launchJson = 'This string contains {config:python.pythonPath} & {config:python.interpreterPath}'; - const correctedlaunchJson = - 'This string contains {command:python.interpreterPath} & {command:python.interpreterPath}'; + const launchJson = '"pythonPath": "{config:python.pythonPath}{config:python.interpreterPath}"'; + const correctedlaunchJson = '"python": "{command:python.interpreterPath}{command:python.interpreterPath}"'; workspaceService .setup((w) => w.hasWorkspaceFolders) .returns(() => true) diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts index bc9365483754..763e18cd0a0c 100644 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -237,7 +237,7 @@ suite('Debugging - Adapter Factory', () => { assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); }); - test('Use custom debug adapter path when specified', async () => { + test('Use "debugAdapterPath" when specified', async () => { const customAdapterPath = 'custom/debug/adapter/path'; const session = createSession({ debugAdapterPath: customAdapterPath }); const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); @@ -246,4 +246,22 @@ suite('Debugging - Adapter Factory', () => { assert.deepEqual(descriptor, debugExecutable); }); + + test('Use "debugAdapterPython" when specified', async () => { + const session = createSession({ debugAdapterPython: '/bin/custompy' }); + const debugExecutable = new DebugAdapterExecutable('/bin/custompy', [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(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.deepEqual(descriptor, debugExecutable); + }); }); 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 474e8bf55601..0d3355623467 100644 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -45,6 +45,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let configurationService: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; const debugOptionsAvailable = getAvailableOptions(); + setup(() => { serviceContainer = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); @@ -64,11 +65,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { configurationService.object ); }); + function createMoqWorkspaceFolder(folderPath: string) { const folder = TypeMoq.Mock.ofType(); folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); return folder.object; } + function setupActiveEditor(fileName: string | undefined, languageId: string) { if (fileName) { const textEditor = TypeMoq.Mock.ofType(); @@ -84,6 +87,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) .returns(() => documentManager.object); } + function setupWorkspaces(folders: string[]) { const workspaceFolders = folders.map(createMoqWorkspaceFolder); workspaceService.setup((w) => w.workspaceFolders).returns(() => workspaceFolders); @@ -91,57 +95,87 @@ getInfoPerOS().forEach(([osName, osType, path]) => { .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); } + + 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 debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { request: 'attach' - } as DebugConfiguration); + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); expect(debugConfig).to.have.property('request', 'attach'); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { const pythonFile = 'xyz.py'; setupActiveEditor(pythonFile, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + const debugConfig = await resolveDebugConfiguration(undefined, { request: 'attach' - } as DebugConfiguration); + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { setupActiveEditor(undefined, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + const debugConfig = await resolveDebugConfiguration(undefined, { request: 'attach' - } as DebugConfiguration); + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { const activeFile = 'xyz.js'; setupActiveEditor(activeFile, 'javascript'); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + const debugConfig = await resolveDebugConfiguration(undefined, { request: 'attach' - } as DebugConfiguration); + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); @@ -149,47 +183,51 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).to.not.have.property('localRoot'); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { const activeFile = 'xyz.py'; setupActiveEditor(activeFile, PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + const debugConfig = await resolveDebugConfiguration(undefined, { request: 'attach' - } as DebugConfiguration); + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Default host should not be added if connect is available.', async () => { const pythonFile = 'xyz.py'; setupActiveEditor(pythonFile, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { - request: 'attach', + const debugConfig = await resolveDebugConfiguration(undefined, { + ...attach, connect: { host: 'localhost', port: 5678 } - } as AttachRequestArguments); + }); 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 debugProvider.resolveDebugConfiguration!(undefined, { - request: 'attach', + 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); @@ -198,13 +236,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - localRoot, - request: 'attach' - } as any) as DebugConfiguration); + 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'; @@ -214,11 +253,11 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); expect(debugConfig).to.have.property('localRoot', localRoot); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; @@ -226,6 +265,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { 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 (getOSType() !== OSType.Windows || osType !== OSType.Windows) { return this.skip(); @@ -237,17 +277,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; 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); }); + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}'`, async function () { if (getOSType() === OSType.Windows || osType === OSType.Windows) { return this.skip(); @@ -259,17 +300,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; 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); }); + 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 (getOSType() !== OSType.Windows || osType !== OSType.Windows) { return this.skip(); @@ -284,18 +326,19 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugPathMappings = [ { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' } ]; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, pathMappings: debugPathMappings, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; 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/'); }); + 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 (getOSType() === OSType.Windows || osType === OSType.Windows) { return this.skip(); @@ -310,18 +353,19 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugPathMappings = [ { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' } ]; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, pathMappings: debugPathMappings, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; expect(pathMappings![0].localRoot).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); }); + 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')); @@ -330,17 +374,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; 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'; @@ -350,17 +395,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - host, - request: 'attach' - } as any) as DebugConfiguration); + host + }); expect(debugConfig).to.have.property('localRoot', localRoot); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; expect(pathMappings || []).to.be.lengthOf(0); }); }); + test("Ensure 'localRoot' and 'remoteRoot' is used", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -370,15 +416,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - remoteRoot, - request: 'attach' - } as any) as DebugConfiguration); + 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); @@ -388,15 +435,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, localRoot, - remoteRoot, - request: 'attach' - } as any) as DebugConfiguration); + 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); @@ -405,13 +453,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - remoteRoot, - request: 'attach' - } as any) as DebugConfiguration); + 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); @@ -420,10 +469,10 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const port = 12341234; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - port, - request: 'attach' - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + port + }); expect(debugConfig).to.have.property('port', port); }); @@ -434,12 +483,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const debugOptions = debugOptionsAvailable + .slice() + .concat(DebugOptions.Jinja, DebugOptions.Sudo) as DebugOptions[]; const expectedDebugOptions = debugOptions.slice(); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - debugOptions, - request: 'attach' - } as any) as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + debugOptions + }); expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); }); @@ -498,15 +549,17 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const debugOptions = debugOptionsAvailable + .slice() + .concat(DebugOptions.Jinja, DebugOptions.Sudo) as DebugOptions[]; testsForJustMyCode.forEach(async (testParams) => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, debugOptions, - request: 'attach', justMyCode: testParams.justMyCode, debugStdLib: testParams.debugStdLib - } as any) as DebugConfiguration); + }); 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 index 1c8854e5a8bb..30f8f6eeb344 100644 --- a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -31,24 +31,38 @@ suite('Debugging - Config Resolver', () => { ): 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 super.getWorkspaceFolder(folder); } + public getProgram(): string | undefined { return super.getProgram(); } + public resolveAndUpdatePythonPath( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments ): void { return super.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); } + public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { return super.debugOption(debugOptions, debugOption); } + public isLocalHost(hostName?: string) { return super.isLocalHost(hostName); } + public isDebuggingFlask(debugConfiguration: Partial) { return super.isDebuggingFlask(debugConfiguration); } @@ -185,7 +199,7 @@ suite('Debugging - Config Resolver', () => { test('Do nothing if debug configuration is undefined', () => { resolver.resolveAndUpdatePythonPath(undefined, undefined as any); }); - test('Python path in debug config must point to pythonpath in settings if pythonPath in config is not set', () => { + test('pythonPath in debug config must point to pythonPath in settings if pythonPath in config is not set', () => { const config = {}; const pythonPath = path.join('1', '2', '3'); @@ -195,7 +209,7 @@ suite('Debugging - Config Resolver', () => { expect(config).to.have.property('pythonPath', pythonPath); }); - test('Python path in debug config must point to pythonpath in settings if pythonPath in config is ${command:python.interpreterPath}', () => { + test('pythonPath in debug config must point to pythonPath in settings if pythonPath in config is ${command:python.interpreterPath}', () => { const config = { pythonPath: '${command:python.interpreterPath}' }; 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 ce0796459b32..c8d292afe2da 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -33,17 +33,20 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let platformService: TypeMoq.IMock; let pythonExecutionService: TypeMoq.IMock; let helper: TypeMoq.IMock; + let configService: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; let documentManager: TypeMoq.IMock; let diagnosticsService: TypeMoq.IMock; let debugEnvHelper: TypeMoq.IMock; + function createMoqWorkspaceFolder(folderPath: string) { const folder = TypeMoq.Mock.ofType(); folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); return folder.object; } + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { - const configService = TypeMoq.Mock.ofType(); + configService = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); @@ -83,6 +86,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { debugEnvHelper.object ); } + function setupActiveEditor(fileName: string | undefined, languageId: string) { if (fileName) { const textEditor = TypeMoq.Mock.ofType(); @@ -95,27 +99,63 @@ getInfoPerOS().forEach(([osName, osType, path]) => { documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); } } + function setupWorkspaces(folders: string[]) { const workspaceFolders = folders.map(createMoqWorkspaceFolder); workspaceService.setup((w) => w.workspaceFolders).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 debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, {}); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', 'python'); expect(debugConfig).to.have.property('request', 'launch'); + 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()); @@ -125,6 +165,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).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); @@ -132,14 +173,17 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath, workspaceFolder); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { noDebug: true - } as any) as DebugConfiguration); + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', 'python'); expect(debugConfig).to.have.property('request', 'launch'); + 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()); @@ -149,6 +193,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).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'; @@ -156,13 +201,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupActiveEditor(pythonFile, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + 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('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', 'python'); expect(debugConfig).to.have.property('request', 'launch'); + 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()); @@ -172,17 +220,21 @@ getInfoPerOS().forEach(([osName, osType, path]) => { // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).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 debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, {}); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', 'python'); + 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'); @@ -191,6 +243,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).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'; @@ -198,12 +251,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupActiveEditor(activeFile, 'javascript'); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, {}); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', 'python'); expect(debugConfig).to.have.property('request', 'launch'); + 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'); @@ -211,6 +267,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).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'; @@ -219,13 +276,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupActiveEditor(activeFile, PYTHON_LANGUAGE); setupWorkspaces([defaultWorkspace]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + 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('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', 'python'); expect(debugConfig).to.have.property('request', 'launch'); + 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()); @@ -235,6 +295,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); }); + test("Ensure 'port' is left unaltered", async () => { const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor('spam.py', PYTHON_LANGUAGE); @@ -242,13 +303,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const port = 12341234; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - port, - request: 'launch' - } as any) as DebugConfiguration); + 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); @@ -256,13 +317,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - localRoot, - request: 'launch' - } as any) as DebugConfiguration); + 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); @@ -270,13 +332,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - remoteRoot, - request: 'launch' - } as any) as DebugConfiguration); + 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); @@ -285,14 +348,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, localRoot, - remoteRoot, - request: 'launch' - } as any) as DebugConfiguration); + 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); @@ -303,29 +367,30 @@ getInfoPerOS().forEach(([osName, osType, path]) => { localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, remoteRoot: `Debug_PythonPath_Remote_Root_${new Date().toString()}` }; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [expected] - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; 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 debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot: '${workspaceFolder}/spam', remoteRoot: '${workspaceFolder}/spam' } ] - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; expect(pathMappings).to.be.deep.equal([ @@ -335,6 +400,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } ]); }); + test('Ensure path mappings are not automatically added if missing', async () => { const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor('spam.py', PYTHON_LANGUAGE); @@ -342,14 +408,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, localRoot: localRoot - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; 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); @@ -357,15 +424,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, localRoot: localRoot, pathMappings: [] - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; 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); @@ -373,8 +441,8 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, localRoot: localRoot, pathMappings: [ { @@ -382,7 +450,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { remoteRoot: '.' } ] - } as any) as DebugConfiguration); + }); expect(debugConfig).to.have.property('localRoot', localRoot); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; @@ -393,6 +461,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } ]); }); + test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async function () { if (getOSType() !== OSType.Windows || osType !== OSType.Windows) { // tslint:disable-next-line: no-invalid-this @@ -404,15 +473,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot, remoteRoot: '/app/' } ] - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; @@ -423,6 +492,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } ]); }); + test('Ensure drive letter is not lower cased for local path mappings on non-Windows when with existing path mappings', async function () { if (getOSType() === OSType.Windows || osType === OSType.Windows) { // tslint:disable-next-line: no-invalid-this @@ -434,15 +504,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot, remoteRoot: '/app/' } ] - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; expect(pathMappings).to.deep.equal([ @@ -452,21 +522,22 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } ]); }); + 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 debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ - request: 'launch', + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pathMappings: [ { localRoot: '/spam', remoteRoot: '.' } ] - } as any) as DebugConfiguration); + }); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; expect(pathMappings).to.deep.equal([ @@ -476,6 +547,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } ]); }); + test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const activeFile = 'xyz.py'; @@ -485,12 +557,37 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pythonPath: '${command:python.interpreterPath}' - } as any) as DebugConfiguration); + }); - expect(debugConfig).to.have.property('pythonPath', pythonPath); + 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'; @@ -501,12 +598,80 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupWorkspaces([defaultWorkspace]); const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, pythonPath: debugPythonPath - } as any) as DebugConfiguration); + }); + + 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]); - expect(debugConfig).to.have.property('pythonPath', debugPythonPath); + 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); @@ -514,10 +679,9 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch + }); expect(debugConfig).to.have.property('console', 'integratedTerminal'); expect(debugConfig).to.have.property('stopOnEntry', false); @@ -529,6 +693,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); }); + test('Test defaults of python debugger', async () => { if ('python' === DebuggerTypeName) { return; @@ -539,16 +704,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch + }); expect(debugConfig).to.have.property('stopOnEntry', false); expect(debugConfig).to.have.property('showReturnValue', true); expect(debugConfig).to.have.property('debugOptions'); expect((debugConfig as any).debugOptions).to.be.deep.equal([]); }); + test('Test overriding defaults of debugger', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -556,10 +721,11 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: true, justMyCode: false - } as LaunchRequestArguments); + }); expect(debugConfig).to.have.property('console', 'integratedTerminal'); expect(debugConfig).to.have.property('stopOnEntry', false); @@ -577,6 +743,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); }); + const testsForJustMyCode = [ { justMyCode: false, @@ -631,13 +798,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); testsForJustMyCode.forEach(async (testParams) => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, debugStdLib: testParams.debugStdLib, justMyCode: testParams.justMyCode - } as LaunchRequestArguments); + }); expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); }); }); + const testsForRedirectOutput = [ { console: 'internalConsole', @@ -692,10 +861,11 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); testsForRedirectOutput.forEach(async (testParams) => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { - console: testParams.console, + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + console: testParams.console as any, redirectOutput: testParams.redirectOutput - } as LaunchRequestArguments); + }); expect(debugConfig).to.have.property('redirectOutput', testParams.expectedRedirectOutput); if (testParams.expectedRedirectOutput) { expect(debugConfig).to.have.property('debugOptions'); @@ -703,6 +873,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { } }); }); + test('Test fixFilePathCase', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -710,16 +881,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - {} as DebugConfiguration - ); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch + }); if (osType === 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'); @@ -729,15 +900,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const options = { debugOptions: [DebugOptions.Pyramid], pyramid: true }; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugOptions: [DebugOptions.Pyramid], + pyramid: true + }); - const debugConfig = await debugProvider.resolveDebugConfiguration!( - workspaceFolder, - (options as any) as DebugConfiguration - ); expect(debugConfig).to.have.property('debugOptions'); expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); }); + test('Auto detect flask debugging', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -745,14 +917,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, module: 'flask' - } as any) as DebugConfiguration); + }); expect(debugConfig).to.have.property('debugOptions'); expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); }); - test('Test validation of Python Path when launching debugger (with invalid python path)', async () => { + + test('Test validation of Python Path when launching debugger (with invalid "python")', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; @@ -765,17 +939,19 @@ getInfoPerOS().forEach(([osName, osType, path]) => { h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); + .verifiable(TypeMoq.Times.atLeastOnce()); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: false, - pythonPath - } as LaunchRequestArguments); + python: pythonPath + }); diagnosticsService.verifyAll(); expect(debugConfig).to.be.equal(undefined, 'Not undefined'); }); - test('Test validation of Python Path when launching debugger (with valid python path)', async () => { + + test('Test validation of Python Path when launching debugger (with valid "python")', async () => { const pythonPath = `PythonPath_${new Date().toString()}`; const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; @@ -788,23 +964,24 @@ getInfoPerOS().forEach(([osName, osType, path]) => { h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); + .verifiable(TypeMoq.Times.atLeastOnce()); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: false, - pythonPath - } as LaunchRequestArguments); + 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 expectedEnvFilePath = `${workspaceFolder.uri.fsPath}${ - osType === OSType.Windows ? '\\' : '/' - }${'wow.envFile'}`; + const sep = osType === OSType.Windows ? '\\' : '/'; + const expectedEnvFilePath = `${workspaceFolder.uri.fsPath}${sep}${'wow.envFile'}`; setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); @@ -815,14 +992,16 @@ getInfoPerOS().forEach(([osName, osType, path]) => { ) .returns(() => Promise.resolve(true)); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, redirectOutput: false, pythonPath, envFile: path.join('${workspaceFolder}', 'wow.envFile') - } as LaunchRequestArguments); + }); expect(debugConfig!.envFile).to.be.equal(expectedEnvFilePath); }); + async function testSetting( requestType: 'launch' | 'attach', settings: Record, @@ -830,7 +1009,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { mustHaveDebugOption: boolean ) { setupIoc('pythonPath'); - const debugConfiguration: DebugConfiguration = { + let debugConfig: DebugConfiguration = { request: requestType, type: 'python', name: '', @@ -838,23 +1017,28 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }; const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); + debugConfig = (await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfig))!; + debugConfig = (await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!( + workspaceFolder, + debugConfig + ))!; + if (mustHaveDebugOption) { - expect((debugConfig as any).debugOptions).contains(debugOptionName); + expect(debugConfig.debugOptions).contains(debugOptionName); } else { - expect((debugConfig as any).debugOptions).not.contains(debugOptionName); + 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 () => { + 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 () => { + 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 () => { + test(`Must not contain Sub Process setting = true(${requestType})`, async () => { await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); }); }); diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index c1782b7c8027..0abe2ce24bdb 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -119,7 +119,6 @@ suite('Unit Tests - Debug Launcher', () => { .setup((d) => d.getEnvironmentVariables(TypeMoq.It.isAny())) .returns(() => Promise.resolve(expected.env)); - //debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) debugService .setup((d) => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) .returns((_wspc: WorkspaceFolder, _expectedParam: DebugConfiguration) => { @@ -207,8 +206,14 @@ suite('Unit Tests - Debug Launcher', () => { } // added by LaunchConfigurationResolver: - if (!expected.pythonPath) { - expected.pythonPath = 'python'; + if (!expected.python) { + expected.python = 'python'; + } + if (!expected.debugAdapterPython) { + expected.debugAdapterPython = 'python'; + } + if (!expected.debugLauncherPython) { + expected.debugLauncherPython = 'python'; } expected.workspaceFolder = workspaceFolders[0].uri.fsPath; expected.debugOptions = []; @@ -324,7 +329,9 @@ suite('Unit Tests - Debug Launcher', () => { name: 'my tests', type: DebuggerTypeName, request: 'launch', - pythonPath: 'some/dir/bin/py3', + python: 'some/dir/bin/py3', + debugAdapterPython: 'some/dir/bin/py3', + debugLauncherPython: 'some/dir/bin/py3', stopOnEntry: true, showReturnValue: true, console: 'integratedTerminal', @@ -345,7 +352,7 @@ suite('Unit Tests - Debug Launcher', () => { name: 'my tests', type: DebuggerTypeName, request: 'test', - pythonPath: expected.pythonPath, + pythonPath: expected.python, stopOnEntry: expected.stopOnEntry, showReturnValue: expected.showReturnValue, console: expected.console,