diff --git a/news/2 Fixes/3568.md b/news/2 Fixes/3568.md new file mode 100644 index 000000000000..619227a39bbe --- /dev/null +++ b/news/2 Fixes/3568.md @@ -0,0 +1 @@ +Add support for the "pathMappings" setting in "launch" debug configs. diff --git a/package.json b/package.json index c022da8be5d3..fc32c53cf181 100644 --- a/package.json +++ b/package.json @@ -1024,6 +1024,31 @@ "description": "IP address of the of the local debug server (default is localhost).", "default": "localhost" }, + "pathMappings": { + "type": "array", + "label": "Path mappings.", + "items": { + "type": "object", + "label": "Path mapping", + "required": [ + "localRoot", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "label": "Local source root.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "label": "Remote source root.", + "default": "" + } + } + }, + "default": [] + }, "logToFile": { "type": "boolean", "description": "Enable logging of debugger events to a log file.", diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts index 65c1623dd3eb..567f2d4bb831 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -7,7 +7,7 @@ import * as os from 'os'; import { coerce, SemVer } from 'semver'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName, PlatformErrors } from '../../telemetry/constants'; -import { OSType } from '../utils/platform'; +import { getOSType, OSType } from '../utils/platform'; import { parseVersion } from '../utils/version'; import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants'; import { IPlatformService } from './types'; @@ -16,6 +16,17 @@ import { IPlatformService } from './types'; export class PlatformService implements IPlatformService { public readonly osType: OSType = getOSType(); public version?: SemVer; + constructor() { + if (this.osType === OSType.Unknown) { + sendTelemetryEvent( + EventName.PLATFORM_INFO, + undefined, + { + failureType: PlatformErrors.FailedToDetermineOS + } + ); + } + } public get pathVariableName() { return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; } @@ -66,16 +77,3 @@ export class PlatformService implements IPlatformService { return arch() === 'x64'; } } - -function getOSType(platform: string = process.platform): OSType { - if (/^win/.test(platform)) { - return OSType.Windows; - } else if (/^darwin/.test(platform)) { - return OSType.OSX; - } else if (/^linux/.test(platform)) { - return OSType.Linux; - } else { - sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { failureType: PlatformErrors.FailedToDetermineOS }); - return OSType.Unknown; - } -} diff --git a/src/client/common/utils/platform.ts b/src/client/common/utils/platform.ts index e293819b364f..770e15e0539e 100644 --- a/src/client/common/utils/platform.ts +++ b/src/client/common/utils/platform.ts @@ -14,3 +14,16 @@ export enum OSType { OSX = 'OSX', Linux = 'Linux' } + +// Return the OS type for the given platform string. +export function getOSType(platform: string = process.platform): OSType { + if (/^win/.test(platform)) { + return OSType.Windows; + } else if (/^darwin/.test(platform)) { + return OSType.OSX; + } else if (/^linux/.test(platform)) { + return OSType.Linux; + } else { + return OSType.Unknown; + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts index cd60af3168cf..32fe7a73b4bf 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -8,17 +8,18 @@ import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; import { IPlatformService } from '../../../../common/platform/types'; import { IConfigurationService } from '../../../../common/types'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; -import { AttachRequestArguments, DebugOptions } from '../../../types'; +import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; import { BaseConfigurationResolver } from './base'; @injectable() export class AttachConfigurationResolver extends BaseConfigurationResolver { - constructor(@inject(IWorkspaceService) workspaceService: IWorkspaceService, + constructor( + @inject(IWorkspaceService) workspaceService: IWorkspaceService, @inject(IDocumentManager) documentManager: IDocumentManager, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IConfigurationService) configurationService: IConfigurationService) { - super(workspaceService, documentManager, configurationService); + @inject(IPlatformService) platformService: IPlatformService, + @inject(IConfigurationService) configurationService: IConfigurationService + ) { + super(workspaceService, documentManager, platformService, configurationService); } public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: AttachRequestArguments, _token?: CancellationToken): Promise { const workspaceFolder = this.getWorkspaceFolder(folder); @@ -83,46 +84,39 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver= 0) { - let configPathMappings; - if (debugConfiguration.pathMappings!.length === 0) { - configPathMappings = [{ - localRoot: workspaceFolder.fsPath, - remoteRoot: workspaceFolder.fsPath - }]; - } else { - // Expand ${workspaceFolder} variable first if necessary. - const systemVariables = new SystemVariables(workspaceFolder.fsPath); - configPathMappings = debugConfiguration.pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => ({ - localRoot: systemVariables.resolveAny(mappedLocalRoot), - remoteRoot - })); - } - // If on Windows, lowercase the drive letter for path mappings. - let pathMappings = configPathMappings; - if (this.platformService.isWindows) { - pathMappings = configPathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { - let localRoot = windowsLocalRoot; - if (windowsLocalRoot.match(/^[A-Z]:/)) { - localRoot = `${windowsLocalRoot[0].toLowerCase()}${windowsLocalRoot.substr(1)}`; - } - return { localRoot, remoteRoot }; - }); - } - debugConfiguration.pathMappings = pathMappings; - } - this.sendTelemetry('attach', debugConfiguration); + if (this.isLocalHost(host)) { + pathMappings = this.fixUpPathMappings( + pathMappings, + workspaceFolder ? workspaceFolder.fsPath : '' + ); + } + return pathMappings.length > 0 + ? pathMappings + : undefined; } } diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index 64c9015f88ba..515e6d5ca510 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -3,27 +3,34 @@ 'use strict'; -// tslint:disable:no-invalid-template-strings +// tslint:disable:no-invalid-template-strings no-suspicious-comment import { injectable } from 'inversify'; import * as path from 'path'; import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; import { PYTHON_LANGUAGE } from '../../../../common/constants'; +import { IPlatformService } from '../../../../common/platform/types'; import { IConfigurationService } from '../../../../common/types'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; import { DebuggerTelemetry } from '../../../../telemetry/types'; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../types'; +import { + AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping +} from '../../../types'; import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; @injectable() export abstract class BaseConfigurationResolver implements IDebugConfigurationResolver { protected pythonPathSource: PythonPathSource = PythonPathSource.launchJson; - constructor(protected readonly workspaceService: IWorkspaceService, + constructor( + protected readonly workspaceService: IWorkspaceService, protected readonly documentManager: IDocumentManager, - protected readonly configurationService: IConfigurationService) { } + protected readonly platformService: IPlatformService, + protected readonly configurationService: IConfigurationService + ) { } public abstract resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise; protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { if (folder) { @@ -71,6 +78,50 @@ export abstract class BaseConfigurationResolver im const LocalHosts = ['localhost', '127.0.0.1', '::1']; return (hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0) ? true : false; } + protected fixUpPathMappings( + pathMappings: PathMapping[], + defaultLocalRoot?: string, + defaultRemoteRoot?: string + ): PathMapping[] { + if (!defaultLocalRoot) { + return []; + } + if (!defaultRemoteRoot) { + defaultRemoteRoot = defaultLocalRoot; + } + + if (pathMappings.length === 0) { + pathMappings = [ + { + localRoot: defaultLocalRoot, + remoteRoot: defaultRemoteRoot + } + ]; + } else { + // Expand ${workspaceFolder} variable first if necessary. + const systemVariables = new SystemVariables(defaultLocalRoot); + pathMappings = pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => ({ + localRoot: systemVariables.resolveAny(mappedLocalRoot), + // TODO: Apply to remoteRoot too? + remoteRoot + })); + } + + // If on Windows, lowercase the drive letter for path mappings. + // TODO: Apply even if no localRoot? + if (this.platformService.isWindows) { + // TODO: Apply to remoteRoot too? + pathMappings = pathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { + let localRoot = windowsLocalRoot; + if (windowsLocalRoot.match(/^[A-Z]:/)) { + localRoot = `${windowsLocalRoot[0].toLowerCase()}${windowsLocalRoot.substr(1)}`; + } + return { localRoot, remoteRoot }; + }); + } + + return pathMappings; + } protected isDebuggingFlask(debugConfiguration: Partial) { return (debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK') ? true : false; } diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index 80d2872d50e3..b6c47ba20d73 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -21,11 +21,11 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { const workspaceFolder = this.getWorkspaceFolder(folder); @@ -123,6 +123,20 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver 0) { + pathMappings = this.fixUpPathMappings( + pathMappings || [], + workspaceFolder ? workspaceFolder.fsPath : '' + ); + } + debugConfiguration.pathMappings = pathMappings.length > 0 + ? pathMappings + : undefined; + } this.sendTelemetry( debugConfiguration.request as 'launch' | 'test', debugConfiguration diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 48beb568c3a2..1241988c1f61 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -22,6 +22,10 @@ export enum DebugOptions { SubProcess = 'Multiprocess' } +export type PathMapping = { + localRoot: string; + remoteRoot: string; +}; interface ICommonDebugArguments { redirectOutput?: boolean; django?: boolean; @@ -36,14 +40,15 @@ interface ICommonDebugArguments { // Show return values of functions while stepping. showReturnValue?: boolean; subProcess?: boolean; + // An absolute path to local directory with source. + pathMappings?: PathMapping[]; } export interface IKnownAttachDebugArguments extends ICommonDebugArguments { workspaceFolder?: string; - // An absolute path to local directory with source. + customDebugger?: boolean; + // localRoot and remoteRoot are deprecated (replaced by pathMappings). localRoot?: string; remoteRoot?: string; - pathMappings?: { localRoot: string; remoteRoot: string }[]; - customDebugger?: boolean; } export interface IKnownLaunchRequestArguments extends ICommonDebugArguments { 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 bcb3294fa306..2d1d174ccd9a 100644 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -6,24 +6,36 @@ // tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion no-invalid-this import { expect } from 'chai'; -import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; import { IFileSystem, IPlatformService } from '../../../../../client/common/platform/types'; import { IConfigurationService } from '../../../../../client/common/types'; -import { getNamesAndValues } from '../../../../../client/common/utils/enum'; import { OSType } from '../../../../../client/common/utils/platform'; import { AttachConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/attach'; import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; import { IServiceContainer } from '../../../../../client/ioc/types'; +import { getInfoPerOS, setUpOSMocks } from './common'; -getNamesAndValues(OSType).forEach(os => { - if (os.value === OSType.Unknown) { +getInfoPerOS().forEach(([osName, osType, path]) => { + if (osType === OSType.Unknown) { return; } - suite(`Debugging - Config Resolver attach, OS = ${os.name}`, () => { + + function getAvailableOptions(): string[] { + const options = [DebugOptions.RedirectOutput]; + if (osType === OSType.Windows) { + options.push(DebugOptions.FixFilePathCase); + options.push(DebugOptions.WindowsClient); + } else { + options.push(DebugOptions.UnixClient); + } + options.push(DebugOptions.ShowReturnValue); + return options; + } + + suite(`Debugging - Config Resolver attach, OS = ${osName}`, () => { let serviceContainer: TypeMoq.IMock; let debugProvider: DebugConfigurationProvider; let platformService: TypeMoq.IMock; @@ -31,14 +43,7 @@ getNamesAndValues(OSType).forEach(os => { let documentManager: TypeMoq.IMock; let configurationService: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; - const debugOptionsAvailable = [DebugOptions.RedirectOutput]; - if (os.value === OSType.Windows) { - debugOptionsAvailable.push(DebugOptions.FixFilePathCase); - debugOptionsAvailable.push(DebugOptions.WindowsClient); - } else { - debugOptionsAvailable.push(DebugOptions.UnixClient); - } - debugOptionsAvailable.push(DebugOptions.ShowReturnValue); + const debugOptionsAvailable = getAvailableOptions(); setup(() => { serviceContainer = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); @@ -47,9 +52,10 @@ getNamesAndValues(OSType).forEach(os => { fileSystem = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - platformService.setup(p => p.isWindows).returns(() => os.value === OSType.Windows); - platformService.setup(p => p.isMac).returns(() => os.value === OSType.OSX); - platformService.setup(p => p.isLinux).returns(() => os.value === OSType.Linux); + setUpOSMocks( + osType, + platformService + ); documentManager = TypeMoq.Mock.ofType(); debugProvider = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); }); @@ -151,7 +157,7 @@ getNamesAndValues(OSType).forEach(os => { expect(debugConfig).to.have.property('localRoot', localRoot); }); - ['localhost', '127.0.0.1', '::1'].forEach(host => { + ['localhost', 'LOCALHOST', '127.0.0.1', '::1'].forEach(host => { test(`Ensure path mappings are automatically added when host is '${host}'`, async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -168,11 +174,7 @@ getNamesAndValues(OSType).forEach(os => { 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 (os.name !== 'Windows') { - return this.skip(); - } - + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}'`, async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -182,15 +184,14 @@ getNamesAndValues(OSType).forEach(os => { const localRoot = `Debug_PythonPath_${new Date().toString()}`; const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - const lowercasedLocalRoot = path.join('c:', 'Debug', 'Python_Path'); - expect(pathMappings![0].localRoot).to.be.equal(lowercasedLocalRoot); + const expected = osType === OSType.Windows + ? path.join('c:', 'Debug', 'Python_Path') + : path.join('C:', 'Debug', 'Python_Path'); + 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 (os.name !== 'Windows') { - return this.skip(); - } - + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -201,9 +202,12 @@ getNamesAndValues(OSType).forEach(os => { const debugPathMappings = [ { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }]; const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, pathMappings: debugPathMappings, host, request: 'attach' } as any as DebugConfiguration); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - const lowercasedLocalRoot = path.join('c:', 'Debug', 'Python_Path', localRoot); - expect(pathMappings![0].localRoot).to.be.equal(lowercasedLocalRoot); + const expected = osType === OSType.Windows + ? path.join('c:', 'Debug', 'Python_Path', localRoot) + : path.join('C:', 'Debug', 'Python_Path', localRoot); + 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'; @@ -217,6 +221,7 @@ getNamesAndValues(OSType).forEach(os => { 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 => { @@ -232,7 +237,7 @@ getNamesAndValues(OSType).forEach(os => { expect(debugConfig).to.have.property('localRoot', localRoot); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - expect(pathMappings).to.be.lengthOf(0); + expect(pathMappings || []).to.be.lengthOf(0); }); }); test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { 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 8e2a4e70bdf8..f4306938f8b7 100644 --- a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -16,6 +16,8 @@ import { IDocumentManager, IWorkspaceService } from '../../../../../client/commo import { WorkspaceService } from '../../../../../client/common/application/workspace'; import { ConfigurationService } from '../../../../../client/common/configuration/service'; import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { PlatformService } from '../../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../../client/common/platform/types'; import { IConfigurationService } from '../../../../../client/common/types'; import { BaseConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/base'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; @@ -46,13 +48,20 @@ suite('Debugging - Config Resolver', () => { } let resolver: BaseResolver; let workspaceService: IWorkspaceService; + let platformService: IPlatformService; let documentManager: IDocumentManager; let configurationService: IConfigurationService; setup(() => { workspaceService = mock(WorkspaceService); documentManager = mock(DocumentManager); + platformService = mock(PlatformService); configurationService = mock(ConfigurationService); - resolver = new BaseResolver(instance(workspaceService), instance(documentManager), instance(configurationService)); + resolver = new BaseResolver( + instance(workspaceService), + instance(documentManager), + instance(platformService), + instance(configurationService) + ); }); test('Program should return filepath of active editor if file is python', () => { diff --git a/src/test/debugger/extension/configuration/resolvers/common.ts b/src/test/debugger/extension/configuration/resolvers/common.ts new file mode 100644 index 000000000000..59cf52c1679d --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/common.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IPlatformService } from '../../../../../client/common/platform/types'; +import { getNamesAndValues } from '../../../../../client/common/utils/enum'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; + +const OS_TYPE = getOSType(); + +interface IPathModule { + sep: string; + dirname(path: string): string; + join(...paths: string[]): string; +} + +// The set of information, related to a target OS, that are available +// to tests. The target OS is not necessarily the native OS. +type OSTestInfo = [ + string, // os name + OSType, + IPathModule +]; + +// For each supported OS, provide a set of helpers to use in tests. +export function getInfoPerOS(): OSTestInfo[] { + return getNamesAndValues(OSType) + .map(os => { + const osType = os.value as OSType; + return [ + os.name, + osType, + getPathModuleForOS(osType) + ]; + }); +} + +// Decide which "path" module to use. +// By default we use the regular module. +function getPathModuleForOS(osType: OSType): IPathModule { + if (osType === OS_TYPE) { + return path; + } + + // We are testing a different OS from the native one. + // So use a "path" module matching the target OS. + return osType === OSType.Windows + ? path.win32 + : path.posix; +} + +// Generate the function to use for populating the +// relevant mocks relative to the target OS. +export function setUpOSMocks( + osType: OSType, + platformService: TypeMoq.IMock +) { + platformService.setup(p => p.isWindows) + .returns(() => osType === OSType.Windows); + platformService.setup(p => p.isMac) + .returns(() => osType === OSType.OSX); + platformService.setup(p => p.isLinux) + .returns(() => osType === OSType.Linux); +} 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 8853a291d988..13d324cd6dce 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -6,7 +6,6 @@ // tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion import { expect } from 'chai'; -import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; import { IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; @@ -15,458 +14,677 @@ import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; import { IPlatformService } from '../../../../../client/common/platform/types'; import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../../client/common/process/types'; import { IConfigurationService, IPythonSettings } from '../../../../../client/common/types'; +import { OSType } from '../../../../../client/common/utils/platform'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; import { IDebugEnvironmentVariablesService } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; import { DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; import { IInterpreterHelper } from '../../../../../client/interpreter/contracts'; +import { getInfoPerOS, setUpOSMocks } from './common'; -suite('Debugging - Config Resolver Launch', () => { - let debugProvider: DebugConfigurationProvider; - let platformService: TypeMoq.IMock; - let pythonExecutionService: TypeMoq.IMock; - let helper: 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; +getInfoPerOS().forEach(([osName, osType, path]) => { + if (osType === OSType.Unknown) { + return; } - function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder, isWindows: boolean = false, isMac: boolean = false, isLinux: boolean = false) { - const confgService = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - documentManager = TypeMoq.Mock.ofType(); - - platformService = TypeMoq.Mock.ofType(); - diagnosticsService = TypeMoq.Mock.ofType(); - debugEnvHelper = TypeMoq.Mock.ofType(); - - pythonExecutionService = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); - const factory = TypeMoq.Mock.ofType(); - factory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); - helper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - const settings = TypeMoq.Mock.ofType(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - if (workspaceFolder) { - settings.setup(s => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); + + suite(`Debugging - Config Resolver Launch, OS = ${osName}`, () => { + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock; + let pythonExecutionService: TypeMoq.IMock; + let helper: 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; } - confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - setupOs(isWindows, isMac, isLinux); - debugEnvHelper.setup(x => x.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - - debugProvider = new LaunchConfigurationResolver( - workspaceService.object, - documentManager.object, - diagnosticsService.object, - platformService.object, - confgService.object, - debugEnvHelper.object); - } - function setupActiveEditor(fileName: string | undefined, languageId: string) { - if (fileName) { - const textEditor = TypeMoq.Mock.ofType(); - const document = TypeMoq.Mock.ofType(); - document.setup(d => d.languageId).returns(() => languageId); - document.setup(d => d.fileName).returns(() => fileName); - textEditor.setup(t => t.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - } else { - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { + const confgService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + + platformService = TypeMoq.Mock.ofType(); + diagnosticsService = TypeMoq.Mock.ofType(); + debugEnvHelper = TypeMoq.Mock.ofType(); + + pythonExecutionService = TypeMoq.Mock.ofType(); + helper = TypeMoq.Mock.ofType(); + pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); + const factory = TypeMoq.Mock.ofType(); + factory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); + helper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + diagnosticsService + .setup(h => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + + const settings = TypeMoq.Mock.ofType(); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + if (workspaceFolder) { + settings.setup(s => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); + } + confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + setUpOSMocks( + osType, + platformService + ); + debugEnvHelper.setup(x => x.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + + debugProvider = new LaunchConfigurationResolver( + workspaceService.object, + documentManager.object, + diagnosticsService.object, + platformService.object, + confgService.object, + debugEnvHelper.object); } - } - function setupWorkspaces(folders: string[]) { - const workspaceFolders = folders.map(createMoqWorkspaceFolder); - workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); - } - function setupOs(isWindows: boolean, isMac: boolean, isLinux: boolean) { - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isMac); - platformService.setup(p => p.isLinux).returns(() => isLinux); - } - 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); - - 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.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // 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); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, workspaceFolder); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.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.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // 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'; - setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - 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.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // 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); - - 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.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - // 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'; - setupIoc(pythonPath); - setupActiveEditor(activeFile, 'javascript'); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} 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.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - // 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'; - const defaultWorkspace = path.join('usr', 'desktop'); - setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - 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.have.property('program', activeFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Ensure `${config:python.pythonPath}` is replaced with actual pythonPath', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { pythonPath: '${config:python.pythonPath}' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('pythonPath', pythonPath); - }); - test('Ensure hardcoded pythonPath is left unaltered', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { pythonPath: debugPythonPath } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('pythonPath', debugPythonPath); - }); - test('Test defaults of debugger', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - 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([DebugOptions.ShowReturnValue, DebugOptions.RedirectOutput]); - }); - test('Test defaults of python debugger', async () => { - if ('python' === DebuggerTypeName) { - return; + function setupActiveEditor(fileName: string | undefined, languageId: string) { + if (fileName) { + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.languageId).returns(() => languageId); + document.setup(d => d.fileName).returns(() => fileName); + textEditor.setup(t => t.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + } } - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - 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([DebugOptions.RedirectOutput]); - }); - test('Test overriding defaults of debugger', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, justMyCode: false } as LaunchRequestArguments); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', true); - expect(debugConfig).to.have.property('justMyCode', false); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([DebugOptions.DebugStdLib, DebugOptions.ShowReturnValue]); - }); - const testsForJustMyCode = - [ - { - justMyCode: false, - debugStdLib: true, - expectedResult: false - }, - { - justMyCode: false, - debugStdLib: false, - expectedResult: false - }, - { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false - }, - { - justMyCode: true, - debugStdLib: false, - expectedResult: true - }, - { - justMyCode: true, - debugStdLib: true, - expectedResult: true - }, - { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true - }, - { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true - }, - { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false - }, - { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); + } + 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); + + 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.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // 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); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.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.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // 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'; + setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + 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.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // 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); + + 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.have.property('program', ''); + expect(debugConfig).not.to.have.property('cwd'); + expect(debugConfig).not.to.have.property('envFile'); + expect(debugConfig).to.have.property('env'); + // 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'; + setupIoc(pythonPath); + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} 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.have.property('program', ''); + expect(debugConfig).not.to.have.property('cwd'); + expect(debugConfig).not.to.have.property('envFile'); + expect(debugConfig).to.have.property('env'); + // 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'; + const defaultWorkspace = path.join('usr', 'desktop'); + setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + 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.have.property('program', activeFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // 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); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'launch' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test('Ensure \'localRoot\' is left unaltered', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'launch' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + test('Ensure \'remoteRoot\' is left unaltered', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'launch' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test('Ensure \'localRoot\' and \'remoteRoot\' are not used', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'launch' } as any as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + test('Ensure non-empty path mappings are used', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const expected = { + localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, + remoteRoot: `Debug_PythonPath_Remote_Root_${new Date().toString()}` + }; + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + { + request: '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', + pathMappings: [{ + localRoot: '${workspaceFolder}/spam', + remoteRoot: '${workspaceFolder}/spam' + }] + } as any as DebugConfiguration + ); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.deep.equal([{ + localRoot: `${workspaceFolder.uri.fsPath}/spam`, + remoteRoot: '${workspaceFolder}/spam' + }]); + }); + test('Ensure path mappings are not automatically added if missing', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + { + request: '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); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + { + request: '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); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + { + request: 'launch', + localRoot: localRoot, + pathMappings: [{ + localRoot: '/spam', + remoteRoot: '.' + }] + } as any as DebugConfiguration + ); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.deep.equal([{ + localRoot: '/spam', + remoteRoot: '.' + }]); + }); + test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async () => { + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = path.join(workspaceFolder.uri.fsPath, 'app'); + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + { + request: 'launch', + pathMappings: [{ + localRoot: localRoot, + remoteRoot: '/app/' + }] + } as any as DebugConfiguration + ); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const expected = osType === OSType.Windows + ? `c${localRoot.substring(1)}` + : localRoot; + expect(pathMappings).to.deep.equal([{ + localRoot: expected, + remoteRoot: '/app/' + }]); + }); + 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', + pathMappings: [{ + localRoot: '/spam', + remoteRoot: '.' + }] + } as any as DebugConfiguration + ); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.deep.equal([{ + localRoot: '/spam', + remoteRoot: '.' + }]); + }); + test('Ensure `${config:python.pythonPath}` is replaced with actual pythonPath', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { pythonPath: '${config:python.pythonPath}' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('pythonPath', pythonPath); + }); + test('Ensure hardcoded pythonPath is left unaltered', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { pythonPath: debugPythonPath } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('pythonPath', debugPythonPath); + }); + test('Test defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); + + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('debugOptions'); + const expectedOptions = [ + DebugOptions.ShowReturnValue, + DebugOptions.RedirectOutput + ]; + if (osType === OSType.Windows) { + expectedOptions.push( + DebugOptions.FixFilePathCase + ); } - ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - testsForJustMyCode.forEach(async testParams => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugStdLib: testParams.debugStdLib, justMyCode: testParams.justMyCode } as LaunchRequestArguments); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); }); - }); - async function testFixFilePathCase(isWindows: boolean, isMac: boolean, isLinux: boolean) { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, undefined, isWindows, isMac, isLinux); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - if (isWindows) { - expect(debugConfig).to.have.property('debugOptions').contains(DebugOptions.FixFilePathCase); - } else { - expect(debugConfig).to.have.property('debugOptions').not.contains(DebugOptions.FixFilePathCase); - } - } - test('Test fixFilePathCase for Windows', async () => { - await testFixFilePathCase(true, false, false); - }); - test('Test fixFilePathCase for Linux', async () => { - await testFixFilePathCase(false, false, true); - }); - test('Test fixFilePathCase for Mac', async () => { - await testFixFilePathCase(false, true, false); - }); - test('Jinja added for Pyramid', async () => { - const workspacePath = path.join('usr', 'development', 'wksp1'); - const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); - const workspaceFolder = createMoqWorkspaceFolder(workspacePath); - const pythonFile = 'xyz.py'; + test('Test defaults of python debugger', async () => { + if ('python' === DebuggerTypeName) { + return; + } + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); + + 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([ + DebugOptions.RedirectOutput + ]); + }); + test('Test overriding defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, justMyCode: false } as LaunchRequestArguments); + + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('justMyCode', false); + expect(debugConfig).to.have.property('debugOptions'); + const expectedOptions = [ + DebugOptions.DebugStdLib, + DebugOptions.ShowReturnValue + ]; + if (osType === OSType.Windows) { + expectedOptions.push( + DebugOptions.FixFilePathCase + ); + } + expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); + }); + const testsForJustMyCode = + [ + { + justMyCode: false, + debugStdLib: true, + expectedResult: false + }, + { + justMyCode: false, + debugStdLib: false, + expectedResult: false + }, + { + justMyCode: false, + debugStdLib: undefined, + expectedResult: false + }, + { + justMyCode: true, + debugStdLib: false, + expectedResult: true + }, + { + justMyCode: true, + debugStdLib: true, + expectedResult: true + }, + { + justMyCode: true, + debugStdLib: undefined, + expectedResult: true + }, + { + justMyCode: undefined, + debugStdLib: false, + expectedResult: true + }, + { + justMyCode: undefined, + debugStdLib: true, + expectedResult: false + }, + { + justMyCode: undefined, + debugStdLib: undefined, + expectedResult: true + } + ]; + test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + testsForJustMyCode.forEach(async testParams => { + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugStdLib: testParams.debugStdLib, justMyCode: testParams.justMyCode } as LaunchRequestArguments); + expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + }); + }); + test('Test fixFilePathCase', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); + 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'); + const workspaceFolder = createMoqWorkspaceFolder(workspacePath); + const pythonFile = 'xyz.py'; - setupIoc(pythonPath, undefined, false, false, true); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const options = { debugOptions: [DebugOptions.Pyramid], pyramid: true }; + const options = { 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); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { module: 'flask' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).contains(DebugOptions.RedirectOutput); - expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); - }); - test('Test validation of Python Path when launching debugger (with invalid python path)', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.be.equal(undefined, 'Not undefined'); - }); - test('Test validation of Python Path when launching debugger (with valid python path)', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); - }); - async function testSetting(requestType: 'launch' | 'attach', settings: Record, debugOptionName: DebugOptions, mustHaveDebugOption: boolean) { - setupIoc('pythonPath'); - const debugConfiguration: DebugConfiguration = { request: requestType, type: 'python', name: '', ...settings }; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); - if (mustHaveDebugOption) { - expect((debugConfig as any).debugOptions).contains(debugOptionName); - } else { - expect((debugConfig as any).debugOptions).not.contains(debugOptionName); - } - } - type LaunchOrAttach = 'launch' | 'attach'; - const items: LaunchOrAttach[] = ['launch', 'attach']; - items.forEach(requestType => { - test(`Must not contain Sub Process when not specified (${requestType})`, async () => { - await testSetting(requestType, {}, DebugOptions.SubProcess, false); + 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(`Must not contain Sub Process setting=false (${requestType})`, async () => { - await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + test('Auto detect flask debugging', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { module: 'flask' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).contains(DebugOptions.RedirectOutput); + expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); }); - test(`Must not contain Sub Process setting=true (${requestType})`, async () => { - await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + test('Test validation of Python Path when launching debugger (with invalid python path)', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); + }); + test('Test validation of Python Path when launching debugger (with valid python path)', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); + }); + async function testSetting(requestType: 'launch' | 'attach', settings: Record, debugOptionName: DebugOptions, mustHaveDebugOption: boolean) { + setupIoc('pythonPath'); + const debugConfiguration: DebugConfiguration = { request: requestType, type: 'python', name: '', ...settings }; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); + if (mustHaveDebugOption) { + expect((debugConfig as any).debugOptions).contains(debugOptionName); + } else { + expect((debugConfig as any).debugOptions).not.contains(debugOptionName); + } + } + type LaunchOrAttach = 'launch' | 'attach'; + const items: LaunchOrAttach[] = ['launch', 'attach']; + items.forEach(requestType => { + test(`Must not contain Sub Process when not specified (${requestType})`, async () => { + await testSetting(requestType, {}, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting=false (${requestType})`, async () => { + await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting=true (${requestType})`, async () => { + await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + }); }); }); });