diff --git a/news/2 Fixes/1352.md b/news/2 Fixes/1352.md new file mode 100644 index 000000000000..3f246199823d --- /dev/null +++ b/news/2 Fixes/1352.md @@ -0,0 +1 @@ +Improvements to the display format of interpreter information in the list of interpreters. diff --git a/src/client/common/installer/pythonInstallation.ts b/src/client/common/installer/pythonInstallation.ts index d70b1e96bcb7..6cfe6b40da46 100644 --- a/src/client/common/installer/pythonInstallation.ts +++ b/src/client/common/installer/pythonInstallation.ts @@ -4,24 +4,28 @@ import { IInterpreterHelper, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { IApplicationShell } from '../application/types'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IPlatformService } from '../platform/types'; import { IPythonSettings } from '../types'; export class PythonInstaller { private locator: IInterpreterLocatorService; private shell: IApplicationShell; + private workspaceService: IWorkspaceService; constructor(private serviceContainer: IServiceContainer) { this.locator = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); this.shell = serviceContainer.get(IApplicationShell); + this.workspaceService = serviceContainer.get(IWorkspaceService); } public async checkPythonInstallation(settings: IPythonSettings): Promise { if (settings.disableInstallationChecks === true) { return true; } - const interpreters = await this.locator.getInterpreters(); + + const workspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; + const interpreters = await this.locator.getInterpreters(workspaceUri); if (interpreters.length > 0) { const platform = this.serviceContainer.get(IPlatformService); const helper = this.serviceContainer.get(IInterpreterHelper); diff --git a/src/client/common/platform/pathUtils.ts b/src/client/common/platform/pathUtils.ts index 319d523402fb..16fc16c530de 100644 --- a/src/client/common/platform/pathUtils.ts +++ b/src/client/common/platform/pathUtils.ts @@ -2,10 +2,15 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { IPathUtils, IsWindows } from '../types'; import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants'; +// tslint:disable-next-line:no-var-requires no-require-imports +const untildify = require('untildify'); @injectable() export class PathUtils implements IPathUtils { - constructor(@inject(IsWindows) private isWindows: boolean) { } + public readonly home = ''; + constructor(@inject(IsWindows) private isWindows: boolean) { + this.home = untildify('~'); + } public get delimiter(): string { return path.delimiter; } @@ -16,5 +21,13 @@ export class PathUtils implements IPathUtils { public basename(pathValue: string, ext?: string): string { return path.basename(pathValue, ext); } - + public getDisplayName(pathValue: string, cwd?: string): string { + if (cwd && pathValue.startsWith(cwd)) { + return `.${path.sep}${path.relative(cwd, pathValue)}`; + } else if (pathValue.startsWith(this.home)) { + return `~${path.sep}${path.relative(this.home, pathValue)}`; + } else { + return pathValue; + } + } } diff --git a/src/client/common/platform/registry.ts b/src/client/common/platform/registry.ts index 05daee3f8c45..9032f1d42a1c 100644 --- a/src/client/common/platform/registry.ts +++ b/src/client/common/platform/registry.ts @@ -18,7 +18,7 @@ export class RegistryImplementation implements IRegistry { } } -export function getArchitectureDislayName(arch?: Architecture) { +export function getArchitectureDisplayName(arch?: Architecture) { switch (arch) { case Architecture.x64: return '64-bit'; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 9c4e3b9b71f0..c42889e9964e 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -101,8 +101,10 @@ export const IPathUtils = Symbol('IPathUtils'); export interface IPathUtils { readonly delimiter: string; + readonly home: string; getPathVariableName(): 'Path' | 'PATH'; basename(pathValue: string, ext?: string): string; + getDisplayName(pathValue: string, cwd?: string): string; } export const IRandom = Symbol('IRandom'); diff --git a/src/client/interpreter/configuration/interpreterComparer.ts b/src/client/interpreter/configuration/interpreterComparer.ts new file mode 100644 index 000000000000..f214a7664c19 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterComparer.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { getArchitectureDisplayName } from '../../common/platform/registry'; +import { IServiceContainer } from '../../ioc/types'; +import { IInterpreterHelper, PythonInterpreter } from '../contracts'; +import { IInterpreterComparer } from './types'; + +@injectable() +export class InterpreterComparer implements IInterpreterComparer { + private readonly interpreterHelper: IInterpreterHelper; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.interpreterHelper = this.serviceContainer.get(IInterpreterHelper); + } + public compare(a: PythonInterpreter, b: PythonInterpreter): number { + return this.getSortName(a) > this.getSortName(b) ? 1 : -1; + } + private getSortName(info: PythonInterpreter): string { + const sortNameParts: string[] = []; + const envSuffixParts: string[] = []; + + // Sort order for interpreters is: + // * Version + // * Architecture + // * Interpreter Type + // * Environment name + if (info.version_info && info.version_info.length > 0) { + sortNameParts.push(info.version_info.slice(0, 3).join('.')); + } + if (info.version_info) { + sortNameParts.push(getArchitectureDisplayName(info.architecture)); + } + if (info.companyDisplayName && info.companyDisplayName.length > 0) { + sortNameParts.push(info.companyDisplayName.trim()); + } else { + sortNameParts.push('Python'); + } + + if (info.type) { + const name = this.interpreterHelper.getInterpreterTypeDisplayName(info.type); + if (name) { + envSuffixParts.push(name); + } + } + if (info.envName && info.envName.length > 0) { + envSuffixParts.push(info.envName); + } + + const envSuffix = envSuffixParts.length === 0 ? '' : + `(${envSuffixParts.join(': ')})`; + return `${sortNameParts.join(' ')} ${envSuffix}`.trim(); + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 4a490b865a39..78a213d19849 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -1,12 +1,12 @@ import { inject, injectable } from 'inversify'; -import * as path from 'path'; import { ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri } from 'vscode'; import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; import * as settings from '../../common/configSettings'; import { Commands } from '../../common/constants'; +import { IPathUtils } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IInterpreterService, IShebangCodeLensProvider, PythonInterpreter, WorkspacePythonPath } from '../contracts'; -import { IInterpreterSelector, IPythonPathUpdaterServiceManager } from './types'; +import { IInterpreterComparer, IInterpreterSelector, IPythonPathUpdaterServiceManager } from './types'; export interface IInterpreterQuickPickItem extends QuickPickItem { path: string; @@ -19,12 +19,16 @@ export class InterpreterSelector implements IInterpreterSelector { private readonly workspaceService: IWorkspaceService; private readonly applicationShell: IApplicationShell; private readonly documentManager: IDocumentManager; + private readonly pathUtils: IPathUtils; + private readonly interpreterComparer: IInterpreterComparer; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.interpreterManager = serviceContainer.get(IInterpreterService); this.workspaceService = this.serviceContainer.get(IWorkspaceService); this.applicationShell = this.serviceContainer.get(IApplicationShell); this.documentManager = this.serviceContainer.get(IDocumentManager); + this.pathUtils = this.serviceContainer.get(IPathUtils); + this.interpreterComparer = this.serviceContainer.get(IInterpreterComparer); const commandManager = serviceContainer.get(ICommandManager); this.disposables.push(commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this))); @@ -36,11 +40,9 @@ export class InterpreterSelector implements IInterpreterSelector { public 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); + interpreters.sort(this.interpreterComparer.compare.bind(this.interpreterComparer)); return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); } - private async getWorkspaceToSetPythonPath(): Promise { if (!Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0) { return undefined; @@ -56,15 +58,11 @@ export class InterpreterSelector implements IInterpreterSelector { } private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { - let detail = suggestion.path; - if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { - detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; - } + const detail = this.pathUtils.getDisplayName(suggestion.path, workspaceUri ? workspaceUri.fsPath : undefined); const cachedPrefix = suggestion.cachedEntry ? '(cached) ' : ''; return { // tslint:disable-next-line:no-non-null-assertion label: suggestion.displayName!, - description: suggestion.companyDisplayName || '', detail: `${cachedPrefix}${detail}`, path: suggestion.path }; @@ -84,10 +82,7 @@ export class InterpreterSelector implements IInterpreterSelector { } const suggestions = await this.getSuggestions(wkspace); - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; - if (wkspace && currentPythonPath.startsWith(wkspace.fsPath)) { - currentPythonPath = `.${path.sep}${path.relative(wkspace.fsPath, currentPythonPath)}`; - } + const currentPythonPath = this.pathUtils.getDisplayName(settings.PythonSettings.getInstance().pythonPath, wkspace ? wkspace.fsPath : undefined); const quickPickOptions: QuickPickOptions = { matchOnDetail: true, matchOnDescription: true, diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 43ec286f77f0..6cf2fed4a1bd 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,4 +1,5 @@ import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { PythonInterpreter } from '../contracts'; export interface IPythonPathUpdaterService { updatePythonPath(pythonPath: string): Promise; @@ -20,3 +21,8 @@ export const IInterpreterSelector = Symbol('IInterpreterSelector'); export interface IInterpreterSelector extends Disposable { } + +export const IInterpreterComparer = Symbol('IInterpreterComparer'); +export interface IInterpreterComparer { + compare(a: PythonInterpreter, b: PythonInterpreter): number; +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index f4dd60ba52a9..df99184498e6 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -78,9 +78,11 @@ export interface IInterpreterService { getInterpreters(resource?: Uri): Promise; autoSetInterpreter(): Promise; getActiveInterpreter(resource?: Uri): Promise; - getInterpreterDetails(pythonPath: string): Promise>; + getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; refresh(): Promise; initialize(): void; + getDisplayName(interpreter: Partial): Promise; + shouldAutoSetInterpreter(): Promise; } export const IInterpreterDisplay = Symbol('IInterpreterDisplay'); @@ -98,6 +100,7 @@ export interface IInterpreterHelper { getActiveWorkspaceUri(): WorkspacePythonPath | undefined; getInterpreterInformation(pythonPath: string): Promise>; isMacDefaultPythonPath(pythonPath: string): Boolean; + getInterpreterTypeDisplayName(interpreterType: InterpreterType): string | undefined; } export const IPipEnvService = Symbol('IPipEnvService'); diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 14291ea76468..0ac73d8af537 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,10 +1,8 @@ import { inject, injectable } from 'inversify'; -import { EOL } from 'os'; -import * as path from 'path'; import { Disposable, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, IPathUtils } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, PythonInterpreter } from '../contracts'; import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; @@ -19,6 +17,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { private readonly configurationService: IConfigurationService; private readonly helper: IInterpreterHelper; private readonly workspaceService: IWorkspaceService; + private readonly pathUtils: IPathUtils; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.interpreterService = serviceContainer.get(IInterpreterService); @@ -27,6 +26,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { this.configurationService = serviceContainer.get(IConfigurationService); this.helper = serviceContainer.get(IInterpreterHelper); this.workspaceService = serviceContainer.get(IWorkspaceService); + this.pathUtils = serviceContainer.get(IPathUtils); const application = serviceContainer.get(IApplicationShell); const disposableRegistry = serviceContainer.get(IDisposableRegistry); @@ -52,34 +52,32 @@ export class InterpreterDisplay implements IInterpreterDisplay { const pythonPath = interpreter ? interpreter.path : this.configurationService.getSettings(workspaceFolder).pythonPath; this.statusBar.color = ''; - this.statusBar.tooltip = pythonPath; + this.statusBar.tooltip = this.pathUtils.getDisplayName(pythonPath, workspaceFolder ? workspaceFolder.fsPath : undefined); if (interpreter) { // tslint:disable-next-line:no-non-null-assertion this.statusBar.text = interpreter.displayName!; - if (interpreter.companyDisplayName) { - const toolTipSuffix = `${EOL}${interpreter.companyDisplayName}`; - this.statusBar.tooltip += toolTipSuffix; - } - } else { - await Promise.all([ - this.fileSystem.fileExists(pythonPath), - this.helper.getInterpreterInformation(pythonPath).catch>(() => undefined), - this.getVirtualEnvironmentName(pythonPath).catch(() => '') - ]) - .then(([interpreterExists, details, virtualEnvName]) => { - const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; - this.statusBar.text = `${details ? details.version : defaultDisplayName}${dislayNameSuffix}`; + this.statusBar.show(); + return; + } + + const [interpreterExists, details, virtualEnvName] = await Promise.all([ + this.fileSystem.fileExists(pythonPath), + this.helper.getInterpreterInformation(pythonPath).catch | undefined>(() => undefined), + this.getVirtualEnvironmentName(pythonPath, workspaceFolder).catch(() => '') + ]); + if (details) { + const displayName = await this.interpreterService.getDisplayName({ ...details, envName: virtualEnvName }); + this.statusBar.text = displayName; + } - if (!interpreterExists && !details && interpreters.length > 0) { - this.statusBar.color = 'yellow'; - this.statusBar.text = '$(alert) Select Python Environment'; - } - }); + if (!interpreterExists && !details && interpreters.length > 0) { + this.statusBar.tooltip = ''; + this.statusBar.color = 'yellow'; + this.statusBar.text = '$(alert) Select Python Environment'; } this.statusBar.show(); } - private async getVirtualEnvironmentName(pythonPath: string): Promise { - return this.virtualEnvMgr.getEnvironmentName(pythonPath); + private async getVirtualEnvironmentName(pythonPath: string, resource?: Uri): Promise { + return this.virtualEnvMgr.getEnvironmentName(pythonPath, resource); } } diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index ee0278b9ef59..7ef8ffdc873c 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -5,7 +5,7 @@ import { IFileSystem } from '../common/platform/types'; import { InterpreterInfomation, IPythonExecutionFactory } from '../common/process/types'; import { IPersistentStateFactory } from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { IInterpreterHelper, PythonInterpreter, WorkspacePythonPath } from './contracts'; +import { IInterpreterHelper, InterpreterType, PythonInterpreter, WorkspacePythonPath } from './contracts'; const EXPITY_DURATION = 24 * 60 * 60 * 1000; type CachedPythonInterpreter = Partial & { fileHash: string }; @@ -46,7 +46,7 @@ export class InterpreterHelper implements IInterpreterHelper { public async getInterpreterInformation(pythonPath: string): Promise> { let fileHash = await this.fs.getFileHash(pythonPath).catch(() => ''); fileHash = fileHash ? fileHash : ''; - const store = this.persistentFactory.createGlobalPersistentState(pythonPath, undefined, EXPITY_DURATION); + const store = this.persistentFactory.createGlobalPersistentState(`${pythonPath}.v1`, undefined, EXPITY_DURATION); if (store.value && (!fileHash || store.value.fileHash === fileHash)) { return store.value; } @@ -71,4 +71,26 @@ export class InterpreterHelper implements IInterpreterHelper { public isMacDefaultPythonPath(pythonPath: string) { return pythonPath === 'python' || pythonPath === '/usr/bin/python'; } + public getInterpreterTypeDisplayName(interpreterType: InterpreterType) { + switch (interpreterType) { + case InterpreterType.Conda: { + return 'conda'; + } + case InterpreterType.PipEnv: { + return 'pipenv'; + } + case InterpreterType.Pyenv: { + return 'pyenv'; + } + case InterpreterType.Venv: { + return 'venv'; + } + case InterpreterType.VirtualEnv: { + return 'virtualEnv'; + } + default: { + return ''; + } + } + } } diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 6eeb4766208a..525e0db387c8 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { ConfigurationTarget, Disposable, Event, EventEmitter, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PythonSettings } from '../common/configSettings'; +import { getArchitectureDisplayName } from '../common/platform/registry'; import { IFileSystem } from '../common/platform/types'; import { IPythonExecutionFactory } from '../common/process/types'; import { IConfigurationService, IDisposableRegistry } from '../common/types'; @@ -11,7 +12,7 @@ import { IPythonPathUpdaterServiceManager } from './configuration/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, - PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE + InterpreterType, PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; @@ -41,8 +42,10 @@ export class InterpreterService implements Disposable, IInterpreterService { (configService.getSettings() as PythonSettings).addListener('change', this.onConfigChanged); } - public getInterpreters(resource?: Uri): Promise { - return this.locator.getInterpreters(resource); + public async getInterpreters(resource?: Uri): Promise { + const interpreters = await this.locator.getInterpreters(resource); + await Promise.all(interpreters.map(async item => item.displayName = await this.getDisplayName(item, resource))); + return interpreters; } public async autoSetInterpreter(): Promise { @@ -65,7 +68,7 @@ export class InterpreterService implements Disposable, IInterpreterService { interpreters = await virtualEnvInterpreterProvider.getInterpreters(activeWorkspace.folderUri); const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); - const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(workspacePathUpper)); + const interpretersInWorkspace = interpreters.filter(interpreter => Uri.file(interpreter.path).fsPath.toUpperCase().startsWith(workspacePathUpper)); if (interpretersInWorkspace.length === 0) { return; } @@ -112,25 +115,70 @@ export class InterpreterService implements Disposable, IInterpreterService { } const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); - const [details, virtualEnvName, type] = await Promise.all([ + const [info, type] = await Promise.all([ interpreterHelper.getInterpreterInformation(pythonPath), - virtualEnvManager.getEnvironmentName(pythonPath), virtualEnvManager.getEnvironmentType(pythonPath) ]); - if (!details) { + if (!info) { return; } + const details: Partial = { + ...(info as PythonInterpreter), + path: pythonPath, + type: type + }; + + const virtualEnvName = await virtualEnvManager.getEnvironmentName(pythonPath, resource); const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; const displayName = `${details.version!}${dislayNameSuffix}`; return { ...(details as PythonInterpreter), - displayName, - path: pythonPath, envName: virtualEnvName, - type: type + displayName }; } - private async shouldAutoSetInterpreter(): Promise { + + /** + * Gets the display name of an interpreter. + * The format is `Python (: )` + * E.g. `Python 3.5.1 32-bit (myenv2: virtualenv)` + * @param {Partial} info + * @returns {string} + * @memberof InterpreterService + */ + public async getDisplayName(info: Partial, resource?: Uri): Promise { + const displayNameParts: string[] = ['Python']; + const envSuffixParts: string[] = []; + + if (info.version_info && info.version_info.length > 0) { + displayNameParts.push(info.version_info.slice(0, 3).join('.')); + } + if (info.architecture) { + displayNameParts.push(getArchitectureDisplayName(info.architecture)); + } + if (!info.envName && info.path && info.type && info.type === InterpreterType.PipEnv) { + // If we do not have the name of the environment, then try to get it again. + // This can happen based on the context (i.e. resource). + // I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource). + const virtualEnvMgr = this.serviceContainer.get(IVirtualEnvironmentManager); + info.envName = await virtualEnvMgr.getEnvironmentName(info.path, resource); + } + if (info.envName && info.envName.length > 0) { + envSuffixParts.push(info.envName); + } + if (info.type) { + const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); + const name = interpreterHelper.getInterpreterTypeDisplayName(info.type); + if (name) { + envSuffixParts.push(name); + } + } + + const envSuffix = envSuffixParts.length === 0 ? '' : + `(${envSuffixParts.join(': ')})`; + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); + } + public async shouldAutoSetInterpreter(): Promise { const activeWorkspace = this.helper.getActiveWorkspaceUri(); if (!activeWorkspace) { return false; diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts index 54e68d2efa01..9b69736a455d 100644 --- a/src/client/interpreter/locators/helpers.ts +++ b/src/client/interpreter/locators/helpers.ts @@ -1,7 +1,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { fsReaddirAsync } from '../../../utils/fs'; -import { getArchitectureDislayName } from '../../common/platform/registry'; +import { getArchitectureDisplayName } from '../../common/platform/registry'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { IS_WINDOWS } from '../../common/util'; import { IServiceContainer } from '../../ioc/types'; @@ -20,7 +20,7 @@ export function lookForInterpretersInDirectory(pathToCheck: string): Promise namePart.length > 0).join(' ').trim(); diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index 8c48d470d9bc..bb7cdd6b00bc 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -39,7 +39,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { .then(dirs => dirs.filter(dir => dir.length > 0)) .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) - .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))) + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter, resource)))) .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)) .catch((err) => { console.error('Python Extension (lookForInterpretersInVenvs):', err); @@ -65,11 +65,11 @@ export class BaseVirtualEnvService extends CacheableLocatorService { return ''; })); } - private async getVirtualEnvDetails(interpreter: string): Promise { + private async getVirtualEnvDetails(interpreter: string, resource?: Uri): Promise { return Promise.all([ this.helper.getInterpreterInformation(interpreter), - this.virtualEnvMgr.getEnvironmentName(interpreter), - this.virtualEnvMgr.getEnvironmentType(interpreter) + this.virtualEnvMgr.getEnvironmentName(interpreter, resource), + this.virtualEnvMgr.getEnvironmentType(interpreter, resource) ]) .then(([details, virtualEnvName, type]) => { if (!details) { diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index 930b40d6cc84..7c7ee00507d3 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -19,7 +19,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ constructor(@unmanaged() name: string, @unmanaged() protected readonly serviceContainer: IServiceContainer, @unmanaged() private cachePerWorkspace: boolean = false) { - this.cacheKeyPrefix = `INTERPRETERS_CACHE_${name}`; + this.cacheKeyPrefix = `INTERPRETERS_CACHE_v1_${name}`; } public abstract dispose(); public async getInterpreters(resource?: Uri): Promise { diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index 0ace3b61f48c..c7c862eb8736 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -43,7 +43,7 @@ export class CurrentPathService extends CacheableLocatorService { * This is used by CacheableLocatorService.getInterpreters(). */ protected getInterpretersImplementation(resource?: Uri): Promise { - return this.suggestionsFromKnownPaths(); + return this.suggestionsFromKnownPaths(resource); } /** @@ -70,7 +70,7 @@ export class CurrentPathService extends CacheableLocatorService { private async getInterpreterDetails(interpreter: string, resource?: Uri): Promise { return Promise.all([ this.helper.getInterpreterInformation(interpreter), - this.virtualEnvMgr.getEnvironmentName(interpreter), + this.virtualEnvMgr.getEnvironmentName(interpreter, resource), this.virtualEnvMgr.getEnvironmentType(interpreter, resource) ]). then(([details, virtualEnvName, type]) => { diff --git a/src/client/interpreter/locators/services/globalVirtualEnvService.ts b/src/client/interpreter/locators/services/globalVirtualEnvService.ts index 12a03d006fac..9812cc72bed1 100644 --- a/src/client/interpreter/locators/services/globalVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/globalVirtualEnvService.ts @@ -31,9 +31,9 @@ export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvi this.config = serviceContainer.get(IConfigurationService); } - public getSearchPaths(_resource?: Uri): string[] { + public getSearchPaths(resource?: Uri): string[] { const homedir = os.homedir(); - const venvFolders = this.config.getSettings(_resource).venvFolders; + const venvFolders = this.config.getSettings(resource).venvFolders; const folders = venvFolders.map(item => path.join(homedir, item)); // tslint:disable-next-line:no-string-literal diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index ddc70750f7a1..af6363e1ddca 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -3,10 +3,11 @@ import { IsWindows } from '../common/types'; import { IServiceManager } from '../ioc/types'; +import { InterpreterComparer } from './configuration/interpreterComparer'; import { InterpreterSelector } from './configuration/interpreterSelector'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; -import { IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './configuration/types'; +import { IInterpreterComparer, IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './configuration/types'; import { CONDA_ENV_FILE_SERVICE, CONDA_ENV_SERVICE, @@ -82,4 +83,5 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelper); + serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); } diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index 61f14d7f1c6f..44d99854aa7a 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -26,7 +26,16 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { this.pipEnvService = serviceContainer.get(IPipEnvService); this.workspaceService = serviceContainer.get(IWorkspaceService); } - public async getEnvironmentName(pythonPath: string): Promise { + public async getEnvironmentName(pythonPath: string, resource?: Uri): Promise { + const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const workspaceUri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; + const grandParentDirName = path.basename(path.dirname(path.dirname(pythonPath))); + if (workspaceUri && await this.pipEnvService.isRelatedPipEnvironment(workspaceUri.fsPath, pythonPath)) { + // In pipenv, return the folder name of the workspace. + return path.basename(workspaceUri.fsPath); + } + // https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv // hasattr(sys, 'real_prefix') works for virtualenv while // '(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))' works for venv @@ -34,9 +43,11 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { const processService = await this.processServiceFactory.create(); const code = 'import sys\nif hasattr(sys, "real_prefix"):\n print("virtualenv")\nelif hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:\n print("venv")'; const output = await processService.exec(pythonPath, ['-c', code]); - if (output.stdout.length > 0) { - return output.stdout.trim(); + const envName = output.stdout.trim(); + if (envName.length === 0) { + return ''; } + return envName.toUpperCase() === 'VIRTUALENV' ? grandParentDirName : envName; } catch { // do nothing. } @@ -59,7 +70,7 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; const workspaceUri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; - if (workspaceUri && this.pipEnvService.isRelatedPipEnvironment(pythonPath, workspaceUri.fsPath)) { + if (workspaceUri && await this.pipEnvService.isRelatedPipEnvironment(workspaceUri.fsPath, pythonPath)) { return InterpreterType.PipEnv; } diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts index 6096f88d374c..e13336760bf9 100644 --- a/src/client/interpreter/virtualEnvs/types.ts +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -6,6 +6,6 @@ import { Uri } from 'vscode'; import { InterpreterType } from '../contracts'; export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); export interface IVirtualEnvironmentManager { - getEnvironmentName(pythonPath: string): Promise; + getEnvironmentName(pythonPath: string, resource?: Uri): Promise; getEnvironmentType(pythonPath: string, resource?: Uri): Promise; } diff --git a/src/test/configuration/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector.unit.test.ts index 2cc72701dc09..49b33cfc9bed 100644 --- a/src/test/configuration/interpreterSelector.unit.test.ts +++ b/src/test/configuration/interpreterSelector.unit.test.ts @@ -2,17 +2,15 @@ // Licensed under the MIT License. import * as assert from 'assert'; -import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { IFileSystem } from '../../client/common/platform/types'; import { IPathUtils } from '../../client/common/types'; import { IInterpreterQuickPickItem, InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; +import { IInterpreterComparer } from '../../client/interpreter/configuration/types'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { IServiceContainer, IServiceManager } from '../../client/ioc/types'; +import { IServiceContainer } from '../../client/ioc/types'; import { Architecture } from '../../utils/platform'; const info: PythonInterpreter = { @@ -41,30 +39,21 @@ class InterpreterQuickPickItem implements IInterpreterQuickPickItem { // tslint:disable-next-line:max-func-body-length suite('Interpreters - selector', () => { - let serviceContainer: IServiceContainer; + let serviceContainer: TypeMoq.IMock; let workspace: TypeMoq.IMock; let appShell: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let documentManager: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; - let serviceManager: IServiceManager; setup(() => { - const cont = new Container(); - serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - workspace = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - + const commandManager = TypeMoq.Mock.ofType(); + const comparer = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - documentManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + workspace = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); fileSystem .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) @@ -73,15 +62,23 @@ suite('Interpreters - selector', () => { .setup(x => x.getRealPath(TypeMoq.It.isAnyString())) .returns((a: string) => new Promise(resolve => resolve(a))); - serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); + comparer.setup(c => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); - const commandManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ICommandManager, commandManager.object); + serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspace.object); + serviceContainer.setup(c => c.get(IApplicationShell)).returns(() => appShell.object); + serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); + serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object); + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(IInterpreterComparer)).returns(() => comparer.object); + serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object); }); [true, false].forEach(isWindows => { test(`Suggestions (${isWindows} ? 'Windows' : 'Non-Windows')`, async () => { - serviceManager.addSingletonInstance(IPathUtils, new PathUtils(isWindows)); + serviceContainer + .setup(c => c.get(IPathUtils)) + .returns(() => new PathUtils(isWindows)); + const initial: PythonInterpreter[] = [ { displayName: '1', path: 'c:/path1/path1', type: InterpreterType.Unknown }, { displayName: '2', path: 'c:/path1/path1', type: InterpreterType.Unknown }, @@ -94,7 +91,7 @@ suite('Interpreters - selector', () => { .setup(x => x.getInterpreters(TypeMoq.It.isAny())) .returns(() => new Promise((resolve) => resolve(initial))); - const selector = new InterpreterSelector(serviceContainer); + const selector = new InterpreterSelector(serviceContainer.object); const actual = await selector.getSuggestions(); const expected: InterpreterQuickPickItem[] = [ diff --git a/src/test/install/pythonInstallation.unit.test.ts b/src/test/install/pythonInstallation.unit.test.ts index 0bb3eda0d8d2..6ed960483c3d 100644 --- a/src/test/install/pythonInstallation.unit.test.ts +++ b/src/test/install/pythonInstallation.unit.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; -import { IApplicationShell } from '../../client/common/application/types'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { PythonInstaller } from '../../client/common/installer/pythonInstallation'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IPersistentStateFactory, IPythonSettings } from '../../client/common/types'; @@ -53,6 +53,7 @@ class TestContext { type: InterpreterType.Unknown, path: '' }; + const workspaceService = TypeMoq.Mock.ofType(); const interpreterService = TypeMoq.Mock.ofType(); interpreterService .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) @@ -65,10 +66,12 @@ class TestContext { this.serviceManager.addSingletonInstance(IApplicationShell, this.appShell.object); this.serviceManager.addSingletonInstance(IInterpreterLocatorService, this.locator.object); this.serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); + this.serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); this.pythonInstaller = new PythonInstaller(this.serviceContainer); this.platform.setup(x => x.isMac).returns(() => isMac); this.platform.setup(x => x.isWindows).returns(() => !isMac); + workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); } } @@ -100,7 +103,7 @@ suite('Installation', () => { openUrlCalled = true; url = s; }); - c.locator.setup(x => x.getInterpreters()).returns(() => Promise.resolve([])); + c.locator.setup(x => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const passed = await c.pythonInstaller.checkPythonInstallation(c.settings.object); assert.equal(passed, false, 'Python reported as present'); @@ -130,7 +133,7 @@ suite('Installation', () => { path: 'python', type: InterpreterType.Unknown }; - c.locator.setup(x => x.getInterpreters()).returns(() => Promise.resolve([interpreter])); + c.locator.setup(x => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([interpreter])); const passed = await c.pythonInstaller.checkPythonInstallation(c.settings.object); assert.equal(passed, true, 'Default MacOS Python not accepted'); diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.unit.test.ts similarity index 98% rename from src/test/interpreters/condaEnvFileService.test.ts rename to src/test/interpreters/condaEnvFileService.unit.test.ts index 062722354c90..47d6b21beb79 100644 --- a/src/test/interpreters/condaEnvFileService.test.ts +++ b/src/test/interpreters/condaEnvFileService.unit.test.ts @@ -8,7 +8,6 @@ import { ICondaService, IInterpreterHelper, IInterpreterLocatorService, Interpre import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; import { IServiceContainer } from '../../client/ioc/types'; -import { initialize, initializeTest } from '../initialize'; import { MockState } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); @@ -21,9 +20,7 @@ suite('Interpreters from Conda Environments Text File', () => { let interpreterHelper: TypeMoq.IMock; let condaFileProvider: IInterpreterLocatorService; let fileSystem: TypeMoq.IMock; - suiteSetup(initialize); - setup(async () => { - await initializeTest(); + setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); const stateFactory = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.unit.test.ts similarity index 99% rename from src/test/interpreters/condaEnvService.test.ts rename to src/test/interpreters/condaEnvService.unit.test.ts index ac9ec03efecd..314bcc39f0cf 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.unit.test.ts @@ -8,23 +8,20 @@ import { InterpreterHelper } from '../../client/interpreter/helpers'; import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvService, parseCondaInfo } from '../../client/interpreter/locators/services/condaEnvService'; import { IServiceContainer } from '../../client/ioc/types'; -import { initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockState } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); // tslint:disable-next-line:max-func-body-length -suite('Interpreters from Conda Environmentsx', () => { +suite('Interpreters from Conda Environments', () => { let ioc: UnitTestIocContainer; let logger: TypeMoq.IMock; let condaProvider: CondaEnvService; let condaService: TypeMoq.IMock; let interpreterHelper: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; - suiteSetup(initialize); - setup(async () => { - await initializeTest(); + setup(() => { initializeDI(); const serviceContainer = TypeMoq.Mock.ofType(); const stateFactory = TypeMoq.Mock.ofType(); diff --git a/src/test/interpreters/condaHelper.test.ts b/src/test/interpreters/condaHelper.unit.test.ts similarity index 95% rename from src/test/interpreters/condaHelper.test.ts rename to src/test/interpreters/condaHelper.unit.test.ts index 322a0fed9ffd..318ae6fa0685 100644 --- a/src/test/interpreters/condaHelper.test.ts +++ b/src/test/interpreters/condaHelper.unit.test.ts @@ -2,13 +2,10 @@ import * as assert from 'assert'; import { CondaInfo } from '../../client/interpreter/contracts'; import { AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaHelper } from '../../client/interpreter/locators/services/condaHelper'; -import { initialize, initializeTest } from '../initialize'; // tslint:disable-next-line:max-func-body-length suite('Interpreters display name from Conda Environments', () => { const condaHelper = new CondaHelper(); - suiteSetup(initialize); - setup(initializeTest); test('Must return default display name for invalid Conda Info', () => { assert.equal(condaHelper.getDisplayName(), AnacondaDisplayName, 'Incorrect display name'); assert.equal(condaHelper.getDisplayName({}), AnacondaDisplayName, 'Incorrect display name'); diff --git a/src/test/interpreters/condaService.test.ts b/src/test/interpreters/condaService.unit.test.ts similarity index 100% rename from src/test/interpreters/condaService.test.ts rename to src/test/interpreters/condaService.unit.test.ts diff --git a/src/test/interpreters/currentPathService.test.ts b/src/test/interpreters/currentPathService.unit.test.ts similarity index 98% rename from src/test/interpreters/currentPathService.test.ts rename to src/test/interpreters/currentPathService.unit.test.ts index 87949998b5e6..ce932c39cac4 100644 --- a/src/test/interpreters/currentPathService.test.ts +++ b/src/test/interpreters/currentPathService.unit.test.ts @@ -57,7 +57,7 @@ suite('Interpreters CurrentPath Service', () => { const version = 'mockVersion'; const envName = 'mockEnvName'; interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version })); - virtualEnvironmentManager.setup(v => v.getEnvironmentName(TypeMoq.It.isAny())).returns(() => Promise.resolve(envName)); + virtualEnvironmentManager.setup(v => v.getEnvironmentName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(envName)); virtualEnvironmentManager.setup(v => v.getEnvironmentType(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(InterpreterType.VirtualEnv)); const execArgs = ['-c', 'import sys;print(sys.executable)']; diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.unit.test.ts similarity index 58% rename from src/test/interpreters/display.test.ts rename to src/test/interpreters/display.unit.test.ts index 6499684079c0..9b71aac720b3 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -1,11 +1,10 @@ import { expect } from 'chai'; -import { EOL } from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Disposable, StatusBarAlignment, StatusBarItem, Uri, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../client/common/types'; +import { IConfigurationService, IDisposableRegistry, IPathUtils, IPythonSettings } from '../../client/common/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; @@ -39,6 +38,7 @@ suite('Interpreters Display', () => { let configurationService: TypeMoq.IMock; let interpreterDisplay: IInterpreterDisplay; let interpreterHelper: TypeMoq.IMock; + let pathUtils: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); @@ -51,6 +51,7 @@ suite('Interpreters Display', () => { statusBar = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); configurationService = TypeMoq.Mock.ofType(); + pathUtils = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => applicationShell.object); @@ -60,8 +61,10 @@ suite('Interpreters Display', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); applicationShell.setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(undefined))).returns(() => statusBar.object); + pathUtils.setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(p => p); interpreterDisplay = new InterpreterDisplay(serviceContainer.object); }); @@ -97,60 +100,25 @@ suite('Interpreters Display', () => { statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!, TypeMoq.Times.once()); }); - test('Display name and tooltip must include company display name from interpreter info', async () => { - const resource = Uri.file('x'); - const workspaceFolder = Uri.file('workspace'); - const activeInterpreter: PythonInterpreter = { - ...info, - displayName: 'Dummy_Display_Name', - type: InterpreterType.Unknown, - companyDisplayName: 'Company Name', - path: path.join('user', 'development', 'env', 'bin', 'python') - }; - setupWorkspaceFolder(resource, workspaceFolder); - interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(activeInterpreter)); - const expectedTooltip = `${activeInterpreter.path}${EOL}${activeInterpreter.companyDisplayName}`; - - await interpreterDisplay.refresh(resource); - - statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); - statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(expectedTooltip)!, TypeMoq.Times.once()); - }); - test('If interpreter is not identified then tooltip should point to python Path and text containing the folder name', async () => { + test('If interpreter is not identified then tooltip should point to python Path', async () => { const resource = Uri.file('x'); const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); const workspaceFolder = Uri.file('workspace'); - setupWorkspaceFolder(resource, workspaceFolder); - interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); - interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); - - await interpreterDisplay.refresh(resource); + const displayName = 'This is the display name'; - statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(pythonPath), TypeMoq.Times.once()); - statusBar.verify(s => s.text = TypeMoq.It.isValue(`${path.basename(pythonPath)} [Environment]`), TypeMoq.Times.once()); - }); - test('If virtual environment interpreter is not identified then text should contain the type of virtual environment', async () => { - const resource = Uri.file('x'); - const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); - const workspaceFolder = Uri.file('workspace'); setupWorkspaceFolder(resource, workspaceFolder); interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); + interpreterService.setup(i => i.getDisplayName(TypeMoq.It.isAny())).returns(() => Promise.resolve(displayName)); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - // tslint:disable-next-line:no-any - virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Name')); - interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({})); await interpreterDisplay.refresh(resource); statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(pythonPath), TypeMoq.Times.once()); - statusBar.verify(s => s.text = TypeMoq.It.isValue(`${path.basename(pythonPath)} [Environment] (Mock Name)`), TypeMoq.Times.once()); + statusBar.verify(s => s.text = TypeMoq.It.isValue(displayName), TypeMoq.Times.once()); }); test('If interpreter file does not exist then update status bar accordingly', async () => { const resource = Uri.file('x'); @@ -171,66 +139,37 @@ suite('Interpreters Display', () => { statusBar.verify(s => s.color = TypeMoq.It.isValue('yellow'), TypeMoq.Times.once()); statusBar.verify(s => s.text = TypeMoq.It.isValue('$(alert) Select Python Environment'), TypeMoq.Times.once()); }); - test('Suffix display name with the virtual env name', async () => { - const resource = Uri.file('x'); - const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); - const workspaceFolder = Uri.file('workspace'); - setupWorkspaceFolder(resource, workspaceFolder); - // tslint:disable-next-line:no-any - interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([{} as any])); - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); - const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); - // tslint:disable-next-line:no-any - virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Env Name')); - const expectedText = `${defaultDisplayName} (Mock Env Name)`; - - await interpreterDisplay.refresh(resource); - - statusBar.verify(s => s.text = TypeMoq.It.isValue(expectedText), TypeMoq.Times.once()); - }); - test('Use version of interpreter instead of a default interpreter name', async () => { - const resource = Uri.file('x'); - const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); - const workspaceFolder = Uri.file('workspace'); - setupWorkspaceFolder(resource, workspaceFolder); - // tslint:disable-next-line:no-any - interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([{} as any])); - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); - const displayName = 'Version from Interperter'; - interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({ version: displayName })); - // tslint:disable-next-line:no-any - virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); - - await interpreterDisplay.refresh(resource); - - statusBar.verify(s => s.text = TypeMoq.It.isValue(displayName), TypeMoq.Times.once()); - }); test('Ensure we try to identify the active workspace when a resource is not provided ', async () => { const workspaceFolder = Uri.file('x'); const resource = workspaceFolder; + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); const activeInterpreter: PythonInterpreter = { ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, companyDisplayName: 'Company Name', - path: path.join('user', 'development', 'env', 'bin', 'python') + path: pythonPath }; - interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(resource))).returns(() => Promise.resolve([])); - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))).returns(() => Promise.resolve(activeInterpreter)); - interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - const expectedTooltip = `${activeInterpreter.path}${EOL}${activeInterpreter.companyDisplayName}`; - interpreterHelper.setup(i => i.getActiveWorkspaceUri()).returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); + interpreterService + .setup(i => i.getInterpreters(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve([])) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(activeInterpreter)) + .verifiable(TypeMoq.Times.once()); + interpreterHelper + .setup(i => i.getActiveWorkspaceUri()) + .returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }) + .verifiable(TypeMoq.Times.once()); await interpreterDisplay.refresh(); + interpreterHelper.verifyAll(); + interpreterService.verifyAll(); statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); - statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(expectedTooltip)!, TypeMoq.Times.once()); + statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(pythonPath)!, TypeMoq.Times.once()); }); }); diff --git a/src/test/interpreters/helper.test.ts b/src/test/interpreters/helper.unit.test.ts similarity index 100% rename from src/test/interpreters/helper.test.ts rename to src/test/interpreters/helper.unit.test.ts diff --git a/src/test/interpreters/interpreterService.test.ts b/src/test/interpreters/interpreterService.test.ts deleted file mode 100644 index 7449d111ce9d..000000000000 --- a/src/test/interpreters/interpreterService.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { EventEmitter } from 'events'; -import { Container } from 'inversify'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../client/common/types'; -import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; -import { - IInterpreterDisplay, - IInterpreterHelper, - IInterpreterLocatorService, - INTERPRETER_LOCATOR_SERVICE, - InterpreterType, - PIPENV_SERVICE, - PythonInterpreter, - WORKSPACE_VIRTUAL_ENV_SERVICE, - WorkspacePythonPath -} from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { noop } from '../../utils/misc'; -import { Architecture } from '../../utils/platform'; - -const info: PythonInterpreter = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - type: InterpreterType.Unknown, - version: '', - version_info: [0, 0, 0, 'alpha'], - sysPrefix: '', - sysVersion: '' -}; - -// tslint:disable-next-line:max-func-body-length -suite('Interpreters service', () => { - let serviceManager: ServiceManager; - let serviceContainer: ServiceContainer; - let updater: TypeMoq.IMock; - let helper: TypeMoq.IMock; - let locator: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let config: TypeMoq.IMock; - let pipenvLocator: TypeMoq.IMock; - let wksLocator: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let interpreterDisplay: TypeMoq.IMock; - - setup(async () => { - const cont = new Container(); - serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - updater = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - locator = TypeMoq.Mock.ofType(); - workspace = TypeMoq.Mock.ofType(); - config = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - interpreterDisplay = TypeMoq.Mock.ofType(); - - workspace.setup(x => x.getConfiguration('python', TypeMoq.It.isAny())).returns(() => config.object); - serviceManager.addSingletonInstance(IDisposableRegistry, []); - serviceManager.addSingletonInstance(IInterpreterHelper, helper.object); - serviceManager.addSingletonInstance(IPythonPathUpdaterServiceManager, updater.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance(IInterpreterLocatorService, locator.object, INTERPRETER_LOCATOR_SERVICE); - serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); - serviceManager.addSingletonInstance(IInterpreterDisplay, interpreterDisplay.object); - - pipenvLocator = TypeMoq.Mock.ofType(); - wksLocator = TypeMoq.Mock.ofType(); - }); - - test('autoset interpreter - no workspace', async () => { - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - global pythonPath in config', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', globalValue: 'global' }; - }); - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - workspace has no pythonPath in config', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python' }; - }); - const interpreter: PythonInterpreter = { - ...info, - path: path.join(path.sep, 'folder', 'py1', 'bin', 'python.exe'), - type: InterpreterType.Unknown - }; - setupLocators([interpreter], []); - await verifyUpdateCalled(TypeMoq.Times.once()); - }); - - test('autoset interpreter - workspace has default pythonPath in config', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', workspaceValue: 'python' }; - }); - setupLocators([], []); - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - pipenv workspace', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', workspaceValue: 'python' }; - }); - const interpreter: PythonInterpreter = { - ...info, - path: 'python', - type: InterpreterType.VirtualEnv - }; - setupLocators([], [interpreter]); - await verifyUpdateCallData('python', ConfigurationTarget.Workspace, 'folder'); - }); - - test('autoset interpreter - workspace without interpreter', async () => { - setupWorkspace('root'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', workspaceValue: 'elsewhere' }; - }); - const interpreter: PythonInterpreter = { - ...info, - path: 'elsewhere', - type: InterpreterType.Unknown - }; - - setupLocators([interpreter], []); - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - workspace with interpreter', async () => { - setupWorkspace('root'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python' }; - }); - const intPath = path.join(path.sep, 'root', 'under', 'bin', 'python.exe'); - const interpreter: PythonInterpreter = { - ...info, - path: intPath, - type: InterpreterType.Unknown - }; - - setupLocators([interpreter], []); - await verifyUpdateCallData(intPath, ConfigurationTarget.Workspace, 'root'); - }); - - async function verifyUpdateCalled(times: TypeMoq.Times) { - const service = new InterpreterService(serviceContainer); - await service.autoSetInterpreter(); - updater - .verify(x => x.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), times); - } - - async function verifyUpdateCallData(pythonPath: string, target: ConfigurationTarget, wksFolder: string) { - let pp: string | undefined; - let confTarget: ConfigurationTarget | undefined; - let trigger; - let wks; - updater - .setup(x => x.updatePythonPath(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - // tslint:disable-next-line:no-any - .callback((p: string, c: ConfigurationTarget, t: any, w: any) => { - pp = p; - confTarget = c; - trigger = t; - wks = w; - }) - .returns(() => Promise.resolve()); - - const service = new InterpreterService(serviceContainer); - await service.autoSetInterpreter(); - - expect(pp).not.to.be.equal(undefined, 'updatePythonPath not called'); - expect(pp!).to.be.equal(pythonPath, 'invalid Python path'); - expect(confTarget).to.be.equal(target, 'invalid configuration target'); - expect(trigger).to.be.equal('load', 'invalid trigger'); - expect(wks.fsPath).to.be.equal(`${path.sep}${wksFolder}`, 'invalid workspace Uri'); - } - - function setupWorkspace(folder: string) { - const wsPath: WorkspacePythonPath = { - folderUri: Uri.file(folder), - configTarget: ConfigurationTarget.Workspace - }; - helper.setup(x => x.getActiveWorkspaceUri()).returns(() => wsPath); - } - - function setupLocators(wks: PythonInterpreter[], pipenv: PythonInterpreter[]) { - pipenvLocator.setup(x => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(pipenv)); - serviceManager.addSingletonInstance(IInterpreterLocatorService, pipenvLocator.object, PIPENV_SERVICE); - wksLocator.setup(x => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(wks)); - serviceManager.addSingletonInstance(IInterpreterLocatorService, wksLocator.object, WORKSPACE_VIRTUAL_ENV_SERVICE); - - } - - test('Changes to active document should invoke intrepreter.refresh method', async () => { - const service = new InterpreterService(serviceContainer); - const configService = TypeMoq.Mock.ofType(); - const documentManager = TypeMoq.Mock.ofType(); - - let activeTextEditorChangeHandler: Function | undefined; - documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { - activeTextEditorChangeHandler = handler; - return { dispose: noop }; - }); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - - // tslint:disable-next-line:no-any - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => new EventEmitter() as any); - service.initialize(); - const textEditor = TypeMoq.Mock.ofType(); - const uri = Uri.file(path.join('usr', 'file.py')); - const document = TypeMoq.Mock.ofType(); - textEditor.setup(t => t.document).returns(() => document.object); - document.setup(d => d.uri).returns(() => uri); - activeTextEditorChangeHandler!(textEditor.object); - - interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); - }); - - test('If there is no active document then intrepreter.refresh should not be invoked', async () => { - const service = new InterpreterService(serviceContainer); - const configService = TypeMoq.Mock.ofType(); - const documentManager = TypeMoq.Mock.ofType(); - - let activeTextEditorChangeHandler: Function | undefined; - documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { - activeTextEditorChangeHandler = handler; - return { dispose: noop }; - }); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - - // tslint:disable-next-line:no-any - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => new EventEmitter() as any); - service.initialize(); - activeTextEditorChangeHandler!(); - - interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); - }); - [undefined, Uri.file('some workspace')] - .forEach(resource => { - test(`Ensure undefined is returned if we're unable to retrieve interpreter info (Resource is ${resource})`, async () => { - const pythonPath = 'SOME VALUE'; - const service = new InterpreterService(serviceContainer); - locator - .setup(l => l.getInterpreters(TypeMoq.It.isValue(resource))) - .returns(() => Promise.resolve([])) - .verifiable(TypeMoq.Times.once()); - helper - .setup(h => h.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - const virtualEnvMgr = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IVirtualEnvironmentManager, virtualEnvMgr.object); - virtualEnvMgr - .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve('')) - .verifiable(TypeMoq.Times.once()); - virtualEnvMgr - .setup(v => v.getEnvironmentType(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(InterpreterType.Unknown)) - .verifiable(TypeMoq.Times.once()); - - const details = await service.getInterpreterDetails(pythonPath, resource); - - locator.verifyAll(); - helper.verifyAll(); - expect(details).to.be.equal(undefined, 'Not undefined'); - }); - }); -}); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts new file mode 100644 index 000000000000..dde4f45bbea1 --- /dev/null +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -0,0 +1,580 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { EventEmitter } from 'events'; +import { Container } from 'inversify'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { getArchitectureDisplayName } from '../../client/common/platform/registry'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IConfigurationService, IDisposableRegistry } from '../../client/common/types'; +import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE, + InterpreterType, + PIPENV_SERVICE, + PythonInterpreter, + WORKSPACE_VIRTUAL_ENV_SERVICE, + WorkspacePythonPath +} from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import * as EnumEx from '../../utils/enum'; +import { noop } from '../../utils/misc'; +import { Architecture } from '../../utils/platform'; + +use(chaiAsPromised); + +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + +suite('Interpreters service', () => { + let serviceManager: ServiceManager; + let serviceContainer: ServiceContainer; + let updater: TypeMoq.IMock; + let helper: TypeMoq.IMock; + let locator: TypeMoq.IMock; + let workspace: TypeMoq.IMock; + let config: TypeMoq.IMock; + let pipenvLocator: TypeMoq.IMock; + let wksLocator: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let interpreterDisplay: TypeMoq.IMock; + let workspacePythonPath: TypeMoq.IMock; + let virtualEnvMgr: TypeMoq.IMock; + type ConfigValue = { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T; workspaceFolderValue?: T }; + + function setupSuite() { + const cont = new Container(); + serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + updater = TypeMoq.Mock.ofType(); + helper = TypeMoq.Mock.ofType(); + locator = TypeMoq.Mock.ofType(); + workspace = TypeMoq.Mock.ofType(); + config = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + interpreterDisplay = TypeMoq.Mock.ofType(); + workspacePythonPath = TypeMoq.Mock.ofType(); + virtualEnvMgr = TypeMoq.Mock.ofType(); + + workspace.setup(x => x.getConfiguration('python', TypeMoq.It.isAny())).returns(() => config.object); + serviceManager.addSingletonInstance(IDisposableRegistry, []); + serviceManager.addSingletonInstance(IInterpreterHelper, helper.object); + serviceManager.addSingletonInstance(IPythonPathUpdaterServiceManager, updater.object); + serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); + serviceManager.addSingletonInstance(IInterpreterLocatorService, locator.object, INTERPRETER_LOCATOR_SERVICE); + serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance(IInterpreterDisplay, interpreterDisplay.object); + serviceManager.addSingletonInstance(IVirtualEnvironmentManager, virtualEnvMgr.object); + + pipenvLocator = TypeMoq.Mock.ofType(); + wksLocator = TypeMoq.Mock.ofType(); + + } + suite('Misc', () => { + setup(setupSuite); + [undefined, Uri.file('xyz')] + .forEach(resource => { + const resourceTestSuffix = `(${resource ? 'with' : 'without'} a resource)`; + + test(`Refresh invokes refresh of display ${resourceTestSuffix}`, async () => { + interpreterDisplay + .setup(i => i.refresh(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer); + await service.refresh(resource); + + interpreterDisplay.verifyAll(); + }); + + test(`get Interpreters uses interpreter locactors to get interpreters ${resourceTestSuffix}`, async () => { + locator + .setup(l => l.getInterpreters(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve([])) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer); + await service.getInterpreters(resource); + + locator.verifyAll(); + }); + }); + + test('Changes to active document should invoke intrepreter.refresh method', async () => { + const service = new InterpreterService(serviceContainer); + const configService = TypeMoq.Mock.ofType(); + const documentManager = TypeMoq.Mock.ofType(); + + let activeTextEditorChangeHandler: Function | undefined; + documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IConfigurationService, configService.object); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => new EventEmitter() as any); + service.initialize(); + const textEditor = TypeMoq.Mock.ofType(); + const uri = Uri.file(path.join('usr', 'file.py')); + const document = TypeMoq.Mock.ofType(); + textEditor.setup(t => t.document).returns(() => document.object); + document.setup(d => d.uri).returns(() => uri); + activeTextEditorChangeHandler!(textEditor.object); + + interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); + }); + + test('If there is no active document then intrepreter.refresh should not be invoked', async () => { + const service = new InterpreterService(serviceContainer); + const configService = TypeMoq.Mock.ofType(); + const documentManager = TypeMoq.Mock.ofType(); + + let activeTextEditorChangeHandler: Function | undefined; + documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IConfigurationService, configService.object); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => new EventEmitter() as any); + service.initialize(); + activeTextEditorChangeHandler!(); + + interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); + }); + + }); + + suite('Get Interpreter Details', () => { + setup(setupSuite); + [undefined, Uri.file('some workspace')] + .forEach(resource => { + test(`Ensure undefined is returned if we're unable to retrieve interpreter info (Resource is ${resource})`, async () => { + const pythonPath = 'SOME VALUE'; + const service = new InterpreterService(serviceContainer); + locator + .setup(l => l.getInterpreters(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve([])) + .verifiable(TypeMoq.Times.once()); + helper + .setup(h => h.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + virtualEnvMgr + .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve('')) + .verifiable(TypeMoq.Times.once()); + virtualEnvMgr + .setup(v => v.getEnvironmentType(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(InterpreterType.Unknown)) + .verifiable(TypeMoq.Times.once()); + + const details = await service.getInterpreterDetails(pythonPath, resource); + + locator.verifyAll(); + helper.verifyAll(); + expect(details).to.be.equal(undefined, 'Not undefined'); + }); + }); + }); + + suite('Should Auto Set Interpreter', () => { + setup(setupSuite); + test('Should not auto set interpreter if there is no workspace', async () => { + const service = new InterpreterService(serviceContainer); + helper + .setup(h => h.getActiveWorkspaceUri()) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(false, 'not false'); + + helper.verifyAll(); + }); + + test('Should not auto set interpreter if there is a value in global user settings (global value is not \'python\')', async () => { + const service = new InterpreterService(serviceContainer); + workspacePythonPath + .setup(w => w.folderUri) + .returns(() => Uri.file('w')) + .verifiable(TypeMoq.Times.once()); + helper + .setup(h => h.getActiveWorkspaceUri()) + .returns(() => workspacePythonPath.object) + .verifiable(TypeMoq.Times.once()); + const pythonPathConfigValue = TypeMoq.Mock.ofType>(); + config + .setup(w => w.inspect(TypeMoq.It.isAny())) + .returns(() => pythonPathConfigValue.object) + .verifiable(TypeMoq.Times.once()); + pythonPathConfigValue + .setup(p => p.globalValue) + .returns(() => path.join('a', 'bin', 'python')) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(false, 'not false'); + + helper.verifyAll(); + workspace.verifyAll(); + config.verifyAll(); + pythonPathConfigValue.verifyAll(); + }); + test('Should not auto set interpreter if there is a value in workspace settings (& value is not \'python\')', async () => { + const service = new InterpreterService(serviceContainer); + workspacePythonPath + .setup(w => w.configTarget) + .returns(() => ConfigurationTarget.Workspace) + .verifiable(TypeMoq.Times.once()); + helper + .setup(h => h.getActiveWorkspaceUri()) + .returns(() => workspacePythonPath.object) + .verifiable(TypeMoq.Times.once()); + const pythonPathConfigValue = TypeMoq.Mock.ofType>(); + config + .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => pythonPathConfigValue.object) + .verifiable(TypeMoq.Times.once()); + pythonPathConfigValue + .setup(p => p.globalValue) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonPathConfigValue + .setup(p => p.workspaceValue) + .returns(() => path.join('a', 'bin', 'python')) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(false, 'not false'); + + helper.verifyAll(); + workspace.verifyAll(); + config.verifyAll(); + pythonPathConfigValue.verifyAll(); + }); + + [ + { configTarget: ConfigurationTarget.Workspace, label: 'Workspace' }, + { configTarget: ConfigurationTarget.WorkspaceFolder, label: 'Workspace Folder' } + ].forEach(item => { + const testSuffix = `(${item.label})`; + const cfgTarget = item.configTarget as (ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder); + test(`Should auto set interpreter if there is no value in workspace settings ${testSuffix}`, async () => { + const service = new InterpreterService(serviceContainer); + workspacePythonPath + .setup(w => w.configTarget) + .returns(() => cfgTarget) + .verifiable(TypeMoq.Times.once()); + helper + .setup(h => h.getActiveWorkspaceUri()) + .returns(() => workspacePythonPath.object) + .verifiable(TypeMoq.Times.once()); + const pythonPathConfigValue = TypeMoq.Mock.ofType>(); + config + .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => pythonPathConfigValue.object) + .verifiable(TypeMoq.Times.once()); + pythonPathConfigValue + .setup(p => p.globalValue) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeastOnce()); + if (cfgTarget === ConfigurationTarget.Workspace) { + pythonPathConfigValue + .setup(p => p.workspaceValue) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else { + pythonPathConfigValue + .setup(p => p.workspaceFolderValue) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeastOnce()); + } + + await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(true, 'not true'); + + helper.verifyAll(); + workspace.verifyAll(); + config.verifyAll(); + pythonPathConfigValue.verifyAll(); + }); + + test(`Should auto set interpreter if there is no value in workspace settings and value is \'python\' ${testSuffix}`, async () => { + const service = new InterpreterService(serviceContainer); + workspacePythonPath + .setup(w => w.configTarget) + .returns(() => ConfigurationTarget.Workspace) + .verifiable(TypeMoq.Times.once()); + helper + .setup(h => h.getActiveWorkspaceUri()) + .returns(() => workspacePythonPath.object) + .verifiable(TypeMoq.Times.once()); + const pythonPathConfigValue = TypeMoq.Mock.ofType>(); + config + .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => pythonPathConfigValue.object) + .verifiable(TypeMoq.Times.once()); + pythonPathConfigValue + .setup(p => p.globalValue) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonPathConfigValue + .setup(p => p.workspaceValue) + .returns(() => 'python') + .verifiable(TypeMoq.Times.atLeastOnce()); + + await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(true, 'not true'); + + helper.verifyAll(); + workspace.verifyAll(); + config.verifyAll(); + pythonPathConfigValue.verifyAll(); + }); + }); + }); + + suite('Auto Set Interpreter', () => { + setup(setupSuite); + test('autoset interpreter - no workspace', async () => { + await verifyUpdateCalled(TypeMoq.Times.never()); + }); + + test('autoset interpreter - global pythonPath in config', async () => { + setupWorkspace('folder'); + config.setup(x => x.inspect('pythonPath')).returns(() => { + return { key: 'python', globalValue: 'global' }; + }); + await verifyUpdateCalled(TypeMoq.Times.never()); + }); + + test('autoset interpreter - workspace has no pythonPath in config', async () => { + setupWorkspace('folder'); + config.setup(x => x.inspect('pythonPath')).returns(() => { + return { key: 'python' }; + }); + const interpreter: PythonInterpreter = { + ...info, + path: path.join('folder', 'py1', 'bin', 'python.exe'), + type: InterpreterType.Unknown + }; + setupLocators([interpreter], []); + await verifyUpdateCalled(TypeMoq.Times.once()); + }); + + test('autoset interpreter - workspace has default pythonPath in config', async () => { + setupWorkspace('folder'); + config.setup(x => x.inspect('pythonPath')).returns(() => { + return { key: 'python', workspaceValue: 'python' }; + }); + setupLocators([], []); + await verifyUpdateCalled(TypeMoq.Times.never()); + }); + + test('autoset interpreter - pipenv workspace', async () => { + setupWorkspace('folder'); + config.setup(x => x.inspect('pythonPath')).returns(() => { + return { key: 'python', workspaceValue: 'python' }; + }); + const interpreter: PythonInterpreter = { + ...info, + path: 'python', + type: InterpreterType.VirtualEnv + }; + setupLocators([], [interpreter]); + await verifyUpdateCallData('python', ConfigurationTarget.Workspace, 'folder'); + }); + + test('autoset interpreter - workspace without interpreter', async () => { + setupWorkspace('root'); + config.setup(x => x.inspect('pythonPath')).returns(() => { + return { key: 'python', workspaceValue: 'elsewhere' }; + }); + const interpreter: PythonInterpreter = { + ...info, + path: 'elsewhere', + type: InterpreterType.Unknown + }; + + setupLocators([interpreter], []); + await verifyUpdateCalled(TypeMoq.Times.never()); + }); + + test('autoset interpreter - workspace with interpreter', async () => { + setupWorkspace('root'); + config.setup(x => x.inspect('pythonPath')).returns(() => { + return { key: 'python' }; + }); + const intPath = path.join('root', 'under', 'bin', 'python.exe'); + const interpreter: PythonInterpreter = { + ...info, + path: intPath, + type: InterpreterType.Unknown + }; + + setupLocators([interpreter], []); + await verifyUpdateCallData(intPath, ConfigurationTarget.Workspace, 'root'); + }); + + async function verifyUpdateCalled(times: TypeMoq.Times) { + const service = new InterpreterService(serviceContainer); + await service.autoSetInterpreter(); + updater + .verify(x => x.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), times); + } + + async function verifyUpdateCallData(pythonPath: string, target: ConfigurationTarget, wksFolder: string) { + let pp: string | undefined; + let confTarget: ConfigurationTarget | undefined; + let trigger; + let wks; + updater + .setup(x => x.updatePythonPath(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + // tslint:disable-next-line:no-any + .callback((p: string, c: ConfigurationTarget, t: any, w: any) => { + pp = p; + confTarget = c; + trigger = t; + wks = w; + }) + .returns(() => Promise.resolve()); + + const service = new InterpreterService(serviceContainer); + await service.autoSetInterpreter(); + + expect(pp).not.to.be.equal(undefined, 'updatePythonPath not called'); + expect(pp!).to.be.equal(pythonPath, 'invalid Python path'); + expect(confTarget).to.be.equal(target, 'invalid configuration target'); + expect(trigger).to.be.equal('load', 'invalid trigger'); + expect(wks.fsPath).to.be.equal(Uri.file(wksFolder).fsPath, 'invalid workspace Uri'); + } + + function setupWorkspace(folder: string) { + const wsPath: WorkspacePythonPath = { + folderUri: Uri.file(folder), + configTarget: ConfigurationTarget.Workspace + }; + helper.setup(x => x.getActiveWorkspaceUri()).returns(() => wsPath); + } + + function setupLocators(wks: PythonInterpreter[], pipenv: PythonInterpreter[]) { + pipenvLocator.setup(x => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(pipenv)); + serviceManager.addSingletonInstance(IInterpreterLocatorService, pipenvLocator.object, PIPENV_SERVICE); + wksLocator.setup(x => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(wks)); + serviceManager.addSingletonInstance(IInterpreterLocatorService, wksLocator.object, WORKSPACE_VIRTUAL_ENV_SERVICE); + + } + }); + + // This is kind of a verbose test, but we need to ensure we have covered all permutations. + // Also we have special handling for certain types of interpreters. + suite('Display Format (with all permutations)', () => { + setup(setupSuite); + [undefined, Uri.file('xyz')].forEach(resource => { + [undefined, [1, 2, 3, 'alpha']].forEach(versionInfo => { + // Forced cast to ignore TS warnings. + (EnumEx.getNamesAndValues(Architecture) as ({ name: string; value: Architecture } | undefined)[]).concat(undefined).forEach(arch => { + [undefined, path.join('a', 'b', 'c', 'd', 'bin', 'python')].forEach(pythonPath => { + // Forced cast to ignore TS warnings. + (EnumEx.getNamesAndValues(InterpreterType) as ({ name: string; value: InterpreterType } | undefined)[]).concat(undefined).forEach(interpreterType => { + [undefined, 'my env name'].forEach(envName => { + ['', 'my pipenv name'].forEach(pipEnvName => { + const testName = [`${resource ? 'With' : 'Without'} a workspace`, + `${versionInfo ? 'with' : 'without'} version information`, + `${arch ? arch.name : 'without'} architecture`, + `${pythonPath ? 'with' : 'without'} python Path`, + `${interpreterType ? `${interpreterType.name} interpreter type` : 'without interpreter type'}`, + `${envName ? 'with' : 'without'} environment name`, + `${pipEnvName ? 'with' : 'without'} pip environment` + ].join(', '); + + test(testName, async () => { + const interpreterInfo: Partial = { + version_info: versionInfo as any, + architecture: arch ? arch.value : undefined, + envName, + type: interpreterType ? interpreterType.value : undefined, + path: pythonPath + }; + + if (interpreterInfo.path && interpreterType && interpreterType.value === InterpreterType.PipEnv) { + virtualEnvMgr + .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(interpreterInfo.path!), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pipEnvName)); + } + if (interpreterType) { + helper + .setup(h => h.getInterpreterTypeDisplayName(TypeMoq.It.isValue(interpreterType.value))) + .returns(() => `${interpreterType!.name}_display`); + } + + const service = new InterpreterService(serviceContainer); + const expectedDisplayName = buildDisplayName(interpreterInfo); + + const displayName = await service.getDisplayName(interpreterInfo, resource); + expect(displayName).to.equal(expectedDisplayName); + }); + + function buildDisplayName(interpreterInfo: Partial) { + const displayNameParts: string[] = ['Python']; + const envSuffixParts: string[] = []; + + if (interpreterInfo.version_info && interpreterInfo.version_info.length > 0) { + displayNameParts.push(interpreterInfo.version_info.slice(0, 3).join('.')); + } + if (interpreterInfo.architecture) { + displayNameParts.push(getArchitectureDisplayName(interpreterInfo.architecture)); + } + if (!interpreterInfo.envName && interpreterInfo.path && interpreterInfo.type && interpreterInfo.type === InterpreterType.PipEnv && pipEnvName) { + // If we do not have the name of the environment, then try to get it again. + // This can happen based on the context (i.e. resource). + // I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource). + interpreterInfo.envName = pipEnvName; + } + if (interpreterInfo.envName && interpreterInfo.envName.length > 0) { + envSuffixParts.push(interpreterInfo.envName); + } + if (interpreterInfo.type) { + envSuffixParts.push(`${interpreterType!.name}_display`); + } + + const envSuffix = envSuffixParts.length === 0 ? '' : + `(${envSuffixParts.join(': ')})`; + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); + } + }); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/src/test/interpreters/interpreterVersion.test.ts b/src/test/interpreters/interpreterVersion.unit.test.ts similarity index 95% rename from src/test/interpreters/interpreterVersion.test.ts rename to src/test/interpreters/interpreterVersion.unit.test.ts index 63b0dc45b3b9..31710ca4726a 100644 --- a/src/test/interpreters/interpreterVersion.test.ts +++ b/src/test/interpreters/interpreterVersion.unit.test.ts @@ -8,18 +8,13 @@ import { IProcessServiceFactory } from '../../client/common/process/types'; import { IInterpreterVersionService } from '../../client/interpreter/contracts'; import { PIP_VERSION_REGEX } from '../../client/interpreter/interpreterVersion'; import { PYTHON_PATH } from '../common'; -import { initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; use(chaiAsPromised); suite('Interpreters display version', () => { let ioc: UnitTestIocContainer; - suiteSetup(initialize); - setup(async () => { - initializeDI(); - await initializeTest(); - }); + setup(initializeDI); teardown(() => ioc.dispose()); function initializeDI() { diff --git a/src/test/interpreters/venv.test.ts b/src/test/interpreters/venv.unit.test.ts similarity index 94% rename from src/test/interpreters/venv.test.ts rename to src/test/interpreters/venv.unit.test.ts index 13f064bcbc4a..cd158b9fe79e 100644 --- a/src/test/interpreters/venv.test.ts +++ b/src/test/interpreters/venv.unit.test.ts @@ -23,7 +23,7 @@ suite('Virtual environments', () => { let workspace: TypeMoq.IMock; let process: TypeMoq.IMock; - setup(async () => { + setup(() => { const cont = new Container(); serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); @@ -68,7 +68,7 @@ suite('Virtual environments', () => { }); test('Workspace search paths', async () => { - settings.setup(x => x.venvPath).returns(() => `~${path.sep}foo`); + settings.setup(x => x.venvPath).returns(() => path.join('~', 'foo')); const wsRoot = TypeMoq.Mock.ofType(); wsRoot.setup(x => x.uri).returns(() => Uri.file('root')); @@ -83,7 +83,7 @@ suite('Virtual environments', () => { const paths = pathProvider.getSearchPaths(Uri.file('')); const homedir = os.homedir(); - const expected = [path.join(homedir, 'foo'), `${path.sep}root`, `${path.sep}root${path.sep}.direnv`]; + const expected = [path.join(homedir, 'foo'), 'root', path.join('root', '.direnv')].map(item => Uri.file(item).fsPath); expect(paths).to.deep.equal(expected, 'Workspace venv folder search list does not match.'); }); }); diff --git a/src/test/interpreters/virtualEnvManager.test.ts b/src/test/interpreters/virtualEnvManager.unit.test.ts similarity index 63% rename from src/test/interpreters/virtualEnvManager.test.ts rename to src/test/interpreters/virtualEnvManager.unit.test.ts index de16e18c03bd..4b3d38e94fa8 100644 --- a/src/test/interpreters/virtualEnvManager.test.ts +++ b/src/test/interpreters/virtualEnvManager.unit.test.ts @@ -5,7 +5,9 @@ import { expect } from 'chai'; import { Container } from 'inversify'; +import * as path from 'path'; import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; import { IWorkspaceService } from '../../client/common/application/types'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { PlatformService } from '../../client/common/platform/platformService'; @@ -17,21 +19,23 @@ import { IPipEnvService } from '../../client/interpreter/contracts'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; -import { PYTHON_PATH } from '../common'; suite('Virtual environment manager', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; - + const virtualEnvFolderName = 'virtual Env Folder Name'; + const pythonPath = path.join('a', 'b', virtualEnvFolderName, 'd', 'python'); setup(async () => { const cont = new Container(); serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); }); - test('Plain Python environment suffix', async () => testSuffix('')); - test('Venv environment suffix', async () => testSuffix('venv')); - test('Virtualenv Python environment suffix', async () => testSuffix('virtualenv')); + test('Plain Python environment suffix', async () => testSuffix('', '')); + test('Plain Python environment suffix with workspace Uri', async () => testSuffix('', '', false, Uri.file(path.join('1', '2', '3', '4')))); + test('Plain Python environment suffix with PipEnv', async () => testSuffix('', 'workspaceName', true, Uri.file(path.join('1', '2', '3', 'workspaceName')))); + test('Venv environment suffix', async () => testSuffix('venv', 'venv')); + test('Virtualenv Python environment suffix', async () => testSuffix('virtualenv', virtualEnvFolderName)); test('Run actual virtual env detection code', async () => { const processServiceFactory = TypeMoq.Mock.ofType(); @@ -41,32 +45,43 @@ suite('Virtual environment manager', () => { serviceManager.addSingleton(IFileSystem, FileSystem); serviceManager.addSingleton(IPlatformService, PlatformService); serviceManager.addSingletonInstance(IPipEnvService, TypeMoq.Mock.ofType().object); - serviceManager.addSingletonInstance(IWorkspaceService, TypeMoq.Mock.ofType().object); + const workspaceService = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); + serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); const venvManager = new VirtualEnvironmentManager(serviceContainer); - const name = await venvManager.getEnvironmentName(PYTHON_PATH); + const name = await venvManager.getEnvironmentName(pythonPath); const result = name === '' || name === 'venv' || name === 'virtualenv'; expect(result).to.be.equal(true, 'Running venv detection code failed.'); }); - async function testSuffix(expectedName: string) { + async function testSuffix(virtualEnvProcOutput: string, expectedEnvName: string, isPipEnvironment: boolean = false, resource?: Uri) { const processService = TypeMoq.Mock.ofType(); const processServiceFactory = TypeMoq.Mock.ofType(); processService.setup((x: any) => x.then).returns(() => undefined); processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); serviceManager.addSingletonInstance(IFileSystem, TypeMoq.Mock.ofType().object); - serviceManager.addSingletonInstance(IPipEnvService, TypeMoq.Mock.ofType().object); - serviceManager.addSingletonInstance(IWorkspaceService, TypeMoq.Mock.ofType().object); + const pipEnvService = TypeMoq.Mock.ofType(); + pipEnvService.setup(w => w.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(isPipEnvironment)); + serviceManager.addSingletonInstance(IPipEnvService, pipEnvService.object); + const workspaceService = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); + if (resource) { + const workspaceFolder = TypeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri).returns(() => resource); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + } + serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); const venvManager = new VirtualEnvironmentManager(serviceContainer); processService - .setup(x => x.exec(PYTHON_PATH, TypeMoq.It.isAny())) + .setup(x => x.exec(pythonPath, TypeMoq.It.isAny())) .returns(() => Promise.resolve({ - stdout: expectedName, + stdout: virtualEnvProcOutput, stderr: '' })); - const name = await venvManager.getEnvironmentName(PYTHON_PATH); - expect(name).to.be.equal(expectedName, 'Virtual envrironment name suffix is incorrect.'); + const name = await venvManager.getEnvironmentName(pythonPath, resource); + expect(name).to.be.equal(expectedEnvName, 'Virtual envrironment name suffix is incorrect.'); } });