diff --git a/build/ci/mocha-vsts-reporter.js b/build/ci/mocha-vsts-reporter.js index a130fcd43433..f88a5808076d 100644 --- a/build/ci/mocha-vsts-reporter.js +++ b/build/ci/mocha-vsts-reporter.js @@ -15,11 +15,9 @@ function MochaVstsReporter(runner, options) { runner.on('suite', function(suite){ if (suite.root === true){ - console.log('Begin test run.............'); indentLevel++; indenter = INDENT_BASE.repeat(indentLevel); } else { - console.log('%sStart "%s"', indenter, suite.title); indentLevel++; indenter = INDENT_BASE.repeat(indentLevel); } @@ -29,9 +27,7 @@ function MochaVstsReporter(runner, options) { if (suite.root === true) { indentLevel=0; indenter = ''; - console.log('.............End test run.'); } else { - console.log('%sEnd "%s"', indenter, suite.title); indentLevel--; indenter = INDENT_BASE.repeat(indentLevel); // ##vso[task.setprogress]current operation diff --git a/news/1 Enhancements/3369.md b/news/1 Enhancements/3369.md new file mode 100644 index 000000000000..8fc39b4319c5 --- /dev/null +++ b/news/1 Enhancements/3369.md @@ -0,0 +1 @@ +Improvements to automatic selection of the python interpreter. diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 9a9adb94ae6a..b10c7ad0c686 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -3,12 +3,13 @@ import * as child_process from 'child_process'; import { EventEmitter } from 'events'; import * as path from 'path'; -import { - ConfigurationChangeEvent, ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, - workspace, WorkspaceConfiguration -} from 'vscode'; +import { ConfigurationChangeEvent, ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, WorkspaceConfiguration } from 'vscode'; +import '../common/extensions'; +import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types'; import { sendTelemetryEvent } from '../telemetry'; import { COMPLETION_ADD_BRACKETS, FORMAT_ON_TYPE } from '../telemetry/constants'; +import { IWorkspaceService } from './application/types'; +import { WorkspaceService } from './application/workspace'; import { isTestExecution } from './constants'; import { IS_WINDOWS } from './platform/constants'; import { @@ -57,21 +58,28 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { private disposables: Disposable[] = []; // tslint:disable-next-line:variable-name private _pythonPath = ''; + private readonly workspace: IWorkspaceService; - constructor(workspaceFolder?: Uri) { + constructor(workspaceFolder: Uri | undefined, private readonly InterpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService) { super(); + this.workspace = workspace || new WorkspaceService(); this.workspaceRoot = workspaceFolder ? workspaceFolder : Uri.file(__dirname); this.initialize(); } // tslint:disable-next-line:function-name - public static getInstance(resource?: Uri): PythonSettings { - const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource).uri; + public static getInstance(resource: Uri | undefined, interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService): PythonSettings { + workspace = workspace || new WorkspaceService(); + const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { - const settings = new PythonSettings(workspaceFolderUri); + const settings = new PythonSettings(workspaceFolderUri, interpreterAutoSelectionService, workspace); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); - const config = workspace.getConfiguration('editor', resource ? resource : null); + // Pass null to avoid VSC from complaining about not passing in a value. + // tslint:disable-next-line:no-any + const config = workspace.getConfiguration('editor', resource ? resource : null as any); const formatOnType = config ? config.get('formatOnType', false) : false; sendTelemetryEvent(COMPLETION_ADD_BRACKETS, undefined, { enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false }); sendTelemetryEvent(FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); @@ -81,7 +89,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { } // tslint:disable-next-line:type-literal-delimiter - public static getSettingsUriAndTarget(resource?: Uri): { uri: Uri | undefined, target: ConfigurationTarget } { + public static getSettingsUriAndTarget(resource: Uri | undefined, workspace?: IWorkspaceService): { uri: Uri | undefined, target: ConfigurationTarget } { + workspace = workspace || new WorkspaceService(); const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; @@ -99,21 +108,29 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { throw new Error('Dispose can only be called from unit tests'); } // tslint:disable-next-line:no-void-expression - PythonSettings.pythonSettings.forEach(item => item.dispose()); + PythonSettings.pythonSettings.forEach(item => item && item.dispose()); PythonSettings.pythonSettings.clear(); } public dispose() { // tslint:disable-next-line:no-unsafe-any - this.disposables.forEach(disposable => disposable.dispose()); + this.disposables.forEach(disposable => disposable && disposable.dispose()); this.disposables = []; } // tslint:disable-next-line:cyclomatic-complexity max-func-body-length - public update(pythonSettings: WorkspaceConfiguration) { + protected update(pythonSettings: WorkspaceConfiguration) { const workspaceRoot = this.workspaceRoot.fsPath; const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion 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); + if (autoSelectedPythonInterpreter) { + this.InterpreterAutoSelectionService.setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter).ignoreErrors(); + } + this.pythonPath = autoSelectedPythonInterpreter ? autoSelectedPythonInterpreter.path : this.pythonPath; + } this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot); // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; @@ -342,25 +359,31 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { // Add support for specifying just the directory where the python executable will be located. // E.g. virtual directory name. try { - this._pythonPath = getPythonExecutable(value); + this._pythonPath = this.getPythonExecutable(value); } catch (ex) { this._pythonPath = value; } } + protected getPythonExecutable(pythonPath: string) { + return getPythonExecutable(pythonPath); + } protected initialize(): void { - this.disposables.push(workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { - if (event.affectsConfiguration('python')) - { - const currentConfig = workspace.getConfiguration('python', this.workspaceRoot); - this.update(currentConfig); - - // If workspace config changes, then we could have a cascading effect of on change events. - // Let's defer the change notification. - setTimeout(() => this.emit('change'), 1); + const onDidChange = () => { + const currentConfig = this.workspace.getConfiguration('python', this.workspaceRoot); + this.update(currentConfig); + + // If workspace config changes, then we could have a cascading effect of on change events. + // Let's defer the change notification. + setTimeout(() => this.emit('change'), 1); + }; + this.disposables.push(this.InterpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this))); + this.disposables.push(this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { + if (event.affectsConfiguration('python')) { + onDidChange(); } })); - const initialConfig = workspace.getConfiguration('python', this.workspaceRoot); + const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); if (initialConfig) { this.update(initialConfig); } diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index 044bb66de584..505039a181ed 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -1,15 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { IInterpreterAutoSeletionProxyService } from '../../interpreter/autoSelection/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; import { PythonSettings } from '../configSettings'; import { IConfigurationService, IPythonSettings } from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + } public getSettings(resource?: Uri): IPythonSettings { - return PythonSettings.getInstance(resource); + const InterpreterAutoSelectionService = this.serviceContainer.get(IInterpreterAutoSeletionProxyService); + return PythonSettings.getInstance(resource, InterpreterAutoSelectionService, this.workspaceService); } public async updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise { @@ -17,7 +25,10 @@ export class ConfigurationService implements IConfigurationService { uri: resource, target: configTarget || ConfigurationTarget.WorkspaceFolder }; - const settingsInfo = section === 'python' && configTarget !== ConfigurationTarget.Global ? PythonSettings.getSettingsUriAndTarget(resource) : defaultSetting; + let settingsInfo = defaultSetting; + if (section === 'python' && configTarget !== ConfigurationTarget.Global) { + settingsInfo = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService); + } const configSection = workspace.getConfiguration(section, settingsInfo.uri); const currentValue = configSection.inspect(setting); diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index bd3e264773fd..8514e978c059 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -197,16 +197,16 @@ export class LinterInstaller extends BaseInstaller { } const response = await this.appShell.showErrorMessage(message, ...options); if (response === install) { - sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'install'}); + sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'install' }); return this.install(product, resource); } else if (response === disableInstallPrompt) { await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'disablePrompt'}); + sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'disablePrompt' }); return InstallerResponse.Ignore; } - if (response === selectLinter){ - sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select'}); + if (response === selectLinter) { + sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); const commandManager = this.serviceContainer.get(ICommandManager); await commandManager.executeCommand(Commands.Set_Linter); } @@ -224,7 +224,7 @@ export class LinterInstaller extends BaseInstaller { protected getStoredResponse(key: string): boolean { const factory = this.serviceContainer.get(IPersistentStateFactory); const state = factory.createGlobalPersistentState(key, undefined); - return state.value; + return state.value === true; } /** diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 8042c7f52d2a..30a479f67fc4 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -7,7 +7,7 @@ import { inject, injectable, named } from 'inversify'; import { Memento } from 'vscode'; import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types'; -class PersistentState implements IPersistentState{ +export class PersistentState implements IPersistentState{ constructor(private storage: Memento, private key: string, private defaultValue?: T, private expiryDurationMs?: number) { } public get value(): T { diff --git a/src/client/common/platform/registry.ts b/src/client/common/platform/registry.ts index 797b64a16ffc..31f758f62ad8 100644 --- a/src/client/common/platform/registry.ts +++ b/src/client/common/platform/registry.ts @@ -1,5 +1,5 @@ import { injectable } from 'inversify'; -import * as Registry from 'winreg'; +import { Options } from 'winreg'; import { Architecture } from '../utils/platform'; import { IRegistry, RegistryHive } from './types'; @@ -29,7 +29,9 @@ export function getArchitectureDisplayName(arch?: Architecture) { } } -async function getRegistryValue(options: Registry.Options, name: string = '') { +async function getRegistryValue(options: Options, name: string = '') { + // tslint:disable-next-line:no-require-imports + const Registry = require('winreg') as typeof import('winreg'); return new Promise((resolve, reject) => { new Registry(options).get(name, (error, result) => { if (error || !result || typeof result.value !== 'string') { @@ -39,7 +41,10 @@ async function getRegistryValue(options: Registry.Options, name: string = '') { }); }); } -async function getRegistryKeys(options: Registry.Options): Promise { + +async function getRegistryKeys(options: Options): Promise { + // tslint:disable-next-line:no-require-imports + const Registry = require('winreg') as typeof import('winreg'); // https://github.com/python/peps/blob/master/pep-0514.txt#L85 return new Promise((resolve, reject) => { new Registry(options).keys((error, result) => { @@ -61,6 +66,8 @@ function translateArchitecture(arch?: Architecture): RegistryArchitectures | und } } function translateHive(hive: RegistryHive): string | undefined { + // tslint:disable-next-line:no-require-imports + const Registry = require('winreg') as typeof import('winreg'); switch (hive) { case RegistryHive.HKCU: return Registry.HKCU; diff --git a/src/client/common/platform/serviceRegistry.ts b/src/client/common/platform/serviceRegistry.ts index dce3511f9f37..d15edf5fc388 100644 --- a/src/client/common/platform/serviceRegistry.ts +++ b/src/client/common/platform/serviceRegistry.ts @@ -11,7 +11,5 @@ import { IFileSystem, IPlatformService, IRegistry } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IPlatformService, PlatformService); serviceManager.addSingleton(IFileSystem, FileSystem); - if (serviceManager.get(IPlatformService).isWindows) { - serviceManager.addSingleton(IRegistry, RegistryImplementation); - } + serviceManager.addSingleton(IRegistry, RegistryImplementation); } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index 13b633c3f7dd..6ba03ccd2e49 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -10,7 +10,7 @@ import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; import { traceError } from '../logger'; import { IFileSystem } from '../platform/types'; import { Architecture } from '../utils/platform'; -import { convertPythonVersionToSemver } from '../utils/version'; +import { parsePythonVersion } from '../utils/version'; import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types'; @injectable() @@ -42,7 +42,7 @@ export class PythonExecutionService implements IPythonExecutionService { return { architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, path: this.pythonPath, - version: convertPythonVersionToSemver(versionValue), + version: parsePythonVersion(versionValue), sysVersion: json.sysVersion, sysPrefix: json.sysPrefix }; diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 6b0124b5c72a..84d42cd0e375 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -3,9 +3,7 @@ import { ChildProcess, ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; import { Observable } from 'rxjs/Observable'; import { CancellationToken, Uri } from 'vscode'; - -import { SemVer } from 'semver'; -import { ExecutionInfo } from '../types'; +import { ExecutionInfo, Version } from '../types'; import { Architecture } from '../utils/platform'; import { EnvironmentVariables } from '../variables/types'; @@ -64,7 +62,7 @@ export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown'; export type PythonVersionInfo = [number, number, number, ReleaseLevel]; export type InterpreterInfomation = { path: string; - version?: SemVer; + version?: Version; sysVersion: string; architecture: Architecture; sysPrefix: string; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index fa56d0ab27ba..874e4f7017ea 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -15,10 +15,19 @@ export const IMemento = Symbol('IGlobalMemento'); export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento'); +export type Resource = Uri | undefined; export interface IPersistentState { readonly value: T; updateValue(value: T): Promise; } +export type Version = { + raw: string; + major: number; + minor: number; + patch: number; + build: string[]; + prerelease: string[]; +}; export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); @@ -365,7 +374,7 @@ export interface IEditorUtils { } export interface IDisposable { - dispose(): Promise | undefined; + dispose(): Promise | undefined | void; } export const IAsyncDisposableRegistry = Symbol('IAsyncDisposableRegistry'); diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 9c5c53dc7892..e7f8d53700e9 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -39,12 +39,14 @@ class DeferredImpl implements Deferred { }); } public resolve(value?: T | PromiseLike) { - this._resolve.apply(this.scope ? this.scope : this, arguments); + // tslint:disable-next-line:no-any + this._resolve.apply(this.scope ? this.scope : this, arguments as any); this._resolved = true; } // tslint:disable-next-line:no-any public reject(reason?: any) { - this._reject.apply(this.scope ? this.scope : this, arguments); + // tslint:disable-next-line:no-any + this._reject.apply(this.scope ? this.scope : this, arguments as any); this._rejected = true; } get promise(): Promise { @@ -67,9 +69,18 @@ export function createDeferred(scope: any = null): Deferred { export function createDeferredFrom(...promises: Promise[]): Deferred { const deferred = createDeferred(); - Promise.all(promises) + Promise.all(promises) + // tslint:disable-next-line:no-any + .then(deferred.resolve.bind(deferred) as any) + // tslint:disable-next-line:no-any + .catch(deferred.reject.bind(deferred) as any); + + return deferred; +} +export function createDeferredFromPromise(promise: Promise): Deferred { + const deferred = createDeferred(); + promise .then(deferred.resolve.bind(deferred)) .catch(deferred.reject.bind(deferred)); - return deferred; } diff --git a/src/client/common/utils/version.ts b/src/client/common/utils/version.ts index c418e3d1ba95..a631efa52248 100644 --- a/src/client/common/utils/version.ts +++ b/src/client/common/utils/version.ts @@ -4,6 +4,7 @@ 'use strict'; import * as semver from 'semver'; +import { Version } from '../types'; export function parseVersion(raw: string): semver.SemVer { raw = raw.replace(/\.00*(?=[1-9]|0\.)/, '.'); @@ -15,24 +16,15 @@ export function parseVersion(raw: string): semver.SemVer { } return ver; } - -export function convertToSemver(version: string) { - const versionParts = (version || '').split('.').filter(item => item.length > 0); - while (versionParts.length < 3) { - versionParts.push('0'); - } - return versionParts.join('.'); -} - -export function convertPythonVersionToSemver(version: string): semver.SemVer | undefined { +export function parsePythonVersion(version: string): Version | undefined { if (!version || version.trim().length === 0) { return; } const versionParts = (version || '') - .split('.') - .map(item => item.trim()) - .filter(item => item.length > 0) - .filter((_, index) => index < 4); + .split('.') + .map(item => item.trim()) + .filter(item => item.length > 0) + .filter((_, index) => index < 4); if (versionParts.length > 0 && versionParts[versionParts.length - 1].indexOf('-') > 0) { const lastPart = versionParts[versionParts.length - 1]; @@ -46,17 +38,10 @@ export function convertPythonVersionToSemver(version: string): semver.SemVer | u for (let index = 0; index < 3; index += 1) { versionParts[index] = /^\d+$/.test(versionParts[index]) ? versionParts[index] : '0'; } - versionParts[3] = ['alpha', 'beta', 'candidate', 'final'].indexOf(versionParts[3]) === -1 ? 'unknown' : versionParts[3]; - - return new semver.SemVer(`${versionParts[0]}.${versionParts[1]}.${versionParts[2]}-${versionParts[3]}`); -} - -export function compareVersion(versionA: string, versionB: string) { - try { - versionA = convertToSemver(versionA); - versionB = convertToSemver(versionB); - return semver.gt(versionA, versionB) ? 1 : 0; - } catch { - return 0; + if (['alpha', 'beta', 'candidate', 'final'].indexOf(versionParts[3]) === -1) { + versionParts.pop(); } + const numberParts = `${versionParts[0]}.${versionParts[1]}.${versionParts[2]}`; + const rawVersion = versionParts.length === 4 ? `${numberParts}-${versionParts[3]}` : numberParts; + return new semver.SemVer(rawVersion); } diff --git a/src/client/datascience/jupyter/jupyterConnection.ts b/src/client/datascience/jupyter/jupyterConnection.ts index dd0fe0893816..661eb0d45937 100644 --- a/src/client/datascience/jupyter/jupyterConnection.ts +++ b/src/client/datascience/jupyter/jupyterConnection.ts @@ -11,8 +11,8 @@ import { IConfigurationService, ILogger } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; -import { JupyterConnectError } from './jupyterConnectError'; import { IConnection } from '../types'; +import { JupyterConnectError } from './jupyterConnectError'; const UrlPatternRegEx = /(https?:\/\/[^\s]+)/ ; const HttpPattern = /https?:\/\//; diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 42b696c4e1a1..eb805bff959b 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { Kernel, ServerConnection, SessionManager } from '@jupyterlab/services'; +import { Kernel } from '@jupyterlab/services'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as os from 'os'; @@ -35,8 +35,8 @@ import { import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { Telemetry } from '../constants'; +import { IConnection, IJupyterExecution, IJupyterKernelSpec, IJupyterSessionManager, INotebookServer } from '../types'; import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; -import { IConnection, IJupyterExecution, IJupyterKernelSpec, INotebookServer, IJupyterSessionManager } from '../types'; import { JupyterKernelSpec } from './jupyterKernelSpec'; const CheckJupyterRegEx = IS_WINDOWS ? /^jupyter?\.exe$/ : /^jupyter?$/; diff --git a/src/client/datascience/jupyter/jupyterImporter.ts b/src/client/datascience/jupyter/jupyterImporter.ts index 6f1443bd8ac2..6cba06920b5e 100644 --- a/src/client/datascience/jupyter/jupyterImporter.ts +++ b/src/client/datascience/jupyter/jupyterImporter.ts @@ -5,14 +5,10 @@ import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as os from 'os'; import * as path from 'path'; -import { Disposable } from 'vscode-jsonrpc'; import { IWorkspaceService } from '../../common/application/types'; import { IFileSystem } from '../../common/platform/types'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../common/process/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; -import { IInterpreterService } from '../../interpreter/contracts'; import { CodeSnippits } from '../constants'; import { IJupyterExecution, INotebookImporter } from '../types'; @@ -33,8 +29,8 @@ export class JupyterImporter implements INotebookImporter { {{ cell.source | comment_lines }} {% endblock markdowncell %}`; - private templatePromise : Promise; - + private templatePromise: Promise; + constructor( @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts index 00a71a939b9f..d97c9aeaa622 100644 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ b/src/client/datascience/jupyter/jupyterServer.ts @@ -33,7 +33,7 @@ import { } from '../types'; class CellSubscriber { - private deferred : Deferred = createDeferred(); + private deferred: Deferred = createDeferred(); private cellRef: ICell; private subscriber: Subscriber; private promiseComplete: (self: CellSubscriber) => void; @@ -50,7 +50,7 @@ class CellSubscriber { return sessionStartTime && this.startTime > sessionStartTime; } - public next(sessionStartTime: number | undefined) { + public next(sessionStartTime: number | undefined) { // Tell the subscriber first if (this.isValid(sessionStartTime)) { this.subscriber.next(this.cellRef); @@ -87,11 +87,11 @@ class CellSubscriber { } } - public get promise() : Promise { + public get promise(): Promise { return this.deferred.promise; } - public get cell() : ICell { + public get cell(): ICell { return this.cellRef; } @@ -113,7 +113,7 @@ export class JupyterServer implements INotebookServer, IDisposable { private connInfo: IConnection | undefined; private workingDir: string | undefined; private sessionStartTime: number | undefined; - private onStatusChangedEvent : vscode.EventEmitter = new vscode.EventEmitter(); + private onStatusChangedEvent: vscode.EventEmitter = new vscode.EventEmitter(); private pendingCellSubscriptions: CellSubscriber[] = []; private ranInitialSetup = false; @@ -126,7 +126,7 @@ export class JupyterServer implements INotebookServer, IDisposable { this.asyncRegistry.push(this); } - public connect = async (connInfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken, workingDir?: string) : Promise => { + public connect = async (connInfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken, workingDir?: string): Promise => { // Save connection info. Determines if we need to change directory or not this.connInfo = connInfo; this.workingDir = workingDir; @@ -144,20 +144,21 @@ export class JupyterServer implements INotebookServer, IDisposable { this.initialNotebookSetup(cancelToken); } - public shutdown() : Promise { - return this.session ? this.session.dispose() : Promise.resolve(); + public shutdown(): Promise { + const dispose = this.session ? this.session.dispose() : undefined; + return dispose ? dispose : Promise.resolve(); } - public dispose() : Promise { + public dispose(): Promise { this.onStatusChangedEvent.dispose(); return this.shutdown(); } - public waitForIdle() : Promise { + public waitForIdle(): Promise { return this.session ? this.session.waitForIdle() : Promise.resolve(); } - public execute(code : string, file: string, line: number, cancelToken?: CancellationToken) : Promise { + public execute(code: string, file: string, line: number, cancelToken?: CancellationToken): Promise { // Do initial setup if necessary this.initialNotebookSetup(); @@ -195,7 +196,7 @@ export class JupyterServer implements INotebookServer, IDisposable { } } - public executeObservable = (code: string, file: string, line: number) : Observable => { + public executeObservable = (code: string, file: string, line: number): Observable => { // Do initial setup if necessary this.initialNotebookSetup(); @@ -224,7 +225,7 @@ export class JupyterServer implements INotebookServer, IDisposable { }); } - public executeSilently = (code: string, cancelToken?: CancellationToken) : Promise => { + public executeSilently = (code: string, cancelToken?: CancellationToken): Promise => { return new Promise((resolve, reject) => { // If we cancel, reject our promise @@ -263,11 +264,11 @@ export class JupyterServer implements INotebookServer, IDisposable { }); } - public get onStatusChanged() : vscode.Event { + public get onStatusChanged(): vscode.Event { return this.onStatusChangedEvent.event.bind(this.onStatusChangedEvent); } - public restartKernel = async () : Promise => { + public restartKernel = async (): Promise => { if (this.session) { // Update our start time so we don't keep sending responses this.sessionStartTime = Date.now(); @@ -289,7 +290,7 @@ export class JupyterServer implements INotebookServer, IDisposable { throw new Error(localize.DataScience.sessionDisposed()); } - public interruptKernel = async (timeoutMs: number) : Promise => { + public interruptKernel = async (timeoutMs: number): Promise => { if (this.session) { // Keep track of our current time. If our start time gets reset, we // restarted the kernel. @@ -377,10 +378,11 @@ export class JupyterServer implements INotebookServer, IDisposable { // Return a copy with a no-op for dispose return { ...this.connInfo, - dispose: noop }; + dispose: noop + }; } - private generateRequest = (code: string, silent: boolean) : Kernel.IFuture | undefined => { + private generateRequest = (code: string, silent: boolean): Kernel.IFuture | undefined => { //this.logger.logInformation(`Executing code in jupyter : ${code}`) try { return this.session ? this.session.requestExecute( @@ -429,17 +431,17 @@ export class JupyterServer implements INotebookServer, IDisposable { ).ignoreErrors(); } - private combineObservables = (...args : Observable[]) : Observable => { + private combineObservables = (...args: Observable[]): Observable => { return new Observable(subscriber => { // When all complete, we have our results - const results : { [id : string] : ICell } = {}; + const results: { [id: string]: ICell } = {}; args.forEach(o => { o.subscribe(c => { results[c.id] = c; // Convert to an array - const array = Object.keys(results).map((k : string) => { + const array = Object.keys(results).map((k: string) => { return results[k]; }); @@ -453,14 +455,14 @@ export class JupyterServer implements INotebookServer, IDisposable { } } }, - e => { - subscriber.error(e); - }); + e => { + subscriber.error(e); + }); }); }); } - private executeMarkdownObservable = (cell: ICell) : Observable => { + private executeMarkdownObservable = (cell: ICell): Observable => { // Markdown doesn't need any execution return new Observable(subscriber => { subscriber.next(cell); @@ -468,7 +470,7 @@ export class JupyterServer implements INotebookServer, IDisposable { }); } - private changeDirectoryIfPossible = async (directory: string) : Promise => { + private changeDirectoryIfPossible = async (directory: string): Promise => { if (this.connInfo && this.connInfo.localLaunch && await fs.pathExists(directory)) { await this.executeSilently(`%cd "${directory}"`); } @@ -533,7 +535,7 @@ export class JupyterServer implements INotebookServer, IDisposable { } - private executeCodeObservable(cell: ICell) : Observable { + private executeCodeObservable(cell: ICell): Observable { return new Observable(subscriber => { // Tell our listener. NOTE: have to do this asap so that markdown cells don't get // run before our cells. @@ -551,14 +553,14 @@ export class JupyterServer implements INotebookServer, IDisposable { }); } - private addToCellData = (cell: ICell, output : nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError) => { - const data : nbformat.ICodeCell = cell.data as nbformat.ICodeCell; + private addToCellData = (cell: ICell, output: nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError) => { + const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; data.outputs = [...data.outputs, output]; cell.data = data; } private handleExecuteResult(msg: KernelMessage.IExecuteResultMsg, cell: ICell) { - this.addToCellData(cell, { output_type : 'execute_result', data: msg.content.data, metadata : msg.content.metadata, execution_count : msg.content.execution_count }); + this.addToCellData(cell, { output_type: 'execute_result', data: msg.content.data, metadata: msg.content.metadata, execution_count: msg.content.execution_count }); } private handleExecuteInput(msg: KernelMessage.IExecuteInputMsg, cell: ICell) { @@ -580,24 +582,24 @@ export class JupyterServer implements INotebookServer, IDisposable { } private handleStreamMesssage(msg: KernelMessage.IStreamMsg, cell: ICell) { - const output : nbformat.IStream = { - output_type : 'stream', - name : msg.content.name, - text : msg.content.text + const output: nbformat.IStream = { + output_type: 'stream', + name: msg.content.name, + text: msg.content.text }; this.addToCellData(cell, output); } private handleDisplayData(msg: KernelMessage.IDisplayDataMsg, cell: ICell) { - const output : nbformat.IDisplayData = { - output_type : 'display_data', + const output: nbformat.IDisplayData = { + output_type: 'display_data', data: msg.content.data, - metadata : msg.content.metadata + metadata: msg.content.metadata }; this.addToCellData(cell, output); } - private handleInterrupted(cell : ICell) { + private handleInterrupted(cell: ICell) { this.handleError({ channel: 'iopub', parent_header: {}, @@ -616,11 +618,11 @@ export class JupyterServer implements INotebookServer, IDisposable { } private handleError(msg: KernelMessage.IErrorMsg, cell: ICell) { - const output : nbformat.IError = { - output_type : 'error', - ename : msg.content.ename, - evalue : msg.content.evalue, - traceback : msg.content.traceback + const output: nbformat.IError = { + output_type: 'error', + ename: msg.content.ename, + evalue: msg.content.evalue, + traceback: msg.content.traceback }; this.addToCellData(cell, output); cell.state = CellState.error; diff --git a/src/client/extension.ts b/src/client/extension.ts index 5ec9b79e9c7e..e2db8b2ffcd0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -50,6 +50,7 @@ import { ILogger, IMemento, IOutputChannel, + Resource, WORKSPACE_MEMENTO } from './common/types'; import { createDeferred } from './common/utils/async'; @@ -63,6 +64,7 @@ import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IDebugConfigurationService, IDebuggerBanner } from './debugger/extension/types'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from './interpreter/autoSelection/types'; import { IInterpreterSelector } from './interpreter/configuration/types'; import { ICondaService, @@ -109,8 +111,8 @@ export async function activate(context: ExtensionContext): Promise(IInterpreterService); - await interpreterManager.autoSetInterpreter(); + const autoSelection = serviceContainer.get(IInterpreterAutoSelectionService); + await autoSelection.autoSelectInterpreter(undefined); // When testing, do not perform health checks, as modal dialogs can be displayed. if (!isTestExecution()) { @@ -136,6 +138,7 @@ export async function activate(context: ExtensionContext): Promise(IWorkspaceService); + const interpreterManager = serviceContainer.get(IInterpreterService); interpreterManager.refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); @@ -290,7 +293,9 @@ async function sendStartupTelemetry(activatedPromise: Promise, serviceConta const condaLocator = serviceContainer.get(ICondaService); const interpreterService = serviceContainer.get(IInterpreterService); const workspaceService = serviceContainer.get(IWorkspaceService); + const configurationService = serviceContainer.get(IConfigurationService); const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; + const settings = configurationService.getSettings(mainWorkspaceUri); const [condaVersion, interpreter, interpreters] = await Promise.all([ condaLocator.getCondaVersion().then(ver => ver ? ver.raw : '').catch(() => ''), interpreterService.getActiveInterpreter().catch(() => undefined), @@ -299,13 +304,31 @@ async function sendStartupTelemetry(activatedPromise: Promise, serviceConta const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; const interpreterType = interpreter ? interpreter.type : undefined; + const hasUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); + const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); + const isAutoSelectedWorkspaceInterpreterUsed = preferredWorkspaceInterpreter ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) : undefined; const hasPython3 = interpreters .filter(item => item && item.version ? item.version.major === 3 : false) .length > 0; - const props = { condaVersion, terminal: terminalShellType, pythonVersion, interpreterType, workspaceFolderCount, hasPython3 }; + const props = { + condaVersion, terminal: terminalShellType, pythonVersion, interpreterType, workspaceFolderCount, hasPython3, + hasUserDefinedInterpreter, isAutoSelectedWorkspaceInterpreterUsed + }; sendTelemetryEvent(EDITOR_LOAD, durations, props); } catch (ex) { logger.logError('sendStartupTelemetry failed.', ex); } } +function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { + const workspaceService = serviceContainer.get(IWorkspaceService); + const settings = workspaceService.getConfiguration('python', resource)!.inspect('pyhontPath')!; + return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || + (settings.workspaceValue && settings.workspaceValue !== 'python') || + (settings.globalValue && settings.globalValue !== 'python'); +} +function getPreferredWorkspaceInterpreter(resource: Resource, serviceContainer: IServiceContainer) { + const workspaceInterpreterSelector = serviceContainer.get(IInterpreterAutoSelectionRule, AutoSelectionRule.workspaceVirtualEnvs); + const interpreter = workspaceInterpreterSelector.getPreviouslyAutoSelectedInterpreter(resource); + return interpreter ? interpreter.path : undefined; +} diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts new file mode 100644 index 000000000000..43fbffa515e7 --- /dev/null +++ b/src/client/interpreter/autoSelection/index.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { compare } from 'semver'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; +import { IFileSystem } from '../../common/platform/types'; +import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { IInterpreterHelper, PythonInterpreter } from '../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from './types'; + +const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; +const workspacePathNameForGlobalWorkspaces = ''; + +@injectable() +export class InterpreterAutoSelectionService implements IInterpreterAutoSelectionService { + private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); + private readonly autoSelectedInterpreterByWorkspace = new Map(); + private globallyPreferredInterpreter!: IPersistentState; + private readonly rules: IInterpreterAutoSelectionRule[] = []; + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.systemWide) systemInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.currentPath) currentPathInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.windowsRegistry) winRegInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.cachedInterpreters) cachedPaths: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.settings) private readonly userDefinedInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.workspaceVirtualEnvs) workspaceInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSeletionProxyService) proxy: IInterpreterAutoSeletionProxyService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) { + + // It is possible we area always opening the same workspace folder, but we still need to determine and cache + // the best available interpreters based on other rules (cache for furture use). + this.rules.push(...[winRegInterpreter, currentPathInterpreter, systemInterpreter, cachedPaths, userDefinedInterpreter, workspaceInterpreter]); + proxy.registerInstance!(this); + // Rules are as follows in order + // 1. First check user settings.json + // If we have user settings, then always use that, do not proceed. + // 2. Check workspace virtual environments (pipenv, etc). + // If we have some, then use those as preferred workspace environments. + // 3. Check list of cached interpreters (previously cachced from all the rules). + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + // 4. Check current path. + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + // 5. Check windows registry. + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + // 6. Check the entire system. + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + userDefinedInterpreter.setNextRule(workspaceInterpreter); + workspaceInterpreter.setNextRule(cachedPaths); + cachedPaths.setNextRule(currentPathInterpreter); + currentPathInterpreter.setNextRule(winRegInterpreter); + winRegInterpreter.setNextRule(systemInterpreter); + } + public async autoSelectInterpreter(resource: Resource): Promise { + Promise.all(this.rules.map(item => item.autoSelectInterpreter(undefined))).ignoreErrors(); + await this.initializeStore(); + await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); + this.didAutoSelectedInterpreterEmitter.fire(); + } + public get onDidChangeAutoSelectedInterpreter(): Event { + return this.didAutoSelectedInterpreterEmitter.event; + } + public getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { + // Do not execute anycode other than fetching fromm a property. + // This method gets invoked from settings class, and this class in turn uses classes that relies on settings. + // I.e. we can end up in a recursive loop. + const workspaceState = this.getWorkspaceState(resource); + if (workspaceState && workspaceState.value) { + return workspaceState.value; + } + + const workspaceFolderPath = this.getWorkspacePathKey(resource); + if (this.autoSelectedInterpreterByWorkspace.has(workspaceFolderPath)) { + return this.autoSelectedInterpreterByWorkspace.get(workspaceFolderPath); + } + + return this.globallyPreferredInterpreter.value; + } + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined) { + await this.storeAutoSelectedInterperter(resource, interpreter); + } + public async setGlobalInterpreter(interpreter: PythonInterpreter) { + await this.storeAutoSelectedInterperter(undefined, interpreter); + } + protected async storeAutoSelectedInterperter(resource: Resource, interpreter: PythonInterpreter | undefined) { + const workspaceFolderPath = this.getWorkspacePathKey(resource); + if (workspaceFolderPath === workspacePathNameForGlobalWorkspaces) { + // Update store only if this version is better. + if (this.globallyPreferredInterpreter.value && + this.globallyPreferredInterpreter.value.version && + interpreter && interpreter.version && + compare(this.globallyPreferredInterpreter.value.version.raw, interpreter.version.raw) > 0) { + return; + } + + // Don't pass in manager instance, as we don't want any updates to take place. + await this.globallyPreferredInterpreter.updateValue(interpreter); + this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); + } else { + const workspaceState = this.getWorkspaceState(resource); + if (workspaceState && interpreter) { + await workspaceState.updateValue(interpreter); + } + this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); + } + + this.didAutoSelectedInterpreterEmitter.fire(); + } + protected async initializeStore() { + if (this.globallyPreferredInterpreter) { + return; + } + await this.clearStoreIfFileIsInvalid(); + } + private async clearStoreIfFileIsInvalid() { + this.globallyPreferredInterpreter = this.stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined); + if (this.globallyPreferredInterpreter.value && !await this.fs.fileExists(this.globallyPreferredInterpreter.value.path)) { + await this.globallyPreferredInterpreter.updateValue(undefined); + } + } + private getWorkspacePathKey(resource: Resource): string { + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + return workspaceFolder ? workspaceFolder.uri.fsPath : workspacePathNameForGlobalWorkspaces; + } + private getWorkspaceState(resource: Resource): undefined | IPersistentState { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + if (!workspaceUri) { + return; + } + const key = `autoSelectedWorkspacePythonInterpreter-${workspaceUri.folderUri.fsPath}`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); + } +} diff --git a/src/client/interpreter/autoSelection/proxy.ts b/src/client/interpreter/autoSelection/proxy.ts new file mode 100644 index 000000000000..fae3bd443c4f --- /dev/null +++ b/src/client/interpreter/autoSelection/proxy.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { IAsyncDisposableRegistry, IDisposableRegistry, Resource } from '../../common/types'; +import { PythonInterpreter } from '../contracts'; +import { IInterpreterAutoSeletionProxyService } from './types'; + +@injectable() +export class InterpreterAutoSeletionProxyService implements IInterpreterAutoSeletionProxyService { + private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); + private instance?: IInterpreterAutoSeletionProxyService; + constructor(@inject(IDisposableRegistry) private readonly disposables: IAsyncDisposableRegistry) { } + public registerInstance(instance: IInterpreterAutoSeletionProxyService): void { + this.instance = instance; + this.disposables.push(this.instance.onDidChangeAutoSelectedInterpreter(() => this.didAutoSelectedInterpreterEmitter.fire())); + } + public get onDidChangeAutoSelectedInterpreter(): Event { + return this.didAutoSelectedInterpreterEmitter.event; + } + public getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { + return this.instance ? this.instance.getAutoSelectedInterpreter(resource) : undefined; + } + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise{ + return this.instance ? this.instance.setWorkspaceInterpreter(resource, interpreter) : undefined; + } +} diff --git a/src/client/interpreter/autoSelection/rules/baseRule.ts b/src/client/interpreter/autoSelection/rules/baseRule.ts new file mode 100644 index 000000000000..e4fa897dd19a --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/baseRule.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, unmanaged } from 'inversify'; +import { compare } from 'semver'; +import '../../../common/extensions'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentState, IPersistentStateFactory, Resource } from '../../../common/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { PYTHON_INTERPRETER_AUTO_SELECTION } from '../../../telemetry/constants'; +import { PythonInterpreter } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; + +export enum NextAction { + runNextRule = 'runNextRule', + exit = 'exit' +} + +@injectable() +export abstract class BaseRuleService implements IInterpreterAutoSelectionRule { + protected nextRule?: IInterpreterAutoSelectionRule; + private readonly stateStore: IPersistentState; + constructor(@unmanaged() private readonly ruleName: AutoSelectionRule, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory) { + this.stateStore = stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${this.ruleName}`, undefined); + } + public setNextRule(rule: IInterpreterAutoSelectionRule): void { + this.nextRule = rule; + } + public async autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + await this.clearCachedInterpreterIfInvalid(resource); + const stopWatch = new StopWatch(); + const action = await this.onAutoSelectInterpreter(resource, manager); + const identified = action === NextAction.runNextRule; + sendTelemetryEvent(PYTHON_INTERPRETER_AUTO_SELECTION, { elapsedTime: stopWatch.elapsedTime }, { rule: this.ruleName, identified }); + if (action === NextAction.runNextRule) { + await this.next(resource, manager); + } + } + public getPreviouslyAutoSelectedInterpreter(_resource: Resource): PythonInterpreter | undefined { + return this.stateStore.value; + } + protected abstract onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise; + protected async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + await this.cacheSelectedInterpreter(undefined, interpreter); + if (!interpreter || !manager || !interpreter.version) { + return false; + } + const preferredInterpreter = manager.getAutoSelectedInterpreter(undefined); + const comparison = preferredInterpreter && preferredInterpreter.version ? compare(interpreter.version.raw, preferredInterpreter.version.raw) : 1; + if (comparison > 0) { + await manager.setGlobalInterpreter(interpreter); + return true; + } + if (comparison === 0) { + return true; + } + + return false; + } + protected async clearCachedInterpreterIfInvalid(resource: Resource) { + if (!this.stateStore.value || await this.fs.fileExists(this.stateStore.value.path)) { + return; + } + sendTelemetryEvent(PYTHON_INTERPRETER_AUTO_SELECTION, {}, { rule: this.ruleName, interpreterMissing: true }); + await this.cacheSelectedInterpreter(resource, undefined); + } + protected async cacheSelectedInterpreter(_resource: Resource, interpreter: PythonInterpreter | undefined) { + const interpreterPath = interpreter ? interpreter.path : ''; + const interpreterPathInCache = this.stateStore.value ? this.stateStore.value.path : ''; + const updated = interpreterPath === interpreterPathInCache; + sendTelemetryEvent(PYTHON_INTERPRETER_AUTO_SELECTION, {}, { rule: this.ruleName, updated }); + await this.stateStore.updateValue(interpreter); + } + protected async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return this.nextRule && manager ? this.nextRule.autoSelectInterpreter(resource, manager) : undefined; + } +} diff --git a/src/client/interpreter/autoSelection/rules/cached.ts b/src/client/interpreter/autoSelection/rules/cached.ts new file mode 100644 index 000000000000..cbc3fe1ef01b --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/cached.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { IInterpreterHelper } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class CachedInterpretersAutoSelectionRule extends BaseRuleService { + protected readonly rules: IInterpreterAutoSelectionRule[]; + constructor(@inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.systemWide) systemInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.currentPath) currentPathInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.windowsRegistry) winRegInterpreter: IInterpreterAutoSelectionRule) { + + super(AutoSelectionRule.cachedInterpreters, fs, stateFactory); + this.rules = [systemInterpreter, currentPathInterpreter, winRegInterpreter]; + } + protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + const cachedInterpreters = this.rules + .map(item => item.getPreviouslyAutoSelectedInterpreter(resource)) + .filter(item => !!item) + .map(item => item!); + const bestInterpreter = this.helper.getBestInterpreter(cachedInterpreters); + return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/currentPath.ts b/src/client/interpreter/autoSelection/rules/currentPath.ts new file mode 100644 index 000000000000..b277cfe9ab31 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/currentPath.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { CURRENT_PATH_SERVICE, IInterpreterHelper, IInterpreterLocatorService } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class CurrentPathInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IInterpreterLocatorService) @named(CURRENT_PATH_SERVICE) private readonly currentPathInterpreterLocator: IInterpreterLocatorService) { + + super(AutoSelectionRule.currentPath, fs, stateFactory); + } + protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + const interpreters = await this.currentPathInterpreterLocator.getInterpreters(resource); + const bestInterpreter = this.helper.getBestInterpreter(interpreters); + return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/settings.ts b/src/client/interpreter/autoSelection/rules/settings.ts new file mode 100644 index 000000000000..793cbd128009 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/settings.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class SettingsInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) { + + super(AutoSelectionRule.settings, fs, stateFactory); + } + protected async onAutoSelectInterpreter(_resource: Resource, _manager?: IInterpreterAutoSelectionService): Promise { + // tslint:disable-next-line:no-any + const pythonConfig = this.workspaceService.getConfiguration('python', null as any)!; + const pythonPathInConfig = pythonConfig.inspect('pythonPath')!; + // No need to store python paths defined in settings in our caches, they can be retrieved from the settings directly. + return (pythonPathInConfig.globalValue && pythonPathInConfig.globalValue !== 'python') ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/system.ts b/src/client/interpreter/autoSelection/rules/system.ts new file mode 100644 index 000000000000..15bee26fd16d --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/system.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class SystemWideInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService) { + + super(AutoSelectionRule.systemWide, fs, stateFactory); + } + protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + const interpreters = await this.interpreterService.getInterpreters(resource); + // Exclude non-local interpreters. + const filteredInterpreters = interpreters.filter(int => int.type !== InterpreterType.VirtualEnv && + int.type !== InterpreterType.Venv && + int.type !== InterpreterType.PipEnv); + const bestInterpreter = this.helper.getBestInterpreter(filteredInterpreters); + return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/winRegistry.ts b/src/client/interpreter/autoSelection/rules/winRegistry.ts new file mode 100644 index 000000000000..e043f751539a --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/winRegistry.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { OSType } from '../../../common/utils/platform'; +import { IInterpreterHelper, IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class WindowsRegistryInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) private winRegInterpreterLocator: IInterpreterLocatorService) { + + super(AutoSelectionRule.windowsRegistry, fs, stateFactory); + } + protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + if (this.platform.osType !== OSType.Windows) { + return NextAction.runNextRule; + } + const interpreters = await this.winRegInterpreterLocator.getInterpreters(resource); + const bestInterpreter = this.helper.getBestInterpreter(interpreters); + return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts new file mode 100644 index 000000000000..6a4230391dad --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { createDeferredFromPromise } from '../../../common/utils/async'; +import { OSType } from '../../../common/utils/platform'; +import { IPythonPathUpdaterServiceManager } from '../../configuration/types'; +import { IInterpreterHelper, IInterpreterLocatorService, PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPythonPathUpdaterServiceManager) private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IInterpreterLocatorService) @named(PIPENV_SERVICE) private readonly pipEnvInterpreterLocator: IInterpreterLocatorService, + @inject(IInterpreterLocatorService) @named(WORKSPACE_VIRTUAL_ENV_SERVICE) private readonly workspaceVirtualEnvInterpreterLocator: IInterpreterLocatorService) { + + super(AutoSelectionRule.workspaceVirtualEnvs, fs, stateFactory); + } + protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + const workspacePath = this.helper.getActiveWorkspaceUri(resource); + if (!workspacePath) { + return NextAction.runNextRule; + } + + const pythonConfig = this.workspaceService.getConfiguration('python', workspacePath.folderUri)!; + const pythonPathInConfig = pythonConfig.inspect('pythonPath')!; + // If user has defined custom values in settings for this workspace folder, then use that. + if (pythonPathInConfig.workspaceFolderValue) { + return NextAction.runNextRule; + } + const pipEnvPromise = createDeferredFromPromise(this.pipEnvInterpreterLocator.getInterpreters(workspacePath.folderUri)); + 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. + // If users have this, then theu can specify which one is to be used. + const interpreters = await Promise.race([pipEnvPromise.promise, virtualEnvPromise.promise]); + let bestInterpreter: PythonInterpreter | undefined; + if (Array.isArray(interpreters) && interpreters.length > 0) { + bestInterpreter = this.helper.getBestInterpreter(interpreters); + } else { + const [pipEnv, virtualEnv] = await Promise.all([pipEnvPromise.promise, virtualEnvPromise.promise]); + const pipEnvList = Array.isArray(pipEnv) ? pipEnv : []; + const virtualEnvList = Array.isArray(virtualEnv) ? virtualEnv : []; + bestInterpreter = this.helper.getBestInterpreter(pipEnvList.concat(virtualEnvList)); + } + if (bestInterpreter && manager) { + await this.cacheSelectedInterpreter(workspacePath.folderUri, bestInterpreter); + await manager.setWorkspaceInterpreter(workspacePath.folderUri!, bestInterpreter); + } + return NextAction.runNextRule; + } + protected async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise { + if (!resource) { + return; + } + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if (!workspaceFolder) { + return; + } + // Now check virtual environments under the workspace root + const interpreters = await this.workspaceVirtualEnvInterpreterLocator.getInterpreters(resource, true); + const workspacePath = this.platform.osType === OSType.Windows ? workspaceFolder.uri.fsPath.toUpperCase() : workspaceFolder.uri.fsPath; + + return interpreters.filter(interpreter => { + const fsPath = Uri.file(interpreter.path).fsPath; + const fsPathToCompare = this.platform.osType === OSType.Windows ? fsPath.toUpperCase() : fsPath; + return fsPathToCompare.startsWith(workspacePath); + }); + } + protected async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { + // We should never clear settings in user settings.json. + if (!interpreter) { + return; + } + const activeWorkspace = this.helper.getActiveWorkspaceUri(resource); + if (!activeWorkspace) { + return; + } + await this.pythonPathUpdaterService.updatePythonPath(interpreter.path, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); + await super.cacheSelectedInterpreter(resource, interpreter); + } +} diff --git a/src/client/interpreter/autoSelection/types.ts b/src/client/interpreter/autoSelection/types.ts new file mode 100644 index 000000000000..4cd37b359358 --- /dev/null +++ b/src/client/interpreter/autoSelection/types.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Event, Uri } from 'vscode'; +import { Resource } from '../../common/types'; +import { PythonInterpreter } from '../contracts'; + +export const IInterpreterAutoSeletionProxyService = Symbol('IInterpreterAutoSeletionProxyService'); +/** + * Interface similar to IInterpreterAutoSelectionService, to avoid chickn n egg situation. + * Do we get python path from config first or get auto selected interpreter first!? + * However, the class that reads python Path, must first give preference to selected interpreter. + * But all classes everywhere make use of python settings! + * Solution - Use a proxy that does nothing first, but later the real instance is injected. + * + * @export + * @interface IInterpreterAutoSeletionProxyService + */ +export interface IInterpreterAutoSeletionProxyService { + readonly onDidChangeAutoSelectedInterpreter: Event; + getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; + registerInstance?(instance: IInterpreterAutoSeletionProxyService): void; + setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise; +} + +export const IInterpreterAutoSelectionService = Symbol('IInterpreterAutoSelectionService'); +export interface IInterpreterAutoSelectionService extends IInterpreterAutoSeletionProxyService { + readonly onDidChangeAutoSelectedInterpreter: Event; + autoSelectInterpreter(resource: Resource): Promise; + setGlobalInterpreter(interpreter: PythonInterpreter | undefined): Promise; +} + +export enum AutoSelectionRule { + currentPath = 'currentPath', + workspaceVirtualEnvs = 'workspaceEnvs', + settings = 'settings', + cachedInterpreters = 'cachedInterpreters', + systemWide = 'system', + windowsRegistry = 'windowsRegistry' +} + +export const IInterpreterAutoSelectionRule = Symbol('IInterpreterAutoSelectionRule'); +export interface IInterpreterAutoSelectionRule { + setNextRule(rule: IInterpreterAutoSelectionRule): void; + autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise; + getPreviouslyAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 53a95711eb6d..846375450f12 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,6 +1,7 @@ import { SemVer } from 'semver'; import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode'; import { InterpreterInfomation } from '../common/process/types'; +import { Resource } from '../common/types'; export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; export const WINDOWS_REGISTRY_SERVICE = 'WindowsRegistryService'; @@ -84,13 +85,11 @@ export interface IInterpreterService { onDidChangeInterpreter: Event; hasInterpreters: Promise; getInterpreters(resource?: Uri): Promise; - autoSetInterpreter(): Promise; getActiveInterpreter(resource?: Uri): Promise; getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; refresh(resource: Uri | undefined): Promise; initialize(): void; getDisplayName(interpreter: Partial): Promise; - shouldAutoSetInterpreter(): Promise; } export const IInterpreterDisplay = Symbol('IInterpreterDisplay'); @@ -105,10 +104,11 @@ export interface IShebangCodeLensProvider extends CodeLensProvider { export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { - getActiveWorkspaceUri(): WorkspacePythonPath | undefined; + getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined; getInterpreterInformation(pythonPath: string): Promise>; isMacDefaultPythonPath(pythonPath: string): Boolean; getInterpreterTypeDisplayName(interpreterType: InterpreterType): string | undefined; + getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined; } export const IPipEnvService = Symbol('IPipEnvService'); diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 281ad604f940..975c4a60473f 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -33,7 +33,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { resource = this.workspaceService.getWorkspaceFolder(resource)!.uri; } if (!resource) { - const wkspc = this.helper.getActiveWorkspaceUri(); + const wkspc = this.helper.getActiveWorkspaceUri(resource); resource = wkspc ? wkspc.folderUri : undefined; } await this.updateDisplay(resource); diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index c0e965239216..9607cc499901 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,9 +1,10 @@ import { inject, injectable } from 'inversify'; +import { compare } from 'semver'; import { ConfigurationTarget } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; import { IFileSystem } from '../common/platform/types'; import { InterpreterInfomation, IPythonExecutionFactory } from '../common/process/types'; -import { IPersistentStateFactory } from '../common/types'; +import { IPersistentStateFactory, Resource } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { IInterpreterHelper, InterpreterType, PythonInterpreter, WorkspacePythonPath } from './contracts'; @@ -26,16 +27,23 @@ export class InterpreterHelper implements IInterpreterHelper { this.persistentFactory = this.serviceContainer.get(IPersistentStateFactory); this.fs = this.serviceContainer.get(IFileSystem); } - public getActiveWorkspaceUri(): WorkspacePythonPath | undefined { + public getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined { const workspaceService = this.serviceContainer.get(IWorkspaceService); - const documentManager = this.serviceContainer.get(IDocumentManager); - if (!workspaceService.hasWorkspaceFolders) { return; } if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length === 1) { return { folderUri: workspaceService.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; } + + if (resource) { + const workspaceFolder = workspaceService.getWorkspaceFolder(resource); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } + } + const documentManager = this.serviceContainer.get(IDocumentManager); + if (documentManager.activeTextEditor) { const workspaceFolder = workspaceService.getWorkspaceFolder(documentManager.activeTextEditor.document.uri); if (workspaceFolder) { @@ -46,7 +54,7 @@ export class InterpreterHelper implements IInterpreterHelper { public async getInterpreterInformation(pythonPath: string): Promise> { let fileHash = await this.fs.getFileHash(pythonPath).catch(() => ''); fileHash = fileHash ? fileHash : ''; - const store = this.persistentFactory.createGlobalPersistentState(`${pythonPath}.v2`, undefined, EXPITY_DURATION); + const store = this.persistentFactory.createGlobalPersistentState(`${pythonPath}.v3`, undefined, EXPITY_DURATION); if (store.value && fileHash && store.value.fileHash === fileHash) { return store.value; } @@ -93,4 +101,15 @@ export class InterpreterHelper implements IInterpreterHelper { } } } + public getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined { + if (!Array.isArray(interpreters) || interpreters.length === 0) { + return; + } + if (interpreters.length === 1) { + return interpreters[0]; + } + const sorted = interpreters.slice(); + sorted.sort((a, b) => (a.version && b.version) ? compare(a.version.raw, b.version.raw) : 0); + return sorted[sorted.length - 1]; + } } diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 71d6a91ea6c8..95d0fb57654d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -1,21 +1,20 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { ConfigurationTarget, Disposable, Event, EventEmitter, Uri } from 'vscode'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; import '../../client/common/extensions'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PythonSettings } from '../common/configSettings'; import { getArchitectureDisplayName } from '../common/platform/registry'; import { IFileSystem } from '../common/platform/types'; import { IPythonExecutionFactory } from '../common/process/types'; import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory } from '../common/types'; import { sleep } from '../common/utils/async'; import { IServiceContainer } from '../ioc/types'; -import { IPythonPathUpdaterServiceManager } from './configuration/types'; +import { captureTelemetry } from '../telemetry'; +import { PYTHON_INTERPRETER_DISCOVERY } from '../telemetry/constants'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, - InterpreterType, PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE -} from './contracts'; + InterpreterType, PythonInterpreter} from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; const EXPITY_DURATION = 24 * 60 * 60 * 1000; @@ -23,18 +22,14 @@ const EXPITY_DURATION = 24 * 60 * 60 * 1000; @injectable() export class InterpreterService implements Disposable, IInterpreterService { private readonly locator: IInterpreterLocatorService; - private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager; private readonly fs: IFileSystem; private readonly persistentStateFactory: IPersistentStateFactory; - private readonly helper: IInterpreterHelper; private readonly configService: IConfigurationService; private readonly didChangeInterpreterEmitter = new EventEmitter(); private pythonPathSetting: string = ''; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.locator = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - this.helper = serviceContainer.get(IInterpreterHelper); - this.pythonPathUpdaterService = this.serviceContainer.get(IPythonPathUpdaterServiceManager); this.fs = this.serviceContainer.get(IFileSystem); this.persistentStateFactory = this.serviceContainer.get(IPersistentStateFactory); this.configService = this.serviceContainer.get(IConfigurationService); @@ -52,11 +47,18 @@ export class InterpreterService implements Disposable, IInterpreterService { const disposables = this.serviceContainer.get(IDisposableRegistry); const documentManager = this.serviceContainer.get(IDocumentManager); disposables.push(documentManager.onDidChangeActiveTextEditor((e) => e ? this.refresh(e.document.uri) : undefined)); - const pySettings = this.configService.getSettings() as PythonSettings; + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const pySettings = this.configService.getSettings(); this.pythonPathSetting = pySettings.pythonPath; - pySettings.addListener('change', this.onConfigChanged); + const disposable = workspaceService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('python.pythonPath', undefined)) { + this.onConfigChanged(); + } + }); + disposables.push(disposable); } + @captureTelemetry(PYTHON_INTERPRETER_DISCOVERY, { locator: 'all' }, true) public async getInterpreters(resource?: Uri): Promise { const interpreters = await this.locator.getInterpreters(resource); await Promise.all(interpreters @@ -65,46 +67,8 @@ export class InterpreterService implements Disposable, IInterpreterService { return interpreters; } - public async autoSetInterpreter(): Promise { - if (!await this.shouldAutoSetInterpreter()) { - return; - } - const activeWorkspace = this.helper.getActiveWorkspaceUri(); - if (!activeWorkspace) { - return; - } - // Check pipenv first. - const pipenvService = this.serviceContainer.get(IInterpreterLocatorService, PIPENV_SERVICE); - let interpreters = await pipenvService.getInterpreters(activeWorkspace.folderUri, true); - if (interpreters.length > 0) { - await this.pythonPathUpdaterService.updatePythonPath(interpreters[0].path, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); - return; - } - // Now check virtual environments under the workspace root - const virtualEnvInterpreterProvider = this.serviceContainer.get(IInterpreterLocatorService, WORKSPACE_VIRTUAL_ENV_SERVICE); - interpreters = await virtualEnvInterpreterProvider.getInterpreters(activeWorkspace.folderUri, true); - const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); - - const interpretersInWorkspace = interpreters.filter(interpreter => Uri.file(interpreter.path).fsPath.toUpperCase().startsWith(workspacePathUpper)); - if (interpretersInWorkspace.length === 0) { - return; - } - // Always pick the highest version by default. - interpretersInWorkspace.sort((a, b) => (a.version && b.version) ? a.version.compare(b.version) : 0); - const pythonPath = interpretersInWorkspace[0].path; - // Ensure this new environment is at the same level as the current workspace. - // In windows the interpreter is under scripts/python.exe on linux it is under bin/python. - // Meaning the sub directory must be either scripts, bin or other (but only one level deep). - const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); - if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { - await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); - } - } - public dispose(): void { this.locator.dispose(); - const configService = this.serviceContainer.get(IConfigurationService); - (configService.getSettings() as PythonSettings).removeListener('change', this.onConfigChanged); this.didChangeInterpreterEmitter.dispose(); } @@ -235,30 +199,9 @@ export class InterpreterService implements Disposable, IInterpreterService { return displayName; } - public async shouldAutoSetInterpreter(): Promise { - const activeWorkspace = this.helper.getActiveWorkspaceUri(); - if (!activeWorkspace) { - return false; - } - const workspaceService = this.serviceContainer.get(IWorkspaceService); - const pythonConfig = workspaceService.getConfiguration('python', activeWorkspace.folderUri); - const pythonPathInConfig = pythonConfig.inspect('pythonPath'); - // If we have a value in user settings, then don't auto set the interpreter path. - if (pythonPathInConfig && pythonPathInConfig!.globalValue !== undefined && pythonPathInConfig!.globalValue !== 'python') { - return false; - } - if (activeWorkspace.configTarget === ConfigurationTarget.Workspace) { - return pythonPathInConfig!.workspaceValue === undefined || pythonPathInConfig!.workspaceValue === 'python'; - } - if (activeWorkspace.configTarget === ConfigurationTarget.WorkspaceFolder) { - return pythonPathInConfig!.workspaceFolderValue === undefined || pythonPathInConfig!.workspaceFolderValue === 'python'; - } - return false; - } - private onConfigChanged = () => { // Check if we actually changed our python path - const pySettings = this.configService.getSettings() as PythonSettings; + const pySettings = this.configService.getSettings(); if (this.pythonPathSetting !== pySettings.pythonPath) { this.pythonPathSetting = pySettings.pythonPath; this.didChangeInterpreterEmitter.fire(); diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index 5ea4fcc0e6b9..395d3e0929da 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -12,6 +12,8 @@ import { Logger } from '../../../common/logger'; import { IDisposableRegistry, IPersistentStateFactory } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { IServiceContainer } from '../../../ioc/types'; +import { sendTelemetryWhenDone } from '../../../telemetry'; +import { PYTHON_INTERPRETER_DISCOVERY } from '../../../telemetry/constants'; import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts'; @injectable() @@ -21,11 +23,11 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ private readonly handlersAddedToResource = new Set(); private readonly cacheKeyPrefix: string; private readonly locating = new EventEmitter>(); - constructor(@unmanaged() name: string, + constructor(@unmanaged() private readonly name: string, @unmanaged() protected readonly serviceContainer: IServiceContainer, @unmanaged() private cachePerWorkspace: boolean = false) { this._hasInterpreters = createDeferred(); - this.cacheKeyPrefix = `INTERPRETERS_CACHE_v2_${name}`; + this.cacheKeyPrefix = `INTERPRETERS_CACHE_v3_${name}`; } public get onLocating(): Event> { return this.locating.event; @@ -45,13 +47,14 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ this.addHandlersForInterpreterWatchers(cacheKey, resource) .ignoreErrors(); - this.getInterpretersImplementation(resource) + const promise = this.getInterpretersImplementation(resource) .then(async items => { await this.cacheInterpreters(items, resource); deferred!.resolve(items); }) .catch(ex => deferred!.reject(ex)); + sendTelemetryWhenDone(PYTHON_INTERPRETER_DISCOVERY, promise, undefined, { locator: this.name }); this.locating.fire(deferred.promise); } deferred.promise diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts index aada050249eb..b752a7a355d7 100644 --- a/src/client/interpreter/locators/services/condaService.ts +++ b/src/client/interpreter/locators/services/condaService.ts @@ -1,7 +1,6 @@ import { inject, injectable, named, optional } from 'inversify'; import * as path from 'path'; -import { parse, SemVer } from 'semver'; - +import { compare, parse, SemVer } from 'semver'; import { ConfigurationChangeEvent, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; import { Logger } from '../../../common/logger'; @@ -57,7 +56,7 @@ export const CondaGetEnvironmentPrefix = 'Outputting Environment Now...'; */ @injectable() export class CondaService implements ICondaService { - private condaFile!: Promise; + private condaFile?: Promise; private isAvailable: boolean | undefined; private readonly condaHelper = new CondaHelper(); private activatedEnvironmentCache: { [key: string]: NodeJS.ProcessEnv } = {}; @@ -399,7 +398,7 @@ export class CondaService implements ICondaService { private getLatestVersion(interpreters: PythonInterpreter[]) { const sortedInterpreters = interpreters.slice(); // tslint:disable-next-line:no-non-null-assertion - sortedInterpreters.sort((a, b) => (a.version && b.version) ? a.version.compare(b.version) : 0); + sortedInterpreters.sort((a, b) => (a.version && b.version) ? compare(a.version.raw, b.version.raw) : 0); if (sortedInterpreters.length > 0) { return sortedInterpreters[sortedInterpreters.length - 1]; } diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index a9817dd9141d..0c6e842f4f3b 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -6,7 +6,7 @@ import { Uri } from 'vscode'; import { IPlatformService, IRegistry, RegistryHive } from '../../../common/platform/types'; import { IPathUtils } from '../../../common/types'; import { Architecture } from '../../../common/utils/platform'; -import { convertPythonVersionToSemver } from '../../../common/utils/version'; +import { parsePythonVersion } from '../../../common/utils/version'; import { IServiceContainer } from '../../../ioc/types'; import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; @@ -39,8 +39,8 @@ export class WindowsRegistryService extends CacheableLocatorService { } // tslint:disable-next-line:no-empty public dispose() { } - protected getInterpretersImplementation(resource?: Uri): Promise { - return this.getInterpretersFromRegistry(); + protected async getInterpretersImplementation(_resource?: Uri): Promise { + return this.platform.isWindows ? this.getInterpretersFromRegistry() : []; } private async getInterpretersFromRegistry() { // https://github.com/python/peps/blob/master/pep-0514.txt#L357 @@ -131,7 +131,7 @@ export class WindowsRegistryService extends CacheableLocatorService { return { ...(details as PythonInterpreter), path: executablePath, - version: convertPythonVersionToSemver(version), + version: parsePythonVersion(version), companyDisplayName: interpreterInfo.companyDisplayName, type: InterpreterType.Unknown } as PythonInterpreter; diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 7256389bca62..13cfc74065f0 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -1,8 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IsWindows } from '../common/types'; import { IServiceManager } from '../ioc/types'; +import { InterpreterAutoSelectionService } from './autoSelection/index'; +import { InterpreterAutoSeletionProxyService } from './autoSelection/proxy'; +import { CachedInterpretersAutoSelectionRule } from './autoSelection/rules/cached'; +import { CurrentPathInterpretersAutoSelectionRule } from './autoSelection/rules/currentPath'; +import { SettingsInterpretersAutoSelectionRule } from './autoSelection/rules/settings'; +import { SystemWideInterpretersAutoSelectionRule } from './autoSelection/rules/system'; +import { WindowsRegistryInterpretersAutoSelectionRule } from './autoSelection/rules/winRegistry'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from './autoSelection/rules/workspaceEnv'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from './autoSelection/types'; import { InterpreterComparer } from './configuration/interpreterComparer'; import { InterpreterSelector } from './configuration/interpreterSelector'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; @@ -80,10 +88,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); serviceManager.addSingleton(IPipEnvService, PipEnvService); - const isWindows = serviceManager.get(IsWindows); - if (isWindows) { - serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); - } + serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); serviceManager.addSingleton(IInterpreterService, InterpreterService); serviceManager.addSingleton(IInterpreterDisplay, InterpreterDisplay); @@ -99,4 +104,13 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(InterpreterLocatorProgressHandler, InterpreterLocatorProgressStatubarHandler); serviceManager.addSingleton(IInterpreterLocatorProgressService, InterpreterLocatorProgressService); + + serviceManager.addSingleton(IInterpreterAutoSelectionRule, CurrentPathInterpretersAutoSelectionRule, AutoSelectionRule.currentPath); + serviceManager.addSingleton(IInterpreterAutoSelectionRule, SystemWideInterpretersAutoSelectionRule, AutoSelectionRule.systemWide); + serviceManager.addSingleton(IInterpreterAutoSelectionRule, WindowsRegistryInterpretersAutoSelectionRule, AutoSelectionRule.windowsRegistry); + serviceManager.addSingleton(IInterpreterAutoSelectionRule, WorkspaceVirtualEnvInterpretersAutoSelectionRule, AutoSelectionRule.workspaceVirtualEnvs); + serviceManager.addSingleton(IInterpreterAutoSelectionRule, CachedInterpretersAutoSelectionRule, AutoSelectionRule.cachedInterpreters); + serviceManager.addSingleton(IInterpreterAutoSelectionRule, SettingsInterpretersAutoSelectionRule, AutoSelectionRule.settings); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, InterpreterAutoSeletionProxyService); + serviceManager.addSingleton(IInterpreterAutoSelectionService, InterpreterAutoSelectionService); } diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts index c28321e59d96..3c2a6dcfd664 100644 --- a/src/client/linters/linterCommands.ts +++ b/src/client/linters/linterCommands.ts @@ -2,23 +2,26 @@ // Licensed under the MIT License. 'use strict'; -import * as vscode from 'vscode'; -import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { DiagnosticCollection, Disposable, QuickPickOptions, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; import { Commands } from '../common/constants'; +import { IDisposable } from '../common/types'; import { Linters } from '../common/utils/localize'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; import { SELECT_LINTER } from '../telemetry/constants'; import { ILinterManager, ILintingEngine, LinterId } from './types'; -export class LinterCommands implements vscode.Disposable { - private disposables: vscode.Disposable[] = []; +export class LinterCommands implements IDisposable { + private disposables: Disposable[] = []; private linterManager: ILinterManager; - private appShell: IApplicationShell; + private readonly appShell: IApplicationShell; + private readonly documentManager: IDocumentManager; constructor(private serviceContainer: IServiceContainer) { this.linterManager = this.serviceContainer.get(ILinterManager); this.appShell = this.serviceContainer.get(IApplicationShell); + this.documentManager = this.serviceContainer.get(IDocumentManager); const commandManager = this.serviceContainer.get(ICommandManager); commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); @@ -48,7 +51,7 @@ export class LinterCommands implements vscode.Disposable { break; } - const quickPickOptions: vscode.QuickPickOptions = { + const quickPickOptions: QuickPickOptions = { matchOnDetail: true, matchOnDescription: true, placeHolder: `current: ${current}` @@ -77,7 +80,7 @@ export class LinterCommands implements vscode.Disposable { const options = ['on', 'off']; const current = await this.linterManager.isLintingEnabled(true, this.settingsUri) ? options[0] : options[1]; - const quickPickOptions: vscode.QuickPickOptions = { + const quickPickOptions: QuickPickOptions = { matchOnDetail: true, matchOnDescription: true, placeHolder: `current: ${current}` @@ -90,12 +93,12 @@ export class LinterCommands implements vscode.Disposable { } } - public runLinting(): Promise { + public runLinting(): Promise { const engine = this.serviceContainer.get(ILintingEngine); return engine.lintOpenPythonFiles(); } - private get settingsUri(): vscode.Uri | undefined { - return vscode.window.activeTextEditor ? vscode.window.activeTextEditor.document.uri : undefined; + private get settingsUri(): Uri | undefined { + return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; } } diff --git a/src/client/linters/linterManager.ts b/src/client/linters/linterManager.ts index af82baff3c62..1b8620fb9fc3 100644 --- a/src/client/linters/linterManager.ts +++ b/src/client/linters/linterManager.ts @@ -37,7 +37,7 @@ class DisabledLinter implements ILinter { @injectable() export class LinterManager implements ILinterManager { - private linters: ILinterInfo[]; + protected linters: ILinterInfo[]; private configService: IConfigurationService; private checkedForInstalledLinters = new Set(); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 7e96c3821263..04652b62f053 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -22,6 +22,8 @@ export const REFACTOR_EXTRACT_VAR = 'REFACTOR_EXTRACT_VAR'; export const REFACTOR_EXTRACT_FUNCTION = 'REFACTOR_EXTRACT_FUNCTION'; export const REPL = 'REPL'; export const PYTHON_INTERPRETER = 'PYTHON_INTERPRETER'; +export const PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY'; +export const PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION'; export const WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD'; export const WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO'; export const EXECUTION_CODE = 'EXECUTION_CODE'; diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index 39bd5fe8965f..3242aad30776 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -4,6 +4,7 @@ import { TerminalShellType } from '../common/terminal/types'; import { DebugConfigurationType } from '../debugger/extension/types'; +import { AutoSelectionRule } from '../interpreter/autoSelection/types'; import { InterpreterType } from '../interpreter/contracts'; import { LinterId } from '../linters/types'; import { PlatformErrors } from './constants'; @@ -11,6 +12,8 @@ import { PlatformErrors } from './constants'; export type EditorLoadTelemetry = { condaVersion: string | undefined; terminal: TerminalShellType; + hasUserDefinedInterpreter: boolean; + isAutoSelectedWorkspaceInterpreterUsed: boolean; }; export type FormatTelemetry = { tool: 'autopep8' | 'black' | 'yapf'; @@ -153,6 +156,16 @@ export type Platform = { osVersion?: string; }; +export type InterpreterAutoSelection = { + rule?: AutoSelectionRule; + interpreterMissing?: boolean; + identified?: boolean; + updated?: boolean; +}; +export type InterpreterDiscovery = { + locator: string; +}; + export type TelemetryProperties = FormatTelemetry | LanguageServerVersionTelemetry | LanguageServerErrorTelemetry @@ -173,4 +186,6 @@ export type TelemetryProperties = FormatTelemetry | ImportNotebook | Platform | LanguageServePlatformSupported - | DebuggerConfigurationPromtpsTelemetry; + | DebuggerConfigurationPromtpsTelemetry + | InterpreterAutoSelection + | InterpreterDiscovery; diff --git a/src/test/common.ts b/src/test/common.ts index 056c93e5ffa9..28794e639399 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -10,10 +10,11 @@ import * as fs from 'fs-extra'; import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; -import { ConfigurationTarget, TextDocument, Uri } from 'vscode'; +import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; import { IExtensionApi } from '../client/api'; import { IProcessService } from '../client/common/process/types'; -import { IPythonSettings } from '../client/common/types'; +import { IPythonSettings, Resource } from '../client/common/types'; +import { PythonInterpreter } from '../client/interpreter/contracts'; import { IServiceContainer } from '../client/ioc/types'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_MULTI_ROOT_TEST, IS_PERF_TEST, IS_SMOKE_TEST } from './constants'; import { noop, sleep } from './core'; @@ -110,8 +111,23 @@ function getWorkspaceRoot() { } export function getExtensionSettings(resource: Uri | undefined): IPythonSettings { + const vscode = require('vscode') as typeof import('vscode'); + class AutoSelectionService { + get onDidChangeAutoSelectedInterpreter(): Event { + return new vscode.EventEmitter().event; + } + public autoSelectInterpreter(_resource: Resource): Promise { + return Promise.resolve(); + } + public getAutoSelectedInterpreter(_resource: Resource): PythonInterpreter | undefined { + return; + } + public async setWorkspaceInterpreter(_resource: Uri, _interpreter: PythonInterpreter | undefined): Promise { + return; + } + } const pythonSettings = require('../client/common/configSettings') as typeof import('../client/common/configSettings'); - return pythonSettings.PythonSettings.getInstance(resource); + return pythonSettings.PythonSettings.getInstance(resource, new AutoSelectionService()); } export function retryAsync(wrapped: Function, retryCount: number = 2) { return async (...args: any[]) => { diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts new file mode 100644 index 000000000000..1e2ade46739d --- /dev/null +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires max-func-body-length no-unnecessary-override no-invalid-template-strings no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { + PythonSettings +} from '../../../client/common/configSettings'; +import { noop } from '../../../client/common/utils/misc'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; +const untildify = require('untildify'); + +suite('Python Settings - pythonPath', () => { + class CustomPythonSettings extends PythonSettings { + public update(settings: WorkspaceConfiguration) { + return super.update(settings); + } + protected getPythonExecutable(pythonPath: string) { + return pythonPath; + } + protected initialize() { noop(); } + } + let configSettings: CustomPythonSettings; + let pythonSettings: typemoq.IMock; + setup(() => { + pythonSettings = typemoq.Mock.ofType(); + }); + teardown(() => { + if (configSettings) { + configSettings.dispose(); + } + }); + + test('Python Path from settings.json is used', () => { + configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + const pythonPath = 'This is the python Path'; + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + }); + test('Python Path from settings.json is used and relative path starting with \'~\' will be resolved from home directory', () => { + configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + const pythonPath = `~${path.sep}This is the python Path`; + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(untildify(pythonPath)); + }); + test('Python Path from settings.json is used and relative path starting with \'.\' will be resolved from workspace folder', () => { + const workspaceFolderUri = Uri.file(__dirname); + configSettings = new CustomPythonSettings(workspaceFolderUri, new MockAutoSelectionService()); + const pythonPath = `.${path.sep}This is the python Path`; + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(path.resolve(workspaceFolderUri.fsPath, pythonPath)); + }); + test('Python Path from settings.json is used and ${workspacecFolder} value will be resolved from workspace folder', () => { + const workspaceFolderUri = Uri.file(__dirname); + configSettings = new CustomPythonSettings(workspaceFolderUri, new MockAutoSelectionService()); + const workspaceFolderToken = '${workspaceFolder}'; + const pythonPath = `${workspaceFolderToken}${path.sep}This is the python Path`; + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(path.join(workspaceFolderUri.fsPath, 'This is the python Path')); + }); + test('If we don\'t have a custom python path and no auto selected interpreters, then use default', () => { + const workspaceFolderUri = Uri.file(__dirname); + const selectionService = mock(MockAutoSelectionService); + configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); + const pythonPath = 'python'; + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal('python'); + }); + test('If we don\'t have a custom python path and we do have an auto selected interpreter, then use it', () => { + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter: any = { path: pythonPath }; + const workspaceFolderUri = Uri.file(__dirname); + const selectionService = mock(MockAutoSelectionService); + when(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).thenReturn(interpreter); + when(selectionService.setWorkspaceInterpreter(workspaceFolderUri, anything())).thenResolve(); + configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => 'python') + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + verify(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).once(); + }); +}); diff --git a/src/test/common/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts similarity index 89% rename from src/test/common/configSettings.unit.test.ts rename to src/test/common/configSettings/configSettings.unit.test.ts index 84099806bc57..e571f73ed049 100644 --- a/src/test/common/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -11,7 +11,7 @@ import untildify = require('untildify'); import { WorkspaceConfiguration } from 'vscode'; import { PythonSettings -} from '../../client/common/configSettings'; +} from '../../../client/common/configSettings'; import { IAnalysisSettings, IAutoCompleteSettings, @@ -22,22 +22,26 @@ import { ITerminalSettings, IUnitTestSettings, IWorkspaceSymbolSettings -} from '../../client/common/types'; -import { noop } from '../../client/common/utils/misc'; +} from '../../../client/common/types'; +import { noop } from '../../../client/common/utils/misc'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; // tslint:disable-next-line:max-func-body-length suite('Python Settings', () => { - let config: TypeMoq.IMock; - let expected: PythonSettings; - let settings: PythonSettings; - const CustomPythonSettings = class extends PythonSettings { + class CustomPythonSettings extends PythonSettings { + // tslint:disable-next-line:no-unnecessary-override + public update(pythonSettings: WorkspaceConfiguration) { + return super.update(pythonSettings); + } protected initialize() { noop(); } - }; - + } + let config: TypeMoq.IMock; + let expected: CustomPythonSettings; + let settings: CustomPythonSettings; setup(() => { config = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - expected = new CustomPythonSettings(); - settings = new CustomPythonSettings(); + expected = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + settings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); }); function initializeConfig(sourceSettings: PythonSettings) { diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 2dd2578dc305..34bac00b6ca4 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; @@ -38,7 +39,7 @@ suite('Installer', () => { await resetSettings(); }); teardown(async () => { - ioc.dispose(); + await ioc.dispose(); await closeActiveWindows(); }); @@ -60,10 +61,7 @@ suite('Installer', () => { ioc.serviceManager.addSingletonInstance(IApplicationShell, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); - - const workspaceService = TypeMoq.Mock.ofType(); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - ioc.serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); + ioc.serviceManager.addSingleton(IWorkspaceService, WorkspaceService); ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance(IsWindows, false); diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts index f6a4b7e4b721..5ce43dcab3cf 100644 --- a/src/test/common/installer/installer.unit.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -147,7 +147,7 @@ suite('Module Installer only', () => { persistVal.setup(p => p.value).returns(() => false); persistVal.setup(p => p.updateValue(TypeMoq.It.isValue(true))); persistentStore.setup(ps => - ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) + ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) ).returns(() => persistVal.object); // Display first prompt. @@ -189,7 +189,7 @@ suite('Module Installer only', () => { return Promise.resolve(); }).verifiable(TypeMoq.Times.once()); persistentStore.setup(ps => - ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) + ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) ).returns(() => { return persistVal.object; }).verifiable(TypeMoq.Times.exactly(3)); @@ -244,7 +244,7 @@ suite('Module Installer only', () => { return Promise.resolve(); }); persistentStore.setup(ps => - ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) + ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) ).returns(() => { return persistVal.object; }); @@ -267,7 +267,7 @@ suite('Module Installer only', () => { persistVal.setup(p => p.value).returns(() => false); persistVal.setup(p => p.updateValue(TypeMoq.It.isValue(true))); persistentStore.setup(ps => - ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) + ps.createGlobalPersistentState(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) ).returns(() => persistVal.object); await installer.promptToInstall(product.value, resource); diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index e066f42ab50b..0ae91f6b7632 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -11,6 +11,8 @@ import { Container } from 'inversify'; import { EOL } from 'os'; import * as path from 'path'; import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { IS_WINDOWS } from '../../../client/common/platform/constants'; import { FileSystem } from '../../../client/common/platform/fileSystem'; @@ -28,6 +30,7 @@ import { OSType } from '../../../client/common/utils/platform'; import { registerTypes as variablesRegisterTypes } from '../../../client/common/variables/serviceRegistry'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/types'; import { ServiceContainer } from '../../../client/ioc/container'; import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -36,6 +39,7 @@ import { isOs, isPythonVersion } from '../../common'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST @@ -74,8 +78,10 @@ suite('PythonExecutableService', () => { serviceManager.addSingleton(ICurrentProcess, CurrentProcess); serviceManager.addSingleton(IConfigurationService, ConfigurationService); serviceManager.addSingleton(IPlatformService, PlatformService); + serviceManager.addSingleton(IWorkspaceService, WorkspaceService); serviceManager.addSingleton(IFileSystem, FileSystem); - + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); processRegisterTypes(serviceManager); variablesRegisterTypes(serviceManager); diff --git a/src/test/common/utils/version.unit.test.ts b/src/test/common/utils/version.unit.test.ts index e78b837da17c..d43def54e1ec 100644 --- a/src/test/common/utils/version.unit.test.ts +++ b/src/test/common/utils/version.unit.test.ts @@ -6,22 +6,41 @@ // tslint:disable: no-any import * as assert from 'assert'; -import { compareVersion, convertToSemver } from '../../../client/common/utils/version'; +import { parsePythonVersion } from '../../../client/common/utils/version'; suite('Version Utils', () => { - test('Must handle invalid versions', async () => { - const version = 'ABC'; - assert.equal(convertToSemver(version), `${version}.0.0`, 'Version is incorrect'); + test('Must convert undefined if empty strinfg', async () => { + assert.equal(parsePythonVersion(undefined as any), undefined); + assert.equal(parsePythonVersion(''), undefined); }); - test('Must handle null, empty and undefined', async () => { - assert.equal(convertToSemver(''), '0.0.0', 'Version is incorrect for empty string'); - assert.equal(convertToSemver(null), '0.0.0', 'Version is incorrect for null value'); - assert.equal(convertToSemver(undefined), '0.0.0', 'Version is incorrect for undefined value'); + test('Must convert version correctly', async () => { + const version = parsePythonVersion('3.7.1')!; + assert.equal(version.raw, '3.7.1'); + assert.equal(version.major, 3); + assert.equal(version.minor, 7); + assert.equal(version.patch, 1); + assert.deepEqual(version.prerelease, []); }); - test('Must be able to compare versions correctly', async () => { - assert.equal(compareVersion('', '1'), 0, '1. Comparison failed'); - assert.equal(compareVersion('1', '0.1'), 1, '2. Comparison failed'); - assert.equal(compareVersion('2.10', '2.9'), 1, '3. Comparison failed'); - assert.equal(compareVersion('2.99.9', '3'), 0, '4. Comparison failed'); + test('Must convert version correctly with pre-release', async () => { + const version = parsePythonVersion('3.7.1-alpha')!; + assert.equal(version.raw, '3.7.1-alpha'); + assert.equal(version.major, 3); + assert.equal(version.minor, 7); + assert.equal(version.patch, 1); + assert.deepEqual(version.prerelease, ['alpha']); + }); + test('Must remove invalid pre-release channels', async () => { + assert.deepEqual(parsePythonVersion('3.7.1-alpha')!.prerelease, ['alpha']); + assert.deepEqual(parsePythonVersion('3.7.1-beta')!.prerelease, ['beta']); + assert.deepEqual(parsePythonVersion('3.7.1-candidate')!.prerelease, ['candidate']); + assert.deepEqual(parsePythonVersion('3.7.1-final')!.prerelease, ['final']); + assert.deepEqual(parsePythonVersion('3.7.1-unknown')!.prerelease, []); + assert.deepEqual(parsePythonVersion('3.7.1-')!.prerelease, []); + assert.deepEqual(parsePythonVersion('3.7.1-prerelease')!.prerelease, []); + }); + test('Must default versions partgs to 0 if they are not numeric', async () => { + assert.deepEqual(parsePythonVersion('3.B.1')!.raw, '3.0.1'); + assert.deepEqual(parsePythonVersion('3.B.C')!.raw, '3.0.0'); + assert.deepEqual(parsePythonVersion('A.B.C')!.raw, '0.0.0'); }); }); diff --git a/src/test/common/variables/envVarsProvider.multiroot.test.ts b/src/test/common/variables/envVarsProvider.multiroot.test.ts index 84d15aa07f5d..7f0cb344d925 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -15,8 +15,10 @@ import { createDeferred } from '../../../client/common/utils/async'; import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { EnvironmentVariables } from '../../../client/common/variables/types'; +import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; import { clearPythonPathInWorkspaceFolder, updateSetting } from '../../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockProcess } from '../../mocks/process'; import { UnitTestIocContainer } from '../../unittests/serviceRegistry'; @@ -48,7 +50,7 @@ suite('Multiroot Environment Variables Provider', () => { }); suiteTeardown(closeActiveWindows); teardown(async () => { - ioc.dispose(); + await ioc.dispose(); await closeActiveWindows(); await clearPythonPathInWorkspaceFolder(workspace4Path); await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); @@ -60,7 +62,8 @@ suite('Multiroot Environment Variables Provider', () => { const mockProcess = new MockProcess(mockVariables); const variablesService = new EnvironmentVariablesService(pathUtils); const disposables = ioc.serviceContainer.get(IDisposableRegistry); - const cfgService = new ConfigurationService(); + ioc.serviceManager.addSingletonInstance(IInterpreterAutoSelectionService, new MockAutoSelectionService()); + const cfgService = new ConfigurationService(ioc.serviceContainer); return new EnvironmentVariablesProvider(variablesService, disposables, new PlatformService(), cfgService, mockProcess); } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 40e4fe22725e..b51bcb431190 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -39,7 +39,7 @@ import { ILogger, IPathUtils, IPersistentStateFactory, - IsWindows, + IsWindows } from '../../client/common/types'; import { noop } from '../../client/common/utils/misc'; import { EnvironmentVariablesService } from '../../client/common/variables/environment'; @@ -128,16 +128,17 @@ import { import { IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockCommandManager } from './mockCommandManager'; import { MockJupyterManager } from './mockJupyterManager'; export class DataScienceIocContainer extends UnitTestIocContainer { - private pythonSettings: PythonSettings = new PythonSettings(); - private commandManager : MockCommandManager = new MockCommandManager(); - private setContexts : { [name: string] : boolean } = {}; - private contextSetEvent : EventEmitter<{name: string; value: boolean}> = new EventEmitter<{name: string; value: boolean}>(); + private pythonSettings: PythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); + private commandManager: MockCommandManager = new MockCommandManager(); + private setContexts: { [name: string]: boolean } = {}; + private contextSetEvent: EventEmitter<{ name: string; value: boolean }> = new EventEmitter<{ name: string; value: boolean }>(); private jupyterMock: MockJupyterManager | undefined; private shouldMockJupyter: boolean; private asyncRegistry: AsyncDisposableRegistry; @@ -149,11 +150,11 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.asyncRegistry = new AsyncDisposableRegistry(); } - public get onContextSet() : Event<{name: string; value: boolean}> { + public get onContextSet(): Event<{ name: string; value: boolean }> { return this.contextSetEvent.event; } - public async dispose() : Promise { + public async dispose(): Promise { await this.asyncRegistry.dispose(); await super.dispose(); } @@ -176,7 +177,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Setup our command list this.commandManager.registerCommand('setContext', (name: string, value: boolean) => { this.setContexts[name] = value; - this.contextSetEvent.fire({name: name, value: value}); + this.contextSetEvent.fire({ name: name, value: value }); }); this.serviceManager.addSingletonInstance(ICommandManager, this.commandManager); @@ -238,17 +239,17 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return new MockFileSystemWatcher(); }); workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true); + .setup(w => w.hasWorkspaceFolders) + .returns(() => true); const testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); const workspaceFolder = this.createMoqWorkspaceFolder(testWorkspaceFolder); workspaceService - .setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]); + .setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder]); workspaceService.setup(w => w.rootPath).returns(() => '~'); const systemVariables: SystemVariables = new SystemVariables(undefined); - const env = {...systemVariables}; + const env = { ...systemVariables }; // Look on the path for python const pythonPath = this.findPythonPath(); @@ -291,11 +292,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); this.serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); this.serviceManager.addSingleton(IPipEnvService, PipEnvService); + this.serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); - const isWindows = this.serviceManager.get(IsWindows); - if (isWindows) { - this.serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); - } this.serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); this.serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); @@ -307,6 +305,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory); this.serviceManager.addSingleton(IPythonPathUpdaterServiceManager, PythonPathUpdaterService); + this.serviceManager.addSingleton(IRegistry, RegistryImplementation); const currentProcess = new CurrentProcess(); this.serviceManager.addSingletonInstance(ICurrentProcess, currentProcess); @@ -326,9 +325,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } this.serviceManager.addSingleton( ITerminalActivationCommandProvider, Bash, 'bashCShellFish'); - this.serviceManager.addSingleton( + this.serviceManager.addSingleton( ITerminalActivationCommandProvider, CommandPromptAndPowerShell, 'commandPromptAndPowerShell'); - this.serviceManager.addSingleton( + this.serviceManager.addSingleton( ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, 'pyenv'); const dummyDisposable = { @@ -353,7 +352,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return folder.object; } - public getContext(name: string) : boolean { + public getContext(name: string): boolean { if (this.setContexts.hasOwnProperty(name)) { return this.setContexts[name]; } @@ -365,7 +364,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.pythonSettings.emit('change'); } - public get mockJupyter() : MockJupyterManager | undefined { + public get mockJupyter(): MockJupyterManager | undefined { return this.jupyterMock; } diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index d00152c2649d..c7bbad7c01d9 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -41,6 +41,7 @@ import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { getOSType, OSType } from '../common'; import { noop } from '../core'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; import { MockJupyterManager } from './mockJupyterManager'; // tslint:disable:no-any no-http-string no-multiline-string max-func-body-length @@ -147,7 +148,7 @@ suite('Jupyter Execution', async () => { const serviceContainer = mock(ServiceContainer); const disposableRegistry = new DisposableRegistry(); const dummyEvent = new EventEmitter(); - const pythonSettings = new PythonSettings(); + const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); const jupyterOnPath = getOSType() === OSType.Windows ? '/foo/bar/jupyter.exe' : '/foo/bar/jupyter'; let ipykernelInstallCount = 0; diff --git a/src/test/datascience/historyCommandListener.unit.test.ts b/src/test/datascience/historyCommandListener.unit.test.ts index 18d650c3c943..980f3206bcb9 100644 --- a/src/test/datascience/historyCommandListener.unit.test.ts +++ b/src/test/datascience/historyCommandListener.unit.test.ts @@ -41,13 +41,14 @@ import { InterpreterService } from '../../client/interpreter/interpreterService' import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService'; import { ServiceContainer } from '../../client/ioc/container'; import { noop } from '../core'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; import * as vscodeMocks from '../vscode-mock'; import { createDocument } from './editor-integration/helpers'; import { MockCommandManager } from './mockCommandManager'; // tslint:disable:no-any no-http-string no-multiline-string max-func-body-length -function createTypeMoq(tag: string) : TypeMoq.IMock { +function createTypeMoq(tag: string): TypeMoq.IMock { // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 const result = TypeMoq.Mock.ofType(); @@ -68,28 +69,28 @@ class MockDocumentManager implements IDocumentManager { private didChangeTextEditorViewColumnEmitter = new EventEmitter(); private didCloseEmitter = new EventEmitter(); private didSaveEmitter = new EventEmitter(); - public get onDidChangeActiveTextEditor() : Event { + public get onDidChangeActiveTextEditor(): Event { return this.didChangeEmitter.event; } - public get onDidOpenTextDocument() : Event { + public get onDidOpenTextDocument(): Event { return this.didOpenEmitter.event; } - public get onDidChangeVisibleTextEditors() : Event { + public get onDidChangeVisibleTextEditors(): Event { return this.didChangeVisibleEmitter.event; } - public get onDidChangeTextEditorSelection() : Event { + public get onDidChangeTextEditorSelection(): Event { return this.didChangeTextEditorSelectionEmitter.event; } - public get onDidChangeTextEditorOptions() : Event { + public get onDidChangeTextEditorOptions(): Event { return this.didChangeTextEditorOptionsEmitter.event; } - public get onDidChangeTextEditorViewColumn() : Event { + public get onDidChangeTextEditorViewColumn(): Event { return this.didChangeTextEditorViewColumnEmitter.event; } - public get onDidCloseTextDocument() : Event { + public get onDidCloseTextDocument(): Event { return this.didCloseEmitter.event; } - public get onDidSaveTextDocument() : Event { + public get onDidSaveTextDocument(): Event { return this.didSaveEmitter.event; } public showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; @@ -109,7 +110,7 @@ class MockDocumentManager implements IDocumentManager { throw new Error('Method not implemented.'); } - private getDocument() : TextDocument { + private getDocument(): TextDocument { const mockDoc = createDocument('#%%\r\nprint("code")', 'bar.ipynb', 1, TypeMoq.Times.atMost(100), true); mockDoc.setup((x: any) => x.then).returns(() => undefined); return mockDoc.object; @@ -138,7 +139,7 @@ suite('History command listener', async () => { const fileSystem = mock(FileSystem); const serviceContainer = mock(ServiceContainer); const dummyEvent = new EventEmitter(); - const pythonSettings = new PythonSettings(); + const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); const disposableRegistry = []; const historyProvider = mock(HistoryProvider); const notebookImporter = mock(JupyterImporter); @@ -176,17 +177,16 @@ suite('History command listener', async () => { public match(value: Object): boolean { return this.func(value); } - public toString(): string - { + public toString(): string { return 'FunctionMatcher'; } } - function argThat(func: (obj: any) => boolean) : any { + function argThat(func: (obj: any) => boolean): any { return new FunctionMatcher(func); } - function createCommandListener(activeHistory: IHistory | undefined) : HistoryCommandListener { + function createCommandListener(activeHistory: IHistory | undefined): HistoryCommandListener { // Setup defaults when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject('Unknown interpreter'); diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts index a01fda507ee7..228c708d5603 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -11,10 +11,12 @@ import { InstallationChannelManager } from '../../client/common/installer/channe import { IModuleInstaller } from '../../client/common/installer/types'; import { Product } from '../../client/common/types'; import { Architecture } from '../../client/common/utils/platform'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { IInterpreterLocatorService, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; const info: PythonInterpreter = { architecture: Architecture.Unknown, @@ -40,6 +42,8 @@ suite('Installation - installation channels', () => { serviceContainer = new ServiceContainer(cont); pipEnv = TypeMoq.Mock.ofType(); serviceManager.addSingletonInstance(IInterpreterLocatorService, pipEnv.object, PIPENV_SERVICE); + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); }); test('Single channel', async () => { diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts index d6e9b25ffda6..394edb546714 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -11,10 +11,12 @@ import { IModuleInstaller } from '../../client/common/installer/types'; import { IPlatformService } from '../../client/common/platform/types'; import { Product } from '../../client/common/types'; import { Architecture } from '../../client/common/utils/platform'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; const info: PythonInterpreter = { architecture: Architecture.Unknown, @@ -51,6 +53,8 @@ suite('Installation - channel messages', () => { const moduleInstaller = TypeMoq.Mock.ofType(); serviceManager.addSingletonInstance(IModuleInstaller, moduleInstaller.object); + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); }); test('No installers message: Unknown/Windows', async () => { diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts new file mode 100644 index 000000000000..dbfe0eec6b52 --- /dev/null +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -0,0 +1,305 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +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'; +import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection'; +import { InterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/proxy'; +import { CachedInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/cached'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/currentPath'; +import { SettingsInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/settings'; +import { SystemWideInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/system'; +import { WindowsRegistryInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/winRegistry'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/workspaceEnv'; +import { IInterpreterAutoSelectionRule, IInterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../client/interpreter/helpers'; + +const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; + +suite('Interpreters - Auto Selection', () => { + let autoSelectionService: InterpreterAutoSelectionServiceTest; + let workspaceService: IWorkspaceService; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let systemInterpreter: IInterpreterAutoSelectionRule; + let currentPathInterpreter: IInterpreterAutoSelectionRule; + let winRegInterpreter: IInterpreterAutoSelectionRule; + let cachedPaths: IInterpreterAutoSelectionRule; + let userDefinedInterpreter: IInterpreterAutoSelectionRule; + let workspaceInterpreter: IInterpreterAutoSelectionRule; + let state: PersistentState; + let helper: IInterpreterHelper; + let proxy: IInterpreterAutoSeletionProxyService; + class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { + public initializeStore(): Promise { + return super.initializeStore(); + } + public storeAutoSelectedInterperter(resource: Resource, interpreter: PythonInterpreter | undefined) { + return super.storeAutoSelectedInterperter(resource, interpreter); + } + } + setup(() => { + workspaceService = mock(WorkspaceService); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + currentPathInterpreter = mock(CurrentPathInterpretersAutoSelectionRule); + winRegInterpreter = mock(WindowsRegistryInterpretersAutoSelectionRule); + cachedPaths = mock(CachedInterpretersAutoSelectionRule); + userDefinedInterpreter = mock(SettingsInterpretersAutoSelectionRule); + workspaceInterpreter = mock(WorkspaceVirtualEnvInterpretersAutoSelectionRule); + helper = mock(InterpreterHelper); + proxy = mock(InterpreterAutoSeletionProxyService); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), instance(stateFactory), instance(fs), + instance(systemInterpreter), instance(currentPathInterpreter), + instance(winRegInterpreter), instance(cachedPaths), + instance(userDefinedInterpreter), instance(workspaceInterpreter), + instance(proxy), instance(helper) + ); + }); + + test('Instance is registere in proxy', () => { + verify(proxy.registerInstance!(autoSelectionService)).once(); + }); + test('Rules are chained in order of preference', () => { + verify(userDefinedInterpreter.setNextRule(instance(workspaceInterpreter))).once(); + verify(workspaceInterpreter.setNextRule(instance(cachedPaths))).once(); + verify(cachedPaths.setNextRule(instance(currentPathInterpreter))).once(); + verify(currentPathInterpreter.setNextRule(instance(winRegInterpreter))).once(); + verify(winRegInterpreter.setNextRule(instance(systemInterpreter))).once(); + verify(systemInterpreter.setNextRule(anything())).never(); + }); + test('Run rules in background', async () => { + autoSelectionService.initializeStore = () => Promise.resolve(); + await autoSelectionService.autoSelectInterpreter(undefined); + + const allRules = [userDefinedInterpreter, winRegInterpreter, currentPathInterpreter, systemInterpreter, workspaceInterpreter, cachedPaths]; + for (const service of allRules) { + verify(service.autoSelectInterpreter(undefined)).once(); + if (service !== userDefinedInterpreter) { + verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); + } + } + verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + }); + test('Run userDefineInterpreter as the first rule', async () => { + autoSelectionService.initializeStore = () => Promise.resolve(); + await autoSelectionService.autoSelectInterpreter(undefined); + + verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); + }); + test('Initialize the store', async () => { + let initialize = false; + autoSelectionService.initializeStore = async () => initialize = true as any; + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(initialize).to.be.equal(true, 'Not invoked'); + }); + 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(); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + }); + test('Clear file stored in cache if it doesn\'t exist', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when(state.value).thenReturn(interpreterInfo); + when(fs.fileExists(pythonPath)).thenResolve(false); + + await autoSelectionService.initializeStore(); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(state.value).atLeast(1); + verify(fs.fileExists(pythonPath)).once(); + verify(state.updateValue(undefined)).once(); + }); + test('Should not clear file stored in cache if it does exist', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when(state.value).thenReturn(interpreterInfo); + when(fs.fileExists(pythonPath)).thenResolve(true); + + await autoSelectionService.initializeStore(); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(state.value).atLeast(1); + verify(fs.fileExists(pythonPath)).once(); + verify(state.updateValue(undefined)).never(); + }); + test('Store interpreter info in state store when resource is undefined', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + + await autoSelectionService.initializeStore(); + await autoSelectionService.storeAutoSelectedInterperter(undefined, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(interpreterInfo)).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + expect(eventFired).to.deep.equal(true, 'event not fired'); + }); + test('Do not store global interpreter info in state store when resource is undefined and version is lower than one already in state', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath, version: new SemVer('1.0.0') } as any; + const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; + when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when(state.value).thenReturn(interpreterInfoInState); + + await autoSelectionService.initializeStore(); + await autoSelectionService.storeAutoSelectedInterperter(undefined, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(anything())).never(); + expect(selectedInterpreter).to.deep.equal(interpreterInfoInState); + expect(eventFired).to.deep.equal(false, 'event fired'); + }); + test('Store global interpreter info in state store when resource is undefined and version is higher than one already in state', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath, version: new SemVer('3.0.0') } as any; + const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; + when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when(state.value).thenReturn(interpreterInfoInState); + + await autoSelectionService.initializeStore(); + await autoSelectionService.storeAutoSelectedInterperter(undefined, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(anything())).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + expect(eventFired).to.deep.equal(true, 'event fired'); + }); + test('Store global interpreter info in state store', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + + await autoSelectionService.initializeStore(); + await autoSelectionService.setGlobalInterpreter(interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(interpreterInfo)).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + }); + test('Store interpreter info in state store when resource is defined', async () => { + let eventFired = false; + 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 }); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + + await autoSelectionService.initializeStore(); + await autoSelectionService.storeAutoSelectedInterperter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); + + verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + expect(eventFired).to.deep.equal(true, 'event not fired'); + }); + 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 }); + + await autoSelectionService.initializeStore(); + await autoSelectionService.setWorkspaceInterpreter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); + + verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + }); + test('Return undefined when we do not have a global value', 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 }); + + await autoSelectionService.initializeStore(); + await autoSelectionService.storeAutoSelectedInterperter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter === null || selectedInterpreter === undefined).to.equal(true, 'Should be undefined'); + }); + test('Return global value if we do not have a matching value for the resource', 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 }); + const globalInterpreterInfo = { path: 'global Value' }; + when(state.value).thenReturn(globalInterpreterInfo as any); + await autoSelectionService.initializeStore(); + await autoSelectionService.storeAutoSelectedInterperter(resource, interpreterInfo); + const anotherResourceOfAnotherWorkspace = Uri.parse('Some other workspace'); + 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 storeAutoSelectedInterperter 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.storeAutoSelectedInterperter(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 storeAutoSelectedInterperter 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.storeAutoSelectedInterperter(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/proxy.unit.test.ts b/src/test/interpreters/autoSelection/proxy.unit.test.ts new file mode 100644 index 000000000000..576ee040fc5e --- /dev/null +++ b/src/test/interpreters/autoSelection/proxy.unit.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this no-any + +import { expect } from 'chai'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { InterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/proxy'; +import { IInterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/types'; +import { PythonInterpreter } from '../../../client/interpreter/contracts'; + +suite('Interpreters - Auto Selection Proxy', () => { + class InstanceClass implements IInterpreterAutoSeletionProxyService { + public eventEmitter = new EventEmitter(); + constructor(private readonly pythonPath: string = '') { } + public get onDidChangeAutoSelectedInterpreter(): Event { + return this.eventEmitter.event; + } + public getAutoSelectedInterpreter(_resource: Uri): PythonInterpreter { + return { path: this.pythonPath } as any; + } + public async setWorkspaceInterpreter(_resource: Uri, _interpreter: PythonInterpreter | undefined): Promise{ + return; + } + } + + let proxy: InterpreterAutoSeletionProxyService; + setup(() => { + proxy = new InterpreterAutoSeletionProxyService([] as any); + }); + + test('Change evnet is fired', () => { + const obj = new InstanceClass(); + proxy.registerInstance(obj); + let eventRaised = false; + + proxy.onDidChangeAutoSelectedInterpreter(() => eventRaised = true); + proxy.registerInstance(obj); + + obj.eventEmitter.fire(); + + expect(eventRaised).to.be.equal(true, 'Change event not fired'); + }); + + [undefined, Uri.parse('one')].forEach(resource => { + const suffix = resource ? '(with a resource)' : '(without a resource)'; + + test(`getAutoSelectedInterpreter should return undefined when instance isn't registered ${suffix}`, () => { + expect(proxy.getAutoSelectedInterpreter(resource)).to.be.equal(undefined, 'Should be undefined'); + }); + test(`getAutoSelectedInterpreter should invoke instance method when instance isn't registered ${suffix}`, () => { + const pythonPath = 'some python path'; + proxy.registerInstance(new InstanceClass(pythonPath)); + + const value = proxy.getAutoSelectedInterpreter(resource); + + expect(value).to.be.deep.equal({ path: pythonPath }); + }); + + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/base.unit.test.ts b/src/test/interpreters/autoSelection/rules/base.unit.test.ts new file mode 100644 index 000000000000..b747634dd1fe --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/base.unit.test.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { BaseRuleService, NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/currentPath'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { PythonInterpreter } from '../../../../client/interpreter/contracts'; + +suite('Interpreters - Auto Selection - Base Rule', () => { + let rule: BaseRuleServiceTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + class BaseRuleServiceTest extends BaseRuleService { + public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.next(resource, manager); + } + public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { + return super.cacheSelectedInterpreter(resource, interpreter); + } + public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + return super.setGlobalInterpreter(interpreter, manager); + } + protected async onAutoSelectInterpreter(_resource: Uri, _manager?: IInterpreterAutoSelectionService): Promise { + return NextAction.runNextRule; + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new BaseRuleServiceTest(AutoSelectionRule.cachedInterpreters, instance(fs), instance(stateFactory)); + }); + + test('State store is created', () => { + verify(stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, undefined)).once(); + }); + test('Next rule should be invoked', async () => { + const nextRule = mock(CurrentPathInterpretersAutoSelectionRule); + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.parse('x'); + + rule.setNextRule(instance(nextRule)); + await rule.next(resource, manager); + + verify(stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, undefined)).once(); + verify(nextRule.autoSelectInterpreter(resource, manager)).once(); + }); + test('Next rule should not be invoked', async () => { + const nextRule = mock(CurrentPathInterpretersAutoSelectionRule); + const resource = Uri.parse('x'); + + rule.setNextRule(instance(nextRule)); + await rule.next(resource); + + verify(stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, undefined)).once(); + verify(nextRule.autoSelectInterpreter(anything(), anything())).never(); + }); + test('State store must be updated', async () => { + const resource = Uri.parse('x'); + const interpreterInfo = { x: '1324' } as any; + when(state.updateValue(anything())).thenResolve(); + + await rule.cacheSelectedInterpreter(resource, interpreterInfo); + + verify(state.updateValue(interpreterInfo)).once(); + }); + test('State store must be cleared when file does not exist', async () => { + const resource = Uri.parse('x'); + const interpreterInfo = { path: '1324' } as any; + when(state.value).thenReturn(interpreterInfo); + when(state.updateValue(anything())).thenResolve(); + when(fs.fileExists(interpreterInfo.path)).thenResolve(false); + + await rule.autoSelectInterpreter(resource); + + verify(state.value).atLeast(1); + verify(state.updateValue(undefined)).once(); + verify(fs.fileExists(interpreterInfo.path)).once(); + }); + test('State store must not be cleared when file exists', async () => { + const resource = Uri.parse('x'); + const interpreterInfo = { path: '1324' } as any; + when(state.value).thenReturn(interpreterInfo); + when(state.updateValue(anything())).thenResolve(); + when(fs.fileExists(interpreterInfo.path)).thenResolve(true); + + await rule.autoSelectInterpreter(resource); + + verify(state.value).atLeast(1); + verify(state.updateValue(anything())).never(); + verify(fs.fileExists(interpreterInfo.path)).once(); + }); + test('Get undefined if there\'s nothing in state store', async () => { + when(state.value).thenReturn(undefined); + + expect(rule.getPreviouslyAutoSelectedInterpreter(Uri.parse('x'))).to.be.equal(undefined, 'Must be undefined'); + + verify(state.value).atLeast(1); + }); + test('Get value from state store', async () => { + const stateStoreValue = 'x'; + when(state.value).thenReturn(stateStoreValue as any); + + expect(rule.getPreviouslyAutoSelectedInterpreter(Uri.parse('x'))).to.be.equal(stateStoreValue); + + verify(state.value).atLeast(1); + }); + test('setGlobalInterpreter should do nothing if interprter is undefined or version is empty', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1324' } as any; + + const result1 = await rule.setGlobalInterpreter(undefined, instance(manager)); + const result2 = await rule.setGlobalInterpreter(interpreterInfo, instance(manager)); + + verify(manager.setGlobalInterpreter(anything())).never(); + assert.equal(result1, false); + assert.equal(result2, false); + }); + test('setGlobalInterpreter should not update manager if interpreter is not better than one stored in manager', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1324', version: new SemVer('1.0.0') } as any; + const interpreterInfoInManager = { path: '2', version: new SemVer('2.0.0') } as any; + when(manager.getAutoSelectedInterpreter(undefined)).thenReturn(interpreterInfoInManager); + + const result = await rule.setGlobalInterpreter(interpreterInfo, instance(manager)); + + verify(manager.getAutoSelectedInterpreter(undefined)).once(); + verify(manager.setGlobalInterpreter(anything())).never(); + assert.equal(result, false); + }); + test('setGlobalInterpreter should update manager if interpreter is better than one stored in manager', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1324', version: new SemVer('3.0.0') } as any; + const interpreterInfoInManager = { path: '2', version: new SemVer('2.0.0') } as any; + when(manager.getAutoSelectedInterpreter(undefined)).thenReturn(interpreterInfoInManager); + + const result = await rule.setGlobalInterpreter(interpreterInfo, instance(manager)); + + verify(manager.getAutoSelectedInterpreter(undefined)).once(); + verify(manager.setGlobalInterpreter(anything())).once(); + assert.equal(result, true); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/cached.unit.test.ts b/src/test/interpreters/autoSelection/rules/cached.unit.test.ts new file mode 100644 index 000000000000..e6900e8fdee1 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/cached.unit.test.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { CachedInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/cached'; +import { SystemWideInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/system'; +import { IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, PythonInterpreter } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; + +suite('Interpreters - Auto Selection - Cached Rule', () => { + let rule: CachedInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + let systemInterpreter: IInterpreterAutoSelectionRule; + let currentPathInterpreter: IInterpreterAutoSelectionRule; + let winRegInterpreter: IInterpreterAutoSelectionRule; + let helper: IInterpreterHelper; + class CachedInterpretersAutoSelectionRuleTest extends CachedInterpretersAutoSelectionRule { + public readonly rules!: IInterpreterAutoSelectionRule[]; + public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + currentPathInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + winRegInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new CachedInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), + instance(stateFactory), instance(systemInterpreter), + instance(currentPathInterpreter), instance(winRegInterpreter)); + }); + + test('Invoke next rule if there are no cached intepreters', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); + when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); + when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); + verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); + verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Invoke next rule if fails to update global state', async () => { + const manager = mock(InterpreterAutoSelectionService); + const winRegInterpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(winRegInterpreterInfo); + when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(winRegInterpreterInfo); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Must not Invoke next rule if updating global state is successful', async () => { + const manager = mock(InterpreterAutoSelectionService); + const winRegInterpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(winRegInterpreterInfo); + when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(winRegInterpreterInfo); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.exit); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts b/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts new file mode 100644 index 000000000000..dad1d8fd457c --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +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 { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/currentPath'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterLocatorService, PythonInterpreter } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { KnownPathsService } from '../../../../client/interpreter/locators/services/KnownPathsService'; + +suite('Interpreters - Auto Selection - Current Path Rule', () => { + let rule: CurrentPathInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + let locator: IInterpreterLocatorService; + let helper: IInterpreterHelper; + class CurrentPathInterpretersAutoSelectionRuleTest extends CurrentPathInterpretersAutoSelectionRule { + public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + locator = mock(KnownPathsService); + + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new CurrentPathInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), + instance(stateFactory), instance(locator)); + }); + + test('Invoke next rule if there are no intepreters in the current path', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + when(locator.getInterpreters(resource)).thenResolve([]); + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(locator.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Invoke next rule if fails to update global state', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(anything())).thenReturn(interpreterInfo); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Not Invoke next rule if succeeds to update global state', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(anything())).thenReturn(interpreterInfo); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.exit); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts new file mode 100644 index 000000000000..a1530b20c7e0 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { SettingsInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/settings'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { PythonInterpreter } from '../../../../client/interpreter/contracts'; + +suite('Interpreters - Auto Selection - Settings Rule', () => { + let rule: SettingsInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + let workspaceService: IWorkspaceService; + class SettingsInterpretersAutoSelectionRuleTest extends SettingsInterpretersAutoSelectionRule { + public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new SettingsInterpretersAutoSelectionRuleTest(instance(fs), + instance(stateFactory), instance(workspaceService)); + }); + + test('Invoke next rule if python Path in user settings is default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = {}; + const pythonPath = { inspect: () => pythonPathInConfig }; + + when(workspaceService.getConfiguration('python', null as any)).thenReturn(pythonPath as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Invoke next rule if python Path in user settings is not defined', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'python' }; + const pythonPath = { inspect: () => pythonPathInConfig }; + + when(workspaceService.getConfiguration('python', null as any)).thenReturn(pythonPath as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Must not Invoke next rule if python Path in user settings is not default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'something else' }; + const pythonPath = { inspect: () => pythonPathInConfig }; + + when(workspaceService.getConfiguration('python', null as any)).thenReturn(pythonPath as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.exit); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/system.unit.test.ts b/src/test/interpreters/autoSelection/rules/system.unit.test.ts new file mode 100644 index 000000000000..543415610aa0 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/system.unit.test.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { SystemWideInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/system'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterService, PythonInterpreter } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; + +suite('Interpreters - Auto Selection - System Interpreters Rule', () => { + let rule: SystemWideInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + let interpreterService: IInterpreterService; + let helper: IInterpreterHelper; + class SystemWideInterpretersAutoSelectionRuleTest extends SystemWideInterpretersAutoSelectionRule { + public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + interpreterService = mock(InterpreterService); + + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new SystemWideInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), + instance(stateFactory), instance(interpreterService)); + }); + + test('Invoke next rule if there are no intepreters in the current path', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + when(interpreterService.getInterpreters(resource)).thenResolve([]); + when(helper.getBestInterpreter(deepEqual([]))).thenReturn(undefined); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + assert.equal(res, undefined); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(interpreterService.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Invoke next rule if there intepreters in the current path but update fails', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(interpreterService.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).deep.equal(interpreterInfo); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(interpreterService.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Do not Invoke next rule if there intepreters in the current path and update does not fail', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(interpreterService.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).deep.equal(interpreterInfo); + return Promise.resolve(true); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(interpreterService.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.exit); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts b/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts new file mode 100644 index 000000000000..c5838a9015f6 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { OSType } from '../../../../client/common/utils/platform'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { WindowsRegistryInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/winRegistry'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterLocatorService, PythonInterpreter } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { WindowsRegistryService } from '../../../../client/interpreter/locators/services/windowsRegistryService'; + +suite('Interpreters - Auto Selection - Windows Registry Rule', () => { + let rule: WindowsRegistryInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + let locator: IInterpreterLocatorService; + let platform: IPlatformService; + let helper: IInterpreterHelper; + class WindowsRegistryInterpretersAutoSelectionRuleTest extends WindowsRegistryInterpretersAutoSelectionRule { + public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + locator = mock(WindowsRegistryService); + platform = mock(PlatformService); + + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new WindowsRegistryInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), + instance(stateFactory), instance(platform), instance(locator)); + }); + + getNamesAndValues(OSType).forEach(osType => { + test(`Invoke next rule if platform is not windows (${osType.name})`, async function () { + const manager = mock(InterpreterAutoSelectionService); + if (osType.value === OSType.Windows) { + return this.skip(); + } + const resource = Uri.file('x'); + when(platform.osType).thenReturn(osType.value); + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(platform.osType).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + }); + test('Invoke next rule if there are no interpreters in the registry', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + when(platform.osType).thenReturn(OSType.Windows); + when(locator.getInterpreters(resource)).thenResolve([]); + when(helper.getBestInterpreter(deepEqual([]))).thenReturn(undefined); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + assert.equal(res, undefined); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(locator.getInterpreters(resource)).once(); + verify(platform.osType).once(); + verify(helper.getBestInterpreter(deepEqual([]))).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Invoke next rule if there are interpreters in the registry and update fails', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(platform.osType).thenReturn(OSType.Windows); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).to.deep.equal(interpreterInfo); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(locator.getInterpreters(resource)).once(); + verify(platform.osType).once(); + verify(helper.getBestInterpreter(deepEqual([interpreterInfo]))).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Do not Invoke next rule if there are interpreters in the registry and update does not fail', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(platform.osType).thenReturn(OSType.Windows); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).to.deep.equal(interpreterInfo); + return Promise.resolve(true); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(locator.getInterpreters(resource)).once(); + verify(platform.osType).once(); + verify(helper.getBestInterpreter(deepEqual([interpreterInfo]))).once(); + expect(nextAction).to.be.equal(NextAction.exit); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts new file mode 100644 index 000000000000..b033144fbb3e --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts @@ -0,0 +1,382 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { OSType } from '../../../../client/common/utils/platform'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { BaseRuleService } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/workspaceEnv'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { PythonPathUpdaterService } from '../../../../client/interpreter/configuration/pythonPathUpdaterService'; +import { IPythonPathUpdaterServiceManager } from '../../../../client/interpreter/configuration/types'; +import { IInterpreterHelper, IInterpreterLocatorService, PythonInterpreter, WorkspacePythonPath } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { KnownPathsService } from '../../../../client/interpreter/locators/services/KnownPathsService'; + +suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { + let rule: WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState; + let helper: IInterpreterHelper; + let platform: IPlatformService; + let pipEnvLocator: IInterpreterLocatorService; + let virtualEnvLocator: IInterpreterLocatorService; + let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; + let workspaceService: IWorkspaceService; + class WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest extends WorkspaceVirtualEnvInterpretersAutoSelectionRule { + public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { + return super.setGlobalInterpreter(interpreter, manager); + } + public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + return super.next(resource, manager); + } + public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { + return super.cacheSelectedInterpreter(resource, interpreter); + } + public async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise { + return super.getWorkspaceVirtualEnvInterpreters(resource); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + platform = mock(PlatformService); + pipEnvLocator = mock(KnownPathsService); + workspaceService = mock(WorkspaceService); + virtualEnvLocator = mock(KnownPathsService); + pythonPathUpdaterService = mock(PythonPathUpdaterService); + + when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); + rule = new WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), + instance(stateFactory), instance(platform), + instance(workspaceService), instance(pythonPathUpdaterService), + instance(pipEnvLocator), instance(virtualEnvLocator)); + }); + test('Invoke next rule if there is no workspace', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn(undefined); + when(nextRule.autoSelectInterpreter(resource, manager)).thenResolve(); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(resource, manager); + + verify(nextRule.autoSelectInterpreter(resource, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + }); + test('Invoke next rule if resource is undefined', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn(undefined); + when(nextRule.autoSelectInterpreter(undefined, manager)).thenResolve(); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(undefined, manager); + + verify(nextRule.autoSelectInterpreter(undefined, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + }); + test('Invoke next rule if user has defined a python path in settings', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + type PythonPathInConfig = { workspaceFolderValue: string }; + const pythonPathInConfig = typemoq.Mock.ofType(); + const pythonPathValue = 'Hello there.exe'; + pythonPathInConfig + .setup(p => p.workspaceFolderValue) + .returns(() => pythonPathValue) + .verifiable(typemoq.Times.once()); + + const pythonPath = { inspect: () => pythonPathInConfig.object }; + const folderUri = Uri.parse('Folder'); + const someUri = Uri.parse('somethign'); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(nextRule.autoSelectInterpreter(someUri, manager)).thenResolve(); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(someUri, manager); + + verify(nextRule.autoSelectInterpreter(someUri, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + pythonPathInConfig.verifyAll(); + }); + test('Does not udpate settings when there is no interpreter', async () => { + await rule.cacheSelectedInterpreter(undefined, {} as any); + + verify(pythonPathUpdaterService.updatePythonPath(anything(), anything(), anything(), anything())).never(); + }); + test('Does not udpate settings when there is not workspace', async () => { + const resource = Uri.file('x'); + when(helper.getActiveWorkspaceUri(resource)).thenReturn(undefined); + + await rule.cacheSelectedInterpreter(resource, {} as any); + + verify(pythonPathUpdaterService.updatePythonPath(anything(), anything(), anything(), anything())).never(); + verify(helper.getActiveWorkspaceUri(resource)).once(); + }); + test('Update settings', async () => { + const resource = Uri.file('x'); + const workspacePythonPath: WorkspacePythonPath = { configTarget: 'xyz' as any, folderUri: Uri.parse('folder') }; + const pythonPath = 'python Path to store in settings'; + when(helper.getActiveWorkspaceUri(resource)).thenReturn(workspacePythonPath); + + await rule.cacheSelectedInterpreter(resource, { path: pythonPath } as any); + + verify(pythonPathUpdaterService.updatePythonPath(pythonPath, workspacePythonPath.configTarget, 'load', workspacePythonPath.folderUri)).once(); + verify(helper.getActiveWorkspaceUri(resource)).once(); + }); + test('getWorkspaceVirtualEnvInterpreters will not return any interpreters if there is no workspace ', async () => { + + let envs = await rule.getWorkspaceVirtualEnvInterpreters(undefined); + expect(envs || []).to.be.lengthOf(0); + + const resource = Uri.file('x'); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(undefined); + envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs || []).to.be.lengthOf(0); + }); + test('getWorkspaceVirtualEnvInterpreters will not return any interpreters if interpreters are not in workspace folder (windows)', async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + + when(virtualEnvLocator.getInterpreters(resource, true)).thenResolve([interpreter1 as any]); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(OSType.Windows); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs || []).to.be.lengthOf(0); + }); + test('getWorkspaceVirtualEnvInterpreters will return workspace related virtual interpreters (windows)', async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const interpreter2 = { path: path.join(folderPath, 'venv', 'bin', 'python.exe') }; + const interpreter3 = { path: path.join(path.join('one', 'two', 'THREE'), 'venv', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + + when(virtualEnvLocator.getInterpreters(resource, true)).thenResolve([interpreter1, interpreter2, interpreter3] as any); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(OSType.Windows); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs).to.be.deep.equal([interpreter2, interpreter3]); + }); + [OSType.OSX, OSType.Linux].forEach(osType => { + test(`getWorkspaceVirtualEnvInterpreters will not return any interpreters if interpreters are not in workspace folder (${osType})`, async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + + when(virtualEnvLocator.getInterpreters(resource, true)).thenResolve([interpreter1 as any]); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(osType); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs || []).to.be.lengthOf(0); + }); + test(`getWorkspaceVirtualEnvInterpreters will return workspace related virtual interpreters (${osType})`, async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const interpreter2 = { path: path.join(folderPath, 'venv', 'bin', 'python.exe') }; + const interpreter3 = { path: path.join(path.join('one', 'two', 'THREE'), 'venv', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + + when(virtualEnvLocator.getInterpreters(resource, true)).thenResolve([interpreter1, interpreter2, interpreter3] as any); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(osType); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs).to.be.deep.equal([interpreter2]); + }); + }); + test('Invoke next rule if there is no workspace', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + when(nextRule.autoSelectInterpreter(resource, manager)).thenResolve(); + when(helper.getActiveWorkspaceUri(resource)).thenReturn(undefined); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(resource, manager); + + verify(nextRule.autoSelectInterpreter(resource, manager)).once(); + verify(helper.getActiveWorkspaceUri(resource)).once(); + }); + test('Invoke next rule if there is no resouece', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + + when(nextRule.autoSelectInterpreter(undefined, manager)).thenResolve(); + when(helper.getActiveWorkspaceUri(undefined)).thenReturn(undefined); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(undefined, manager); + + verify(nextRule.autoSelectInterpreter(undefined, manager)).once(); + verify(helper.getActiveWorkspaceUri(undefined)).once(); + }); + test('Use pipEnv if that completes first with results', async () => { + const folderUri = Uri.parse('Folder'); + type PythonPathInConfig = { workspaceFolderValue: string }; + const pythonPathInConfig = typemoq.Mock.ofType(); + const pythonPath = { inspect: () => pythonPathInConfig.object }; + pythonPathInConfig + .setup(p => p.workspaceFolderValue) + .returns(() => undefined as any) + .verifiable(typemoq.Times.once()); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + const resource = Uri.file('x'); + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const virtualEnvPromise = createDeferred(); + const nextInvoked = createDeferred(); + rule.next = () => Promise.resolve(nextInvoked.resolve()); + rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; + when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + + rule.cacheSelectedInterpreter = () => Promise.resolve(); + + await rule.autoSelectInterpreter(resource, instance(manager)); + virtualEnvPromise.resolve([]); + + expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); + verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); + verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); + }); + test('Use virtualEnv if that completes first with results', async () => { + const folderUri = Uri.parse('Folder'); + type PythonPathInConfig = { workspaceFolderValue: string }; + const pythonPathInConfig = typemoq.Mock.ofType(); + const pythonPath = { inspect: () => pythonPathInConfig.object }; + pythonPathInConfig + .setup(p => p.workspaceFolderValue) + .returns(() => undefined as any) + .verifiable(typemoq.Times.once()); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + const resource = Uri.file('x'); + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const pipEnvPromise = createDeferred(); + const nextInvoked = createDeferred(); + rule.next = () => Promise.resolve(nextInvoked.resolve()); + rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([interpreterInfo]); + when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + + rule.cacheSelectedInterpreter = () => Promise.resolve(); + + await rule.autoSelectInterpreter(resource, instance(manager)); + pipEnvPromise.resolve([]); + + expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); + verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); + verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); + }); + test('Wait for virtualEnv if pipEnv completes without any intepreters', async () => { + const folderUri = Uri.parse('Folder'); + type PythonPathInConfig = { workspaceFolderValue: string }; + const pythonPathInConfig = typemoq.Mock.ofType(); + const pythonPath = { inspect: () => pythonPathInConfig.object }; + pythonPathInConfig + .setup(p => p.workspaceFolderValue) + .returns(() => undefined as any) + .verifiable(typemoq.Times.once()); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const virtualEnvPromise = createDeferred(); + const nextInvoked = createDeferred(); + rule.next = () => Promise.resolve(nextInvoked.resolve()); + rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; + when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([]); + when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(interpreterInfo); + + rule.cacheSelectedInterpreter = () => Promise.resolve(); + + setTimeout(() => virtualEnvPromise.resolve([interpreterInfo]), 10); + await rule.autoSelectInterpreter(resource, instance(manager)); + + expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); + verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); + verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); + }); + test('Wait for pipEnv if VirtualEnv completes without any intepreters', async () => { + const folderUri = Uri.parse('Folder'); + type PythonPathInConfig = { workspaceFolderValue: string }; + const pythonPathInConfig = typemoq.Mock.ofType(); + const pythonPath = { inspect: () => pythonPathInConfig.object }; + pythonPathInConfig + .setup(p => p.workspaceFolderValue) + .returns(() => undefined as any) + .verifiable(typemoq.Times.once()); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const pipEnvPromise = createDeferred(); + const nextInvoked = createDeferred(); + rule.next = () => Promise.resolve(nextInvoked.resolve()); + rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([]); + when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([]); + when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(interpreterInfo); + + rule.cacheSelectedInterpreter = () => Promise.resolve(); + + setTimeout(() => pipEnvPromise.resolve([interpreterInfo]), 10); + await rule.autoSelectInterpreter(resource, instance(manager)); + + expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); + verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); + verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); + }); +}); diff --git a/src/test/interpreters/condaEnvFileService.unit.test.ts b/src/test/interpreters/condaEnvFileService.unit.test.ts index 6dd925ddf753..138809e7b261 100644 --- a/src/test/interpreters/condaEnvFileService.unit.test.ts +++ b/src/test/interpreters/condaEnvFileService.unit.test.ts @@ -95,5 +95,4 @@ suite('Interpreters from Conda Environments Text File', () => { test('Must filter files in the list and return valid items (windows)', async () => { await filterFilesInEnvironmentsFileAndReturnValidItems(true); }); - }); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index 9ae7752a208c..dc9090525ecc 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -156,7 +156,7 @@ suite('Interpreters Display', () => { .returns(() => Promise.resolve(activeInterpreter)) .verifiable(TypeMoq.Times.once()); interpreterHelper - .setup(i => i.getActiveWorkspaceUri()) + .setup(i => i.getActiveWorkspaceUri(undefined)) .returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/interpreters/helper.unit.test.ts b/src/test/interpreters/helpers.unit.test.ts similarity index 75% rename from src/test/interpreters/helper.unit.test.ts rename to src/test/interpreters/helpers.unit.test.ts index 7ff97624c9d0..0027bf096782 100644 --- a/src/test/interpreters/helper.unit.test.ts +++ b/src/test/interpreters/helpers.unit.test.ts @@ -4,13 +4,14 @@ 'use strict'; import { expect } from 'chai'; +import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, TextDocument, TextEditor, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { InterpreterHelper } from '../../client/interpreter/helpers'; import { IServiceContainer } from '../../client/ioc/types'; -// tslint:disable-next-line:max-func-body-length +// tslint:disable:max-func-body-length no-any suite('Interpreters Display Helper', () => { let documentManager: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; @@ -29,7 +30,7 @@ suite('Interpreters Display Helper', () => { test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { workspaceService.setup(w => w.workspaceFolders).returns(() => []); documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); - const workspace = helper.getActiveWorkspaceUri(); + const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.equal(undefined, 'incorrect value'); }); test('getActiveWorkspaceUri should return the workspace if there is only one', () => { @@ -37,7 +38,7 @@ suite('Interpreters Display Helper', () => { // tslint:disable-next-line:no-any workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any]); - const workspace = helper.getActiveWorkspaceUri(); + const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.not.equal(undefined, 'incorrect value'); expect(workspace!.folderUri).to.be.equal(folderUri); expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.Workspace); @@ -48,7 +49,7 @@ suite('Interpreters Display Helper', () => { workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); documentManager.setup(d => d.activeTextEditor).returns(() => undefined); - const workspace = helper.getActiveWorkspaceUri(); + const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.equal(undefined, 'incorrect value'); }); test('getActiveWorkspaceUri should return undefined of the active editor does not belong to a workspace and if we have more than one workspace folder', () => { @@ -63,7 +64,7 @@ suite('Interpreters Display Helper', () => { documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => undefined); - const workspace = helper.getActiveWorkspaceUri(); + const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.equal(undefined, 'incorrect value'); }); test('getActiveWorkspaceUri should return workspace folder of the active editor if belongs to a workspace and if we have more than one workspace folder', () => { @@ -80,9 +81,24 @@ suite('Interpreters Display Helper', () => { // tslint:disable-next-line:no-any workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => { return { uri: documentWorkspaceFolderUri } as any; }); - const workspace = helper.getActiveWorkspaceUri(); + const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.not.equal(undefined, 'incorrect value'); expect(workspace!.folderUri).to.be.equal(documentWorkspaceFolderUri); expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.WorkspaceFolder); }); + test('getBestInterpreter should return undefined for an empty list', () => { + expect(helper.getBestInterpreter([])).to.be.equal(undefined, 'should be undefined'); + expect(helper.getBestInterpreter(undefined)).to.be.equal(undefined, 'should be undefined'); + }); + test('getBestInterpreter should return first item if there is only one', () => { + expect(helper.getBestInterpreter(['a'] as any)).to.be.equal('a', 'should be undefined'); + }); + test('getBestInterpreter should return interpreter with highest version', () => { + const interpreter1 = { version: JSON.parse(JSON.stringify(new SemVer('1.0.0-alpha'))) }; + const interpreter2 = { version: JSON.parse(JSON.stringify(new SemVer('3.6.0'))) }; + const interpreter3 = { version: JSON.parse(JSON.stringify(new SemVer('3.7.1-alpha'))) }; + const interpreter4 = { version: JSON.parse(JSON.stringify(new SemVer('3.6.0-alpha'))) }; + const interpreters = [interpreter1, interpreter2, interpreter3, interpreter4] as any; + expect(helper.getBestInterpreter(interpreters)).to.be.deep.equal(interpreter3); + }); }); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 7c3fd6efbd44..21bf8cfbcc68 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -11,7 +11,7 @@ import { Container } from 'inversify'; import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; +import { Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { getArchitectureDisplayName } from '../../client/common/platform/registry'; import { IFileSystem } from '../../client/common/platform/types'; @@ -20,6 +20,7 @@ import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory, IP import * as EnumEx from '../../client/common/utils/enum'; import { noop } from '../../client/common/utils/misc'; import { Architecture } from '../../client/common/utils/platform'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; import { IInterpreterDisplay, @@ -27,31 +28,17 @@ import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, - PIPENV_SERVICE, - PythonInterpreter, - WORKSPACE_VIRTUAL_ENV_SERVICE, - WorkspacePythonPath + PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterService } from '../../client/interpreter/interpreterService'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { PYTHON_PATH } from '../common'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; use(chaiAsPromised); -const info: PythonInterpreter = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - type: InterpreterType.Unknown, - version: new SemVer('0.0.0-alpha'), - sysPrefix: '', - sysVersion: '' -}; - suite('Interpreters service', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; @@ -60,18 +47,14 @@ suite('Interpreters service', () => { let locator: TypeMoq.IMock; let workspace: TypeMoq.IMock; let config: TypeMoq.IMock; - let pipenvLocator: TypeMoq.IMock; - let wksLocator: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let interpreterDisplay: TypeMoq.IMock; - let workspacePythonPath: TypeMoq.IMock; let virtualEnvMgr: TypeMoq.IMock; let persistentStateFactory: TypeMoq.IMock; let pythonExecutionFactory: TypeMoq.IMock; let pythonExecutionService: TypeMoq.IMock; let configService: TypeMoq.IMock; let pythonSettings: TypeMoq.IMock; - type ConfigValue = { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T; workspaceFolderValue?: T }; function setupSuite() { const cont = new Container(); @@ -85,7 +68,6 @@ suite('Interpreters service', () => { config = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); interpreterDisplay = TypeMoq.Mock.ofType(); - workspacePythonPath = TypeMoq.Mock.ofType(); virtualEnvMgr = TypeMoq.Mock.ofType(); persistentStateFactory = TypeMoq.Mock.ofType(); pythonExecutionFactory = TypeMoq.Mock.ofType(); @@ -120,11 +102,9 @@ suite('Interpreters service', () => { serviceManager.addSingletonInstance(IPersistentStateFactory, persistentStateFactory.object); serviceManager.addSingletonInstance(IPythonExecutionFactory, pythonExecutionFactory.object); serviceManager.addSingletonInstance(IPythonExecutionService, pythonExecutionService.object); + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); serviceManager.addSingletonInstance(IConfigurationService, configService.object); - - pipenvLocator = TypeMoq.Mock.ofType(); - wksLocator = TypeMoq.Mock.ofType(); - } suite('Misc', () => { setup(setupSuite); @@ -238,290 +218,6 @@ suite('Interpreters service', () => { }); }); - suite('Should Auto Set Interpreter', () => { - setup(setupSuite); - test('Should not auto set interpreter if there is no workspace', async () => { - const service = new InterpreterService(serviceContainer); - helper - .setup(h => h.getActiveWorkspaceUri()) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - - await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(false, 'not false'); - - helper.verifyAll(); - }); - - test('Should not auto set interpreter if there is a value in global user settings (global value is not \'python\')', async () => { - const service = new InterpreterService(serviceContainer); - workspacePythonPath - .setup(w => w.folderUri) - .returns(() => Uri.file('w')) - .verifiable(TypeMoq.Times.once()); - helper - .setup(h => h.getActiveWorkspaceUri()) - .returns(() => workspacePythonPath.object) - .verifiable(TypeMoq.Times.once()); - const pythonPathConfigValue = TypeMoq.Mock.ofType>(); - config - .setup(w => w.inspect(TypeMoq.It.isAny())) - .returns(() => pythonPathConfigValue.object) - .verifiable(TypeMoq.Times.once()); - pythonPathConfigValue - .setup(p => p.globalValue) - .returns(() => path.join('a', 'bin', 'python')) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(false, 'not false'); - - helper.verifyAll(); - workspace.verifyAll(); - config.verifyAll(); - pythonPathConfigValue.verifyAll(); - }); - test('Should not auto set interpreter if there is a value in workspace settings (& value is not \'python\')', async () => { - const service = new InterpreterService(serviceContainer); - workspacePythonPath - .setup(w => w.configTarget) - .returns(() => ConfigurationTarget.Workspace) - .verifiable(TypeMoq.Times.once()); - helper - .setup(h => h.getActiveWorkspaceUri()) - .returns(() => workspacePythonPath.object) - .verifiable(TypeMoq.Times.once()); - const pythonPathConfigValue = TypeMoq.Mock.ofType>(); - config - .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) - .returns(() => pythonPathConfigValue.object) - .verifiable(TypeMoq.Times.once()); - pythonPathConfigValue - .setup(p => p.globalValue) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - pythonPathConfigValue - .setup(p => p.workspaceValue) - .returns(() => path.join('a', 'bin', 'python')) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(false, 'not false'); - - helper.verifyAll(); - workspace.verifyAll(); - config.verifyAll(); - pythonPathConfigValue.verifyAll(); - }); - - [ - { configTarget: ConfigurationTarget.Workspace, label: 'Workspace' }, - { configTarget: ConfigurationTarget.WorkspaceFolder, label: 'Workspace Folder' } - ].forEach(item => { - const testSuffix = `(${item.label})`; - const cfgTarget = item.configTarget as (ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder); - test(`Should auto set interpreter if there is no value in workspace settings ${testSuffix}`, async () => { - const service = new InterpreterService(serviceContainer); - workspacePythonPath - .setup(w => w.configTarget) - .returns(() => cfgTarget) - .verifiable(TypeMoq.Times.once()); - helper - .setup(h => h.getActiveWorkspaceUri()) - .returns(() => workspacePythonPath.object) - .verifiable(TypeMoq.Times.once()); - const pythonPathConfigValue = TypeMoq.Mock.ofType>(); - config - .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) - .returns(() => pythonPathConfigValue.object) - .verifiable(TypeMoq.Times.once()); - pythonPathConfigValue - .setup(p => p.globalValue) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - if (cfgTarget === ConfigurationTarget.Workspace) { - pythonPathConfigValue - .setup(p => p.workspaceValue) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - } else { - pythonPathConfigValue - .setup(p => p.workspaceFolderValue) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - } - - await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(true, 'not true'); - - helper.verifyAll(); - workspace.verifyAll(); - config.verifyAll(); - pythonPathConfigValue.verifyAll(); - }); - - test(`Should auto set interpreter if there is no value in workspace settings and value is \'python\' ${testSuffix}`, async () => { - const service = new InterpreterService(serviceContainer); - workspacePythonPath - .setup(w => w.configTarget) - .returns(() => ConfigurationTarget.Workspace) - .verifiable(TypeMoq.Times.once()); - helper - .setup(h => h.getActiveWorkspaceUri()) - .returns(() => workspacePythonPath.object) - .verifiable(TypeMoq.Times.once()); - const pythonPathConfigValue = TypeMoq.Mock.ofType>(); - config - .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) - .returns(() => pythonPathConfigValue.object) - .verifiable(TypeMoq.Times.once()); - pythonPathConfigValue - .setup(p => p.globalValue) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeastOnce()); - pythonPathConfigValue - .setup(p => p.workspaceValue) - .returns(() => 'python') - .verifiable(TypeMoq.Times.atLeastOnce()); - - await expect(service.shouldAutoSetInterpreter()).to.eventually.equal(true, 'not true'); - - helper.verifyAll(); - workspace.verifyAll(); - config.verifyAll(); - pythonPathConfigValue.verifyAll(); - }); - }); - }); - - suite('Auto Set Interpreter', () => { - setup(setupSuite); - test('autoset interpreter - no workspace', async () => { - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - global pythonPath in config', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', globalValue: 'global' }; - }); - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - workspace has no pythonPath in config', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python' }; - }); - const interpreter: PythonInterpreter = { - ...info, - path: path.join('folder', 'py1', 'bin', 'python.exe'), - type: InterpreterType.Unknown - }; - setupLocators([interpreter], []); - await verifyUpdateCalled(TypeMoq.Times.once()); - }); - - test('autoset interpreter - workspace has default pythonPath in config', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', workspaceValue: 'python' }; - }); - setupLocators([], []); - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - pipenv workspace', async () => { - setupWorkspace('folder'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', workspaceValue: 'python' }; - }); - const interpreter: PythonInterpreter = { - ...info, - path: 'python', - type: InterpreterType.VirtualEnv - }; - setupLocators([], [interpreter]); - await verifyUpdateCallData('python', ConfigurationTarget.Workspace, 'folder'); - }); - - test('autoset interpreter - workspace without interpreter', async () => { - setupWorkspace('root'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python', workspaceValue: 'elsewhere' }; - }); - const interpreter: PythonInterpreter = { - ...info, - path: 'elsewhere', - type: InterpreterType.Unknown - }; - - setupLocators([interpreter], []); - await verifyUpdateCalled(TypeMoq.Times.never()); - }); - - test('autoset interpreter - workspace with interpreter', async () => { - setupWorkspace('root'); - config.setup(x => x.inspect('pythonPath')).returns(() => { - return { key: 'python' }; - }); - const intPath = path.join('root', 'under', 'bin', 'python.exe'); - const interpreter: PythonInterpreter = { - ...info, - path: intPath, - type: InterpreterType.Unknown - }; - - setupLocators([interpreter], []); - await verifyUpdateCallData(intPath, ConfigurationTarget.Workspace, 'root'); - }); - - async function verifyUpdateCalled(times: TypeMoq.Times) { - const service = new InterpreterService(serviceContainer); - await service.autoSetInterpreter(); - updater - .verify(x => x.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), times); - } - - async function verifyUpdateCallData(pythonPath: string, target: ConfigurationTarget, wksFolder: string) { - let pp: string | undefined; - let confTarget: ConfigurationTarget | undefined; - let trigger; - let wks; - updater - .setup(x => x.updatePythonPath(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - // tslint:disable-next-line:no-any - .callback((p: string, c: ConfigurationTarget, t: any, w: any) => { - pp = p; - confTarget = c; - trigger = t; - wks = w; - }) - .returns(() => Promise.resolve()); - - const service = new InterpreterService(serviceContainer); - await service.autoSetInterpreter(); - - expect(pp).not.to.be.equal(undefined, 'updatePythonPath not called'); - expect(pp!).to.be.equal(pythonPath, 'invalid Python path'); - expect(confTarget).to.be.equal(target, 'invalid configuration target'); - expect(trigger).to.be.equal('load', 'invalid trigger'); - expect(wks.fsPath).to.be.equal(Uri.file(wksFolder).fsPath, 'invalid workspace Uri'); - } - - function setupWorkspace(folder: string) { - const wsPath: WorkspacePythonPath = { - folderUri: Uri.file(folder), - configTarget: ConfigurationTarget.Workspace - }; - helper.setup(x => x.getActiveWorkspaceUri()).returns(() => wsPath); - } - - function setupLocators(wks: PythonInterpreter[], pipenv: PythonInterpreter[]) { - pipenvLocator.setup(x => x.getInterpreters(TypeMoq.It.isAny(), TypeMoq.It.isValue(true))).returns(() => Promise.resolve(pipenv)); - serviceManager.addSingletonInstance(IInterpreterLocatorService, pipenvLocator.object, PIPENV_SERVICE); - wksLocator.setup(x => x.getInterpreters(TypeMoq.It.isAny(), TypeMoq.It.isValue(true))).returns(() => Promise.resolve(wks)); - serviceManager.addSingletonInstance(IInterpreterLocatorService, wksLocator.object, WORKSPACE_VIRTUAL_ENV_SERVICE); - - } - }); - suite('Caching Display name', () => { setup(() => { setupSuite(); diff --git a/src/test/interpreters/venv.unit.test.ts b/src/test/interpreters/venv.unit.test.ts index fa4e64c2ffb6..8ab27cbb71ca 100644 --- a/src/test/interpreters/venv.unit.test.ts +++ b/src/test/interpreters/venv.unit.test.ts @@ -10,11 +10,13 @@ import { Uri, WorkspaceFolder } from 'vscode'; import { IWorkspaceService } from '../../client/common/application/types'; import { PlatformService } from '../../client/common/platform/platformService'; import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../client/common/types'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { GlobalVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/globalVirtualEnvService'; import { WorkspaceVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/workspaceVirtualEnvService'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; suite('Virtual environments', () => { let serviceManager: ServiceManager; @@ -42,6 +44,8 @@ suite('Virtual environments', () => { serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); serviceManager.addSingletonInstance(ICurrentProcess, process.object); serviceManager.addSingletonInstance(IVirtualEnvironmentManager, virtualEnvMgr.object); + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); }); test('Global search paths', async () => { diff --git a/src/test/interpreters/windowsRegistryService.unit.test.ts b/src/test/interpreters/windowsRegistryService.unit.test.ts index cc59274419b2..82ae18b7f57e 100644 --- a/src/test/interpreters/windowsRegistryService.unit.test.ts +++ b/src/test/interpreters/windowsRegistryService.unit.test.ts @@ -176,22 +176,22 @@ suite('Interpreters from Windows Registry (unit)', () => { assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[0].version!.raw, '1.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[0].version!.raw, '1.0.0', 'Incorrect version'); assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[1].version!.raw, '2.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[1].version!.raw, '2.0.0', 'Incorrect version'); assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); assert.equal(interpreters[2].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[2].version!.raw, '4.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[2].version!.raw, '4.0.0', 'Incorrect version'); assert.equal(interpreters[3].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[3].companyDisplayName, 'Company Two', 'Incorrect company name'); assert.equal(interpreters[3].path, path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[3].version!.raw, '5.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[3].version!.raw, '5.0.0', 'Incorrect version'); }); test('Must return multiple entries excluding the invalid registry items and duplicate paths', async () => { const registryKeys = [ @@ -240,22 +240,22 @@ suite('Interpreters from Windows Registry (unit)', () => { assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[0].version!.raw, '1.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[0].version!.raw, '1.0.0', 'Incorrect version'); assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); assert.equal(interpreters[1].path, path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[1].version!.raw, '2.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[1].version!.raw, '2.0.0', 'Incorrect version'); assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); assert.equal(interpreters[2].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[2].version!.raw, '3.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[2].version!.raw, '3.0.0', 'Incorrect version'); assert.equal(interpreters[3].architecture, Architecture.x86, 'Incorrect arhictecture'); assert.equal(interpreters[3].companyDisplayName, 'Company Two', 'Incorrect company name'); assert.equal(interpreters[3].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); - assert.equal(interpreters[3].version!.raw, '4.0.0-unknown', 'Incorrect version'); + assert.equal(interpreters[3].version!.raw, '4.0.0', 'Incorrect version'); }); test('Must return multiple entries excluding the invalid registry items and nonexistent paths', async () => { const registryKeys = [ @@ -305,11 +305,11 @@ suite('Interpreters from Windows Registry (unit)', () => { assert.equal(interpreters[0].architecture, Architecture.x86, '1. Incorrect arhictecture'); assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', '1. Incorrect company name'); assert.equal(interpreters[0].path, path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), '1. Incorrect path'); - assert.equal(interpreters[0].version!.raw, '1.0.0-unknown', '1. Incorrect version'); + assert.equal(interpreters[0].version!.raw, '1.0.0', '1. Incorrect version'); assert.equal(interpreters[1].architecture, Architecture.x86, '2. Incorrect arhictecture'); assert.equal(interpreters[1].companyDisplayName, 'Company Two', '2. Incorrect company name'); assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), '2. Incorrect path'); - assert.equal(interpreters[1].version!.raw, '2.0.0-unknown', '2. Incorrect version'); + assert.equal(interpreters[1].version!.raw, '2.0.0', '2. Incorrect version'); }); }); diff --git a/src/test/languageServers/jedi/autocomplete/pep526.test.ts b/src/test/languageServers/jedi/autocomplete/pep526.test.ts index de9c5de44eab..3c7d5bb98e57 100644 --- a/src/test/languageServers/jedi/autocomplete/pep526.test.ts +++ b/src/test/languageServers/jedi/autocomplete/pep526.test.ts @@ -34,7 +34,7 @@ suite('Autocomplete PEP 526', () => { suiteTeardown(closeActiveWindows); teardown(async () => { await closeActiveWindows(); - ioc.dispose(); + await ioc.dispose(); }); function initializeDI() { ioc = new UnitTestIocContainer(); diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts index 43824b00a88a..d9826c6a056f 100644 --- a/src/test/linters/lint.args.test.ts +++ b/src/test/linters/lint.args.test.ts @@ -14,6 +14,7 @@ import { IDocumentManager, IWorkspaceService } from '../../client/common/applica import '../../client/common/extensions'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IConfigurationService, IInstaller, ILintingSettings, ILogger, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -29,6 +30,7 @@ import { PyLama } from '../../client/linters/pylama'; import { Pylint } from '../../client/linters/pylint'; import { ILinterManager, ILintingEngine } from '../../client/linters/types'; import { initialize } from '../initialize'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; suite('Linting - Arguments', () => { [undefined, path.join('users', 'dev_user')].forEach(workspaceUri => { @@ -62,7 +64,8 @@ suite('Linting - Arguments', () => { interpreterService = TypeMoq.Mock.ofType(); serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); engine = TypeMoq.Mock.ofType(); serviceManager.addSingletonInstance(ILintingEngine, engine.object); diff --git a/src/test/linters/lint.commands.test.ts b/src/test/linters/lint.commands.test.ts deleted file mode 100644 index 64c37c26e150..000000000000 --- a/src/test/linters/lint.commands.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as assert from 'assert'; -import { Container } from 'inversify'; -import * as TypeMoq from 'typemoq'; -import { QuickPickOptions } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { IConfigurationService, Product } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LinterCommands } from '../../client/linters/linterCommands'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; - -// tslint:disable-next-line:max-func-body-length -suite('Linting - Linter Selector', () => { - let serviceContainer: IServiceContainer; - let appShell: TypeMoq.IMock; - let commands: LinterCommands; - let lm: ILinterManager; - let engine: TypeMoq.IMock; - - suiteSetup(initialize); - setup(async () => { - await initializeTest(); - initializeServices(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => closeActiveWindows()); - - function initializeServices() { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - - appShell = TypeMoq.Mock.ofType(); - serviceManager.addSingleton(IConfigurationService, ConfigurationService); - - const commandManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ICommandManager, commandManager.object); - serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - const workspaceService = TypeMoq.Mock.ofType(); - lm = new LinterManager(serviceContainer, workspaceService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - - commands = new LinterCommands(serviceContainer); - } - - test('Enable linting', async () => { - await enableDisableLinterAsync(true); - }); - - test('Disable linting', async () => { - await enableDisableLinterAsync(false); - }); - - test('Single linter active', async () => { - await selectLinterAsync([Product.pylama]); - }); - - test('Multiple linters active', async () => { - await selectLinterAsync([Product.flake8, Product.pydocstyle]); - }); - - test('No linters active', async () => { - await selectLinterAsync([Product.flake8]); - }); - - test('Run linter command', async () => { - await commands.runLinting(); - engine.verify(p => p.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - async function enableDisableLinterAsync(enable: boolean): Promise { - let suggestions: string[] = []; - let options: QuickPickOptions; - - await lm.enableLintingAsync(!enable); - appShell.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((s, o) => { - suggestions = s as string[]; - options = o as QuickPickOptions; - }) - .returns((s) => enable - ? new Promise((resolve, reject) => { return resolve('on'); }) - : new Promise((resolve, reject) => { return resolve('off'); }) - ); - const current = enable ? 'off' : 'on'; - await commands.enableLintingAsync(); - assert.notEqual(suggestions.length, 0, 'showQuickPick was not called'); - assert.notEqual(options!, undefined, 'showQuickPick was not called'); - - assert.equal(suggestions.length, 2, 'Wrong number of suggestions'); - assert.equal(suggestions[0], 'on', 'Wrong first suggestions'); - assert.equal(suggestions[1], 'off', 'Wrong second suggestions'); - - assert.equal(options!.matchOnDescription, true, 'Quick pick options are incorrect'); - assert.equal(options!.matchOnDetail, true, 'Quick pick options are incorrect'); - assert.equal(options!.placeHolder, `current: ${current}`, 'Quick pick current option is incorrect'); - assert.equal(await lm.isLintingEnabled(true, undefined), enable, 'Linting selector did not change linting on/off flag'); - } - - async function selectLinterAsync(products: Product[]): Promise { - let suggestions: string[] = []; - let options: QuickPickOptions; - let warning: string; - - appShell.setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((s, o) => { - suggestions = s as string[]; - options = o as QuickPickOptions; - }) - .returns(s => new Promise((resolve, reject) => resolve('pylint'))); - appShell.setup(x => x.showWarningMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((s, o) => { - warning = s; - }) - .returns(s => new Promise((resolve, reject) => resolve('Yes'))); - - const linters = lm.getAllLinterInfos(); - await lm.setActiveLintersAsync(products); - - let current: string; - let activeLinters = await lm.getActiveLinters(true); - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - await commands.setLinterAsync(); - - assert.notEqual(suggestions.length, 0, 'showQuickPick was not called'); - assert.notEqual(options!, undefined, 'showQuickPick was not called'); - - assert.equal(suggestions.length, linters.length + 1, 'Wrong number of suggestions'); - assert.deepEqual(suggestions, ['Disable Linting', ...linters.map(x => x.id).sort()], 'Wrong linters order in suggestions'); - - assert.equal(options!.matchOnDescription, true, 'Quick pick options are incorrect'); - assert.equal(options!.matchOnDetail, true, 'Quick pick options are incorrect'); - assert.equal(options!.placeHolder, `current: ${current}`, 'Quick pick current option is incorrect'); - - activeLinters = await lm.getActiveLinters(true); - assert.equal(activeLinters.length, 1, 'Linting selector did not change active linter'); - assert.equal(activeLinters[0].product, Product.pylint, 'Linting selector did not change to pylint'); - - if (products.length > 1) { - assert.notEqual(warning!, undefined, 'Warning was not shown when overwriting multiple linters'); - } - } -}); diff --git a/src/test/linters/lint.manager.test.ts b/src/test/linters/lint.manager.test.ts deleted file mode 100644 index 45532719a5de..000000000000 --- a/src/test/linters/lint.manager.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as assert from 'assert'; -import { Container } from 'inversify'; -import * as typeMoq from 'typemoq'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { IConfigurationService, ILintingSettings, IPythonSettings, Product } from '../../client/common/types'; -import * as EnumEx from '../../client/common/utils/enum'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager, LinterId } from '../../client/linters/types'; -import { initialize } from '../initialize'; - -// tslint:disable-next-line:max-func-body-length -suite('Linting - Manager', () => { - let lm: ILinterManager; - let configService: IConfigurationService; - let settings: IPythonSettings; - - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - const serviceContainer = new ServiceContainer(cont); - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - - serviceManager.addSingleton(IConfigurationService, ConfigurationService); - configService = serviceManager.get(IConfigurationService); - - settings = configService.getSettings(); - const workspaceService = typeMoq.Mock.ofType(); - lm = new LinterManager(serviceContainer, workspaceService.object); - - await lm.setActiveLintersAsync([Product.pylint]); - await lm.enableLintingAsync(true); - }); - - test('Ensure product is set in Execution Info', async () => { - [Product.bandit, Product.flake8, Product.mypy, Product.pep8, - Product.pydocstyle, Product.pylama, Product.pylint].forEach(product => { - const execInfo = lm.getLinterInfo(product).getExecutionInfo([]); - assert.equal(execInfo.product, product, `Incorrect information for ${product}`); - }); - }); - - test('Ensure executable is set in Execution Info', async () => { - [Product.bandit, Product.flake8, Product.mypy, Product.pep8, - Product.pydocstyle, Product.pylama, Product.pylint].forEach(product => { - const info = lm.getLinterInfo(product); - const execInfo = info.getExecutionInfo([]); - const execPath = settings.linting[info.pathSettingName] as string; - assert.equal(execInfo.execPath, execPath, `Incorrect executable paths for product ${info.id}`); - }); - }); - - test('Ensure correct setting names are returned', async () => { - [Product.bandit, Product.flake8, Product.mypy, Product.pep8, - Product.pydocstyle, Product.pylama, Product.pylint].forEach(product => { - const linter = lm.getLinterInfo(product); - const expected = { - argsName: `${linter.id}Args` as keyof ILintingSettings, - pathName: `${linter.id}Path` as keyof ILintingSettings, - enabledName: `${linter.id}Enabled` as keyof ILintingSettings - }; - - assert.equal(linter.argsSettingName, expected.argsName, `Incorrect args settings for product ${linter.id}`); - assert.equal(linter.pathSettingName, expected.pathName, `Incorrect path settings for product ${linter.id}`); - assert.equal(linter.enabledSettingName, expected.enabledName, `Incorrect enabled settings for product ${linter.id}`); - }); - }); - - test('Ensure linter id match product', async () => { - const ids = ['bandit', 'flake8', 'mypy', 'pep8', 'prospector', 'pydocstyle', 'pylama', 'pylint']; - const products = [Product.bandit, Product.flake8, Product.mypy, Product.pep8, Product.prospector, Product.pydocstyle, Product.pylama, Product.pylint]; - for (let i = 0; i < products.length; i += 1) { - const linter = lm.getLinterInfo(products[i]); - assert.equal(linter.id, ids[i], `Id ${ids[i]} does not match product ${products[i]}`); - } - }); - - test('Enable/disable linting', async () => { - await lm.enableLintingAsync(false); - assert.equal(await lm.isLintingEnabled(true), false, 'Linting not disabled'); - await lm.enableLintingAsync(true); - assert.equal(await lm.isLintingEnabled(true), true, 'Linting not enabled'); - }); - - test('Set single linter', async () => { - for (const linter of lm.getAllLinterInfos()) { - await lm.setActiveLintersAsync([linter.product]); - const selected = await lm.getActiveLinters(true); - assert.notEqual(selected.length, 0, 'Current linter is undefined'); - assert.equal(linter!.id, selected![0].id, `Selected linter ${selected} does not match requested ${linter.id}`); - } - }); - - test('Set multiple linters', async () => { - await lm.setActiveLintersAsync([Product.flake8, Product.pydocstyle]); - const selected = await lm.getActiveLinters(true); - assert.equal(selected.length, 2, 'Selected linters lengths does not match'); - assert.equal(Product.flake8, selected[0].product, `Selected linter ${selected[0].id} does not match requested 'flake8'`); - assert.equal(Product.pydocstyle, selected[1].product, `Selected linter ${selected[1].id} does not match requested 'pydocstyle'`); - }); - - test('Try setting unsupported linter', async () => { - const before = await lm.getActiveLinters(true); - assert.notEqual(before, undefined, 'Current/before linter is undefined'); - - await lm.setActiveLintersAsync([Product.nosetest]); - const after = await lm.getActiveLinters(true); - assert.notEqual(after, undefined, 'Current/after linter is undefined'); - - assert.equal(after![0].id, before![0].id, 'Should not be able to set unsupported linter'); - }); - - test('Pylint configuration file watch', async () => { - const pylint = lm.getLinterInfo(Product.pylint); - assert.equal(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); - assert.notEqual(pylint.configFileNames.indexOf('pylintrc'), -1, 'Pylint configuration files miss pylintrc.'); - assert.notEqual(pylint.configFileNames.indexOf('.pylintrc'), -1, 'Pylint configuration files miss .pylintrc.'); - }); - - EnumEx.getValues(Product).forEach(product => { - const linterIdMapping = new Map(); - linterIdMapping.set(Product.bandit, 'bandit'); - linterIdMapping.set(Product.flake8, 'flake8'); - linterIdMapping.set(Product.mypy, 'mypy'); - linterIdMapping.set(Product.pep8, 'pep8'); - linterIdMapping.set(Product.prospector, 'prospector'); - linterIdMapping.set(Product.pydocstyle, 'pydocstyle'); - linterIdMapping.set(Product.pylama, 'pylama'); - linterIdMapping.set(Product.pylint, 'pylint'); - if (linterIdMapping.has(product)) { - return; - } - - test(`Ensure translation of ids throws exceptions for unknown linters (${product})`, async () => { - assert.throws(() => lm.getLinterInfo(product)); - }); - }); -}); diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts index a78232b54491..458f469b9a3b 100644 --- a/src/test/linters/lint.provider.test.ts +++ b/src/test/linters/lint.provider.test.ts @@ -14,6 +14,7 @@ import { IPythonSettings, Product } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -24,6 +25,7 @@ import { } from '../../client/linters/types'; import { LinterProvider } from '../../client/providers/linterProvider'; import { initialize } from '../initialize'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; // tslint:disable-next-line:max-func-body-length suite('Linting - Provider', () => { @@ -82,7 +84,8 @@ suite('Linting - Provider', () => { serviceManager.addSingletonInstance(IInstaller, linterInstaller.object); serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); serviceManager.add(IAvailableLinterActivator, AvailableLinterActivator); - + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); lm = new LinterManager(serviceContainer, workspaceService.object); serviceManager.addSingletonInstance(ILinterManager, lm); emitter = new vscode.EventEmitter(); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index 828d46838a6b..153c317f3768 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -114,7 +114,7 @@ suite('Linting - General Tests', () => { }); suiteTeardown(closeActiveWindows); teardown(async () => { - ioc.dispose(); + await ioc.dispose(); await closeActiveWindows(); await resetSettings(); await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); diff --git a/src/test/linters/linterCommands.unit.test.ts b/src/test/linters/linterCommands.unit.test.ts new file mode 100644 index 000000000000..ff244b893d1e --- /dev/null +++ b/src/test/linters/linterCommands.unit.test.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length messages-must-be-localized + +import { expect } from 'chai'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { ServiceContainer } from '../../client/ioc/container'; +import { LinterCommands } from '../../client/linters/linterCommands'; +import { LinterManager } from '../../client/linters/linterManager'; +import { LintingEngine } from '../../client/linters/lintingEngine'; +import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; + +suite('Linting - Linter Commands', () => { + let linterCommands: LinterCommands; + let manager: ILinterManager; + let shell: IApplicationShell; + let docManager: IDocumentManager; + let cmdManager: ICommandManager; + let lintingEngine: ILintingEngine; + setup(() => { + const svcContainer = mock(ServiceContainer); + manager = mock(LinterManager); + shell = mock(ApplicationShell); + docManager = mock(DocumentManager); + cmdManager = mock(CommandManager); + lintingEngine = mock(LintingEngine); + when(svcContainer.get(ILinterManager)).thenReturn(instance(manager)); + when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); + when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); + when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); + when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); + linterCommands = new LinterCommands(instance(svcContainer)); + }); + + test('Commands are registered', () => { + verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); + verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); + verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); + }); + + test('Run Linting method will lint all open files', async () => { + when(lintingEngine.lintOpenPythonFiles()).thenResolve('Hello' as any); + + const result = await linterCommands.runLinting(); + + expect(result).to.be.equal('Hello'); + }); + + async function testEnableLintingWithCurrentState(currentState: boolean, selectedState: 'on' | 'off' | undefined) { + when(manager.isLintingEnabled(true, anything())).thenResolve(currentState); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${currentState ? 'on' : 'off'}` + }; + when(shell.showQuickPick(anything(), anything())).thenResolve(selectedState as any); + + await linterCommands.enableLintingAsync(); + + verify(shell.showQuickPick(anything(), anything())).once(); + const options = capture(shell.showQuickPick).last()[0]; + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(options).to.deep.equal(['on', 'off']); + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + + if (selectedState) { + verify(manager.enableLintingAsync(selectedState === 'on', anything())).once(); + } else { + verify(manager.enableLintingAsync(anything(), anything())).never(); + } + } + test('Enable linting should check if linting is enabled, and display current state of \'on\' and select nothing', async () => { + await testEnableLintingWithCurrentState(true, undefined); + }); + test('Enable linting should check if linting is enabled, and display current state of \'on\' and select \'on\'', async () => { + await testEnableLintingWithCurrentState(true, 'on'); + }); + test('Enable linting should check if linting is enabled, and display current state of \'on\' and select \'off\'', async () => { + await testEnableLintingWithCurrentState(true, 'off'); + }); + test('Enable linting should check if linting is enabled, and display current state of \'off\' and select \'on\'', async () => { + await testEnableLintingWithCurrentState(true, 'on'); + }); + test('Enable linting should check if linting is enabled, and display current state of \'off\' and select \'off\'', async () => { + await testEnableLintingWithCurrentState(true, 'off'); + }); + + test('Set Linter should display a quickpick', async () => { + when(manager.getAllLinterInfos()).thenReturn([]); + when(manager.getActiveLinters(true, anything())).thenResolve([]); + when(shell.showQuickPick(anything(), anything())).thenResolve(); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: 'current: none' + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + }); + test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { + const linterId = 'Hello World'; + const activeLinters: ILinterInfo[] = [{ id: linterId } as any]; + when(manager.getAllLinterInfos()).thenReturn([]); + when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); + when(shell.showQuickPick(anything(), anything())).thenResolve(); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${linterId}` + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())).once(); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + }); + test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { + const activeLinters: ILinterInfo[] = [{ id: 'linterId' } as any, { id: 'linterId2' } as any]; + when(manager.getAllLinterInfos()).thenReturn([]); + when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); + when(shell.showQuickPick(anything(), anything())).thenResolve(); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: 'current: multiple selected' + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + }); + test('Selecting a linter should display warning message about multiple linters', async () => { + const linters: ILinterInfo[] = [{ id: '1' }, { id: '2' }, { id: '3', product: 'Three' }] as any; + const activeLinters: ILinterInfo[] = [{ id: '1' }, { id: '3' }] as any; + when(manager.getAllLinterInfos()).thenReturn(linters); + when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); + when(shell.showQuickPick(anything(), anything())).thenResolve('3' as any); + when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenResolve('Yes' as any); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: 'current: multiple selected' + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())).once(); + verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + verify(manager.setActiveLintersAsync(deepEqual(['Three']), anything())).once(); + }); +}); diff --git a/src/test/linters/linterManager.unit.test.ts b/src/test/linters/linterManager.unit.test.ts new file mode 100644 index 000000000000..f6b253b58724 --- /dev/null +++ b/src/test/linters/linterManager.unit.test.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length messages-must-be-localized + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { ProductNames } from '../../client/common/installer/productNames'; +import { ProductService } from '../../client/common/installer/productService'; +import { IConfigurationService, Product, ProductType } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; +import { ServiceContainer } from '../../client/ioc/container'; +import { LinterInfo } from '../../client/linters/linterInfo'; +import { LinterManager } from '../../client/linters/linterManager'; +import { LintingEngine } from '../../client/linters/lintingEngine'; +import { ILinterInfo, ILintingEngine } from '../../client/linters/types'; + +suite('Linting - Linter Manager', () => { + let linterManager: LinterManagerTest; + let shell: IApplicationShell; + let docManager: IDocumentManager; + let cmdManager: ICommandManager; + let lintingEngine: ILintingEngine; + let configService: IConfigurationService; + let workspaceService: IWorkspaceService; + class LinterManagerTest extends LinterManager { + // Override base class property to make it public. + public linters!: ILinterInfo[]; + public async enableUnconfiguredLinters(resource?: Uri) { + await super.enableUnconfiguredLinters(resource); + } + } + setup(() => { + const svcContainer = mock(ServiceContainer); + shell = mock(ApplicationShell); + docManager = mock(DocumentManager); + cmdManager = mock(CommandManager); + lintingEngine = mock(LintingEngine); + configService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); + when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); + when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); + when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); + when(svcContainer.get(IConfigurationService)).thenReturn(instance(configService)); + when(svcContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); + linterManager = new LinterManagerTest(instance(svcContainer), instance(workspaceService)); + }); + + test('Get all linters will return a list of all linters', () => { + const linters = linterManager.getAllLinterInfos(); + + expect(linters).to.be.lengthOf(8); + + const productService = new ProductService(); + const linterProducts = getNamesAndValues(Product) + .filter(product => productService.getProductType(product.value) === ProductType.Linter) + .map(item => ProductNames.get(item.value)); + expect(linters.map(item => item.id).sort()).to.be.deep.equal(linterProducts.sort()); + }); + + test('Get linter info for non-linter product should throw an exception', () => { + const productService = new ProductService(); + getNamesAndValues(Product).forEach(prod => { + if (productService.getProductType(prod.value) === ProductType.Linter) { + const info = linterManager.getLinterInfo(prod.value); + expect(info.id).to.equal(ProductNames.get(prod.value)); + expect(info).not.to.be.equal(undefined, 'should not be unedfined'); + } else { + expect(() => linterManager.getLinterInfo(prod.value)).to.throw(); + } + }); + }); + test('Pylint configuration file watch', async () => { + const pylint = linterManager.getLinterInfo(Product.pylint); + assert.equal(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); + assert.notEqual(pylint.configFileNames.indexOf('pylintrc'), -1, 'Pylint configuration files miss pylintrc.'); + assert.notEqual(pylint.configFileNames.indexOf('.pylintrc'), -1, 'Pylint configuration files miss .pylintrc.'); + }); + + [undefined, Uri.parse('something')].forEach(resource => { + const testResourceSuffix = `(${resource ? 'with a resource' : 'without a resource'})`; + [true, false].forEach(enabled => { + const testSuffix = `(${enabled ? 'enable' : 'disable'}) & ${testResourceSuffix}`; + test(`Enable linting should update config ${testSuffix}`, async () => { + when(configService.updateSetting('linting.enabled', enabled, resource)).thenResolve(); + + await linterManager.enableLintingAsync(enabled, resource); + + verify(configService.updateSetting('linting.enabled', enabled, resource)).once(); + }); + }); + test(`getActiveLinters will check if linter is enabled and in silent mode ${testResourceSuffix}`, async () => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters = [instanceOfLinterInfo]; + when(linterInfo.isEnabled(resource)).thenReturn(true); + + const linters = await linterManager.getActiveLinters(true, resource); + + verify(linterInfo.isEnabled(resource)).once(); + expect(linters[0]).to.deep.equal(instanceOfLinterInfo); + }); + test(`getActiveLinters will check if linter is enabled and not in silent mode ${testResourceSuffix}`, async () => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters = [instanceOfLinterInfo]; + when(linterInfo.isEnabled(resource)).thenReturn(true); + let enableUnconfiguredLintersInvoked = false; + linterManager.enableUnconfiguredLinters = async () => { + enableUnconfiguredLintersInvoked = true; + }; + + const linters = await linterManager.getActiveLinters(false, resource); + + verify(linterInfo.isEnabled(resource)).once(); + expect(linters[0]).to.deep.equal(instanceOfLinterInfo); + expect(enableUnconfiguredLintersInvoked).to.equal(true, 'not invoked'); + }); + + test(`setActiveLintersAsync with invalid products does nothing ${testResourceSuffix}`, async () => { + let getActiveLintersInvoked = false; + linterManager.getActiveLinters = async () => { getActiveLintersInvoked = true; return []; }; + + await linterManager.setActiveLintersAsync([Product.ctags, Product.pytest], resource); + + expect(getActiveLintersInvoked).to.be.equal(false, 'Should not be invoked'); + }); + test(`setActiveLintersAsync with single product will disable it then enable it ${testResourceSuffix}`, async () => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters = [instanceOfLinterInfo]; + when(linterInfo.product).thenReturn(Product.flake8); + when(linterInfo.enableAsync(false, resource)).thenResolve(); + linterManager.getActiveLinters = () => Promise.resolve([instanceOfLinterInfo]); + linterManager.enableLintingAsync = () => Promise.resolve(); + + await linterManager.setActiveLintersAsync([Product.flake8], resource); + + verify(linterInfo.enableAsync(false, resource)).atLeast(1); + verify(linterInfo.enableAsync(true, resource)).atLeast(1); + }); + test(`setActiveLintersAsync with single product will disable all existing then enable the necessary two ${testResourceSuffix}`, async () => { + const linters = new Map(); + const linterInstances = new Map(); + linterManager.linters = []; + [Product.flake8, Product.mypy, Product.prospector, Product.bandit, Product.pydocstyle].forEach(product => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters.push(instanceOfLinterInfo); + linters.set(product, linterInfo); + linterInstances.set(product, instanceOfLinterInfo); + when(linterInfo.product).thenReturn(product); + when(linterInfo.enableAsync(anything(), resource)).thenResolve(); + }); + + linterManager.getActiveLinters = () => Promise.resolve(Array.from(linterInstances.values())); + linterManager.enableLintingAsync = () => Promise.resolve(); + + const lintersToEnable = [Product.flake8, Product.mypy, Product.pydocstyle]; + await linterManager.setActiveLintersAsync([Product.flake8, Product.mypy, Product.pydocstyle], resource); + + linters.forEach((item, product) => { + verify(item.enableAsync(false, resource)).atLeast(1); + if (lintersToEnable.indexOf(product) >= 0) { + verify(item.enableAsync(true, resource)).atLeast(1); + } + }); + }); + }); +}); diff --git a/src/test/linters/pylint.test.ts b/src/test/linters/pylint.test.ts index 4c59626ad324..459f233ffc89 100644 --- a/src/test/linters/pylint.test.ts +++ b/src/test/linters/pylint.test.ts @@ -11,12 +11,14 @@ import { IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IPythonToolExecutionService } from '../../client/common/process/types'; import { ExecutionInfo, IConfigurationService, IInstaller, ILogger, IPythonSettings } from '../../client/common/types'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { LinterManager } from '../../client/linters/linterManager'; import { Pylint } from '../../client/linters/pylint'; import { ILinterManager } from '../../client/linters/types'; import { MockLintingSettings } from '../mockClasses'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; // tslint:disable-next-line:max-func-body-length suite('Linting - Pylint', () => { @@ -51,7 +53,8 @@ suite('Linting - Pylint', () => { serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); serviceManager.addSingletonInstance(IPythonToolExecutionService, execService.object); serviceManager.addSingletonInstance(IPlatformService, platformService.object); - + serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); config = TypeMoq.Mock.ofType(); serviceManager.addSingletonInstance(IConfigurationService, config.object); const linterManager = new LinterManager(serviceContainer, workspace.object); diff --git a/src/test/mocks/autoSelector.ts b/src/test/mocks/autoSelector.ts new file mode 100644 index 000000000000..43e4262d285b --- /dev/null +++ b/src/test/mocks/autoSelector.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { Resource } from '../../client/common/types'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; + +@injectable() +export class MockAutoSelectionService implements IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService { + public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonInterpreter): Promise { + return Promise.resolve(); + } + public async setGlobalInterpreter(_interpreter: PythonInterpreter): Promise { + return; + } + get onDidChangeAutoSelectedInterpreter(): Event { + return new EventEmitter().event; + } + public autoSelectInterpreter(_resource: Resource): Promise { + return Promise.resolve(); + } + public getAutoSelectedInterpreter(_resource: Resource): PythonInterpreter | undefined { + return; + } + public registerInstance(_instance: IInterpreterAutoSeletionProxyService): void { + return; + } +} diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 5d0ba9d3ae20..8233f09f8b01 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -22,6 +22,7 @@ import { registerTypes as commonRegisterTypes } from '../client/common/serviceRe import { GLOBAL_MEMENTO, ICurrentProcess, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPathUtils, IsWindows, WORKSPACE_MEMENTO } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../client/interpreter/autoSelection/types'; import { registerTypes as interpretersRegisterTypes } from '../client/interpreter/serviceRegistry'; import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; @@ -30,6 +31,7 @@ import { registerTypes as lintersRegisterTypes } from '../client/linters/service import { TEST_OUTPUT_CHANNEL } from '../client/unittests/common/constants'; import { registerTypes as unittestsRegisterTypes } from '../client/unittests/serviceRegistry'; import { MockOutputChannel } from './mockClasses'; +import { MockAutoSelectionService } from './mocks/autoSelector'; import { MockMemento } from './mocks/mementos'; import { MockProcessService } from './mocks/proc'; import { MockProcess } from './mocks/process'; @@ -56,6 +58,9 @@ export class IocContainer { const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); this.serviceManager.addSingletonInstance(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + + this.serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); + this.serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); } public async dispose() : Promise { for (let i = 0; i < this.disposables.length; i += 1) { diff --git a/src/test/unittests/unittest/unittest.test.ts b/src/test/unittests/unittest/unittest.test.ts index fe9e41f92f63..d4bf60eaac43 100644 --- a/src/test/unittests/unittest/unittest.test.ts +++ b/src/test/unittests/unittest/unittest.test.ts @@ -41,7 +41,7 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { suiteSetup(async () => { await initialize(); - await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); }); setup(async () => { const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); @@ -52,8 +52,8 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { initializeDI(); }); teardown(async () => { - ioc.dispose(); - await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await ioc.dispose(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); }); function initializeDI() { @@ -65,9 +65,9 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { } test('Discover Tests (single test file)', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri, UNITTEST_SINGLE_TEST_FILE_PATH); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); @@ -76,9 +76,9 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { }); test('Discover Tests (many test files, subdir included)', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri, UNITTEST_MULTI_TEST_FILE_PATH); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_MULTI_TEST_FILE_PATH); const tests = await testManager.discoverTests(CommandSource.ui, true, true); assert.equal(tests.testFiles.length, 3, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); @@ -89,9 +89,9 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { }); test('Run single test', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri, UNITTEST_MULTI_TEST_FILE_PATH); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_MULTI_TEST_FILE_PATH); const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); const testFile: TestFile | undefined = testsDiscovered.testFiles.find( (value: TestFile) => value.nameToRun.endsWith('_3A') @@ -120,9 +120,9 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { return this.skip(); } - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri, UNITTEST_COUNTS_TEST_FILE_PATH); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_COUNTS_TEST_FILE_PATH); const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); const testsFile: TestFile | undefined = testsDiscovered.testFiles.find( (value: TestFile) => value.name.startsWith('test_unit_test_counter') @@ -150,9 +150,9 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { return this.skip(); } - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); const factory = ioc.serviceContainer.get(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri, UNITTEST_COUNTS_TEST_FILE_PATH); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_COUNTS_TEST_FILE_PATH); const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); const testsFile: TestFile | undefined = testsDiscovered.testFiles.find( (value: TestFile) => value.name.startsWith('test_unit_test_counter') diff --git a/src/test/workspaceSymbols/generator.unit.test.ts b/src/test/workspaceSymbols/generator.unit.test.ts index e4dfc60728e0..5b425f8fc357 100644 --- a/src/test/workspaceSymbols/generator.unit.test.ts +++ b/src/test/workspaceSymbols/generator.unit.test.ts @@ -12,11 +12,10 @@ import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; +import { IApplicationShell } from '../../client/common/application/types'; import { ConfigurationService } from '../../client/common/configuration/service'; import { FileSystem } from '../../client/common/platform/fileSystem'; -import { PlatformService } from '../../client/common/platform/platformService'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IFileSystem } from '../../client/common/platform/types'; import { ProcessService } from '../../client/common/process/proc'; import { IProcessService, IProcessServiceFactory, Output } from '../../client/common/process/types'; import { IConfigurationService, IOutputChannel, IPythonSettings } from '../../client/common/types'; @@ -32,17 +31,13 @@ suite('Workspace Symbols Generator', () => { let shell: IApplicationShell; let processService: IProcessService; let fs: IFileSystem; - let platformService: typemoq.IMock; - let commandManager: typemoq.IMock; const folderUri = Uri.parse(path.join('a', 'b', 'c')); setup(() => { pythonSettings = typemoq.Mock.ofType(); configurationService = mock(ConfigurationService); factory = typemoq.Mock.ofType(); - platformService = typemoq.Mock.ofType(); shell = mock(ApplicationShell); fs = mock(FileSystem); - commandManager = typemoq.Mock.ofType(); processService = mock(ProcessService); factory.setup(f => f.create(typemoq.It.isAny())).returns(() => Promise.resolve(instance(processService))); when(configurationService.getSettings(anything())).thenReturn(pythonSettings.object);