diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 21c1bcbf94f4..e040b35a7e14 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -5,7 +5,7 @@ // tslint:disable:no-any unified-signatures import * as vscode from 'vscode'; -import { CancellationToken, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions } from 'vscode'; +import { CancellationToken, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions, WorkspaceConfiguration } from 'vscode'; import { TextEditor, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent } from 'vscode'; import { Uri, ViewColumn, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { Terminal, TerminalOptions } from 'vscode'; @@ -363,6 +363,11 @@ export interface IWorkspaceService { */ readonly onDidChangeWorkspaceFolders: Event; + /** + * An event that is emitted when the [configuration](#WorkspaceConfiguration) changed. + */ + readonly onDidChangeConfiguration: Event; + /** * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. * * returns `undefined` when the given uri doesn't match any workspace folder @@ -419,6 +424,21 @@ export interface IWorkspaceService { * [workspace folders](#workspace.workspaceFolders) are opened. */ findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable; + + /** + * Get a workspace configuration object. + * + * When a section-identifier is provided only that part of the configuration + * is returned. Dots in the section-identifier are interpreted as child-access, + * like `{ myExt: { setting: { doIt: true }}}` and `getConfiguration('myExt.setting').get('doIt') === true`. + * + * When a resource is provided, configuration scoped to that resource is returned. + * + * @param section A dot-separated identifier. + * @param resource A resource for which the configuration is asked for + * @return The full configuration or a subset. + */ + getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration; } export const ITerminalManager = Symbol('ITerminalManager'); diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index fca0f32a1420..c2a3772931cd 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -2,11 +2,14 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { IWorkspaceService } from './types'; @injectable() export class WorkspaceService implements IWorkspaceService { + public get onDidChangeConfiguration(): Event { + return workspace.onDidChangeConfiguration; + } public get rootPath(): string | undefined { return workspace.rootPath; } @@ -16,6 +19,9 @@ export class WorkspaceService implements IWorkspaceService { public get onDidChangeWorkspaceFolders(): Event { return workspace.onDidChangeWorkspaceFolders; } + public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { + return workspace.getConfiguration(section, resource); + } public getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { return workspace.getWorkspaceFolder(uri); } diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts index 7746cb5b29a3..629b809e8d06 100644 --- a/src/client/common/extensions.ts +++ b/src/client/common/extensions.ts @@ -15,6 +15,11 @@ interface String { * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. */ splitLines(splitOptions?: { trim: boolean, removeEmptyEntries: boolean }): string[]; + /** + * Appropriately formats a string so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + */ + toCommandArgument(): string; } /** @@ -32,3 +37,15 @@ String.prototype.splitLines = function (this: string, splitOptions: { trim: bool } return lines; }; + +/** + * Appropriately formats a string so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + * @param {String} value. + */ +String.prototype.toCommandArgument = function (this: string): string { + if (!this) { + return this; + } + return (this.indexOf(' ') > 0 && !this.startsWith('"') && !this.endsWith('"')) ? `"${this}"` : this.toString(); +}; diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index f51ebdd3ecc4..0203e453bb0c 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -2,15 +2,10 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import * as path from 'path'; import { Uri } from 'vscode'; -import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts'; -import { CONDA_RELATIVE_PY_PATH } from '../../interpreter/locators/services/conda'; +import { ICondaService, IInterpreterService, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { PythonSettings } from '../configSettings'; -import { IPythonExecutionFactory } from '../process/types'; import { ExecutionInfo } from '../types'; -import { arePathsSame } from '../utils'; import { ModuleInstaller } from './moduleInstaller'; import { IModuleInstaller } from './types'; @@ -35,7 +30,7 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller if (typeof this.isCondaAvailable === 'boolean') { return this.isCondaAvailable!; } - const condaLocator = this.serviceContainer.get(ICondaLocatorService); + const condaLocator = this.serviceContainer.get(ICondaService); const available = await condaLocator.isCondaAvailable(); if (!available) { @@ -46,20 +41,21 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller return this.isCurrentEnvironmentACondaEnvironment(resource); } protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise { - const condaLocator = this.serviceContainer.get(ICondaLocatorService); + const condaLocator = this.serviceContainer.get(ICondaService); const condaFile = await condaLocator.getCondaFile(); - const info = await this.getCurrentInterpreterInfo(resource); + const interpreterService = this.serviceContainer.get(IInterpreterService); + const info = await interpreterService.getActiveInterpreter(resource); const args = ['install']; - if (info.envName) { + if (info!.envName) { // If we have the name of the conda environment, then use that. args.push('--name'); - args.push(info.envName!); + args.push(info!.envName!); } else { // Else provide the full path to the environment path. args.push('--prefix'); - args.push(info.envPath); + args.push(info!.envPath!); } args.push(moduleName); return { @@ -68,37 +64,9 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller moduleName: '' }; } - private async getCurrentPythonPath(resource?: Uri): Promise { - const pythonPath = PythonSettings.getInstance(resource).pythonPath; - if (path.basename(pythonPath) === pythonPath) { - const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); - return pythonProc.getExecutablePath().catch(() => pythonPath); - } else { - return pythonPath; - } - } private isCurrentEnvironmentACondaEnvironment(resource?: Uri) { - return this.getCurrentInterpreterInfo(resource) - .then(info => info && info.isConda === true).catch(() => false); - } - private async getCurrentInterpreterInfo(resource?: Uri) { - // Use this service, though it returns everything it is cached. - const interpreterLocator = this.serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - const interpretersPromise = interpreterLocator.getInterpreters(resource); - const pythonPathPromise = this.getCurrentPythonPath(resource); - const [interpreters, currentPythonPath] = await Promise.all([interpretersPromise, pythonPathPromise]); - - // Check if we have the info about the current python path. - const pathToCompareWith = path.dirname(currentPythonPath); - const info = interpreters.find(item => arePathsSame(path.dirname(item.path), pathToCompareWith)); - // tslint:disable-next-line:prefer-array-literal - const pathsToRemove = new Array(CONDA_RELATIVE_PY_PATH.length).fill('..') as string[]; - const envPath = path.join(path.dirname(currentPythonPath), ...pathsToRemove); - return { - isConda: info && info!.type === InterpreterType.Conda, - pythonPath: currentPythonPath, - envPath, - envName: info ? info!.envName : undefined - }; + const interpreterService = this.serviceContainer.get(IInterpreterService); + return interpreterService.getActiveInterpreter(resource) + .then(info => info ? info.type === InterpreterType.Conda : false).catch(() => false); } } diff --git a/src/client/common/installer/installer.ts b/src/client/common/installer/installer.ts index d73e97179fee..28a553b6afb8 100644 --- a/src/client/common/installer/installer.ts +++ b/src/client/common/installer/installer.ts @@ -11,8 +11,8 @@ import { PythonSettings } from '../configSettings'; import { STANDARD_OUTPUT_CHANNEL } from '../constants'; import { IPlatformService } from '../platform/types'; import { IProcessService, IPythonExecutionFactory } from '../process/types'; -import { ITerminalService } from '../terminal/types'; -import { IInstaller, ILogger, InstallerResponse, IOutputChannel, IsWindows, ModuleNamePurpose, Product } from '../types'; +import { ITerminalServiceFactory } from '../terminal/types'; +import { IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types'; import { IModuleInstaller } from './types'; export { Product } from '../types'; @@ -237,7 +237,8 @@ export class Installer implements IInstaller { this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); this.outputChannel.show(); } else { - const terminalService = this.serviceContainer.get(ITerminalService); + const terminalServiceFactory = this.serviceContainer.get(ITerminalServiceFactory); + const terminalService = terminalServiceFactory.getTerminalService(); const logger = this.serviceContainer.get(ILogger); terminalService.sendCommand(CTagsInsllationScript, []) .catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`)); diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 2fb09a7216ec..7f6b028b1a93 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -13,7 +13,7 @@ import { IServiceContainer } from '../../ioc/types'; import { PythonSettings } from '../configSettings'; import { STANDARD_OUTPUT_CHANNEL } from '../constants'; import { IFileSystem } from '../platform/types'; -import { ITerminalService } from '../terminal/types'; +import { ITerminalServiceFactory } from '../terminal/types'; import { ExecutionInfo, IOutputChannel } from '../types'; @injectable() @@ -21,7 +21,7 @@ export abstract class ModuleInstaller { constructor(protected serviceContainer: IServiceContainer) { } public async installModule(name: string, resource?: vscode.Uri): Promise { const executionInfo = await this.getExecutionInfo(name, resource); - const terminalService = this.serviceContainer.get(ITerminalService); + const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(); if (executionInfo.moduleName) { const settings = PythonSettings.getInstance(resource); diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index bfe38a2ab836..d573256e403e 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -2,16 +2,14 @@ // Licensed under the MIT License. 'use strict'; -import * as fs from 'fs'; -import * as fse from 'fs-extra'; +import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { IServiceContainer } from '../../ioc/types'; import { IFileSystem, IPlatformService } from './types'; @injectable() export class FileSystem implements IFileSystem { - constructor( @inject(IServiceContainer) private platformService: IPlatformService) { } + constructor( @inject(IPlatformService) private platformService: IPlatformService) { } public get directorySeparatorChar(): string { return path.sep; @@ -19,7 +17,7 @@ export class FileSystem implements IFileSystem { public objectExistsAsync(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { return new Promise(resolve => { - fse.stat(filePath, (error, stats) => { + fs.stat(filePath, (error, stats) => { if (error) { return resolve(false); } @@ -31,13 +29,22 @@ export class FileSystem implements IFileSystem { public fileExistsAsync(filePath: string): Promise { return this.objectExistsAsync(filePath, (stats) => stats.isFile()); } + /** + * Reads the contents of the file using utf8 and returns the string contents. + * @param {string} filePath + * @returns {Promise} + * @memberof FileSystem + */ + public readFile(filePath: string): Promise { + return fs.readFile(filePath).then(buffer => buffer.toString()); + } public directoryExistsAsync(filePath: string): Promise { return this.objectExistsAsync(filePath, (stats) => stats.isDirectory()); } public createDirectoryAsync(directoryPath: string): Promise { - return fse.mkdirp(directoryPath); + return fs.mkdirp(directoryPath); } public getSubDirectoriesAsync(rootDir: string): Promise { @@ -46,7 +53,7 @@ export class FileSystem implements IFileSystem { if (error) { return resolve([]); } - const subDirs = []; + const subDirs: string[] = []; files.forEach(name => { const fullPath = path.join(rootDir, name); try { diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index 58f940afdc53..7d861532dd20 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -36,4 +36,5 @@ export interface IFileSystem { createDirectoryAsync(path: string): Promise; getSubDirectoriesAsync(rootDir: string): Promise; arePathsSame(path1: string, path2: string): boolean; + readFile(filePath: string): Promise; } diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 66c2c8b50133..43d1735ebb16 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -15,10 +15,11 @@ import { PersistentStateFactory } from './persistentState'; import { IS_64_BIT, IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; import { CurrentProcess } from './process/currentProcess'; +import { Bash } from './terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt'; import { TerminalServiceFactory } from './terminal/factory'; import { TerminalHelper } from './terminal/helper'; -import { TerminalService } from './terminal/service'; -import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from './terminal/types'; +import { ITerminalActivationCommandProvider, ITerminalHelper, ITerminalServiceFactory } from './terminal/types'; import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types'; export function registerTypes(serviceManager: IServiceManager) { @@ -27,9 +28,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); serviceManager.addSingleton(ILogger, Logger); - serviceManager.addSingleton(ITerminalService, TerminalService); serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); - serviceManager.addSingleton(ITerminalHelper, TerminalHelper); serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(IApplicationShell, ApplicationShell); serviceManager.addSingleton(ICurrentProcess, CurrentProcess); @@ -39,4 +38,8 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IWorkspaceService, WorkspaceService); serviceManager.addSingleton(IDocumentManager, DocumentManager); serviceManager.addSingleton(ITerminalManager, TerminalManager); + + serviceManager.addSingleton(ITerminalHelper, TerminalHelper); + serviceManager.addSingleton(ITerminalActivationCommandProvider, Bash, 'bashCShellFish'); + serviceManager.addSingleton(ITerminalActivationCommandProvider, CommandPromptAndPowerShell, 'commandPromptAndPowerShell'); } diff --git a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts new file mode 100644 index 000000000000..36a8ee11db94 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { PythonInterpreter } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { IFileSystem } from '../../platform/types'; +import { TerminalShellType } from '../types'; +import { ITerminalActivationCommandProvider } from '../types'; + +@injectable() +export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(protected readonly serviceContainer: IServiceContainer) { } + + public abstract isShellSupported(targetShell: TerminalShellType): boolean; + public abstract getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise; + + protected async findScriptFile(interpreter: PythonInterpreter, scriptFileNames: string[]): Promise { + const fs = this.serviceContainer.get(IFileSystem); + + for (const scriptFileName of scriptFileNames) { + // Generate scripts are found in the same directory as the interpreter. + const scriptFile = path.join(path.dirname(interpreter.path), scriptFileName); + const found = await fs.fileExistsAsync(scriptFile); + if (found) { + return scriptFile; + } + } + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/bash.ts b/src/client/common/terminal/environmentActivationProviders/bash.ts new file mode 100644 index 000000000000..c047d8a21338 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { PythonInterpreter } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import '../../extensions'; +import { TerminalShellType } from '../types'; +import { BaseActivationCommandProvider } from './baseActivationProvider'; + +@injectable() +export class Bash extends BaseActivationCommandProvider { + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public isShellSupported(targetShell: TerminalShellType): boolean { + return targetShell === TerminalShellType.bash || + targetShell === TerminalShellType.cshell || + targetShell === TerminalShellType.fish; + } + public async getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise { + const scriptFile = await this.findScriptFile(interpreter, this.getScriptsInOrderOfPreference(targetShell)); + if (!scriptFile) { + return; + } + const envName = interpreter.envName ? interpreter.envName! : ''; + // In the case of conda environments, the name of the environment must be provided. + // E.g. `source acrtivate `. + return [`source ${scriptFile.toCommandArgument()} ${envName}`.trim()]; + } + + private getScriptsInOrderOfPreference(targetShell: TerminalShellType): string[] { + switch (targetShell) { + case TerminalShellType.bash: { + return ['activate.sh', 'activate']; + } + case TerminalShellType.cshell: { + return ['activate.csh']; + } + case TerminalShellType.fish: { + return ['activate.fish']; + } + default: { + return []; + } + } + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts new file mode 100644 index 000000000000..4eea57cf5a41 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { PythonInterpreter } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import '../../extensions'; +import { IPlatformService } from '../../platform/types'; +import { TerminalShellType } from '../types'; +import { BaseActivationCommandProvider } from './baseActivationProvider'; + +@injectable() +export class CommandPromptAndPowerShell extends BaseActivationCommandProvider { + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public isShellSupported(targetShell: TerminalShellType): boolean { + return targetShell === TerminalShellType.commandPrompt || + targetShell === TerminalShellType.powershell; + } + public async getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise { + // Dependending on the target shell, look for the preferred script file. + const scriptsInOrderOfPreference = targetShell === TerminalShellType.commandPrompt ? ['activate.bat', 'activate.ps1'] : ['activate.ps1', 'activate.bat']; + const scriptFile = await this.findScriptFile(interpreter, scriptsInOrderOfPreference); + if (!scriptFile) { + return; + } + + const envName = interpreter.envName ? interpreter.envName! : ''; + + if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.bat')) { + return [`${scriptFile.toCommandArgument()} ${envName}`.trim()]; + } else if (targetShell === TerminalShellType.powershell && scriptFile.endsWith('activate.ps1')) { + return [`${scriptFile.toCommandArgument()} ${envName}`.trim()]; + } else if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.ps1')) { + return [`powershell ${scriptFile.toCommandArgument()} ${envName}`.trim()]; + } else { + // This means we're in powershell and we have a .bat file. + if (this.serviceContainer.get(IPlatformService).isWindows) { + // On windows, the solution is to go into cmd, then run the batch (.bat) file and go back into powershell. + return [ + 'cmd', + `${scriptFile.toCommandArgument()} ${envName}`.trim(), + 'powershell' + ]; + } else { + // Powershell on non-windows os, we cannot execute the batch file. + return; + } + } + } +} diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 5a3f167890ed..a8ac07e4c114 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -2,31 +2,36 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Disposable } from 'vscode'; -import { ITerminalManager } from '../application/types'; -import { IDisposableRegistry } from '../types'; +import { Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; import { TerminalService } from './service'; -import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from './types'; +import { ITerminalService, ITerminalServiceFactory } from './types'; @injectable() export class TerminalServiceFactory implements ITerminalServiceFactory { private terminalServices: Map; - constructor( @inject(ITerminalService) private defaultTerminalService: ITerminalService, - @inject(IDisposableRegistry) private disposableRegistry: Disposable[], - @inject(ITerminalManager) private terminalManager: ITerminalManager, - @inject(ITerminalHelper) private terminalHelper: ITerminalHelper) { + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.terminalServices = new Map(); } - public getTerminalService(title?: string): ITerminalService { - if (typeof title !== 'string' || title.trim().length === 0) { - return this.defaultTerminalService; + public getTerminalService(resource?: Uri, title?: string): ITerminalService { + + const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + const id = this.getTerminalId(terminalTitle, resource); + if (!this.terminalServices.has(id)) { + const terminalService = new TerminalService(this.serviceContainer, resource, terminalTitle); + this.terminalServices.set(id, terminalService); } - if (!this.terminalServices.has(title)) { - const terminalService = new TerminalService(this.terminalHelper, this.terminalManager, this.disposableRegistry, title); - this.terminalServices.set(title, terminalService); + + return this.terminalServices.get(id)!; + } + private getTerminalId(title: string, resource?: Uri): string { + if (!resource) { + return title; } - return this.terminalServices.get(title)!; + const workspaceFolder = this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource!); + return workspaceFolder ? `${title}:${workspaceFolder.uri.fsPath}` : title; } } diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index ad2c84839512..60c880804951 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -2,29 +2,37 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Terminal, workspace } from 'vscode'; -import { ITerminalManager } from '../application/types'; +import { Terminal, Uri } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { ITerminalManager, IWorkspaceService } from '../application/types'; +import '../extensions'; import { IPlatformService } from '../platform/types'; -import { ITerminalHelper, TerminalShellType } from './types'; +import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalShellType } from './types'; -const IS_BASH = /(bash.exe$|wsl.exe$|bash$|zsh$)/i; +// Types of shells can be found here: +// 1. https://wiki.ubuntu.com/ChangingShells +const IS_BASH = /(bash.exe$|wsl.exe$|bash$|zsh$|ksh$)/i; const IS_COMMAND = /cmd.exe$/i; const IS_POWERSHELL = /(powershell.exe$|pwsh$|powershell$)/i; const IS_FISH = /(fish$)/i; +const IS_CSHELL = /(csh$)/i; @injectable() export class TerminalHelper implements ITerminalHelper { private readonly detectableShells: Map; - constructor( @inject(IPlatformService) private platformService: IPlatformService, - @inject(ITerminalManager) private terminalManager: ITerminalManager) { + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.detectableShells = new Map(); this.detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); this.detectableShells.set(TerminalShellType.bash, IS_BASH); this.detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); this.detectableShells.set(TerminalShellType.fish, IS_FISH); + this.detectableShells.set(TerminalShellType.cshell, IS_CSHELL); } public createTerminal(title?: string): Terminal { - return this.terminalManager.createTerminal({ name: title }); + const terminalManager = this.serviceContainer.get(ITerminalManager); + return terminalManager.createTerminal({ name: title }); } public identifyTerminalShell(shellPath: string): TerminalShellType { return Array.from(this.detectableShells.keys()) @@ -36,13 +44,16 @@ export class TerminalHelper implements ITerminalHelper { }, TerminalShellType.other); } public getTerminalShellPath(): string { + const workspace = this.serviceContainer.get(IWorkspaceService); const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); + + const platformService = this.serviceContainer.get(IPlatformService); let osSection = ''; - if (this.platformService.isWindows) { + if (platformService.isWindows) { osSection = 'windows'; - } else if (this.platformService.isMac) { + } else if (platformService.isMac) { osSection = 'osx'; - } else if (this.platformService.isLinux) { + } else if (platformService.isLinux) { osSection = 'linux'; } if (osSection.length === 0) { @@ -51,9 +62,26 @@ export class TerminalHelper implements ITerminalHelper { return shellConfig.get(osSection)!; } public buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]) { - const executable = command.indexOf(' ') > 0 ? `"${command}"` : command; const isPowershell = terminalShellType === TerminalShellType.powershell; const commandPrefix = isPowershell ? '& ' : ''; - return `${commandPrefix}${executable} ${args.join(' ')}`.trim(); + return `${commandPrefix}${command.toCommandArgument()} ${args.join(' ')}`.trim(); + } + public async getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interperterInfo = await interpreterService.getActiveInterpreter(resource); + if (!interperterInfo) { + return; + } + + // Search from the list of providers. + const providers = this.serviceContainer.getAll(ITerminalActivationCommandProvider); + const supportedProviders = providers.filter(provider => provider.isShellSupported(terminalShellType)); + + for (const provider of supportedProviders) { + const activationCommands = await provider.getActivationCommands(interperterInfo, terminalShellType); + if (Array.isArray(activationCommands)) { + return activationCommands; + } + } } } diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 79b488b540d3..77961815b18b 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -2,7 +2,8 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, Terminal } from 'vscode'; +import { Disposable, Event, EventEmitter, Terminal, Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; import { ITerminalManager } from '../application/types'; import { IDisposableRegistry } from '../types'; import { ITerminalHelper, ITerminalService, TerminalShellType } from './types'; @@ -12,16 +13,20 @@ export class TerminalService implements ITerminalService, Disposable { private terminal?: Terminal; private terminalShellType: TerminalShellType; private terminalClosed = new EventEmitter(); + private terminalManager: ITerminalManager; + private terminalHelper: ITerminalHelper; public get onDidCloseTerminal(): Event { return this.terminalClosed.event; } - constructor( @inject(ITerminalHelper) private terminalHelper: ITerminalHelper, - @inject(ITerminalManager) terminalManager: ITerminalManager, - @inject(IDisposableRegistry) disposableRegistry: Disposable[], + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, + private resource?: Uri, private title: string = 'Python') { + const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); disposableRegistry.push(this); - terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.terminalManager = this.serviceContainer.get(ITerminalManager); + this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); } public dispose() { if (this.terminal) { @@ -39,18 +44,33 @@ export class TerminalService implements ITerminalService, Disposable { this.terminal!.show(); this.terminal!.sendText(text); } + public async show(): Promise { + await this.ensureTerminal(); + this.terminal!.show(); + } private async ensureTerminal(): Promise { if (this.terminal) { return; } const shellPath = this.terminalHelper.getTerminalShellPath(); this.terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.terminalHelper.identifyTerminalShell(shellPath); - this.terminal = this.terminalHelper.createTerminal(this.title); - this.terminal!.show(); + this.terminal = this.terminalManager.createTerminal({ name: this.title }); // Sometimes the terminal takes some time to start up before it can start accepting input. - // tslint:disable-next-line:no-unnecessary-callback-wrapper - await new Promise(resolve => setTimeout(() => resolve(), 1000)); + await new Promise(resolve => setTimeout(resolve, 100)); + + const activationCommamnds = await this.terminalHelper.getEnvironmentActivationCommands(this.terminalShellType, this.resource); + if (activationCommamnds) { + for (const command of activationCommamnds!) { + this.terminal!.sendText(command); + + // Give the command some time to complete. + // Its been observed that sending commands too early will strip some text off. + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + this.terminal!.show(); } private terminalCloseHandler(terminal: Terminal) { if (terminal === this.terminal) { diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index d279e22e4a52..656f11ae76a7 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -2,21 +2,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, Terminal } from 'vscode'; -export const ITerminalService = Symbol('ITerminalService'); +import { Event, Terminal, Uri } from 'vscode'; +import { PythonInterpreter } from '../../interpreter/contracts'; export enum TerminalShellType { powershell = 1, commandPrompt = 2, bash = 3, fish = 4, - other = 5 + cshell = 5, + other = 6 } export interface ITerminalService { readonly onDidCloseTerminal: Event; sendCommand(command: string, args: string[]): Promise; sendText(text: string): Promise; + show(): void; } export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory'); @@ -25,11 +27,12 @@ export interface ITerminalServiceFactory { /** * Gets a terminal service with a specific title. * If one exists, its returned else a new one is created. + * @param {Uri} resource * @param {string} title * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(title?: string): ITerminalService; + getTerminalService(resource?: Uri, title?: string): ITerminalService; } export const ITerminalHelper = Symbol('ITerminalHelper'); @@ -39,4 +42,12 @@ export interface ITerminalHelper { identifyTerminalShell(shellPath: string): TerminalShellType; getTerminalShellPath(): string; buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string; + getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise; +} + +export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCommandProvider'); + +export interface ITerminalActivationCommandProvider { + isShellSupported(targetShell: TerminalShellType): boolean; + getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise; } diff --git a/src/client/extension.ts b/src/client/extension.ts index caae8b7f5913..deeebe91610a 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -7,8 +7,8 @@ if ((Reflect as any).metadata === undefined) { } import { Container } from 'inversify'; import * as os from 'os'; -import * as vscode from 'vscode'; import { Disposable, Memento, OutputChannel, window } from 'vscode'; +import * as vscode from 'vscode'; import { BannerService } from './banner'; import { PythonSettings } from './common/configSettings'; import * as settings from './common/configSettings'; @@ -19,15 +19,14 @@ import { PythonInstaller } from './common/installer/pythonInstallation'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; -import { IProcessService, IPythonExecutionFactory } from './common/process/types'; +import { IProcessService } from './common/process/types'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; import { SimpleConfigurationProvider } from './debugger'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; -import { InterpreterManager } from './interpreter'; import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider'; -import { ICondaLocatorService, IInterpreterVersionService } from './interpreter/contracts'; +import { ICondaService, IInterpreterService, IInterpreterVersionService } from './interpreter/contracts'; import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; @@ -95,10 +94,11 @@ export async function activate(context: vscode.ExtensionContext) { const persistentStateFactory = serviceManager.get(IPersistentStateFactory); const pythonSettings = settings.PythonSettings.getInstance(); + // tslint:disable-next-line:no-floating-promises sendStartupTelemetry(activated, serviceContainer); sortImports.activate(context, standardOutputChannel, serviceContainer); - const interpreterManager = new InterpreterManager(serviceContainer); + const interpreterManager = serviceContainer.get(IInterpreterService); const pythonInstaller = new PythonInstaller(serviceContainer); await pythonInstaller.checkPythonInstallation(PythonSettings.getInstance()); @@ -108,7 +108,7 @@ export async function activate(context: vscode.ExtensionContext) { interpreterManager.refresh() .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); - context.subscriptions.push(interpreterManager); + const processService = serviceContainer.get(IProcessService); const interpreterVersionService = serviceContainer.get(IInterpreterVersionService); context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService, processService)); @@ -117,7 +117,7 @@ export async function activate(context: vscode.ExtensionContext) { const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - context.subscriptions.push(new ReplProvider(serviceContainer.get(IPythonExecutionFactory))); + context.subscriptions.push(new ReplProvider(serviceContainer)); // Enable indentAction // tslint:disable-next-line:no-non-null-assertion @@ -205,7 +205,7 @@ async function sendStartupTelemetry(activatedPromise: Promise, serviceCont try { await activatedPromise; const duration = stopWatch.elapsedTime; - const condaLocator = serviceContainer.get(ICondaLocatorService); + const condaLocator = serviceContainer.get(ICondaService); const condaVersion = await condaLocator.getCondaVersion().catch(() => undefined); const props = condaVersion ? { condaVersion } : undefined; sendTelemetryEvent(EDITOR_LOAD, duration, props); diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts index a359473c41cd..22a2da9aabbc 100644 --- a/src/client/interpreter/configuration/setInterpreterProvider.ts +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -1,9 +1,8 @@ import * as path from 'path'; import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; -import { InterpreterManager } from '../'; import * as settings from '../../common/configSettings'; import { IProcessService } from '../../common/process/types'; -import { IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { IInterpreterService, IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts'; import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; import { PythonPathUpdaterService } from './pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; @@ -16,7 +15,7 @@ interface PythonPathQuickPickItem extends QuickPickItem { export class SetInterpreterProvider implements Disposable { private disposables: Disposable[] = []; private pythonPathUpdaterService: PythonPathUpdaterService; - constructor(private interpreterManager: InterpreterManager, + constructor(private interpreterManager: IInterpreterService, interpreterVersionService: IInterpreterVersionService, private processService: IProcessService) { this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 271dd203e1f4..e457988be018 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -15,7 +15,6 @@ export interface IInterpreterVersionService { getPipVersion(pythonPath: string): Promise; } -export const ICondaEnvironmentFile = Symbol('ICondaEnvironmentFile'); export const IKnownSearchPathsForInterpreters = Symbol('IKnownSearchPathsForInterpreters'); export const IKnownSearchPathsForVirtualEnvironments = Symbol('IKnownSearchPathsForVirtualEnvironments'); @@ -25,12 +24,24 @@ export interface IInterpreterLocatorService extends Disposable { getInterpreters(resource?: Uri): Promise; } -export const ICondaLocatorService = Symbol('ICondaLocatorService'); +export type CondaInfo = { + envs?: string[]; + 'sys.version'?: string; + 'sys.prefix'?: string; + 'python_version'?: string; + default_prefix?: string; +}; + +export const ICondaService = Symbol('ICondaService'); -export interface ICondaLocatorService { +export interface ICondaService { + readonly condaEnvironmentsFile: string | undefined; getCondaFile(): Promise; isCondaAvailable(): Promise; getCondaVersion(): Promise; + getCondaInfo(): Promise; + getCondaEnvironments(): Promise<({ name: string, path: string }[]) | undefined>; + getInterpreterPath(condaEnvironmentPath: string): string; } export enum InterpreterType { @@ -48,6 +59,7 @@ export type PythonInterpreter = { architecture?: Architecture; type: InterpreterType; envName?: string; + envPath?: string; }; export type WorkspacePythonPath = { @@ -55,3 +67,12 @@ export type WorkspacePythonPath = { pytonPath?: string; configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; }; + +export const IInterpreterService = Symbol('IInterpreterService'); + +export interface IInterpreterService { + getInterpreters(resource?: Uri): Promise; + autoSetInterpreter(): Promise; + getActiveInterpreter(resource?: Uri): Promise; + refresh(): Promise; +} diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 8ef1f093e1d7..2be3b8563102 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -2,19 +2,17 @@ import { EOL } from 'os'; import * as path from 'path'; import { Disposable, StatusBarItem, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { IProcessService } from '../../common/process/types'; import * as utils from '../../common/utils'; -import { IInterpreterLocatorService, IInterpreterVersionService } from '../contracts'; +import { IInterpreterService, IInterpreterVersionService } from '../contracts'; import { getActiveWorkspaceUri } from '../helpers'; import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; // tslint:disable-next-line:completed-docs export class InterpreterDisplay implements Disposable { constructor(private statusBar: StatusBarItem, - private interpreterLocator: IInterpreterLocatorService, + private interpreterService: IInterpreterService, private virtualEnvMgr: IVirtualEnvironmentManager, - private versionProvider: IInterpreterVersionService, - private processService: IProcessService) { + private versionProvider: IInterpreterVersionService) { this.statusBar.command = 'python.setInterpreter'; } @@ -26,15 +24,12 @@ export class InterpreterDisplay implements Disposable { if (!wkspc) { return; } - const pythonPath = await this.getFullyQualifiedPathToInterpreter(PythonSettings.getInstance(wkspc.folderUri).pythonPath); - await this.updateDisplay(pythonPath, wkspc.folderUri); + await this.updateDisplay(wkspc.folderUri); } - private async getInterpreters(resource?: Uri) { - return this.interpreterLocator.getInterpreters(resource); - } - private async updateDisplay(pythonPath: string, resource?: Uri) { - const interpreters = await this.getInterpreters(resource); - const interpreter = interpreters.find(i => utils.arePathsSame(i.path, pythonPath)); + private async updateDisplay(resource?: Uri) { + const interpreters = await this.interpreterService.getInterpreters(resource); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const pythonPath = interpreter ? interpreter.path : PythonSettings.getInstance(resource).pythonPath; this.statusBar.color = ''; this.statusBar.tooltip = pythonPath; @@ -69,10 +64,4 @@ export class InterpreterDisplay implements Disposable { .detect(pythonPath) .then(env => env ? env.name : ''); } - private async getFullyQualifiedPathToInterpreter(pythonPath: string) { - return this.processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)']) - .then(output => output.stdout.trim()) - .then(value => value.length === 0 ? pythonPath : value) - .catch(() => pythonPath); - } } diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index 4291269b6537..644b340c139c 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,34 +1,38 @@ +import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { IProcessService } from '../common/process/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IDisposableRegistry } from '../common/types'; +import * as utils from '../common/utils'; import { IServiceContainer } from '../ioc/types'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; -import { IInterpreterLocatorService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE } from './contracts'; +import { IInterpreterLocatorService, IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PythonInterpreter } from './contracts'; import { InterpreterDisplay } from './display'; import { getActiveWorkspaceUri } from './helpers'; import { PythonInterpreterLocatorService } from './locators'; import { VirtualEnvService } from './locators/services/virtualEnvService'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; -export class InterpreterManager implements Disposable { - private disposables: Disposable[] = []; +@injectable() +export class InterpreterManager implements Disposable, IInterpreterService { private display: InterpreterDisplay | null | undefined; private interpreterProvider: PythonInterpreterLocatorService; private pythonPathUpdaterService: PythonPathUpdaterService; - constructor(private serviceContainer: IServiceContainer) { + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { const virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); this.interpreterProvider = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); const versionService = serviceContainer.get(IInterpreterVersionService); - const processService = serviceContainer.get(IProcessService); - this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService, processService); + this.display = new InterpreterDisplay(statusBar, this, virtualEnvMgr, versionService); this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), versionService); PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); - this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); - this.disposables.push(statusBar); - this.disposables.push(this.display!); + + const disposables = this.serviceContainer.get(IDisposableRegistry); + disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); + disposables.push(statusBar); + disposables.push(this.display!); } public async refresh() { return this.display!.refresh(); @@ -59,18 +63,35 @@ export class InterpreterManager implements Disposable { // Ensure this new environment is at the same level as the current workspace. // In windows the interpreter is under scripts/python.exe on linux it is under bin/python. // Meaning the sub directory must be either scripts, bin or other (but only one level deep). - const pythonPath = interpretersInWorkspace.sort((a, b) => a.version > b.version ? 1 : -1)[0].path; + const pythonPath = interpretersInWorkspace.sort((a, b) => a.version! > b.version! ? 1 : -1)[0].path; const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); } } public dispose(): void { - // tslint:disable-next-line:prefer-type-cast - this.disposables.forEach(disposable => disposable.dispose() as void); this.display = null; this.interpreterProvider.dispose(); } + + public async getActiveInterpreter(resource?: Uri): Promise { + const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); + const pythonExecutionService = await pythonExecutionFactory.create(resource); + const fullyQualifiedPath = await pythonExecutionService.getExecutablePath(); + const interpreters = await this.getInterpreters(resource); + const interpreter = interpreters.find(i => utils.arePathsSame(i.path, fullyQualifiedPath)); + + if (interpreter) { + return interpreter; + } + const pythonExecutableName = path.basename(fullyQualifiedPath); + const versionInfo = await this.serviceContainer.get(IInterpreterVersionService).getVersion(fullyQualifiedPath, pythonExecutableName); + return { + path: fullyQualifiedPath, + type: InterpreterType.Unknown, + version: versionInfo + }; + } private shouldAutoSetInterpreter() { const activeWorkspace = getActiveWorkspaceUri(); if (!activeWorkspace) { diff --git a/src/client/interpreter/locators/services/conda.ts b/src/client/interpreter/locators/services/conda.ts index 808a3d6ea049..1f25682b4f6d 100644 --- a/src/client/interpreter/locators/services/conda.ts +++ b/src/client/interpreter/locators/services/conda.ts @@ -1,7 +1,3 @@ -import { IS_WINDOWS } from '../../../common/utils'; - -// where to find the Python binary within a conda env. -export const CONDA_RELATIVE_PY_PATH = IS_WINDOWS ? ['python.exe'] : ['bin', 'python']; // tslint:disable-next-line:variable-name export const AnacondaCompanyNames = ['Anaconda, Inc.', 'Continuum Analytics, Inc.']; // tslint:disable-next-line:variable-name @@ -10,10 +6,3 @@ export const AnacondaCompanyName = 'Anaconda, Inc.'; export const AnacondaDisplayName = 'Anaconda'; // tslint:disable-next-line:variable-name export const AnacondaIdentfiers = ['Anaconda', 'Conda', 'Continuum']; - -export type CondaInfo = { - envs?: string[]; - 'sys.version'?: string; - 'python_version'?: string; - default_prefix?: string; -}; diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts index 5ca921911afa..bd538f6c8f04 100644 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -1,21 +1,23 @@ -import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IS_WINDOWS } from '../../../common/configSettings'; +import { IFileSystem } from '../../../common/platform/types'; +import { ILogger } from '../../../common/types'; import { - ICondaEnvironmentFile, + ICondaService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; -import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; +import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from './conda'; @injectable() export class CondaEnvFileService implements IInterpreterLocatorService { - constructor( @inject(ICondaEnvironmentFile) private condaEnvironmentFile: string, - @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService) { + constructor( @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, + @inject(ICondaService) private condaService: ICondaService, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(ILogger) private logger: ILogger) { } public async getInterpreters(_?: Uri) { return this.getSuggestionsFromConda(); @@ -23,40 +25,58 @@ export class CondaEnvFileService implements IInterpreterLocatorService { // tslint:disable-next-line:no-empty public dispose() { } private async getSuggestionsFromConda(): Promise { - return fs.pathExists(this.condaEnvironmentFile) - .then(exists => exists ? this.getEnvironmentsFromFile(this.condaEnvironmentFile) : Promise.resolve([])); + if (!this.condaService.condaEnvironmentsFile) { + return []; + } + return this.fileSystem.fileExistsAsync(this.condaService.condaEnvironmentsFile!) + .then(exists => exists ? this.getEnvironmentsFromFile(this.condaService.condaEnvironmentsFile!) : Promise.resolve([])); } private async getEnvironmentsFromFile(envFile: string) { - return fs.readFile(envFile) - .then(buffer => buffer.toString().split(/\r?\n/g)) - .then(lines => lines.map(line => line.trim())) - .then(lines => lines.map(line => path.join(line, ...CONDA_RELATIVE_PY_PATH))) - .then(interpreterPaths => interpreterPaths.map(item => fs.pathExists(item).then(exists => exists ? item : ''))) - .then(promises => Promise.all(promises)) - .then(interpreterPaths => interpreterPaths.filter(item => item.length > 0)) - .then(interpreterPaths => interpreterPaths.map(item => this.getInterpreterDetails(item))) - .then(promises => Promise.all(promises)) - .catch((err) => { - console.error('Python Extension (getEnvironmentsFromFile.readFile):', err); - // Ignore errors in reading the file. - return [] as PythonInterpreter[]; - }); + try { + const fileContents = await this.fileSystem.readFile(envFile); + const environmentPaths = fileContents.split(/\r?\n/g) + .map(environmentPath => environmentPath.trim()) + .filter(environmentPath => environmentPath.length > 0); + + const interpreters = (await Promise.all(environmentPaths + .map(environmentPath => this.getInterpreterDetails(environmentPath)))) + .filter(item => !!item) + .map(item => item!); + + const environments = await this.condaService.getCondaEnvironments(); + if (Array.isArray(environments) && environments.length > 0) { + interpreters + .forEach(interpreter => { + const environment = environments.find(item => this.fileSystem.arePathsSame(item.path, interpreter!.envPath!)); + if (environment) { + interpreter.envName = environment!.name; + interpreter.displayName = `${interpreter.displayName} (${environment!.name})`; + } + }); + } + return interpreters; + } catch (err) { + this.logger.logError('Python Extension (getEnvironmentsFromFile.readFile):', err); + // Ignore errors in reading the file. + return [] as PythonInterpreter[]; + } } - private async getInterpreterDetails(interpreter: string) { - return this.versionService.getVersion(interpreter, path.basename(interpreter)) - .then(version => { - version = this.stripCompanyName(version); - const envName = this.getEnvironmentRootDirectory(interpreter); - // tslint:disable-next-line:no-unnecessary-local-variable - const info: PythonInterpreter = { - displayName: `${AnacondaDisplayName} ${version} (${envName})`, - path: interpreter, - companyDisplayName: AnacondaCompanyName, - version: version, - type: InterpreterType.Conda - }; - return info; - }); + private async getInterpreterDetails(environmentPath: string): Promise { + const interpreter = this.condaService.getInterpreterPath(environmentPath); + if (!interpreter || !await this.fileSystem.fileExistsAsync(interpreter)) { + return; + } + + const version = await this.versionService.getVersion(interpreter, path.basename(interpreter)); + const versionWithoutCompanyName = this.stripCompanyName(version); + return { + displayName: `${AnacondaDisplayName} ${versionWithoutCompanyName}`, + path: interpreter, + companyDisplayName: AnacondaCompanyName, + version: version, + type: InterpreterType.Conda, + envPath: environmentPath + }; } private stripCompanyName(content: string) { // Strip company name from version. @@ -69,13 +89,4 @@ export class CondaEnvFileService implements IInterpreterLocatorService { return startOfCompanyName > 0 ? content.substring(0, startOfCompanyName).trim() : content; } - private getEnvironmentRootDirectory(interpreter: string) { - const envDir = interpreter.substring(0, interpreter.length - path.join(...CONDA_RELATIVE_PY_PATH).length); - return path.basename(envDir); - } -} - -export function getEnvironmentsFile() { - const homeDir = IS_WINDOWS ? process.env.USERPROFILE : (process.env.HOME || process.env.HOMEPATH); - return homeDir ? path.join(homeDir, '.conda', 'environments.txt') : ''; } diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index eb67c1c5a044..764320034c3f 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -1,37 +1,27 @@ -import * as fs from 'fs-extra'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + import { inject, injectable } from 'inversify'; -import * as path from 'path'; import { Uri } from 'vscode'; -import { IProcessService } from '../../../common/process/types'; -import { VersionUtils } from '../../../common/versionUtils'; -import { ICondaLocatorService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; -import { AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda'; +import { IFileSystem } from '../../../common/platform/types'; +import { ILogger } from '../../../common/types'; +import { CondaInfo, ICondaService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; import { CondaHelper } from './condaHelper'; @injectable() export class CondaEnvService implements IInterpreterLocatorService { private readonly condaHelper = new CondaHelper(); - constructor( @inject(ICondaLocatorService) private condaLocator: ICondaLocatorService, + constructor( @inject(ICondaService) private condaService: ICondaService, @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, - @inject(IProcessService) private processService: IProcessService) { + @inject(ILogger) private logger: ILogger, + @inject(IFileSystem) private fileSystem: IFileSystem) { } public async getInterpreters(resource?: Uri) { return this.getSuggestionsFromConda(); } // tslint:disable-next-line:no-empty public dispose() { } - public isCondaEnvironment(interpreter: PythonInterpreter) { - return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || - (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0; - } - public getLatestVersion(interpreters: PythonInterpreter[]) { - const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0); - // tslint:disable-next-line:no-non-null-assertion - sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); - if (sortedInterpreters.length > 0) { - return sortedInterpreters[sortedInterpreters.length - 1]; - } - } public async parseCondaInfo(info: CondaInfo) { const condaDisplayName = this.condaHelper.getDisplayName(info); @@ -43,12 +33,11 @@ export class CondaEnvService implements IInterpreterLocatorService { } const promises = envs - .map(async env => { - const envName = path.basename(env); - const pythonPath = path.join(env, ...CONDA_RELATIVE_PY_PATH); + .map(async envPath => { + const pythonPath = this.condaService.getInterpreterPath(envPath); - const existsPromise = fs.pathExists(pythonPath); - const versionPromise = this.versionService.getVersion(pythonPath, envName); + const existsPromise = pythonPath ? this.fileSystem.fileExistsAsync(pythonPath) : Promise.resolve(false); + const versionPromise = this.versionService.getVersion(pythonPath, ''); const [exists, version] = await Promise.all([existsPromise, versionPromise]); if (!exists) { @@ -57,15 +46,13 @@ export class CondaEnvService implements IInterpreterLocatorService { const versionWithoutCompanyName = this.stripCondaDisplayName(this.stripCompanyName(version), condaDisplayName); const displayName = `${condaDisplayName} ${versionWithoutCompanyName}`.trim(); - // If it is an environment, hence suffix with env name. - const interpreterDisplayName = env === info.default_prefix ? displayName : `${displayName} (${envName})`; // tslint:disable-next-line:no-unnecessary-local-variable const interpreter: PythonInterpreter = { path: pythonPath, - displayName: interpreterDisplayName, + displayName, companyDisplayName: AnacondaCompanyName, type: InterpreterType.Conda, - envName + envPath }; return interpreter; }); @@ -99,25 +86,33 @@ export class CondaEnvService implements IInterpreterLocatorService { } } private async getSuggestionsFromConda(): Promise { - return this.condaLocator.getCondaFile() - .then(condaFile => this.processService.exec(condaFile, ['info', '--json'])) - .then(output => output.stdout) - .then(stdout => { - if (stdout.length === 0) { - return []; - } + try { + const info = await this.condaService.getCondaInfo(); + if (!info) { + return []; + } + const interpreters = await this.parseCondaInfo(info); + const environments = await this.condaService.getCondaEnvironments(); + if (Array.isArray(environments) && environments.length > 0) { + interpreters + .forEach(interpreter => { + const environment = environments.find(item => this.fileSystem.arePathsSame(item.path, interpreter!.envPath!)); + if (environment) { + interpreter.envName = environment!.name; + interpreter.displayName = `${interpreter.displayName} (${environment!.name})`; + } + }); + } - try { - const info = JSON.parse(stdout) as CondaInfo; - return this.parseCondaInfo(info); - } catch { - // Failed because either: - // 1. conda is not installed. - // 2. `conda info --json` has changed signature. - // 3. output of `conda info --json` has changed in structure. - // In all cases, we can't offer conda pythonPath suggestions. - return []; - } - }).catch(() => []); + return interpreters; + } catch (ex) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + // 3. output of `conda info --json` has changed in structure. + // In all cases, we can't offer conda pythonPath suggestions. + this.logger.logError('Failed to get Suggestions from conda', ex); + return []; + } } } diff --git a/src/client/interpreter/locators/services/condaHelper.ts b/src/client/interpreter/locators/services/condaHelper.ts index 52ea0cbecd15..5fe8490dd914 100644 --- a/src/client/interpreter/locators/services/condaHelper.ts +++ b/src/client/interpreter/locators/services/condaHelper.ts @@ -1,4 +1,12 @@ -import { AnacondaDisplayName, AnacondaIdentfiers, CondaInfo } from './conda'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import '../../../common/extensions'; +import { CondaInfo } from '../../contracts'; +import { AnacondaDisplayName, AnacondaIdentfiers } from './conda'; + +export type EnvironmentPath = string; +export type EnvironmentName = string; export class CondaHelper { public getDisplayName(condaInfo: CondaInfo = {}): string { @@ -19,17 +27,49 @@ export class CondaHelper { return AnacondaDisplayName; } } + /** + * Parses output returned by the command `conda env list`. + * Sample output is as follows: + * # conda environments: + * # + * base * /Users/donjayamanne/anaconda3 + * one /Users/donjayamanne/anaconda3/envs/one + * one two /Users/donjayamanne/anaconda3/envs/one two + * py27 /Users/donjayamanne/anaconda3/envs/py27 + * py36 /Users/donjayamanne/anaconda3/envs/py36 + * three /Users/donjayamanne/anaconda3/envs/three + * @param {string} condaEnvironmentList + * @param {CondaInfo} condaInfo + * @returns {{ name: string, path: string }[] | undefined} + * @memberof CondaHelper + */ + public parseCondaEnvironmentNames(condaEnvironmentList: string): { name: string, path: string }[] | undefined { + const environments = condaEnvironmentList.splitLines(); + const baseEnvironmentLine = environments.filter(line => line.indexOf('*') > 0); + if (baseEnvironmentLine.length === 0) { + return; + } + const pathStartIndex = baseEnvironmentLine[0].indexOf(baseEnvironmentLine[0].split('*')[1].trim()); + const envs: { name: string, path: string }[] = []; + environments.forEach(line => { + if (line.length <= pathStartIndex) { + return; + } + let name = line.substring(0, pathStartIndex).trim(); + if (name.endsWith('*')) { + name = name.substring(0, name.length - 1).trim(); + } + const envPath = line.substring(pathStartIndex).trim(); + + if (name.length > 0 && envPath.length > 0) { + envs.push({ name, path: envPath }); + } + }); + + return envs; + } private isIdentifiableAsAnaconda(value: string) { const valueToSearch = value.toLowerCase(); return AnacondaIdentfiers.some(item => valueToSearch.indexOf(item.toLowerCase()) !== -1); } - private getPythonVersion(condaInfo: CondaInfo): string | undefined { - // Sample. - // 3.6.2.final.0 (hence just take everything untill the third period). - const pythonVersion = condaInfo.python_version; - if (!pythonVersion) { - return undefined; - } - return pythonVersion.split('.').filter((_, index) => index < 3).join('.'); - } } diff --git a/src/client/interpreter/locators/services/condaLocator.ts b/src/client/interpreter/locators/services/condaLocator.ts deleted file mode 100644 index 0e28c2424238..000000000000 --- a/src/client/interpreter/locators/services/condaLocator.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as fs from 'fs-extra'; -import { inject, injectable, named, optional } from 'inversify'; -import * as path from 'path'; -import { IProcessService } from '../../../common/process/types'; -import { IsWindows } from '../../../common/types'; -import { VersionUtils } from '../../../common/versionUtils'; -import { ICondaLocatorService, IInterpreterLocatorService, PythonInterpreter, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const untildify: (value: string) => string = require('untildify'); - -const KNOWN_CONDA_LOCATIONS = ['~/anaconda/bin/conda', '~/miniconda/bin/conda', - '~/anaconda2/bin/conda', '~/miniconda2/bin/conda', - '~/anaconda3/bin/conda', '~/miniconda3/bin/conda']; - -@injectable() -export class CondaLocatorService implements ICondaLocatorService { - private condaFile: string | undefined; - private isAvailable: boolean | undefined; - constructor( @inject(IsWindows) private isWindows: boolean, - @inject(IProcessService) private processService: IProcessService, - @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService) { - } - // tslint:disable-next-line:no-empty - public dispose() { } - public async getCondaFile(): Promise { - if (this.condaFile) { - return this.condaFile!; - } - const isAvailable = await this.isCondaInCurrentPath(); - if (isAvailable) { - return 'conda'; - } - if (this.isWindows && this.registryLookupForConda) { - return this.registryLookupForConda.getInterpreters() - .then(interpreters => interpreters.filter(this.isCondaEnvironment)) - .then(condaInterpreters => this.getLatestVersion(condaInterpreters)) - .then(condaInterpreter => { - return condaInterpreter ? path.join(path.dirname(condaInterpreter.path), 'conda.exe') : 'conda'; - }) - .then(async condaPath => { - return fs.pathExists(condaPath).then(exists => exists ? condaPath : 'conda'); - }); - } - this.condaFile = await this.getCondaFileFromKnownLocations(); - return this.condaFile!; - } - public async isCondaAvailable(): Promise { - return this.getCondaVersion() - .then(() => this.isAvailable = true) - .catch(() => this.isAvailable = false); - } - public async getCondaVersion(): Promise { - return this.getCondaFile() - .then(condaFile => this.processService.exec(condaFile, ['--version'], {})) - .then(result => result.stdout.trim()) - .catch(() => undefined); - } - public isCondaEnvironment(interpreter: PythonInterpreter) { - return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || - (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0; - } - public getLatestVersion(interpreters: PythonInterpreter[]) { - const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0); - // tslint:disable-next-line:no-non-null-assertion - sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); - if (sortedInterpreters.length > 0) { - return sortedInterpreters[sortedInterpreters.length - 1]; - } - } - public async isCondaInCurrentPath() { - return this.processService.exec('conda', ['--version']) - .then(output => output.stdout.length > 0) - .catch(() => false); - } - private async getCondaFileFromKnownLocations(): Promise { - const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS - .map(untildify) - .map(async (condaPath: string) => fs.pathExists(condaPath).then(exists => exists ? condaPath : ''))); - - const validCondaFiles = condaFiles.filter(condaPath => condaPath.length > 0); - return validCondaFiles.length === 0 ? 'conda' : validCondaFiles[0]; - } -} diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts new file mode 100644 index 000000000000..d7e16bbcbd9c --- /dev/null +++ b/src/client/interpreter/locators/services/condaService.ts @@ -0,0 +1,129 @@ +import { inject, injectable, named, optional } from 'inversify'; +import * as path from 'path'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IProcessService } from '../../../common/process/types'; +import { ILogger } from '../../../common/types'; +import { VersionUtils } from '../../../common/versionUtils'; +import { CondaInfo, ICondaService, IInterpreterLocatorService, PythonInterpreter, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; +import { CondaHelper } from './condaHelper'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +export const KNOWN_CONDA_LOCATIONS = ['~/anaconda/bin/conda', '~/miniconda/bin/conda', + '~/anaconda2/bin/conda', '~/miniconda2/bin/conda', + '~/anaconda3/bin/conda', '~/miniconda3/bin/conda']; + +@injectable() +export class CondaService implements ICondaService { + private condaFile: Promise; + private isAvailable: boolean | undefined; + private readonly condaHelper = new CondaHelper(); + public get condaEnvironmentsFile(): string | undefined { + const homeDir = this.platform.isWindows ? process.env.USERPROFILE : (process.env.HOME || process.env.HOMEPATH); + return homeDir ? path.join(homeDir, '.conda', 'environments.txt') : undefined; + } + constructor( @inject(IProcessService) private processService: IProcessService, + @inject(IPlatformService) private platform: IPlatformService, + @inject(ILogger) private logger: ILogger, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService) { + } + // tslint:disable-next-line:no-empty + public dispose() { } + public async getCondaFile(): Promise { + if (!this.condaFile) { + this.condaFile = this.getCondaFileImpl(); + } + // tslint:disable-next-line:no-unnecessary-local-variable + const condaFile = await this.condaFile!; + return condaFile!; + } + public async isCondaAvailable(): Promise { + if (typeof this.isAvailable === 'boolean') { + return this.isAvailable; + } + return this.getCondaVersion() + .then(version => this.isAvailable = typeof version === 'string') + .catch(() => this.isAvailable = false); + } + public async getCondaVersion(): Promise { + return this.getCondaFile() + .then(condaFile => this.processService.exec(condaFile, ['--version'], {})) + .then(result => result.stdout.trim()) + .catch(() => undefined); + } + public async isCondaInCurrentPath() { + return this.processService.exec('conda', ['--version']) + .then(output => output.stdout.length > 0) + .catch(() => false); + } + public async getCondaInfo(): Promise { + try { + const condaFile = await this.getCondaFile(); + const condaInfo = await this.processService.exec(condaFile, ['info', '--json']).then(output => output.stdout); + + return JSON.parse(condaInfo) as CondaInfo; + } catch (ex) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + this.logger.logError('Failed to get conda info from conda', ex); + } + } + public async getCondaEnvironments(): Promise<({ name: string, path: string }[]) | undefined> { + try { + const condaFile = await this.getCondaFile(); + const envInfo = await this.processService.exec(condaFile, ['env', 'list']).then(output => output.stdout); + return this.condaHelper.parseCondaEnvironmentNames(envInfo); + } catch (ex) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda env list has changed signature. + this.logger.logError('Failed to get conda environment list from conda', ex); + } + } + public getInterpreterPath(condaEnvironmentPath: string): string { + // where to find the Python binary within a conda env. + const relativePath = this.platform.isWindows ? 'python.exe' : path.join('bin', 'python'); + return path.join(condaEnvironmentPath, relativePath); + } + private isCondaEnvironment(interpreter: PythonInterpreter) { + return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || + (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0; + } + private getLatestVersion(interpreters: PythonInterpreter[]) { + const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0); + // tslint:disable-next-line:no-non-null-assertion + sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); + if (sortedInterpreters.length > 0) { + return sortedInterpreters[sortedInterpreters.length - 1]; + } + } + private async getCondaFileImpl() { + const isAvailable = await this.isCondaInCurrentPath(); + if (isAvailable) { + return 'conda'; + } + if (this.platform.isWindows && this.registryLookupForConda) { + return this.registryLookupForConda.getInterpreters() + .then(interpreters => interpreters.filter(this.isCondaEnvironment)) + .then(condaInterpreters => this.getLatestVersion(condaInterpreters)) + .then(condaInterpreter => { + return condaInterpreter ? path.join(path.dirname(condaInterpreter.path), 'conda.exe') : 'conda'; + }) + .then(async condaPath => { + return this.fileSystem.fileExistsAsync(condaPath).then(exists => exists ? condaPath : 'conda'); + }); + } + return this.getCondaFileFromKnownLocations(); + } + private async getCondaFileFromKnownLocations(): Promise { + const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS + .map(untildify) + .map(async (condaPath: string) => this.fileSystem.fileExistsAsync(condaPath).then(exists => exists ? condaPath : ''))); + + const validCondaFiles = condaFiles.filter(condaPath => condaPath.length > 0); + return validCondaFiles.length === 0 ? 'conda' : validCondaFiles[0]; + } +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 699d36471153..de0776a73218 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -7,9 +7,9 @@ import { CONDA_ENV_FILE_SERVICE, CONDA_ENV_SERVICE, CURRENT_PATH_SERVICE, - ICondaEnvironmentFile, - ICondaLocatorService, + ICondaService, IInterpreterLocatorService, + IInterpreterService, IInterpreterVersionService, IKnownSearchPathsForInterpreters, IKnownSearchPathsForVirtualEnvironments, @@ -18,11 +18,12 @@ import { VIRTUAL_ENV_SERVICE, WINDOWS_REGISTRY_SERVICE } from './contracts'; +import { InterpreterManager } from './index'; import { InterpreterVersionService } from './interpreterVersion'; import { PythonInterpreterLocatorService } from './locators/index'; -import { CondaEnvFileService, getEnvironmentsFile } from './locators/services/condaEnvFileService'; +import { CondaEnvFileService } from './locators/services/condaEnvFileService'; import { CondaEnvService } from './locators/services/condaEnvService'; -import { CondaLocatorService } from './locators/services/condaLocator'; +import { CondaService } from './locators/services/condaService'; import { CurrentPathService } from './locators/services/currentPathService'; import { getKnownSearchPathsForInterpreters, KnownPathsService } from './locators/services/KnownPathsService'; import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './locators/services/virtualEnvService'; @@ -33,11 +34,10 @@ import { VEnv } from './virtualEnvs/venv'; import { VirtualEnv } from './virtualEnvs/virtualEnv'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingletonInstance(ICondaEnvironmentFile, getEnvironmentsFile()); serviceManager.addSingletonInstance(IKnownSearchPathsForInterpreters, getKnownSearchPathsForInterpreters()); serviceManager.addSingletonInstance(IKnownSearchPathsForVirtualEnvironments, getKnownSearchPathsForVirtualEnvs()); - serviceManager.addSingleton(ICondaLocatorService, CondaLocatorService); + serviceManager.addSingleton(ICondaService, CondaService); serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VirtualEnv); serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VEnv); @@ -56,4 +56,5 @@ export function registerTypes(serviceManager: IServiceManager) { } else { serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); } + serviceManager.addSingleton(IInterpreterService, InterpreterManager); } diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts index 2928da9b06e7..ddde0fea33ce 100644 --- a/src/client/providers/replProvider.ts +++ b/src/client/providers/replProvider.ts @@ -1,29 +1,38 @@ -import { commands, Disposable, Uri, window } from 'vscode'; +import { Disposable, Uri } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { Commands } from '../common/constants'; -import { IPythonExecutionFactory } from '../common/process/types'; +import { IServiceContainer } from '../ioc/types'; import { captureTelemetry } from '../telemetry'; import { REPL } from '../telemetry/constants'; +import { ICodeExecutionService } from '../terminals/types'; export class ReplProvider implements Disposable { private readonly disposables: Disposable[] = []; - constructor(private pythonExecutionFactory: IPythonExecutionFactory) { + constructor(private serviceContainer: IServiceContainer) { this.registerCommand(); } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); } private registerCommand() { - const disposable = commands.registerCommand(Commands.Start_REPL, this.commandHandler, this); + const commandManager = this.serviceContainer.get(ICommandManager); + const disposable = commandManager.registerCommand(Commands.Start_REPL, this.commandHandler, this); this.disposables.push(disposable); } @captureTelemetry(REPL) private async commandHandler() { - // If we have any active window open, then use that as the uri - const resource: Uri | undefined = window.activeTextEditor ? window.activeTextEditor!.document.uri : undefined; - const executionFactory = await this.pythonExecutionFactory.create(resource); - const pythonInterpreterPath = await executionFactory.getExecutablePath().catch(() => 'python'); - const term = window.createTerminal('Python', pythonInterpreterPath); - term.show(); - this.disposables.push(term); + const resource = this.getActiveResourceUri(); + const replProvider = this.serviceContainer.get(ICodeExecutionService, 'repl'); + await replProvider.initializeRepl(resource); + } + private getActiveResourceUri(): Uri | undefined { + const documentManager = this.serviceContainer.get(IDocumentManager); + if (documentManager.activeTextEditor && !documentManager.activeTextEditor!.document.isUntitled) { + return documentManager.activeTextEditor!.document.uri; + } + const workspace = this.serviceContainer.get(IWorkspaceService); + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + return workspace.workspaceFolders[0].uri; + } } } diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index d2c7f0d72943..d188a2091547 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -7,6 +7,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService } from '../../common/types'; @@ -38,9 +39,8 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi const defaultWorkspace = Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 ? this.workspace.workspaceFolders[0].uri.fsPath : ''; const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; const managePyPath = workspaceRoot.length === 0 ? 'manage.py' : path.join(workspaceRoot, 'manage.py'); - const escapedManagePyPath = managePyPath.indexOf(' ') > 0 ? `"${managePyPath}"` : managePyPath; - args.push(escapedManagePyPath); + args.push(managePyPath.toCommandArgument()); args.push('shell'); return { command, args }; } diff --git a/src/client/terminals/codeExecution/repl.ts b/src/client/terminals/codeExecution/repl.ts new file mode 100644 index 000000000000..f745745213b0 --- /dev/null +++ b/src/client/terminals/codeExecution/repl.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IPlatformService } from '../../common/platform/types'; +import { ITerminalServiceFactory } from '../../common/terminal/types'; +import { IConfigurationService } from '../../common/types'; +import { IDisposableRegistry } from '../../common/types'; +import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; + +@injectable() +export class ReplProvider extends TerminalCodeExecutionProvider { + constructor( @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IDisposableRegistry) disposableRegistry: Disposable[], + @inject(IPlatformService) platformService: IPlatformService) { + + super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + this.terminalTitle = 'REPL'; + } +} diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index ad7c8ef9aeea..a3aaedcc2584 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -7,6 +7,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Disposable, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService } from '../../common/types'; @@ -18,15 +19,6 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { protected terminalTitle: string; private _terminalService: ITerminalService; private replActive?: Promise; - private get terminalService(): ITerminalService { - if (!this._terminalService) { - this._terminalService = this.terminalServiceFactory.getTerminalService(this.terminalTitle); - this.disposables.push(this.terminalService.onDidCloseTerminal(() => { - this.replActive = undefined; - })); - } - return this._terminalService; - } constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @inject(IWorkspaceService) protected readonly workspace: IWorkspaceService, @@ -37,13 +29,12 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async executeFile(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); - this.setCwdForFileExecution(file); + await this.setCwdForFileExecution(file); const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const filePath = file.fsPath.indexOf(' ') > 0 ? `"${file.fsPath}"` : file.fsPath; const launchArgs = pythonSettings.terminal.launchArgs; - this.terminalService.sendCommand(command, launchArgs.concat(filePath)); + await this.getTerminalService(file).sendCommand(command, launchArgs.concat(file.fsPath.toCommandArgument())); } public async execute(code: string, resource?: Uri): Promise { @@ -51,18 +42,40 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { return; } - await this.ensureRepl(); - this.terminalService.sendText(code); + await this.initializeRepl(); + await this.getTerminalService(resource).sendText(code); } + public async initializeRepl(resource?: Uri) { + if (this.replActive && await this.replActive!) { + this._terminalService!.show(); + return; + } + this.replActive = new Promise(async resolve => { + const replCommandArgs = this.getReplCommandArgs(resource); + await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + + // Give python repl time to start before we start sending text. + setTimeout(() => resolve(true), 1000); + }); + await this.replActive; + } public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } { const pythonSettings = this.configurationService.getSettings(resource); const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; const args = pythonSettings.terminal.launchArgs.slice(); return { command, args }; } - - private setCwdForFileExecution(file: Uri) { + private getTerminalService(resource?: Uri): ITerminalService { + if (!this._terminalService) { + this._terminalService = this.terminalServiceFactory.getTerminalService(resource, this.terminalTitle); + this.disposables.push(this._terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + })); + } + return this._terminalService; + } + private async setCwdForFileExecution(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); if (!pythonSettings.terminal.executeInFileDir) { return; @@ -70,23 +83,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { const fileDirPath = path.dirname(file.fsPath); const wkspace = this.workspace.getWorkspaceFolder(file); if (wkspace && fileDirPath !== wkspace.uri.fsPath && fileDirPath.length > 0) { - const escapedPath = fileDirPath.indexOf(' ') > 0 ? `"${fileDirPath}"` : fileDirPath; - this.terminalService.sendText(`cd ${escapedPath}`); + await this.getTerminalService(file).sendText(`cd ${fileDirPath.toCommandArgument()}`); } } - - private async ensureRepl(resource?: Uri) { - if (this.replActive && await this.replActive!) { - return; - } - this.replActive = new Promise(resolve => { - const replCommandArgs = this.getReplCommandArgs(resource); - this.terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); - - // Give python repl time to start before we start sending text. - setTimeout(() => resolve(true), 1000); - }); - - await this.replActive; - } } diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index ef706bf31e51..c9ef59191a78 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -5,6 +5,7 @@ import { IServiceManager } from '../ioc/types'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; +import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from './types'; @@ -13,4 +14,5 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); serviceManager.addSingleton(ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'); serviceManager.addSingleton(ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'); + serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 3573a886d4b6..67c50d4b887b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,6 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; executeFile(file: Uri): Promise; + initializeRepl(resource?: Uri): Promise; } export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); diff --git a/src/test/common/helpers.test.ts b/src/test/common/helpers.test.ts index c1711a2adef1..919bf7c90b2c 100644 --- a/src/test/common/helpers.test.ts +++ b/src/test/common/helpers.test.ts @@ -1,11 +1,3 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// Place this right on top -import { initialize } from './../initialize'; -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module @@ -57,9 +49,11 @@ suite('Deferred', () => { const error = new Error('something is not installed'); assert.equal(isNotInstalledError(error), false, 'Standard error'); + // tslint:disable-next-line:no-any (error as any).code = 'ENOENT'; assert.equal(isNotInstalledError(error), true, 'ENOENT error code not detected'); + // tslint:disable-next-line:no-any (error as any).code = 127; assert.equal(isNotInstalledError(error), true, '127 error code not detected'); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 7730e09da58a..bb7a5d9df28e 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -15,12 +15,10 @@ import { PlatformService } from '../../client/common/platform/platformService'; import { Architecture, IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { CurrentProcess } from '../../client/common/process/currentProcess'; import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; -import { ITerminalService } from '../../client/common/terminal/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../client/common/terminal/types'; import { ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows } from '../../client/common/types'; -import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../client/interpreter/contracts'; +import { ICondaService, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockProvider } from '../interpreters/mocks'; -import { MockCondaLocator } from '../mocks/condaLocator'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -30,6 +28,8 @@ import { closeActiveWindows, initializeTest } from './../initialize'; suite('Module Installer', () => { let ioc: UnitTestIocContainer; let mockTerminalService: TypeMoq.IMock; + let condaService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); suiteSetup(initializeTest); setup(async () => { @@ -43,7 +43,7 @@ suite('Module Installer', () => { }); teardown(async () => { ioc.dispose(); - closeActiveWindows(); + await closeActiveWindows(); }); function initializeDI() { @@ -58,11 +58,16 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton(IInstaller, Installer); mockTerminalService = TypeMoq.Mock.ofType(); - ioc.serviceManager.addSingletonInstance(ITerminalService, mockTerminalService.object); + const mockTerminalFactory = TypeMoq.Mock.ofType(); + mockTerminalFactory.setup(t => t.getTerminalService(TypeMoq.It.isAny())).returns(() => mockTerminalService.object); + ioc.serviceManager.addSingletonInstance(ITerminalServiceFactory, mockTerminalFactory.object); ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); - ioc.serviceManager.addSingleton(ICondaLocatorService, MockCondaLocator); + condaService = TypeMoq.Mock.ofType(); + ioc.serviceManager.addSingletonInstance(ICondaService, condaService.object); + interpreterService = TypeMoq.Mock.ofType(); + ioc.serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); ioc.serviceManager.addSingleton(IPathUtils, PathUtils); ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); ioc.serviceManager.addSingleton(IFileSystem, FileSystem); @@ -86,8 +91,9 @@ suite('Module Installer', () => { } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); - const mockInterpreterLocator = new MockProvider([]); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); const processService = ioc.serviceContainer.get(IProcessService); processService.onExec((file, args, options, callback) => { @@ -117,8 +123,9 @@ suite('Module Installer', () => { test('Ensure pip and conda are supported', async () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); const pythonPath = await getCurrentPythonPath(); - const mockInterpreterLocator = new MockProvider([{ architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: '' }]); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: '' }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); const processService = ioc.serviceContainer.get(IProcessService); processService.onExec((file, args, options, callback) => { @@ -136,14 +143,25 @@ suite('Module Installer', () => { expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => { + const pythonInterpreter: PythonInterpreter = { + path: 'xyz', + version: 'zbc', + type: InterpreterType.Conda + }; + return Promise.resolve(pythonInterpreter); + }); const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); await expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); }); test('Validate pip install arguments', async () => { - const mockInterpreterLocator = new MockProvider([{ path: await getCurrentPythonPath(), type: InterpreterType.Unknown }]); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE); + const interpreterPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: interpreterPath, type: InterpreterType.Unknown }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); const moduleName = 'xyz'; @@ -162,8 +180,10 @@ suite('Module Installer', () => { }); test('Validate Conda install arguments', async () => { - const mockInterpreterLocator = new MockProvider([{ path: await getCurrentPythonPath(), type: InterpreterType.Conda }]); - ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE); + const interpreterPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType(); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: interpreterPath, type: InterpreterType.Conda }])); + ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); const moduleName = 'xyz'; diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts new file mode 100644 index 000000000000..f11873b08afc --- /dev/null +++ b/src/test/common/platform/filesystem.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as TypeMoq from 'typemoq'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; + +// tslint:disable-next-line:max-func-body-length +suite('FileSystem', () => { + let platformService: TypeMoq.IMock; + let fileSystem: IFileSystem; + setup(() => { + platformService = TypeMoq.Mock.ofType(); + fileSystem = new FileSystem(platformService.object); + }); + + test('ReadFile returns contents of a file', async () => { + const file = __filename; + const expectedContents = await fs.readFile(file).then(buffer => buffer.toString()); + const content = await fileSystem.readFile(file); + + expect(content).to.be.equal(expectedContents); + }); + + test('ReadFile throws an exception if file does not exist', async () => { + const readPromise = fs.readFile('xyz', { encoding: 'utf8' }); + await expect(readPromise).to.be.rejectedWith(); + }); + + function caseSensitivityFileCheck(isWindows: boolean, isOsx: boolean, isLinux: boolean) { + platformService.setup(p => p.isWindows).returns(() => isWindows); + platformService.setup(p => p.isMac).returns(() => isOsx); + platformService.setup(p => p.isLinux).returns(() => isLinux); + const path1 = 'c:\\users\\Peter Smith\\my documents\\test.txt'; + const path2 = 'c:\\USERS\\Peter Smith\\my documents\\test.TXT'; + const path3 = 'c:\\USERS\\Peter Smith\\my documents\\test.exe'; + + if (isWindows) { + expect(fileSystem.arePathsSame(path1, path2)).to.be.equal(true, 'file paths do not match (windows)'); + } else { + expect(fileSystem.arePathsSame(path1, path2)).to.be.equal(false, 'file match (non windows)'); + } + + expect(fileSystem.arePathsSame(path1, path1)).to.be.equal(true, '1. file paths do not match'); + expect(fileSystem.arePathsSame(path2, path2)).to.be.equal(true, '2. file paths do not match'); + expect(fileSystem.arePathsSame(path1, path3)).to.be.equal(false, '2. file paths do not match'); + } + + test('Case sensitivity is ignored when comparing file names on windows', async () => { + caseSensitivityFileCheck(true, false, false); + }); + + test('Case sensitivity is not ignored when comparing file names on osx', async () => { + caseSensitivityFileCheck(false, true, false); + }); + + test('Case sensitivity is not ignored when comparing file names on linux', async () => { + caseSensitivityFileCheck(false, false, true); + }); +}); diff --git a/src/test/common/terminals/activation.bash.test.ts b/src/test/common/terminals/activation.bash.test.ts new file mode 100644 index 000000000000..36d9040d118f --- /dev/null +++ b/src/test/common/terminals/activation.bash.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { InterpreterType } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Environment Activation (bash)', () => { + [undefined, 'dummyEnvName'].forEach(environmentName => { + const environmentSuiteTitle = environmentName ? 'When there is no environment Name,' : 'When there is an environment name,'; + suite(environmentSuiteTitle, () => { + ['usr/bin/python', 'usr/bin/env with spaces/env more/python'].forEach(pythonPath => { + const hasSpaces = pythonPath.indexOf(' ') > 0; + const suiteTitle = hasSpaces ? 'and there are spaces in the script file (pythonpath),' : 'and there are no spaces in the script file (pythonpath),'; + suite(suiteTitle, () => { + ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'activate.ps1'].forEach(scriptFileName => { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); + }); + + EnumEx.getNamesAndValues(TerminalShellType).forEach(shellType => { + let isScriptFileSupported = false; + switch (shellType.value) { + case TerminalShellType.bash: { + isScriptFileSupported = ['activate', 'activate.sh'].indexOf(scriptFileName) >= 0; + break; + } + case TerminalShellType.fish: { + isScriptFileSupported = ['activate.fish'].indexOf(scriptFileName) >= 0; + break; + } + case TerminalShellType.cshell: { + isScriptFileSupported = ['activate.csh'].indexOf(scriptFileName) >= 0; + break; + } + default: { + isScriptFileSupported = false; + } + } + const titleTitle = isScriptFileSupported ? `Ensure bash Activation command returns activation command (Shell: ${shellType.name})` : + `Ensure bash Activation command returns undefined (Shell: ${shellType.name})`; + + test(titleTitle, async () => { + const bash = new Bash(serviceContainer.object); + + const supported = bash.isShellSupported(shellType.value); + switch (shellType.value) { + case TerminalShellType.bash: + case TerminalShellType.cshell: + case TerminalShellType.fish: { + expect(supported).to.be.equal(true, `${shellType.name} shell not supported (it should be)`); + break; + } + default: { + expect(supported).to.be.equal(false, `${shellType.name} incorrectly supported (should not be)`); + // No point proceeding with other tests. + return; + } + } + + const pathToScriptFile = path.join(path.dirname(pythonPath), scriptFileName); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands({ path: pythonPath, version: '', type: InterpreterType.Unknown, envName: environmentName }, shellType.value); + + if (isScriptFileSupported) { + // Ensure the script file is of the following form: + // source "" + // Ensure the path is quoted if it contains any spaces. + // Ensure it contains the name of the environment as an argument to the script file. + + const envName = environmentName ? environmentName! : ''; + const quotedScriptFile = pathToScriptFile.indexOf(' ') > 0 ? `"${pathToScriptFile}"` : pathToScriptFile; + expect(command).to.be.deep.equal([`source ${quotedScriptFile} ${envName}`.trim()], 'Invalid command'); + } else { + expect(command).to.be.equal(undefined, 'Command should be undefined'); + } + }); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/src/test/common/terminals/activation.commandPrompt.test.ts b/src/test/common/terminals/activation.commandPrompt.test.ts new file mode 100644 index 000000000000..39ac5193257e --- /dev/null +++ b/src/test/common/terminals/activation.commandPrompt.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { InterpreterType } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Terminal Environment Activation (cmd/powershell)', () => { + [undefined, 'dummyEnvName'].forEach(environmentName => { + const environmentSuiteTitle = environmentName ? 'When there is no environment Name,' : 'When there is an environment name,'; + suite(environmentSuiteTitle, () => { + ['c:/programfiles/python/python', 'c:/program files/python/python'].forEach(pythonPath => { + const hasSpaces = pythonPath.indexOf(' ') > 0; + const suiteTitle = hasSpaces ? 'and there are spaces in the script file (pythonpath),' : 'and there are no spaces in the script file (pythonpath),'; + suite(suiteTitle, () => { + ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'activate.ps1'].forEach(scriptFileName => { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); + }); + + EnumEx.getNamesAndValues(TerminalShellType).forEach(shellType => { + const isScriptFileSupported = ['activate.bat', 'activate.ps1'].indexOf(scriptFileName) >= 0; + const titleTitle = isScriptFileSupported ? `Ensure terminal type is supported (Shell: ${shellType.name})` : + `Ensure terminal type is not supported (Shell: ${shellType.name})`; + + test(titleTitle, async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + const supported = bash.isShellSupported(shellType.value); + switch (shellType.value) { + case TerminalShellType.commandPrompt: + case TerminalShellType.powershell: { + expect(supported).to.be.equal(true, `${shellType.name} shell not supported (it should be)`); + break; + } + default: { + expect(supported).to.be.equal(false, `${shellType.name} incorrectly supported (should not be)`); + } + } + }); + }); + }); + }); + + suite('and script file is activate.bat', () => { + let serviceContainer: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let platform: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + platform = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platform.object); + }); + + test('Ensure batch files are supported by command prompt', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const commands = await bash.getActivationCommands({ path: pythonPath, version: '', type: InterpreterType.Unknown, envName: environmentName }, TerminalShellType.commandPrompt); + + // Ensure the script file is of the following form: + // source "" + // Ensure the path is quoted if it contains any spaces. + // Ensure it contains the name of the environment as an argument to the script file. + + const envName = environmentName ? environmentName! : ''; + const quotedScriptFile = pathToScriptFile.indexOf(' ') > 0 ? `"${pathToScriptFile}"` : pathToScriptFile; + expect(commands).to.be.deep.equal([`${quotedScriptFile} ${envName}`.trim()], 'Invalid command'); + }); + + test('Ensure batch files are supported by powershell (on windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup(p => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands({ path: pythonPath, version: '', type: InterpreterType.Unknown, envName: environmentName }, TerminalShellType.powershell); + + // Executing batch files from powershell requires going back to cmd, then into powershell + + const envName = environmentName ? environmentName! : ''; + const quotedScriptFile = pathToScriptFile.indexOf(' ') > 0 ? `"${pathToScriptFile}"` : pathToScriptFile; + const commands = ['cmd', `${quotedScriptFile} ${envName}`.trim(), 'powershell']; + expect(command).to.be.deep.equal(commands, 'Invalid command'); + }); + + test('Ensure batch files are not supported by powershell (on non-windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup(p => p.isWindows).returns(() => false); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands({ path: pythonPath, version: '', type: InterpreterType.Unknown, envName: environmentName }, TerminalShellType.powershell); + + expect(command).to.be.equal(undefined, 'Invalid command'); + }); + }); + + suite('and script file is activate.ps1', () => { + let serviceContainer: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let platform: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + platform = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platform.object); + }); + + test('Ensure powershell files are supported by command prompt', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup(p => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands({ path: pythonPath, version: '', type: InterpreterType.Unknown, envName: environmentName }, TerminalShellType.commandPrompt); + + const envName = environmentName ? environmentName! : ''; + const quotedScriptFile = pathToScriptFile.indexOf(' ') > 0 ? `"${pathToScriptFile}"` : pathToScriptFile; + expect(command).to.be.deep.equal([`powershell ${quotedScriptFile} ${envName}`.trim()], 'Invalid command'); + }); + + test('Ensure powershell files are supported by powershell', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup(p => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands({ path: pythonPath, version: '', type: InterpreterType.Unknown, envName: environmentName }, TerminalShellType.powershell); + + const envName = environmentName ? environmentName! : ''; + const quotedScriptFile = pathToScriptFile.indexOf(' ') > 0 ? `"${pathToScriptFile}"` : pathToScriptFile; + expect(command).to.be.deep.equal([`${quotedScriptFile} ${envName}`.trim()], 'Invalid command'); + }); + }); + }); + }); + }); + }); +}); diff --git a/src/test/common/terminals/factory.test.ts b/src/test/common/terminals/factory.test.ts index e853b03c0a75..c3fc699708ce 100644 --- a/src/test/common/terminals/factory.test.ts +++ b/src/test/common/terminals/factory.test.ts @@ -2,33 +2,93 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { TerminalServiceFactory } from '../../../client/common/terminal/factory'; +import { ITerminalHelper, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; import { initialize } from '../../initialize'; -import { UnitTestIocContainer } from '../../unittests/serviceRegistry'; // tslint:disable-next-line:max-func-body-length suite('Terminal Service Factory', () => { - let ioc: UnitTestIocContainer; + let factory: ITerminalServiceFactory; + let disposables: Disposable[] = []; + let workspaceService: TypeMoq.IMock; suiteSetup(initialize); setup(() => { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerPlatformTypes(); + const serviceContainer = TypeMoq.Mock.ofType(); + const interpreterService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); + disposables = []; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); + const terminalHelper = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny())).returns(() => terminalHelper.object); + const terminalManager = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny())).returns(() => terminalManager.object); + factory = new TerminalServiceFactory(serviceContainer.object); + + workspaceService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); + + }); + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); }); - teardown(() => ioc.dispose()); test('Ensure same instance of terminal service is returned', () => { - const defaultInstance = ioc.serviceContainer.get(ITerminalService); - const factory = ioc.serviceContainer.get(ITerminalServiceFactory); - const sameInstance = factory.getTerminalService() === defaultInstance; + const instance = factory.getTerminalService(); + const sameInstance = factory.getTerminalService() === instance; expect(sameInstance).to.equal(true, 'Instances are not the same'); + + const differentInstance = factory.getTerminalService(undefined, 'New Title'); + const notTheSameInstance = differentInstance === instance; + expect(notTheSameInstance).not.to.equal(true, 'Instances are the same'); }); test('Ensure different instance of terminal service is returned when title is provided', () => { - const defaultInstance = ioc.serviceContainer.get(ITerminalService); - const factory = ioc.serviceContainer.get(ITerminalServiceFactory); - const instance = factory.getTerminalService('New Title'); - const sameInstance = instance === defaultInstance; - expect(sameInstance).to.not.equal(true, 'Instances are the same'); + const defaultInstance = factory.getTerminalService(); + const notSameAsDefaultInstance = factory.getTerminalService(undefined, 'New Title') === defaultInstance; + expect(notSameAsDefaultInstance).to.not.equal(true, 'Instances are the same as default instance'); + + const instance = factory.getTerminalService(undefined, 'New Title'); + const sameInstance = factory.getTerminalService(undefined, 'New Title') === instance; + expect(sameInstance).to.equal(true, 'Instances are not the same'); + + const differentInstance = factory.getTerminalService(undefined, 'Another New Title'); + const notTheSameInstance = differentInstance === instance; + expect(notTheSameInstance).not.to.equal(true, 'Instances are the same'); + }); + + test('Ensure same terminal is returned when using resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType(); + workspaceFolderA.setup(w => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType(); + workspaceFolderB.setup(w => w.uri).returns(() => workspaceUriB); + + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))).returns(() => workspaceFolderA.object); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))).returns(() => workspaceFolderA.object); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))).returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService(file1A); + const terminalForFile2A = factory.getTerminalService(file2A); + const terminalForFileB = factory.getTerminalService(fileB); + + const terminalsAreSameForWorkspaceA = terminalForFile1A === terminalForFile2A; + expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = terminalForFile1A === terminalForFileB; + expect(terminalsForWorkspaceABAreDifferent).to.equal(false, 'Instances should be different for different workspaces'); }); }); diff --git a/src/test/common/terminals/helper.activation.test.ts b/src/test/common/terminals/helper.activation.test.ts new file mode 100644 index 000000000000..618f355f5d3d --- /dev/null +++ b/src/test/common/terminals/helper.activation.test.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { TerminalHelper } from '../../../client/common/terminal/helper'; +import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Service helpers', () => { + let helper: ITerminalHelper; + let terminalManager: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let disposables: Disposable[] = []; + let serviceContainer: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + + setup(() => { + terminalManager = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + disposables = []; + + serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); + serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); + serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); + serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); + + helper = new TerminalHelper(serviceContainer.object); + }); + teardown(() => { + disposables.filter(item => !!item).forEach(item => item.dispose()); + }); + + test('Activation command is undefined for unknown active interpreter', async () => { + // tslint:disable-next-line:no-any + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined as any)); + const commands = await helper.getEnvironmentActivationCommands(TerminalShellType.other); + + expect(commands).to.equal(undefined, 'Activation command should be undefined if terminal type cannot be determined'); + }); + + test('Activation command is undefined for unknown terminal', async () => { + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => { + const interpreterInfo: PythonInterpreter = { + path: 'python', + version: '', + type: InterpreterType.Unknown + }; + return Promise.resolve(interpreterInfo); + }); + + const bashActivation = new Bash(serviceContainer.object); + const commandPromptActivation = new CommandPromptAndPowerShell(serviceContainer.object); + serviceContainer.setup(c => c.getAll(ITerminalActivationCommandProvider)).returns(() => [bashActivation, commandPromptActivation]); + const commands = await helper.getEnvironmentActivationCommands(TerminalShellType.other); + + expect(commands).to.equal(undefined, 'Activation command should be undefined if terminal type cannot be determined'); + }); +}); + +EnumEx.getNamesAndValues(TerminalShellType).forEach(terminalShell => { + suite(`Terminal Service helpers (${terminalShell.name})`, () => { + let helper: ITerminalHelper; + let terminalManager: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let disposables: Disposable[] = []; + let serviceContainer: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + + setup(() => { + terminalManager = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + disposables = []; + + serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); + serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); + serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); + serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); + + helper = new TerminalHelper(serviceContainer.object); + }); + teardown(() => { + disposables.filter(disposable => !!disposable).forEach(disposable => disposable.dispose()); + }); + + async function activationCommandShouldReturnCorrectly(shellType: TerminalShellType, expectedActivationCommand?: string[]) { + const interpreterInfo: PythonInterpreter = { + path: 'python', + version: '', + type: InterpreterType.Unknown + }; + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreterInfo)); + + // This will only work for the current shell type. + const validProvider = TypeMoq.Mock.ofType(); + validProvider.setup(p => p.isShellSupported(TypeMoq.It.isValue(shellType))).returns(() => true); + validProvider.setup(p => p.getActivationCommands(TypeMoq.It.isValue(interpreterInfo), TypeMoq.It.isValue(shellType))).returns(() => Promise.resolve(expectedActivationCommand)); + + // This will support other providers. + const invalidProvider = TypeMoq.Mock.ofType(); + invalidProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(item => shellType !== shellType); + + serviceContainer.setup(c => c.getAll(ITerminalActivationCommandProvider)).returns(() => [validProvider.object, invalidProvider.object]); + const commands = await helper.getEnvironmentActivationCommands(shellType); + + validProvider.verify(p => p.getActivationCommands(TypeMoq.It.isValue(interpreterInfo), TypeMoq.It.isValue(shellType)), TypeMoq.Times.once()); + validProvider.verify(p => p.isShellSupported(TypeMoq.It.isValue(shellType)), TypeMoq.Times.once()); + invalidProvider.verify(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + invalidProvider.verify(p => p.isShellSupported(TypeMoq.It.isValue(shellType)), TypeMoq.Times.once()); + + expect(commands).to.deep.equal(expectedActivationCommand, 'Incorrect activation command'); + } + + test(`Activation command should be correctly identified for ${terminalShell.name} (command array)`, async () => { + await activationCommandShouldReturnCorrectly(terminalShell.value, ['a', 'b']); + }); + test(`Activation command should be correctly identified for ${terminalShell.name} (command string)`, async () => { + await activationCommandShouldReturnCorrectly(terminalShell.value, ['command to be executed']); + }); + test(`Activation command should be correctly identified for ${terminalShell.name} (undefined)`, async () => { + await activationCommandShouldReturnCorrectly(terminalShell.value); + }); + + async function activationCommandShouldReturnUndefined(shellType: TerminalShellType) { + const interpreterInfo: PythonInterpreter = { + path: 'python', + version: '', + type: InterpreterType.Unknown + }; + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreterInfo)); + + // This will support other providers. + const invalidProvider = TypeMoq.Mock.ofType(); + invalidProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(item => shellType !== shellType); + + serviceContainer.setup(c => c.getAll(ITerminalActivationCommandProvider)).returns(() => [invalidProvider.object]); + const commands = await helper.getEnvironmentActivationCommands(shellType); + + invalidProvider.verify(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + expect(commands).to.deep.equal(undefined, 'Incorrect activation command'); + } + + test(`Activation command should return undefined ${terminalShell.name} (no matching providers)`, async () => { + await activationCommandShouldReturnUndefined(terminalShell.value); + }); + }); +}); diff --git a/src/test/common/terminals/helper.test.ts b/src/test/common/terminals/helper.test.ts index 6feaa886af4d..241de1dc1cc3 100644 --- a/src/test/common/terminals/helper.test.ts +++ b/src/test/common/terminals/helper.test.ts @@ -3,31 +3,44 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { Terminal as VSCodeTerminal, workspace } from 'vscode'; -import { ITerminalManager } from '../../../client/common/application/types'; +import { Disposable, WorkspaceConfiguration } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; import { EnumEx } from '../../../client/common/enumUtils'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalHelper } from '../../../client/common/terminal/helper'; import { ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; -import { initialize, IS_MULTI_ROOT_TEST } from '../../initialize'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; // tslint:disable-next-line:max-func-body-length -suite('Terminal Helper', () => { - let platformService: TypeMoq.IMock; - let terminalManager: TypeMoq.IMock; +suite('Terminal Service helpers', () => { let helper: ITerminalHelper; - suiteSetup(function () { - if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } - return initialize(); - }); + let terminalManager: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let disposables: Disposable[] = []; + let serviceContainer: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + setup(() => { - platformService = TypeMoq.Mock.ofType(); terminalManager = TypeMoq.Mock.ofType(); - helper = new TerminalHelper(platformService.object, terminalManager.object); + platformService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + disposables = []; + + serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); + serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); + serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); + serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); + + helper = new TerminalHelper(serviceContainer.object); + }); + teardown(() => { + disposables.filter(item => !!item).forEach(item => item.dispose()); }); test('Test identification of Terminal Shells', async () => { @@ -39,7 +52,7 @@ suite('Terminal Helper', () => { shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.bash); shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.bash); - shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.bash); + shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.bash); shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); @@ -50,34 +63,37 @@ suite('Terminal Helper', () => { shellPathsAndIdentification.set('c:\\windows\\system32\\shell.exe', TerminalShellType.other); shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); + shellPathsAndIdentification.set('/usr/bin/csh', TerminalShellType.cshell); + shellPathsAndIdentification.forEach((shellType, shellPath) => { expect(helper.identifyTerminalShell(shellPath)).to.equal(shellType, `Incorrect Shell Type for path '${shellPath}'`); }); }); - test('Ensure path for shell is correctly retrieved from settings (osx)', async () => { - const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); + async function ensurePathForShellIsCorrectlyRetrievedFromSettings(os: 'windows' | 'osx' | 'linux', expectedShellPat: string) { + const shellPath = 'abcd'; + workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))).returns(() => { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceConfig.setup(c => c.get(os)).returns(() => shellPath); + return workspaceConfig.object; + }); - platformService.setup(p => p.isWindows).returns(() => false); - platformService.setup(p => p.isLinux).returns(() => false); - platformService.setup(p => p.isMac).returns(() => true); - expect(helper.getTerminalShellPath()).to.equal(shellConfig.get('osx'), 'Incorrect path for Osx'); + platformService.setup(p => p.isWindows).returns(() => os === 'windows'); + platformService.setup(p => p.isLinux).returns(() => os === 'linux'); + platformService.setup(p => p.isMac).returns(() => os === 'osx'); + expect(helper.getTerminalShellPath()).to.equal(shellPath, 'Incorrect path for Osx'); + } + test('Ensure path for shell is correctly retrieved from settings (osx)', async () => { + await ensurePathForShellIsCorrectlyRetrievedFromSettings('osx', 'abcd'); }); test('Ensure path for shell is correctly retrieved from settings (linux)', async () => { - const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); - - platformService.setup(p => p.isWindows).returns(() => false); - platformService.setup(p => p.isLinux).returns(() => true); - platformService.setup(p => p.isMac).returns(() => false); - expect(helper.getTerminalShellPath()).to.equal(shellConfig.get('linux'), 'Incorrect path for Linux'); + await ensurePathForShellIsCorrectlyRetrievedFromSettings('linux', 'abcd'); }); test('Ensure path for shell is correctly retrieved from settings (windows)', async () => { - const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); - - platformService.setup(p => p.isWindows).returns(() => true); - platformService.setup(p => p.isLinux).returns(() => false); - platformService.setup(p => p.isMac).returns(() => false); - expect(helper.getTerminalShellPath()).to.equal(shellConfig.get('windows'), 'Incorrect path for Windows'); + await ensurePathForShellIsCorrectlyRetrievedFromSettings('windows', 'abcd'); + }); + test('Ensure path for shell is correctly retrieved from settings (unknown os)', async () => { + await ensurePathForShellIsCorrectlyRetrievedFromSettings('windows', ''); }); test('Ensure spaces in command is quoted', async () => { @@ -117,19 +133,12 @@ suite('Terminal Helper', () => { }); test('Ensure a terminal is created (without a title)', () => { - const expectedTerminal = { x: 'Dummy' }; - // tslint:disable-next-line:no-any - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => expectedTerminal as any as VSCodeTerminal); helper.createTerminal(); terminalManager.verify(t => t.createTerminal(TypeMoq.It.isValue({ name: undefined })), TypeMoq.Times.once()); }); - test('Ensure a terminal is created with the title provided', () => { - const expectedTerminal = { x: 'Dummy' }; - // tslint:disable-next-line:no-any - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => expectedTerminal as any as VSCodeTerminal); + test('Ensure a terminal is created with the provided title', () => { helper.createTerminal('1234'); terminalManager.verify(t => t.createTerminal(TypeMoq.It.isValue({ name: '1234' })), TypeMoq.Times.once()); }); - }); diff --git a/src/test/common/terminals/service.test.ts b/src/test/common/terminals/service.test.ts index 9cb91f6e7ca1..fe769340faa8 100644 --- a/src/test/common/terminals/service.test.ts +++ b/src/test/common/terminals/service.test.ts @@ -3,29 +3,40 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { Disposable, Terminal as VSCodeTerminal } from 'vscode'; -import { ITerminalManager } from '../../../client/common/application/types'; +import { Disposable, Terminal as VSCodeTerminal, WorkspaceConfiguration } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; import { ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; import { initialize } from '../../initialize'; // tslint:disable-next-line:max-func-body-length suite('Terminal Service', () => { let service: TerminalService; - let helper: TypeMoq.IMock; let terminal: TypeMoq.IMock; let terminalManager: TypeMoq.IMock; + let terminalHelper: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; let disposables: Disposable[] = []; + let mockServiceContainer: TypeMoq.IMock; suiteSetup(initialize); setup(() => { - helper = TypeMoq.Mock.ofType(); terminal = TypeMoq.Mock.ofType(); terminalManager = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + terminalHelper = TypeMoq.Mock.ofType(); disposables = []; - helper.setup(h => h.createTerminal()).returns(() => terminal.object); - helper.setup(h => h.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - helper.setup(h => h.getTerminalShellPath()).returns(() => ''); - helper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAnyString())).returns(() => TerminalShellType.other); + + mockServiceContainer = TypeMoq.Mock.ofType(); + mockServiceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); + mockServiceContainer.setup(c => c.get(ITerminalHelper)).returns(() => terminalHelper.object); + mockServiceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); + mockServiceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); + mockServiceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); }); teardown(() => { if (service) { @@ -36,7 +47,23 @@ suite('Terminal Service', () => { }); test('Ensure terminal is disposed', async () => { - service = new TerminalService(helper.object, terminalManager.object, disposables); + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + const os: string = 'windows'; + service = new TerminalService(mockServiceContainer.object); + const shellPath = 'powershell.exe'; + workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))).returns(() => { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceConfig.setup(c => c.get(os)).returns(() => shellPath); + return workspaceConfig.object; + }); + + platformService.setup(p => p.isWindows).returns(() => os === 'windows'); + platformService.setup(p => p.isLinux).returns(() => os === 'linux'); + platformService.setup(p => p.isMac).returns(() => os === 'osx'); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + terminalHelper.setup(h => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 'dummy text'); + + // Sending a command will cause the terminal to be created await service.sendCommand('', []); terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); @@ -45,11 +72,16 @@ suite('Terminal Service', () => { }); test('Ensure command is sent to terminal and it is shown', async () => { - service = new TerminalService(helper.object, terminalManager.object, disposables); + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); const commandToSend = 'SomeCommand'; const args = ['1', '2']; const commandToExpect = [commandToSend].concat(args).join(' '); - helper.setup(h => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => commandToExpect); + terminalHelper.setup(h => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => commandToExpect); + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + await service.sendCommand(commandToSend, args); terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); @@ -57,15 +89,67 @@ suite('Terminal Service', () => { }); test('Ensure text is sent to terminal and it is shown', async () => { - service = new TerminalService(helper.object, terminalManager.object, disposables); + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); const textToSend = 'Some Text'; + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + await service.sendText(textToSend); terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); terminal.verify(t => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); + test('Ensure terminal shown', async () => { + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(); + + terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); + }); + + test('Ensure terminal is activated once after creation', async () => { + service = new TerminalService(mockServiceContainer.object); + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalHelper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(['activation Command'])); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(); + await service.show(); + await service.show(); + await service.show(); + + terminal.verify(t => t.show(), TypeMoq.Times.exactly(5)); + terminal.verify(t => t.sendText(TypeMoq.It.isValue('activation Command')), TypeMoq.Times.exactly(1)); + }); + + test('Ensure terminal is activated once before sending text', async () => { + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalHelper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(['activation Command'])); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.sendText(textToSend); + await service.sendText(textToSend); + await service.sendText(textToSend); + await service.sendText(textToSend); + + terminal.verify(t => t.show(), TypeMoq.Times.exactly(5)); + terminal.verify(t => t.sendText(TypeMoq.It.isValue('activation Command')), TypeMoq.Times.exactly(1)); + terminal.verify(t => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(4)); + }); + test('Ensure close event is not fired when another terminal is closed', async () => { + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); let eventFired = false; let eventHandler: undefined | (() => void); terminalManager.setup(m => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { @@ -73,8 +157,12 @@ suite('Terminal Service', () => { // tslint:disable-next-line:no-empty return { dispose: () => { } }; }); - service = new TerminalService(helper.object, terminalManager.object, disposables); + service = new TerminalService(mockServiceContainer.object); service.onDidCloseTerminal(() => eventFired = true); + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + // This will create the terminal. await service.sendText('blah'); @@ -84,6 +172,7 @@ suite('Terminal Service', () => { }); test('Ensure close event is not fired when terminal is closed', async () => { + terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); let eventFired = false; let eventHandler: undefined | ((t: VSCodeTerminal) => void); terminalManager.setup(m => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { @@ -91,8 +180,13 @@ suite('Terminal Service', () => { // tslint:disable-next-line:no-empty return { dispose: () => { } }; }); - service = new TerminalService(helper.object, terminalManager.object, disposables); + service = new TerminalService(mockServiceContainer.object); service.onDidCloseTerminal(() => eventFired = true); + + terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); + terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + // This will create the terminal. await service.sendText('blah'); diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.test.ts index 88f3ae8934e3..a610feae1b94 100644 --- a/src/test/interpreters/condaEnvFileService.test.ts +++ b/src/test/interpreters/condaEnvFileService.test.ts @@ -1,71 +1,109 @@ import * as assert from 'assert'; -import * as fs from 'fs-extra'; import { EOL } from 'os'; import * as path from 'path'; -import { IS_WINDOWS } from '../../client/common/utils'; -import { - AnacondaCompanyName, - AnacondaCompanyNames, - AnacondaDisplayName, - CONDA_RELATIVE_PY_PATH -} from '../../client/interpreter/locators/services/conda'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../client/common/platform/types'; +import { ILogger } from '../../client/common/types'; +import { ICondaService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; +import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; import { initialize, initializeTest } from '../initialize'; -import { MockInterpreterVersionProvider } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); +// tslint:disable-next-line:max-func-body-length suite('Interpreters from Conda Environments Text File', () => { + let logger: TypeMoq.IMock; + let condaService: TypeMoq.IMock; + let interpreterVersion: TypeMoq.IMock; + let condaFileProvider: IInterpreterLocatorService; + let fileSystem: TypeMoq.IMock; suiteSetup(initialize); - setup(initializeTest); - suiteTeardown(async () => { - // Clear the file so we don't get unwanted changes prompting for a checkin of this file - await updateEnvWithInterpreters([]); + setup(async () => { + await initializeTest(); + condaService = TypeMoq.Mock.ofType(); + interpreterVersion = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + logger = TypeMoq.Mock.ofType(); + condaFileProvider = new CondaEnvFileService(interpreterVersion.object, condaService.object, fileSystem.object, logger.object); + }); + test('Must return an empty list if environment file cannot be found', async () => { + condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + const interpreters = await condaFileProvider.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); - - async function updateEnvWithInterpreters(envs: string[]) { - await fs.writeFile(environmentsFilePath, envs.join(EOL), { flag: 'w' }); - } test('Must return an empty list for an empty file', async () => { - await updateEnvWithInterpreters([]); - const displayNameProvider = new MockInterpreterVersionProvider('Mock Name'); - const condaFileProvider = new CondaEnvFileService(environmentsFilePath, displayNameProvider); + condaService.setup(c => c.condaEnvironmentsFile).returns(() => environmentsFilePath); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve('')); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); - test('Must return filter files in the list and return valid items', async () => { - const interpreterPaths = [ + + async function filterFilesInEnvironmentsFileAndReturnValidItems(isWindows: boolean) { + const validPaths = [ path.join(environmentsPath, 'conda', 'envs', 'numpy'), - path.join(environmentsPath, 'path1'), - path.join('Invalid and non existent'), - path.join(environmentsPath, 'path2'), - path.join('Another Invalid and non existent') - ]; - await updateEnvWithInterpreters(interpreterPaths); - const displayNameProvider = new MockInterpreterVersionProvider('Mock Name'); - const condaFileProvider = new CondaEnvFileService(environmentsFilePath, displayNameProvider); + path.join(environmentsPath, 'conda', 'envs', 'scipy')]; + const interpreterPaths = [ + path.join(environmentsPath, 'xyz', 'one'), + path.join(environmentsPath, 'xyz', 'two'), + path.join(environmentsPath, 'xyz', 'python.exe') + ].concat(validPaths); + condaService.setup(c => c.condaEnvironmentsFile).returns(() => environmentsFilePath); + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + condaService.setup(c => c.getCondaEnvironments()).returns(() => { + const condaEnvironments = validPaths.map(item => { + return { + path: item, + name: path.basename(item) + }; + }); + return Promise.resolve(condaEnvironments); + }); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); + validPaths.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + + fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(interpreterPaths.join(EOL))); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + const interpreters = await condaFileProvider.getInterpreters(); - // This is because conda environments will be under 'bin/python' however the folders path1 and path2 do not have such files - const numberOfEnvs = IS_WINDOWS ? 3 : 1; - assert.equal(interpreters.length, numberOfEnvs, 'Incorrect number of entries'); + + const expectedPythonPath = isWindows ? path.join(validPaths[0], 'python.exe') : path.join(validPaths[0], 'bin', 'python'); + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} Mock Name (numpy)`, 'Incorrect display name'); assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect display name'); - assert.equal(interpreters[0].path, path.join(interpreterPaths[0], ...CONDA_RELATIVE_PY_PATH), 'Incorrect company display name'); + assert.equal(interpreters[0].path, expectedPythonPath, 'Incorrect path'); + assert.equal(interpreters[0].envPath, validPaths[0], 'Incorrect envpath'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Incorrect type'); + } + test('Must filter files in the list and return valid items (non windows)', async () => { + await filterFilesInEnvironmentsFileAndReturnValidItems(false); + }); + test('Must filter files in the list and return valid items (windows)', async () => { + await filterFilesInEnvironmentsFileAndReturnValidItems(true); }); + test('Must strip company name from version info', async () => { const interpreterPaths = [ path.join(environmentsPath, 'conda', 'envs', 'numpy') ]; - await updateEnvWithInterpreters(interpreterPaths); + condaService.setup(c => c.condaEnvironmentsFile).returns(() => environmentsFilePath); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(interpreterPaths.join(EOL))); AnacondaCompanyNames.forEach(async companyDisplayName => { - const displayNameProvider = new MockInterpreterVersionProvider(`Mock Version :: ${companyDisplayName}`); - const condaFileProvider = new CondaEnvFileService(environmentsFilePath, displayNameProvider); const interpreters = await condaFileProvider.getInterpreters(); - // This is because conda environments will be under 'bin/python' however the folders path1 and path2 do not have such files - const numberOfEnvs = IS_WINDOWS ? 3 : 1; - assert.equal(interpreters.length, numberOfEnvs, 'Incorrect number of entries'); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} Mock Version (numpy)`, 'Incorrect display name'); }); }); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 3ece985b08f8..b775b1a477b1 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -1,27 +1,32 @@ import * as assert from 'assert'; import * as path from 'path'; -import { Uri } from 'vscode'; -import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; -import { IProcessService } from '../../client/common/process/types'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../client/common/platform/types'; +import { ILogger } from '../../client/common/types'; +import { ICondaService, IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; -import { CondaLocatorService } from '../../client/interpreter/locators/services/condaLocator'; import { initialize, initializeTest } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { MockCondaLocatorService, MockInterpreterVersionProvider, MockProvider } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); -const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); // tslint:disable-next-line:max-func-body-length suite('Interpreters from Conda Environments', () => { let ioc: UnitTestIocContainer; - let processService: IProcessService; + let logger: TypeMoq.IMock; + let condaProvider: CondaEnvService; + let condaService: TypeMoq.IMock; + let interpreterVersion: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; suiteSetup(initialize); setup(async () => { await initializeTest(); initializeDI(); + condaService = TypeMoq.Mock.ofType(); + interpreterVersion = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + condaProvider = new CondaEnvService(condaService.object, interpreterVersion.object, logger.object, fileSystem.object); }); teardown(() => ioc.dispose()); function initializeDI() { @@ -29,79 +34,263 @@ suite('Interpreters from Conda Environments', () => { ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); - processService = ioc.serviceContainer.get(IProcessService); + logger = TypeMoq.Mock.ofType(); } test('Must return an empty list for empty json', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); // tslint:disable-next-line:no-any prefer-type-cast const interpreters = await condaProvider.parseCondaInfo({} as any); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); - test('Must extract display name from version info', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); + + async function extractDisplayNameFromVersionInfo(isWindows: boolean) { const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'conda', 'envs', 'scipy')], default_prefix: '', 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' }; + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + info.envs.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 2, 'Incorrect number of entries'); - const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python'); + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + + const path2 = path.join(info.envs[1], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[1].path, path2, 'Incorrect path for first env'); + assert.equal(interpreters[1].displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name for first env'); + assert.equal(interpreters[1].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[1].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + test('Must extract display name from version info (non windows)', async () => { + await extractDisplayNameFromVersionInfo(false); + }); + test('Must extract display name from version info (windows)', async () => { + await extractDisplayNameFromVersionInfo(true); + }); + async function extractDisplayNameFromVersionInfoSuffixedWithEnvironmentName(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy')], + default_prefix: path.join(environmentsPath, 'conda', 'envs', 'root'), + 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + info.envs.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); + condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); + condaService.setup(c => c.getCondaEnvironments()).returns(() => Promise.resolve([ + { name: 'base', path: environmentsPath }, + { name: 'numpy', path: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { name: 'scipy', path: path.join(environmentsPath, 'conda', 'envs', 'scipy') } + ])); + fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); + + const interpreters = await condaProvider.getInterpreters(); + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); assert.equal(interpreters[0].displayName, 'Anaconda 4.4.0 (64-bit) (numpy)', 'Incorrect display name for first env'); assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); - const path2 = path.join(info.envs[1], IS_WINDOWS ? 'python.exe' : 'bin/python'); + const path2 = path.join(info.envs[1], isWindows ? 'python.exe' : path.join('bin', 'python')); assert.equal(interpreters[1].path, path2, 'Incorrect path for first env'); assert.equal(interpreters[1].displayName, 'Anaconda 4.4.0 (64-bit) (scipy)', 'Incorrect display name for first env'); assert.equal(interpreters[1].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[1].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + test('Must extract display name from version info suffixed with the environment name (oxs/linux)', async () => { + await extractDisplayNameFromVersionInfoSuffixedWithEnvironmentName(false); }); - test('Must use the default display name if sys.version is invalid', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); + test('Must extract display name from version info suffixed with the environment name (windows)', async () => { + await extractDisplayNameFromVersionInfoSuffixedWithEnvironmentName(true); + }); + + async function useDefaultNameIfSysVersionIsInvalid(isWindows: boolean) { const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], default_prefix: '', 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' }; + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + info.envs.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); - const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python'); + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, `Anaonda 4.4.0 (64-bit) : ${AnacondaDisplayName}`, 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + test('Must use the default display name if sys.version is invalid (non windows)', async () => { + await useDefaultNameIfSysVersionIsInvalid(false); + }); + test('Must use the default display name if sys.version is invalid (windows)', async () => { + await useDefaultNameIfSysVersionIsInvalid(true); + }); + + async function useDefaultNameIfSysVersionIsValidAndSuffixWithEnvironmentName(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], + default_prefix: '', + 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); + condaService.setup(c => c.getCondaEnvironments()).returns(() => Promise.resolve([ + { name: 'base', path: environmentsPath }, + { name: 'numpy', path: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { name: 'scipy', path: path.join(environmentsPath, 'conda', 'envs', 'scipy') } + ])); + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + info.envs.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); + + const interpreters = await condaProvider.getInterpreters(); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); assert.equal(interpreters[0].displayName, `Anaonda 4.4.0 (64-bit) : ${AnacondaDisplayName} (numpy)`, 'Incorrect display name for first env'); assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + test('Must use the default display name if sys.version is invalid and suffixed with environment name (non windows)', async () => { + await useDefaultNameIfSysVersionIsValidAndSuffixWithEnvironmentName(false); + }); + test('Must use the default display name if sys.version is invalid and suffixed with environment name (windows)', async () => { + await useDefaultNameIfSysVersionIsValidAndSuffixWithEnvironmentName(false); }); - test('Must use the default display name if sys.version is empty', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); + + async function useDefaultNameIfSysVersionIsEmpty(isWindows: boolean) { const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')] }; + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + info.envs.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); - const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python'); + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal(interpreters[0].displayName, `${AnacondaDisplayName}`, 'Incorrect display name for first env'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + + test('Must use the default display name if sys.version is empty (non windows)', async () => { + await useDefaultNameIfSysVersionIsEmpty(false); + }); + test('Must use the default display name if sys.version is empty (windows)', async () => { + await useDefaultNameIfSysVersionIsEmpty(true); + }); + + async function useDefaultNameIfSysVersionIsEmptyAndSuffixWithEnvironmentName(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')] + }; + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + info.envs.forEach(validPath => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); + condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); + condaService.setup(c => c.getCondaEnvironments()).returns(() => Promise.resolve([ + { name: 'base', path: environmentsPath }, + { name: 'numpy', path: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { name: 'scipy', path: path.join(environmentsPath, 'conda', 'envs', 'scipy') } + ])); + fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); + + const interpreters = await condaProvider.getInterpreters(); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} (numpy)`, 'Incorrect display name for first env'); assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + test('Must use the default display name if sys.version is empty and suffixed with environment name (non windows)', async () => { + await useDefaultNameIfSysVersionIsEmptyAndSuffixWithEnvironmentName(false); }); - test('Must include the default_prefix into the list of interpreters', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); + test('Must use the default display name if sys.version is empty and suffixed with environment name (windows)', async () => { + await useDefaultNameIfSysVersionIsEmptyAndSuffixWithEnvironmentName(true); + }); + + async function includeDefaultPrefixIntoListOfInterpreters(isWindows: boolean) { const info = { default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy') }; + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isAny())).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + const pythonPath = isWindows ? path.join(info.default_prefix, 'python.exe') : path.join(info.default_prefix, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); - const path1 = path.join(info.default_prefix, IS_WINDOWS ? 'python.exe' : 'bin/python'); + const path1 = path.join(info.default_prefix, isWindows ? 'python.exe' : path.join('bin', 'python')); assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); assert.equal(interpreters[0].displayName, AnacondaDisplayName, 'Incorrect display name for first env'); assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); + assert.equal(interpreters[0].type, InterpreterType.Conda, 'Environment not detected as a conda environment'); + } + test('Must include the default_prefix into the list of interpreters (non windows)', async () => { + await includeDefaultPrefixIntoListOfInterpreters(false); }); - test('Must exclude interpreters that do not exist on disc', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider(''), processService); + test('Must include the default_prefix into the list of interpreters (windows)', async () => { + await includeDefaultPrefixIntoListOfInterpreters(true); + }); + + async function excludeInterpretersThatDoNotExistOnFileSystem(isWindows: boolean) { const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'path0', 'one.exe'), @@ -110,87 +299,31 @@ suite('Interpreters from Conda Environments', () => { path.join(environmentsPath, 'conda', 'envs', 'scipy'), path.join(environmentsPath, 'path3', 'three.exe')] }; + const validPaths = info.envs.filter((_, index) => index % 2 === 0); + interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + validPaths.forEach(envPath => { + condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isValue(envPath))).returns(environmentPath => { + return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); + }); + const pythonPath = isWindows ? path.join(envPath, 'python.exe') : path.join(envPath, 'bin', 'python'); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + }); + const interpreters = await condaProvider.parseCondaInfo(info); - assert.equal(interpreters.length, 2, 'Incorrect number of entries'); - // Go up one dir for linux (cuz exe is under a sub directory named 'bin') - let path0 = path.dirname(interpreters[0].path); - path0 = IS_WINDOWS ? path0 : path.dirname(path0); - assert.equal(path0, info.envs[0], 'Incorrect path for first env'); - // Go up one dir for linux (cuz exe is under a sub directory named 'bin') - let path1 = path.dirname(interpreters[1].path); - path1 = IS_WINDOWS ? path1 : path.dirname(path1); - assert.equal(path1, info.envs[4], 'Incorrect path for second env'); - }); - test('Must detect conda environments from a list', async () => { - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1', type: InterpreterType.Unknown }, - { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', type: InterpreterType.Unknown }, - { displayName: 'Three', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'Three 3', type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', type: InterpreterType.Unknown }, - { displayName: 'xAnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', type: InterpreterType.Unknown }, - { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'xContinuum Analytics, Inc.', type: InterpreterType.Unknown }, - { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; - const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider(''), processService); - - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[2]), false, '3. Identified environment incorrectly'); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[3]), true, '4. Failed to identify conda environment when displayName starts with \'Anaconda\''); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[4]), true, '5. Failed to identify conda environment when displayName contains text \'Anaconda\''); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[5]), true, '6. Failed to identify conda environment when comanyDisplayName contains \'Continuum\''); - assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[6]), true, '7. Failed to identify conda environment when companyDisplayName starts with \'Continuum\''); - }); - test('Correctly identifies latest version when major version is different', async () => { - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, - { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '3.1.3', type: InterpreterType.Unknown }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, - // tslint:disable-next-line:no-any - { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null, type: InterpreterType.Unknown }, - // tslint:disable-next-line:no-any - { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined, type: InterpreterType.Unknown }, - { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2', type: InterpreterType.Unknown }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; - const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider(''), processService); - - // tslint:disable-next-line:no-non-null-assertion - assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); - }); - test('Correctly identifies latest version when major version is same', async () => { - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, - { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '2.11.3', type: InterpreterType.Unknown }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, - // tslint:disable-next-line:no-any - { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null, type: InterpreterType.Unknown }, - // tslint:disable-next-line:no-any - { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined, type: InterpreterType.Unknown }, - { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2', type: InterpreterType.Unknown }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; - const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider(''), processService); - - // tslint:disable-next-line:no-non-null-assertion - assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); - }); - test('Must use Conda env from Registry to locate conda.exe', async () => { - const condaPythonExePath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments', 'conda', 'Scripts', 'python.exe'); - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; - const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new MockCondaLocatorService(true, processService, mockRegistryProvider, false); - - const condaExe = await condaProvider.getCondaFile(); - assert.equal(condaExe, path.join(path.dirname(condaPythonExePath), 'conda.exe'), 'Failed to identify conda.exe'); + assert.equal(interpreters.length, validPaths.length, 'Incorrect number of entries'); + validPaths.forEach((envPath, index) => { + assert.equal(interpreters[index].envPath!, envPath, 'Incorrect env path'); + const pythonPath = isWindows ? path.join(envPath, 'python.exe') : path.join(envPath, 'bin', 'python'); + assert.equal(interpreters[index].path, pythonPath, 'Incorrect python Path'); + }); + } + + test('Must exclude interpreters that do not exist on disc (non windows)', async () => { + await excludeInterpretersThatDoNotExistOnFileSystem(false); }); + test('Must exclude interpreters that do not exist on disc (windows)', async () => { + await excludeInterpretersThatDoNotExistOnFileSystem(true); + }); + }); diff --git a/src/test/interpreters/condaHelper.test.ts b/src/test/interpreters/condaHelper.test.ts index e289dd2f473f..322a0fed9ffd 100644 --- a/src/test/interpreters/condaHelper.test.ts +++ b/src/test/interpreters/condaHelper.test.ts @@ -1,5 +1,6 @@ import * as assert from 'assert'; -import { AnacondaDisplayName, CondaInfo } from '../../client/interpreter/locators/services/conda'; +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'; diff --git a/src/test/interpreters/condaService.test.ts b/src/test/interpreters/condaService.test.ts new file mode 100644 index 000000000000..7b7a6918b57f --- /dev/null +++ b/src/test/interpreters/condaService.test.ts @@ -0,0 +1,251 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IProcessService } from '../../client/common/process/types'; +import { ILogger } from '../../client/common/types'; +import { IInterpreterLocatorService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { CondaService, KNOWN_CONDA_LOCATIONS } from '../../client/interpreter/locators/services/condaService'; +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters Conda Service', () => { + let logger: TypeMoq.IMock; + let processService: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let condaService: CondaService; + let fileSystem: TypeMoq.IMock; + let registryInterpreterLocatorService: TypeMoq.IMock; + + setup(async () => { + logger = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + registryInterpreterLocatorService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + condaService = new CondaService(processService.object, platformService.object, logger.object, fileSystem.object, registryInterpreterLocatorService.object); + }); + + test('Must use Conda env from Registry to locate conda.exe', async () => { + const condaPythonExePath = path.join('dumyPath', 'environments', 'conda', 'Scripts', 'python.exe'); + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } + ]; + const condaInterpreterIndex = registryInterpreters.findIndex(i => i.displayName === 'Anaconda'); + const expectedCodnaPath = path.join(path.dirname(registryInterpreters[condaInterpreterIndex].path), 'conda.exe'); + platformService.setup(p => p.isWindows).returns(() => true); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); + }); + + test('Must use Conda env from Registry to latest version of locate conda.exe', async () => { + const condaPythonExePath = path.join('dumyPath', 'environments'); + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), companyDisplayName: 'Two 1', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.11', version: '2.11.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.31', version: '2.31.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: '2.21.0', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } + ]; + const indexOfLatestVersion = 3; + const expectedCodnaPath = path.join(path.dirname(registryInterpreters[indexOfLatestVersion].path), 'conda.exe'); + platformService.setup(p => p.isWindows).returns(() => true); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); + }); + + test('Must use \'conda\' if conda.exe cannot be located using registry entries', async () => { + const condaPythonExePath = path.join('dumyPath', 'environments'); + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), companyDisplayName: 'Two 1', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.11', version: '2.11.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.31', version: '2.31.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: '2.21.0', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } + ]; + platformService.setup(p => p.isWindows).returns(() => true); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(false)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); + }); + + test('Must use \'conda\' if is available in the current path', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))).returns(() => Promise.resolve({ stdout: 'xyz' })); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); + + // We should not try to call other unwanted methods. + platformService.verify(p => p.isWindows, TypeMoq.Times.never()); + registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test('Must invoke process only once to check if conda is in the current path', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))).returns(() => Promise.resolve({ stdout: 'xyz' })); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); + processService.verify(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + + // We should not try to call other unwanted methods. + platformService.verify(p => p.isWindows, TypeMoq.Times.never()); + registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); + + await condaService.getCondaFile(); + processService.verify(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + KNOWN_CONDA_LOCATIONS.forEach(knownLocation => { + test(`Must return conda path from known location '${knownLocation}' (non windows)`, async () => { + const expectedCondaLocation = untildify(knownLocation); + platformService.setup(p => p.isWindows).returns(() => false); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCondaLocation)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCondaLocation, 'Failed to identify'); + }); + }); + + test('Must return \'conda\' if conda could not be found in known locations', async () => { + platformService.setup(p => p.isWindows).returns(() => false); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(false)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify'); + }); + + test('Correctly identify interpreter location relative to entironment path (non windows)', async () => { + const environmentPath = path.join('a', 'b', 'c'); + platformService.setup(p => p.isWindows).returns(() => false); + const pythonPath = condaService.getInterpreterPath(environmentPath); + assert.equal(pythonPath, path.join(environmentPath, 'bin', 'python'), 'Incorrect path'); + }); + + test('Correctly identify interpreter location relative to entironment path (windows)', async () => { + const environmentPath = path.join('a', 'b', 'c'); + platformService.setup(p => p.isWindows).returns(() => true); + const pythonPath = condaService.getInterpreterPath(environmentPath); + assert.equal(pythonPath, path.join(environmentPath, 'python.exe'), 'Incorrect path'); + }); + + test('Returns condaInfo when conda exists', async () => { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy')], + default_prefix: '', + 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' + }; + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(info) })); + + const condaInfo = await condaService.getCondaInfo(); + assert.deepEqual(condaInfo, info, 'Conda info does not match'); + }); + + test('Returns undefined if there\'s and error in getting the info', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('unknown'))); + + const condaInfo = await condaService.getCondaInfo(); + assert.equal(condaInfo, undefined, 'Conda info does not match'); + logger.verify(l => l.logError(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + test('Returns conda environments when conda exists', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: '' })); + const environments = await condaService.getCondaEnvironments(); + assert.equal(environments, undefined, 'Conda environments do not match'); + }); + + test('Returns undefined if there\'s and error in getting the info', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('unknown'))); + + const condaInfo = await condaService.getCondaInfo(); + assert.equal(condaInfo, undefined, 'Conda info does not match'); + logger.verify(l => l.logError(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + test('Must use Conda env from Registry to locate conda.exe', async () => { + const condaPythonExePath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments', 'conda', 'Scripts', 'python.exe'); + const registryInterpreters: PythonInterpreter[] = [ + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } + ]; + + const expectedCodaExe = path.join(path.dirname(condaPythonExePath), 'conda.exe'); + + platformService.setup(p => p.isWindows).returns(() => true); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isValue(expectedCodaExe))).returns(() => Promise.resolve(true)); + registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCodaExe, 'Failed to identify conda.exe'); + }); + + test('isAvailable will return true if conda is available', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); + const isAvailable = await condaService.isCondaAvailable(); + assert.equal(isAvailable, true); + }); + + test('isAvailable will return false if conda is not available', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('not found'))); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + platformService.setup(p => p.isWindows).returns(() => false); + + const isAvailable = await condaService.isCondaAvailable(); + assert.equal(isAvailable, false); + }); + + test('Version info from conda process will be returned in getCondaVersion', async () => { + const expectedVersion = new Date().toString(); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: expectedVersion })); + + const version = await condaService.getCondaVersion(); + assert.equal(version, expectedVersion); + }); + + test('isCondaInCurrentPath will return true if conda is available', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); + const isAvailable = await condaService.isCondaInCurrentPath(); + assert.equal(isAvailable, true); + }); + + test('isCondaInCurrentPath will return false if conda is not available', async () => { + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('not found'))); + fileSystem.setup(fs => fs.fileExistsAsync(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + platformService.setup(p => p.isWindows).returns(() => false); + + const isAvailable = await condaService.isCondaInCurrentPath(); + assert.equal(isAvailable, false); + }); + +}); diff --git a/src/test/interpreters/display.multiroot.test.ts b/src/test/interpreters/display.multiroot.test.ts index cee12255a878..3686b7239b81 100644 --- a/src/test/interpreters/display.multiroot.test.ts +++ b/src/test/interpreters/display.multiroot.test.ts @@ -1,8 +1,9 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { IProcessService } from '../../client/common/process/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { clearPythonPathInWorkspaceFolder } from '../common'; @@ -10,7 +11,6 @@ import { closeActiveWindows, initialize, initializePython, initializeTest, IS_MU import { MockStatusBarItem } from '../mockClasses'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockInterpreterVersionProvider } from './mocks'; -import { MockProvider } from './mocks'; const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); @@ -54,11 +54,12 @@ suite('Multiroot Interpreters Display', () => { await window.showTextDocument(document); const statusBar = new MockStatusBarItem(); - const provider = new MockProvider([]); + const provider = TypeMoq.Mock.ofType(); + provider.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + provider.setup(p => p.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); const displayName = `${path.basename(pythonPath)} [Environment]`; const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const processService = ioc.serviceContainer.get(IProcessService); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, provider.object, new VirtualEnvironmentManager([]), displayNameProvider); await display.refresh(); assert.equal(statusBar.text, displayName, 'Incorrect display name'); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index 8cfd46524922..27f2e80648b0 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -2,10 +2,10 @@ import * as assert from 'assert'; import * as child_process from 'child_process'; import { EOL } from 'os'; import * as path from 'path'; +import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { IProcessService } from '../../client/common/process/types'; -import { InterpreterType } from '../../client/interpreter/contracts'; +import { IInterpreterService, InterpreterType } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; @@ -14,14 +14,13 @@ import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } fr import { MockStatusBarItem } from '../mockClasses'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockInterpreterVersionProvider } from './mocks'; -import { MockProvider, MockVirtualEnv } from './mocks'; +import { MockVirtualEnv } from './mocks'; const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); // tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { let ioc: UnitTestIocContainer; - let processService: IProcessService; const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suiteSetup(initialize); setup(async () => { @@ -42,43 +41,47 @@ suite('Interpreters Display', () => { ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); - processService = ioc.serviceContainer.get(IProcessService); } test('Must have command name', () => { const statusBar = new MockStatusBarItem(); const displayNameProvider = new MockInterpreterVersionProvider(''); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); // tslint:disable-next-line:no-unused-expression - new InterpreterDisplay(statusBar, new MockProvider([]), new VirtualEnvironmentManager([]), displayNameProvider, processService); + new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); assert.equal(statusBar.command, 'python.setInterpreter', 'Incorrect command name'); }); test('Must get display name from interpreter itself', async () => { const statusBar = new MockStatusBarItem(); - const provider = new MockProvider([]); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); await display.refresh(); assert.equal(statusBar.text, displayName, 'Incorrect display name'); }); test('Must suffix display name with name of interpreter', async () => { const statusBar = new MockStatusBarItem(); - const provider = new MockProvider([]); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const env1 = new MockVirtualEnv(false, 'Mock 1'); const env2 = new MockVirtualEnv(true, 'Mock 2'); const env3 = new MockVirtualEnv(true, 'Mock 3'); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([env1, env2, env3]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([env1, env2, env3]), displayNameProvider); await display.refresh(); assert.equal(statusBar.text, `${displayName} (${env2.name})`, 'Incorrect display name'); }); test('Must display default \'Display name\' for unknown interpreter', async () => { const statusBar = new MockStatusBarItem(); - const provider = new MockProvider([]); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); // Change interpreter to an invalid value const pythonPath = 'UnknownInterpreter'; await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); @@ -99,10 +102,12 @@ suite('Interpreters Display', () => { { displayName: 'Two', path: pythonPath, type: InterpreterType.VirtualEnv }, { displayName: 'Three', path: 'c:/path3/three.exe', type: InterpreterType.VirtualEnv } ]; - const provider = new MockProvider(interpreters); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters)); + interpreterService.setup(p => p.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters[1])); const displayName = 'Mock Display Name'; const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); await display.refresh(); assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); @@ -120,9 +125,11 @@ suite('Interpreters Display', () => { { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2', type: InterpreterType.VirtualEnv }, { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3', type: InterpreterType.VirtualEnv } ]; - const provider = new MockProvider(interpreters); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters)); + interpreterService.setup(p => p.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters[1])); const displayNameProvider = new MockInterpreterVersionProvider(''); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); await display.refresh(); assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); @@ -135,9 +142,10 @@ suite('Interpreters Display', () => { { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2', type: InterpreterType.VirtualEnv }, { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3', type: InterpreterType.VirtualEnv } ]; - const provider = new MockProvider(interpreters); + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters)); const displayNameProvider = new MockInterpreterVersionProvider('', true); - const display = new InterpreterDisplay(statusBar, provider, new VirtualEnvironmentManager([]), displayNameProvider, processService); + const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); // Change interpreter to an invalid value const pythonPath = 'UnknownInterpreter'; await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index 6fb344f94f27..2bfc9b1cf9a0 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -1,20 +1,8 @@ import { injectable } from 'inversify'; import { Architecture, IRegistry, RegistryHive } from '../../client/common/platform/types'; -import { IProcessService } from '../../client/common/process/types'; -import { IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { CondaLocatorService } from '../../client/interpreter/locators/services/condaLocator'; +import { IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; import { IVirtualEnvironmentIdentifier } from '../../client/interpreter/virtualEnvs/types'; -export class MockProvider implements IInterpreterLocatorService { - constructor(private suggestions: PythonInterpreter[]) { - } - public async getInterpreters(): Promise { - return Promise.resolve(this.suggestions); - } - // tslint:disable-next-line:no-empty - public dispose() { } -} - @injectable() export class MockRegistry implements IRegistry { constructor(private keys: { key: string, hive: RegistryHive, arch?: Architecture, values: string[] }[], @@ -72,16 +60,3 @@ export class MockInterpreterVersionProvider implements IInterpreterVersionServic // tslint:disable-next-line:no-empty public dispose() { } } - -// tslint:disable-next-line:max-classes-per-file -export class MockCondaLocatorService extends CondaLocatorService { - constructor(isWindows: boolean, procService: IProcessService, registryLookupForConda?: IInterpreterLocatorService, private isCondaInEnv?: boolean) { - super(isWindows, procService, registryLookupForConda); - } - public async isCondaInCurrentPath() { - if (typeof this.isCondaInEnv === 'boolean') { - return this.isCondaInEnv; - } - return super.isCondaInCurrentPath(); - } -} diff --git a/src/test/mocks/condaLocator.ts b/src/test/mocks/condaLocator.ts deleted file mode 100644 index 573a4083f9a1..000000000000 --- a/src/test/mocks/condaLocator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { injectable } from 'inversify'; -import { ICondaLocatorService } from '../../client/interpreter/contracts'; - -@injectable() -export class MockCondaLocator implements ICondaLocatorService { - constructor(private condaFile: string = 'conda', private available: boolean = true, private version: string = '1') { } - public async getCondaFile(): Promise { - return this.condaFile; - } - public async isCondaAvailable(): Promise { - return this.available; - } - public async getCondaVersion(): Promise { - return this.version; - } -} diff --git a/src/test/providers/repl.test.ts b/src/test/providers/repl.test.ts new file mode 100644 index 000000000000..784a07868d71 --- /dev/null +++ b/src/test/providers/repl.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { IServiceContainer } from '../../client/ioc/types'; +import { ReplProvider } from '../../client/providers/replProvider'; +import { ICodeExecutionService } from '../../client/terminals/types'; + +// tslint:disable-next-line:max-func-body-length +suite('REPL Provider', () => { + let serviceContainer: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + let workspace: TypeMoq.IMock; + let codeExecutionService: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; + let replProvider: ReplProvider; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + workspace = TypeMoq.Mock.ofType(); + codeExecutionService = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object); + serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspace.object); + serviceContainer.setup(c => c.get(ICodeExecutionService, TypeMoq.It.isValue('repl'))).returns(() => codeExecutionService.object); + serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object); + }); + teardown(() => { + try { + replProvider.dispose(); + // tslint:disable-next-line:no-empty + } catch { } + }); + + test('Ensure command is registered', () => { + replProvider = new ReplProvider(serviceContainer.object); + commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + test('Ensure command handler is disposed', () => { + const disposable = TypeMoq.Mock.ofType(); + commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => disposable.object); + + replProvider = new ReplProvider(serviceContainer.object); + replProvider.dispose(); + + disposable.verify(d => d.dispose(), TypeMoq.Times.once()); + }); + + test('Ensure resource is \'undefined\' if there\s no active document nor a workspace', () => { + const disposable = TypeMoq.Mock.ofType(); + let commandHandler: undefined | (() => void); + commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + + replProvider = new ReplProvider(serviceContainer.object); + expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + commandHandler!.call(replProvider); + + serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); + codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(undefined)), TypeMoq.Times.once()); + }); + + test('Ensure resource is uri of the active document', () => { + const disposable = TypeMoq.Mock.ofType(); + let commandHandler: undefined | (() => void); + commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + const documentUri = Uri.file('a'); + const editor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.uri).returns(() => documentUri); + document.setup(d => d.isUntitled).returns(() => false); + editor.setup(e => e.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); + + replProvider = new ReplProvider(serviceContainer.object); + expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + commandHandler!.call(replProvider); + + serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); + codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(documentUri)), TypeMoq.Times.once()); + }); + + test('Ensure resource is \'undefined\' if the active document is not used if it is untitled (new document)', () => { + const disposable = TypeMoq.Mock.ofType(); + let commandHandler: undefined | (() => void); + commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + const editor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.isUntitled).returns(() => true); + editor.setup(e => e.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); + + replProvider = new ReplProvider(serviceContainer.object); + expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + commandHandler!.call(replProvider); + + serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); + codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(undefined)), TypeMoq.Times.once()); + }); + + test('Ensure first available workspace folder is used if there no document', () => { + const disposable = TypeMoq.Mock.ofType(); + let commandHandler: undefined | (() => void); + commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + + const workspaceUri = Uri.file('a'); + const workspaceFolder = TypeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri).returns(() => workspaceUri); + workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder.object]); + + replProvider = new ReplProvider(serviceContainer.object); + expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + commandHandler!.call(replProvider); + + serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); + codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(workspaceUri)), TypeMoq.Times.once()); + }); +}); diff --git a/src/test/pythonFiles/environments/environments.txt b/src/test/pythonFiles/environments/environments.txt deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/test/terminals/codeExecution/terminalCodeExec.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.test.ts index 0cd5756e9d8c..c6bf45d2277b 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.test.ts @@ -7,237 +7,356 @@ import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { IPlatformService } from '../../../client/common/platform/types'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; +import { ReplProvider } from '../../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExecution/terminalCodeExecution'; import { ICodeExecutionService } from '../../../client/terminals/types'; // tslint:disable-next-line:max-func-body-length -suite('Terminal - Code Execution', () => { - let executor: ICodeExecutionService; - let terminalSettings: TypeMoq.IMock; - let terminalService: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let platform: TypeMoq.IMock; - let workspaceFolder: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let disposables: Disposable[] = []; - setup(() => { - const terminalFactory = TypeMoq.Mock.ofType(); - terminalSettings = TypeMoq.Mock.ofType(); - terminalService = TypeMoq.Mock.ofType(); - const configService = TypeMoq.Mock.ofType(); - workspace = TypeMoq.Mock.ofType(); - platform = TypeMoq.Mock.ofType(); - executor = new TerminalCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); - workspaceFolder = TypeMoq.Mock.ofType(); - - terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); - - settings = TypeMoq.Mock.ofType(); - settings.setup(s => s.terminal).returns(() => terminalSettings.object); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - }); - teardown(() => { - disposables.forEach(disposable => { - if (disposable) { - disposable.dispose(); - } +suite('Terminal Code Execution', () => { + // tslint:disable-next-line:max-func-body-length + ['Terminal Execution', 'Repl Execution', 'Django Execution'].forEach(testSuiteName => { + let terminalSettings: TypeMoq.IMock; + let terminalService: TypeMoq.IMock; + let workspace: TypeMoq.IMock; + let platform: TypeMoq.IMock; + let workspaceFolder: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let disposables: Disposable[] = []; + let executor: ICodeExecutionService; + let expectedTerminalTitle: string | undefined; + let terminalFactory: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let isDjangoRepl: boolean; + + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; }); - disposables = []; - }); - test('Ensure we set current directory before executing file', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); - - executor.executeFile(file); - - terminalService.verify(t => t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.path)}`)), TypeMoq.Times.once()); - }); - - test('Ensure we set current directory (and quote it when containing spaces) before executing file', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); - - executor.executeFile(file); - - terminalService.verify(t => t.sendText(TypeMoq.It.isValue(`cd "${path.dirname(file.path)}"`)), TypeMoq.Times.once()); - }); - - test('Ensure we do not set current directory before executing file if in the same directory', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); - - executor.executeFile(file); - - terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - test('Ensure we do not set current directory before executing file if file is not in a workspace', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - - executor.executeFile(file); - - terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - async function testFileExecution(isWindows: boolean, pythonPath: string, terminalArgs: string[], file: Uri) { - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); - terminalSettings.setup(t => t.executeInFileDir).returns(() => false); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - - await executor.executeFile(file); - const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; - const expectedArgs = terminalArgs.concat(file.fsPath.indexOf(' ') > 0 ? `"${file.fsPath}"` : file.fsPath); - terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); - } - - test('Ensure python file execution script is sent to terminal on windows', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - testFileExecution(true, 'python', [], file); - }); - - test('Ensure python file execution script is sent to terminal on windows with fully qualified python path', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - testFileExecution(true, 'c:\\program files\\python', [], file); - }); - - test('Ensure python file execution script is not quoted when no spaces in file path', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - testFileExecution(true, 'python', [], file); - }); - - test('Ensure python file execution script supports custom python arguments', async () => { - const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - testFileExecution(false, 'python', ['-a', '-b', '-c'], file); - }); - - function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); - - const replCommandArgs = (executor as TerminalCodeExecutionProvider).getReplCommandArgs(); - expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); - expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); - expect(replCommandArgs.args).to.be.deep.equal(terminalArgs, 'Incorrect arguments'); - } - - test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { - const pythonPath = 'c:\\program files\\python\\python.exe'; - const terminalArgs = ['-a', 'b', 'c']; - - testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); - }); + // tslint:disable-next-line:max-func-body-length + setup(() => { + terminalFactory = TypeMoq.Mock.ofType(); + terminalSettings = TypeMoq.Mock.ofType(); + terminalService = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + workspace = TypeMoq.Mock.ofType(); + platform = TypeMoq.Mock.ofType(); + workspaceFolder = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + + settings = TypeMoq.Mock.ofType(); + settings.setup(s => s.terminal).returns(() => terminalSettings.object); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + switch (testSuiteName) { + case 'Terminal Execution': { + executor = new TerminalCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + break; + } + case 'Repl Execution': { + executor = new ReplProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + expectedTerminalTitle = 'REPL'; + break; + } + case 'Django Execution': { + isDjangoRepl = true; + workspace.setup(w => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + // tslint:disable-next-line:no-empty + return { dispose: () => { } }; + }); + executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, documentManager.object, + platform.object, commandManager.object, fileSystem.object, disposables); + expectedTerminalTitle = 'Django Shell'; + break; + } + default: { + break; + } + } + // replExecutor = new TerminalCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + }); - test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { - const pythonPath = 'c:/program files/python/python.exe'; - const terminalArgs = ['-a', 'b', 'c']; + suite(`${testSuiteName} (validation of title)`, () => { + setup(() => { + terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isValue(expectedTerminalTitle))).returns(() => terminalService.object); + }); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); - }); + async function ensureTerminalIsCreatedUponInvokingInitializeRepl(isWindows: boolean, isOsx: boolean, isLinux: boolean) { + platform.setup(p => p.isWindows).returns(() => isWindows); + platform.setup(p => p.isMac).returns(() => isOsx); + platform.setup(p => p.isLinux).returns(() => isLinux); + settings.setup(s => s.pythonPath).returns(() => 'python'); + terminalSettings.setup(t => t.launchArgs).returns(() => []); - test('Ensure python path is returned as is, when building repl args on Windows', async () => { - const pythonPath = 'python'; - const terminalArgs = ['-a', 'b', 'c']; + await executor.initializeRepl(); + } - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); - }); + test('Ensure terminal is created upon invoking initializeRepl (windows)', async () => { + await ensureTerminalIsCreatedUponInvokingInitializeRepl(true, false, false); + }); - test('Ensure fully qualified python path is returned as is, on non Windows', async () => { - const pythonPath = 'usr/bin/python'; - const terminalArgs = ['-a', 'b', 'c']; + test('Ensure terminal is created upon invoking initializeRepl (osx)', async () => { + await ensureTerminalIsCreatedUponInvokingInitializeRepl(false, true, false); + }); - testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); - }); + test('Ensure terminal is created upon invoking initializeRepl (linux)', async () => { + await ensureTerminalIsCreatedUponInvokingInitializeRepl(false, false, true); + }); + }); - test('Ensure python path is returned as is, on non Windows', async () => { - const pythonPath = 'python'; - const terminalArgs = ['-a', 'b', 'c']; + // tslint:disable-next-line:max-func-body-length + suite(testSuiteName, () => { + setup(() => { + terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => terminalService.object); + }); - testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); - }); + async function ensureWeSetCurrentDirectoryBeforeExecutingAFile(isWindows: boolean) { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => 'python'); + terminalSettings.setup(t => t.launchArgs).returns(() => []); - test('Ensure nothing happens when blank text is sent to the terminal', async () => { - await executor.execute(''); - await executor.execute(' '); - // tslint:disable-next-line:no-any - await executor.execute(undefined as any as string); + await executor.executeFile(file); - terminalService.verify(t => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); + terminalService.verify(t => t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.path)}`)), TypeMoq.Times.once()); + } + test('Ensure we set current directory before executing file (non windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingAFile(false); + }); + test('Ensure we set current directory before executing file (windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingAFile(true); + }); + + async function ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(isWindows: boolean) { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => 'python'); + terminalSettings.setup(t => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + terminalService.verify(t => t.sendText(TypeMoq.It.isValue(`cd "${path.dirname(file.path)}"`)), TypeMoq.Times.once()); + } - test('Ensure repl is initialized once before sending text to the repl', async () => { - const pythonPath = 'usr/bin/python1234'; - const terminalArgs = ['-a', 'b', 'c']; - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + test('Ensure we set current directory (and quote it when containing spaces) before executing file (non windows)', async () => { + await ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(false); + }); - await executor.execute('cmd1'); - await executor.execute('cmd2'); - await executor.execute('cmd3'); + test('Ensure we set current directory (and quote it when containing spaces) before executing file (windows)', async () => { + await ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(true); + }); - terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.once()); - }); + async function ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(isWindows: boolean) { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => 'python'); + terminalSettings.setup(t => t.launchArgs).returns(() => []); - test('Ensure repl is re-initialized when temrinal is closed', async () => { - const pythonPath = 'usr/bin/python1234'; - const terminalArgs = ['-a', 'b', 'c']; - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); - - let closeTerminalCallback: undefined | (() => void); - terminalService.setup(t => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((callback => { - closeTerminalCallback = callback; - return { - // tslint:disable-next-line:no-empty - dispose: () => void 0 - }; - })); - - await executor.execute('cmd1'); - await executor.execute('cmd2'); - await executor.execute('cmd3'); - - expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized'); - terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.once()); - - closeTerminalCallback!.call(terminalService.object); - await executor.execute('cmd4'); - terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.exactly(2)); - - closeTerminalCallback!.call(terminalService.object); - await executor.execute('cmd5'); - terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.exactly(3)); - }); + await executor.executeFile(file); - test('Ensure code is sent to terminal', async () => { - const pythonPath = 'usr/bin/python1234'; - const terminalArgs = ['-a', 'b', 'c']; - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + test('Ensure we do not set current directory before executing file if in the same directory (non windows)', async () => { + await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(false); + }); + test('Ensure we do not set current directory before executing file if in the same directory (windows)', async () => { + await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(true); + }); + + async function ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(isWindows: boolean) { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => 'python'); + terminalSettings.setup(t => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + + terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + test('Ensure we do not set current directory before executing file if file is not in a workspace (non windows)', async () => { + await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(false); + }); + test('Ensure we do not set current directory before executing file if file is not in a workspace (windows)', async () => { + await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(true); + }); + + async function testFileExecution(isWindows: boolean, pythonPath: string, terminalArgs: string[], file: Uri) { + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + terminalSettings.setup(t => t.executeInFileDir).returns(() => false); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + await executor.executeFile(file); + const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; + const expectedArgs = terminalArgs.concat(file.fsPath.indexOf(' ') > 0 ? `"${file.fsPath}"` : file.fsPath); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); + } - await executor.execute('cmd1'); - terminalService.verify(t => t.sendText('cmd1'), TypeMoq.Times.once()); + test('Ensure python file execution script is sent to terminal on windows', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + await testFileExecution(true, 'python', [], file); + }); + + test('Ensure python file execution script is sent to terminal on windows with fully qualified python path', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + await testFileExecution(true, 'c:\\program files\\python', [], file); + }); + + test('Ensure python file execution script is not quoted when no spaces in file path', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testFileExecution(true, 'python', [], file); + }); + + test('Ensure python file execution script supports custom python arguments', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testFileExecution(false, 'python', ['-a', '-b', '-c'], file); + }); + + function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; + + const replCommandArgs = (executor as TerminalCodeExecutionProvider).getReplCommandArgs(); + expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); + expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); + } - await executor.execute('cmd2'); - terminalService.verify(t => t.sendText('cmd2'), TypeMoq.Times.once()); + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { + const pythonPath = 'c:\\program files\\python\\python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'python'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { + const pythonPath = 'usr/bin/python'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure python path is returned as is, on non Windows', async () => { + const pythonPath = 'python'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure nothing happens when blank text is sent to the terminal', async () => { + await executor.execute(''); + await executor.execute(' '); + // tslint:disable-next-line:no-any + await executor.execute(undefined as any as string); + + terminalService.verify(t => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test('Ensure repl is initialized once before sending text to the repl', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1'); + await executor.execute('cmd2'); + await executor.execute('cmd3'); + + const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); + }); + + test('Ensure repl is re-initialized when temrinal is closed', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + let closeTerminalCallback: undefined | (() => void); + terminalService.setup(t => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((callback => { + closeTerminalCallback = callback; + return { + // tslint:disable-next-line:no-empty + dispose: () => void 0 + }; + })); + + await executor.execute('cmd1'); + await executor.execute('cmd2'); + await executor.execute('cmd3'); + + const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; + + expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized'); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); + + closeTerminalCallback!.call(terminalService.object); + await executor.execute('cmd4'); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.exactly(2)); + + closeTerminalCallback!.call(terminalService.object); + await executor.execute('cmd5'); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.exactly(3)); + }); + + test('Ensure code is sent to terminal', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1'); + terminalService.verify(t => t.sendText('cmd1'), TypeMoq.Times.once()); + + await executor.execute('cmd2'); + terminalService.verify(t => t.sendText('cmd2'), TypeMoq.Times.once()); + }); + }); }); }); diff --git a/tslint.json b/tslint.json index 7004970518fd..b288b7017c41 100644 --- a/tslint.json +++ b/tslint.json @@ -48,7 +48,7 @@ "variable-name": false, "no-import-side-effect": false, "no-string-based-set-timeout": false, - "no-floating-promises": false, + "no-floating-promises": true, "no-empty-interface": false, "no-bitwise": false, "eofline": true