diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index 18fcff77047a..a114595f1343 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -7,7 +7,7 @@ import { inject, injectable, multiInject } from 'inversify'; import { TextDocument, workspace } from 'vscode'; import { IApplicationDiagnostics } from '../application/types'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; +import { PYTHON_LANGUAGE } from '../common/constants'; import { traceDecorators } from '../common/logger'; import { IDisposable, Resource } from '../common/types'; import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types'; @@ -26,14 +26,14 @@ export class ExtensionActivationManager implements IExtensionActivationManager { @inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService, @inject(IApplicationDiagnostics) private readonly appDiagnostics: IApplicationDiagnostics, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService - ) { } + ) {} public dispose() { while (this.disposables.length > 0) { - const disposable = this.disposables.shift(); + const disposable = this.disposables.shift()!; disposable.dispose(); } - if (this. docOpenedHandler){ + if (this.docOpenedHandler) { this.docOpenedHandler.dispose(); this.docOpenedHandler = undefined; } @@ -41,12 +41,25 @@ export class ExtensionActivationManager implements IExtensionActivationManager { public async activate(): Promise { await this.initialize(); await this.activateWorkspace(this.getActiveResource()); + await this.autoSelection.autoSelectInterpreter(undefined); + } + @traceDecorators.error('Failed to activate a workspace') + public async activateWorkspace(resource: Resource) { + const key = this.getWorkspaceKey(resource); + if (this.activatedWorkspaces.has(key)) { + return; + } + this.activatedWorkspaces.add(key); + // Get latest interpreter list in the background. + this.interpreterService.getInterpreters(resource).ignoreErrors(); + + await this.autoSelection.autoSelectInterpreter(resource); + await Promise.all(this.activationServices.map(item => item.activate(resource))); + await this.appDiagnostics.performPreStartupHealthCheck(resource); } protected async initialize() { - // Get latest interpreter list. - const mainWorkspaceUri = this.getActiveResource(); - this.interpreterService.getInterpreters(mainWorkspaceUri).ignoreErrors(); this.addHandlers(); + this.addRemoveDocOpenedHandlers(); } protected addHandlers() { this.disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); @@ -67,45 +80,32 @@ export class ExtensionActivationManager implements IExtensionActivationManager { this.addRemoveDocOpenedHandlers(); } protected hasMultipleWorkspaces() { - return this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders.length > 1; + return this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders!.length > 1; } protected onDocOpened(doc: TextDocument) { + if (doc.languageId !== PYTHON_LANGUAGE) { + return; + } const key = this.getWorkspaceKey(doc.uri); + // If we have opened a doc that does not belong to workspace, then do nothing. + if (key === '' && this.workspaceService.hasWorkspaceFolders) { + return; + } if (this.activatedWorkspaces.has(key)) { return; } const folder = this.workspaceService.getWorkspaceFolder(doc.uri); this.activateWorkspace(folder ? folder.uri : undefined).ignoreErrors(); } - @traceDecorators.error('Failed to activate a worksapce') - protected async activateWorkspace(resource: Resource) { - const key = this.getWorkspaceKey(resource); - this.activatedWorkspaces.add(key); - - await Promise.all(this.activationServices.map(item => item.activate(resource))); - - // When testing, do not perform health checks, as modal dialogs can be displayed. - if (!isTestExecution()) { - await this.appDiagnostics.performPreStartupHealthCheck(resource); - } - await this.autoSelection.autoSelectInterpreter(resource); - } protected getWorkspaceKey(resource: Resource) { - if (!resource) { - return ''; - } - const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - if (!workspaceFolder) { - return ''; - } - return workspaceFolder.uri.fsPath; + return this.workspaceService.getWorkspaceFolderIdentifier(resource, ''); } private getActiveResource(): Resource { if (this.documentManager.activeTextEditor && !this.documentManager.activeTextEditor.document.isUntitled) { return this.documentManager.activeTextEditor.document.uri; } - return Array.isArray(this.workspaceService.workspaceFolders) && workspace.workspaceFolders.length > 0 - ? workspace.workspaceFolders[0].uri + return Array.isArray(this.workspaceService.workspaceFolders) && workspace.workspaceFolders!.length > 0 + ? workspace.workspaceFolders![0].uri : undefined; } } diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 58a1ad7ce7a3..5dad35d07073 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -22,11 +22,12 @@ type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator }; @injectable() export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable { private currentActivator?: ActivatorInfo; - private activatedOnce: boolean; + private activatedOnce: boolean = false; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; private readonly appShell: IApplicationShell; private readonly lsNotSupportedDiagnosticService: IDiagnosticsService; + private resource!: Resource; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.workspaceService = this.serviceContainer.get(IWorkspaceService); @@ -41,10 +42,11 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); } - public async activate(_resource: Resource): Promise { + public async activate(resource: Resource): Promise { if (this.currentActivator || this.activatedOnce) { return; } + this.resource = resource; this.activatedOnce = true; let jedi = this.useJedi(); @@ -114,10 +116,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv } } private useJedi(): boolean { - const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders - ? this.workspaceService.workspaceFolders!.map(item => item.uri) - : [undefined]; - const configuraionService = this.serviceContainer.get(IConfigurationService); - return workspacesUris.filter(uri => configuraionService.getSettings(uri).jediEnabled).length > 0; + const configurationService = this.serviceContainer.get(IConfigurationService); + return configurationService.getSettings(this.resource).jediEnabled; } } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 694e396a0b6d..1e85c3cad556 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -13,6 +13,7 @@ import { IDisposable, LanguageServerDownloadChannels, Resource } from '../common export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); export interface IExtensionActivationManager extends IDisposable { activate(): Promise; + activateWorkspace(resource: Resource): Promise; } export const IExtensionActivationService = Symbol('IExtensionActivationService'); diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index 126d8138ee3d..725cfe8f8826 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -5,7 +5,7 @@ import { inject, injectable, named } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; -import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; +import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import { ILogger, IOutputChannel, Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IApplicationDiagnostics } from '../types'; @@ -21,6 +21,10 @@ export class ApplicationDiagnostics implements IApplicationDiagnostics { this.serviceContainer.get(ISourceMapSupportService).register(); } public async performPreStartupHealthCheck(resource: Resource): Promise { + // When testing, do not perform health checks, as modal dialogs can be displayed. + if (!isTestExecution()) { + return; + } const services = this.serviceContainer.getAll(IDiagnosticsService); // Perform these validation checks in the foreground. await this.runDiagnostics(services.filter(item => !item.runInBackground), resource); diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 0552eb75ba34..339dda254c99 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -586,7 +586,7 @@ export interface IWorkspaceService { * @returns {string} * @memberof IWorkspaceService */ - getWorkspaceFolderIdentifier(resource: Uri | undefined): string; + getWorkspaceFolderIdentifier(resource: Uri | undefined, defaultValue?: string): string; /** * Returns a path that is relative to the workspace folder or folders. * diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 20a104842a7e..950cc19d5176 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -3,6 +3,7 @@ import { injectable } from 'inversify'; import { CancellationToken, ConfigurationChangeEvent, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { Resource } from '../types'; import { IWorkspaceService } from './types'; @injectable() @@ -37,8 +38,8 @@ export class WorkspaceService implements IWorkspaceService { public findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable { return workspace.findFiles(include, exclude, maxResults, token); } - public getWorkspaceFolderIdentifier(resource: Uri): string { + public getWorkspaceFolderIdentifier(resource: Resource, defaultValue: string = ''): string { const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : ''; + return workspaceFolder ? workspaceFolder.uri.fsPath : defaultValue; } } diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index db0f5e9c55f2..e0016a6919c8 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -65,7 +65,7 @@ export class PythonSettings implements IPythonSettings { return this.changed.event; } - constructor(workspaceFolder: Uri | undefined, private readonly InterpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + constructor(workspaceFolder: Uri | undefined, private readonly interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, workspace?: IWorkspaceService) { this.workspace = workspace || new WorkspaceService(); this.workspaceRoot = workspaceFolder ? workspaceFolder : Uri.file(__dirname); @@ -129,9 +129,9 @@ export class PythonSettings implements IPythonSettings { this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath'))!; // If user has defined a custom value, use it else try to get the best interpreter ourselves. if (this.pythonPath.length === 0 || this.pythonPath === 'python') { - const autoSelectedPythonInterpreter = this.InterpreterAutoSelectionService.getAutoSelectedInterpreter(this.workspaceRoot); + const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter(this.workspaceRoot); if (autoSelectedPythonInterpreter) { - this.InterpreterAutoSelectionService.setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter).ignoreErrors(); + this.interpreterAutoSelectionService.setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter).ignoreErrors(); } this.pythonPath = autoSelectedPythonInterpreter ? autoSelectedPythonInterpreter.path : this.pythonPath; } @@ -382,7 +382,7 @@ export class PythonSettings implements IPythonSettings { // Let's defer the change notification. this.debounceChangeNotification(); }; - this.disposables.push(this.InterpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this))); + this.disposables.push(this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this))); this.disposables.push(this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { if (event.affectsConfiguration('python')) { onDidChange(); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 9881bc317539..a7a92dd913d5 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -73,6 +73,7 @@ export type InterpreterInfomation = { sysVersion: string; architecture: Architecture; sysPrefix: string; + pipEnvWorkspaceFolder?: string; }; export const IPythonExecutionService = Symbol('IPythonExecutionService'); diff --git a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts index 03a929b21ad2..b1a03861140f 100644 --- a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -6,13 +6,17 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IInterpreterService, InterpreterType, IPipEnvService } from '../../../interpreter/contracts'; +import { IWorkspaceService } from '../../application/types'; +import { IFileSystem } from '../../platform/types'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; @injectable() export class PipEnvActivationCommandProvider implements ITerminalActivationCommandProvider { constructor( @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IPipEnvService) private readonly pipenvService: IPipEnvService + @inject(IPipEnvService) private readonly pipenvService: IPipEnvService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IFileSystem) private readonly fs: IFileSystem ) { } public isShellSupported(_targetShell: TerminalShellType): boolean { @@ -24,7 +28,12 @@ export class PipEnvActivationCommandProvider implements ITerminalActivationComma if (!interpreter || interpreter.type !== InterpreterType.Pipenv) { return; } - + // Activate using `pipenv shell` only if the current folder relates pipenv environment. + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + if (workspaceFolder && interpreter.pipEnvWorkspaceFolder && + !this.fs.arePathsSame(workspaceFolder.uri.fsPath, interpreter.pipEnvWorkspaceFolder)) { + return; + } const execName = this.pipenvService.executable; return [`${execName.fileToCommandArgument()} shell`]; } diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index 05f6ce7654c0..b21a056ff12c 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -10,7 +10,8 @@ import { IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; -import { captureTelemetry } from '../../telemetry'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterHelper, PythonInterpreter } from '../contracts'; import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from './types'; @@ -20,6 +21,7 @@ const workspacePathNameForGlobalWorkspaces = ''; @injectable() export class InterpreterAutoSelectionService implements IInterpreterAutoSelectionService { + protected readonly autoSelectedWorkspacePromises = new Map>(); private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); private readonly autoSelectedInterpreterByWorkspace = new Map(); private globallyPreferredInterpreter!: IPersistentState; @@ -65,10 +67,18 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } @captureTelemetry(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { rule: AutoSelectionRule.all }, true) public async autoSelectInterpreter(resource: Resource): Promise { - await this.initializeStore(); - await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); - this.didAutoSelectedInterpreterEmitter.fire(); - Promise.all(this.rules.map(item => item.autoSelectInterpreter(undefined))).ignoreErrors(); + const key = this.getWorkspacePathKey(resource); + if (!this.autoSelectedWorkspacePromises.has(key)) { + const deferred = createDeferred(); + this.autoSelectedWorkspacePromises.set(key, deferred); + await this.initializeStore(resource); + await this.clearWorkspaceStoreIfInvalid(resource); + await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); + this.didAutoSelectedInterpreterEmitter.fire(); + Promise.all(this.rules.map(item => item.autoSelectInterpreter(resource))).ignoreErrors(); + deferred.resolve(); + } + return this.autoSelectedWorkspacePromises.get(key)!.promise; } public get onDidChangeAutoSelectedInterpreter(): Event { return this.didAutoSelectedInterpreterEmitter.event; @@ -90,11 +100,25 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.globallyPreferredInterpreter.value; } public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined) { + // We can only update the stored interpreter once we have done the necessary + // work of auto selecting the interpreters. + if (!this.autoSelectedWorkspacePromises.has(this.getWorkspacePathKey(resource)) || + !this.autoSelectedWorkspacePromises.get(this.getWorkspacePathKey(resource))!.completed) { + return; + } + await this.storeAutoSelectedInterpreter(resource, interpreter); } public async setGlobalInterpreter(interpreter: PythonInterpreter) { await this.storeAutoSelectedInterpreter(undefined, interpreter); } + protected async clearWorkspaceStoreIfInvalid(resource: Resource) { + const stateStore = this.getWorkspaceState(resource); + if (stateStore && stateStore.value && !await this.fs.fileExists(stateStore.value.path)) { + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, {}, { interpreterMissing: true }); + await stateStore.updateValue(undefined); + } + } protected async storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { const workspaceFolderPath = this.getWorkspacePathKey(resource); if (workspaceFolderPath === workspacePathNameForGlobalWorkspaces) { @@ -117,7 +141,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); } } - protected async initializeStore() { + protected async initializeStore(resource: Resource) { + const workspaceFolderPath = this.getWorkspacePathKey(resource); + // Since we're initializing for this resource, + // Ensure any cached information for this workspace have been removed. + this.autoSelectedInterpreterByWorkspace.delete(workspaceFolderPath); if (this.globallyPreferredInterpreter) { return; } @@ -130,8 +158,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } } private getWorkspacePathKey(resource: Resource): string { - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : workspacePathNameForGlobalWorkspaces; + return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); } private getWorkspaceState(resource: Resource): undefined | IPersistentState { const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); diff --git a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts index a6bd68cae4d4..eaac18687904 100644 --- a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts +++ b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts @@ -42,7 +42,7 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe if (pythonPathInConfig.workspaceFolderValue) { return NextAction.runNextRule; } - const pipEnvPromise = createDeferredFromPromise(this.pipEnvInterpreterLocator.getInterpreters(workspacePath.folderUri)); + const pipEnvPromise = createDeferredFromPromise(this.pipEnvInterpreterLocator.getInterpreters(workspacePath.folderUri, true)); const virtualEnvPromise = createDeferredFromPromise(this.getWorkspaceVirtualEnvInterpreters(workspacePath.folderUri)); // Use only one, we currently do not have support for both pipenv and virtual env in same workspace. @@ -61,6 +61,7 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe await this.cacheSelectedInterpreter(workspacePath.folderUri, bestInterpreter); await manager.setWorkspaceInterpreter(workspacePath.folderUri!, bestInterpreter); } + traceVerbose(`Selected Interpreter from ${this.ruleName}, ${bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected'}`); return NextAction.runNextRule; } @@ -85,6 +86,7 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe protected async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { // We should never clear settings in user settings.json. if (!interpreter) { + await super.cacheSelectedInterpreter(resource, interpreter); return; } const activeWorkspace = this.helper.getActiveWorkspaceUri(resource); diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index a854499d4a45..37a236bdaf9e 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -120,7 +120,7 @@ export interface IPipEnvService { export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper'); export interface IInterpreterLocatorHelper { - mergeInterpreters(interpreters: PythonInterpreter[]): PythonInterpreter[]; + mergeInterpreters(interpreters: PythonInterpreter[]): Promise; } export const IInterpreterWatcher = Symbol('IInterpreterWatcher'); diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 6d6c46fe2039..56cd458fdfcc 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -4,6 +4,7 @@ import { IApplicationShell, IWorkspaceService } from '../../common/application/t import '../../common/extensions'; import { IDisposableRegistry, IPathUtils, Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { IInterpreterAutoSelectionService } from '../autoSelection/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, PythonInterpreter } from '../contracts'; // tslint:disable-next-line:completed-docs @@ -16,12 +17,14 @@ export class InterpreterDisplay implements IInterpreterDisplay { private readonly interpreterService: IInterpreterService; private currentlySelectedInterpreterPath?: string; private currentlySelectedWorkspaceFolder: Resource; + private readonly autoSelection: IInterpreterAutoSelectionService; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.helper = serviceContainer.get(IInterpreterHelper); this.workspaceService = serviceContainer.get(IWorkspaceService); this.pathUtils = serviceContainer.get(IPathUtils); this.interpreterService = serviceContainer.get(IInterpreterService); + this.autoSelection = serviceContainer.get(IInterpreterAutoSelectionService); const application = serviceContainer.get(IApplicationShell); const disposableRegistry = serviceContainer.get(IDisposableRegistry); @@ -44,11 +47,12 @@ export class InterpreterDisplay implements IInterpreterDisplay { await this.updateDisplay(resource); } private onDidChangeInterpreterInformation(info: PythonInterpreter) { - if (this.currentlySelectedInterpreterPath === info.path) { + if (!this.currentlySelectedInterpreterPath || this.currentlySelectedInterpreterPath === info.path) { this.updateDisplay(this.currentlySelectedWorkspaceFolder).ignoreErrors(); } } private async updateDisplay(workspaceFolder?: Uri) { + await this.autoSelection.autoSelectInterpreter(workspaceFolder); const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); this.currentlySelectedWorkspaceFolder = workspaceFolder; if (interpreter) { diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 396bee271080..4019ed324ecf 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -7,7 +7,7 @@ import { IDocumentManager, IWorkspaceService } from '../common/application/types import { getArchitectureDisplayName } from '../common/platform/registry'; import { IFileSystem } from '../common/platform/types'; import { IPythonExecutionFactory } from '../common/process/types'; -import { IConfigurationService, IDisposableRegistry, IPersistentState, IPersistentStateFactory } from '../common/types'; +import { IConfigurationService, IDisposableRegistry, IPersistentState, IPersistentStateFactory, Resource } from '../common/types'; import { sleep } from '../common/utils/async'; import { IServiceContainer } from '../ioc/types'; import { captureTelemetry } from '../telemetry'; @@ -28,6 +28,8 @@ export class InterpreterService implements Disposable, IInterpreterService { private readonly configService: IConfigurationService; private readonly didChangeInterpreterEmitter = new EventEmitter(); private readonly didChangeInterpreterInformation = new EventEmitter(); + private readonly inMemoryCacheOfDisplayNames = new Map(); + private readonly updatedInterpreters = new Set(); private pythonPathSetting: string = ''; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { @@ -67,9 +69,9 @@ export class InterpreterService implements Disposable, IInterpreterService { .filter(item => !item.displayName) .map(async item => { item.displayName = await this.getDisplayName(item, resource); - // Always keep information up to date with latest details. + // Keep information up to date with latest details. if (!item.cachedEntry) { - this.updateCachedInterpreterInformation(item).ignoreErrors(); + this.updateCachedInterpreterInformation(item, resource).ignoreErrors(); } })); return interpreters; @@ -147,13 +149,13 @@ export class InterpreterService implements Disposable, IInterpreterService { // tslint:disable-next-line:no-any if (interpreterInfo && (interpreterInfo as any).__store) { - await this.updateCachedInterpreterInformation(interpreterInfo); + await this.updateCachedInterpreterInformation(interpreterInfo, resource); } else { // If we got information from option1, then when option2 finishes cache it for later use (ignoring erors); option2.then(async info => { // tslint:disable-next-line:no-any if (info && (info as any).__store) { - await this.updateCachedInterpreterInformation(info); + await this.updateCachedInterpreterInformation(info, resource); } }).ignoreErrors(); } @@ -168,12 +170,55 @@ export class InterpreterService implements Disposable, IInterpreterService { * @memberof InterpreterService */ public async getDisplayName(info: Partial, resource?: Uri): Promise { + // faster than calculating file has agian and again, only when deailing with cached items. + if (!info.cachedEntry && info.path && this.inMemoryCacheOfDisplayNames.has(info.path)) { + return this.inMemoryCacheOfDisplayNames.get(info.path)!; + } const fileHash = (info.path ? await this.fs.getFileHash(info.path).catch(() => '') : '') || ''; - const interpreterHash = `${fileHash}-${md5(JSON.stringify(info))}`; - const store = this.persistentStateFactory.createGlobalPersistentState<{ hash: string; displayName: string }>(`${info.path}${interpreterHash}.interpreter.displayName.v5`, undefined, EXPITY_DURATION); + // Do not include dipslay name into hash as that changes. + const interpreterHash = `${fileHash}-${md5(JSON.stringify({ ...info, displayName: '' }))}`; + const store = this.persistentStateFactory.createGlobalPersistentState<{ hash: string; displayName: string }>(`${info.path}.interpreter.displayName.v7`, undefined, EXPITY_DURATION); if (store.value && store.value.hash === interpreterHash && store.value.displayName) { + this.inMemoryCacheOfDisplayNames.set(info.path!, store.value.displayName); return store.value.displayName; } + + const displayName = await this.buildInterpreterDisplayName(info, resource); + + // If dealing with cached entry, then do not store the display name in cache. + if (!info.cachedEntry) { + await store.updateValue({ displayName, hash: interpreterHash }); + this.inMemoryCacheOfDisplayNames.set(info.path!, displayName); + } + + return displayName; + } + public async getInterpreterCache(pythonPath: string): Promise> { + const fileHash = (pythonPath ? await this.fs.getFileHash(pythonPath).catch(() => '') : '') || ''; + const store = this.persistentStateFactory.createGlobalPersistentState<{ fileHash: string; info?: PythonInterpreter }>(`${pythonPath}.interpreter.Details.v7`, undefined, EXPITY_DURATION); + if (!store.value || store.value.fileHash !== fileHash) { + await store.updateValue({ fileHash }); + } + return store; + } + protected async updateCachedInterpreterInformation(info: PythonInterpreter, resource: Resource): Promise{ + const key = JSON.stringify(info); + if (this.updatedInterpreters.has(key)) { + return; + } + this.updatedInterpreters.add(key); + const state = await this.getInterpreterCache(info.path); + info.displayName = await this.getDisplayName(info, resource); + // Check if info has indeed changed. + if (state.value && state.value.info && + JSON.stringify(info) === JSON.stringify(state.value.info)) { + return; + } + this.inMemoryCacheOfDisplayNames.delete(info.path); + await state.updateValue({ fileHash: state.value.fileHash, info }); + this.didChangeInterpreterInformation.fire(info); + } + protected async buildInterpreterDisplayName(info: Partial, resource?: Uri): Promise{ const displayNameParts: string[] = ['Python']; const envSuffixParts: string[] = []; @@ -203,27 +248,7 @@ export class InterpreterService implements Disposable, IInterpreterService { const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; - const displayName = `${displayNameParts.join(' ')} ${envSuffix}`.trim(); - - // If dealing with cached entry, then do not store the display name in cache. - if (!info.cachedEntry) { - await store.updateValue({ displayName, hash: interpreterHash }); - } - - return displayName; - } - protected async getInterpreterCache(pythonPath: string): Promise> { - const fileHash = (pythonPath ? await this.fs.getFileHash(pythonPath).catch(() => '') : '') || ''; - const store = this.persistentStateFactory.createGlobalPersistentState<{ fileHash: string; info?: PythonInterpreter }>(`${pythonPath}.interpreter.Details.v6`, undefined, EXPITY_DURATION); - if (!store.value || store.value.fileHash !== fileHash) { - await store.updateValue({ fileHash }); - } - return store; - } - protected async updateCachedInterpreterInformation(info: PythonInterpreter): Promise{ - this.didChangeInterpreterInformation.fire(info); - const state = await this.getInterpreterCache(info.path); - await state.updateValue({ fileHash: state.value.fileHash, info }); + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); } private onConfigChanged = () => { // Check if we actually changed our python path diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts index 401aa02d18f0..1c103577f35d 100644 --- a/src/client/interpreter/locators/helpers.ts +++ b/src/client/interpreter/locators/helpers.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import { IS_WINDOWS } from '../../common/platform/constants'; import { IFileSystem } from '../../common/platform/types'; import { fsReaddirAsync } from '../../common/utils/fs'; -import { IServiceContainer } from '../../ioc/types'; import { IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../contracts'; +import { IPipEnvServiceHelper } from './types'; const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; @@ -19,13 +19,13 @@ export function lookForInterpretersInDirectory(pathToCheck: string): Promise(IFileSystem); + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IPipEnvServiceHelper) private readonly pipEnvServiceHelper: IPipEnvServiceHelper + ) { } - public mergeInterpreters(interpreters: PythonInterpreter[]) { - return interpreters + public async mergeInterpreters(interpreters: PythonInterpreter[]): Promise { + const items = interpreters .map(item => { return { ...item }; }) .map(item => { item.path = path.normalize(item.path); return item; }) .reduce((accumulator, current) => { @@ -58,5 +58,15 @@ export class InterpreterLocatorHelper implements IInterpreterLocatorHelper { } return accumulator; }, []); + // This stuff needs to be fast. + await Promise.all(items.map(async item => { + const info = await this.pipEnvServiceHelper.getPipEnvInfo(item.path); + if (info) { + item.type = InterpreterType.Pipenv; + item.pipEnvWorkspaceFolder = info.workspaceFolder.fsPath; + item.envName = info.envName || item.envName; + } + })); + return items; } } diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index 22a356312d50..ef6a53b653d7 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -3,6 +3,7 @@ import { injectable, unmanaged } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; +import { traceError } from '../../../common/logger'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; import { IServiceContainer } from '../../../ioc/types'; import { IInterpreterHelper, IVirtualEnvironmentsSearchPathProvider, PythonInterpreter } from '../../contracts'; @@ -44,7 +45,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter, resource)))) .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)) .catch((err) => { - console.error('Python Extension (lookForInterpretersInVenvs):', err); + traceError('Python Extension (lookForInterpretersInVenvs):', err); // Ignore exceptions. return [] as PythonInterpreter[]; }); diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index 8d6d281d9d0a..8ddfc9fc6221 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -66,7 +66,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ return deferred.promise; } - const cachedInterpreters = this.getCachedInterpreters(resource); + const cachedInterpreters = ignoreCache ? undefined : this.getCachedInterpreters(resource); return Array.isArray(cachedInterpreters) ? cachedInterpreters : deferred.promise; } protected async addHandlersForInterpreterWatchers(cacheKey: string, resource: Uri | undefined): Promise { diff --git a/src/client/interpreter/locators/services/pipEnvService.ts b/src/client/interpreter/locators/services/pipEnvService.ts index be048873702f..e79132d49453 100644 --- a/src/client/interpreter/locators/services/pipEnvService.ts +++ b/src/client/interpreter/locators/services/pipEnvService.ts @@ -11,6 +11,7 @@ import { IProcessServiceFactory } from '../../../common/process/types'; import { IConfigurationService, ICurrentProcess, ILogger } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { IInterpreterHelper, InterpreterType, IPipEnvService, PythonInterpreter } from '../../contracts'; +import { IPipEnvServiceHelper } from '../types'; import { CacheableLocatorService } from './cacheableLocatorService'; const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; @@ -23,18 +24,20 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer private readonly fs: IFileSystem; private readonly logger: ILogger; private readonly configService: IConfigurationService; + private readonly pipEnvServiceHelper: IPipEnvServiceHelper; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('PipEnvService', serviceContainer); + super('PipEnvService', serviceContainer, true); this.helper = this.serviceContainer.get(IInterpreterHelper); this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); this.workspace = this.serviceContainer.get(IWorkspaceService); this.fs = this.serviceContainer.get(IFileSystem); this.logger = this.serviceContainer.get(ILogger); this.configService = this.serviceContainer.get(IConfigurationService); + this.pipEnvServiceHelper = this.serviceContainer.get(IPipEnvServiceHelper); } // tslint:disable-next-line:no-empty - public dispose() { } + public dispose() {} public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise { // In PipEnv, the name of the cwd is used as a prefix in the virtual env. if (pythonPath.indexOf(`${path.sep}${path.basename(dir)}-`) === -1) { @@ -55,7 +58,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer } return this.getInterpreterFromPipenv(pipenvCwd) - .then(item => item ? [item] : []) + .then(item => (item ? [item] : [])) .catch(() => []); } @@ -70,10 +73,12 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer return; } this._hasInterpreters.resolve(true); + await this.pipEnvServiceHelper.trackWorkspaceFolder(interpreterPath, Uri.file(pipenvCwd)); return { ...(details as PythonInterpreter), path: interpreterPath, - type: InterpreterType.Pipenv + type: InterpreterType.Pipenv, + pipEnvWorkspaceFolder: pipenvCwd }; } @@ -89,12 +94,12 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer private async getInterpreterPathFromPipenv(cwd: string, ignoreErrors = false): Promise { // Quick check before actually running pipenv - if (!await this.checkIfPipFileExists(cwd)) { + if (!(await this.checkIfPipFileExists(cwd))) { return; } try { const pythonPath = await this.invokePipenv('--py', cwd); - return (pythonPath && await this.fs.fileExists(pythonPath)) ? pythonPath : undefined; + return pythonPath && (await this.fs.fileExists(pythonPath)) ? pythonPath : undefined; // tslint:disable-next-line:no-empty } catch (error) { traceError('PipEnv identification failed', error); @@ -103,13 +108,15 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer } const errorMessage = error.message || error; const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --py' failed with ${errorMessage}. Make sure pipenv is on the PATH.`); + appShell.showWarningMessage( + `Workspace contains pipfile but attempt to run 'pipenv --py' failed with ${errorMessage}. Make sure pipenv is on the PATH.` + ); } } private async checkIfPipFileExists(cwd: string): Promise { const currentProcess = this.serviceContainer.get(ICurrentProcess); const pipFileName = currentProcess.env[pipEnvFileNameVariable]; - if (typeof pipFileName === 'string' && await this.fs.fileExists(path.join(cwd, pipFileName))) { + if (typeof pipFileName === 'string' && (await this.fs.fileExists(path.join(cwd, pipFileName)))) { return true; } if (await this.fs.fileExists(path.join(cwd, 'Pipfile'))) { @@ -139,13 +146,18 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer LC_ALL: currentProc.env.LC_ALL, LANG: currentProc.env.LANG }; - enviromentVariableValues[platformService.pathVariableName] = currentProc.env[platformService.pathVariableName]; + enviromentVariableValues[platformService.pathVariableName] = + currentProc.env[platformService.pathVariableName]; this.logger.logWarning('Error in invoking PipEnv', error); - this.logger.logWarning(`Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}`); + this.logger.logWarning( + `Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}` + ); const errorMessage = error.message || error; const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --venv' failed with '${errorMessage}'. Make sure pipenv is on the PATH.`); + appShell.showWarningMessage( + `Workspace contains pipfile but attempt to run 'pipenv --venv' failed with '${errorMessage}'. Make sure pipenv is on the PATH.` + ); } } } diff --git a/src/client/interpreter/locators/services/pipEnvServiceHelper.ts b/src/client/interpreter/locators/services/pipEnvServiceHelper.ts new file mode 100644 index 000000000000..490c54e3c47d --- /dev/null +++ b/src/client/interpreter/locators/services/pipEnvServiceHelper.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentState, IPersistentStateFactory } from '../../../common/types'; +import { IPipEnvServiceHelper } from '../types'; + +type PipEnvInformation = { pythonPath: string; workspaceFolder: string; envName: string }; +@injectable() +export class PipEnvServiceHelper implements IPipEnvServiceHelper { + private initialized = false; + private readonly state: IPersistentState>; + constructor( + @inject(IPersistentStateFactory) private readonly statefactory: IPersistentStateFactory, + @inject(IFileSystem) private readonly fs: IFileSystem + ) { + this.state = this.statefactory.createGlobalPersistentState>( + 'PipEnvInformation', + [] + ); + } + public async getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string} | undefined> { + await this.initializeStateStore(); + const info = this.state.value.find(item => this.fs.arePathsSame(item.pythonPath, pythonPath)); + return info ? { workspaceFolder: Uri.file(info.workspaceFolder), envName: info.envName } : undefined; + } + public async trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise { + await this.initializeStateStore(); + const values = [...this.state.value].filter(item => !this.fs.arePathsSame(item.pythonPath, pythonPath)); + const envName = path.basename(workspaceFolder.fsPath); + values.push({ pythonPath, workspaceFolder: workspaceFolder.fsPath, envName }); + await this.state.updateValue(values); + } + protected async initializeStateStore() { + if (this.initialized) { + return; + } + const list = await Promise.all( + this.state.value.map(async item => ((await this.fs.fileExists(item.pythonPath)) ? item : undefined)) + ); + const filteredList = list.filter(item => !!item) as PipEnvInformation[]; + await this.state.updateValue(filteredList); + this.initialized = true; + } +} diff --git a/src/client/interpreter/locators/types.ts b/src/client/interpreter/locators/types.ts index d6cefcec2e6b..d6f76b751761 100644 --- a/src/client/interpreter/locators/types.ts +++ b/src/client/interpreter/locators/types.ts @@ -3,7 +3,14 @@ 'use strict'; +import { Uri } from 'vscode'; + export const IPythonInPathCommandProvider = Symbol('IPythonInPathCommandProvider'); export interface IPythonInPathCommandProvider { getCommands(): { command: string; args?: string[] }[]; } +export const IPipEnvServiceHelper = Symbol('IPipEnvServiceHelper'); +export interface IPipEnvServiceHelper { + getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string} | undefined>; + trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 27b9173e21e8..ea5b0c37d732 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -61,10 +61,11 @@ import { GlobalVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvService } import { InterpreterWatcherBuilder } from './locators/services/interpreterWatcherBuilder'; import { KnownPathsService, KnownSearchPathsForInterpreters } from './locators/services/KnownPathsService'; import { PipEnvService } from './locators/services/pipEnvService'; +import { PipEnvServiceHelper } from './locators/services/pipEnvServiceHelper'; import { WindowsRegistryService } from './locators/services/windowsRegistryService'; import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService'; import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspaceVirtualEnvWatcherService'; -import { IPythonInPathCommandProvider } from './locators/types'; +import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from './locators/types'; import { VirtualEnvironmentManager } from './virtualEnvs/index'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; @@ -74,6 +75,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); serviceManager.addSingleton(ICondaService, CondaService); + serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); serviceManager.addSingleton(IVirtualEnvironmentManager, VirtualEnvironmentManager); serviceManager.addSingleton(IPythonInPathCommandProvider, PythonInPathCommandProvider); diff --git a/src/client/terminals/activation.ts b/src/client/terminals/activation.ts index 131840c83489..a2b0dc1eff42 100644 --- a/src/client/terminals/activation.ts +++ b/src/client/terminals/activation.ts @@ -4,25 +4,42 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Disposable, Terminal } from 'vscode'; -import { ITerminalManager } from '../common/application/types'; +import { Terminal } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../common/application/types'; import { ITerminalActivator } from '../common/terminal/types'; -import { IDisposableRegistry } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { ITerminalAutoActivation } from './types'; @injectable() export class TerminalAutoActivation implements ITerminalAutoActivation { - constructor(@inject(IServiceContainer) private container: IServiceContainer, - @inject(ITerminalActivator) private readonly activator: ITerminalActivator) { + private handler?: IDisposable; + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(ITerminalActivator) private readonly activator: ITerminalActivator, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + ) { + disposableRegistry.push(this); + } + public dispose() { + if (this.handler) { + this.handler.dispose(); + this.handler = undefined; + } } public register() { - const manager = this.container.get(ITerminalManager); - const disposables = this.container.get(IDisposableRegistry); - const disposable = manager.onDidOpenTerminal(this.activateTerminal, this); - disposables.push(disposable); + if (this.handler) { + return; + } + this.handler = this.terminalManager.onDidOpenTerminal(this.activateTerminal, this); } private async activateTerminal(terminal: Terminal): Promise { - await this.activator.activateEnvironmentInTerminal(terminal, undefined); + // If we have just one workspace, then pass that as the resource. + // Until upstream VSC issue is resolved https://github.com/Microsoft/vscode/issues/63052. + const workspaceFolder = + this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders!.length > 0 + ? this.workspaceService.workspaceFolders![0].uri + : undefined; + await this.activator.activateEnvironmentInTerminal(terminal, workspaceFolder); } } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index d47d4b612e51..bb019956af28 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { TextEditor, Uri } from 'vscode'; +import { IDisposable } from '../common/types'; export const ICodeExecutionService = Symbol('ICodeExecutionService'); @@ -27,6 +28,6 @@ export interface ICodeExecutionManager { } export const ITerminalAutoActivation = Symbol('ITerminalAutoActivation'); -export interface ITerminalAutoActivation { +export interface ITerminalAutoActivation extends IDisposable { register(): void; } diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 8cb339de3be8..d1e295afa07c 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -13,12 +13,14 @@ import { IExtensionActivationService } from '../../client/activation/types'; import { IApplicationDiagnostics } from '../../client/application/types'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; -import { IDisposable, Resource } from '../../client/common/types'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { IDisposable } from '../../client/common/types'; import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { sleep } from '../core'; -// tslint:disable-next-line:max-func-body-length +// tslint:disable:max-func-body-length no-any suite('Activation - ActivationManager', () => { class ExtensionActivationManagerTest extends ExtensionActivationManager { // tslint:disable-next-line:no-unnecessary-override @@ -30,10 +32,6 @@ suite('Activation - ActivationManager', () => { return super.initialize(); } // tslint:disable-next-line:no-unnecessary-override - public async activateWorkspace(resource: Resource) { - await super.activateWorkspace(resource); - } - // tslint:disable-next-line:no-unnecessary-override public addRemoveDocOpenedHandlers() { super.addRemoveDocOpenedHandlers(); } @@ -46,11 +44,7 @@ suite('Activation - ActivationManager', () => { let documentManager: typemoq.IMock; let activationService1: IExtensionActivationService; let activationService2: IExtensionActivationService; - const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; - const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; setup(() => { - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; workspaceService = mock(WorkspaceService); appDiagnostics = typemoq.Mock.ofType(); autoSelection = typemoq.Mock.ofType(); @@ -58,37 +52,73 @@ suite('Activation - ActivationManager', () => { documentManager = typemoq.Mock.ofType(); activationService1 = mock(LanguageServerExtensionActivationService); activationService2 = mock(LanguageServerExtensionActivationService); - managerTest = new ExtensionActivationManagerTest([instance(activationService1), instance(activationService2)], + managerTest = new ExtensionActivationManagerTest( + [instance(activationService1), instance(activationService2)], documentManager.object, instance(interpreterService), autoSelection.object, appDiagnostics.object, - instance(workspaceService)); - }); - teardown(() => { - process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; - process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + instance(workspaceService) + ); }); test('Initialize will add event handlers and will dispose them when running dispose', async () => { const disposable = typemoq.Mock.ofType(); + const disposable2 = typemoq.Mock.ofType(); when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); + when(workspaceService.workspaceFolders).thenReturn([1 as any, 2 as any]); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const eventDef = () => disposable2.object; + documentManager.setup(d => d.onDidOpenTextDocument).returns(() => eventDef).verifiable(typemoq.Times.once()); - when(workspaceService.workspaceFolders).thenReturn([]); - when(interpreterService.getInterpreters(undefined)).thenResolve([]); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined).verifiable(typemoq.Times.once()); await managerTest.initialize(); - verify(workspaceService.onDidChangeWorkspaceFolders).once(); verify(workspaceService.workspaceFolders).once(); - verify(interpreterService.getInterpreters(undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); documentManager.verifyAll(); disposable.setup(d => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); managerTest.dispose(); disposable.verifyAll(); + disposable2.verifyAll(); + }); + test('Remove text document opened handler if there is only one workspace', async () => { + const disposable = typemoq.Mock.ofType(); + const disposable2 = typemoq.Mock.ofType(); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); + when(workspaceService.workspaceFolders).thenReturn([1 as any, 2 as any]); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const eventDef = () => disposable2.object; + documentManager.setup(d => d.onDidOpenTextDocument).returns(() => eventDef).verifiable(typemoq.Times.once()); + disposable.setup(d => d.dispose()); + disposable2.setup(d => d.dispose()); + + await managerTest.initialize(); + + verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + documentManager.verifyAll(); + disposable.verify(d => d.dispose(), typemoq.Times.never()); + disposable2.verify(d => d.dispose(), typemoq.Times.never()); + + when(workspaceService.workspaceFolders).thenReturn([]); + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + + await managerTest.initialize(); + + verify(workspaceService.hasWorkspaceFolders).twice(); + disposable.verify(d => d.dispose(), typemoq.Times.never()); + disposable2.verify(d => d.dispose(), typemoq.Times.once()); + + managerTest.dispose(); + + disposable.verify(d => d.dispose(), typemoq.Times.atLeast(1)); + disposable2.verify(d => d.dispose(), typemoq.Times.once()); }); test('Activate workspace specific to the resource in case of Multiple workspaces when a file is opened', async () => { const disposable1 = typemoq.Mock.ofType(); @@ -98,8 +128,12 @@ suite('Activation - ActivationManager', () => { const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType(); document.setup(d => d.uri).returns(() => documentUri); + document.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { workspaceFoldersChangedHandler = cb; return disposable1.object; }); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { + workspaceFoldersChangedHandler = cb; + return disposable1.object; + }); documentManager .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) .callback(cb => (fileOpenedHandler = cb)) @@ -109,6 +143,7 @@ suite('Activation - ActivationManager', () => { const resource = Uri.parse('two'); const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; const folder2 = { name: 'two', uri: resource, index: 2 }; + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn('one'); when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); when(workspaceService.hasWorkspaceFolders).thenReturn(true); when(workspaceService.getWorkspaceFolder(document.object.uri)).thenReturn(folder2); @@ -116,6 +151,7 @@ suite('Activation - ActivationManager', () => { when(workspaceService.getWorkspaceFolder(resource)).thenReturn(folder2); when(activationService1.activate(resource)).thenResolve(); when(activationService2.activate(resource)).thenResolve(); + when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection .setup(a => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) @@ -134,22 +170,21 @@ suite('Activation - ActivationManager', () => { // Check if activate workspace is called on opening a file fileOpenedHandler.call(managerTest, document.object); + await sleep(1); documentManager.verifyAll(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); verify(workspaceService.workspaceFolders).once(); verify(workspaceService.hasWorkspaceFolders).once(); - verify(workspaceService.getWorkspaceFolder(anything())).thrice(); + verify(workspaceService.getWorkspaceFolder(anything())).atLeast(1); verify(activationService1.activate(resource)).once(); verify(activationService2.activate(resource)).once(); }); test('Function activateWorkspace() will be filtered to current resource', async () => { const resource = Uri.parse('two'); - const folder = { name: 'two', uri: resource, index: 2 }; - - when(workspaceService.getWorkspaceFolder(resource)).thenReturn(folder); when(activationService1.activate(resource)).thenResolve(); when(activationService2.activate(resource)).thenResolve(); + when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection .setup(a => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) @@ -161,7 +196,6 @@ suite('Activation - ActivationManager', () => { await managerTest.activateWorkspace(resource); - verify(workspaceService.getWorkspaceFolder(resource)).once(); verify(activationService1.activate(resource)).once(); verify(activationService2.activate(resource)).once(); }); diff --git a/src/test/common/terminals/activation.unit.test.ts b/src/test/common/terminals/activation.unit.test.ts index 91ad4dbe55f8..6e98a699ad90 100644 --- a/src/test/common/terminals/activation.unit.test.ts +++ b/src/test/common/terminals/activation.unit.test.ts @@ -3,59 +3,105 @@ 'use strict'; import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { Terminal } from 'vscode'; -import { ITerminalManager } from '../../../client/common/application/types'; -import { ITerminalActivator, ITerminalHelper } from '../../../client/common/terminal/types'; -import { IDisposableRegistry } from '../../../client/common/types'; -import { noop } from '../../../client/common/utils/misc'; -import { IServiceContainer } from '../../../client/ioc/types'; +import { Terminal, Uri } from 'vscode'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { TerminalActivator } from '../../../client/common/terminal/activator'; +import { ITerminalActivator } from '../../../client/common/terminal/types'; +import { IDisposable } from '../../../client/common/types'; import { TerminalAutoActivation } from '../../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; suite('Terminal Auto Activation', () => { - let activator: TypeMoq.IMock; - let terminalManager: TypeMoq.IMock; + let activator: ITerminalActivator; + let terminalManager: ITerminalManager; let terminalAutoActivation: ITerminalAutoActivation; + let workspaceService: IWorkspaceService; setup(() => { - terminalManager = TypeMoq.Mock.ofType(); - activator = TypeMoq.Mock.ofType(); - const disposables = []; - - const serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny())) - .returns(() => terminalManager.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny())) - .returns(() => activator.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposables); - - terminalAutoActivation = new TerminalAutoActivation(serviceContainer.object, activator.object); + terminalManager = mock(TerminalManager); + activator = mock(TerminalActivator); + workspaceService = mock(WorkspaceService); + + terminalAutoActivation = new TerminalAutoActivation( + instance(terminalManager), + [], + instance(activator), + instance(workspaceService) + ); }); test('New Terminals should be activated', async () => { - let eventHandler: undefined | ((e: Terminal) => void); + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType(); + const terminal = TypeMoq.Mock.ofType(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything(), anything())).thenResolve(); + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal.object); + + verify(activator.activateEnvironmentInTerminal(terminal.object, undefined)).once(); + }); + test('New Terminals should be activated with resource of single workspace', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType(); + const terminal = TypeMoq.Mock.ofType(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + const resource = Uri.file(__filename); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything(), anything())).thenResolve(); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri: resource }]); + + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal.object); + + verify(activator.activateEnvironmentInTerminal(terminal.object, resource)).once(); + }); + test('New Terminals should be activated with resource of main workspace', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType(); const terminal = TypeMoq.Mock.ofType(); - terminalManager - .setup(m => m.onDidOpenTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(handler => { - eventHandler = handler; - return { dispose: noop }; - }); - activator - .setup(h => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.once()); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + const resource = Uri.file(__filename); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything(), anything())).thenResolve(); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: resource }, + { index: 2, name: '2', uri: Uri.file('1234') } + ]); terminalAutoActivation.register(); - expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized'); + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); - eventHandler!.bind(terminalAutoActivation)(terminal.object); + handler!.bind(terminalAutoActivation)(terminal.object); - activator.verifyAll(); + verify(activator.activateEnvironmentInTerminal(terminal.object, resource)).once(); }); }); diff --git a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts index 04e292dc524c..cb6d177713fc 100644 --- a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts +++ b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts @@ -7,6 +7,10 @@ import * as assert from 'assert'; import { instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; import { PipEnvActivationCommandProvider } from '../../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../../client/common/terminal/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; @@ -22,12 +26,19 @@ suite('Terminals Activation - Pipenv', () => { let activationProvider: ITerminalActivationCommandProvider; let interpreterService: IInterpreterService; let pipenvService: TypeMoq.IMock; + let workspaceService: IWorkspaceService; + let fs: IFileSystem; setup(() => { + interpreterService = mock(InterpreterService); + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); interpreterService = mock(InterpreterService); pipenvService = TypeMoq.Mock.ofType(); activationProvider = new PipEnvActivationCommandProvider( instance(interpreterService), - pipenvService.object + pipenvService.object, + instance(workspaceService), + instance(fs) ); pipenvService.setup(p => p.executable).returns(() => pipenvExecFile); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 857f5647f6c8..0f08af348f57 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -143,6 +143,7 @@ import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService'; import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; +import { PipEnvServiceHelper } from '../../client/interpreter/locators/services/pipEnvServiceHelper'; import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; import { WorkspaceVirtualEnvironmentsSearchPathProvider, @@ -151,7 +152,7 @@ import { import { WorkspaceVirtualEnvWatcherService } from '../../client/interpreter/locators/services/workspaceVirtualEnvWatcherService'; -import { IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; +import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { MockAutoSelectionService } from '../mocks/autoSelector'; @@ -219,6 +220,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton( ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv); this.serviceManager.addSingleton(ITerminalManager, TerminalManager); + this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); // Setup our command list this.commandManager.registerCommand('setContext', (name: string, value: boolean) => { diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index 952e6ae1b834..6c7b5eca86be 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -8,7 +8,6 @@ import { expect } from 'chai'; import { SemVer } from 'semver'; import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; @@ -16,6 +15,7 @@ import { PersistentState, PersistentStateFactory } from '../../../client/common/ import { FileSystem } from '../../../client/common/platform/fileSystem'; import { IFileSystem } from '../../../client/common/platform/types'; import { IPersistentStateFactory, Resource } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; import { InterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection'; import { InterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/proxy'; import { CachedInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/cached'; @@ -30,7 +30,7 @@ import { InterpreterHelper } from '../../../client/interpreter/helpers'; const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; -suite('Interpreters - Auto Selection', () => { +suite('xInterpreters - Auto Selection', () => { let autoSelectionService: InterpreterAutoSelectionServiceTest; let workspaceService: IWorkspaceService; let stateFactory: IPersistentStateFactory; @@ -45,12 +45,15 @@ suite('Interpreters - Auto Selection', () => { let helper: IInterpreterHelper; let proxy: IInterpreterAutoSeletionProxyService; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { - public initializeStore(): Promise { - return super.initializeStore(); + public initializeStore(resource: Resource): Promise { + return super.initializeStore(resource); } public storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { return super.storeAutoSelectedInterpreter(resource, interpreter); } + public getAutoSelectedWorkspacePromises() { + return this.autoSelectedWorkspacePromises; + } } setup(() => { workspaceService = mock(WorkspaceService); @@ -75,7 +78,7 @@ suite('Interpreters - Auto Selection', () => { ); }); - test('Instance is registere in proxy', () => { + test('Instance is registered in proxy', () => { verify(proxy.registerInstance!(autoSelectionService)).once(); }); test('Rules are chained in order of preference', () => { @@ -127,9 +130,9 @@ suite('Interpreters - Auto Selection', () => { test('Initializing the store would be executed once', async () => { when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - await autoSelectionService.initializeStore(); - await autoSelectionService.initializeStore(); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.initializeStore(undefined); verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); }); @@ -140,7 +143,7 @@ suite('Interpreters - Auto Selection', () => { when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(false); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); verify(state.value).atLeast(1); @@ -154,7 +157,7 @@ suite('Interpreters - Auto Selection', () => { when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(true); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); verify(state.value).atLeast(1); @@ -165,10 +168,11 @@ suite('Interpreters - Auto Selection', () => { let eventFired = false; const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); @@ -185,8 +189,9 @@ suite('Interpreters - Auto Selection', () => { when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); when(state.value).thenReturn(interpreterInfoInState); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); @@ -203,8 +208,9 @@ suite('Interpreters - Auto Selection', () => { when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); when(state.value).thenReturn(interpreterInfoInState); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); @@ -216,8 +222,9 @@ suite('Interpreters - Auto Selection', () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.setGlobalInterpreter(interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); @@ -232,8 +239,9 @@ suite('Interpreters - Auto Selection', () => { when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); @@ -241,18 +249,37 @@ suite('Interpreters - Auto Selection', () => { expect(selectedInterpreter).to.deep.equal(interpreterInfo); expect(eventFired).to.deep.equal(false, 'event fired'); }); - test('Store workspace interpreter info in state store', async () => { + test('Storing workspace interpreter info in state store should fail', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.setWorkspaceInterpreter(resource, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter ? selectedInterpreter : undefined).to.deep.equal(undefined, 'not undefined'); + }); + test('Store workspace interpreter info in state store', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + const resource = Uri.parse('one'); + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); + const deferred = createDeferred(); + deferred.resolve(); + autoSelectionService.getAutoSelectedWorkspacePromises().set('', deferred); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.setWorkspaceInterpreter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); + + verify(state.updateValue(interpreterInfo)).once(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); }); test('Return undefined when we do not have a global value', async () => { @@ -261,8 +288,9 @@ suite('Interpreters - Auto Selection', () => { const resource = Uri.parse('one'); when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); @@ -274,44 +302,22 @@ suite('Interpreters - Auto Selection', () => { const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); const globalInterpreterInfo = { path: 'global Value' }; when(state.value).thenReturn(globalInterpreterInfo as any); - await autoSelectionService.initializeStore(); + when(workspaceService.getWorkspaceFolderIdentifier(resource, anything())).thenReturn('1'); + const deferred = createDeferred(); + deferred.resolve(); + autoSelectionService.getAutoSelectedWorkspacePromises().set('', deferred); + + await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); + const anotherResourceOfAnotherWorkspace = Uri.parse('Some other workspace'); + when(workspaceService.getWorkspaceFolderIdentifier(anotherResourceOfAnotherWorkspace, anything())).thenReturn('2'); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(anotherResourceOfAnotherWorkspace); - verify(workspaceService.getWorkspaceFolder(resource)).once(); - verify(workspaceService.getWorkspaceFolder(anotherResourceOfAnotherWorkspace)).once(); verify(state.updateValue(interpreterInfo)).never(); expect(selectedInterpreter).to.deep.equal(globalInterpreterInfo); }); - test('setWorkspaceInterpreter will invoke storeAutoSelectedInterpreter with same args', async () => { - const pythonPath = 'Hello World'; - const interpreterInfo = { path: pythonPath } as any; - const resource = Uri.parse('one'); - const moq = typemoq.Mock.ofInstance(autoSelectionService, typemoq.MockBehavior.Loose, false); - moq - .setup(m => m.storeAutoSelectedInterpreter(typemoq.It.isValue(resource), typemoq.It.isValue(interpreterInfo))) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - moq.callBase = true; - await moq.object.setWorkspaceInterpreter(resource, interpreterInfo); - - moq.verifyAll(); - }); - test('setGlobalInterpreter will invoke storeAutoSelectedInterpreter with same args and without a resource', async () => { - const pythonPath = 'Hello World'; - const interpreterInfo = { path: pythonPath } as any; - const moq = typemoq.Mock.ofInstance(autoSelectionService, typemoq.MockBehavior.Loose, false); - moq - .setup(m => m.storeAutoSelectedInterpreter(typemoq.It.isValue(undefined), typemoq.It.isValue(interpreterInfo))) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - moq.callBase = true; - await moq.object.setGlobalInterpreter(interpreterInfo); - - moq.verifyAll(); - }); }); diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts index 013c778e7991..3a3ddf9fc11c 100644 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts @@ -35,7 +35,7 @@ import { import { InterpreterHelper } from '../../../../client/interpreter/helpers'; import { KnownPathsService } from '../../../../client/interpreter/locators/services/KnownPathsService'; -suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { +suite('xInterpreters - Auto Selection - Workspace Virtual Envs Rule', () => { let rule: WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest; let stateFactory: IPersistentStateFactory; let fs: IFileSystem; @@ -304,7 +304,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { const nextInvoked = createDeferred(); rule.next = () => Promise.resolve(nextInvoked.resolve()); rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([interpreterInfo]); + when(pipEnvLocator.getInterpreters(folderUri, true)).thenResolve([interpreterInfo]); when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); rule.cacheSelectedInterpreter = () => Promise.resolve(); @@ -335,7 +335,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { const nextInvoked = createDeferred(); rule.next = () => Promise.resolve(nextInvoked.resolve()); rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([interpreterInfo]); - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([interpreterInfo]); + when(pipEnvLocator.getInterpreters(folderUri, true)).thenResolve([interpreterInfo]); when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); rule.cacheSelectedInterpreter = () => Promise.resolve(); @@ -366,7 +366,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { const nextInvoked = createDeferred(); rule.next = () => Promise.resolve(nextInvoked.resolve()); rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([]); + when(pipEnvLocator.getInterpreters(folderUri, true)).thenResolve([]); when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(interpreterInfo); rule.cacheSelectedInterpreter = () => Promise.resolve(); @@ -397,7 +397,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { const nextInvoked = createDeferred(); rule.next = () => Promise.resolve(nextInvoked.resolve()); rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([]); - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([]); + when(pipEnvLocator.getInterpreters(folderUri, true)).thenResolve([]); when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(interpreterInfo); rule.cacheSelectedInterpreter = () => Promise.resolve(); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index dc9090525ecc..45c6236d6a5a 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -1,12 +1,15 @@ import { expect } from 'chai'; import * as path from 'path'; import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Disposable, StatusBarAlignment, StatusBarItem, Uri, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposableRegistry, IPathUtils, IPythonSettings } from '../../client/common/types'; import { Architecture } from '../../client/common/utils/platform'; +import { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; +import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; @@ -40,6 +43,7 @@ suite('Interpreters Display', () => { let interpreterDisplay: IInterpreterDisplay; let interpreterHelper: TypeMoq.IMock; let pathUtils: TypeMoq.IMock; + let autoSelection: IInterpreterAutoSelectionService; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); @@ -53,6 +57,7 @@ suite('Interpreters Display', () => { pythonSettings = TypeMoq.Mock.ofType(); configurationService = TypeMoq.Mock.ofType(); pathUtils = TypeMoq.Mock.ofType(); + autoSelection = mock(InterpreterAutoSelectionService); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => applicationShell.object); @@ -63,6 +68,7 @@ suite('Interpreters Display', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))).returns(() => instance(autoSelection)); applicationShell.setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))).returns(() => statusBar.object); pathUtils.setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(p => p); @@ -93,11 +99,13 @@ suite('Interpreters Display', () => { path: path.join('user', 'development', 'env', 'bin', 'python') }; setupWorkspaceFolder(resource, workspaceFolder); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(activeInterpreter)); await interpreterDisplay.refresh(resource); + verify(autoSelection.autoSelectInterpreter(anything())).once(); statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!, TypeMoq.Times.once()); }); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index c0368cc02425..84eef49031f8 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -229,7 +229,7 @@ suite('Interpreters service', () => { const pythonPath = '1234'; const interpreterInfo: Partial = { path: pythonPath }; const fileHash = 'File_Hash'; - const hash = `${fileHash}-${md5(JSON.stringify(interpreterInfo))}`; + const hash = `${fileHash}-${md5(JSON.stringify({ ...interpreterInfo, displayName: '' }))}`; fileSystem .setup(fs => fs.getFileHash(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(fileHash)) @@ -368,14 +368,6 @@ suite('Interpreters service', () => { }); suite('Interprter Cache', () => { - class InterpreterServiceTest extends InterpreterService { - public async getInterpreterCache(pythonPath: string): Promise> { - return super.getInterpreterCache(pythonPath); - } - public async updateCachedInterpreterInformation(info: PythonInterpreter): Promise { - return super.updateCachedInterpreterInformation(info); - } - } setup(() => { setupSuite(); fileSystem.reset(); @@ -412,7 +404,7 @@ suite('Interpreters service', () => { .returns(() => state.object) .verifiable(TypeMoq.Times.once()); - const service = new InterpreterServiceTest(serviceContainer); + const service = new InterpreterService(serviceContainer); const store = await service.getInterpreterCache(pythonPath); @@ -452,7 +444,7 @@ suite('Interpreters service', () => { .returns(() => state.object) .verifiable(TypeMoq.Times.once()); - const service = new InterpreterServiceTest(serviceContainer); + const service = new InterpreterService(serviceContainer); const store = await service.getInterpreterCache(pythonPath); diff --git a/src/test/interpreters/locators/helpers.unit.test.ts b/src/test/interpreters/locators/helpers.unit.test.ts index b059f9d15af9..87d72666d7a1 100644 --- a/src/test/interpreters/locators/helpers.unit.test.ts +++ b/src/test/interpreters/locators/helpers.unit.test.ts @@ -8,12 +8,15 @@ import { expect } from 'chai'; import * as path from 'path'; import { SemVer } from 'semver'; +import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { Architecture } from '../../../client/common/utils/platform'; import { IInterpreterHelper, IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; import { InterpreterLocatorHelper } from '../../../client/interpreter/locators/helpers'; +import { PipEnvServiceHelper } from '../../../client/interpreter/locators/services/pipEnvServiceHelper'; +import { IPipEnvServiceHelper } from '../../../client/interpreter/locators/types'; import { IServiceContainer } from '../../../client/ioc/types'; enum OS { @@ -27,17 +30,19 @@ suite('Interpreters - Locators Helper', () => { let platform: TypeMoq.IMock; let helper: IInterpreterLocatorHelper; let fs: TypeMoq.IMock; + let pipEnvHelper: IPipEnvServiceHelper; let interpreterServiceHelper: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); platform = TypeMoq.Mock.ofType(); fs = TypeMoq.Mock.ofType(); + pipEnvHelper = mock(PipEnvServiceHelper); interpreterServiceHelper = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platform.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterServiceHelper.object); - helper = new InterpreterLocatorHelper(serviceContainer.object); + helper = new InterpreterLocatorHelper(fs.object, instance(pipEnvHelper)); }); test('Ensure default Mac interpreter is not excluded from the list of interpreters', async () => { platform.setup(p => p.isWindows).returns(() => false); @@ -71,8 +76,9 @@ suite('Interpreters - Locators Helper', () => { }); const expectedInterpreters = interpreters.slice(0); + when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); - const items = helper.mergeInterpreters(interpreters); + const items = await helper.mergeInterpreters(interpreters); interpreterServiceHelper.verifyAll(); platform.verifyAll(); @@ -138,7 +144,8 @@ suite('Interpreters - Locators Helper', () => { } }); - const items = helper.mergeInterpreters(interpreters); + when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); + const items = await helper.mergeInterpreters(interpreters); interpreterServiceHelper.verifyAll(); platform.verifyAll(); @@ -182,7 +189,8 @@ suite('Interpreters - Locators Helper', () => { } }); - const items = helper.mergeInterpreters(interpreters); + when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); + const items = await helper.mergeInterpreters(interpreters); interpreterServiceHelper.verifyAll(); platform.verifyAll(); diff --git a/src/test/interpreters/locators/index.unit.test.ts b/src/test/interpreters/locators/index.unit.test.ts index fc4fa6c7c6f5..00c028caaf77 100644 --- a/src/test/interpreters/locators/index.unit.test.ts +++ b/src/test/interpreters/locators/index.unit.test.ts @@ -90,7 +90,7 @@ suite('Interpreters - Locators Index', () => { helper .setup(h => h.mergeInterpreters(TypeMoq.It.isAny())) - .returns(() => locatorsWithInterpreters.map(item => item.interpreters[0])) + .returns(() => Promise.resolve(locatorsWithInterpreters.map(item => item.interpreters[0]))) .verifiable(TypeMoq.Times.once()); await locator.getInterpreters(resource); @@ -151,7 +151,7 @@ suite('Interpreters - Locators Index', () => { const expectedInterpreters = locatorsWithInterpreters.map(item => item.interpreters[0]); helper .setup(h => h.mergeInterpreters(TypeMoq.It.isAny())) - .returns(() => expectedInterpreters) + .returns(() => Promise.resolve(expectedInterpreters)) .verifiable(TypeMoq.Times.once()); const interpreters = await locator.getInterpreters(resource); diff --git a/src/test/interpreters/pipEnvService.unit.test.ts b/src/test/interpreters/pipEnvService.unit.test.ts index 70591d98fc2a..2441dc34bfec 100644 --- a/src/test/interpreters/pipEnvService.unit.test.ts +++ b/src/test/interpreters/pipEnvService.unit.test.ts @@ -9,6 +9,7 @@ import * as assert from 'assert'; import { expect } from 'chai'; import * as path from 'path'; import { SemVer } from 'semver'; +import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; @@ -26,13 +27,15 @@ import { getNamesAndValues } from '../../client/common/utils/enum'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; import { IInterpreterHelper } from '../../client/interpreter/contracts'; import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; +import { PipEnvServiceHelper } from '../../client/interpreter/locators/services/pipEnvServiceHelper'; +import { IPipEnvServiceHelper } from '../../client/interpreter/locators/types'; import { IServiceContainer } from '../../client/ioc/types'; enum OS { Mac, Windows, Linux } -suite('Interpreters - PipEnv', () => { +suite('xInterpreters - PipEnv', () => { const rootWorkspace = Uri.file(path.join('usr', 'desktop', 'wkspc1')).fsPath; getNamesAndValues(OS).forEach(os => { [undefined, Uri.file(path.join(rootWorkspace, 'one.py'))].forEach(resource => { @@ -53,6 +56,7 @@ suite('Interpreters - PipEnv', () => { let config: TypeMoq.IMock; let settings: TypeMoq.IMock; let pipenvPathSetting: string; + let pipEnvServiceHelper: IPipEnvServiceHelper; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); const workspaceService = TypeMoq.Mock.ofType(); @@ -66,6 +70,7 @@ suite('Interpreters - PipEnv', () => { procServiceFactory = TypeMoq.Mock.ofType(); logger = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); + pipEnvServiceHelper = mock(PipEnvServiceHelper); processService.setup((x: any) => x.then).returns(() => undefined); procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); @@ -92,7 +97,9 @@ suite('Interpreters - PipEnv', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => config.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPipEnvServiceHelper), TypeMoq.It.isAny())).returns(() => instance(pipEnvServiceHelper)); + when(pipEnvServiceHelper.trackWorkspaceFolder(anything(), anything())).thenResolve(); config = TypeMoq.Mock.ofType(); settings = TypeMoq.Mock.ofType(); config.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object); @@ -151,6 +158,7 @@ suite('Interpreters - PipEnv', () => { interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: new SemVer('1.0.0') })); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)).verifiable(); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)).verifiable(); + const environments = await pipEnvService.getInterpreters(resource); expect(environments).to.be.lengthOf(1); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts index c3d8632c107e..7bbb7bfbfe07 100644 --- a/src/test/telemetry/index.unit.test.ts +++ b/src/test/telemetry/index.unit.test.ts @@ -11,7 +11,7 @@ import { EXTENSION_ROOT_DIR } from '../../client/constants'; import { sendTelemetryEvent } from '../../client/telemetry'; import { correctPathForOsType } from '../common'; -suite('Telemetry', () => { +suite('xTelemetry', () => { const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; setup(() => {