diff --git a/.vscode/launch.json b/.vscode/launch.json index 8c80fc1679cd..3cc9f6c671c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,25 +1,7 @@ // A launch configuration that compiles the extension and then opens it inside a new window { "version": "0.1.0", - "configurations": [{ - "name": "CompletionServer.ppy", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "python", - "program": "${workspaceRoot}/pythonFiles/completionServer.py", - "cwd": "${workspaceRoot}", - "env": {}, - "args": [ - "123" - ], - "envFile": "${workspaceRoot}/.env", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, + "configurations": [ { "name": "Launch Extension", "type": "extensionHost", @@ -83,28 +65,15 @@ "${workspaceRoot}/out/**/*.js" ], "preLaunchTask": "compile" - }, - { - "name": "Python", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "python", - "program": "${file}", - "console": "integratedTerminal", - "args": [], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ], - "cwd": "${workspaceRoot}" } ], - "compounds": [{ - "name": "Extension + Debugger", - "configurations": [ - "Launch Extension", "Launch Extension as debugServer" - ] - }] + "compounds": [ + { + "name": "Extension + Debugger", + "configurations": [ + "Launch Extension", + "Launch Extension as debugServer" + ] + } + ] } diff --git a/pythonFiles/PythonTools/ipythonServer.py b/pythonFiles/PythonTools/ipythonServer.py index 99ff5dc7ac6c..0aac8c30bfb4 100644 --- a/pythonFiles/PythonTools/ipythonServer.py +++ b/pythonFiles/PythonTools/ipythonServer.py @@ -55,7 +55,7 @@ from queue import Empty, Queue # Python 3 DEBUG = os.environ.get('DEBUG_DJAYAMANNE_IPYTHON', '0') == '1' -TEST = os.environ.get('PYTHON_DONJAYAMANNE_TEST', '0') == '1' +TEST = os.environ.get('VSC_PYTHON_CI_TEST', '0') == '1' # The great "support IPython 2, 3, 4" strat begins if not TEST: diff --git a/pythonFiles/completionServer.py b/pythonFiles/completionServer.py index f364f1fddce3..2e2934724841 100644 --- a/pythonFiles/completionServer.py +++ b/pythonFiles/completionServer.py @@ -55,7 +55,7 @@ from queue import Empty, Queue # Python 3 DEBUG = os.environ.get('DEBUG_DJAYAMANNE_IPYTHON', '0') == '1' -TEST = os.environ.get('PYTHON_DONJAYAMANNE_TEST', '0') == '1' +TEST = os.environ.get('VSC_PYTHON_CI_TEST', '0') == '1' def _debug_write(out): if DEBUG: diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index bece06d457fa..797e644d3b3f 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -5,7 +5,9 @@ import { EventEmitter } from 'events'; import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; +import { InterpreterInfoCache } from './interpreterInfoCache'; import { SystemVariables } from './systemVariables'; + // tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); @@ -135,7 +137,7 @@ export interface JupyterSettings { } // tslint:disable-next-line:no-string-literal -const IS_TEST_EXECUTION = process.env['PYTHON_DONJAYAMANNE_TEST'] === '1'; +const IS_TEST_EXECUTION = process.env['VSC_PYTHON_CI_TEST'] === '1'; // tslint:disable-next-line:completed-docs export class PythonSettings extends EventEmitter implements IPythonSettings { @@ -197,10 +199,12 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { // tslint:disable-next-line:no-unsafe-any this.disposables.forEach(disposable => disposable.dispose()); this.disposables = []; + InterpreterInfoCache.clear(); } // tslint:disable-next-line:cyclomatic-complexity max-func-body-length private initializeSettings() { + InterpreterInfoCache.clear(); const workspaceRoot = this.workspaceRoot.fsPath; const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); const pythonSettings = vscode.workspace.getConfiguration('python', this.workspaceRoot); @@ -213,8 +217,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); - } - else { + } else { this.jediPath = ''; } // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion @@ -230,16 +233,14 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.disablePromptForFeatures = Array.isArray(this.disablePromptForFeatures) ? this.disablePromptForFeatures : []; if (this.linting) { Object.assign(this.linting, lintingSettings); - } - else { + } else { this.linting = lintingSettings; } // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; if (this.sortImports) { Object.assign(this.sortImports, sortImportSettings); - } - else { + } else { this.sortImports = sortImportSettings; } // Support for travis. @@ -290,8 +291,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; if (this.formatting) { Object.assign(this.formatting, formattingSettings); - } - else { + } else { this.formatting = formattingSettings; } // Support for travis. @@ -309,8 +309,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { const autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; if (this.autoComplete) { Object.assign(this.autoComplete, autoCompleteSettings); - } - else { + } else { this.autoComplete = autoCompleteSettings; } // Support for travis. @@ -324,8 +323,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { const workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get('workspaceSymbols'))!; if (this.workspaceSymbols) { Object.assign(this.workspaceSymbols, workspaceSymbolsSettings); - } - else { + } else { this.workspaceSymbols = workspaceSymbolsSettings; } // Support for travis. @@ -343,8 +341,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { const unitTestSettings = systemVariables.resolveAny(pythonSettings.get('unitTest'))!; if (this.unitTest) { Object.assign(this.unitTest, unitTestSettings); - } - else { + } else { this.unitTest = unitTestSettings; if (IS_TEST_EXECUTION && !this.unitTest) { // tslint:disable-next-line:prefer-type-cast @@ -381,8 +378,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; if (this.terminal) { Object.assign(this.terminal, terminalSettings); - } - else { + } else { this.terminal = terminalSettings; if (IS_TEST_EXECUTION && !this.terminal) { // tslint:disable-next-line:prefer-type-cast @@ -402,7 +398,9 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { appendResults: true, defaultKernel: '', startupCode: [] }; - this.emit('change'); + // If workspace config changes, then we could have a cascading effect of on change events. + // Let's defer the change notification. + setTimeout(() => this.emit('change'), 1); } public get pythonPath(): string { @@ -416,8 +414,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { // E.g. virtual directory name. try { this._pythonPath = getPythonExecutable(value); - } - catch (ex) { + } catch (ex) { this._pythonPath = value; } } @@ -461,8 +458,7 @@ function getPythonExecutable(pythonPath: string): string { if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { return path.join(pythonPath, 'scripts', executableName); } - } - else { + } else { if (isValidPythonPath(path.join(pythonPath, executableName))) { return path.join(pythonPath, executableName); } @@ -479,8 +475,7 @@ function isValidPythonPath(pythonPath: string): boolean { try { const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); return output.startsWith('1234'); - } - catch (ex) { + } catch (ex) { return false; } } diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index 59e22331f743..6cdd8a67608f 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -263,7 +263,7 @@ export class Installer implements vscode.Disposable { if (this.outputChannel && installArgs[0] === '-m') { // Errors are just displayed to the user this.outputChannel.show(); - installationPromise = execPythonFile(settings.PythonSettings.getInstance(resource).pythonPath, + installationPromise = execPythonFile(resource, settings.PythonSettings.getInstance(resource).pythonPath, installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); }); } else { @@ -271,7 +271,7 @@ export class Installer implements vscode.Disposable { // Cuz people may launch vs code from terminal when they have activated the appropriate virtual env // Problem is terminal doesn't use the currently activated virtual env // Must have something to do with the process being launched in the terminal - installationPromise = getFullyQualifiedPythonInterpreterPath() + installationPromise = getFullyQualifiedPythonInterpreterPath(resource) .then(pythonPath => { let installScript = installArgs.join(' '); @@ -340,7 +340,7 @@ async function isProductInstalled(product: Product, resource?: Uri): Promise true) .catch(reason => !isNotInstalledError(reason)); } @@ -350,5 +350,5 @@ function uninstallproduct(product: Product, resource?: Uri): Promise { return Promise.resolve(); } const uninstallArgs = ProductUninstallScripts.get(product)!; - return execPythonFile('python', uninstallArgs, getCwdForInstallScript(resource), false); -} \ No newline at end of file + return execPythonFile(resource, 'python', uninstallArgs, getCwdForInstallScript(resource), false); +} diff --git a/src/client/common/interpreterInfoCache.ts b/src/client/common/interpreterInfoCache.ts new file mode 100644 index 000000000000..f23f6ef7563b --- /dev/null +++ b/src/client/common/interpreterInfoCache.ts @@ -0,0 +1,54 @@ +import { Uri, workspace } from 'vscode'; + +type InterpreterCache = { + pythonInterpreterDirectory?: string; + pythonInterpreterPath?: string; + pythonSettingsPath?: string; + // tslint:disable-next-line:no-any + customEnvVariables?: any; +}; + +const cache = new Map(); + +// tslint:disable-next-line:no-stateless-class +export class InterpreterInfoCache { + // tslint:disable-next-line:function-name + public static clear(): void { + cache.clear(); + } + // tslint:disable-next-line:function-name + public static get(resource?: Uri) { + const cacheKey = InterpreterInfoCache.getCacheKey(resource) || ''; + return cache.has(cacheKey) ? cache.get(cacheKey) : {}; + } + // tslint:disable-next-line:function-name + public static setPaths(resource?: Uri, pythonSettingsPath?: string, pythonInterpreterPath?: string, pythonInterpreterDirectory?: string) { + InterpreterInfoCache.setCacheData('pythonInterpreterDirectory', resource, pythonInterpreterDirectory); + InterpreterInfoCache.setCacheData('pythonInterpreterPath', resource, pythonInterpreterPath); + InterpreterInfoCache.setCacheData('pythonSettingsPath', resource, pythonSettingsPath); + } + + // tslint:disable-next-line:no-any function-name + public static setCustomEnvVariables(resource?: Uri, envVars?: any) { + // tslint:disable-next-line:no-any + InterpreterInfoCache.setCacheData('customEnvVariables', resource, envVars); + } + // tslint:disable-next-line:no-any function-name + private static setCacheData(property: keyof InterpreterCache, resource?: Uri, value?: any) { + const cacheKey = InterpreterInfoCache.getCacheKey(resource) || ''; + // tslint:disable-next-line:prefer-type-cast + const data = cache.has(cacheKey) ? cache.get(cacheKey) : {} as InterpreterCache; + data[property] = value; + cache.set(cacheKey, data); + } + private static getCacheKey(resource?: Uri): string { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return ''; + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.fsPath : ''; + } +} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index 173da70d78ab..076e1a008964 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -22,7 +22,7 @@ class Logger { Logger.writeLine(category, message); } static writeLine(category: string = "log", line: any) { - if (process.env['PYTHON_DONJAYAMANNE_TEST'] !== '1') { + if (process.env['VSC_PYTHON_CI_TEST'] !== '1') { console[category](line); } if (outChannel) { diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index bd3fd3be17d1..2d171de81d19 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -1,17 +1,16 @@ -/// -/// - 'use strict'; // TODO: Cleanup this place // Add options for execPythonFile -import * as path from 'path'; +import * as child_process from 'child_process'; import * as fs from 'fs'; +import * as fsExtra from 'fs-extra'; import * as os from 'os'; -import * as child_process from 'child_process'; +import * as path from 'path'; +import { CancellationToken, Range, TextDocument, Uri } from 'vscode'; import * as settings from './configSettings'; -import { CancellationToken, TextDocument, Range } from 'vscode'; -import { isNotInstalledError } from './helpers'; import { mergeEnvVariables, parseEnvFile } from './envFileParser'; +import { isNotInstalledError } from './helpers'; +import { InterpreterInfoCache } from './interpreterInfoCache'; export const IS_WINDOWS = /^win/.test(process.platform); export const Is_64Bit = os.arch() === 'x64'; @@ -52,32 +51,29 @@ export function fsReaddirAsync(root: string): Promise { }); } -let pythonInterpretterDirectory: string = null; -let previouslyIdentifiedPythonPath: string = null; -let customEnvVariables: any = null; - -// If config settings change then clear env variables that we have cached -// Remember, the path to the python interpreter can change, hence we need to re-set the paths -settings.PythonSettings.getInstance().on('change', function () { - pythonInterpretterDirectory = null; - previouslyIdentifiedPythonPath = null; - customEnvVariables = null; -}); +async function getPythonInterpreterDirectory(resource?: Uri): Promise { + const cache = InterpreterInfoCache.get(resource); + const pythonFileName = settings.PythonSettings.getInstance(resource).pythonPath; -export function getPythonInterpreterDirectory(): Promise { // If we already have it and the python path hasn't changed, yay - if (pythonInterpretterDirectory && previouslyIdentifiedPythonPath === settings.PythonSettings.getInstance().pythonPath) { - return Promise.resolve(pythonInterpretterDirectory); + if (cache.pythonInterpreterDirectory && cache.pythonInterpreterDirectory.length > 0 + && cache.pythonSettingsPath === pythonFileName) { + return cache.pythonInterpreterDirectory; } - let pythonFileName = settings.PythonSettings.getInstance().pythonPath; // Check if we have the path if (path.basename(pythonFileName) === pythonFileName) { - // No path provided, however we can get it by using sys.executableFile - return getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]) - .then(pythonExecutablePath => pythonInterpretterDirectory = path.dirname(pythonExecutablePath)) - .catch(() => pythonInterpretterDirectory = ''); + try { + const pythonInterpreterPath = await getPathFromPythonCommand(pythonFileName); + const pythonInterpreterDirectory = path.dirname(pythonInterpreterPath); + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonInterpreterPath, pythonInterpreterDirectory); + return pythonInterpreterDirectory; + // tslint:disable-next-line:variable-name + } catch (_ex) { + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, ''); + return ''; + } } return new Promise(resolve => { @@ -85,88 +81,78 @@ export function getPythonInterpreterDirectory(): Promise { child_process.execFile(pythonFileName, ['-c', 'print(1234)'], (error, stdout, stderr) => { // Yes this is a valid python path if (stdout.startsWith('1234')) { - previouslyIdentifiedPythonPath = path.dirname(pythonFileName); - } - else { - previouslyIdentifiedPythonPath = ''; + const pythonInterpreterDirectory = path.dirname(pythonFileName); + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, pythonInterpreterDirectory); + resolve(pythonInterpreterDirectory); + } else { + // No idea, didn't work, hence don't reject, but return empty path + InterpreterInfoCache.setPaths(resource, pythonFileName, pythonFileName, ''); + resolve(''); } - // No idea, didn't work, hence don't reject, but return empty path - resolve(previouslyIdentifiedPythonPath); }); }); } -export function getFullyQualifiedPythonInterpreterPath(): Promise { - return getPythonInterpreterDirectory() - .then(pyPath => path.join(pyPath, path.basename(settings.PythonSettings.getInstance().pythonPath))); +export async function getFullyQualifiedPythonInterpreterPath(resource?: Uri): Promise { + const pyDir = await getPythonInterpreterDirectory(resource); + const cache = InterpreterInfoCache.get(resource); + return cache.pythonInterpreterPath; } -export function getPathFromPythonCommand(args: string[]): Promise { - return execPythonFile(settings.PythonSettings.getInstance().pythonPath, args, __dirname).then(stdout => { - if (stdout.length === 0) { - return ""; - } - let lines = stdout.split(/\r?\n/g).filter(line => line.length > 0); - return validatePath(lines[0]); - }).catch(() => { - return ""; +export async function getPathFromPythonCommand(pythonPath: string): Promise { + return await new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + if (stdout) { + const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + resolve(lines.length > 0 ? lines[0] : ''); + } else { + reject(); + } + }); }); } -export function execPythonFile(file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { - const execAsModule = file.toUpperCase() === 'PYTHON' && args.length > 0 && args[0] === '-m'; - - // If running the python file, then always revert to execFileInternal - // Cuz python interpreter is always a file and we can and will always run it using child_process.execFile() - if (file === settings.PythonSettings.getInstance().pythonPath) { - if (stdOut) { - return spawnFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, stdOut, token); - } - if (execAsModule) { - return getFullyQualifiedPythonInterpreterPath() - .then(p => execPythonModule(p, args, { cwd: cwd }, includeErrorAsResponse, token)); - } - return execFileInternal(file, args, { cwd: cwd }, includeErrorAsResponse, token); +async function getEnvVariables(resource?: Uri): Promise<{}> { + const cache = InterpreterInfoCache.get(resource); + if (cache.customEnvVariables) { + return cache.customEnvVariables; } - return getPythonInterpreterDirectory().then(pyPath => { - // We don't have a path - if (pyPath.length === 0) { - let options: child_process.ExecFileOptions = { cwd }; - const envVars = customEnvVariables || getCustomEnvVars(); - if (envVars) { - options.env = envVars; - } - if (stdOut) { - return spawnFileInternal(file, args, options, includeErrorAsResponse, stdOut, token); - } - return execFileInternal(file, args, options, includeErrorAsResponse, token); + const pyPath = await getPythonInterpreterDirectory(resource); + let customEnvVariables = await getCustomEnvVars(resource) || {}; + + if (pyPath.length > 0) { + // Ensure to include the path of the current python. + let newPath = ''; + const currentPath = typeof customEnvVariables[PATH_VARIABLE_NAME] === 'string' ? customEnvVariables[PATH_VARIABLE_NAME] : process.env[PATH_VARIABLE_NAME]; + if (IS_WINDOWS) { + newPath = `${pyPath}\\${path.delimiter}${path.join(pyPath, 'Scripts\\')}${path.delimiter}${currentPath}`; + // This needs to be done for windows. + process.env[PATH_VARIABLE_NAME] = newPath; + } else { + newPath = `${pyPath}${path.delimiter}${currentPath}`; } + customEnvVariables = mergeEnvVariables(customEnvVariables, process.env); + customEnvVariables[PATH_VARIABLE_NAME] = newPath; + } - if (customEnvVariables === null) { - customEnvVariables = getCustomEnvVars(); - customEnvVariables = customEnvVariables ? customEnvVariables : {}; - // Ensure to include the path of the current python - let newPath = ''; - let currentPath = typeof customEnvVariables[PATH_VARIABLE_NAME] === 'string' ? customEnvVariables[PATH_VARIABLE_NAME] : process.env[PATH_VARIABLE_NAME]; - if (IS_WINDOWS) { - newPath = pyPath + '\\' + path.delimiter + path.join(pyPath, 'Scripts\\') + path.delimiter + currentPath; - // This needs to be done for windows - process.env[PATH_VARIABLE_NAME] = newPath; - } - else { - newPath = pyPath + path.delimiter + currentPath; - } - customEnvVariables = mergeEnvVariables(customEnvVariables, process.env); - customEnvVariables[PATH_VARIABLE_NAME] = newPath; - } + InterpreterInfoCache.setCustomEnvVariables(resource, customEnvVariables); + return customEnvVariables; +} +export async function execPythonFile(resource: string | Uri | undefined, file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { + const resourceUri = typeof resource === 'string' ? Uri.file(resource) : resource; + const env = await getEnvVariables(resourceUri); + const options = { cwd, env }; - if (stdOut) { - return spawnFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, stdOut, token); - } - if (execAsModule) { - return getFullyQualifiedPythonInterpreterPath() - .then(p => execPythonModule(p, args, { cwd: cwd, env: customEnvVariables }, includeErrorAsResponse, token)); - } - return execFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, token); - }); + if (stdOut) { + return spawnFileInternal(file, args, options, includeErrorAsResponse, stdOut, token); + } + + const fileIsPythonInterpreter = (file.toUpperCase() === 'PYTHON' || file === settings.PythonSettings.getInstance(resourceUri).pythonPath); + const execAsModule = fileIsPythonInterpreter && args.length > 0 && args[0] === '-m'; + + if (execAsModule) { + return getFullyQualifiedPythonInterpreterPath(resourceUri) + .then(p => execPythonModule(p, args, options, includeErrorAsResponse, token)); + } + return execFileInternal(file, args, options, includeErrorAsResponse, token); } function handleResponse(file: string, includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string, token?: CancellationToken): Promise { @@ -361,21 +347,41 @@ export function getSubDirectories(rootDir: string): Promise { }); } -export function getCustomEnvVars(): any { - const envFile = settings.PythonSettings.getInstance().envFile; - if (typeof envFile === 'string' && - envFile.length > 0 && - fs.existsSync(envFile)) { - - try { - let vars = parseEnvFile(envFile); - if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { - return vars; - } +export async function getCustomEnvVars(resource?: Uri): Promise<{} | undefined | null> { + const envFile = settings.PythonSettings.getInstance(resource).envFile; + if (typeof envFile !== 'string' || envFile.length === 0) { + return null; + } + const exists = await fsExtra.pathExists(envFile); + if (!exists) { + return null; + } + try { + const vars = parseEnvFile(envFile); + if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { + return vars; } - catch (ex) { - console.error('Failed to load env file', ex); + } catch (ex) { + console.error('Failed to parse env file', ex); + } + return null; +} +export function getCustomEnvVarsSync(resource?: Uri): {} | undefined | null { + const envFile = settings.PythonSettings.getInstance(resource).envFile; + if (typeof envFile !== 'string' || envFile.length === 0) { + return null; + } + const exists = fsExtra.pathExistsSync(envFile); + if (!exists) { + return null; + } + try { + const vars = parseEnvFile(envFile); + if (vars && typeof vars === 'object' && Object.keys(vars).length > 0) { + return vars; } + } catch (ex) { + console.error('Failed to parse env file', ex); } return null; } @@ -417,10 +423,12 @@ export function areBasePathsSame(path1: string, path2: string) { path2 = IS_WINDOWS ? path2.replace(/\//g, "\\") : path2; return path.dirname(path1).toUpperCase() === path.dirname(path2).toUpperCase(); } -export function getInterpreterDisplayName(pythonPath: string) { - return execPythonFile(pythonPath, ['--version'], __dirname, true) - .then(version => { - version = version.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0).join(''); - return version; +export async function getInterpreterDisplayName(pythonPath: string) { + return await new Promise((resolve, reject) => { + child_process.execFile(pythonPath, ['--version'], (error, stdout, stdErr) => { + const out = (typeof stdErr === 'string' ? stdErr : '') + os.EOL + (typeof stdout === 'string' ? stdout : ''); + const lines = out.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + resolve(lines.length > 0 ? lines[0] : ''); }); + }); } diff --git a/src/client/extension.ts b/src/client/extension.ts index 8eebcbf0e8dd..dc7c850595e6 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -30,11 +30,12 @@ import { WorkspaceSymbols } from './workspaceSymbols/main'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; import * as os from 'os'; import * as fs from 'fs'; -import { getPathFromPythonCommand } from './common/utils'; import { JupyterProvider } from './jupyter/provider'; import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; import { InterpreterManager } from './interpreter'; import { SimpleConfigurationProvider } from './debugger'; +import { ReplProvider } from './providers/replProvider'; +import { workspace } from 'vscode'; const PYTHON: vscode.DocumentFilter = { language: 'python' }; let unitTestOutChannel: vscode.OutputChannel; @@ -47,11 +48,7 @@ export async function activate(context: vscode.ExtensionContext) { const pythonSettings = settings.PythonSettings.getInstance(); const pythonExt = new PythonExt(); context.subscriptions.push(pythonExt); - // telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD, { - // CodeComplete_Has_ExtraPaths: pythonSettings.autoComplete.extraPaths.length > 0 ? 'true' : 'false', - // Format_Has_Custom_Python_Path: pythonSettings.pythonPath.length !== 'python'.length ? 'true' : 'false', - // Has_PySpark_Path: hasPySparkInCompletionPath ? 'true' : 'false' - // }); + sendStartupTelemetry(); lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow); formatOutChannel = lintingOutChannel; if (pythonSettings.linting.outputWindow !== pythonSettings.formatting.outputWindow) { @@ -66,6 +63,7 @@ export async function activate(context: vscode.ExtensionContext) { sortImports.activate(context, formatOutChannel); const interpreterManager = new InterpreterManager(); await interpreterManager.autoSetInterpreter(); + await interpreterManager.refresh(); context.subscriptions.push(interpreterManager); context.subscriptions.push(new SetInterpreterProvider(interpreterManager)); context.subscriptions.push(...activateExecInTerminalProvider()); @@ -75,15 +73,7 @@ export async function activate(context: vscode.ExtensionContext) { const jediFactory = new JediFactory(context.asAbsolutePath('.')); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - context.subscriptions.push(vscode.commands.registerCommand(Commands.Start_REPL, () => { - getPathFromPythonCommand(["-c", "import sys;print(sys.executable)"]).catch(() => { - return pythonSettings.pythonPath; - }).then(pythonExecutablePath => { - let term = vscode.window.createTerminal('Python', pythonExecutablePath); - term.show(); - context.subscriptions.push(term); - }); - })); + context.subscriptions.push(new ReplProvider()); // Enable indentAction vscode.languages.setLanguageConfiguration(PYTHON.language, { @@ -196,4 +186,8 @@ class ContextKey { this.lastValue = value; vscode.commands.executeCommand('setContext', this.name, this.lastValue); } -} +} + +function sendStartupTelemetry() { + telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD); +} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index 0beaa02e9291..1bbc4b3659c3 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -50,7 +50,7 @@ export abstract class BaseFormatter { if (token && token.isCancellationRequested) { return [filePath, '']; } - return Promise.all([Promise.resolve(filePath), execPythonFile(command, args.concat([filePath]), cwd)]); + return Promise.all([Promise.resolve(filePath), execPythonFile(document.uri, command, args.concat([filePath]), cwd)]); }).then(data => { // Delete the temporary file created if (tmpFileCreated) { diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index cb5120a549e0..24c11f906597 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,12 +1,20 @@ -import { Architecture } from "../common/registry"; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { Architecture } from '../common/registry'; -export interface IInterpreterLocatorService { - getInterpreters(): Promise; +export interface IInterpreterLocatorService extends Disposable { + getInterpreters(resource?: Uri): Promise; } -export interface PythonInterpreter { + +export type PythonInterpreter = { path: string; companyDisplayName?: string; displayName?: string; version?: string; architecture?: Architecture; -} +}; + +export type WorkspacePythonPath = { + folderUri: Uri; + pytonPath?: string; + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; +}; diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 6ce8fdc4be93..b104f4953216 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -6,7 +6,7 @@ import { Disposable, StatusBarItem } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import * as utils from '../../common/utils'; import { IInterpreterLocatorService } from '../contracts'; -import { getFirstNonEmptyLineFromMultilineString } from '../helpers'; +import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers'; import { IInterpreterVersionService } from '../interpreterVersion'; import { VirtualEnvironmentManager } from '../virtualEnvs/index'; @@ -16,13 +16,18 @@ export class InterpreterDisplay implements Disposable { private interpreterLocator: IInterpreterLocatorService, private virtualEnvMgr: VirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { + this.statusBar.command = 'python.setInterpreter'; } public dispose() { // } public async refresh() { - const pythonPath = await this.getFullyQualifiedPathToInterpreter(PythonSettings.getInstance().pythonPath); + const wkspc = getActiveWorkspaceUri(); + if (!wkspc) { + return; + } + const pythonPath = await this.getFullyQualifiedPathToInterpreter(PythonSettings.getInstance(wkspc.folderUri).pythonPath); await this.updateDisplay(pythonPath); } private async getInterpreters() { @@ -41,8 +46,7 @@ export class InterpreterDisplay implements Disposable { const toolTipSuffix = `${EOL}${interpreter.companyDisplayName}`; this.statusBar.tooltip += toolTipSuffix; } - } - else { + } else { const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; await Promise.all([ utils.fsExistsAsync(pythonPath), @@ -72,6 +76,7 @@ export class InterpreterDisplay implements Disposable { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); }) - .then(value => value.length === 0 ? pythonPath : value); + .then(value => value.length === 0 ? pythonPath : value) + .catch(() => pythonPath); } } diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index 7bd180b5b067..f1371ca40922 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,3 +1,6 @@ +import { ConfigurationTarget, window, workspace } from 'vscode'; +import { WorkspacePythonPath } from './contracts'; + export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { return ''; @@ -5,3 +8,65 @@ export function getFirstNonEmptyLineFromMultilineString(stdout: string) { const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); return lines.length > 0 ? lines[0] : ''; } +export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + if (window.activeTextEditor) { + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } + } + return undefined; +} + +export function getInterpretersForEachFolderAndWorkspace(): WorkspacePythonPath[] { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return []; + } + const value = workspace.getConfiguration('python').inspect('pythonPath'); + const workspacePythonPath = value && typeof value.workspaceValue === 'string' ? value.workspaceValue : undefined; + + if (workspace.workspaceFolders.length === 1) { + if (workspacePythonPath) { + return [{ + folderUri: workspace.workspaceFolders[0].uri, + pytonPath: workspacePythonPath, + configTarget: ConfigurationTarget.Workspace + }]; + } + else { + return []; + } + } + + const workspaceConfig: WorkspacePythonPath[] = workspacePythonPath ? [{ + folderUri: workspace.workspaceFolders[0].uri, + pytonPath: workspacePythonPath, + configTarget: ConfigurationTarget.Workspace + }] : []; + + return workspace.workspaceFolders.reduce((accumulator, folder) => { + // tslint:disable-next-line:no-backbone-get-set-outside-model + const folderValue = workspace.getConfiguration('python', folder.uri).inspect('pythonPath'); + + if (folderValue && typeof folderValue.workspaceFolderValue === 'string' && + folderValue.workspaceFolderValue !== workspacePythonPath && + accumulator.findIndex(item => item.pytonPath === folderValue.workspaceFolderValue) === -1) { + + const info: WorkspacePythonPath = { + folderUri: folder.uri, + pytonPath: folderValue.workspaceFolderValue, + configTarget: ConfigurationTarget.WorkspaceFolder + }; + + accumulator.push(info); + } + + return accumulator; + }, workspaceConfig); +} diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index d2eb6fd9e71d..e6ebea67f7a9 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,16 +1,16 @@ 'use strict'; -import { InterpreterVersionService } from './interpreterVersion'; -import { VirtualEnv } from './virtualEnvs/virtualEnv'; -import { VEnv } from './virtualEnvs/venv'; -import { Disposable, window, StatusBarAlignment, workspace } from 'vscode'; +import * as path from 'path'; +import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { IS_WINDOWS } from '../common/utils'; +import { WorkspacePythonPath } from './contracts'; import { InterpreterDisplay } from './display'; +import { getActiveWorkspaceUri } from './helpers'; +import { InterpreterVersionService } from './interpreterVersion'; import { PythonInterpreterLocatorService } from './locators'; import { VirtualEnvironmentManager } from './virtualEnvs/index'; -import { IS_WINDOWS } from '../common/utils'; -import * as path from 'path'; - -const settings = PythonSettings.getInstance(); +import { VEnv } from './virtualEnvs/venv'; +import { VirtualEnv } from './virtualEnvs/virtualEnv'; export class InterpreterManager implements Disposable { private disposables: Disposable[] = []; @@ -22,22 +22,28 @@ export class InterpreterManager implements Disposable { this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr); const versionService = new InterpreterVersionService(); this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); - settings.addListener('change', this.onConfigChanged.bind(this)); - this.display.refresh(); - + PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); + this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); this.disposables.push(statusBar); this.disposables.push(this.display); } - public getInterpreters() { - return this.interpreterProvider.getInterpreters(); + public async refresh() { + return this.display.refresh(); + } + public getInterpreters(resource?: Uri) { + return this.interpreterProvider.getInterpreters(resource); } public async autoSetInterpreter() { if (!this.shouldAutoSetInterpreter()) { return; } - const interpreters = await this.interpreterProvider.getInterpreters(); - const rootPath = workspace.rootPath!.toUpperCase(); - const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(rootPath)); + const activeWorkspace = getActiveWorkspaceUri(); + if (!activeWorkspace) { + return; + } + const interpreters = await this.interpreterProvider.getInterpreters(activeWorkspace.folderUri); + const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); + const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(workspacePathUpper)); if (interpretersInWorkspace.length !== 1) { return; } @@ -46,45 +52,78 @@ export class InterpreterManager implements Disposable { // In windows the interpreter is under scripts/python.exe on linux it is under bin/python. // Meaning the sub directory must be either scripts, bin or other (but only one level deep). const pythonPath = interpretersInWorkspace[0].path; - const relativePath = path.dirname(pythonPath).substring(workspace.rootPath!.length); + const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { - this.setPythonPath(pythonPath); + await this.setPythonPath(pythonPath, activeWorkspace); } } - public setPythonPath(pythonPath: string) { - pythonPath = IS_WINDOWS ? pythonPath.replace(/\\/g, "/") : pythonPath; - if (pythonPath.startsWith(workspace.rootPath!)) { - pythonPath = path.join('${workspaceRoot}', path.relative(workspace.rootPath!, pythonPath)); + /** + * Sets the python path in the settings. + * @param {string} pythonPath + * @param {WorkspacePythonPath} [workspacePythonPath] If this is not passed, then user setting will be updated + * @returns {Promise} + * @memberof InterpreterManager + */ + public async setPythonPath(pythonPath: string, workspacePythonPath?: WorkspacePythonPath): Promise { + pythonPath = IS_WINDOWS ? pythonPath.replace(/\\/g, '/') : pythonPath; + const isMultiRootWorkspace = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; + try { + if (!workspacePythonPath) { + return await this.setPythonPathInUserSettings(pythonPath); + } + if (!isMultiRootWorkspace) { + return await this.setPythonPathInSingleWorkspace(pythonPath); + } + await this.setPythonPathInWorkspace(pythonPath, workspacePythonPath.configTarget, workspacePythonPath.folderUri); + } catch (reason) { + // tslint:disable-next-line:no-unsafe-any prefer-type-cast + const message = reason && typeof reason.message === 'string' ? reason.message as string : ''; + window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); + console.error(reason); } + } + public dispose(): void { + // tslint:disable-next-line:prefer-type-cast + this.disposables.forEach(disposable => disposable.dispose() as void); + this.display = null; + this.interpreterProvider.dispose(); + } + private async setPythonPathInUserSettings(pythonPath) { const pythonConfig = workspace.getConfiguration('python'); - let global = null; - if (typeof workspace.rootPath !== 'string') { - global = true; + return pythonConfig.update('pythonPath', pythonPath, true); + } + private async setPythonPathInSingleWorkspace(pythonPath: string) { + const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:no-non-null-assertion + const workspacePath = workspace.workspaceFolders![0].uri.fsPath; + if (pythonPath.toUpperCase().startsWith(workspacePath.toUpperCase())) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(workspacePath, pythonPath)); } - pythonConfig.update('pythonPath', pythonPath, global).then(() => { - //Done - }, reason => { - window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${reason.message}`); - console.error(reason); - }); + return pythonConfig.update('pythonPath', pythonPath, false); } - - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - this.display = null; + private async setPythonPathInWorkspace(pythonPath, configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder, resource?: Uri) { + const pythonConfig = workspace.getConfiguration('python', resource); + if (configTarget === ConfigurationTarget.WorkspaceFolder && resource && pythonPath.toUpperCase().startsWith(resource.fsPath.toUpperCase())) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(resource.fsPath, pythonPath)); + } + return pythonConfig.update('pythonPath', pythonPath, configTarget); } private shouldAutoSetInterpreter() { - if (!workspace.rootPath) { + const activeWorkspace = getActiveWorkspaceUri(); + if (!activeWorkspace) { return false; } const pythonConfig = workspace.getConfiguration('python'); const pythonPathInConfig = pythonConfig.get('pythonPath', 'python'); - return pythonPathInConfig.toUpperCase() === 'PYTHON'; + return path.basename(pythonPathInConfig) === pythonPathInConfig; } private onConfigChanged() { if (this.display) { + // tslint:disable-next-line:no-floating-promises this.display.refresh(); } } -} \ No newline at end of file +} diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index d12e527dcb13..09ab1c21bc18 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -1,61 +1,90 @@ -"use strict"; +'use strict'; import * as _ from 'lodash'; -import { fixInterpreterPath, fixInterpreterDisplayName } from './helpers'; -import { IInterpreterLocatorService, PythonInterpreter } from '../contracts'; -import { InterpreterVersionService } from '../interpreterVersion'; -import { IS_WINDOWS, Is_64Bit, arePathsSame, areBasePathsSame } from '../../common/utils'; +import { Disposable, Uri, workspace } from 'vscode'; import { RegistryImplementation } from '../../common/registry'; +import { areBasePathsSame, arePathsSame, Is_64Bit, IS_WINDOWS } from '../../common/utils'; +import { IInterpreterLocatorService, PythonInterpreter } from '../contracts'; +import { IInterpreterVersionService, InterpreterVersionService } from '../interpreterVersion'; +import { VirtualEnvironmentManager } from '../virtualEnvs'; +import { fixInterpreterDisplayName, fixInterpreterPath } from './helpers'; +import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService'; import { CondaEnvService } from './services/condaEnvService'; -import { VirtualEnvService, getKnownSearchPathsForVirtualEnvs } from './services/virtualEnvService'; -import { KnownPathsService, getKnownSearchPathsForInterpreters } from './services/KnownPathsService'; import { CurrentPathService } from './services/currentPathService'; +import { getKnownSearchPathsForInterpreters, KnownPathsService } from './services/KnownPathsService'; +import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './services/virtualEnvService'; import { WindowsRegistryService } from './services/windowsRegistryService'; -import { VirtualEnvironmentManager } from '../virtualEnvs'; -import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService'; export class PythonInterpreterLocatorService implements IInterpreterLocatorService { - private interpreters: PythonInterpreter[] = []; - private locators: IInterpreterLocatorService[] = []; + private interpretersPerResource: Map; + private disposables: Disposable[] = []; constructor(private virtualEnvMgr: VirtualEnvironmentManager) { + this.interpretersPerResource = new Map(); + this.disposables.push(workspace.onDidChangeConfiguration(this.onConfigChanged, this)); + } + public async getInterpreters(resource?: Uri) { + const resourceKey = this.getResourceKey(resource); + if (!this.interpretersPerResource.has(resourceKey)) { + const interpreters = await this.getInterpretersPerResource(resource); + this.interpretersPerResource.set(resourceKey, interpreters); + } + + // tslint:disable-next-line:no-non-null-assertion + return this.interpretersPerResource.get(resourceKey)!; + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private onConfigChanged() { + this.interpretersPerResource.clear(); + } + private getResourceKey(resource?: Uri) { + if (!resource) { + return ''; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + return workspaceFolder ? workspaceFolder.uri.fsPath : ''; + } + private async getInterpretersPerResource(resource?: Uri) { + const locators = this.getLocators(resource); + const promises = locators.map(provider => provider.getInterpreters(resource)); + const listOfInterpreters = await Promise.all(promises); + + // tslint:disable-next-line:underscore-consistent-invocation + return _.flatten(listOfInterpreters) + .map(fixInterpreterDisplayName) + .map(fixInterpreterPath) + .reduce((accumulator, current) => { + if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1 && + accumulator.findIndex(item => areBasePathsSame(item.path, current.path)) === -1) { + accumulator.push(current); + } + return accumulator; + }, []); + } + private getLocators(resource?: Uri) { + const locators: IInterpreterLocatorService[] = []; const versionService = new InterpreterVersionService(); // The order of the services is important. if (IS_WINDOWS) { const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); - this.locators.push(windowsRegistryProvider); - this.locators.push(new CondaEnvService(windowsRegistryProvider)); - } - else { - this.locators.push(new CondaEnvService()); + locators.push(windowsRegistryProvider); + locators.push(new CondaEnvService(windowsRegistryProvider)); + } else { + locators.push(new CondaEnvService()); } // Supplements the above list of conda environments. - this.locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); - this.locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(), this.virtualEnvMgr, versionService)); + locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); + locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(resource), this.virtualEnvMgr, versionService)); if (!IS_WINDOWS) { - // This must be last, it is possible we have paths returned here that are already returned + // This must be last, it is possible we have paths returned here that are already returned // in one of the above lists. - this.locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService)); + locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService)); } - // This must be last, it is possible we have paths returned here that are already returned + // This must be last, it is possible we have paths returned here that are already returned // in one of the above lists. - this.locators.push(new CurrentPathService(this.virtualEnvMgr, versionService)); - } - public async getInterpreters() { - if (this.interpreters.length > 0) { - return this.interpreters; - } - const promises = this.locators.map(provider => provider.getInterpreters()); - return Promise.all(promises) - .then(interpreters => _.flatten(interpreters)) - .then(items => items.map(fixInterpreterDisplayName)) - .then(items => items.map(fixInterpreterPath)) - .then(items => items.reduce((accumulator, current) => { - if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1 && - accumulator.findIndex(item => areBasePathsSame(item.path, current.path)) === -1) { - accumulator.push(current); - } - return accumulator; - }, [])) - .then(interpreters => this.interpreters = interpreters); + locators.push(new CurrentPathService(this.virtualEnvMgr, versionService)); + + return locators; } } diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts index bbfdea9ab4cd..bdb240ededad 100644 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -1,22 +1,27 @@ -"use strict"; -import * as path from 'path'; +'use strict'; import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; import { IInterpreterLocatorService } from '../../contracts'; import { IInterpreterVersionService } from '../../interpreterVersion'; -import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; import { lookForInterpretersInDirectory } from '../helpers'; +// tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); export class KnownPathsService implements IInterpreterLocatorService { public constructor(private knownSearchPaths: string[], private versionProvider: IInterpreterVersionService) { } - public getInterpreters() { + // tslint:disable-next-line:no-shadowed-variable + public getInterpreters(_?: Uri) { return this.suggestionsFromKnownPaths(); } - + // tslint:disable-next-line:no-empty + public dispose() { } private suggestionsFromKnownPaths() { const promises = this.knownSearchPaths.map(dir => this.getInterpretersInDirectory(dir)); return Promise.all(promises) + // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)) .then(interpreters => interpreters.filter(item => item.length > 0)) .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); @@ -40,14 +45,14 @@ export function getKnownSearchPathsForInterpreters(): string[] { if (IS_WINDOWS) { return []; } else { - let paths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin']; + const paths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin']; paths.forEach(p => { - paths.push(untildify('~' + p)); + paths.push(untildify(`~${p}`)); }); // Add support for paths such as /Users/xxx/anaconda/bin. - if (process.env['HOME']) { - paths.push(path.join(process.env['HOME'], 'anaconda', 'bin')); - paths.push(path.join(process.env['HOME'], 'python', 'bin')); + if (process.env.HOME) { + paths.push(path.join(process.env.HOME, 'anaconda', 'bin')); + paths.push(path.join(process.env.HOME, 'python', 'bin')); } return paths; } diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts index 5a8c010a961b..1383d571088b 100644 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -1,24 +1,26 @@ -"use strict"; +'use strict'; import * as fs from 'fs-extra'; import * as path from 'path'; +import { Uri } from 'vscode'; import { IS_WINDOWS } from '../../../common/configSettings'; -import { IInterpreterVersionService } from '../../interpreterVersion'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { AnacondaDisplayName, AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH } from './conda'; +import { IInterpreterVersionService } from '../../interpreterVersion'; +import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; export class CondaEnvFileService implements IInterpreterLocatorService { constructor(private condaEnvironmentFile: string, private versionService: IInterpreterVersionService) { } - public getInterpreters() { + public async getInterpreters(_?: Uri) { return this.getSuggestionsFromConda(); } - - private getSuggestionsFromConda(): Promise { + // tslint:disable-next-line:no-empty + public dispose() { } + private async getSuggestionsFromConda(): Promise { return fs.pathExists(this.condaEnvironmentFile) .then(exists => exists ? this.getEnvironmentsFromFile(this.condaEnvironmentFile) : Promise.resolve([])); } - private getEnvironmentsFromFile(envFile: string) { + private async getEnvironmentsFromFile(envFile: string) { return fs.readFile(envFile) .then(buffer => buffer.toString().split(/\r?\n/g)) .then(lines => lines.map(line => line.trim())) @@ -29,11 +31,12 @@ export class CondaEnvFileService implements IInterpreterLocatorService { .then(interpreterPaths => interpreterPaths.map(item => this.getInterpreterDetails(item))) .then(promises => Promise.all(promises)); } - private getInterpreterDetails(interpreter: string) { + private async getInterpreterDetails(interpreter: string) { return this.versionService.getVersion(interpreter, path.basename(interpreter)) .then(version => { version = this.stripCompanyName(version); const envName = this.getEnvironmentRootDirectory(interpreter); + // tslint:disable-next-line:no-unnecessary-local-variable const info: PythonInterpreter = { displayName: `${AnacondaDisplayName} ${version} (${envName})`, path: interpreter, diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index a11cdc54f64b..0b7b281efff1 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -1,48 +1,25 @@ -"use strict"; +'use strict'; import * as child_process from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; +import { Uri } from 'vscode'; +import { VersionUtils } from '../../../common/versionUtils'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { VersionUtils } from "../../../common/versionUtils"; import { AnacondaCompanyName, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; type CondaInfo = { envs?: string[]; - "sys.version"?: string; + 'sys.version'?: string; default_prefix?: string; -} +}; export class CondaEnvService implements IInterpreterLocatorService { constructor(private registryLookupForConda?: IInterpreterLocatorService) { } - public getInterpreters() { + public getInterpreters(resource?: Uri) { return this.getSuggestionsFromConda(); } - - private getSuggestionsFromConda(): Promise { - return this.getCondaFile() - .then(condaFile => { - return new Promise((resolve, reject) => { - // interrogate conda (if it's on the path) to find all environments. - child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => { - if (stdout.length === 0) { - return resolve([]); - } - - try { - const info = JSON.parse(stdout); - resolve(this.parseCondaInfo(info)); - } catch (e) { - // Failed because either: - // 1. conda is not installed. - // 2. `conda info --json` has changed signature. - // 3. output of `conda info --json` has changed in structure. - // In all cases, we can't offer conda pythonPath suggestions. - return resolve([]); - } - }); - }); - }); - } + // tslint:disable-next-line:no-empty + public dispose() { } public getCondaFile() { if (this.registryLookupForConda) { return this.registryLookupForConda.getInterpreters() @@ -63,6 +40,7 @@ export class CondaEnvService implements IInterpreterLocatorService { } public getLatestVersion(interpreters: PythonInterpreter[]) { const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0); + // tslint:disable-next-line:no-non-null-assertion sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); if (sortedInterpreters.length > 0) { return sortedInterpreters[sortedInterpreters.length - 1]; @@ -83,6 +61,7 @@ export class CondaEnvService implements IInterpreterLocatorService { .map(env => { // If it is an environment, hence suffix with env name. const interpreterDisplayName = env === info.default_prefix ? displayName : `${displayName} (${path.basename(env)})`; + // tslint:disable-next-line:no-unnecessary-local-variable const interpreter: PythonInterpreter = { path: path.join(env, ...CONDA_RELATIVE_PY_PATH), displayName: interpreterDisplayName, @@ -94,8 +73,34 @@ export class CondaEnvService implements IInterpreterLocatorService { return Promise.all(promises) .then(interpreters => interpreters.filter(interpreter => interpreter !== null && interpreter !== undefined)) + // tslint:disable-next-line:no-non-null-assertion .then(interpreters => interpreters.map(interpreter => interpreter!)); } + private getSuggestionsFromConda(): Promise { + return this.getCondaFile() + .then(condaFile => { + return new Promise((resolve, reject) => { + // interrogate conda (if it's on the path) to find all environments. + child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => { + if (stdout.length === 0) { + return resolve([]); + } + + try { + const info = JSON.parse(stdout); + resolve(this.parseCondaInfo(info)); + } catch (e) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + // 3. output of `conda info --json` has changed in structure. + // In all cases, we can't offer conda pythonPath suggestions. + return resolve([]); + } + }); + }); + }); + } private getDisplayNameFromVersionInfo(versionInfo: string = '') { if (!versionInfo) { return AnacondaDisplayName; diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index e2ddf45e4fba..55b6c4728fe9 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -1,36 +1,39 @@ -"use strict"; +'use strict'; +import * as child_process from 'child_process'; import * as _ from 'lodash'; import * as path from 'path'; -import * as child_process from 'child_process'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../../common/configSettings'; import { IInterpreterLocatorService } from '../../contracts'; -import { IInterpreterVersionService } from '../../interpreterVersion'; import { getFirstNonEmptyLineFromMultilineString } from '../../helpers'; +import { IInterpreterVersionService } from '../../interpreterVersion'; import { VirtualEnvironmentManager } from '../../virtualEnvs'; -import { PythonSettings } from '../../../common/configSettings'; - -const settings = PythonSettings.getInstance(); export class CurrentPathService implements IInterpreterLocatorService { public constructor(private virtualEnvMgr: VirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { } - public getInterpreters() { + public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownPaths(); } - - private suggestionsFromKnownPaths() { - const currentPythonInterpreter = this.getInterpreter(settings.pythonPath, '').then(interpreter => [interpreter]); + // tslint:disable-next-line:no-empty + public dispose() { } + private async suggestionsFromKnownPaths(resource?: Uri) { + const currentPythonInterpreter = this.getInterpreter(PythonSettings.getInstance(resource).pythonPath, '').then(interpreter => [interpreter]); const python = this.getInterpreter('python', '').then(interpreter => [interpreter]); const python2 = this.getInterpreter('python2', '').then(interpreter => [interpreter]); const python3 = this.getInterpreter('python3', '').then(interpreter => [interpreter]); return Promise.all([currentPythonInterpreter, python, python2, python3]) + // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)) .then(interpreters => interpreters.filter(item => item.length > 0)) + // tslint:disable-next-line:promise-function-async .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); } - private getInterpreterDetails(interpreter: string) { - const virtualEnv = this.virtualEnvMgr.detect(interpreter); - const displayName = this.versionProvider.getVersion(interpreter, path.basename(interpreter)); - return Promise.all([displayName, virtualEnv]) + private async getInterpreterDetails(interpreter: string) { + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) .then(([displayName, virtualEnv]) => { displayName += virtualEnv ? ` (${virtualEnv.name})` : ''; return { @@ -39,9 +42,10 @@ export class CurrentPathService implements IInterpreterLocatorService { }; }); } - private getInterpreter(pythonPath: string, defaultValue: string) { + private async getInterpreter(pythonPath: string, defaultValue: string) { return new Promise(resolve => { - child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + // tslint:disable-next-line:variable-name + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_err, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); }) diff --git a/src/client/interpreter/locators/services/virtualEnvService.ts b/src/client/interpreter/locators/services/virtualEnvService.ts index 64e3b7a3d4e9..a10818f1c3ec 100644 --- a/src/client/interpreter/locators/services/virtualEnvService.ts +++ b/src/client/interpreter/locators/services/virtualEnvService.ts @@ -1,32 +1,36 @@ -"use strict"; +'use strict'; import * as _ from 'lodash'; import * as path from 'path'; -import * as settings from './../../../common/configSettings'; -import { VirtualEnvironmentManager } from '../../virtualEnvs'; +import { Uri, workspace } from 'vscode'; +import { fsReaddirAsync, IS_WINDOWS } from '../../../common/utils'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; import { IInterpreterVersionService } from '../../interpreterVersion'; -import { IS_WINDOWS, fsReaddirAsync } from "../../../common/utils"; +import { VirtualEnvironmentManager } from '../../virtualEnvs'; import { lookForInterpretersInDirectory } from '../helpers'; -import { workspace } from 'vscode'; +import * as settings from './../../../common/configSettings'; +// tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); export class VirtualEnvService implements IInterpreterLocatorService { public constructor(private knownSearchPaths: string[], private virtualEnvMgr: VirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { } - public getInterpreters() { + public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownVenvs(); } - - private suggestionsFromKnownVenvs() { + // tslint:disable-next-line:no-empty + public dispose() { } + private async suggestionsFromKnownVenvs() { return Promise.all(this.knownSearchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) + // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)); } - private lookForInterpretersInVenvs(pathToCheck: string) { + private async lookForInterpretersInVenvs(pathToCheck: string) { return fsReaddirAsync(pathToCheck) .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) .then(dirs => dirs.filter(dir => dir.length > 0)) - .then(dirs => Promise.all(dirs.map(dir => lookForInterpretersInDirectory(dir)))) + .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) + // tslint:disable-next-line:underscore-consistent-invocation .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))); } @@ -41,9 +45,10 @@ export class VirtualEnvService implements IInterpreterLocatorService { })); } private async getVirtualEnvDetails(interpreter: string): Promise { - const displayName = this.versionProvider.getVersion(interpreter, path.basename(interpreter)); - const virtualEnv = this.virtualEnvMgr.detect(interpreter); - return Promise.all([displayName, virtualEnv]) + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) .then(([displayName, virtualEnv]) => { const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); return { @@ -57,15 +62,15 @@ export class VirtualEnvService implements IInterpreterLocatorService { } } -export function getKnownSearchPathsForVirtualEnvs(): string[] { +export function getKnownSearchPathsForVirtualEnvs(resource?: Uri): string[] { const paths: string[] = []; if (!IS_WINDOWS) { const defaultPaths = ['/Envs', '/.virtualenvs', '/.pyenv', '/.pyenv/versions']; defaultPaths.forEach(p => { - paths.push(untildify('~' + p)); + paths.push(untildify(`~${p}`)); }); } - const venvPath = settings.PythonSettings.getInstance().venvPath; + const venvPath = settings.PythonSettings.getInstance(resource).venvPath; if (venvPath) { paths.push(untildify(venvPath)); } @@ -73,4 +78,4 @@ export function getKnownSearchPathsForVirtualEnvs(): string[] { paths.push(workspace.rootPath); } return paths; -} \ No newline at end of file +} diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index 222c88e7139d..de01c5bfadce 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -1,13 +1,18 @@ -import * as path from 'path'; -import * as _ from 'lodash'; import * as fs from 'fs-extra'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; import { Architecture, Hive, IRegistry } from '../../../common/registry'; import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +// tslint:disable-next-line:variable-name const DefaultPythonExecutable = 'python.exe'; +// tslint:disable-next-line:variable-name const CompaniesToIgnore = ['PYLAUNCHER']; -const PythonCoreCompanyDisplayName = "Python Software Foundation"; -const PythonCoreComany = "PYTHONCORE"; +// tslint:disable-next-line:variable-name +const PythonCoreCompanyDisplayName = 'Python Software Foundation'; +// tslint:disable-next-line:variable-name +const PythonCoreComany = 'PYTHONCORE'; type CompanyInterpreter = { companyKey: string, @@ -19,9 +24,12 @@ export class WindowsRegistryService implements IInterpreterLocatorService { constructor(private registry: IRegistry, private is64Bit: boolean) { } - public getInterpreters() { + // tslint:disable-next-line:variable-name + public getInterpreters(_resource?: Uri) { return this.getInterpretersFromRegistry(); } + // tslint:disable-next-line:no-empty + public dispose() { } private async getInterpretersFromRegistry() { // https://github.com/python/peps/blob/master/pep-0514.txt#L357 const hkcuArch = this.is64Bit ? undefined : Architecture.x86; @@ -35,14 +43,17 @@ export class WindowsRegistryService implements IInterpreterLocatorService { } const companies = await Promise.all(promises); + // tslint:disable-next-line:underscore-consistent-invocation const companyInterpreters = await Promise.all(_.flatten(companies) .filter(item => item !== undefined && item !== null) .map(company => { return this.getInterpretersForCompany(company.companyKey, company.hive, company.arch); })); + // tslint:disable-next-line:underscore-consistent-invocation return _.flatten(companyInterpreters) .filter(item => item !== undefined && item !== null) + // tslint:disable-next-line:no-non-null-assertion .map(item => item!) .reduce((prev, current) => { if (prev.findIndex(item => item.path.toUpperCase() === current.path.toUpperCase()) === -1) { @@ -52,7 +63,7 @@ export class WindowsRegistryService implements IInterpreterLocatorService { }, []); } private async getCompanies(hive: Hive, arch?: Architecture): Promise { - return this.registry.getKeys(`\\Software\\Python`, hive, arch) + return this.registry.getKeys('\\Software\\Python', hive, arch) .then(companyKeys => companyKeys .filter(companyKey => CompaniesToIgnore.indexOf(path.basename(companyKey).toUpperCase()) === -1) .map(companyKey => { @@ -84,12 +95,14 @@ export class WindowsRegistryService implements IInterpreterLocatorService { return Promise.all([ Promise.resolve(installPath), this.registry.getValue(key, hive, arch, 'ExecutablePath'), - this.getInterpreterDisplayName(tagKey, companyKey, hive, arch)!, - this.registry.getValue(tagKey, hive, arch, 'Version')!, - this.getCompanyDisplayName(companyKey, hive, arch)! + // tslint:disable-next-line:no-non-null-assertion + this.getInterpreterDisplayName(tagKey, companyKey, hive, arch), + this.registry.getValue(tagKey, hive, arch, 'Version'), + this.getCompanyDisplayName(companyKey, hive, arch) ]) - .then(([installPath, executablePath, displayName, version, companyDisplayName]) => { - return { installPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; + .then(([installedPath, executablePath, displayName, version, companyDisplayName]) => { + // tslint:disable-next-line:prefer-type-cast + return { installPath: installedPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; }); }) .then((interpreterInfo?: InterpreterInformation) => { @@ -100,6 +113,7 @@ export class WindowsRegistryService implements IInterpreterLocatorService { const executablePath = interpreterInfo.executablePath && interpreterInfo.executablePath.length > 0 ? interpreterInfo.executablePath : path.join(interpreterInfo.installPath, DefaultPythonExecutable); const displayName = interpreterInfo.displayName; const version = interpreterInfo.version ? path.basename(interpreterInfo.version) : path.basename(tagKey); + // tslint:disable-next-line:prefer-type-cast return { architecture: arch, displayName, @@ -129,4 +143,4 @@ export class WindowsRegistryService implements IInterpreterLocatorService { const company = path.basename(companyKey); return company.toUpperCase() === PythonCoreComany ? PythonCoreCompanyDisplayName : company; } -} \ No newline at end of file +} diff --git a/src/client/jupyter/jupyter_client/main.ts b/src/client/jupyter/jupyter_client/main.ts index 8d715fa6940a..76ef4b6f4dfa 100644 --- a/src/client/jupyter/jupyter_client/main.ts +++ b/src/client/jupyter/jupyter_client/main.ts @@ -70,7 +70,7 @@ export class JupyterClientAdapter extends EventEmitter implements IJupyterClient let processStarted = false; let handshakeDone = false; - let isInTestRun = newEnv['PYTHON_DONJAYAMANNE_TEST'] === "1"; + let isInTestRun = newEnv['VSC_PYTHON_CI_TEST'] === "1"; const testDef = createDeferred(); const promiseToResolve = isInTestRun ? testDef.resolve.bind(testDef) : def.resolve.bind(def); diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index d4d6f89d58de..551b6bc84622 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -131,7 +131,7 @@ export abstract class BaseLinter { this.outputChannel.append(data); } protected run(command: string, args: string[], document: vscode.TextDocument, cwd: string, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise { - return execPythonFile(command, args, cwd, true, null, cancellation).then(data => { + return execPythonFile(document.uri, command, args, cwd, true, null, cancellation).then(data => { if (!data) { data = ''; } diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index bf1c6b1af4e9..2fb44b7d9449 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -43,7 +43,7 @@ export class Linter extends baseLinter.BaseLinter { } return new Promise((resolve, reject) => { - execPythonFile(prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', document.uri.fsPath]), this.getWorkspaceRootPath(document), false, null, cancellation).then(data => { + execPythonFile(document.uri, prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', document.uri.fsPath]), this.getWorkspaceRootPath(document), false, null, cancellation).then(data => { let parsedData: IProspectorResponse; try { parsedData = JSON.parse(data); diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index 7759ee23a26f..81f2cdbd029f 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -42,7 +42,7 @@ export class Linter extends baseLinter.BaseLinter { let outputChannel = this.outputChannel; return new Promise((resolve, reject) => { - execPythonFile(commandLine, args, this.getWorkspaceRootPath(document), true, null, cancellation).then(data => { + execPythonFile(document.uri, commandLine, args, this.getWorkspaceRootPath(document), true, null, cancellation).then(data => { outputChannel.append('#'.repeat(10) + 'Linting Output - ' + this.Id + '#'.repeat(10) + '\n'); outputChannel.append(data); let outputLines = data.split(/\r?\n/g); diff --git a/src/client/providers/execInTerminalProvider.ts b/src/client/providers/execInTerminalProvider.ts index 0487f7362cf4..15a2277f4525 100644 --- a/src/client/providers/execInTerminalProvider.ts +++ b/src/client/providers/execInTerminalProvider.ts @@ -74,7 +74,7 @@ function execInTerminal(fileUri?: vscode.Uri) { terminal.sendText(`cd "${fileDirPath}"`); } } - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; + const launchArgs = settings.PythonSettings.getInstance(fileUri).terminal.launchArgs; const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; const command = `${currentPythonPath}${launchArgsString} ${filePath}`; if (IS_WINDOWS) { @@ -93,19 +93,19 @@ function execInTerminal(fileUri?: vscode.Uri) { } function execSelectionInTerminal() { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; if (currentPythonPath.indexOf(' ') > 0) { currentPythonPath = `"${currentPythonPath}"`; } - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - const selection = vscode.window.activeTextEditor.selection; let code: string; if (selection.isEmpty) { @@ -119,7 +119,7 @@ function execSelectionInTerminal() { return; } code = removeBlankLines(code); - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; + const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; const command = `${currentPythonPath}${launchArgsString}`; if (!terminal) { @@ -148,19 +148,19 @@ function execSelectionInTerminal() { } function execSelectionInDjangoShell() { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; if (currentPythonPath.indexOf(' ') > 0) { currentPythonPath = `"${currentPythonPath}"`; } - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - const workspaceRoot = vscode.workspace.rootPath; const djangoShellCmd = `"${workspaceRoot}/manage.py" shell`; const selection = vscode.window.activeTextEditor.selection; @@ -175,7 +175,7 @@ function execSelectionInDjangoShell() { if (code.length === 0) { return; } - const launchArgs = settings.PythonSettings.getInstance().terminal.launchArgs; + const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; const command = `${currentPythonPath}${launchArgsString} ${djangoShellCmd}`; if (!terminal) { @@ -201,4 +201,4 @@ function execSelectionInDjangoShell() { terminal.sendText(unix_code); } terminal.show(); -} \ No newline at end of file +} diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 6fe7e25c296a..9b01703ba13a 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as settings from './../common/configSettings'; import * as logger from './../common/logger'; import * as telemetryHelper from '../common/telemetry'; -import { execPythonFile, validatePath } from "../common/utils"; +import { execPythonFile, getCustomEnvVarsSync, validatePath } from '../common/utils'; import { createDeferred, Deferred } from '../common/helpers'; import { getCustomEnvVars } from '../common/utils'; import { mergeEnvVariables } from '../common/envFileParser'; @@ -189,7 +189,7 @@ export class JediProxy implements vscode.Disposable { private spawnProcess(dir: string) { try { let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; - let customEnvironmentVars = getCustomEnvVars(); + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(dir)); if (customEnvironmentVars) { environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); } @@ -488,7 +488,7 @@ export class JediProxy implements vscode.Disposable { private lastKnownPythonPath: string = null; private additionalAutoCopletePaths: string[] = []; private getPathFromPythonCommand(args: string[]): Promise { - return execPythonFile(this.pythonSettings.pythonPath, args, this.workspacePath).then(stdout => { + return execPythonFile(this.workspacePath, this.pythonSettings.pythonPath, args, this.workspacePath).then(stdout => { if (stdout.length === 0) { return ""; } @@ -520,7 +520,7 @@ export class JediProxy implements vscode.Disposable { if (typeof PYTHONPATH !== 'string') { PYTHONPATH = ''; } - let customEnvironmentVars = getCustomEnvVars(); + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(this.pythonProcessCWD)); if (customEnvironmentVars && customEnvironmentVars['PYTHONPATH']) { let PYTHONPATHFromEnvFile = customEnvironmentVars['PYTHONPATH'] as string; if (!path.isAbsolute(PYTHONPATHFromEnvFile) && this.workspacePath === 'string') { diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts new file mode 100644 index 000000000000..1ef8b6cffc8e --- /dev/null +++ b/src/client/providers/replProvider.ts @@ -0,0 +1,46 @@ +import { commands, Disposable, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../common/configSettings'; +import { Commands } from '../common/constants'; +import { getPathFromPythonCommand } from '../common/utils'; + +export class ReplProvider implements Disposable { + private readonly disposables: Disposable[] = []; + constructor() { + this.registerCommand(); + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private registerCommand() { + const disposable = commands.registerCommand(Commands.Start_REPL, this.commandHandler, this); + this.disposables.push(disposable); + } + private async commandHandler() { + const pythonPath = await this.getPythonPath(); + if (!pythonPath) { + return; + } + let pythonInterpreterPath: string; + try { + pythonInterpreterPath = await getPathFromPythonCommand(pythonPath).catch(() => pythonPath); + // tslint:disable-next-line:variable-name + } catch (_ex) { + pythonInterpreterPath = pythonPath; + } + const term = window.createTerminal('Python', pythonInterpreterPath); + term.show(); + this.disposables.push(term); + } + private async getPythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return PythonSettings.getInstance().pythonPath; + } + if (workspace.workspaceFolders.length === 1) { + return PythonSettings.getInstance(workspace.workspaceFolders[0].uri).pythonPath; + } + + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspace ? PythonSettings.getInstance(workspaceFolder.uri).pythonPath : undefined; + } +} diff --git a/src/client/providers/setInterpreterProvider.ts b/src/client/providers/setInterpreterProvider.ts index e9b31c9c9093..47cdc95de966 100644 --- a/src/client/providers/setInterpreterProvider.ts +++ b/src/client/providers/setInterpreterProvider.ts @@ -1,72 +1,116 @@ -"use strict"; +'use strict'; import * as path from 'path'; -import * as vscode from 'vscode'; -import * as settings from './../common/configSettings'; +import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { InterpreterManager } from '../interpreter'; -import { PythonInterpreter } from '../interpreter/contracts'; +import { PythonInterpreter, WorkspacePythonPath } from '../interpreter/contracts'; +import { getInterpretersForEachFolderAndWorkspace } from '../interpreter/helpers'; +import * as settings from './../common/configSettings'; import { ShebangCodeLensProvider } from './shebangCodeLensProvider'; - -interface PythonPathQuickPickItem extends vscode.QuickPickItem { +// tslint:disable-next-line:interface-name +interface PythonPathQuickPickItem extends QuickPickItem { path: string; } -export class SetInterpreterProvider implements vscode.Disposable { - private disposables: vscode.Disposable[] = []; +export class SetInterpreterProvider implements Disposable { + private disposables: Disposable[] = []; constructor(private interpreterManager: InterpreterManager) { - this.disposables.push(vscode.commands.registerCommand("python.setInterpreter", this.setInterpreter.bind(this))); - this.disposables.push(vscode.commands.registerCommand("python.setShebangInterpreter", this.setShebangInterpreter.bind(this))); + this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); + this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); } + private async getWorkspacePythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + // We could have each workspace folder with different python paths. + // Or, we could the workspace with a pythonPath and one of the workspace folders with different python paths. + // Lets just find how many different setups we have. + const configs = getInterpretersForEachFolderAndWorkspace(); + if (configs.length === 1) { + return configs[0]; + } - private suggestionToQuickPickItem(suggestion: PythonInterpreter): PythonPathQuickPickItem { + // Ok we have multiple interpreters, get the user to pick a folder. + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; + } + private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { let detail = suggestion.path; - if (vscode.workspace.rootPath && suggestion.path.startsWith(vscode.workspace.rootPath)) { - detail = `.${path.sep}` + path.relative(vscode.workspace.rootPath!, suggestion.path); + if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { + detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; } return { + // tslint:disable-next-line:no-non-null-assertion label: suggestion.displayName!, description: suggestion.companyDisplayName || '', detail: detail, path: suggestion.path }; } - private presentQuickPick() { - this.getSuggestions().then(suggestions => { - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; - if (vscode.workspace.rootPath && currentPythonPath.startsWith(vscode.workspace.rootPath)) { - currentPythonPath = `.${path.sep}` + path.relative(vscode.workspace.rootPath, currentPythonPath); - } - const quickPickOptions: vscode.QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentPythonPath}` - }; - vscode.window.showQuickPick(suggestions, quickPickOptions).then( - value => { - if (value !== undefined) { - this.interpreterManager.setPythonPath(value.path); - } - }); - }); + private async presentQuickPick() { + const targetConfig = await this.getWorkspacePythonPath(); + const resourceUri = targetConfig ? targetConfig.folderUri : undefined; + const suggestions = await this.getSuggestions(resourceUri); + let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + if (workspace.rootPath && currentPythonPath.startsWith(workspace.rootPath)) { + currentPythonPath = `.${path.sep}${path.relative(workspace.rootPath, currentPythonPath)}`; + } + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${currentPythonPath}` + }; + const selection = await window.showQuickPick(suggestions, quickPickOptions); + if (selection !== undefined) { + this.interpreterManager.setPythonPath(selection.path, targetConfig); + } } - private getSuggestions() { - return this.interpreterManager.getInterpreters() - .then(interpreters => interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1)) - .then(interpreters => interpreters.map(this.suggestionToQuickPickItem)); + private async getSuggestions(resourceUri?: Uri) { + const interpreters = await this.interpreterManager.getInterpreters(resourceUri); + // tslint:disable-next-line:no-non-null-assertion + interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); + return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); } private setInterpreter() { this.presentQuickPick(); } - private async setShebangInterpreter() { - const shebang = await ShebangCodeLensProvider.detectShebang(vscode.window.activeTextEditor.document); - if (shebang) { - this.interpreterManager.setPythonPath(shebang); + private async setShebangInterpreter(): Promise { + const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); + if (!shebang) { + return; + } + + const existingConfigs = getInterpretersForEachFolderAndWorkspace(); + const hasFoldersWithPythonPathSet = existingConfigs.filter(item => item.configTarget === ConfigurationTarget.WorkspaceFolder).length > 0; + + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + // tslint:disable-next-line:no-backbone-get-set-outside-model + const value = workspace.getConfiguration('python', window.activeTextEditor.document.uri).inspect('pythonPath'); + const currentValueSetInWorkspaceFolder = value && typeof value.workspaceFolderValue === 'string'; + + const setPythonPathInSpecificFolder = hasFoldersWithPythonPathSet || currentValueSetInWorkspaceFolder; + if (setPythonPathInSpecificFolder) { + const configTarget = workspaceFolder ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + const workspaceTarget: WorkspacePythonPath = { folderUri: workspaceFolder.uri, configTarget: configTarget }; + return this.interpreterManager.setPythonPath(shebang, workspaceTarget); + } + + const setPythonPathInRootWorkspace = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0; + if (setPythonPathInRootWorkspace) { + const configTarget: WorkspacePythonPath = { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + return this.interpreterManager.setPythonPath(shebang, configTarget); } + + return this.interpreterManager.setPythonPath(shebang); } } diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index aada24c2323d..5a238273d8d8 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import * as child_process from 'child_process'; import { IPythonSettings } from '../common/configSettings'; import { REFACTOR } from '../common/telemetryContracts'; -import { IS_WINDOWS, getCustomEnvVars, getWindowsLineEndingCount } from '../common/utils'; +import { getCustomEnvVars, getCustomEnvVarsSync, getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; import { mergeEnvVariables } from '../common/envFileParser'; export class RefactorProxy extends vscode.Disposable { @@ -106,7 +106,7 @@ export class RefactorProxy extends vscode.Disposable { return new Promise((resolve, reject) => { this._initializeReject = reject; let environmentVariables = { 'PYTHONUNBUFFERED': '1' }; - let customEnvironmentVars = getCustomEnvVars(); + let customEnvironmentVars = getCustomEnvVarsSync(vscode.Uri.file(this.workspaceRoot)); if (customEnvironmentVars) { environmentVariables = mergeEnvVariables(environmentVariables, customEnvironmentVars); } @@ -188,4 +188,4 @@ export class RefactorProxy extends vscode.Disposable { this._commandResolve(response[0]); this._commandResolve = null; } -} \ No newline at end of file +} diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts index 90235b9a5094..9ba64f0a0dbf 100644 --- a/src/client/sortImports.ts +++ b/src/client/sortImports.ts @@ -46,4 +46,4 @@ export function activate(context: vscode.ExtensionContext, outChannel: vscode.Ou }); context.subscriptions.push(disposable); -} \ No newline at end of file +} diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts index 026a46759eff..a1d754f20dbd 100644 --- a/src/client/unittests/common/debugLauncher.ts +++ b/src/client/unittests/common/debugLauncher.ts @@ -9,7 +9,7 @@ export function launchDebugger(rootDirectory: string, testArgs: string[], token? const def = createDeferred(); const launchDef = createDeferred(); let outputChannelShown = false; - execPythonFile(pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { + execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { if (data.startsWith('READY' + os.EOL)) { // debug socket server has started. launchDef.resolve(); diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 0b4b328d4c3e..de3b90245e6e 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -1,44 +1,7 @@ -import { execPythonFile } from './../../common/utils'; import { CancellationToken, OutputChannel, window, workspace } from 'vscode'; -import { getPythonInterpreterDirectory, IS_WINDOWS, PATH_VARIABLE_NAME } from '../../common/utils'; - -let terminal = null; -export function run(file: string, args: string[], cwd: string, token?: CancellationToken, outChannel?: OutputChannel): Promise { - return execPythonFile(file, args, cwd, true, (data: string) => outChannel.append(data), token); +import { IS_WINDOWS, PATH_VARIABLE_NAME } from '../../common/utils'; +import { execPythonFile } from './../../common/utils'; - // Bug, we cannot resolve this - // Resolving here means that tests have completed - // We need a way to determine that the tests have completed succefully.. hmm - // We could use a hack, such as generating a textfile at the end of the command and monitoring.. hack hack hack - // Or we could generate a shell script file and embed all of the hacks in here... hack hack hack... - // return runTestInTerminal(file, args, cwd); +export async function run(file: string, args: string[], cwd: string, token?: CancellationToken, outChannel?: OutputChannel): Promise { + return execPythonFile(cwd, file, args, cwd, true, (data: string) => outChannel.append(data), token); } - -function runTestInTerminal(file: string, args: string[], cwd: string): Promise { - return getPythonInterpreterDirectory().then(pyPath => { - let commands = []; - if (IS_WINDOWS) { - commands.push(`set ${PATH_VARIABLE_NAME}=%${PATH_VARIABLE_NAME}%;${pyPath}`); - } - else { - commands.push(`export ${PATH_VARIABLE_NAME}=$${PATH_VARIABLE_NAME}:${pyPath}`); - } - if (cwd !== workspace.rootPath && typeof cwd === 'string') { - commands.push(`cd ${cwd}`); - } - commands.push(`${file} ${args.join(' ')}`); - terminal = window.createTerminal(`Python Test Log`); - - return new Promise((resolve) => { - setTimeout(function () { - terminal.show(); - terminal.sendText(commands.join(' && ')); - - // Bug, we cannot resolve this - // Resolving here means that tests have completed - // We need a way to determine that the tests have completed succefully.. hmm - resolve(); - }, 1000); - }); - }); -} \ No newline at end of file diff --git a/src/client/unittests/nosetest/collector.ts b/src/client/unittests/nosetest/collector.ts index 1926aa694237..c78d1fd86f9f 100644 --- a/src/client/unittests/nosetest/collector.ts +++ b/src/client/unittests/nosetest/collector.ts @@ -77,7 +77,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc }); } - return execPythonFile(PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.nosetestPath, args.concat(['--collect-only', '-vvv']), rootDirectory, true) + return execPythonFile(rootDirectory, PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.nosetestPath, args.concat(['--collect-only', '-vvv']), rootDirectory, true) .then(data => { outChannel.appendLine(data); processOutput(data); diff --git a/src/client/unittests/pytest/collector.ts b/src/client/unittests/pytest/collector.ts index eecc8055fa11..175bf16414d5 100644 --- a/src/client/unittests/pytest/collector.ts +++ b/src/client/unittests/pytest/collector.ts @@ -83,7 +83,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco }); } - return execPythonFile(PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).unitTest.pyTestPath, args.concat(['--collect-only']), rootDirectory, false, null, token) + return execPythonFile(rootDirectory, PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).unitTest.pyTestPath, args.concat(['--collect-only']), rootDirectory, false, null, token) .then(data => { outChannel.appendLine(data); processOutput(data); diff --git a/src/client/unittests/unittest/collector.ts b/src/client/unittests/unittest/collector.ts index 49e34b4232f0..633cbe4bc671 100644 --- a/src/client/unittests/unittest/collector.ts +++ b/src/client/unittests/unittest/collector.ts @@ -71,7 +71,7 @@ for suite in suites._tests: }); } args = []; - return execPythonFile(PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).pythonPath, args.concat(['-c', pythonScript]), rootDirectory, true, null, token) + return execPythonFile(rootDirectory, PythonSettings.getInstance(vscode.Uri.file(rootDirectory)).pythonPath, args.concat(['-c', pythonScript]), rootDirectory, true, null, token) .then(data => { outChannel.appendLine(data); processOutput(data); diff --git a/src/test/autocomplete/base.test.ts b/src/test/autocomplete/base.test.ts index ee203a46835f..a5398b7beb43 100644 --- a/src/test/autocomplete/base.test.ts +++ b/src/test/autocomplete/base.test.ts @@ -28,7 +28,7 @@ suite('Autocomplete', () => { let isPython3: Promise; suiteSetup(async () => { await initialize(); - let version = await execPythonFile(PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); isPython3 = Promise.resolve(version.indexOf('3.') >= 0); }); setup(() => initializeTest()); diff --git a/src/test/autocomplete/pep484.test.ts b/src/test/autocomplete/pep484.test.ts index 1ad220ea929c..c0c327bdf7d1 100644 --- a/src/test/autocomplete/pep484.test.ts +++ b/src/test/autocomplete/pep484.test.ts @@ -22,7 +22,7 @@ suite('Autocomplete PEP 484', () => { let isPython3: Promise; suiteSetup(async () => { await initialize(); - const version = await execPythonFile(PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); isPython3 = Promise.resolve(version.indexOf('3.') >= 0); }); setup(() => initializeTest()); diff --git a/src/test/autocomplete/pep526.test.ts b/src/test/autocomplete/pep526.test.ts index 6d330195ccdf..01dc988c000f 100644 --- a/src/test/autocomplete/pep526.test.ts +++ b/src/test/autocomplete/pep526.test.ts @@ -22,7 +22,7 @@ suite('Autocomplete PEP 526', () => { let isPython3: Promise; suiteSetup(async () => { await initialize(); - const version = await execPythonFile(PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(rootWorkspaceUri, PythonSettings.getInstance(rootWorkspaceUri).pythonPath, ['--version'], __dirname, true); isPython3 = Promise.resolve(version.indexOf('3.') >= 0); }); setup(() => initializeTest()); diff --git a/src/test/common.ts b/src/test/common.ts index 2013b9487c59..486293b24015 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { PythonSettings } from '../client/common/configSettings'; +import { IS_MULTI_ROOT_TEST } from './initialize'; const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); export const rootWorkspaceUri = getWorkspaceRoot(); @@ -37,3 +38,50 @@ function getWorkspaceRoot() { const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(fileInNonRootWorkspace)); return workspaceFolder ? workspaceFolder.uri : workspace.workspaceFolders[0].uri; } + +// tslint:disable-next-line:no-any +export function retryAsync(wrapped: Function, retryCount: number = 2) { + // tslint:disable-next-line:no-any + return async (...args: any[]) => { + return new Promise((resolve, reject) => { + // tslint:disable-next-line:no-any + const reasons: any[] = []; + + const makeCall = () => { + // tslint:disable-next-line:no-unsafe-any no-any + // tslint:disable-next-line:no-invalid-this + wrapped.call(this, ...args) + // tslint:disable-next-line:no-unsafe-any no-any + .then(resolve, (reason: any) => { + reasons.push(reason); + if (reasons.length >= retryCount) { + reject(reasons); + } else { + // If failed once, lets wait for some time before trying again. + // tslint:disable-next-line:no-string-based-set-timeout + setTimeout(makeCall, 500); + } + }); + }; + + makeCall(); + }); + }; +} + +async function setPythonPathInWorkspace(resource: string | Uri | undefined, config: ConfigurationTarget, pythonPath?: string) { + if (config === ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { + return; + } + const resourceUri = typeof resource === 'string' ? Uri.file(resource) : resource; + const settings = workspace.getConfiguration('python', resourceUri); + const value = settings.inspect('pythonPath'); + const prop: 'workspaceFolderValue' | 'workspaceValue' = config === ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + if (value && value[prop] !== pythonPath) { + await settings.update('pythonPath', pythonPath, config); + PythonSettings.dispose(); + } +} + +export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); +export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath); diff --git a/src/test/common/common.test.ts b/src/test/common/common.test.ts index 2a04c3c2737f..0e72ac770b71 100644 --- a/src/test/common/common.test.ts +++ b/src/test/common/common.test.ts @@ -1,30 +1,23 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; -import * as vscode from 'vscode'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import { initialize } from './../initialize'; -import { execPythonFile } from '../../client/common/utils'; import { EOL } from 'os'; +import * as vscode from 'vscode'; import { createDeferred } from '../../client/common/helpers'; +import { execPythonFile, getInterpreterDisplayName } from '../../client/common/utils'; +import { initialize } from './../initialize'; // Defines a Mocha test suite to group tests of similar kind together suite('ChildProc', () => { - setup(() => initialize()); + setup(initialize); + teardown(initialize); test('Standard Response', done => { - execPythonFile('python', ['-c', 'print(1)'], __dirname, false).then(data => { - assert.ok(data === '1' + EOL); + execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false).then(data => { + assert.ok(data === `1${EOL}`); }).then(done).catch(done); }); test('Error Response', done => { + // tslint:disable-next-line:no-any const def = createDeferred(); - execPythonFile('python', ['-c', 'print(1'], __dirname, false).then(() => { + execPythonFile(undefined, 'python', ['-c', 'print(1'], __dirname, false).then(() => { def.reject('Should have failed'); }).catch(() => { def.resolve(); @@ -38,9 +31,9 @@ suite('ChildProc', () => { function handleOutput(data: string) { output.push(data); } - execPythonFile('python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { + execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { assert.equal(output.length, 1, 'Ouput length incorrect'); - assert.equal(output[0], '1' + EOL, 'Ouput value incorrect'); + assert.equal(output[0], `1${EOL}`, 'Ouput value incorrect'); }).then(done).catch(done); }); @@ -49,32 +42,34 @@ suite('ChildProc', () => { function handleOutput(data: string) { output.push(data); } - await execPythonFile('python', ['-c', `print('öä')`], __dirname, false, handleOutput) + await execPythonFile(undefined, 'python', ['-c', 'print(\'öä\')'], __dirname, false, handleOutput); assert.equal(output.length, 1, 'Ouput length incorrect'); - assert.equal(output[0], 'öä' + EOL, 'Ouput value incorrect'); + assert.equal(output[0], `öä${EOL}`, 'Ouput value incorrect'); }); test('Stream Stdout with Threads', function (done) { + // tslint:disable-next-line:no-invalid-this this.timeout(6000); const output: string[] = []; function handleOutput(data: string) { output.push(data); } - execPythonFile('python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { + execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { assert.equal(output.length, 2, 'Ouput length incorrect'); - assert.equal(output[0], '1' + EOL, 'First Ouput value incorrect'); - assert.equal(output[1], '2' + EOL, 'Second Ouput value incorrect'); + assert.equal(output[0], `1${EOL}`, 'First Ouput value incorrect'); + assert.equal(output[1], `2${EOL}`, 'Second Ouput value incorrect'); }).then(done).catch(done); }); test('Kill', done => { + // tslint:disable-next-line:no-any const def = createDeferred(); const output: string[] = []; function handleOutput(data: string) { output.push(data); } const cancellation = new vscode.CancellationTokenSource(); - execPythonFile('python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { + execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { def.reject('Should not have completed'); }).catch(() => { def.resolve(); @@ -86,4 +81,10 @@ suite('ChildProc', () => { def.promise.then(done).catch(done); }); + + test('Get Python display name', async () => { + const displayName = await getInterpreterDisplayName('python'); + assert.equal(typeof displayName, 'string', 'Display name not returned'); + assert.notEqual(displayName.length, 0, 'Display name cannot be empty'); + }); }); diff --git a/src/test/common/configSettings.multiroot.test.ts b/src/test/common/configSettings.multiroot.test.ts index 17c96c631dd3..dc7ef9590885 100644 --- a/src/test/common/configSettings.multiroot.test.ts +++ b/src/test/common/configSettings.multiroot.test.ts @@ -2,33 +2,29 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); // tslint:disable-next-line:max-func-body-length suite('Multiroot Config Settings', () => { - suiteSetup(async () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); await initialize(); - await resetSettings(); }); setup(initializeTest); suiteTeardown(closeActiveWindows); teardown(async () => { - await resetSettings(); await closeActiveWindows(); + await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); + await initializeTest(); }); - async function resetSettings() { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - const settings = workspace.getConfiguration('python', workspaceUri); - const value = settings.inspect('pythonPath'); - if (value && typeof value.workspaceFolderValue === 'string') { - await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); - } - PythonSettings.dispose(); - } - async function enableDisableLinterSetting(resource: Uri, configTarget: ConfigurationTarget, setting: string, enabled: boolean | undefined): Promise { const settings = workspace.getConfiguration('python.linting', resource); const cfgValue = settings.inspect(setting); @@ -51,8 +47,6 @@ suite('Multiroot Config Settings', () => { if (value && typeof value.workspaceFolderValue === 'string') { await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); } - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); settings = workspace.getConfiguration('python', workspaceUri); PythonSettings.dispose(); const cfgSetting = PythonSettings.getInstance(workspaceUri); @@ -66,8 +60,6 @@ suite('Multiroot Config Settings', () => { await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); const privatePythonPath = `x${new Date().getTime()}`; await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); const cfgSetting = PythonSettings.getInstance(workspaceUri); assert.equal(cfgSetting.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); @@ -83,8 +75,6 @@ suite('Multiroot Config Settings', () => { // Update workspace folder to something else so it gets refreshed. await settings.update('pythonPath', `x${new Date().getTime()}`, ConfigurationTarget.WorkspaceFolder); await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); const document = await workspace.openTextDocument(fileToOpen); const cfg = PythonSettings.getInstance(document.uri); @@ -100,8 +90,6 @@ suite('Multiroot Config Settings', () => { await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); const privatePythonPath = `x${new Date().getTime()}`; await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); const document = await workspace.openTextDocument(fileToOpen); const cfg = PythonSettings.getInstance(document.uri); @@ -114,12 +102,8 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); let settings = PythonSettings.getInstance(workspaceUri); assert.equal(settings.linting.pylintEnabled, true, 'Pylint not enabled when it should be'); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); settings = PythonSettings.getInstance(workspaceUri); assert.equal(settings.linting.pylintEnabled, false, 'Pylint enabled when it should not be'); }); @@ -129,8 +113,6 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); let cfgSetting = PythonSettings.getInstance(workspaceUri); assert.equal(cfgSetting.linting.pylintEnabled, false, 'Workspace folder pylint setting is true when it should not be'); @@ -138,8 +120,6 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); cfgSetting = PythonSettings.getInstance(workspaceUri); assert.equal(cfgSetting.linting.pylintEnabled, true, 'Workspace folder pylint setting is false when it should not be'); @@ -151,8 +131,6 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); let document = await workspace.openTextDocument(fileToOpen); let cfg = PythonSettings.getInstance(document.uri); assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); @@ -160,8 +138,6 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); document = await workspace.openTextDocument(fileToOpen); cfg = PythonSettings.getInstance(document.uri); assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); @@ -173,8 +149,6 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); let document = await workspace.openTextDocument(fileToOpen); let cfg = PythonSettings.getInstance(document.uri); assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); @@ -182,8 +156,6 @@ suite('Multiroot Config Settings', () => { await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); document = await workspace.openTextDocument(fileToOpen); cfg = PythonSettings.getInstance(document.uri); assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index d49b32c0a066..786ae7bb278e 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -1,8 +1,10 @@ import * as assert from 'assert'; -import { closeActiveWindows, IS_TRAVIS, initializeTest } from './../initialize'; -import { MockOutputChannel } from './../mockClasses'; -import { Installer, Product } from '../../client/common/installer'; +import * as path from 'path'; +import { Uri } from 'vscode'; import { EnumEx } from '../../client/common/enumUtils'; +import { Installer, Product } from '../../client/common/installer'; +import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; // TODO: Need to mock the command runner, to check what commands are being sent. // Instead of altering the environment. @@ -10,23 +12,25 @@ import { EnumEx } from '../../client/common/enumUtils'; suite('Installer', () => { let outputChannel: MockOutputChannel; let installer: Installer; - - suiteSetup(() => { + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; + suiteSetup(async () => { outputChannel = new MockOutputChannel('Installer'); installer = new Installer(outputChannel); + await initializeTest(); }); - setup(() => initializeTest()); - suiteTeardown(() => closeActiveWindows()); - teardown(() => closeActiveWindows()); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); async function testUninstallingProduct(product: Product) { - const isInstalled = await installer.isInstalled(product); + let isInstalled = await installer.isInstalled(product, resource); if (isInstalled) { - await installer.uninstall(product); - const isInstalled = await installer.isInstalled(product); + await installer.uninstall(product, resource); + isInstalled = await installer.isInstalled(product, resource); // Someimtes installation doesn't work on Travis if (!IS_TRAVIS) { - assert.equal(isInstalled, false, `Product uninstall failed`); + assert.equal(isInstalled, false, 'Product uninstall failed'); } } } @@ -41,14 +45,14 @@ suite('Installer', () => { }); async function testInstallingProduct(product: Product) { - const isInstalled = await installer.isInstalled(product); + const isInstalled = await installer.isInstalled(product, resource); if (!isInstalled) { - await installer.install(product); + await installer.install(product, resource); } - const checkIsInstalledAgain = await installer.isInstalled(product); + const checkIsInstalledAgain = await installer.isInstalled(product, resource); // Someimtes installation doesn't work on Travis if (!IS_TRAVIS) { - assert.notEqual(checkIsInstalledAgain, false, `Product installation failed`); + assert.notEqual(checkIsInstalledAgain, false, 'Product installation failed'); } } EnumEx.getNamesAndValues(Product).forEach(prod => { diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts index ed792f087f6b..ce41edead475 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -41,8 +41,8 @@ suite('Formatting', () => { fs.copySync(originalUnformattedFile, file, { overwrite: true }); }); fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); - const yapf = execPythonFile('yapf', [originalUnformattedFile], workspaceRootPath, false); - const autoPep8 = execPythonFile('autopep8', [originalUnformattedFile], workspaceRootPath, false); + const yapf = execPythonFile(workspaceRootPath, 'yapf', [originalUnformattedFile], workspaceRootPath, false); + const autoPep8 = execPythonFile(workspaceRootPath, 'autopep8', [originalUnformattedFile], workspaceRootPath, false); await Promise.all([yapf, autoPep8]).then(formattedResults => { formattedYapf = formattedResults[0]; formattedAutoPep8 = formattedResults[1]; diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts index f5768eb0da99..c8c2f1c6ffc3 100644 --- a/src/test/format/extension.sort.test.ts +++ b/src/test/format/extension.sort.test.ts @@ -26,8 +26,6 @@ suite('Sorting', () => { fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); await closeActiveWindows(); }); setup(async () => { @@ -35,8 +33,6 @@ suite('Sorting', () => { fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); await closeActiveWindows(); }); @@ -78,8 +74,6 @@ suite('Sorting', () => { test('With Changes and Config in Args', async () => { await updateSetting('sortImports.args', ['-sp', path.join(sortingPath, 'withconfig')], Uri.file(sortingPath), ConfigurationTarget.Workspace); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); const editor = await window.showTextDocument(textDocument); await editor.edit(builder => { @@ -91,8 +85,6 @@ suite('Sorting', () => { }); test('With Changes and Config in Args (via Command)', async () => { await updateSetting('sortImports.args', ['-sp', path.join(sortingPath, 'withconfig')], Uri.file(sortingPath), configTarget); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 2000)); const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); const editor = await window.showTextDocument(textDocument); await editor.edit(builder => { diff --git a/src/test/index.ts b/src/test/index.ts index 10bd39fbd4bb..077d65e200e8 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,22 +1,12 @@ import { initializePython, IS_MULTI_ROOT_TEST } from './initialize'; const testRunner = require('vscode/lib/testrunner'); - -const singleWorkspaceTestConfig = { - ui: 'tdd', - useColors: true, - timeout: 25000, - grep: 'Multiroot', - invert: 'invert' -}; -const multiWorkspaceTestConfig = { - ui: 'tdd', - useColors: true, - timeout: 25000, - grep: 'Jupyter', - invert: 'invert' -}; +process.env['VSC_PYTHON_CI_TEST'] = '1'; // You can directly control Mocha options by uncommenting the following lines. // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info. -testRunner.configure(IS_MULTI_ROOT_TEST ? multiWorkspaceTestConfig : singleWorkspaceTestConfig); +testRunner.configure({ + ui: 'tdd', + useColors: true, + timeout: 25000 +}); module.exports = testRunner; diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 8cd932698ba9..f67afc30b8ec 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,52 +1,54 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it import * as assert from 'assert'; import * as fs from 'fs'; -import * as vscode from 'vscode'; import * as path from 'path'; -let dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +import * as vscode from 'vscode'; +import { PythonSettings } from '../client/common/configSettings'; +import { activated } from '../client/extension'; +import { clearPythonPathInWorkspaceFolder, setPythonPathInWorkspaceRoot } from './common'; + +const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); -//First thing to be executed -process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; +//First thing to be executed. +// tslint:disable-next-line:no-string-literal +process.env['VSC_PYTHON_CI_TEST'] = '1'; -let configSettings: any = undefined; -let extensionActivated: boolean = false; +const PYTHON_PATH = getPythonPath(); +// tslint:disable-next-line:no-string-literal prefer-template +export const IS_TRAVIS = (process.env['TRAVIS'] + '') === 'true'; +export const TEST_TIMEOUT = 25000; +export const IS_MULTI_ROOT_TEST = isMultitrootTest(); + +// Ability to use custom python environments for testing +export async function initializePython() { + await clearPythonPathInWorkspaceFolder(dummyPythonFile); + await setPythonPathInWorkspaceRoot(PYTHON_PATH); +} + +// tslint:disable-next-line:no-any export async function initialize(): Promise { await initializePython(); // Opening a python file activates the extension. await vscode.workspace.openTextDocument(dummyPythonFile); - if (!extensionActivated) { - const ext = require('../client/extension'); - await ext.activated; - extensionActivated = true; - } - if (!configSettings) { - configSettings = await require('../client/common/configSettings'); - } + await activated; // Dispose any cached python settings (used only in test env). - configSettings.PythonSettings.dispose(); + PythonSettings.dispose(); } +// tslint:disable-next-line:no-any export async function initializeTest(): Promise { await initializePython(); await closeActiveWindows(); - if (!configSettings) { - configSettings = await require('../client/common/configSettings'); - } - // Dispose any cached python settings (used only in test env) - configSettings.PythonSettings.dispose(); + // Dispose any cached python settings (used only in test env). + PythonSettings.dispose(); } export async function wait(timeoutMilliseconds: number) { return new Promise(resolve => { + // tslint:disable-next-line:no-string-based-set-timeout setTimeout(resolve, timeoutMilliseconds); }); } +// tslint:disable-next-line:no-any export async function closeActiveWindows(): Promise { // https://github.com/Microsoft/vscode/blob/master/extensions/vscode-api-tests/src/utils.ts return new Promise(resolve => { @@ -71,6 +73,7 @@ export async function closeActiveWindows(): Promise { return resolve(); } vscode.commands.executeCommand('workbench.action.closeAllEditors') + // tslint:disable-next-line:no-any .then(() => null, (err: any) => { clearInterval(interval); resolve(); @@ -82,9 +85,10 @@ export async function closeActiveWindows(): Promise { }); } - function getPythonPath(): string { + // tslint:disable-next-line:no-unsafe-any if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) { + // tslint:disable-next-line:no-unsafe-any return process.env.TRAVIS_PYTHON_PATH; } return 'python'; @@ -93,18 +97,3 @@ function getPythonPath(): string { function isMultitrootTest() { return Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 1; } - -const PYTHON_PATH = getPythonPath(); -export const IS_TRAVIS = (process.env['TRAVIS'] + '') === 'true'; -export const TEST_TIMEOUT = 25000; -export const IS_MULTI_ROOT_TEST = isMultitrootTest(); - -// Ability to use custom python environments for testing -export async function initializePython() { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const value = pythonConfig.inspect('pythonPath'); - if (value && value.workspaceValue !== PYTHON_PATH) { - return await pythonConfig.update('pythonPath', PYTHON_PATH, vscode.ConfigurationTarget.Workspace); - } -} - diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 823f773ede6c..59f9d44c78c6 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -1,21 +1,25 @@ import * as assert from 'assert'; import * as path from 'path'; +import { Uri } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { initialize, initializeTest } from '../initialize'; import { IS_WINDOWS } from '../../client/common/utils'; -import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; import { AnacondaCompanyName } from '../../client/interpreter/locators/services/conda'; +import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; +import { initialize, initializeTest } from '../initialize'; import { MockProvider } from './mocks'; -import { PythonInterpreter } from '../../client/interpreter/contracts'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +// tslint:disable-next-line:max-func-body-length suite('Interpreters from Conda Environments', () => { - suiteSetup(() => initialize()); - setup(() => initializeTest()); + suiteSetup(initialize); + setup(initializeTest); test('Must return an empty list for empty json', async () => { const condaProvider = new CondaEnvService(); - const interpreters = await condaProvider.parseCondaInfo({} as any) + // tslint:disable-next-line:no-any prefer-type-cast + const interpreters = await condaProvider.parseCondaInfo({} as any); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); test('Must extract display name from version info', async () => { @@ -105,12 +109,12 @@ suite('Interpreters from Conda Environments', () => { test('Must detect conda environments from a list', async () => { const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, - { displayName: 'Two', path: PythonSettings.getInstance().pythonPath, companyDisplayName: 'Two 2' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2' }, { displayName: 'Three', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'Three 3' }, { displayName: 'Anaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, { displayName: 'xAnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'xContinuum Analytics, Inc.' }, - { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' }, + { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); const condaProvider = new CondaEnvService(mockRegistryProvider); @@ -118,39 +122,45 @@ suite('Interpreters from Conda Environments', () => { assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[2]), false, '3. Identified environment incorrectly'); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[3]), true, `4. Failed to identify conda environment when displayName starts with 'Anaconda'`); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[4]), true, `5. Failed to identify conda environment when displayName contains text 'Anaconda'`); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[5]), true, `6. Failed to identify conda environment when comanyDisplayName contains 'Continuum'`); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[6]), true, `7. Failed to identify conda environment when companyDisplayName starts with 'Continuum'`); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[3]), true, '4. Failed to identify conda environment when displayName starts with \'Anaconda\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[4]), true, '5. Failed to identify conda environment when displayName contains text \'Anaconda\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[5]), true, '6. Failed to identify conda environment when comanyDisplayName contains \'Continuum\''); + assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[6]), true, '7. Failed to identify conda environment when companyDisplayName starts with \'Continuum\''); }); test('Correctly identifies latest version when major version is different', async () => { const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, - { displayName: 'Two', path: PythonSettings.getInstance().pythonPath, companyDisplayName: 'Two 2', version: '3.1.3' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '3.1.3' }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + // tslint:disable-next-line:no-any { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + // tslint:disable-next-line:no-any { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); const condaProvider = new CondaEnvService(mockRegistryProvider); + // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); }); test('Correctly identifies latest version when major version is same', async () => { const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, - { displayName: 'Two', path: PythonSettings.getInstance().pythonPath, companyDisplayName: 'Two 2', version: '2.11.3' }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '2.11.3' }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + // tslint:disable-next-line:no-any { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + // tslint:disable-next-line:no-any { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); const condaProvider = new CondaEnvService(mockRegistryProvider); + // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); }); test('Must use Conda env from Registry to locate conda.exe', async () => { diff --git a/src/test/interpreters/display.multiroot.test.ts b/src/test/interpreters/display.multiroot.test.ts new file mode 100644 index 000000000000..c2710638b23e --- /dev/null +++ b/src/test/interpreters/display.multiroot.test.ts @@ -0,0 +1,52 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializePython, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockStatusBarItem } from '../mockClasses'; +import { MockInterpreterVersionProvider } from './mocks'; +import { MockProvider } from './mocks'; + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); +const fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Interpreters Display', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(initializePython); + teardown(async () => { + await clearPythonPathInWorkspaceFolder(fileToOpen); + await initialize(); + await closeActiveWindows(); + }); + + test('Must get display name from workspace folder interpreter and not from interpreter in workspace', async () => { + const settings = workspace.getConfiguration('python', Uri.file(fileToOpen)); + const pythonPath = fileToOpen; + await settings.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + PythonSettings.dispose(); + + const document = await workspace.openTextDocument(fileToOpen); + await window.showTextDocument(document); + + const statusBar = new MockStatusBarItem(); + const provider = new MockProvider([]); + const displayName = `${path.basename(pythonPath)} [Environment]`; + const displayNameProvider = new MockInterpreterVersionProvider(displayName); + const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); + await display.refresh(); + + assert.equal(statusBar.text, displayName, 'Incorrect display name'); + }); +}); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index 8c5b3b517c2b..0cf3271124a3 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -1,29 +1,39 @@ -import { PythonSettings } from '../../client/common/configSettings'; import * as assert from 'assert'; import * as child_process from 'child_process'; +import { EOL } from 'os'; import * as path from 'path'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { clearPythonPathInWorkspaceFolder, rootWorkspaceUri, updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockStatusBarItem } from '../mockClasses'; import { MockInterpreterVersionProvider } from './mocks'; -import { InterpreterDisplay } from '../../client/interpreter/display'; import { MockProvider, MockVirtualEnv } from './mocks'; -import { EOL } from 'os'; -import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; -import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { ConfigurationTarget } from 'vscode'; +const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); + +// tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { - suiteSetup(() => initialize()); - setup(() => initializeTest()); + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(initialize); + setup(async () => { + await initializeTest(); + if (IS_MULTI_ROOT_TEST) { + await initializeMultiRoot(); + } + }); teardown(async () => { + await clearPythonPathInWorkspaceFolder(fileInNonRootWorkspace); await initialize(); await closeActiveWindows(); }); - test('Must have command name', () => { const statusBar = new MockStatusBarItem(); const displayNameProvider = new MockInterpreterVersionProvider(''); + // tslint:disable-next-line:no-unused-expression new InterpreterDisplay(statusBar, new MockProvider([]), new VirtualEnvironmentManager([]), displayNameProvider); assert.equal(statusBar.command, 'python.setInterpreter', 'Incorrect command name'); }); @@ -49,7 +59,7 @@ suite('Interpreters Display', () => { await display.refresh(); assert.equal(statusBar.text, `${displayName} (${env2.name})`, 'Incorrect display name'); }); - test(`Must display default 'Display name' for unknown interpreter`, async () => { + test('Must display default \'Display name\' for unknown interpreter', async () => { const statusBar = new MockStatusBarItem(); const provider = new MockProvider([]); const displayName = 'Mock Display Name'; @@ -57,7 +67,7 @@ suite('Interpreters Display', () => { const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); // Change interpreter to an invalid value const pythonPath = 'UnknownInterpreter'; - await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, ConfigurationTarget.Workspace); + await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); await display.refresh(); const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; @@ -65,15 +75,15 @@ suite('Interpreters Display', () => { }); test('Must get display name from a list of interpreters', async () => { const pythonPath = await new Promise(resolve => { - child_process.execFile(PythonSettings.getInstance().pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); - }).then(value => value.length === 0 ? PythonSettings.getInstance().pythonPath : value); + }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); const statusBar = new MockStatusBarItem(); const interpreters = [ { displayName: 'One', path: 'c:/path1/one.exe', type: 'One 1' }, { displayName: 'Two', path: pythonPath, type: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', type: 'Three 3' }, + { displayName: 'Three', path: 'c:/path3/three.exe', type: 'Three 3' } ]; const provider = new MockProvider(interpreters); const displayName = 'Mock Display Name'; @@ -85,16 +95,16 @@ suite('Interpreters Display', () => { }); test('Must suffix tooltip with the companyDisplayName of interpreter', async () => { const pythonPath = await new Promise(resolve => { - child_process.execFile(PythonSettings.getInstance().pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { + child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { resolve(getFirstNonEmptyLineFromMultilineString(stdout)); }); - }).then(value => value.length === 0 ? PythonSettings.getInstance().pythonPath : value); + }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); const statusBar = new MockStatusBarItem(); const interpreters = [ { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider(''); @@ -109,16 +119,24 @@ suite('Interpreters Display', () => { const interpreters = [ { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider('', true); const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider); // Change interpreter to an invalid value const pythonPath = 'UnknownInterpreter'; - await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, ConfigurationTarget.Workspace); + await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); await display.refresh(); assert.equal(statusBar.text, '$(alert) Select Python Environment', 'Incorrect display name'); }); + async function initializeMultiRoot() { + // For multiroot environments, we need a file open to determine the best interpreter that needs to be displayed + await openDummyFile(); + } + async function openDummyFile() { + const document = await workspace.openTextDocument(fileInNonRootWorkspace); + await window.showTextDocument(document); + } }); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index c4dde62edf6c..34c41bb41c75 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -1,21 +1,23 @@ +import { Architecture, Hive, IRegistry } from '../../client/common/registry'; +import { IInterpreterLocatorService, PythonInterpreter } from '../../client/interpreter/contracts'; import { IInterpreterVersionService } from '../../client/interpreter/interpreterVersion'; import { IVirtualEnvironment } from '../../client/interpreter/virtualEnvs/contracts'; -import { IInterpreterLocatorService, PythonInterpreter } from "../../client/interpreter/contracts"; -import { IRegistry, Hive, Architecture } from "../../client/common/registry"; export class MockProvider implements IInterpreterLocatorService { constructor(private suggestions: PythonInterpreter[]) { } - getInterpreters(): Promise { + public getInterpreters(): Promise { return Promise.resolve(this.suggestions); } + // tslint:disable-next-line:no-empty + public dispose() { } } export class MockRegistry implements IRegistry { constructor(private keys: { key: string, hive: Hive, arch?: Architecture, values: string[] }[], private values: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[]) { } - getKeys(key: string, hive: Hive, arch?: Architecture): Promise { + public getKeys(key: string, hive: Hive, arch?: Architecture): Promise { const items = this.keys.find(item => { if (item.arch) { return item.key === key && item.hive === hive && item.arch === arch; @@ -25,7 +27,7 @@ export class MockRegistry implements IRegistry { return items ? Promise.resolve(items.values) : Promise.resolve([]); } - getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { + public getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { const items = this.values.find(item => { if (item.key !== key || item.hive !== hive) { return false; @@ -46,14 +48,17 @@ export class MockRegistry implements IRegistry { export class MockVirtualEnv implements IVirtualEnvironment { constructor(private isDetected: boolean, public name: string) { } - detect(pythonPath: string): Promise { + public detect(pythonPath: string): Promise { return Promise.resolve(this.isDetected); } } +// tslint:disable-next-line:max-classes-per-file export class MockInterpreterVersionProvider implements IInterpreterVersionService { constructor(private displayName: string, private useDefaultDisplayName: boolean = false) { } - getVersion(pythonPath: string, defaultDisplayName: string): Promise { + public getVersion(pythonPath: string, defaultDisplayName: string): Promise { return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); } + // tslint:disable-next-line:no-empty + public dispose() { } } diff --git a/src/test/jupyter/jupyter.codeHelper.test.ts b/src/test/jupyter/jupyter.codeHelper.test.ts index 60643f2d6b2b..27b529b72530 100644 --- a/src/test/jupyter/jupyter.codeHelper.test.ts +++ b/src/test/jupyter/jupyter.codeHelper.test.ts @@ -1,14 +1,20 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import * as path from 'path'; -import { initialize, closeActiveWindows, initializeTest } from './../initialize'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { CodeHelper } from '../../client/jupyter/common/codeHelper'; import { JupyterCodeLensProvider } from '../../client/jupyter/editorIntegration/codeLensProvider'; const FILE_WITH_CELLS = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'jupyter', 'cells.py'); suite('Jupyter Code Helper', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => initializeTest()); teardown(() => closeActiveWindows()); const codeLensProvider = new JupyterCodeLensProvider(); diff --git a/src/test/jupyter/jupyterClient.test.ts b/src/test/jupyter/jupyterClient.test.ts index c74d027db47c..60cac7bb9d66 100644 --- a/src/test/jupyter/jupyterClient.test.ts +++ b/src/test/jupyter/jupyterClient.test.ts @@ -1,22 +1,28 @@ import * as assert from 'assert'; import { MockOutputChannel } from './mocks'; -import { initialize, initializeTest } from './../initialize'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelRestartedError, KernelShutdownError } from '../../client/jupyter/common/errors'; import { createDeferred } from '../../client/common/helpers'; import { KernelspecMetadata } from '../../client/jupyter/contracts'; suite('JupyterClient', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; output = new MockOutputChannel('Jupyter'); jupyter = new JupyterClientAdapter(output, __dirname); return initializeTest(); }); teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; output.dispose(); jupyter.dispose(); @@ -26,7 +32,7 @@ suite('JupyterClient', () => { let jupyter: JupyterClientAdapter; test('Ping (Process and Socket)', done => { - jupyter.start({ 'PYTHON_DONJAYAMANNE_TEST': '1', 'DEBUG_DJAYAMANNE_IPYTHON': '1' }).then(() => { + jupyter.start({ 'VSC_PYTHON_CI_TEST': '1', 'DEBUG_DJAYAMANNE_IPYTHON': '1' }).then(() => { done(); }).catch(reason => { assert.fail(reason, undefined, 'Starting Jupyter failed', ''); @@ -82,7 +88,7 @@ suite('JupyterClient', () => { }); test('Start Kernel (without start)', done => { jupyter.getAllKernelSpecs().then(kernelSpecs => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; // Ok we got the kernelspecs, now create another new jupyter client // and tell it to start a specific kernel @@ -98,7 +104,7 @@ suite('JupyterClient', () => { done(); }); - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; }).catch(reason => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); @@ -408,7 +414,7 @@ suite('JupyterClient', () => { assert.fail(reason, undefined, 'Failed to retrieve kernelspecs', ''); done(); }); - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; }); test('Execute multiple blocks of Code', done => { jupyter.start().then(() => { diff --git a/src/test/jupyter/jupyterKernel.test.ts b/src/test/jupyter/jupyterKernel.test.ts index fddedbe166c1..89377d2c897c 100644 --- a/src/test/jupyter/jupyterKernel.test.ts +++ b/src/test/jupyter/jupyterKernel.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import { MockOutputChannel } from './mocks'; -import { initialize, initializeTest } from './../initialize'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelShutdownError } from '../../client/jupyter/common/errors'; import { createDeferred } from '../../client/common/helpers'; @@ -8,9 +8,15 @@ import { JupyterClientKernel } from '../../client/jupyter/jupyter_client-Kernel' import { KernelspecMetadata } from '../../client/jupyter/contracts'; suite('Jupyter Kernel', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; disposables = []; output = new MockOutputChannel('Jupyter'); @@ -20,7 +26,7 @@ suite('Jupyter Kernel', () => { return initializeTest(); }); teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; output.dispose(); jupyter.dispose(); diff --git a/src/test/jupyter/jupyterKernelManager.test.ts b/src/test/jupyter/jupyterKernelManager.test.ts index f4c5e9f51c4f..27b99e77c5e6 100644 --- a/src/test/jupyter/jupyterKernelManager.test.ts +++ b/src/test/jupyter/jupyterKernelManager.test.ts @@ -1,14 +1,20 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import { MockOutputChannel } from './mocks'; -import { initialize, initializeTest } from './../initialize'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { JupyterClientAdapter } from '../../client/jupyter/jupyter_client/main'; import { KernelManagerImpl } from '../../client/jupyter/kernel-manager'; suite('Kernel Manager', () => { - suiteSetup(() => initialize()); + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); setup(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '1'; disposables = []; output = new MockOutputChannel('Jupyter'); @@ -20,7 +26,7 @@ suite('Kernel Manager', () => { return initializeTest(); }); teardown(() => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '1'; + process.env['VSC_PYTHON_CI_TEST'] = '1'; process.env['DEBUG_DJAYAMANNE_IPYTHON'] = '0'; output.dispose(); jupyter.dispose(); @@ -40,7 +46,7 @@ suite('Kernel Manager', () => { const oldRegisterCommand = vscode.commands.registerCommand; test('GetAllKernelSpecsFor python', done => { - process.env['PYTHON_DONJAYAMANNE_TEST'] = '0'; + process.env['VSC_PYTHON_CI_TEST'] = '0'; const mgr = new KernelManagerImpl(output, jupyter); disposables.push(mgr); mgr.getAllKernelSpecsFor('python').then(specMetadata => { diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index 17089260ab49..5ad0f11b6cbb 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -1,19 +1,25 @@ import * as assert from 'assert'; import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; import * as baseLinter from '../../client/linters/baseLinter'; -import * as pyLint from '../../client/linters/pylint'; import * as flake8 from '../../client/linters/flake8'; -import { CancellationTokenSource, ConfigurationTarget, Uri, window, workspace } from 'vscode'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import * as pyLint from '../../client/linters/pylint'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { MockOutputChannel } from '../mockClasses'; -import { PythonSettings } from '../../client/common/configSettings'; const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); suite('Multiroot Linting', () => { - suiteSetup(() => initialize()); - setup(() => initializeTest()); - suiteTeardown(() => closeActiveWindows()); + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + return initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); teardown(async () => { await closeActiveWindows(); PythonSettings.dispose(); @@ -35,42 +41,42 @@ suite('Multiroot Linting', () => { } test('Enabling Pylint in root and also in Workspace, should return errors', async () => { - let ch = new MockOutputChannel('Lint'); + const ch = new MockOutputChannel('Lint'); await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', true); await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', true); }); test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { - let ch = new MockOutputChannel('Lint'); + const ch = new MockOutputChannel('Lint'); await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', true); await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', false); }); test('Disabling Pylint in root and enabling in Workspace, should return errors', async () => { - let ch = new MockOutputChannel('Lint'); + const ch = new MockOutputChannel('Lint'); await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'pylintEnabled', false); await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); await testLinterInWorkspaceFolder(new pyLint.Linter(ch), 'workspace1', true); }); test('Enabling Flake8 in root and also in Workspace, should return errors', async () => { - let ch = new MockOutputChannel('Lint'); + const ch = new MockOutputChannel('Lint'); await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', true); await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', true); await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', true); }); test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { - let ch = new MockOutputChannel('Lint'); + const ch = new MockOutputChannel('Lint'); await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', true); await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', false); await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', false); }); test('Disabling Flake8 in root and enabling in Workspace, should return errors', async () => { - let ch = new MockOutputChannel('Lint'); + const ch = new MockOutputChannel('Lint'); await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, 'flake8Enabled', false); await enableDisableSetting(path.join(multirootPath, 'workspace1'), ConfigurationTarget.WorkspaceFolder, 'flake8Enabled', true); await testLinterInWorkspaceFolder(new flake8.Linter(ch), 'workspace1', true); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index de989eab0355..6eaa262f398e 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -116,7 +116,7 @@ suite('Linting', () => { suiteSetup(async () => { pylintFileToLintLines = fs.readFileSync(fileToLint).toString('utf-8').split(/\r?\n/g); await initialize(); - const version = await execPythonFile(PythonSettings.getInstance().pythonPath, ['--version'], __dirname, true); + const version = await execPythonFile(fileToLint, PythonSettings.getInstance(vscode.Uri.file(fileToLint)).pythonPath, ['--version'], __dirname, true); isPython3Deferred.resolve(version.indexOf('3.') >= 0); }); setup(async () => { diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py new file mode 100644 index 000000000000..4825f3a4db3b --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py @@ -0,0 +1,15 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def tst_A(self): + self.fail("Not implemented") + + def tst_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py new file mode 100644 index 000000000000..c9a76c07f933 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py @@ -0,0 +1,18 @@ +import unittest + +class Tst_test2(unittest.TestCase): + def tst_A2(self): + self.fail("Not implemented") + + def tst_B2(self): + self.assertEqual(1,1,'Not equal') + + def tst_C2(self): + self.assertEqual(1,2,'Not equal') + + def tst_D2(self): + raise ArithmeticError() + pass + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/test_root.py b/src/test/pythonFiles/testFiles/noseFiles/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py new file mode 100644 index 000000000000..734b84cd342e --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test4A(self): + self.fail("Not implemented") + + def test4B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py new file mode 100644 index 000000000000..ad89d873e879 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py @@ -0,0 +1,32 @@ +import unittest + +class Test_test2(unittest.TestCase): + def test_A2(self): + self.fail("Not implemented") + + def test_B2(self): + self.assertEqual(1,1,'Not equal') + + def test_C2(self): + self.assertEqual(1,2,'Not equal') + + def test_D2(self): + raise ArithmeticError() + pass + +class Test_test2a(unittest.TestCase): + def test_222A2(self): + self.fail("Not implemented") + + def test_222B2(self): + self.assertEqual(1,1,'Not equal') + + class Test_test2a1(unittest.TestCase): + def test_222A2wow(self): + self.fail("Not implemented") + + def test_222B2wow(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py new file mode 100644 index 000000000000..507e6af02063 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index 870ae5639aa1..ddf743277aaa 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -1,22 +1,28 @@ import * as assert from 'assert'; -import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Tests, TestsToRun } from '../../client/unittests/common/contracts'; +import { TestResultDisplay } from '../../client/unittests/display/main'; import * as nose from '../../client/unittests/nosetest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { TestsToRun } from '../../client/unittests/common/contracts'; -import { TestResultDisplay } from '../../client/unittests/display/main'; import { MockOutputChannel } from './../mockClasses'; -const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const filesToDelete = [path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard', '.noseids'), -path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src', '.noseids')]; -const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); +const filesToDelete = [ + path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), + path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') +]; +// tslint:disable-next-line:max-func-body-length suite('Unit Tests (nosetest)', () => { const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + let testManager: nose.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: vscode.OutputChannel; suiteSetup(async () => { filesToDelete.forEach(file => { if (fs.existsSync(file)) { @@ -38,95 +44,123 @@ suite('Unit Tests (nosetest)', () => { testResultDisplay = new TestResultDisplay(outChannel); await initializeTest(); }); - teardown(() => { + teardown(async () => { outChannel.dispose(); testManager.dispose(); testResultDisplay.dispose(); - return updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { testManager = new nose.TestManager(rootDir, outChannel); } - const rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: nose.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; - test('Discover Tests (single test file)', async () => { - testManager = new nose.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); - const tests = await testManager.discoverTests(true, true) + test('Discover Tests (single test file)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_one.py') && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Check that nameToRun in testSuits has class name after : (single test file)', async () => { - testManager = new nose.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + test('Check that nameToRun in testSuits has class name after : (single test file)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testSuits.every(t => t.testSuite.name === t.testSuite.nameToRun.split(":")[1]), true, 'Suite name does not match class name'); + assert.equal(tests.testSuits.every(t => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), true, 'Suite name does not match class name'); }); - test('Discover Tests (pattern = test_)', async () => { + function lookForTestFile(tests: Tests, testFile: string) { + const found = tests.testFiles.some(t => t.name === testFile && t.nameToRun === t.name); + assert.equal(found, true, `Test File not found '${testFile}'`); + } + test('Discover Tests (-m=test)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 22, 'Incorrect number of test functions'); + assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); assert.equal(tests.testSuits.length, 6, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_one.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_two.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_another_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'unittest_three_test.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); + lookForTestFile(tests, path.join('tests', 'test_unittest_one.py')); + lookForTestFile(tests, path.join('tests', 'test_unittest_two.py')); + lookForTestFile(tests, path.join('tests', 'unittest_three_test.py')); + lookForTestFile(tests, path.join('tests', 'test4.py')); + lookForTestFile(tests, 'test_root.py'); }); - test('Discover Tests (pattern = _test_)', async () => { - await updateSetting('unitTest.nosetestArgs', ['-m=*test*'], rootWorkspaceUri, configTarget); + test('Discover Tests (-w=specific -m=tst)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 18, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 5, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_one.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_unittest_two.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_another_pytest.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'unittest_three_test.py') && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('specific', 'tst_unittest_one.py')); + lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); }); - test('Run Tests', async () => { + test('Discover Tests (-m=test_)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); + createTestManager(); + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + lookForTestFile(tests, 'test_root.py'); + }); + + test('Run Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); - assert.equal(results.summary.errors, 5, 'Errors'); - assert.equal(results.summary.failures, 6, 'Failures'); - assert.equal(results.summary.passed, 8, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); }); - test('Run Failed Tests', async () => { + test('Run Failed Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); - assert.equal(results.summary.errors, 5, 'Errors'); - assert.equal(results.summary.failures, 6, 'Failures'); - assert.equal(results.summary.passed, 8, 'Passed'); - assert.equal(results.summary.skipped, 3, 'skipped'); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); results = await testManager.runTest(true); - assert.equal(results.summary.errors, 5, 'Errors again'); - assert.equal(results.summary.failures, 6, 'Failures again'); + assert.equal(results.summary.errors, 1, 'Errors again'); + assert.equal(results.summary.failures, 7, 'Failures again'); assert.equal(results.summary.passed, 0, 'Passed again'); assert.equal(results.summary.skipped, 0, 'skipped again'); }); - test('Run Specific Test File', async () => { + test('Run Specific Test File', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testFile: TestsToRun = { testFile: [tests.testFiles[0]], testFolder: [], testFunction: [], testSuite: [] }; + const testFileToRun = tests.testFiles.find(t => t.fullPath.endsWith('test_root.py')); + assert.ok(testFileToRun, 'Test file not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFile: TestsToRun = { testFile: [testFileToRun!], testFolder: [], testFunction: [], testSuite: [] }; const results = await testManager.runTest(testFile); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -134,10 +168,16 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Suite', async () => { + test('Run Specific Test Suite', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; + const testSuiteToRun = tests.testSuits.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testSuiteToRun, 'Test suite not found'); + // tslint:disable-next-line:no-non-null-assertion + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToRun!.testSuite] }; const results = await testManager.runTest(testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -145,25 +185,20 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Function', async () => { + test('Run Specific Test Function', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; + const testFnToRun = tests.testFunctions.find(f => f.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testFnToRun, 'Test function not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [testFnToRun!.testFunction], testSuite: [] }; const results = await testManager.runTest(testFn); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); assert.equal(results.summary.passed, 0, 'Passed'); assert.equal(results.summary.skipped, 0, 'skipped'); }); - - test('Setting cwd should return tests', async () => { - await updateSetting('unitTest.nosetestArgs', ['tests'], rootWorkspaceUri, configTarget); - createTestManager(unitTestTestFilesCwdPath); - - const tests = await testManager.discoverTests(true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); - assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); - }); }); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index 7156c76ecab1..77086d86808f 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -1,46 +1,48 @@ import * as assert from 'assert'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { TestFile, TestsToRun } from '../../client/unittests/common/contracts'; +import { TestResultDisplay } from '../../client/unittests/display/main'; import * as pytest from '../../client/unittests/pytest/main'; -import * as path from 'path'; -import * as configSettings from '../../client/common/configSettings'; +import { rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { TestsToRun, TestFile } from '../../client/unittests/common/contracts'; -import { TestResultDisplay } from '../../client/unittests/display/main'; import { MockOutputChannel } from './../mockClasses'; -import { rootWorkspaceUri, updateSetting } from '../common'; const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'unitestsWithConfigs'); const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); +// tslint:disable-next-line:max-func-body-length suite('Unit Tests (PyTest)', () => { + let rootDirectory = UNITTEST_TEST_FILES_PATH; + let testManager: pytest.TestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: vscode.OutputChannel; const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; suiteSetup(async () => { await initialize(); await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); }); - setup(() => { + setup(async () => { rootDirectory = UNITTEST_TEST_FILES_PATH; outChannel = new MockOutputChannel('Python Test Log'); testResultDisplay = new TestResultDisplay(outChannel); - return initializeTest(); + await initializeTest(); }); - teardown(() => { + teardown(async () => { outChannel.dispose(); testManager.dispose(); testResultDisplay.dispose(); - return updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { testManager = new pytest.TestManager(rootDir, outChannel); } - let rootDirectory = UNITTEST_TEST_FILES_PATH; - let testManager: pytest.TestManager; - let testResultDisplay: TestResultDisplay; - let outChannel: vscode.OutputChannel; - test('Discover Tests (single test file)', async () => { + test('Discover Tests (single test file)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); @@ -50,7 +52,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Discover Tests (pattern = test_)', async () => { + test('Discover Tests (pattern = test_)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -65,7 +69,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Discover Tests (pattern = _test)', async () => { + test('Discover Tests (pattern = _test)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -75,8 +81,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - - test('Discover Tests (with config)', async () => { + test('Discover Tests (with config)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); rootDirectory = UNITTEST_TEST_FILES_PATH_WITH_CONFIGS; createTestManager(); @@ -88,7 +95,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Run Tests', async () => { + test('Run Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); @@ -98,7 +107,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 3, 'skipped'); }); - test('Run Failed Tests', async () => { + test('Run Failed Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); @@ -114,7 +125,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'Failed skipped'); }); - test('Run Specific Test File', async () => { + test('Run Specific Test File', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); await testManager.discoverTests(true, true); @@ -135,7 +148,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - test('Run Specific Test Suite', async () => { + test('Run Specific Test Suite', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -147,7 +162,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Function', async () => { + test('Run Specific Test Function', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -159,8 +176,9 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - - test('Setting cwd should return tests', async () => { + test('Setting cwd should return tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index 478111c1d620..be12df3af776 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -51,7 +51,9 @@ suite('Unit Tests (unittest)', () => { testManager = new unittest.TestManager(rootDir, outChannel); } - test('Discover Tests (single test file)', async () => { + test('Discover Tests (single test file)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); const tests = await testManager.discoverTests(true, true); @@ -61,7 +63,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); }); - test('Discover Tests', async () => { + test('Discover Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -72,7 +76,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found'); }); - test('Discover Tests (pattern = *_test_*.py)', async () => { + test('Discover Tests (pattern = *_test_*.py)', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -82,7 +88,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found'); }); - test('Run Tests', async () => { + test('Run Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', 'test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); @@ -92,7 +100,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Failed Tests', async () => { + test('Run Failed Tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); @@ -108,7 +118,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 0, 'Failed skipped'); }); - test('Run Specific Test File', async () => { + test('Run Specific Test File', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); const tests = await testManager.discoverTests(true, true); @@ -124,7 +136,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Suite', async () => { + test('Run Specific Test Suite', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); const tests = await testManager.discoverTests(true, true); @@ -140,7 +154,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Function', async () => { + test('Run Specific Test Function', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -152,7 +168,9 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - test('Setting cwd should return tests', async () => { + test('Setting cwd should return tests', async function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); diff --git a/src/test/workspaceSymbols/common.ts b/src/test/workspaceSymbols/common.ts index f713b64969c3..527b852ab6ad 100644 --- a/src/test/workspaceSymbols/common.ts +++ b/src/test/workspaceSymbols/common.ts @@ -2,7 +2,7 @@ import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; export async function enableDisableWorkspaceSymbols(resource: Uri, enabled: boolean, configTarget: ConfigurationTarget) { - let settings = workspace.getConfiguration('python', resource); + const settings = workspace.getConfiguration('python', resource); await settings.update('workspaceSymbols.enabled', enabled, configTarget); PythonSettings.dispose(); -} \ No newline at end of file +} diff --git a/src/test/workspaceSymbols/multiroot.test.ts b/src/test/workspaceSymbols/multiroot.test.ts index bbd352f11d6f..9d4de7940274 100644 --- a/src/test/workspaceSymbols/multiroot.test.ts +++ b/src/test/workspaceSymbols/multiroot.test.ts @@ -1,25 +1,31 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri } from 'vscode'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; import { Generator } from '../../client/workspaceSymbols/generator'; -import { MockOutputChannel } from '../mockClasses'; import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { MockOutputChannel } from '../mockClasses'; import { updateSetting } from './../common'; const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); suite('Multiroot Workspace Symbols', () => { - suiteSetup(() => initialize()); - setup(() => initializeTest()); - suiteTeardown(() => closeActiveWindows()); + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + return initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); teardown(async () => { await closeActiveWindows(); await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'parent', 'child')), ConfigurationTarget.WorkspaceFolder); await updateSetting('workspaceSymbols.enabled', false, Uri.file(path.join(multirootPath, 'workspace2')), ConfigurationTarget.WorkspaceFolder); }); - test(`symbols should be returned when enabeld and vice versa`, async () => { + test('symbols should be returned when enabeld and vice versa', async () => { const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); const outputChannel = new MockOutputChannel('Output'); @@ -38,7 +44,7 @@ suite('Multiroot Workspace Symbols', () => { symbols = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); assert.notEqual(symbols.length, 0, 'Symbols should be returned when workspace symbols are turned on'); }); - test(`symbols should be filtered correctly`, async () => { + test('symbols should be filtered correctly', async () => { const childWorkspaceUri = Uri.file(path.join(multirootPath, 'parent', 'child')); const workspace2Uri = Uri.file(path.join(multirootPath, 'workspace2')); const outputChannel = new MockOutputChannel('Output'); diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 09c3fbb10f1b..6aca26f07b90 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -24,13 +24,16 @@ "python.linting.mypyEnabled": false, "python.linting.pydocstyleEnabled": false, "python.linting.pylamaEnabled": false, - "python.linting.pylintEnabled": false, + "python.linting.pylintEnabled": true, "python.linting.pep8Enabled": false, "python.linting.prospectorEnabled": false, "python.workspaceSymbols.enabled": false, "python.formatting.formatOnSave": false, "python.formatting.provider": "yapf", - "python.sortImports.args": [], + "python.sortImports.args": [ + "-sp", + "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" + ], "python.linting.lintOnSave": false, "python.linting.lintOnTextChange": false, "python.linting.enabled": true, diff --git a/src/testMultiRootWkspc/workspace1/.vscode/settings.json b/src/testMultiRootWkspc/workspace1/.vscode/settings.json index a783cfe01962..f4d89e3bc0e4 100644 --- a/src/testMultiRootWkspc/workspace1/.vscode/settings.json +++ b/src/testMultiRootWkspc/workspace1/.vscode/settings.json @@ -1,5 +1,5 @@ { - "python.linting.pylintEnabled": true, "python.linting.enabled": false, - "python.linting.flake8Enabled": true + "python.linting.flake8Enabled": true, + "python.linting.pylintEnabled": false } diff --git a/tslint.json b/tslint.json index a3918b71ef07..f55dca7c2b4c 100644 --- a/tslint.json +++ b/tslint.json @@ -18,7 +18,12 @@ "typedef": false, "no-string-throw": true, "missing-jsdoc": false, - "one-line": [true, "check-catch", "check-finally", "check-else"], + "one-line": [ + true, + "check-catch", + "check-finally", + "check-else" + ], "no-parameter-properties": false, "no-reserved-keywords": false, "newline-before-return": false, @@ -32,6 +37,10 @@ "allow-string", "allow-number" ], - "await-promise": [true, "Thenable"] + "await-promise": [ + true, + "Thenable" + ], + "completed-docs": false } }