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,