From de42aaac86d32352f92ea06da8d18c4fbac30114 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 2 Mar 2020 12:49:32 -0800 Subject: [PATCH 1/7] Modified `Select interpreter` command & add a reset interpreter command (#10373) * Modified `Select interpreter` command to support setting interpreter at workspace level * Added a command to reset value of Python interpreter * Code reviews * Update src/client/interpreter/configuration/interpreterSelector.ts Co-Authored-By: Karthik Nadig Co-authored-by: Karthik Nadig --- news/1 Enhancements/10372.md | 1 + news/1 Enhancements/10374.md | 1 + package.json | 11 + package.nls.json | 2 + src/client/common/application/commands.ts | 1 + src/client/common/constants.ts | 1 + src/client/common/utils/localize.ts | 1 + .../configuration/interpreterSelector.ts | 75 +- .../configuration/pythonPathUpdaterService.ts | 8 +- .../services/globalUpdaterService.ts | 2 +- .../services/workspaceFolderUpdaterService.ts | 4 +- src/client/interpreter/configuration/types.ts | 4 +- .../interpreterSelector.unit.test.ts | 644 ++++++++++++------ 13 files changed, 530 insertions(+), 225 deletions(-) create mode 100644 news/1 Enhancements/10372.md create mode 100644 news/1 Enhancements/10374.md diff --git a/news/1 Enhancements/10372.md b/news/1 Enhancements/10372.md new file mode 100644 index 000000000000..73b1707ddaf3 --- /dev/null +++ b/news/1 Enhancements/10372.md @@ -0,0 +1 @@ +Modified `Select interpreter` command to support setting interpreter at workspace level. diff --git a/news/1 Enhancements/10374.md b/news/1 Enhancements/10374.md new file mode 100644 index 000000000000..1baf92bbcc75 --- /dev/null +++ b/news/1 Enhancements/10374.md @@ -0,0 +1 @@ +Added a command to reset value of Python interpreter. diff --git a/package.json b/package.json index 32229e419018..03bb8c0f321f 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "onCommand:python.switchOffInsidersChannel", "onCommand:python.switchToDailyChannel", "onCommand:python.switchToWeeklyChannel", + "onCommand:python.resetPythonInterpreter", "onCommand:python.datascience.createnewnotebook", "onCommand:python.datascience.showhistorypane", "onCommand:python.datascience.importnotebook", @@ -224,6 +225,11 @@ "title": "%python.command.python.switchToWeeklyChannel.title%", "category": "Python" }, + { + "command": "python.resetPythonInterpreter", + "title": "%python.command.python.resetPythonInterpreter.title%", + "category": "Python" + }, { "command": "python.refactorExtractVariable", "title": "%python.command.python.refactorExtractVariable.title%", @@ -753,6 +759,11 @@ "category": "Python", "when": "config.python.insidersChannel != 'weekly'" }, + { + "command": "python.resetPythonInterpreter", + "title": "%python.command.python.resetPythonInterpreter.title%", + "category": "Python" + }, { "command": "python.viewOutput", "title": "%python.command.python.viewOutput.title%", diff --git a/package.nls.json b/package.nls.json index 0a899e1991b1..059bf32fe9e6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,6 +10,7 @@ "python.command.python.switchOffInsidersChannel.title": "Switch to Default Channel", "python.command.python.switchToDailyChannel.title": "Switch to Insiders Daily Channel", "python.command.python.switchToWeeklyChannel.title": "Switch to Insiders Weekly Channel", + "python.command.python.resetPythonInterpreter.title": "Reset Python Interpreter", "python.command.python.refactorExtractVariable.title": "Extract Variable", "python.command.python.refactorExtractMethod.title": "Extract Method", "python.command.python.viewOutput.title": "Show Output", @@ -131,6 +132,7 @@ "DataScience.connectingToJupyter": "Connecting to Jupyter server", "Experiments.inGroup": "User belongs to experiment group '{0}'", "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters", + "Interpreters.entireWorkspace": "Entire workspace", "Interpreters.LoadingInterpreters": "Loading Python Interpreters", "Interpreters.condaInheritEnvMessage": "We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change \"terminal.integrated.inheritEnv\" to false in your user settings.", "Logging.CurrentWorkingDirectory": "cwd:", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 9aa84e46b09c..698e26e07e8a 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -22,6 +22,7 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; interface ICommandNameWithoutArgumentTypeMapping { [Commands.SwitchToInsidersDaily]: []; [Commands.SwitchToInsidersWeekly]: []; + [Commands.ResetPythonInterpreter]: []; [Commands.SwitchOffInsidersChannel]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 94ead56524ca..f9b54a8cfa2c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -63,6 +63,7 @@ export namespace Commands { export const SwitchToInsidersDaily = 'python.switchToDailyChannel'; export const SwitchToInsidersWeekly = 'python.switchToWeeklyChannel'; export const PickLocalProcess = 'python.pickLocalProcess'; + export const ResetPythonInterpreter = 'python.resetPythonInterpreter'; } export namespace Octicons { export const Test_Pass = '$(check)'; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b7c2113e6e92..457a5715eb43 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -140,6 +140,7 @@ export namespace Interpreters { 'Interpreters.environmentPromptMessage', 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?' ); + export const entireWorkspace = localize('Interpreters.entireWorkspace', 'Entire workspace'); export const selectInterpreterTip = localize( 'Interpreters.selectInterpreterTip', 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar' diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 93a859a94483..293e6cb83895 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -1,5 +1,6 @@ import { inject, injectable } from 'inversify'; -import { ConfigurationTarget, Disposable, QuickPickOptions, Uri } from 'vscode'; +import * as path from 'path'; +import { ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri } from 'vscode'; import { IApplicationShell, ICommandManager, @@ -8,7 +9,8 @@ import { } from '../../common/application/types'; import { Commands } from '../../common/constants'; import { IConfigurationService, IPathUtils, Resource } from '../../common/types'; -import { IInterpreterService, IShebangCodeLensProvider, PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { Interpreters } from '../../common/utils/localize'; +import { IInterpreterService, IShebangCodeLensProvider, PythonInterpreter } from '../contracts'; import { IInterpreterComparer, IInterpreterQuickPickItem, @@ -41,6 +43,9 @@ export class InterpreterSelector implements IInterpreterSelector { this.disposables.push( this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)) ); + this.disposables.push( + this.commandManager.registerCommand(Commands.ResetPythonInterpreter, this.resetInterpreter.bind(this)) + ); this.disposables.push( this.commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this)) ); @@ -51,6 +56,17 @@ export class InterpreterSelector implements IInterpreterSelector { interpreters.sort(this.interpreterComparer.compare.bind(this.interpreterComparer)); return Promise.all(interpreters.map((item) => this.suggestionToQuickPickItem(item, resource))); } + + protected async resetInterpreter() { + const targetConfig = await this.getConfigTarget(); + if (!targetConfig) { + return; + } + const configTarget = targetConfig.configTarget; + const wkspace = targetConfig.folderUri; + + await this.pythonPathUpdaterService.updatePythonPath(undefined, configTarget, 'ui', wkspace); + } protected async suggestionToQuickPickItem( suggestion: PythonInterpreter, workspaceUri?: Uri @@ -67,19 +83,12 @@ export class InterpreterSelector implements IInterpreterSelector { } protected async setInterpreter() { - const setInterpreterGlobally = - !Array.isArray(this.workspaceService.workspaceFolders) || - this.workspaceService.workspaceFolders.length === 0; - let configTarget = ConfigurationTarget.Global; - let wkspace: Uri | undefined; - if (!setInterpreterGlobally) { - const targetConfig = await this.getWorkspaceToSetPythonPath(); - if (!targetConfig) { - return; - } - configTarget = targetConfig.configTarget; - wkspace = targetConfig.folderUri; + const targetConfig = await this.getConfigTarget(); + if (!targetConfig) { + return; } + const configTarget = targetConfig.configTarget; + const wkspace = targetConfig.folderUri; const suggestions = await this.getSuggestions(wkspace); const currentPythonPath = this.pathUtils.getDisplayName( @@ -138,12 +147,21 @@ export class InterpreterSelector implements IInterpreterSelector { workspaceFolder.uri ); } - private async getWorkspaceToSetPythonPath(): Promise { + private async getConfigTarget(): Promise< + | { + folderUri: Resource; + configTarget: ConfigurationTarget; + } + | undefined + > { if ( !Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0 ) { - return undefined; + return { + folderUri: undefined, + configTarget: ConfigurationTarget.Global + }; } if (this.workspaceService.workspaceFolders.length === 1) { return { @@ -153,11 +171,30 @@ export class InterpreterSelector implements IInterpreterSelector { } // Ok we have multiple workspaces, get the user to pick a folder. - const workspaceFolder = await this.applicationShell.showWorkspaceFolderPick({ + + type WorkspaceSelectionQuickPickItem = QuickPickItem & { uri: Uri }; + const quickPickItems: WorkspaceSelectionQuickPickItem[] = [ + ...this.workspaceService.workspaceFolders.map(w => { + ({ + label: w.name, + description: path.dirname(w.uri.fsPath), + uri: w.uri + }) + }), + { + label: Interpreters.entireWorkspace(), + uri: this.workspaceService.workspaceFolders[0].uri + } + ]; + + const selection = await this.applicationShell.showQuickPick(quickPickItems, { placeHolder: 'Select the workspace to set the interpreter' }); - return workspaceFolder - ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } + + return selection + ? selection.label === Interpreters.entireWorkspace() + ? { folderUri: selection.uri, configTarget: ConfigurationTarget.Workspace } + : { folderUri: selection.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; } } diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index d750fb3782fe..fadfd04334f0 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -24,7 +24,7 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage this.executionFactory = serviceContainer.get(IPythonExecutionFactory); } public async updatePythonPath( - pythonPath: string, + pythonPath: string | undefined, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri @@ -33,7 +33,7 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); let failed = false; try { - await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); + await pythonPathUpdater.updatePythonPath(pythonPath ? path.normalize(pythonPath) : undefined); } catch (reason) { failed = true; // tslint:disable-next-line:no-unsafe-any prefer-type-cast @@ -50,13 +50,13 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage duration: number, failed: boolean, trigger: 'ui' | 'shebang' | 'load', - pythonPath: string + pythonPath: string | undefined ) { const telemtryProperties: PythonInterpreterTelemetry = { failed, trigger }; - if (!failed) { + if (!failed && pythonPath) { const processService = await this.executionFactory.create({ pythonPath }); const infoPromise = processService .getInterpreterInformation() diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index fecaf38d65e0..5ed874423b56 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -3,7 +3,7 @@ import { IPythonPathUpdaterService } from '../types'; export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { constructor(private readonly workspaceService: IWorkspaceService) {} - public async updatePythonPath(pythonPath: string): Promise { + public async updatePythonPath(pythonPath: string | undefined): Promise { const pythonConfig = this.workspaceService.getConfiguration('python'); const pythonPathValue = pythonConfig.inspect('pythonPath'); diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts index 6c0489fdbadc..e1bf153f5509 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -5,14 +5,14 @@ import { IPythonPathUpdaterService } from '../types'; export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { constructor(private workspaceFolder: Uri, private readonly workspaceService: IWorkspaceService) {} - public async updatePythonPath(pythonPath: string): Promise { + public async updatePythonPath(pythonPath: string | undefined): Promise { const pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceFolder); const pythonPathValue = pythonConfig.inspect('pythonPath'); if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { return; } - if (pythonPath.startsWith(this.workspaceFolder.fsPath)) { + if (pythonPath && pythonPath.startsWith(this.workspaceFolder.fsPath)) { pythonPath = path.relative(this.workspaceFolder.fsPath, pythonPath); } await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index d4a8add2cac0..4fcfecb4ff0a 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -3,7 +3,7 @@ import { Resource } from '../../common/types'; import { PythonInterpreter } from '../contracts'; export interface IPythonPathUpdaterService { - updatePythonPath(pythonPath: string): Promise; + updatePythonPath(pythonPath: string | undefined): Promise; } export const IPythonPathUpdaterServiceFactory = Symbol('IPythonPathUpdaterServiceFactory'); @@ -16,7 +16,7 @@ export interface IPythonPathUpdaterServiceFactory { export const IPythonPathUpdaterServiceManager = Symbol('IPythonPathUpdaterServiceManager'); export interface IPythonPathUpdaterServiceManager { updatePythonPath( - pythonPath: string, + pythonPath: string | undefined, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri diff --git a/src/test/configuration/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector.unit.test.ts index ec9a7ca4e3ea..665a07e48ee3 100644 --- a/src/test/configuration/interpreterSelector.unit.test.ts +++ b/src/test/configuration/interpreterSelector.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; @@ -14,6 +15,7 @@ import { import { PathUtils } from '../../client/common/platform/pathUtils'; import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { Interpreters } from '../../client/common/utils/localize'; import { Architecture } from '../../client/common/utils/platform'; import { InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; import { @@ -66,6 +68,8 @@ suite('Interpreters - selector', () => { let shebangProvider: TypeMoq.IMock; let configurationService: TypeMoq.IMock; let pythonSettings: TypeMoq.IMock; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; class TestInterpreterSelector extends InterpreterSelector { // tslint:disable-next-line:no-unnecessary-override @@ -83,7 +87,14 @@ suite('Interpreters - selector', () => { public async setShebangInterpreter() { return super.setShebangInterpreter(); } + // tslint:disable-next-line:no-unnecessary-override + public async resetInterpreter() { + return super.resetInterpreter(); + } } + + let selector: TestInterpreterSelector; + setup(() => { commandManager = TypeMoq.Mock.ofType(); comparer = TypeMoq.Mock.ofType(); @@ -98,16 +109,28 @@ suite('Interpreters - selector', () => { workspace = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) .returns((a: string, b: string) => a === b); - configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + configurationService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - comparer.setup((c) => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); + comparer.setup(c => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); + selector = new TestInterpreterSelector( + interpreterService.object, + workspace.object, + appShell.object, + documentManager.object, + new PathUtils(false), + comparer.object, + pythonPathUpdater.object, + shebangProvider.object, + configurationService.object, + commandManager.object + ); }); - [true, false].forEach((isWindows) => { + [true, false].forEach(isWindows => { test(`Suggestions (${isWindows ? 'Windows' : 'Non-Windows'})`, async () => { - const selector = new InterpreterSelector( + const interpreterSelector = new InterpreterSelector( interpreterService.object, workspace.object, appShell.object, @@ -127,14 +150,14 @@ suite('Interpreters - selector', () => { { displayName: '2 (virtualenv)', path: 'c:/path2/path2', type: InterpreterType.VirtualEnv }, { displayName: '3', path: 'c:/path2/path2', type: InterpreterType.Unknown }, { displayName: '4', path: 'c:/path4/path4', type: InterpreterType.Conda } - ].map((item) => { + ].map(item => { return { ...info, ...item }; }); interpreterService - .setup((x) => x.getInterpreters(TypeMoq.It.isAny())) - .returns(() => new Promise((resolve) => resolve(initial))); + .setup(x => x.getInterpreters(TypeMoq.It.isAny())) + .returns(() => new Promise(resolve => resolve(initial))); - const actual = await selector.getSuggestions(undefined); + const actual = await interpreterSelector.getSuggestions(undefined); const expected: InterpreterQuickPickItem[] = [ new InterpreterQuickPickItem('1', 'c:/path1/path1'), @@ -161,204 +184,431 @@ suite('Interpreters - selector', () => { }); }); - test('Update Global settings when there are no workspaces', async () => { - const selector = new TestInterpreterSelector( - interpreterService.object, - workspace.object, - appShell.object, - documentManager.object, - new PathUtils(false), - comparer.object, - pythonPathUpdater.object, - shebangProvider.object, - configurationService.object, - commandManager.object - ); - pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); - const selectedItem: IInterpreterQuickPickItem = { - description: '', - detail: '', - label: '', - path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any - }; - - workspace.setup((w) => w.workspaceFolders).returns(() => undefined); - - selector.getSuggestions = () => Promise.resolve([]); - appShell - .setup((s) => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater - .setup((p) => - p.updatePythonPath( - TypeMoq.It.isValue(selectedItem.path), - TypeMoq.It.isValue(ConfigurationTarget.Global), - TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(undefined) + // tslint:disable-next-line: max-func-body-length + suite('Test method setInterpreter()', async () => { + test('Update Global settings when there are no workspaces', async () => { + pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup(w => w.workspaceFolders).returns(() => undefined); + + selector.getSuggestions = () => Promise.resolve([]); + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(selectedItem)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(undefined) + ) ) - ) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); - await selector.setInterpreter(); + await selector.setInterpreter(); - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); - }); - test('Update workspace folder settings when there is one workspace folder', async () => { - const selector = new TestInterpreterSelector( - interpreterService.object, - workspace.object, - appShell.object, - documentManager.object, - new PathUtils(false), - comparer.object, - pythonPathUpdater.object, - shebangProvider.object, - configurationService.object, - commandManager.object - ); - pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); - const selectedItem: IInterpreterQuickPickItem = { - description: '', - detail: '', - label: '', - path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any - }; - - const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; - workspace.setup((w) => w.workspaceFolders).returns(() => [folder]); - - selector.getSuggestions = () => Promise.resolve([]); - appShell - .setup((s) => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater - .setup((p) => - p.updatePythonPath( - TypeMoq.It.isValue(selectedItem.path), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), - TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder.uri) + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update workspace folder settings when there is one workspace folder', async () => { + pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; + workspace.setup(w => w.workspaceFolders).returns(() => [folder]); + + selector.getSuggestions = () => Promise.resolve([]); + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(selectedItem)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder.uri) + ) ) - ) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); - await selector.setInterpreter(); + await selector.setInterpreter(); - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); - }); - test('Update seleted workspace folder settings when there is more than one workspace folder', async () => { - const selector = new TestInterpreterSelector( - interpreterService.object, - workspace.object, - appShell.object, - documentManager.object, - new PathUtils(false), - comparer.object, - pythonPathUpdater.object, - shebangProvider.object, - configurationService.object, - commandManager.object - ); - pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); - const selectedItem: IInterpreterQuickPickItem = { - description: '', - detail: '', - label: '', - path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any - }; - - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); - - selector.getSuggestions = () => Promise.resolve([]); - appShell - .setup((s) => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.once()); - appShell - .setup((s) => s.showWorkspaceFolderPick(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(folder2)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater - .setup((p) => - p.updatePythonPath( - TypeMoq.It.isValue(selectedItem.path), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), - TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder2.uri) + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update selected workspace folder settings when there is more than one workspace folder', async () => { + pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + selector.getSuggestions = () => Promise.resolve([]); + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue([]), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(selectedItem)) + .verifiable(TypeMoq.Times.once()); + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder2.uri) + ) ) - ) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); - await selector.setInterpreter(); + await selector.setInterpreter(); - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { + pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + selector.getSuggestions = () => Promise.resolve([selectedItem]); + appShell + .setup(s => + s.showQuickPick(TypeMoq.It.isValue([selectedItem]), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(selectedItem)) + .verifiable(TypeMoq.Times.once()); + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: Interpreters.entireWorkspace(), + uri: folder1.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.Workspace), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder1.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await selector.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + + selector.getSuggestions = () => Promise.resolve([]); + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue([]), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(selectedItem)) + .verifiable(TypeMoq.Times.never()); + + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await selector.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); }); - test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { - const selector = new TestInterpreterSelector( - interpreterService.object, - workspace.object, - appShell.object, - documentManager.object, - new PathUtils(false), - comparer.object, - pythonPathUpdater.object, - shebangProvider.object, - configurationService.object, - commandManager.object - ); - const selectedItem: IInterpreterQuickPickItem = { - description: '', - detail: '', - label: '', - path: 'This is the selected Python path', - // tslint:disable-next-line: no-any - interpreter: {} as any - }; - - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); - - selector.getSuggestions = () => Promise.resolve([]); - appShell - .setup((s) => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.never()); - appShell - .setup((s) => s.showWorkspaceFolderPick(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater - .setup((p) => - p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - - await selector.setInterpreter(); - - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); + // tslint:disable-next-line: max-func-body-length + suite('Test method resetInterpreter()', async () => { + test('Update Global settings when there are no workspaces', async () => { + workspace.setup(w => w.workspaceFolders).returns(() => undefined); + + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await selector.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update workspace folder settings when there is one workspace folder', async () => { + const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; + workspace.setup(w => w.workspaceFolders).returns(() => [folder]); + + selector.getSuggestions = () => Promise.resolve([]); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await selector.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update selected workspace folder settings when there is more than one workspace folder', async () => { + workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder2.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await selector.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { + workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: Interpreters.entireWorkspace(), + uri: folder1.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.Workspace), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder1.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await selector.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { + workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + + appShell + .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup(p => + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await selector.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); }); }); From 08e9e45f1b9aa1234dac59587947034f8be005ae Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 2 Apr 2020 02:45:44 +0530 Subject: [PATCH 2/7] Implemented new interpreter storage supporting multiroot workspaces (#10325) * Added python.defaultInterpreterPath setting * Created persistent storage infrastructure * Added vscode.workspace.workspaceFile API * Implemented inspect interpreter path * Implement config service updater for pythonPath * Correct build errors * Change extension to use new infrastructure * Store service container in a global variable * Update settings in the new way src/test/common * Correct cyclic dependency between IExperimentsManager and IConfigurationService * Detect change in new interpreter storage and act accordingly * Added reset python interpreters command * Cache the auto selected interpreter * Log python interpreter in the output channel * Fix autoselection and speed up the interpreter change process * Reset experiments * Reset python interpreters for workspace * Remove reset interpreter command from this PR * Added news entry * Update getWorkspaceFolderIdentifier * Code reviews * Dispose handler * More code reviews * Resolve merge conflicts * Fix issues * Some more fixes * More fixes * Do not assume ACTIVATED_SERVICE_CONTAINER is defined * Fix running tests * Fix cacheUtils.ts and test/common.ts * Correct cacheResourceSpecificInterpreterData decorator * Added tests for decorator-like implementation in enviroment provider * Code reviews * Fix interpreter service unit tests * Fix interpreter display unit tests * Fix mac interpreter unit tests * Fix interpreter selector unit tests * Fix configSettings pythonPath unit tests * Fix configSettings unit tests * Rebase with master * Fix installer.test.ts tests * Fix moduleInstaller.test.ts tests * Fix pythonPathUpdater.test.ts tests * Fix pythonProc.simple.multiroot.test.ts tests * Fix data science functional tests * Fix smoke tests * Use user friendly path in the output channel * Use symbols directly * Fix bug with configSettings.ts * Fix venv tests * Run all tests with the new interpreter storage" * Restore YAML pipeline * Fix absolute path resolver * Run all tests with old interpreter storage * Reduce run time of venv tests * Run only mac multiroot * Remove experiment from experiments.json * Added tests for Interpreter path service * saa * Ensure we use `CI_PYTHON_PATH` when running tests with the new interpreter storage * Reset global interpreter path as well * Added tests for mac interpreter diagnostic * Added tests for src\client\interpreter\autoSelection\rules\settings.ts * Added tests for src\client\interpreter\configuration\pythonPathUpdaterServiceFactory.ts * Added tests for src\client\interpreter\autoSelection\rules\workspaceEnv.ts * Added tests for src\client\interpreter\interpreterService.ts * Added tests for src\client\common\configuration\service.ts * Added tests for src\client\startupTelemetry.ts * Correct tests * Module installer tests * Code reviews * More code reviews * Fix lint error --- news/1 Enhancements/10325.md | 1 + package.json | 6 + package.nls.json | 1 + src/client/activation/serviceRegistry.ts | 4 +- .../checks/macPythonInterpreter.ts | 57 +- src/client/common/application/types.ts | 31 + src/client/common/application/workspace.ts | 11 +- src/client/common/configSettings.ts | 1292 +++++++++-------- src/client/common/configuration/service.ts | 49 +- src/client/common/constants.ts | 2 + src/client/common/experimentGroups.ts | 11 + src/client/common/experiments.ts | 39 +- src/client/common/interpreterPathService.ts | 127 ++ src/client/common/serviceRegistry.ts | 7 +- src/client/common/types.ts | 19 + src/client/common/utils/cacheUtils.ts | 40 +- src/client/common/utils/decorators.ts | 60 +- src/client/common/utils/localize.ts | 1 + .../variables/environmentVariablesProvider.ts | 43 +- .../autoSelection/rules/settings.ts | 12 +- .../autoSelection/rules/workspaceEnv.ts | 12 +- .../configuration/interpreterSelector.ts | 14 +- .../pythonPathUpdaterServiceFactory.ts | 28 +- .../services/globalUpdaterService.ts | 18 +- .../services/workspaceFolderUpdaterService.ts | 22 +- .../services/workspaceUpdaterService.ts | 24 +- src/client/interpreter/display/index.ts | 23 +- src/client/interpreter/interpreterService.ts | 89 +- src/client/startupTelemetry.ts | 21 +- .../activation/serviceRegistry.unit.test.ts | 5 - .../checks/macPythonInterpreter.unit.test.ts | 280 +++- src/test/common.ts | 23 +- .../configSettings.pythonPath.unit.test.ts | 82 +- .../configSettings.unit.test.ts | 73 +- .../common/configuration/service.unit.test.ts | 174 +++ src/test/common/experiments.unit.test.ts | 116 +- src/test/common/installer.test.ts | 181 ++- .../interpreterPathService.unit.test.ts | 410 ++++++ src/test/common/moduleInstaller.test.ts | 266 +++- .../pythonProc.simple.multiroot.test.ts | 117 +- src/test/common/serviceRegistry.unit.test.ts | 378 ++--- .../terminalActivation.testvirtualenvs.ts | 19 +- src/test/common/utils/cacheUtils.unit.test.ts | 40 +- src/test/common/utils/decorators.unit.test.ts | 96 +- .../envVarsProvider.multiroot.test.ts | 3 +- .../environmentVariablesProvider.unit.test.ts | 63 +- .../interpreterSelector.unit.test.ts | 6 +- .../datascience/dataScienceIocContainer.ts | 49 +- .../autoSelection/rules/settings.unit.test.ts | 59 +- .../rules/workspaceEnv.unit.test.ts | 50 +- src/test/interpreters/display.unit.test.ts | 122 +- .../interpreterService.unit.test.ts | 116 +- .../interpreters/pythonPathUpdater.test.ts | 190 --- .../pythonPathUpdaterFactory.unit.test.ts | 347 +++++ src/test/startupTelemetry.unit.test.ts | 78 + 55 files changed, 3722 insertions(+), 1685 deletions(-) create mode 100644 news/1 Enhancements/10325.md create mode 100644 src/client/common/interpreterPathService.ts create mode 100644 src/test/common/configuration/service.unit.test.ts create mode 100644 src/test/common/interpreterPathService.unit.test.ts delete mode 100644 src/test/interpreters/pythonPathUpdater.test.ts create mode 100644 src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts create mode 100644 src/test/startupTelemetry.unit.test.ts diff --git a/news/1 Enhancements/10325.md b/news/1 Enhancements/10325.md new file mode 100644 index 000000000000..f532b69c0c2a --- /dev/null +++ b/news/1 Enhancements/10325.md @@ -0,0 +1 @@ +Use new interpreter storage supporting multiroot workspaces when in PythonPath experiment. diff --git a/package.json b/package.json index 03bb8c0f321f..97afe3a23211 100644 --- a/package.json +++ b/package.json @@ -1565,6 +1565,12 @@ "description": "Enables/disables A/B tests.", "scope": "machine" }, + "python.defaultInterpreterPath": { + "type": "string", + "default": "python", + "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", + "scope": "machine" + }, "python.experiments.optInto": { "type": "array", "default": [], diff --git a/package.nls.json b/package.nls.json index 059bf32fe9e6..7561efe83563 100644 --- a/package.nls.json +++ b/package.nls.json @@ -133,6 +133,7 @@ "Experiments.inGroup": "User belongs to experiment group '{0}'", "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters", "Interpreters.entireWorkspace": "Entire workspace", + "Interpreters.pythonInterpreterPath": "Python interpreter path: {0}", "Interpreters.LoadingInterpreters": "Loading Python Interpreters", "Interpreters.condaInheritEnvMessage": "We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change \"terminal.integrated.inheritEnv\" to false in your user settings.", "Logging.CurrentWorkingDirectory": "cwd:", diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 21897a0617e4..f50d4438b17e 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ActiveResourceService } from '../common/application/activeResource'; -import { IActiveResourceService } from '../common/application/types'; + import { registerTypes as registerDotNetTypes } from '../common/dotnet/serviceRegistry'; import { INugetRepository } from '../common/nuget/types'; import { @@ -211,6 +210,5 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp IExtensionSingleActivationService, ExtensionSurveyPrompt ); - serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); serviceManager.addSingleton(IExtensionSingleActivationService, AATesting); } diff --git a/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/src/client/application/diagnostics/checks/macPythonInterpreter.ts index ec1e21010935..f409c473a454 100644 --- a/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -6,9 +6,17 @@ import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, DiagnosticSeverity, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import { DeprecatePythonPath } from '../../../common/experimentGroups'; import '../../../common/extensions'; import { IPlatformService } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, + InterpreterConfigurationScope, + Resource +} from '../../../common/types'; import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; @@ -94,7 +102,7 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { } const interpreters = await this.interpreterService.getInterpreters(resource); - if (interpreters.filter((i) => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { + if (interpreters.filter(i => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { return [ new InvalidMacPythonInterpreterDiagnostic( DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, @@ -119,7 +127,7 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { DiagnosticCommandPromptHandlerServiceId ); await Promise.all( - diagnostics.map(async (diagnostic) => { + diagnostics.map(async diagnostic => { const canHandle = await this.canHandle(diagnostic); const shouldIgnore = await this.filterService.shouldIgnoreDiagnostic(diagnostic.code); if (!canHandle || shouldIgnore) { @@ -133,18 +141,37 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { protected addPythonPathChangedHandler() { const workspaceService = this.serviceContainer.get(IWorkspaceService); const disposables = this.serviceContainer.get(IDisposableRegistry); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const experiments = this.serviceContainer.get(IExperimentsManager); + if (experiments.inExperiment(DeprecatePythonPath.experiment)) { + disposables.push(interpreterPathService.onDidChange(i => this.onDidChangeConfiguration(undefined, i))); + } + experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); } - protected async onDidChangeConfiguration(event: ConfigurationChangeEvent) { - const workspaceService = this.serviceContainer.get(IWorkspaceService); - const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders - ? workspaceService.workspaceFolders!.map((workspace) => workspace.uri) - : [undefined]; - const workspaceUriIndex = workspacesUris.findIndex((uri) => - event.affectsConfiguration('python.pythonPath', uri) - ); - if (workspaceUriIndex === -1) { - return; + protected async onDidChangeConfiguration( + event?: ConfigurationChangeEvent, + interpreterConfigurationScope?: InterpreterConfigurationScope + ) { + let workspaceUri: Resource; + if (event) { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders + ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) + : [undefined]; + const workspaceUriIndex = workspacesUris.findIndex(uri => + event.affectsConfiguration('python.pythonPath', uri) + ); + if (workspaceUriIndex === -1) { + return; + } + workspaceUri = workspacesUris[workspaceUriIndex]; + } else if (interpreterConfigurationScope) { + workspaceUri = interpreterConfigurationScope.uri; + } else { + throw new Error( + 'One of `interpreterConfigurationScope` or `event` should be defined when calling `onDidChangeConfiguration`.' + ); } // Lets wait, for more changes, dirty simple throttling. if (this.timeOut) { @@ -154,8 +181,8 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { } this.timeOut = setTimeout(() => { this.timeOut = undefined; - this.diagnose(workspacesUris[workspaceUriIndex]) - .then((diagnostics) => this.handle(diagnostics)) + this.diagnose(workspaceUri) + .then(diagnostics => this.handle(diagnostics)) .ignoreErrors(); }, this.changeThrottleTimeout); } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 6b23bd02dd6c..9e1eba45a6db 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -667,6 +667,37 @@ export interface IWorkspaceService { */ readonly workspaceFolders: readonly WorkspaceFolder[] | undefined; + /** + * The location of the workspace file, for example: + * + * `file:///Users/name/Development/myProject.code-workspace` + * + * or + * + * `untitled:1555503116870` + * + * for a workspace that is untitled and not yet saved. + * + * Depending on the workspace that is opened, the value will be: + * * `undefined` when no workspace or a single folder is opened + * * the path of the workspace file as `Uri` otherwise. if the workspace + * is untitled, the returned URI will use the `untitled:` scheme + * + * The location can e.g. be used with the `vscode.openFolder` command to + * open the workspace again after it has been closed. + * + * **Example:** + * ```typescript + * vscode.commands.executeCommand('vscode.openFolder', uriOfWorkspace); + * ``` + * + * **Note:** it is not advised to use `workspace.workspaceFile` to write + * configuration data into the file. You can use `workspace.getConfiguration().update()` + * for that purpose which will work both when a single folder is opened as + * well as an untitled or saved workspace. + */ + readonly workspaceFile: Resource; + /** * An event that is emitted when a workspace folder is added or removed. */ diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 70110ee71643..740d9c8711f2 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; +import * as path from 'path'; import { CancellationToken, ConfigurationChangeEvent, @@ -15,6 +16,7 @@ import { WorkspaceFoldersChangeEvent } from 'vscode'; import { Resource } from '../types'; +import { getOSType, OSType } from '../utils/platform'; import { IWorkspaceService } from './types'; @injectable() @@ -34,6 +36,9 @@ export class WorkspaceService implements IWorkspaceService { public get hasWorkspaceFolders() { return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0; } + public get workspaceFile() { + return workspace.workspaceFile; + } public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { return workspace.getConfiguration(section, resource || null); } @@ -66,6 +71,10 @@ export class WorkspaceService implements IWorkspaceService { } public getWorkspaceFolderIdentifier(resource: Resource, defaultValue: string = ''): string { const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : defaultValue; + return workspaceFolder + ? path.normalize( + getOSType() === OSType.Windows ? workspaceFolder.uri.fsPath.toUpperCase() : workspaceFolder.uri.fsPath + ) + : defaultValue; } } diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index fde4ea230c30..793b46198043 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,620 +1,672 @@ -'use strict'; - -import * as child_process from 'child_process'; -import * as path from 'path'; -import { - ConfigurationChangeEvent, - ConfigurationTarget, - DiagnosticSeverity, - Disposable, - Event, - EventEmitter, - Uri, - WorkspaceConfiguration -} from 'vscode'; -import { LanguageServerType } from '../activation/types'; -import '../common/extensions'; -import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; -import { IWorkspaceService } from './application/types'; -import { WorkspaceService } from './application/workspace'; -import { isTestExecution } from './constants'; -import { ExtensionChannels } from './insidersBuild/types'; -import { IS_WINDOWS } from './platform/constants'; -import { - IAnalysisSettings, - IAutoCompleteSettings, - IDataScienceSettings, - IExperiments, - IFormattingSettings, - ILintingSettings, - IPythonSettings, - ISortImportSettings, - ITerminalSettings, - ITestingSettings, - IWorkspaceSymbolSettings, - Resource -} from './types'; -import { debounceSync } from './utils/decorators'; -import { SystemVariables } from './variables/systemVariables'; - -// tslint:disable:no-require-imports no-var-requires -const untildify = require('untildify'); - -// tslint:disable-next-line:completed-docs -export class PythonSettings implements IPythonSettings { - private static pythonSettings: Map = new Map(); - public downloadLanguageServer = true; - public jediEnabled = true; - public jediPath = ''; - public jediMemoryLimit = 1024; - public envFile = ''; - public venvPath = ''; - public venvFolders: string[] = []; - public condaPath = ''; - public pipenvPath = ''; - public poetryPath = ''; - public devOptions: string[] = []; - public linting!: ILintingSettings; - public formatting!: IFormattingSettings; - public autoComplete!: IAutoCompleteSettings; - public testing!: ITestingSettings; - public terminal!: ITerminalSettings; - public sortImports!: ISortImportSettings; - public workspaceSymbols!: IWorkspaceSymbolSettings; - public disableInstallationChecks = false; - public globalModuleInstallation = false; - public analysis!: IAnalysisSettings; - public autoUpdateLanguageServer: boolean = true; - public datascience!: IDataScienceSettings; - public insidersChannel!: ExtensionChannels; - public experiments!: IExperiments; - public languageServer: LanguageServerType = LanguageServerType.Microsoft; - - protected readonly changed = new EventEmitter(); - private workspaceRoot: Uri; - private disposables: Disposable[] = []; - // tslint:disable-next-line:variable-name - private _pythonPath = ''; - private readonly workspace: IWorkspaceService; - public get onDidChange(): Event { - return this.changed.event; - } - - constructor( - workspaceFolder: Resource, - private readonly interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, - workspace?: IWorkspaceService - ) { - 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 | 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, interpreterAutoSelectionService, workspace); - PythonSettings.pythonSettings.set(workspaceFolderKey, settings); - // 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(EventName.COMPLETION_ADD_BRACKETS, undefined, { - enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false - }); - sendTelemetryEvent(EventName.FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); - } - // tslint:disable-next-line:no-non-null-assertion - return PythonSettings.pythonSettings.get(workspaceFolderKey)!; - } - - // tslint:disable-next-line:type-literal-delimiter - 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; - - if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - workspaceFolderUri = workspace.workspaceFolders[0].uri; - } - - const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; - return { uri: workspaceFolderUri, target }; - } - - // tslint:disable-next-line:function-name - public static dispose() { - if (!isTestExecution()) { - throw new Error('Dispose can only be called from unit tests'); - } - // tslint:disable-next-line:no-void-expression - PythonSettings.pythonSettings.forEach((item) => item && item.dispose()); - PythonSettings.pythonSettings.clear(); - } - public dispose() { - // tslint:disable-next-line:no-unsafe-any - this.disposables.forEach((disposable) => disposable && disposable.dispose()); - this.disposables = []; - } - // tslint:disable-next-line:cyclomatic-complexity max-func-body-length - protected update(pythonSettings: WorkspaceConfiguration) { - const workspaceRoot = this.workspaceRoot.fsPath; - const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace); - - // 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'))!; - this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; - const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; - this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; - const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; - this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; - const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; - this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; - - this.downloadLanguageServer = systemVariables.resolveAny( - pythonSettings.get('downloadLanguageServer', true) - )!; - this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled', true))!; - this.autoUpdateLanguageServer = systemVariables.resolveAny( - pythonSettings.get('autoUpdateLanguageServer', true) - )!; - if (this.jediEnabled) { - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; - if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); - } else { - this.jediPath = ''; - } - this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; - } - - let ls = pythonSettings.get('languageServer'); - if (!ls) { - ls = LanguageServerType.Jedi; - } - this.languageServer = systemVariables.resolveAny(ls)!; - - const envFileSetting = pythonSettings.get('envFile'); - this.envFile = systemVariables.resolveAny(envFileSetting)!; - sendSettingTelemetry(this.workspace, envFileSetting); - - // tslint:disable-next-line:no-any - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any - this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; - this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; - if (this.linting) { - Object.assign(this.linting, lintingSettings); - } else { - this.linting = lintingSettings; - } - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const analysisSettings = systemVariables.resolveAny(pythonSettings.get('analysis'))!; - if (this.analysis) { - Object.assign(this.analysis, analysisSettings); - } else { - this.analysis = analysisSettings; - } - - this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; - this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; - if (this.sortImports) { - Object.assign(this.sortImports, sortImportSettings); - } else { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; - // Support for travis. - this.linting = this.linting - ? this.linting - : { - enabled: false, - ignorePatterns: [], - flake8Args: [], - flake8Enabled: false, - flake8Path: 'flake', - lintOnSave: false, - maxNumberOfProblems: 100, - mypyArgs: [], - mypyEnabled: false, - mypyPath: 'mypy', - banditArgs: [], - banditEnabled: false, - banditPath: 'bandit', - pycodestyleArgs: [], - pycodestyleEnabled: false, - pycodestylePath: 'pycodestyle', - pylamaArgs: [], - pylamaEnabled: false, - pylamaPath: 'pylama', - prospectorArgs: [], - prospectorEnabled: false, - prospectorPath: 'prospector', - pydocstyleArgs: [], - pydocstyleEnabled: false, - pydocstylePath: 'pydocstyle', - pylintArgs: [], - pylintEnabled: false, - pylintPath: 'pylint', - pylintCategorySeverity: { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning - }, - pycodestyleCategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning - }, - flake8CategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code - // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as - // unused imports, variables, etc. - F: DiagnosticSeverity.Warning - }, - mypyCategorySeverity: { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint - }, - pylintUseMinimalCheckers: false - }; - this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); - this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); - this.linting.pycodestylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pycodestylePath), - workspaceRoot - ); - this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); - this.linting.prospectorPath = getAbsolutePath( - systemVariables.resolveAny(this.linting.prospectorPath), - workspaceRoot - ); - this.linting.pydocstylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pydocstylePath), - workspaceRoot - ); - this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; - if (this.formatting) { - Object.assign(this.formatting, formattingSettings); - } else { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting - ? this.formatting - : { - autopep8Args: [], - autopep8Path: 'autopep8', - provider: 'autopep8', - blackArgs: [], - blackPath: 'black', - yapfArgs: [], - yapfPath: 'yapf' - }; - this.formatting.autopep8Path = getAbsolutePath( - systemVariables.resolveAny(this.formatting.autopep8Path), - workspaceRoot - ); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - this.formatting.blackPath = getAbsolutePath( - systemVariables.resolveAny(this.formatting.blackPath), - workspaceRoot - ); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const autoCompleteSettings = systemVariables.resolveAny( - pythonSettings.get('autoComplete') - )!; - if (this.autoComplete) { - Object.assign(this.autoComplete, autoCompleteSettings); - } else { - this.autoComplete = autoCompleteSettings; - } - // Support for travis. - this.autoComplete = this.autoComplete - ? this.autoComplete - : { - extraPaths: [], - addBrackets: false, - showAdvancedMembers: false, - typeshedPaths: [] - }; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const workspaceSymbolsSettings = systemVariables.resolveAny( - pythonSettings.get('workspaceSymbols') - )!; - if (this.workspaceSymbols) { - Object.assign( - this.workspaceSymbols, - workspaceSymbolsSettings - ); - } else { - this.workspaceSymbols = workspaceSymbolsSettings; - } - // Support for travis. - this.workspaceSymbols = this.workspaceSymbols - ? this.workspaceSymbols - : { - ctagsPath: 'ctags', - enabled: true, - exclusionPatterns: [], - rebuildOnFileSave: true, - rebuildOnStart: true, - tagFilePath: path.join(workspaceRoot, 'tags') - }; - this.workspaceSymbols.tagFilePath = getAbsolutePath( - systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), - workspaceRoot - ); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; - if (this.testing) { - Object.assign(this.testing, testSettings); - } else { - this.testing = testSettings; - if (isTestExecution() && !this.testing) { - // tslint:disable-next-line:prefer-type-cast - // tslint:disable-next-line:no-object-literal-type-assertion - this.testing = { - nosetestArgs: [], - pytestArgs: [], - unittestArgs: [], - promptToConfigure: true, - debugPort: 3000, - nosetestsEnabled: false, - pytestEnabled: false, - unittestEnabled: false, - nosetestPath: 'nosetests', - pytestPath: 'pytest', - autoTestDiscoverOnSaveEnabled: true - } as ITestingSettings; - } - } - - // Support for travis. - this.testing = this.testing - ? this.testing - : { - promptToConfigure: true, - debugPort: 3000, - nosetestArgs: [], - nosetestPath: 'nosetest', - nosetestsEnabled: false, - pytestArgs: [], - pytestEnabled: false, - pytestPath: 'pytest', - unittestArgs: [], - unittestEnabled: false, - autoTestDiscoverOnSaveEnabled: true - }; - this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot); - this.testing.nosetestPath = getAbsolutePath( - systemVariables.resolveAny(this.testing.nosetestPath), - workspaceRoot - ); - if (this.testing.cwd) { - this.testing.cwd = getAbsolutePath(systemVariables.resolveAny(this.testing.cwd), workspaceRoot); - } - - // Resolve any variables found in the test arguments. - this.testing.nosetestArgs = this.testing.nosetestArgs.map((arg) => systemVariables.resolveAny(arg)); - this.testing.pytestArgs = this.testing.pytestArgs.map((arg) => systemVariables.resolveAny(arg)); - this.testing.unittestArgs = this.testing.unittestArgs.map((arg) => systemVariables.resolveAny(arg)); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; - if (this.terminal) { - Object.assign(this.terminal, terminalSettings); - } else { - this.terminal = terminalSettings; - if (isTestExecution() && !this.terminal) { - // tslint:disable-next-line:prefer-type-cast - // tslint:disable-next-line:no-object-literal-type-assertion - this.terminal = {} as ITerminalSettings; - } - } - // Support for travis. - this.terminal = this.terminal - ? this.terminal - : { - executeInFileDir: true, - launchArgs: [], - activateEnvironment: true, - activateEnvInCurrentTerminal: false - }; - - const experiments = systemVariables.resolveAny(pythonSettings.get('experiments'))!; - if (this.experiments) { - Object.assign(this.experiments, experiments); - } else { - this.experiments = experiments; - } - this.experiments = this.experiments - ? this.experiments - : { - enabled: true, - optInto: [], - optOutFrom: [] - }; - - const dataScienceSettings = systemVariables.resolveAny( - pythonSettings.get('dataScience') - )!; - if (this.datascience) { - Object.assign(this.datascience, dataScienceSettings); - } else { - this.datascience = dataScienceSettings; - } - - this.insidersChannel = pythonSettings.get('insidersChannel')!; - } - - public get pythonPath(): string { - return this._pythonPath; - } - public set pythonPath(value: string) { - if (this._pythonPath === value) { - return; - } - // Add support for specifying just the directory where the python executable will be located. - // E.g. virtual directory name. - try { - this._pythonPath = this.getPythonExecutable(value); - } catch (ex) { - this._pythonPath = value; - } - } - protected getPythonExecutable(pythonPath: string) { - return getPythonExecutable(pythonPath); - } - protected onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, delete its key - const workspaceKeys = this.workspace.workspaceFolders!.map((workspaceFolder) => workspaceFolder.uri.fsPath); - const activatedWkspcKeys = Array.from(PythonSettings.pythonSettings.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); - if (activatedWkspcFoldersRemoved.length > 0) { - for (const folder of activatedWkspcFoldersRemoved) { - PythonSettings.pythonSettings.delete(folder); - } - } - } - protected initialize(): void { - 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. - this.debounceChangeNotification(); - }; - this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); - this.disposables.push( - this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)) - ); - this.disposables.push( - this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { - if (event.affectsConfiguration('python')) { - onDidChange(); - } - }) - ); - - const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); - if (initialConfig) { - this.update(initialConfig); - } - } - @debounceSync(1) - protected debounceChangeNotification() { - this.changed.fire(); - } -} - -function getAbsolutePath(pathToCheck: string, rootDir: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any - pathToCheck = untildify(pathToCheck) as string; - if (isTestExecution() && !pathToCheck) { - return rootDir; - } - if (pathToCheck.indexOf(path.sep) === -1) { - return pathToCheck; - } - return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); -} - -function getPythonExecutable(pythonPath: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any - pythonPath = untildify(pythonPath) as string; - - // If only 'python'. - if ( - pythonPath === 'python' || - pythonPath.indexOf(path.sep) === -1 || - path.basename(pythonPath) === path.dirname(pythonPath) - ) { - return pythonPath; - } - - if (isValidPythonPath(pythonPath)) { - return pythonPath; - } - // Keep python right on top, for backwards compatibility. - // tslint:disable-next-line:variable-name - const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; - - for (let executableName of KnownPythonExecutables) { - // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (IS_WINDOWS) { - executableName = `${executableName}.exe`; - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { - return path.join(pythonPath, 'scripts', executableName); - } - } else { - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { - return path.join(pythonPath, 'bin', executableName); - } - } - } - - return pythonPath; -} - -function isValidPythonPath(pythonPath: string): boolean { - try { - const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); - return output.startsWith('1234'); - } catch (ex) { - return false; - } -} +'use strict'; + +import * as child_process from 'child_process'; +import * as path from 'path'; +import { + ConfigurationChangeEvent, + ConfigurationTarget, + DiagnosticSeverity, + Disposable, + Event, + EventEmitter, + Uri, + WorkspaceConfiguration +} from 'vscode'; +import { LanguageServerType } from '../activation/types'; +import '../common/extensions'; +import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { IWorkspaceService } from './application/types'; +import { WorkspaceService } from './application/workspace'; +import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; +import { DeprecatePythonPath } from './experimentGroups'; +import { ExtensionChannels } from './insidersBuild/types'; +import { IS_WINDOWS } from './platform/constants'; +import { + IAnalysisSettings, + IAutoCompleteSettings, + IDataScienceSettings, + IExperiments, + IExperimentsManager, + IFormattingSettings, + IInterpreterPathService, + ILintingSettings, + IPythonSettings, + ISortImportSettings, + ITerminalSettings, + ITestingSettings, + IWorkspaceSymbolSettings, + Resource +} from './types'; +import { debounceSync } from './utils/decorators'; +import { SystemVariables } from './variables/systemVariables'; + +// tslint:disable:no-require-imports no-var-requires +const untildify = require('untildify'); + +// tslint:disable-next-line:completed-docs +export class PythonSettings implements IPythonSettings { + private static pythonSettings: Map = new Map(); + public downloadLanguageServer = true; + public jediEnabled = true; + public jediPath = ''; + public jediMemoryLimit = 1024; + public envFile = ''; + public venvPath = ''; + public venvFolders: string[] = []; + public condaPath = ''; + public pipenvPath = ''; + public poetryPath = ''; + public devOptions: string[] = []; + public linting!: ILintingSettings; + public formatting!: IFormattingSettings; + public autoComplete!: IAutoCompleteSettings; + public testing!: ITestingSettings; + public terminal!: ITerminalSettings; + public sortImports!: ISortImportSettings; + public workspaceSymbols!: IWorkspaceSymbolSettings; + public disableInstallationChecks = false; + public globalModuleInstallation = false; + public analysis!: IAnalysisSettings; + public autoUpdateLanguageServer: boolean = true; + public datascience!: IDataScienceSettings; + public insidersChannel!: ExtensionChannels; + public experiments!: IExperiments; + public languageServer: LanguageServerType = LanguageServerType.Microsoft; + + protected readonly changed = new EventEmitter(); + private workspaceRoot: Resource; + private disposables: Disposable[] = []; + // tslint:disable-next-line:variable-name + private _pythonPath = ''; + private _defaultInterpreterPath = ''; + private readonly workspace: IWorkspaceService; + public get onDidChange(): Event { + return this.changed.event; + } + + constructor( + workspaceFolder: Resource, + private readonly interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService, + private readonly experimentsManager?: IExperimentsManager, + private readonly interpreterPathService?: IInterpreterPathService + ) { + this.workspace = workspace || new WorkspaceService(); + this.workspaceRoot = workspaceFolder; + this.initialize(); + } + // tslint:disable-next-line:function-name + public static getInstance( + resource: Uri | undefined, + interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService, + experimentsManager?: IExperimentsManager, + interpreterPathService?: IInterpreterPathService + ): 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, + interpreterAutoSelectionService, + workspace, + experimentsManager, + interpreterPathService + ); + PythonSettings.pythonSettings.set(workspaceFolderKey, settings); + // 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(EventName.COMPLETION_ADD_BRACKETS, undefined, { + enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false + }); + sendTelemetryEvent(EventName.FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); + } + // tslint:disable-next-line:no-non-null-assertion + return PythonSettings.pythonSettings.get(workspaceFolderKey)!; + } + + // tslint:disable-next-line:type-literal-delimiter + 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; + + if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolderUri = workspace.workspaceFolders[0].uri; + } + + const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; + return { uri: workspaceFolderUri, target }; + } + + // tslint:disable-next-line:function-name + public static dispose() { + if (!isTestExecution()) { + throw new Error('Dispose can only be called from unit tests'); + } + // tslint:disable-next-line:no-void-expression + PythonSettings.pythonSettings.forEach((item) => item && item.dispose()); + PythonSettings.pythonSettings.clear(); + } + public dispose() { + // tslint:disable-next-line:no-unsafe-any + this.disposables.forEach((disposable) => disposable && disposable.dispose()); + this.disposables = []; + } + // tslint:disable-next-line:cyclomatic-complexity max-func-body-length + protected update(pythonSettings: WorkspaceConfiguration) { + const workspaceRoot = this.workspaceRoot?.fsPath; + const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace); + + /** + * Note that while calling `IExperimentsManager.inExperiment()`, we assume `IExperimentsManager.activate()` is already called. + * That's not true here, as this method is often called in the constructor,which runs before `.activate()` methods. + * But we can still use it here for this particular experiment. Reason being that this experiment only changes + * `pythonPath` setting, and I've checked that `pythonPath` setting is not accessed anywhere in the constructor. + */ + if (this.experimentsManager && this.interpreterPathService) { + if (this.experimentsManager.inExperiment(DeprecatePythonPath.experiment)) { + this.pythonPath = systemVariables.resolveAny(this.interpreterPathService.get(this.workspaceRoot))!; + } else { + this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath'))!; + } + this.experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + } else { + 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.workspaceRoot) { + 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 + const defaultInterpreterPath = systemVariables.resolveAny(pythonSettings.get('defaultInterpreterPath')); + this.defaultInterpreterPath = defaultInterpreterPath ? defaultInterpreterPath : DEFAULT_INTERPRETER_SETTING; + this.defaultInterpreterPath = getAbsolutePath(this.defaultInterpreterPath, workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; + this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; + const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; + this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; + const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; + this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; + const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; + this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + + this.downloadLanguageServer = systemVariables.resolveAny( + pythonSettings.get('downloadLanguageServer', true) + )!; + this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled', true))!; + this.autoUpdateLanguageServer = systemVariables.resolveAny( + pythonSettings.get('autoUpdateLanguageServer', true) + )!; + if (this.jediEnabled) { + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; + if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { + this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); + } else { + this.jediPath = ''; + } + this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; + } + + let ls = pythonSettings.get('languageServer'); + if (!ls) { + ls = LanguageServerType.Jedi; + } + this.languageServer = systemVariables.resolveAny(ls)!; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.envFile = systemVariables.resolveAny(pythonSettings.get('envFile'))!; + // tslint:disable-next-line:no-any + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any + this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; + this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; + if (this.linting) { + Object.assign(this.linting, lintingSettings); + } else { + this.linting = lintingSettings; + } + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const analysisSettings = systemVariables.resolveAny(pythonSettings.get('analysis'))!; + if (this.analysis) { + Object.assign(this.analysis, analysisSettings); + } else { + this.analysis = analysisSettings; + } + + this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; + this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; + if (this.sortImports) { + Object.assign(this.sortImports, sortImportSettings); + } else { + this.sortImports = sortImportSettings; + } + // Support for travis. + this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; + // Support for travis. + this.linting = this.linting + ? this.linting + : { + enabled: false, + ignorePatterns: [], + flake8Args: [], + flake8Enabled: false, + flake8Path: 'flake', + lintOnSave: false, + maxNumberOfProblems: 100, + mypyArgs: [], + mypyEnabled: false, + mypyPath: 'mypy', + banditArgs: [], + banditEnabled: false, + banditPath: 'bandit', + pycodestyleArgs: [], + pycodestyleEnabled: false, + pycodestylePath: 'pycodestyle', + pylamaArgs: [], + pylamaEnabled: false, + pylamaPath: 'pylama', + prospectorArgs: [], + prospectorEnabled: false, + prospectorPath: 'prospector', + pydocstyleArgs: [], + pydocstyleEnabled: false, + pydocstylePath: 'pydocstyle', + pylintArgs: [], + pylintEnabled: false, + pylintPath: 'pylint', + pylintCategorySeverity: { + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning + }, + pycodestyleCategorySeverity: { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning + }, + flake8CategorySeverity: { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning, + // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code + // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as + // unused imports, variables, etc. + F: DiagnosticSeverity.Warning + }, + mypyCategorySeverity: { + error: DiagnosticSeverity.Error, + note: DiagnosticSeverity.Hint + }, + pylintUseMinimalCheckers: false + }; + this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); + this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); + this.linting.pycodestylePath = getAbsolutePath( + systemVariables.resolveAny(this.linting.pycodestylePath), + workspaceRoot + ); + this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); + this.linting.prospectorPath = getAbsolutePath( + systemVariables.resolveAny(this.linting.prospectorPath), + workspaceRoot + ); + this.linting.pydocstylePath = getAbsolutePath( + systemVariables.resolveAny(this.linting.pydocstylePath), + workspaceRoot + ); + this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); + this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; + if (this.formatting) { + Object.assign(this.formatting, formattingSettings); + } else { + this.formatting = formattingSettings; + } + // Support for travis. + this.formatting = this.formatting + ? this.formatting + : { + autopep8Args: [], + autopep8Path: 'autopep8', + provider: 'autopep8', + blackArgs: [], + blackPath: 'black', + yapfArgs: [], + yapfPath: 'yapf' + }; + this.formatting.autopep8Path = getAbsolutePath( + systemVariables.resolveAny(this.formatting.autopep8Path), + workspaceRoot + ); + this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); + this.formatting.blackPath = getAbsolutePath( + systemVariables.resolveAny(this.formatting.blackPath), + workspaceRoot + ); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const autoCompleteSettings = systemVariables.resolveAny( + pythonSettings.get('autoComplete') + )!; + if (this.autoComplete) { + Object.assign(this.autoComplete, autoCompleteSettings); + } else { + this.autoComplete = autoCompleteSettings; + } + // Support for travis. + this.autoComplete = this.autoComplete + ? this.autoComplete + : { + extraPaths: [], + addBrackets: false, + showAdvancedMembers: false, + typeshedPaths: [] + }; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const workspaceSymbolsSettings = systemVariables.resolveAny( + pythonSettings.get('workspaceSymbols') + )!; + if (this.workspaceSymbols) { + Object.assign( + this.workspaceSymbols, + workspaceSymbolsSettings + ); + } else { + this.workspaceSymbols = workspaceSymbolsSettings; + } + // Support for travis. + this.workspaceSymbols = this.workspaceSymbols + ? this.workspaceSymbols + : { + ctagsPath: 'ctags', + enabled: true, + exclusionPatterns: [], + rebuildOnFileSave: true, + rebuildOnStart: true, + tagFilePath: workspaceRoot ? path.join(workspaceRoot, 'tags') : '' + }; + this.workspaceSymbols.tagFilePath = getAbsolutePath( + systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), + workspaceRoot + ); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; + if (this.testing) { + Object.assign(this.testing, testSettings); + } else { + this.testing = testSettings; + if (isTestExecution() && !this.testing) { + // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion + this.testing = { + nosetestArgs: [], + pytestArgs: [], + unittestArgs: [], + promptToConfigure: true, + debugPort: 3000, + nosetestsEnabled: false, + pytestEnabled: false, + unittestEnabled: false, + nosetestPath: 'nosetests', + pytestPath: 'pytest', + autoTestDiscoverOnSaveEnabled: true + } as ITestingSettings; + } + } + + // Support for travis. + this.testing = this.testing + ? this.testing + : { + promptToConfigure: true, + debugPort: 3000, + nosetestArgs: [], + nosetestPath: 'nosetest', + nosetestsEnabled: false, + pytestArgs: [], + pytestEnabled: false, + pytestPath: 'pytest', + unittestArgs: [], + unittestEnabled: false, + autoTestDiscoverOnSaveEnabled: true + }; + this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot); + this.testing.nosetestPath = getAbsolutePath( + systemVariables.resolveAny(this.testing.nosetestPath), + workspaceRoot + ); + if (this.testing.cwd) { + this.testing.cwd = getAbsolutePath(systemVariables.resolveAny(this.testing.cwd), workspaceRoot); + } + + // Resolve any variables found in the test arguments. + this.testing.nosetestArgs = this.testing.nosetestArgs.map((arg) => systemVariables.resolveAny(arg)); + this.testing.pytestArgs = this.testing.pytestArgs.map((arg) => systemVariables.resolveAny(arg)); + this.testing.unittestArgs = this.testing.unittestArgs.map((arg) => systemVariables.resolveAny(arg)); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; + if (this.terminal) { + Object.assign(this.terminal, terminalSettings); + } else { + this.terminal = terminalSettings; + if (isTestExecution() && !this.terminal) { + // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion + this.terminal = {} as ITerminalSettings; + } + } + // Support for travis. + this.terminal = this.terminal + ? this.terminal + : { + executeInFileDir: true, + launchArgs: [], + activateEnvironment: true, + activateEnvInCurrentTerminal: false + }; + + const experiments = systemVariables.resolveAny(pythonSettings.get('experiments'))!; + if (this.experiments) { + Object.assign(this.experiments, experiments); + } else { + this.experiments = experiments; + } + this.experiments = this.experiments + ? this.experiments + : { + enabled: true, + optInto: [], + optOutFrom: [] + }; + + const dataScienceSettings = systemVariables.resolveAny( + pythonSettings.get('dataScience') + )!; + if (this.datascience) { + Object.assign(this.datascience, dataScienceSettings); + } else { + this.datascience = dataScienceSettings; + } + + this.insidersChannel = pythonSettings.get('insidersChannel')!; + } + + public get pythonPath(): string { + return this._pythonPath; + } + public set pythonPath(value: string) { + if (this._pythonPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._pythonPath = this.getPythonExecutable(value); + } catch (ex) { + this._pythonPath = value; + } + } + + public get defaultInterpreterPath(): string { + return this._defaultInterpreterPath; + } + public set defaultInterpreterPath(value: string) { + if (this._defaultInterpreterPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._defaultInterpreterPath = this.getPythonExecutable(value); + } catch (ex) { + this._defaultInterpreterPath = value; + } + } + protected getPythonExecutable(pythonPath: string) { + return getPythonExecutable(pythonPath); + } + protected onWorkspaceFoldersChanged() { + //If an activated workspace folder was removed, delete its key + const workspaceKeys = this.workspace.workspaceFolders!.map((workspaceFolder) => workspaceFolder.uri.fsPath); + const activatedWkspcKeys = Array.from(PythonSettings.pythonSettings.keys()); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + PythonSettings.pythonSettings.delete(folder); + } + } + } + protected initialize(): void { + 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. + this.debounceChangeNotification(); + }; + this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); + this.disposables.push( + this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)) + ); + this.disposables.push( + this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { + if (event.affectsConfiguration('python')) { + onDidChange(); + } + }) + ); + if (this.interpreterPathService) { + this.disposables.push(this.interpreterPathService.onDidChange(onDidChange.bind(this))); + } + + const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); + if (initialConfig) { + this.update(initialConfig); + } + } + @debounceSync(1) + protected debounceChangeNotification() { + this.changed.fire(); + } +} + +function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): string { + if (!rootDir) { + rootDir = __dirname; + } + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pathToCheck = untildify(pathToCheck) as string; + if (isTestExecution() && !pathToCheck) { + return rootDir; + } + if (pathToCheck.indexOf(path.sep) === -1) { + return pathToCheck; + } + return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); +} + +function getPythonExecutable(pythonPath: string): string { + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pythonPath = untildify(pythonPath) as string; + + // If only 'python'. + if ( + pythonPath === 'python' || + pythonPath.indexOf(path.sep) === -1 || + path.basename(pythonPath) === path.dirname(pythonPath) + ) { + return pythonPath; + } + + if (isValidPythonPath(pythonPath)) { + return pythonPath; + } + // Keep python right on top, for backwards compatibility. + // tslint:disable-next-line:variable-name + const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; + + for (let executableName of KnownPythonExecutables) { + // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. + if (IS_WINDOWS) { + executableName = `${executableName}.exe`; + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { + return path.join(pythonPath, 'scripts', executableName); + } + } else { + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { + return path.join(pythonPath, 'bin', executableName); + } + } + } + + return pythonPath; +} + +function isValidPythonPath(pythonPath: string): boolean { + try { + const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); + return output.startsWith('1234'); + } catch (ex) { + return false; + } +} diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index e05200105f55..e6da5ab0c19d 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -2,12 +2,14 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { ConfigurationTarget, Uri, 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'; +import { isUnitTestExecution } from '../constants'; +import { DeprecatePythonPath } from '../experimentGroups'; +import { IConfigurationService, IExperimentsManager, IInterpreterPathService, IPythonSettings } from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { @@ -19,7 +21,15 @@ export class ConfigurationService implements IConfigurationService { const InterpreterAutoSelectionService = this.serviceContainer.get( IInterpreterAutoSeletionProxyService ); - return PythonSettings.getInstance(resource, InterpreterAutoSelectionService, this.workspaceService); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const experiments = this.serviceContainer.get(IExperimentsManager); + return PythonSettings.getInstance( + resource, + InterpreterAutoSelectionService, + this.workspaceService, + experiments, + interpreterPathService + ); } public async updateSectionSetting( @@ -29,6 +39,10 @@ export class ConfigurationService implements IConfigurationService { resource?: Uri, configTarget?: ConfigurationTarget ): Promise { + const experiments = this.serviceContainer.get(IExperimentsManager); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const inExperiment = experiments.inExperiment(DeprecatePythonPath.experiment); + experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); const defaultSetting = { uri: resource, target: configTarget || ConfigurationTarget.WorkspaceFolder @@ -37,22 +51,31 @@ export class ConfigurationService implements IConfigurationService { if (section === 'python' && configTarget !== ConfigurationTarget.Global) { settingsInfo = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService); } + configTarget = configTarget ? configTarget : settingsInfo.target; - const configSection = workspace.getConfiguration(section, settingsInfo.uri ? settingsInfo.uri : null); - const currentValue = configSection.inspect(setting); + const configSection = this.workspaceService.getConfiguration(section, settingsInfo.uri); + const currentValue = + inExperiment && section === 'python' && setting === 'pythonPath' + ? interpreterPathService.inspect(settingsInfo.uri) + : configSection.inspect(setting); if ( currentValue !== undefined && - ((settingsInfo.target === ConfigurationTarget.Global && currentValue.globalValue === value) || - (settingsInfo.target === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || - (settingsInfo.target === ConfigurationTarget.WorkspaceFolder && - currentValue.workspaceFolderValue === value)) + ((configTarget === ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value)) ) { return; } - - await configSection.update(setting, value, settingsInfo.target); - await this.verifySetting(configSection, settingsInfo.target, setting, value); + if (section === 'python' && setting === 'pythonPath') { + if (inExperiment) { + // tslint:disable-next-line: no-any + await interpreterPathService.update(settingsInfo.uri, configTarget, value as any); + } + } else { + await configSection.update(setting, value, configTarget); + await this.verifySetting(configSection, configTarget, setting, value); + } } public async updateSetting( @@ -74,7 +97,7 @@ export class ConfigurationService implements IConfigurationService { settingName: string, value?: {} ): Promise { - if (this.isTestExecution()) { + if (this.isTestExecution() && !isUnitTestExecution()) { let retries = 0; do { const setting = configSection.inspect(settingName); diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index f9b54a8cfa2c..c573d052475a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -85,6 +85,8 @@ export namespace Delays { export const MaxUnitTestCodeLensDelay = 5000; } +export const DEFAULT_INTERPRETER_SETTING = 'python'; + export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; diff --git a/src/client/common/experimentGroups.ts b/src/client/common/experimentGroups.ts index 29d5a1a65627..6f14c5c3bdfc 100644 --- a/src/client/common/experimentGroups.ts +++ b/src/client/common/experimentGroups.ts @@ -82,3 +82,14 @@ export enum EnableIPyWidgets { control = 'EnableIPyWidgets - control', experiment = 'EnableIPyWidgets - experiment' } + +/* + * Experiment to check whether the extension should deprecate `python.pythonPath` setting + + * Note: 'DeprecatePythonPath - experiment' string is directly used in `src\test\common.ts` instead + * of accessing through the object. Be sure to remove it when removing the experiment. + */ +export enum DeprecatePythonPath { + control = 'DeprecatePythonPath - control', + experiment = 'DeprecatePythonPath - experiment' +} diff --git a/src/client/common/experiments.ts b/src/client/common/experiments.ts index 8fbf35b3a810..d2372dbc4339 100644 --- a/src/client/common/experiments.ts +++ b/src/client/common/experiments.ts @@ -8,7 +8,7 @@ import { inject, injectable, named, optional } from 'inversify'; import { parse } from 'jsonc-parser'; import * as path from 'path'; -import { IConfigurationService, IHttpClient } from '../common/types'; +import { IConfigurationService, IHttpClient, IPythonSettings } from '../common/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { IApplicationEnvironment } from './application/types'; @@ -53,11 +53,15 @@ export class ExperimentsManager implements IExperimentsManager { /** * Experiments user requested to opt into manually */ - public _experimentsOptedInto: string[]; + public _experimentsOptedInto: string[] = []; /** * Experiments user requested to opt out from manually */ - public _experimentsOptedOutFrom: string[]; + public _experimentsOptedOutFrom: string[] = []; + /** + * Returns `true` if experiments are enabled, else `false`. + */ + public _enabled: boolean = true; /** * Keeps track of the experiments to be used in the current session */ @@ -74,10 +78,6 @@ export class ExperimentsManager implements IExperimentsManager { * Function updateExperimentStorage() makes sure these are used in the next session. */ private downloadedExperimentsStorage: IPersistentState; - /** - * Returns `true` if experiments are enabled, else `false`. - */ - private readonly enabled: boolean; /** * Keeps track if the storage needs updating or not. * Note this has to be separate from the actual storage as @@ -85,6 +85,7 @@ export class ExperimentsManager implements IExperimentsManager { */ private isDownloadedStorageValid: IPersistentState; private activatedOnce: boolean = false; + private settings!: IPythonSettings; constructor( @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, @inject(IHttpClient) private readonly httpClient: IHttpClient, @@ -92,7 +93,7 @@ export class ExperimentsManager implements IExperimentsManager { @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @optional() private experimentEffortTimeout: number = EXPERIMENTS_EFFORT_TIMEOUT_MS ) { this.isDownloadedStorageValid = this.persistentStateFactory.createGlobalPersistentState( @@ -107,10 +108,6 @@ export class ExperimentsManager implements IExperimentsManager { this.downloadedExperimentsStorage = this.persistentStateFactory.createGlobalPersistentState< ABExperiments | undefined >(downloadedExperimentStorageKey, undefined); - const settings = configurationService.getSettings(undefined); - this.enabled = settings.experiments.enabled; - this._experimentsOptedInto = settings.experiments.optInto; - this._experimentsOptedOutFrom = settings.experiments.optOutFrom; } @swallowExceptions('Failed to activate experiments') @@ -119,7 +116,11 @@ export class ExperimentsManager implements IExperimentsManager { return; } this.activatedOnce = true; - if (!this.enabled) { + this.settings = this.configurationService.getSettings(undefined); + this._experimentsOptedInto = this.settings.experiments.optInto; + this._experimentsOptedOutFrom = this.settings.experiments.optOutFrom; + this._enabled = this.settings.experiments.enabled; + if (!this._enabled) { sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_DISABLED); return; } @@ -134,11 +135,11 @@ export class ExperimentsManager implements IExperimentsManager { @traceDecorators.error('Failed to identify if user is in experiment') public inExperiment(experimentName: string): boolean { - if (!this.enabled) { + if (!this._enabled) { return false; } this.sendTelemetryIfInExperiment(experimentName); - return this.userExperiments.find((exp) => exp.name === experimentName) ? true : false; + return this.userExperiments.find(exp => exp.name === experimentName) ? true : false; } /** @@ -179,7 +180,7 @@ export class ExperimentsManager implements IExperimentsManager { @traceDecorators.error('Failed to send telemetry when user is in experiment') public sendTelemetryIfInExperiment(experimentName: string): void { - if (this.userExperiments.find((exp) => exp.name === experimentName)) { + if (this.userExperiments.find(exp => exp.name === experimentName)) { sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS, undefined, { expName: experimentName }); } } @@ -222,7 +223,7 @@ export class ExperimentsManager implements IExperimentsManager { throw new Error('Machine ID should be a string'); } let hash: number; - if (oldExperimentSalts.find((oldSalt) => oldSalt === salt)) { + if (oldExperimentSalts.find(oldSalt => oldSalt === salt)) { hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'SHA512'); } else { hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'FNV'); @@ -338,7 +339,7 @@ export class ExperimentsManager implements IExperimentsManager { this._experimentsOptedOutFrom[i] = ''; } } - this._experimentsOptedInto = this._experimentsOptedInto.filter((exp) => exp !== ''); - this._experimentsOptedOutFrom = this._experimentsOptedOutFrom.filter((exp) => exp !== ''); + this._experimentsOptedInto = this._experimentsOptedInto.filter(exp => exp !== ''); + this._experimentsOptedOutFrom = this._experimentsOptedOutFrom.filter(exp => exp !== ''); } } diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts new file mode 100644 index 000000000000..8d198cb932a7 --- /dev/null +++ b/src/client/common/interpreterPathService.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from './application/types'; +import { PythonSettings } from './configSettings'; +import { isTestExecution } from './constants'; +import { + IDisposable, + IDisposableRegistry, + IInterpreterPathService, + InspectInterpreterSettingType, + InterpreterConfigurationScope, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + Resource +} from './types'; + +export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; +const CI_PYTHON_PATH = getCIPythonPath(); + +export function getCIPythonPath(): string { + if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { + return process.env.CI_PYTHON_PATH; + } + return 'python'; +} +@injectable() +export class InterpreterPathService implements IInterpreterPathService { + public _didChangeInterpreterEmitter = new EventEmitter(); + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDisposableRegistry) disposables: IDisposable[] + ) { + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + } + + public async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + if (event.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) { + this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global }); + } + } + + public inspect(resource: Resource): InspectInterpreterSettingType { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + let workspaceFolderSetting: IPersistentState | undefined; + let workspaceSetting: IPersistentState | undefined; + if (resource) { + workspaceFolderSetting = this.persistentStateFactory.createGlobalPersistentState( + this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder), + undefined + ); + workspaceSetting = this.persistentStateFactory.createGlobalPersistentState( + this.getSettingKey(resource, ConfigurationTarget.Workspace), + undefined + ); + } + const globalValue = this.workspaceService.getConfiguration('python')!.inspect('defaultInterpreterPath')! + .globalValue; + return { + globalValue, + workspaceFolderValue: workspaceFolderSetting?.value, + workspaceValue: workspaceSetting?.value + }; + } + + public get(resource: Resource): string { + const settings = this.inspect(resource); + return ( + settings.workspaceFolderValue || + settings.workspaceValue || + settings.globalValue || + (isTestExecution() ? CI_PYTHON_PATH : 'python') + ); + } + + public async update( + resource: Resource, + configTarget: ConfigurationTarget, + pythonPath: string | undefined + ): Promise { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + if (configTarget === ConfigurationTarget.Global) { + const pythonConfig = this.workspaceService.getConfiguration('python'); + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget }); + return; + } + if (!resource) { + throw new Error('Cannot update workspace settings as no workspace is opened'); + } + const settingKey = this.getSettingKey(resource, configTarget); + const persistentSetting = this.persistentStateFactory.createGlobalPersistentState( + settingKey, + undefined + ); + await persistentSetting.updateValue(pythonPath); + this._didChangeInterpreterEmitter.fire({ uri: resource, configTarget }); + } + + public get onDidChange(): Event { + return this._didChangeInterpreterEmitter.event; + } + + public getSettingKey( + resource: Uri, + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder + ): string { + let settingKey: string; + const folderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource); + if (configTarget === ConfigurationTarget.WorkspaceFolder) { + settingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; + } else { + settingKey = this.workspaceService.workspaceFile + ? `WORKSPACE_INTERPRETER_PATH_${this.workspaceService.workspaceFile.fsPath}` + : // Only a single folder is opened, use fsPath of the folder as key + `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; + } + return settingKey; + } +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 210106aedb2a..3105e902c3d9 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { IExtensionSingleActivationService } from '../activation/types'; -import { IFileDownloader, IHttpClient } from '../common/types'; +import { IFileDownloader, IHttpClient, IInterpreterPathService } from '../common/types'; import { LiveShareApi } from '../datascience/liveshare/liveshare'; import { INotebookExecutionLogger } from '../datascience/types'; import { IServiceManager } from '../ioc/types'; import { ImportTracker } from '../telemetry/importTracker'; import { IImportTracker } from '../telemetry/types'; +import { ActiveResourceService } from './application/activeResource'; import { ApplicationEnvironment } from './application/applicationEnvironment'; import { ApplicationShell } from './application/applicationShell'; import { ClipboardService } from './application/clipboard'; @@ -20,6 +21,7 @@ import { Extensions } from './application/extensions'; import { LanguageService } from './application/languageService'; import { TerminalManager } from './application/terminalManager'; import { + IActiveResourceService, IApplicationEnvironment, IApplicationShell, IClipboard, @@ -54,6 +56,7 @@ import { IInsiderExtensionPrompt } from './insidersBuild/types'; import { ProductInstaller } from './installer/productInstaller'; +import { InterpreterPathService } from './interpreterPathService'; import { BrowserService } from './net/browser'; import { FileDownloader } from './net/fileDownloader'; import { HttpClient } from './net/httpClient'; @@ -110,6 +113,8 @@ import { Random } from './utils/random'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); + serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); serviceManager.addSingleton(IExtensions, Extensions); serviceManager.addSingleton(IRandom, Random); serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index ff6b90c7938a..3baf18cf5d23 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -191,6 +191,7 @@ export interface IPythonSettings { readonly onDidChange: Event; readonly experiments: IExperiments; readonly languageServer: LanguageServerType; + readonly defaultInterpreterPath: string; } export interface ISortImportSettings { readonly path: string; @@ -614,3 +615,21 @@ export interface IExperimentsManager { */ sendTelemetryIfInExperiment(experimentName: string): void; } + +export type InterpreterConfigurationScope = { uri: Resource; configTarget: ConfigurationTarget }; +export type InspectInterpreterSettingType = { + globalValue?: string; + workspaceValue?: string; + workspaceFolderValue?: string; +}; + +/** + * Interface used to access current Interpreter Path + */ +export const IInterpreterPathService = Symbol('IInterpreterPathService'); +export interface IInterpreterPathService { + onDidChange: Event; + get(resource: Resource): string; + inspect(resource: Resource): InspectInterpreterSettingType; + update(resource: Resource, configTarget: ConfigurationTarget, value: string | undefined): Promise; +} diff --git a/src/client/common/utils/cacheUtils.ts b/src/client/common/utils/cacheUtils.ts index f6b74de2fb3f..66fe0c35f626 100644 --- a/src/client/common/utils/cacheUtils.ts +++ b/src/client/common/utils/cacheUtils.ts @@ -7,7 +7,10 @@ import { Uri } from 'vscode'; import '../../common/extensions'; -import { Resource } from '../types'; +import { IServiceContainer } from '../../ioc/types'; +import { DEFAULT_INTERPRETER_SETTING } from '../constants'; +import { DeprecatePythonPath } from '../experimentGroups'; +import { IExperimentsManager, IInterpreterPathService, Resource } from '../types'; type VSCodeType = typeof import('vscode'); type CacheData = { @@ -25,12 +28,27 @@ const resourceSpecificCacheStores = new Map>(); * @param {VSCodeType} [vscode=require('vscode')] * @returns */ -function getCacheKey(resource: Resource, vscode: VSCodeType = require('vscode')) { +function getCacheKey( + resource: Resource, + vscode: VSCodeType = require('vscode'), + serviceContainer: IServiceContainer | undefined +) { const section = vscode.workspace.getConfiguration('python', vscode.Uri.file(__filename)); if (!section) { return 'python'; } - const globalPythonPath = section.inspect('pythonPath')!.globalValue || 'python'; + let interpreterPathService: IInterpreterPathService | undefined; + let inExperiment: boolean | undefined; + if (serviceContainer) { + interpreterPathService = serviceContainer.get(IInterpreterPathService); + const abExperiments = serviceContainer.get(IExperimentsManager); + inExperiment = abExperiments.inExperiment(DeprecatePythonPath.experiment); + abExperiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + } + const globalPythonPath = + inExperiment && interpreterPathService + ? interpreterPathService.inspect(vscode.Uri.file(__filename)).globalValue || DEFAULT_INTERPRETER_SETTING + : section.inspect('pythonPath')!.globalValue || DEFAULT_INTERPRETER_SETTING; // Get the workspace related to this resource. if ( !resource || @@ -44,7 +62,10 @@ function getCacheKey(resource: Resource, vscode: VSCodeType = require('vscode')) return globalPythonPath; } const workspacePythonPath = - vscode.workspace.getConfiguration('python', resource).get('pythonPath') || 'python'; + inExperiment && interpreterPathService + ? interpreterPathService.get(resource) + : vscode.workspace.getConfiguration('python', resource).get('pythonPath') || + DEFAULT_INTERPRETER_SETTING; return `${folder.uri.fsPath}-${workspacePythonPath}`; } /** @@ -54,8 +75,12 @@ function getCacheKey(resource: Resource, vscode: VSCodeType = require('vscode')) * @param {VSCodeType} [vscode=require('vscode')] * @returns */ -function getCacheStore(resource: Resource, vscode: VSCodeType = require('vscode')) { - const key = getCacheKey(resource, vscode); +function getCacheStore( + resource: Resource, + vscode: VSCodeType = require('vscode'), + serviceContainer: IServiceContainer | undefined +) { + const key = getCacheKey(resource, vscode, serviceContainer); if (!resourceSpecificCacheStores.has(key)) { resourceSpecificCacheStores.set(key, new Map()); } @@ -145,12 +170,13 @@ export class InMemoryCache { export class InMemoryInterpreterSpecificCache extends InMemoryCache { private readonly resource: Resource; protected get store() { - return getCacheStore(this.resource, this.vscode); + return getCacheStore(this.resource, this.vscode, this.serviceContainer); } constructor( keyPrefix: string, expiryDurationMs: number, args: [Uri | undefined, ...any[]], + private readonly serviceContainer: IServiceContainer | undefined, private readonly vscode: VSCodeType = require('vscode') ) { super(expiryDurationMs, getCacheKeyFromFunctionArgs(keyPrefix, args.slice(1))); diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts index 644819e1ae53..10da3870d363 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -1,7 +1,8 @@ // tslint:disable:no-any no-require-imports no-function-expression no-invalid-this -import { ProgressLocation, ProgressOptions, Uri, window } from 'vscode'; +import { ProgressLocation, ProgressOptions, window } from 'vscode'; import '../../common/extensions'; +import { IServiceContainer } from '../../ioc/types'; import { isTestExecution } from '../constants'; import { traceError, traceVerbose } from '../logger'; import { Resource } from '../types'; @@ -56,7 +57,7 @@ export function debounceAsync(wait?: number) { export function makeDebounceDecorator(wait?: number) { // tslint:disable-next-line:no-any no-function-expression - return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function(_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { // We could also make use of _debounce() options. For instance, // the following causes the original method to be called // immediately: @@ -71,7 +72,7 @@ export function makeDebounceDecorator(wait?: number) { const options = {}; const originalMethod = descriptor.value!; const debounced = _debounce( - function (this: any) { + function(this: any) { return originalMethod.apply(this, arguments as any); }, wait, @@ -83,7 +84,7 @@ export function makeDebounceDecorator(wait?: number) { export function makeDebounceAsyncDecorator(wait?: number) { // tslint:disable-next-line:no-any no-function-expression - return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function(_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { type StateInformation = { started: boolean; deferred: Deferred | undefined; @@ -93,7 +94,7 @@ export function makeDebounceAsyncDecorator(wait?: number) { const state: StateInformation = { started: false, deferred: undefined, timer: undefined }; // Lets defer execution using a setTimeout for the given time. - (descriptor as any).value = function (this: any) { + (descriptor as any).value = function(this: any) { const existingDeferred: Deferred | undefined = state.deferred; if (existingDeferred && state.started) { return existingDeferred.promise; @@ -111,11 +112,11 @@ export function makeDebounceAsyncDecorator(wait?: number) { state.started = true; originalMethod .apply(this) - .then((r) => { + .then(r => { state.started = false; deferred.resolve(r); }) - .catch((ex) => { + .catch(ex => { state.started = false; deferred.reject(ex); }); @@ -126,44 +127,21 @@ export function makeDebounceAsyncDecorator(wait?: number) { } type VSCodeType = typeof import('vscode'); -type PromiseFunctionWithFirstArgOfResource = (...any: [Uri | undefined, ...any[]]) => Promise; export function clearCachedResourceSpecificIngterpreterData( key: string, resource: Resource, + serviceContainer: IServiceContainer, vscode: VSCodeType = require('vscode') ) { - const cacheStore = new InMemoryInterpreterSpecificCache(key, 0, [resource], vscode); + const cacheStore = new InMemoryInterpreterSpecificCache(key, 0, [resource], serviceContainer, vscode); cacheStore.clear(); } -export function cacheResourceSpecificInterpreterData( - key: string, - expiryDurationMs: number, - vscode: VSCodeType = require('vscode') -) { - return function ( - _target: Object, - _propertyName: string, - descriptor: TypedPropertyDescriptor - ) { - const originalMethod = descriptor.value!; - descriptor.value = async function (...args: [Uri | undefined, ...any[]]) { - const cacheStore = new InMemoryInterpreterSpecificCache(key, expiryDurationMs, args, vscode); - if (cacheStore.hasData) { - traceVerbose(`Cached data exists ${key}, ${args[0] ? args[0].fsPath : ''}`); - return Promise.resolve(cacheStore.data); - } - const promise = originalMethod.apply(this, args) as Promise; - promise.then((result) => (cacheStore.data = result)).ignoreErrors(); - return promise; - }; - }; -} type PromiseFunctionWithAnyArgs = (...any: any) => Promise; const cacheStoreForMethods = getGlobalCacheStore(); export function cache(expiryDurationMs: number) { - return function ( + return function( target: Object, propertyName: string, descriptor: TypedPropertyDescriptor @@ -171,7 +149,7 @@ export function cache(expiryDurationMs: number) { const originalMethod = descriptor.value!; const className = 'constructor' in target && target.constructor.name ? target.constructor.name : ''; const keyPrefix = `Cache_Method_Output_${className}.${propertyName}`; - descriptor.value = async function (...args: any) { + descriptor.value = async function(...args: any) { if (isTestExecution()) { return originalMethod.apply(this, args) as Promise; } @@ -183,9 +161,7 @@ export function cache(expiryDurationMs: number) { } const promise = originalMethod.apply(this, args) as Promise; promise - .then((result) => - cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryDurationMs }) - ) + .then(result => cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryDurationMs })) .ignoreErrors(); return promise; }; @@ -201,18 +177,18 @@ export function cache(expiryDurationMs: number) { */ export function swallowExceptions(scopeName: string) { // tslint:disable-next-line:no-any no-function-expression - return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { + return function(_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value!; const errorMessage = `Python Extension (Error in ${scopeName}, method:${propertyName}):`; // tslint:disable-next-line:no-any no-function-expression - descriptor.value = function (...args: any[]) { + descriptor.value = function(...args: any[]) { try { // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any const result = originalMethod.apply(this, args); // If method being wrapped returns a promise then wait and swallow errors. if (result && typeof result.then === 'function' && typeof result.catch === 'function') { - return (result as Promise).catch((error) => { + return (result as Promise).catch(error => { if (isTestExecution()) { return; } @@ -233,10 +209,10 @@ export function swallowExceptions(scopeName: string) { type PromiseFunction = (...any: any[]) => Promise; export function displayProgress(title: string, location = ProgressLocation.Window) { - return function (_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function(_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value!; // tslint:disable-next-line:no-any no-function-expression - descriptor.value = async function (...args: any[]) { + descriptor.value = async function(...args: any[]) { const progressOptions: ProgressOptions = { location, title }; // tslint:disable-next-line:no-invalid-this const promise = originalMethod.apply(this, args); diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 457a5715eb43..d658303c562b 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -145,6 +145,7 @@ export namespace Interpreters { 'Interpreters.selectInterpreterTip', 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar' ); + export const pythonInterpreterPath = localize('Interpreters.pythonInterpreterPath', 'Python interpreter path: {0}'); } export namespace ExtensionChannels { export const yesWeekly = localize('ExtensionChannels.yesWeekly', 'Yes, weekly'); diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts index 583d4bd63914..fd083b10e88f 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -1,16 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; +import { inject, injectable, optional } from 'inversify'; import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, FileSystemWatcher, Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; import { sendFileCreationTelemetry } from '../../telemetry/envFileTelemetry'; import { IWorkspaceService } from '../application/types'; +import { traceVerbose } from '../logger'; import { IPlatformService } from '../platform/types'; import { IConfigurationService, ICurrentProcess, IDisposableRegistry } from '../types'; -import { cacheResourceSpecificInterpreterData, clearCachedResourceSpecificIngterpreterData } from '../utils/decorators'; +import { InMemoryInterpreterSpecificCache } from '../utils/cacheUtils'; +import { clearCachedResourceSpecificIngterpreterData } from '../utils/decorators'; import { EnvironmentVariables, IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './types'; -const cacheDuration = 60 * 60 * 1000; +const CACHE_DURATION = 60 * 60 * 1000; @injectable() export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvider, Disposable { public trackedWorkspaceFolders = new Set(); @@ -23,7 +26,9 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid @inject(IPlatformService) private platformService: IPlatformService, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ICurrentProcess) private process: ICurrentProcess + @inject(ICurrentProcess) private process: ICurrentProcess, + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @optional() private cacheDuration: number = CACHE_DURATION ) { disposableRegistry.push(this); this.changeEventEmitter = new EventEmitter(); @@ -43,8 +48,24 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } }); } - @cacheResourceSpecificInterpreterData('getEnvironmentVariables', cacheDuration) + public async getEnvironmentVariables(resource?: Uri): Promise { + // Cache resource specific interpreter data + const cacheStore = new InMemoryInterpreterSpecificCache( + 'getEnvironmentVariables', + this.cacheDuration, + [resource], + this.serviceContainer + ); + if (cacheStore.hasData) { + traceVerbose(`Cached data exists getEnvironmentVariables, ${resource ? resource.fsPath : ''}`); + return Promise.resolve(cacheStore.data) as Promise; + } + const promise = this._getEnvironmentVariables(resource); + promise.then((result) => (cacheStore.data = result)).ignoreErrors(); + return promise; + } + public async _getEnvironmentVariables(resource?: Uri): Promise { let mergedVars = await this.getCustomEnvironmentVariables(resource); if (!mergedVars) { mergedVars = {}; @@ -101,8 +122,16 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } private onEnvironmentFileChanged(workspaceFolderUri?: Uri) { - clearCachedResourceSpecificIngterpreterData('getEnvironmentVariables', workspaceFolderUri); - clearCachedResourceSpecificIngterpreterData('CustomEnvironmentVariables', workspaceFolderUri); + clearCachedResourceSpecificIngterpreterData( + 'getEnvironmentVariables', + workspaceFolderUri, + this.serviceContainer + ); + clearCachedResourceSpecificIngterpreterData( + 'CustomEnvironmentVariables', + workspaceFolderUri, + this.serviceContainer + ); this.changeEventEmitter.fire(workspaceFolderUri); } } diff --git a/src/client/interpreter/autoSelection/rules/settings.ts b/src/client/interpreter/autoSelection/rules/settings.ts index 96217cfa918a..bbd79abe6f0c 100644 --- a/src/client/interpreter/autoSelection/rules/settings.ts +++ b/src/client/interpreter/autoSelection/rules/settings.ts @@ -5,8 +5,9 @@ import { inject, injectable } from 'inversify'; import { IWorkspaceService } from '../../../common/application/types'; +import { DeprecatePythonPath } from '../../../common/experimentGroups'; import { IFileSystem } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { IExperimentsManager, IInterpreterPathService, IPersistentStateFactory, Resource } from '../../../common/types'; import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; import { BaseRuleService, NextAction } from './baseRule'; @@ -15,7 +16,9 @@ export class SettingsInterpretersAutoSelectionRule extends BaseRuleService { constructor( @inject(IFileSystem) fs: IFileSystem, @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService ) { super(AutoSelectionRule.settings, fs, stateFactory); } @@ -25,7 +28,10 @@ export class SettingsInterpretersAutoSelectionRule extends BaseRuleService { ): Promise { // tslint:disable-next-line:no-any const pythonConfig = this.workspaceService.getConfiguration('python', null as any)!; - const pythonPathInConfig = pythonConfig.inspect('pythonPath')!; + const pythonPathInConfig = this.experiments.inExperiment(DeprecatePythonPath.experiment) + ? this.interpreterPathService.inspect(undefined) + : pythonConfig.inspect('pythonPath')!; + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); // 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 diff --git a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts index b98aabe26ae0..f8e262bc827c 100644 --- a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts +++ b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts @@ -6,9 +6,10 @@ import { inject, injectable, named } from 'inversify'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import { DeprecatePythonPath } from '../../../common/experimentGroups'; import { traceVerbose } from '../../../common/logger'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { IExperimentsManager, IInterpreterPathService, IPersistentStateFactory, Resource } from '../../../common/types'; import { createDeferredFromPromise } from '../../../common/utils/async'; import { OSType } from '../../../common/utils/platform'; import { IPythonPathUpdaterServiceManager } from '../../configuration/types'; @@ -37,7 +38,9 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe private readonly pipEnvInterpreterLocator: IInterpreterLocatorService, @inject(IInterpreterLocatorService) @named(WORKSPACE_VIRTUAL_ENV_SERVICE) - private readonly workspaceVirtualEnvInterpreterLocator: IInterpreterLocatorService + private readonly workspaceVirtualEnvInterpreterLocator: IInterpreterLocatorService, + @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService ) { super(AutoSelectionRule.workspaceVirtualEnvs, fs, stateFactory); } @@ -51,7 +54,10 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe } const pythonConfig = this.workspaceService.getConfiguration('python', workspacePath.folderUri)!; - const pythonPathInConfig = pythonConfig.inspect('pythonPath')!; + const pythonPathInConfig = this.experiments.inExperiment(DeprecatePythonPath.experiment) + ? this.interpreterPathService.inspect(workspacePath.folderUri) + : pythonConfig.inspect('pythonPath')!; + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); // If user has defined custom values in settings for this workspace folder, then use that. if (pythonPathInConfig.workspaceFolderValue) { return NextAction.runNextRule; diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 293e6cb83895..4f677690967e 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -163,7 +163,7 @@ export class InterpreterSelector implements IInterpreterSelector { configTarget: ConfigurationTarget.Global }; } - if (this.workspaceService.workspaceFolders.length === 1) { + if (!this.workspaceService.workspaceFile && this.workspaceService.workspaceFolders.length === 1) { return { folderUri: this.workspaceService.workspaceFolders[0].uri, configTarget: ConfigurationTarget.WorkspaceFolder @@ -174,13 +174,11 @@ export class InterpreterSelector implements IInterpreterSelector { type WorkspaceSelectionQuickPickItem = QuickPickItem & { uri: Uri }; const quickPickItems: WorkspaceSelectionQuickPickItem[] = [ - ...this.workspaceService.workspaceFolders.map(w => { - ({ - label: w.name, - description: path.dirname(w.uri.fsPath), - uri: w.uri - }) - }), + ...this.workspaceService.workspaceFolders.map(w => ({ + label: w.name, + description: path.dirname(w.uri.fsPath), + uri: w.uri + })), { label: Interpreters.entireWorkspace(), uri: this.workspaceService.workspaceFolders[0].uri diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts index d7e6451aeb40..b1e70235c4b8 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -1,6 +1,8 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import { DeprecatePythonPath } from '../../common/experimentGroups'; +import { IExperimentsManager, IInterpreterPathService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; @@ -9,17 +11,37 @@ import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './t @injectable() export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { + private readonly inDeprecatePythonPathExperiment: boolean; private readonly workspaceService: IWorkspaceService; + private readonly interpreterPathService: IInterpreterPathService; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + const experiments = serviceContainer.get(IExperimentsManager); this.workspaceService = serviceContainer.get(IWorkspaceService); + this.interpreterPathService = serviceContainer.get(IInterpreterPathService); + this.inDeprecatePythonPathExperiment = experiments.inExperiment(DeprecatePythonPath.experiment); + experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); } public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { - return new GlobalPythonPathUpdaterService(this.workspaceService); + return new GlobalPythonPathUpdaterService( + this.inDeprecatePythonPathExperiment, + this.workspaceService, + this.interpreterPathService + ); } public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { - return new WorkspacePythonPathUpdaterService(wkspace, this.workspaceService); + return new WorkspacePythonPathUpdaterService( + wkspace, + this.inDeprecatePythonPathExperiment, + this.workspaceService, + this.interpreterPathService + ); } public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { - return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder, this.workspaceService); + return new WorkspaceFolderPythonPathUpdaterService( + workspaceFolder, + this.inDeprecatePythonPathExperiment, + this.workspaceService, + this.interpreterPathService + ); } } diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index 5ed874423b56..39a1c1731019 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -1,15 +1,27 @@ +import { ConfigurationTarget } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import { IInterpreterPathService } from '../../../common/types'; import { IPythonPathUpdaterService } from '../types'; export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private readonly workspaceService: IWorkspaceService) {} + constructor( + private readonly inDeprecatePythonPathExperiment: boolean, + private readonly workspaceService: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService + ) {} public async updatePythonPath(pythonPath: string | undefined): Promise { const pythonConfig = this.workspaceService.getConfiguration('python'); - const pythonPathValue = pythonConfig.inspect('pythonPath'); + const pythonPathValue = this.inDeprecatePythonPathExperiment + ? this.interpreterPathService.inspect(undefined) + : pythonConfig.inspect('pythonPath')!; if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { return; } - await pythonConfig.update('pythonPath', pythonPath, true); + if (this.inDeprecatePythonPathExperiment) { + await this.interpreterPathService.update(undefined, ConfigurationTarget.Global, pythonPath); + } else { + await pythonConfig.update('pythonPath', pythonPath, true); + } } } diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts index e1bf153f5509..3e7888bab2b3 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -1,13 +1,21 @@ import * as path from 'path'; import { ConfigurationTarget, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import { IInterpreterPathService } from '../../../common/types'; import { IPythonPathUpdaterService } from '../types'; export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private workspaceFolder: Uri, private readonly workspaceService: IWorkspaceService) {} + constructor( + private workspaceFolder: Uri, + private readonly inDeprecatePythonPathExperiment: boolean, + private readonly workspaceService: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService + ) {} public async updatePythonPath(pythonPath: string | undefined): Promise { const pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceFolder); - const pythonPathValue = pythonConfig.inspect('pythonPath'); + const pythonPathValue = this.inDeprecatePythonPathExperiment + ? this.interpreterPathService.inspect(this.workspaceFolder) + : pythonConfig.inspect('pythonPath')!; if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { return; @@ -15,6 +23,14 @@ export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdat if (pythonPath && pythonPath.startsWith(this.workspaceFolder.fsPath)) { pythonPath = path.relative(this.workspaceFolder.fsPath, pythonPath); } - await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + if (this.inDeprecatePythonPathExperiment) { + await this.interpreterPathService.update( + this.workspaceFolder, + ConfigurationTarget.WorkspaceFolder, + pythonPath + ); + } else { + await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + } } } diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts index 3948872c96a4..198eaf5f59cb 100644 --- a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -1,20 +1,32 @@ import * as path from 'path'; -import { Uri } from 'vscode'; +import { ConfigurationTarget, Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; +import { IInterpreterPathService } from '../../../common/types'; import { IPythonPathUpdaterService } from '../types'; export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private workspace: Uri, private readonly workspaceService: IWorkspaceService) {} - public async updatePythonPath(pythonPath: string): Promise { + constructor( + private workspace: Uri, + private readonly inDeprecatePythonPathExperiment: boolean, + private readonly workspaceService: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService + ) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { const pythonConfig = this.workspaceService.getConfiguration('python', this.workspace); - const pythonPathValue = pythonConfig.inspect('pythonPath'); + const pythonPathValue = this.inDeprecatePythonPathExperiment + ? this.interpreterPathService.inspect(this.workspace) + : pythonConfig.inspect('pythonPath')!; if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { return; } - if (pythonPath.startsWith(this.workspace.fsPath)) { + if (pythonPath && pythonPath.startsWith(this.workspace.fsPath)) { pythonPath = path.relative(this.workspace.fsPath, pythonPath); } - await pythonConfig.update('pythonPath', pythonPath, false); + if (this.inDeprecatePythonPathExperiment) { + await this.interpreterPathService.update(this.workspace, ConfigurationTarget.Workspace, pythonPath); + } else { + await pythonConfig.update('pythonPath', pythonPath, false); + } } } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 6dbf50c29451..d1812b172b2f 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,8 +1,10 @@ import { inject, injectable } from 'inversify'; -import { Disposable, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; +import { Disposable, OutputChannel, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import '../../common/extensions'; -import { IDisposableRegistry, IPathUtils, Resource } from '../../common/types'; +import { IDisposableRegistry, IOutputChannel, IPathUtils, Resource } from '../../common/types'; +import { Interpreters } from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; import { IInterpreterAutoSelectionService } from '../autoSelection/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, PythonInterpreter } from '../contracts'; @@ -18,8 +20,9 @@ export class InterpreterDisplay implements IInterpreterDisplay { private currentlySelectedInterpreterPath?: string; private currentlySelectedWorkspaceFolder: Resource; private readonly autoSelection: IInterpreterAutoSelectionService; + private interpreterPath: string | undefined; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.helper = serviceContainer.get(IInterpreterHelper); this.workspaceService = serviceContainer.get(IWorkspaceService); this.pathUtils = serviceContainer.get(IPathUtils); @@ -61,10 +64,16 @@ export class InterpreterDisplay implements IInterpreterDisplay { this.currentlySelectedWorkspaceFolder = workspaceFolder; if (interpreter) { this.statusBar.color = ''; - this.statusBar.tooltip = this.pathUtils.getDisplayName( - interpreter.path, - workspaceFolder ? workspaceFolder.fsPath : undefined - ); + this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); + if (this.interpreterPath !== interpreter.path) { + const output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + output.appendLine( + Interpreters.pythonInterpreterPath().format( + this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath) + ) + ); + this.interpreterPath = interpreter.path; + } this.statusBar.text = interpreter.displayName!; this.currentlySelectedInterpreterPath = interpreter.path; } else { diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index c8b961c3b7e7..33f08370bfc1 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { Disposable, Event, EventEmitter, Uri } from 'vscode'; import '../../client/common/extensions'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { DeprecatePythonPath } from '../common/experimentGroups'; import { traceError } from '../common/logger'; import { getArchitectureDisplayName } from '../common/platform/registry'; import { IFileSystem } from '../common/platform/types'; @@ -11,6 +12,8 @@ import { IPythonExecutionFactory } from '../common/process/types'; import { IConfigurationService, IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, IPersistentState, IPersistentStateFactory, Resource @@ -36,14 +39,27 @@ const EXPITY_DURATION = 24 * 60 * 60 * 1000; @injectable() export class InterpreterService implements Disposable, IInterpreterService { + public get hasInterpreters(): Promise { + return this.locator.hasInterpreters; + } + + public get onDidChangeInterpreter(): Event { + return this.didChangeInterpreterEmitter.event; + } + + public get onDidChangeInterpreterInformation(): Event { + return this.didChangeInterpreterInformation.event; + } + public _pythonPathSetting: string = ''; private readonly locator: IInterpreterLocatorService; private readonly persistentStateFactory: IPersistentStateFactory; private readonly configService: IConfigurationService; + private readonly interpreterPathService: IInterpreterPathService; + private readonly experiments: IExperimentsManager; private readonly didChangeInterpreterEmitter = new EventEmitter(); private readonly didChangeInterpreterInformation = new EventEmitter(); private readonly inMemoryCacheOfDisplayNames = new Map(); private readonly updatedInterpreters = new Set(); - private pythonPathSetting: string = ''; constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @@ -55,9 +71,8 @@ export class InterpreterService implements Disposable, IInterpreterService { ); this.persistentStateFactory = this.serviceContainer.get(IPersistentStateFactory); this.configService = this.serviceContainer.get(IConfigurationService); - } - public get hasInterpreters(): Promise { - return this.locator.hasInterpreters; + this.interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + this.experiments = this.serviceContainer.get(IExperimentsManager); } public async refresh(resource?: Uri) { @@ -69,17 +84,31 @@ 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)) + documentManager.onDidChangeActiveTextEditor(e => (e ? this.refresh(e.document.uri) : undefined)) ); const workspaceService = this.serviceContainer.get(IWorkspaceService); const pySettings = this.configService.getSettings(); - this.pythonPathSetting = pySettings.pythonPath; - const disposable = workspaceService.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('python.pythonPath', undefined)) { - this.onConfigChanged(); - } - }); - disposables.push(disposable); + this._pythonPathSetting = pySettings.pythonPath; + if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { + disposables.push( + this.interpreterPathService.onDidChange(i => { + this._onConfigChanged(i.uri); + }) + ); + } else { + const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders + ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) + : [undefined]; + const disposable = workspaceService.onDidChangeConfiguration(e => { + const workspaceUriIndex = workspacesUris.findIndex(uri => + e.affectsConfiguration('python.pythonPath', uri) + ); + const workspaceUri = workspaceUriIndex === -1 ? undefined : workspacesUris[workspaceUriIndex]; + this._onConfigChanged(workspaceUri); + }); + disposables.push(disposable); + } + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); } @captureTelemetry(EventName.PYTHON_INTERPRETER_DISCOVERY, { locator: 'all' }, true) @@ -87,8 +116,8 @@ export class InterpreterService implements Disposable, IInterpreterService { const interpreters = await this.locator.getInterpreters(resource); await Promise.all( interpreters - .filter((item) => !item.displayName) - .map(async (item) => { + .filter(item => !item.displayName) + .map(async item => { item.displayName = await this.getDisplayName(item, resource); // Keep information up to date with latest details. if (!item.cachedEntry) { @@ -105,14 +134,6 @@ export class InterpreterService implements Disposable, IInterpreterService { this.didChangeInterpreterInformation.dispose(); } - public get onDidChangeInterpreter(): Event { - return this.didChangeInterpreterEmitter.event; - } - - public get onDidChangeInterpreterInformation(): Event { - return this.didChangeInterpreterInformation.event; - } - public async getActiveInterpreter(resource?: Uri): Promise { const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); const pythonExecutionService = await pythonExecutionFactory.create({ resource }); @@ -155,7 +176,7 @@ export class InterpreterService implements Disposable, IInterpreterService { // This is the preferred approach, hence the delay in option 1. const option2 = (async () => { const interpreters = await this.getInterpreters(resource); - const found = interpreters.find((i) => fs.arePathsSame(i.path, pythonPath)); + const found = interpreters.find(i => fs.arePathsSame(i.path, pythonPath)); if (found) { // Cache the interpreter info, only if we get the data from interpretr list. // tslint:disable-next-line:no-any @@ -229,10 +250,20 @@ export class InterpreterService implements Disposable, IInterpreterService { } return store; } + public _onConfigChanged = (resource?: Uri) => { + // Check if we actually changed our python path + const pySettings = this.configService.getSettings(resource); + if (this._pythonPathSetting === '' || this._pythonPathSetting !== pySettings.pythonPath) { + this._pythonPathSetting = pySettings.pythonPath; + this.didChangeInterpreterEmitter.fire(); + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + interpreterDisplay.refresh().catch(ex => traceError('Python Extension: display.refresh', ex)); + } + }; protected async getInterepreterFileHash(pythonPath: string): Promise { return this.hashProviderFactory .create({ pythonPath }) - .then((provider) => provider.getInterpreterHash(pythonPath)); + .then(provider => provider.getInterpreterHash(pythonPath)); } protected async updateCachedInterpreterInformation(info: PythonInterpreter, resource: Resource): Promise { const key = JSON.stringify(info); @@ -281,16 +312,6 @@ export class InterpreterService implements Disposable, IInterpreterService { const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); } - private onConfigChanged = () => { - // Check if we actually changed our python path - const pySettings = this.configService.getSettings(); - if (this.pythonPathSetting !== pySettings.pythonPath) { - this.pythonPathSetting = pySettings.pythonPath; - this.didChangeInterpreterEmitter.fire(); - const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); - interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); - } - }; private async collectInterpreterDetails(pythonPath: string, resource: Uri | undefined) { const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index e4c137bc54b8..c84e78c10837 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -3,9 +3,16 @@ import { IWorkspaceService } from './common/application/types'; import { isTestExecution } from './common/constants'; +import { DeprecatePythonPath } from './common/experimentGroups'; import { traceError } from './common/logger'; import { ITerminalHelper } from './common/terminal/types'; -import { IConfigurationService, Resource } from './common/types'; +import { + IConfigurationService, + IExperimentsManager, + IInterpreterPathService, + InspectInterpreterSettingType, + Resource +} from './common/types'; import { AutoSelectionRule, IInterpreterAutoSelectionRule, @@ -72,9 +79,17 @@ function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceC return currentPythonPath === globalInterpreter.path; } -function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { +export function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { + const abExperiments = serviceContainer.get(IExperimentsManager); const workspaceService = serviceContainer.get(IWorkspaceService); - const settings = workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + let settings: InspectInterpreterSettingType; + if (abExperiments.inExperiment(DeprecatePythonPath.experiment)) { + settings = interpreterPathService.inspect(resource); + } else { + settings = workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; + } + abExperiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || (settings.workspaceValue && settings.workspaceValue !== 'python') || (settings.globalValue && settings.globalValue !== 'python') diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index e58b98ddc07b..afbdcd229b01 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -46,8 +46,6 @@ import { IPlatformData, LanguageServerType } from '../../client/activation/types'; -import { ActiveResourceService } from '../../client/common/application/activeResource'; -import { IActiveResourceService } from '../../client/common/application/types'; import { INugetRepository } from '../../client/common/nuget/types'; import { BANNER_NAME_DS_SURVEY, @@ -214,9 +212,6 @@ suite('Unit Tests - Language Server Activation Service Registry', () => { LanguageServerOutputChannel ) ).once(); - verify( - serviceManager.addSingleton(IActiveResourceService, ActiveResourceService) - ).once(); verify( serviceManager.addSingleton( IExtensionSingleActivationService, diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index 49523ed18fe0..fb5cc2b8ee76 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -5,9 +5,9 @@ // tslint:disable:max-func-body-length no-any max-classes-per-file -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import * as typemoq from 'typemoq'; -import { ConfigurationChangeEvent } from 'vscode'; +import { ConfigurationChangeEvent, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; import { InvalidMacPythonInterpreterDiagnostic, @@ -29,14 +29,23 @@ import { } from '../../../../client/application/diagnostics/types'; import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; import { IWorkspaceService } from '../../../../client/common/application/types'; +import { DeprecatePythonPath } from '../../../../client/common/experimentGroups'; import { IPlatformService } from '../../../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, + InterpreterConfigurationScope, + IPythonSettings, + Resource +} from '../../../../client/common/types'; import { sleep } from '../../../../client/common/utils/async'; import { noop } from '../../../../client/common/utils/misc'; import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; -suite('Application Diagnostics - Checks Python Interpreter', () => { +suite('Application Diagnostics - Checks Mac Python Interpreter', () => { let diagnosticService: IDiagnosticsService; let messageHandler: typemoq.IMock>; let commandFactory: typemoq.IMock; @@ -51,7 +60,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { serviceContainer = typemoq.Mock.ofType(); messageHandler = typemoq.Mock.ofType>(); serviceContainer - .setup((s) => + .setup(s => s.get( typemoq.It.isValue(IDiagnosticHandlerService), typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) @@ -60,33 +69,31 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .returns(() => messageHandler.object); commandFactory = typemoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) .returns(() => commandFactory.object); settings = typemoq.Mock.ofType(); - settings.setup((s) => s.pythonPath).returns(() => pythonPath); + settings.setup(s => s.pythonPath).returns(() => pythonPath); const configService = typemoq.Mock.ofType(); - configService.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + configService.setup(c => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .setup(s => s.get(typemoq.It.isValue(IConfigurationService))) .returns(() => configService.object); interpreterService = typemoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IInterpreterService))) + .setup(s => s.get(typemoq.It.isValue(IInterpreterService))) .returns(() => interpreterService.object); platformService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))).returns(() => platformService.object); helper = typemoq.Mock.ofType(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); filterService = typemoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object); platformService - .setup((p) => p.isMac) + .setup(p => p.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); return serviceContainer.object; @@ -113,7 +120,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ]) { const diagnostic = typemoq.Mock.ofType(); diagnostic - .setup((d) => d.code) + .setup(d => d.code) .returns(() => code) .verifiable(typemoq.Times.atLeastOnce()); @@ -125,7 +132,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { const diagnostic = typemoq.Mock.ofType(); diagnostic - .setup((d) => d.code) + .setup(d => d.code) .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); @@ -136,7 +143,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { test('Should return empty diagnostics if not a Mac', async () => { platformService.reset(); platformService - .setup((p) => p.isMac) + .setup(p => p.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); @@ -146,7 +153,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { }); test('Should return empty diagnostics if installer check is disabled', async () => { settings - .setup((s) => s.disableInstallationChecks) + .setup(s => s.disableInstallationChecks) .returns(() => true) .verifiable(typemoq.Times.once()); @@ -157,25 +164,25 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { }); test('Should return empty diagnostics if there are interpreters, one is selected, and platform is not mac', async () => { settings - .setup((s) => s.disableInstallationChecks) + .setup(s => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.hasInterpreters) + .setup(i => i.hasInterpreters) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .setup(i => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([{} as any])) .verifiable(typemoq.Times.never()); interpreterService - .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) .verifiable(typemoq.Times.once()); platformService - .setup((i) => i.isMac) + .setup(i => i.isMac) .returns(() => false) .verifiable(typemoq.Times.once()); @@ -187,29 +194,29 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { }); test('Should return empty diagnostics if there are interpreters, platform is mac and selected interpreter is not default mac interpreter', async () => { settings - .setup((s) => s.disableInstallationChecks) + .setup(s => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.hasInterpreters) + .setup(i => i.hasInterpreters) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .setup(i => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([{} as any])) .verifiable(typemoq.Times.never()); interpreterService - .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) .verifiable(typemoq.Times.once()); platformService - .setup((i) => i.isMac) + .setup(i => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isAny())) + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isAny())) .returns(() => false) .verifiable(typemoq.Times.once()); @@ -222,25 +229,25 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { }); test('Should return diagnostic if there are no other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { settings - .setup((s) => s.disableInstallationChecks) + .setup(s => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .setup(i => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([{ path: pythonPath } as any, { path: pythonPath } as any])) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) .verifiable(typemoq.Times.once()); platformService - .setup((i) => i.isMac) + .setup(i => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); @@ -262,11 +269,11 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { test('Should return diagnostic if there are other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { const nonMacStandardInterpreter = 'Non Mac Std Interpreter'; settings - .setup((s) => s.disableInstallationChecks) + .setup(s => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .setup(i => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([ { path: pythonPath } as any, @@ -276,19 +283,19 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ) .verifiable(typemoq.Times.once()); platformService - .setup((i) => i.isMac) + .setup(i => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); helper - .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) + .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) .returns(() => false) .verifiable(typemoq.Times.atLeastOnce()); interpreterService - .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) @@ -318,12 +325,12 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { const cmdIgnore = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler - .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); commandFactory - .setup((f) => + .setup(f => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -334,7 +341,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .returns(() => cmd) .verifiable(typemoq.Times.once()); commandFactory - .setup((f) => + .setup(f => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -366,12 +373,12 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { const cmdIgnore = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler - .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); commandFactory - .setup((f) => + .setup(f => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -383,7 +390,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .returns(() => cmdLearn) .verifiable(typemoq.Times.once()); commandFactory - .setup((f) => + .setup(f => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -395,7 +402,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .returns(() => cmdDownload) .verifiable(typemoq.Times.once()); commandFactory - .setup((f) => + .setup(f => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -425,7 +432,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ); filterService - .setup((f) => + .setup(f => f.shouldIgnoreDiagnostic( typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic) ) @@ -433,10 +440,10 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); commandFactory - .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .setup(f => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); messageHandler - .setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .setup(f => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); await diagnosticService.handle([diagnostic]); @@ -465,7 +472,14 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { onDidChangeConfiguration: (cb: (e: ConfigurationChangeEvent) => Promise) => (callbackHandler = cb) } as any; const serviceContainerObject = createContainer(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); diagnosticService = new (class extends InvalidMacPythonInterpreterService { protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; @@ -479,7 +493,13 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { let invoked = false; const workspaceService = { onDidChangeConfiguration: noop } as any; const serviceContainerObject = createContainer(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); diagnosticService = new (class extends InvalidMacPythonInterpreterService { protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; @@ -488,22 +508,28 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { expect(invoked).to.be.equal(false, 'Not invoked'); }); - test('Diagnostics are checked when path changes', async () => { + test('Diagnostics are checked with Config change event uri when path changes and event is passed', async () => { const event = typemoq.Mock.ofType(); const workspaceService = typemoq.Mock.ofType(); const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; workspaceService - .setup((w) => w.hasWorkspaceFolders) + .setup(w => w.hasWorkspaceFolders) .returns(() => true) .verifiable(typemoq.Times.once()); workspaceService - .setup((w) => w.workspaceFolders) + .setup(w => w.workspaceFolders) .returns(() => [{ uri: '' }] as any) .verifiable(typemoq.Times.once()); serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { constructor( arg1: IServiceContainer, @@ -527,7 +553,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ); event - .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); @@ -540,22 +566,114 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { await sleep(100); expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); }); + + test('Diagnostics are checked with correct interpreter config uri when path changes and only config uri is passed', async () => { + const configUri = Uri.parse('i'); + const interpreterConfigurationScope = typemoq.Mock.ofType(); + interpreterConfigurationScope.setup(i => i.uri).returns(() => Uri.parse('i')); + const workspaceService = typemoq.Mock.ofType(); + const serviceContainerObject = createContainer(); + let diagnoseInvocationCount = 0; + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor( + arg1: IServiceContainer, + arg2: IInterpreterService, + arg3: IPlatformService, + arg4: IInterpreterHelper + ) { + super(arg1, arg2, [], arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = (e?: ConfigurationChangeEvent, i?: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e, i); + public diagnose(resource: Resource): Promise { + diagnoseInvocationCount += 1; + assert.deepEqual(resource, configUri); + return Promise.resolve(); + } + })( + serviceContainerObject, + typemoq.Mock.ofType().object, + typemoq.Mock.ofType().object, + typemoq.Mock.ofType().object + ); + + await diagnosticSvc.onDidChangeConfigurationEx(undefined, interpreterConfigurationScope.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + + await diagnosticSvc.onDidChangeConfigurationEx(undefined, interpreterConfigurationScope.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); + }); + + test('Diagnostics throws error when none of config uri or config change event uri is passed', async () => { + const workspaceService = typemoq.Mock.ofType(); + const serviceContainerObject = createContainer(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor( + arg1: IServiceContainer, + arg2: IInterpreterService, + arg3: IPlatformService, + arg4: IInterpreterHelper + ) { + super(arg1, arg2, [], arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = (e?: ConfigurationChangeEvent, i?: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e, i); + })( + serviceContainerObject, + typemoq.Mock.ofType().object, + typemoq.Mock.ofType().object, + typemoq.Mock.ofType().object + ); + + await expect(diagnosticSvc.onDidChangeConfigurationEx(undefined, undefined)).to.eventually.be.rejectedWith( + Error + ); + }); + test('Diagnostics are checked and throttled when path changes', async () => { const event = typemoq.Mock.ofType(); const workspaceService = typemoq.Mock.ofType(); const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; workspaceService - .setup((w) => w.hasWorkspaceFolders) + .setup(w => w.hasWorkspaceFolders) .returns(() => true) .verifiable(typemoq.Times.once()); workspaceService - .setup((w) => w.workspaceFolders) + .setup(w => w.workspaceFolders) .returns(() => [{ uri: '' }] as any) .verifiable(typemoq.Times.once()); serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { constructor( arg1: IServiceContainer, @@ -579,7 +697,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ); event - .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); @@ -592,5 +710,39 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { event.verifyAll(); expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); }); + + test('Ensure event Handler is registered correctly if in Deprecate Python path experiment', async () => { + let interpreterPathServiceHandler: Function; + const workspaceService = { onDidChangeConfiguration: noop } as any; + const serviceContainerObject = createContainer(); + + const interpreterPathService = typemoq.Mock.ofType(); + interpreterPathService + .setup(d => d.onDidChange(typemoq.It.isAny(), typemoq.It.isAny())) + .callback(cb => (interpreterPathServiceHandler = cb)) + .returns(() => { + return { dispose: noop }; + }); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); + + serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + const experiments = typemoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + diagnosticService = new (class extends InvalidMacPythonInterpreterService {})( + serviceContainerObject, + undefined as any, + [], + undefined as any, + undefined as any + ); + + expect(interpreterPathServiceHandler!).to.not.equal(undefined, 'Handler not set'); + }); }); }); diff --git a/src/test/common.ts b/src/test/common.ts index 9c8d4f693f64..b6a6fb43ad56 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -118,6 +118,24 @@ export async function restorePythonPathInWorkspaceRoot() { return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, PYTHON_PATH); } +export async function setGlobalInterpreterPath(pythonPath: string) { + return retryAsync(setGlobalPathToInterpreter)(pythonPath); +} + +export const resetGlobalInterpreterPathSetting = async () => retryAsync(restoreGlobalInterpreterPathSetting)(); + +async function restoreGlobalInterpreterPathSetting(): Promise { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await pythonConfig.update('defaultInterpreterPath', undefined, true); + await disposePythonSettings(); +} +async function setGlobalPathToInterpreter(pythonPath?: string): Promise { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + await disposePythonSettings(); +} export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); export async function setAutoSaveDelayInWorkspaceRoot(delayinMS: number) { @@ -222,7 +240,10 @@ async function setPythonPathInWorkspace( async function restoreGlobalPythonPathSetting(): Promise { const vscode = require('vscode') as typeof import('vscode'); const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); - await pythonConfig.update('pythonPath', undefined, true); + await Promise.all([ + pythonConfig.update('pythonPath', undefined, true), + pythonConfig.update('defaultInterpreterPath', undefined, true) + ]); await disposePythonSettings(); } diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index 00261daa3237..da5d194afcb5 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -11,7 +11,10 @@ import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; import { PythonSettings } from '../../../client/common/configSettings'; +import { DeprecatePythonPath } from '../../../client/common/experimentGroups'; +import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; @@ -30,10 +33,17 @@ suite('Python Settings - pythonPath', () => { } } let configSettings: CustomPythonSettings; + let workspaceService: typemoq.IMock; + let experimentsManager: typemoq.IMock; + let interpreterPathService: typemoq.IMock; let pythonSettings: typemoq.IMock; setup(() => { pythonSettings = typemoq.Mock.ofType(); sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); + interpreterPathService = typemoq.Mock.ofType(); + experimentsManager = typemoq.Mock.ofType(); + workspaceService = typemoq.Mock.ofType(); + pythonSettings.setup(p => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); }); teardown(() => { if (configSettings) { @@ -46,7 +56,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); const pythonPath = 'This is the python Path'; pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -57,7 +67,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); const pythonPath = `~${path.sep}This is the python Path`; pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -69,7 +79,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(workspaceFolderUri, new MockAutoSelectionService()); const pythonPath = `.${path.sep}This is the python Path`; pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); @@ -83,7 +93,7 @@ suite('Python Settings - pythonPath', () => { const workspaceFolderToken = '${workspaceFolder}'; const pythonPath = `${workspaceFolderToken}${path.sep}This is the python Path`; pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -96,7 +106,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); const pythonPath = 'python'; pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -112,7 +122,7 @@ suite('Python Settings - pythonPath', () => { when(selectionService.setWorkspaceInterpreter(workspaceFolderUri, anything())).thenResolve(); configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); pythonSettings - .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => 'python') .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -120,4 +130,64 @@ suite('Python Settings - pythonPath', () => { expect(configSettings.pythonPath).to.be.equal(pythonPath); verify(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).once(); }); + test('If user is in Deprecate Python Path experiment, use the new API to fetch Python Path', () => { + const resource = Uri.parse('a'); + configSettings = new CustomPythonSettings( + resource, + new MockAutoSelectionService(), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object + ); + const pythonPath = 'This is the new API python Path'; + pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))).verifiable(typemoq.Times.never()); + experimentsManager + .setup(e => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => true) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + interpreterPathService + .setup(i => i.get(resource)) + .returns(() => pythonPath) + .verifiable(typemoq.Times.once()); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + }); + test('If user is not in Deprecate Python Path experiment, use the settings to fetch Python Path', () => { + const resource = Uri.parse('a'); + configSettings = new CustomPythonSettings( + resource, + new MockAutoSelectionService(), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object + ); + const pythonPath = 'This is the settings python Path'; + pythonSettings + .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeastOnce()); + experimentsManager + .setup(e => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => false) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + interpreterPathService.setup(i => i.get(resource)).verifiable(typemoq.Times.never()); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + }); }); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index a1ab05c7f15b..0f7e7ad55ac8 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -49,6 +49,7 @@ suite('Python Settings', async () => { config = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); expected = new CustomPythonSettings(undefined, new MockAutoSelectionService()); settings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + expected.defaultInterpreterPath = 'python'; }); teardown(() => { @@ -64,19 +65,20 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', - 'insidersChannel' + 'insidersChannel', + 'defaultInterpreterPath' ]) { config - .setup((c) => c.get(name)) + .setup(c => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } if (sourceSettings.jediEnabled) { - config.setup((c) => c.get('jediPath')).returns(() => sourceSettings.jediPath); + config.setup(c => c.get('jediPath')).returns(() => sourceSettings.jediPath); } for (const name of ['venvFolders']) { config - .setup((c) => c.get(name)) + .setup(c => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } @@ -84,42 +86,42 @@ suite('Python Settings', async () => { // boolean settings for (const name of ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer']) { config - .setup((c) => c.get(name, true)) + .setup(c => c.get(name, true)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } for (const name of ['disableInstallationCheck', 'globalModuleInstallation']) { config - .setup((c) => c.get(name)) + .setup(c => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } // number settings if (sourceSettings.jediEnabled) { - config.setup((c) => c.get('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); + config.setup(c => c.get('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); } // Language server type settings - config.setup((c) => c.get('languageServer')).returns(() => sourceSettings.languageServer); + config.setup(c => c.get('languageServer')).returns(() => sourceSettings.languageServer); // "any" settings // tslint:disable-next-line:no-any - config.setup((c) => c.get('devOptions')).returns(() => sourceSettings.devOptions); + config.setup(c => c.get('devOptions')).returns(() => sourceSettings.devOptions); // complex settings - config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('analysis')).returns(() => sourceSettings.analysis); - config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); - config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); - config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); + config.setup(c => c.get('linting')).returns(() => sourceSettings.linting); + config.setup(c => c.get('analysis')).returns(() => sourceSettings.analysis); + config.setup(c => c.get('sortImports')).returns(() => sourceSettings.sortImports); + config.setup(c => c.get('formatting')).returns(() => sourceSettings.formatting); + config.setup(c => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config - .setup((c) => c.get('workspaceSymbols')) + .setup(c => c.get('workspaceSymbols')) .returns(() => sourceSettings.workspaceSymbols); - config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); - config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); - config.setup((c) => c.get('dataScience')).returns(() => sourceSettings.datascience); - config.setup((c) => c.get('experiments')).returns(() => sourceSettings.experiments); + config.setup(c => c.get('testing')).returns(() => sourceSettings.testing); + config.setup(c => c.get('terminal')).returns(() => sourceSettings.terminal); + config.setup(c => c.get('dataScience')).returns(() => sourceSettings.datascience); + config.setup(c => c.get('experiments')).returns(() => sourceSettings.experiments); } function testIfValueIsUpdated(settingName: string, value: any) { @@ -136,23 +138,30 @@ suite('Python Settings', async () => { } suite('String settings', async () => { - ['pythonPath', 'venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'insidersChannel'].forEach( - async (settingName) => { - testIfValueIsUpdated(settingName, 'stringValue'); - } - ); + [ + 'pythonPath', + 'venvPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'insidersChannel', + 'defaultInterpreterPath' + ].forEach(async settingName => { + testIfValueIsUpdated(settingName, 'stringValue'); + }); }); suite('Boolean settings', async () => { ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( - async (settingName) => { + async settingName => { testIfValueIsUpdated(settingName, true); } ); }); suite('Number settings', async () => { - ['jediMemoryLimit'].forEach(async (settingName) => { + ['jediMemoryLimit'].forEach(async settingName => { testIfValueIsUpdated(settingName, 1001); }); }); @@ -162,7 +171,7 @@ suite('Python Settings', async () => { expected.condaPath = 'spam'; initializeConfig(expected); config - .setup((c) => c.get('condaPath')) + .setup(c => c.get('condaPath')) .returns(() => expected.condaPath) .verifiable(TypeMoq.Times.once()); @@ -177,7 +186,7 @@ suite('Python Settings', async () => { expected.condaPath = path.join('~', 'anaconda3', 'bin', 'conda'); initializeConfig(expected); config - .setup((c) => c.get('condaPath')) + .setup(c => c.get('condaPath')) .returns(() => expected.condaPath) .verifiable(TypeMoq.Times.once()); @@ -197,7 +206,7 @@ suite('Python Settings', async () => { }; initializeConfig(expected); config - .setup((c) => c.get('experiments')) + .setup(c => c.get('experiments')) .returns(() => expected.experiments) .verifiable(TypeMoq.Times.once()); @@ -228,7 +237,7 @@ suite('Python Settings', async () => { expected.formatting.blackPath = 'spam'; initializeConfig(expected); config - .setup((c) => c.get('formatting')) + .setup(c => c.get('formatting')) .returns(() => expected.formatting) .verifiable(TypeMoq.Times.once()); @@ -255,7 +264,7 @@ suite('Python Settings', async () => { expected.formatting.blackPath = 'spam'; initializeConfig(expected); config - .setup((c) => c.get('formatting')) + .setup(c => c.get('formatting')) .returns(() => expected.formatting) .verifiable(TypeMoq.Times.once()); @@ -310,7 +319,7 @@ suite('Python Settings', async () => { }; initializeConfig(expected); config - .setup((c) => c.get('experiments')) + .setup(c => c.get('experiments')) .returns(() => expected.experiments) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/common/configuration/service.unit.test.ts b/src/test/common/configuration/service.unit.test.ts new file mode 100644 index 000000000000..8238e6702149 --- /dev/null +++ b/src/test/common/configuration/service.unit.test.ts @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { DeprecatePythonPath } from '../../../client/common/experimentGroups'; +import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Configuration Service', () => { + const resource = Uri.parse('a'); + let workspaceService: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + let experimentsManager: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; + let configService: ConfigurationService; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + workspaceService + .setup(w => w.getWorkspaceFolder(resource)) + .returns(() => ({ + uri: resource, + index: 0, + name: '0' + })); + interpreterPathService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + serviceContainer.setup(s => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup(s => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + serviceContainer.setup(s => s.get(IExperimentsManager)).returns(() => experimentsManager.object); + configService = new ConfigurationService(serviceContainer.object); + }); + + function setupConfigProvider(): TypeMoq.IMock { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService + .setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + test('Do not update global settings if global value is already equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); + workspaceConfig + .setup(w => w.update('setting', 'globalValue', ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting('setting', 'globalValue', resource, ConfigurationTarget.Global); + + workspaceConfig.verifyAll(); + }); + + test('Update global settings if global value is not equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); + workspaceConfig + .setup(w => w.update('setting', 'newGlobalValue', ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting('setting', 'newGlobalValue', resource, ConfigurationTarget.Global); + + workspaceConfig.verifyAll(); + }); + + test('Do not update workspace settings if workspace value is already equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); + workspaceConfig + .setup(w => w.update('setting', 'workspaceValue', ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting('setting', 'workspaceValue', resource, ConfigurationTarget.Workspace); + + workspaceConfig.verifyAll(); + }); + + test('Update workspace settings if workspace value is not equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); + workspaceConfig + .setup(w => w.update('setting', 'newWorkspaceValue', ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting('setting', 'newWorkspaceValue', resource, ConfigurationTarget.Workspace); + + workspaceConfig.verifyAll(); + }); + + test('Do not update workspace folder settings if workspace folder value is already equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup(w => w.inspect('setting')) + // tslint:disable-next-line: no-any + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); + workspaceConfig + .setup(w => w.update('setting', 'workspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting( + 'setting', + 'workspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder + ); + + workspaceConfig.verifyAll(); + }); + + test('Update workspace folder settings if workspace folder value is not equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup(w => w.inspect('setting')) + // tslint:disable-next-line: no-any + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); + workspaceConfig + .setup(w => w.update('setting', 'newWorkspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting( + 'setting', + 'newWorkspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder + ); + + workspaceConfig.verifyAll(); + }); + + test('If in Deprecate PythonPath experiment & setting to update is `python.pythonPath`, update settings using new API if stored value is not equal to the new value', async () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + interpreterPathService + .setup(w => w.inspect(resource)) + // tslint:disable-next-line: no-any + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); + interpreterPathService + .setup(w => w.update(resource, ConfigurationTarget.WorkspaceFolder, 'newWorkspaceFolderValue')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting( + 'pythonPath', + 'newWorkspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder + ); + + interpreterPathService.verifyAll(); + }); +}); diff --git a/src/test/common/experiments.unit.test.ts b/src/test/common/experiments.unit.test.ts index ee95c98c3265..6d412d044b7b 100644 --- a/src/test/common/experiments.unit.test.ts +++ b/src/test/common/experiments.unit.test.ts @@ -66,8 +66,8 @@ suite('A/B experiments', () => { experiments = TypeMoq.Mock.ofType(); const settings = mock(PythonSettings); when(settings.experiments).thenReturn(experiments.object); - experiments.setup((e) => e.optInto).returns(() => []); - experiments.setup((e) => e.optOutFrom).returns(() => []); + experiments.setup(e => e.optInto).returns(() => []); + experiments.setup(e => e.optOutFrom).returns(() => []); when(configurationService.getSettings(undefined)).thenReturn(instance(settings)); fs = mock(FileSystem); when( @@ -114,7 +114,7 @@ suite('A/B experiments', () => { test('Initializing experiments does not download experiments if storage is valid and contains experiments', async () => { isDownloadedStorageValid - .setup((n) => n.value) + .setup(n => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); @@ -126,15 +126,15 @@ suite('A/B experiments', () => { test('If storage has expired, initializing experiments downloads the experiments, but does not store them if they are invalid or incomplete', async () => { const abExperiments = [{ name: 'experiment1', salt: 'salt', max: 100 }]; isDownloadedStorageValid - .setup((n) => n.value) + .setup(n => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup((n) => n.updateValue(true)) + .setup(n => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); downloadedExperimentsStorage - .setup((n) => n.updateValue(abExperiments)) + .setup(n => n.updateValue(abExperiments)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -146,15 +146,15 @@ suite('A/B experiments', () => { test('If storage has expired, initializing experiments downloads the experiments, and stores them if they are valid', async () => { isDownloadedStorageValid - .setup((n) => n.value) + .setup(n => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup((n) => n.updateValue(true)) + .setup(n => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -165,15 +165,15 @@ suite('A/B experiments', () => { test('If downloading experiments fails with error, the storage is left as it is', async () => { isDownloadedStorageValid - .setup((n) => n.value) + .setup(n => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup((n) => n.updateValue(true)) + .setup(n => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); downloadedExperimentsStorage - .setup((n) => n.updateValue(anything())) + .setup(n => n.updateValue(anything())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -191,7 +191,7 @@ suite('A/B experiments', () => { const initializeInBackground = sinon.stub(ExperimentsManager.prototype, 'initializeInBackground'); initializeInBackground.callsFake(() => Promise.resolve()); experiments - .setup((e) => e.enabled) + .setup(e => e.enabled) .returns(() => enabled) .verifiable(TypeMoq.Times.atLeastOnce()); @@ -220,10 +220,6 @@ suite('A/B experiments', () => { async function testEnablingExperimentsToCheckIfInExperiment(enabled: boolean) { const sendTelemetry = sinon.stub(ExperimentsManager.prototype, 'sendTelemetryIfInExperiment'); sendTelemetry.callsFake((_: string) => noop()); - experiments - .setup((e) => e.enabled) - .returns(() => enabled) - .verifiable(TypeMoq.Times.atLeastOnce()); expManager = new ExperimentsManager( instance(persistentStateFactory), @@ -234,6 +230,8 @@ suite('A/B experiments', () => { instance(fs), instance(configurationService) ); + + expManager._enabled = enabled; expManager.userExperiments.push({ name: 'this should be in experiment', max: 0, min: 0, salt: '' }); // If experiments are disabled, then `inExperiment` will return false & vice versa. @@ -306,15 +304,15 @@ suite('A/B experiments', () => { test('Ensure experiment storage is updated to contain the latest downloaded experiments', async () => { downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) .verifiable(TypeMoq.Times.atLeastOnce()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -341,20 +339,20 @@ suite('A/B experiments', () => { ); downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -382,16 +380,16 @@ suite('A/B experiments', () => { ); downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); @@ -418,11 +416,11 @@ suite('A/B experiments', () => { instance(configurationService) ); downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -437,11 +435,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve(fileContent); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -469,11 +467,11 @@ suite('A/B experiments', () => { instance(configurationService) ); downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -488,11 +486,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve(fileContent); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -525,11 +523,11 @@ suite('A/B experiments', () => { }); test('If checking the existence of config file fails', async () => { downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -538,11 +536,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve('fileContent'); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -556,11 +554,11 @@ suite('A/B experiments', () => { test('If reading config file fails', async () => { downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -569,11 +567,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenThrow(error); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -587,11 +585,11 @@ suite('A/B experiments', () => { test('If config file does not exist', async () => { downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -599,11 +597,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve('fileContent'); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -617,11 +615,11 @@ suite('A/B experiments', () => { test('If parsing file or updating storage fails', async () => { downloadedExperimentsStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup((n) => n.updateValue(undefined)) + .setup(n => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -636,11 +634,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve(fileContent); experimentStorage - .setup((n) => n.value) + .setup(n => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.reject(error)) .verifiable(TypeMoq.Times.once()); @@ -676,7 +674,7 @@ suite('A/B experiments', () => { } ]; - testsForInExperiment.forEach((testParams) => { + testsForInExperiment.forEach(testParams => { test(testParams.testName, async () => { expManager.userExperiments = testParams.userExperiments; expect(expManager.inExperiment(testParams.experimentName)).to.equal( @@ -718,7 +716,7 @@ suite('A/B experiments', () => { ]; suite('Function IsUserInRange()', () => { - testsForIsUserInRange.forEach((testParams) => { + testsForIsUserInRange.forEach(testParams => { test(testParams.testName, async () => { when(appEnvironment.machineId).thenReturn('101'); if (testParams.machineIdError) { @@ -866,9 +864,9 @@ suite('A/B experiments', () => { ]; suite('Function populateUserExperiments', async () => { - testsForPopulateUserExperiments.forEach((testParams) => { + testsForPopulateUserExperiments.forEach(testParams => { test(testParams.testName, async () => { - experimentStorage.setup((n) => n.value).returns(() => testParams.experimentStorageValue); + experimentStorage.setup(n => n.value).returns(() => testParams.experimentStorageValue); when(appEnvironment.machineId).thenReturn('101'); if (testParams.hash) { when(crypto.createHash(anything(), 'number', anything())).thenReturn(testParams.hash); @@ -934,7 +932,7 @@ suite('A/B experiments', () => { ]; suite('Function areExperimentsValid()', () => { - testsForAreExperimentsValid.forEach((testParams) => { + testsForAreExperimentsValid.forEach(testParams => { test(testParams.testName, () => { expect(expManager.areExperimentsValid(testParams.experiments as any)).to.equal( testParams.expectedResult @@ -1015,15 +1013,15 @@ suite('A/B experiments', () => { test('If storage as parameter is passed in as argument to function downloadAndStoreExperiments(), download experiments into that storage', async () => { downloadedExperimentsStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); experimentStorage - .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .setup(n => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup((n) => n.updateValue(true)) + .setup(n => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); when(httpClient.getJSON(configUri, false)).thenResolve([ diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 84789ca472fd..1d1aa4b63d56 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -2,9 +2,49 @@ import * as path from 'path'; import { instance, mock } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ClipboardService } from '../../client/common/application/clipboard'; +import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; +import { CustomEditorService } from '../../client/common/application/customEditorService'; +import { DebugService } from '../../client/common/application/debugService'; +import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + ICustomEditorService, + IDebugService, + IDocumentManager, + ILiveShareApi, + IWorkspaceService +} from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { ConfigurationService } from '../../client/common/configuration/service'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EditorUtils } from '../../client/common/editor'; +import { ExperimentsManager } from '../../client/common/experiments'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from '../../client/common/insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../client/common/insidersBuild/types'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { @@ -22,19 +62,56 @@ import { IProductPathService, IProductService } from '../../client/common/installer/types'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; +import { FileDownloader } from '../../client/common/net/fileDownloader'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { NugetService } from '../../client/common/nuget/nugetService'; +import { INugetService } from '../../client/common/nuget/types'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; import { ProcessLogger } from '../../client/common/process/logger'; import { IProcessLogger, IProcessServiceFactory } from '../../client/common/process/types'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalServiceFactory } from '../../client/common/terminal/factory'; import { TerminalHelper } from '../../client/common/terminal/helper'; -import { ITerminalHelper } from '../../client/common/terminal/types'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalServiceFactory, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, IConfigurationService, + ICryptoUtils, ICurrentProcess, + IEditorUtils, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IFileDownloader, + IHttpClient, IInstaller, + IInterpreterPathService, IPathUtils, IPersistentStateFactory, + IRandom, IsWindows, ModuleNamePurpose, Product, @@ -42,12 +119,18 @@ import { } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; import { getNamesAndValues } from '../../client/common/utils/enum'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Random } from '../../client/common/utils/random'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { INotebookExecutionLogger } from '../../client/datascience/types'; import { ICondaService } from '../../client/interpreter/contracts'; import { CondaService } from '../../client/interpreter/locators/services/condaService'; import { InterpreterHashProvider } from '../../client/interpreter/locators/services/hashProvider'; import { InterpeterHashProviderFactory } from '../../client/interpreter/locators/services/hashProviderFactory'; import { InterpreterFilter } from '../../client/interpreter/locators/services/interpreterFilter'; import { WindowsStoreInterpreter } from '../../client/interpreter/locators/services/windowsStoreInterpreter'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; import { rootWorkspaceUri, updateSetting } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; @@ -146,6 +229,100 @@ suite('Installer', () => { InterpeterHashProviderFactory ); ioc.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); + + ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); + ioc.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); + ioc.serviceManager.addSingleton(IExtensions, Extensions); + ioc.serviceManager.addSingleton(IRandom, Random); + ioc.serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); + ioc.serviceManager.addSingleton(IClipboard, ClipboardService); + ioc.serviceManager.addSingleton(IDocumentManager, DocumentManager); + ioc.serviceManager.addSingleton(IDebugService, DebugService); + ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); + ioc.serviceManager.addSingleton(IBrowserService, BrowserService); + ioc.serviceManager.addSingleton(IHttpClient, HttpClient); + ioc.serviceManager.addSingleton(IFileDownloader, FileDownloader); + ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); + ioc.serviceManager.addSingleton(INugetService, NugetService); + ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); + ioc.serviceManager.addSingleton( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler + ); + ioc.serviceManager.addSingleton(ILiveShareApi, LiveShareApi); + ioc.serviceManager.addSingleton(ICryptoUtils, CryptoUtils); + ioc.serviceManager.addSingleton(IExperimentsManager, ExperimentsManager); + + ioc.serviceManager.addSingleton(ITerminalHelper, TerminalHelper); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv + ); + ioc.serviceManager.addSingleton( + IFeatureDeprecationManager, + FeatureDeprecationManager + ); + + ioc.serviceManager.addSingleton(IAsyncDisposableRegistry, AsyncDisposableRegistry); + ioc.serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); + ioc.serviceManager.addSingleton(IImportTracker, ImportTracker); + ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + ioc.serviceManager.addBinding(IImportTracker, INotebookExecutionLogger); + ioc.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); + ioc.serviceManager.addSingleton(IShellDetector, SettingsShellDetector); + ioc.serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); + ioc.serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); + ioc.serviceManager.addSingleton(IInsiderExtensionPrompt, InsidersExtensionPrompt); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + InsidersExtensionService + ); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler + ); + ioc.serviceManager.addSingleton(IExtensionChannelService, ExtensionChannelService); + ioc.serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionChannel.off + ); + ioc.serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersDailyChannelRule, + ExtensionChannel.daily + ); + ioc.serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersWeeklyChannelRule, + ExtensionChannel.weekly + ); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugSessionTelemetry + ); + ioc.serviceManager.addSingleton(ICustomEditorService, CustomEditorService); } async function resetSettings() { await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts new file mode 100644 index 000000000000..a0e6d5595dde --- /dev/null +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { + ConfigurationChangeEvent, + ConfigurationTarget, + Event, + EventEmitter, + Uri, + WorkspaceConfiguration +} from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { defaultInterpreterPathSetting, InterpreterPathService } from '../../client/common/interpreterPathService'; +import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; +import { createDeferred, sleep } from '../../client/common/utils/async'; + +suite('Interpreter Path Service', async () => { + let interpreterPathService: InterpreterPathService; + let persistentStateFactory: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + const resource = Uri.parse('a'); + const resourceOutsideOfWorkspace = Uri.parse('b'); + const interpreterPath = 'path/to/interpreter'; + setup(() => { + const event = TypeMoq.Mock.ofType>(); + workspaceService = TypeMoq.Mock.ofType(); + workspaceService + .setup(w => w.getWorkspaceFolder(resource)) + .returns(() => ({ + uri: resource, + name: 'Workspacefolder', + index: 0 + })); + workspaceService.setup(w => w.getWorkspaceFolder(resourceOutsideOfWorkspace)).returns(() => undefined); + persistentStateFactory = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.onDidChangeConfiguration).returns(() => event.object); + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + }); + + test('Global settings are correctly updated', async () => { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup(w => w.update('defaultInterpreterPath', interpreterPath, true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Global, interpreterPath); + + workspaceConfig.verifyAll(); + }); + + test('Workspace settings are correctly updated if a folder is directly opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup(w => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup(p => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Ensure the correct event is fired if Workspace settings are updated', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup(w => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object); + persistentState.setup(p => p.updateValue(interpreterPath)).returns(() => Promise.resolve()); + + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup(emitter => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.Workspace })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + _didChangeInterpreterEmitter.verifyAll(); + }); + + test('Workspace settings are correctly updated in case of multiroot folders', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedSettingKey = `WORKSPACE_INTERPRETER_PATH_${workspaceFileUri.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup(w => w.workspaceFile).returns(() => workspaceFileUri); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup(p => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Workspace folder settings are correctly updated in case of multiroot folders', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup(p => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Ensure the correct event is fired if Workspace folder settings are updated', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup(p => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup(emitter => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.WorkspaceFolder })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, interpreterPath); + + _didChangeInterpreterEmitter.verifyAll(); + }); + + test('Updating workspace settings throws error if no workspace is opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + persistentState + .setup(p => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const promise = interpreterPathService.update( + resourceOutsideOfWorkspace, + ConfigurationTarget.Workspace, + interpreterPath + ); + await expect(promise).to.eventually.be.rejectedWith(Error); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Updating workspace folder settings throws error if no workspace is opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + persistentState + .setup(p => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const promise = interpreterPathService.update( + resourceOutsideOfWorkspace, + ConfigurationTarget.Workspace, + interpreterPath + ); + await expect(promise).to.eventually.be.rejectedWith(Error); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Inspecting settings returns as expected if no workspace is opened', async () => { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup(w => w.inspect('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter' + // tslint:disable-next-line: no-any + } as any) + ); + const persistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + + const settings = interpreterPathService.inspect(resourceOutsideOfWorkspace); + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: undefined + }); + + persistentStateFactory.verifyAll(); + }); + + test('Inspecting settings returns as expected if a folder is directly opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const workspaceConfig = TypeMoq.Mock.ofType(); + // No workspace file is present if a folder is directly opened + workspaceService.setup(w => w.workspaceFile).returns(() => undefined); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup(w => w.inspect('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter' + // tslint:disable-next-line: no-any + } as any) + ); + const workspaceFolderPersistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + workspaceFolderPersistentState.setup(p => p.value).returns(() => 'workspaceFolderValue'); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceFolderValue' + }); + }); + + test('Inspecting settings returns as expected in case of multiroot folders', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedWorkspaceSettingKey = `WORKSPACE_INTERPRETER_PATH_${workspaceFileUri.fsPath}`; + const expectedWorkspaceFolderSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const workspaceConfig = TypeMoq.Mock.ofType(); + // A workspace file is present in case of multiroot workspace folders + workspaceService.setup(w => w.workspaceFile).returns(() => workspaceFileUri); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup(w => w.inspect('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter' + // tslint:disable-next-line: no-any + } as any) + ); + const workspaceFolderPersistentState = TypeMoq.Mock.ofType>(); + const workspacePersistentState = TypeMoq.Mock.ofType>(); + workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedWorkspaceFolderSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(expectedWorkspaceSettingKey, undefined)) + .returns(() => workspacePersistentState.object); + workspaceFolderPersistentState.setup(p => p.value).returns(() => 'workspaceFolderValue'); + workspacePersistentState.setup(p => p.value).returns(() => 'workspaceValue'); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue' + }); + }); + + test(`Getting setting value returns workspace folder value if it's defined`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue' + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceFolderValue'); + }); + + test(`Getting setting value returns workspace value if workspace folder value is 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: 'workspaceValue' + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceValue'); + }); + + test(`Getting setting value returns workspace value if workspace folder value is 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: 'workspaceValue' + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceValue'); + }); + + test(`Getting setting value returns global value if workspace folder & workspace value are 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: undefined + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('default/path/to/interpreter'); + }); + + test(`Getting setting value returns 'python' if all workspace folder, workspace, and global value are 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: undefined, + workspaceFolderValue: undefined, + workspaceValue: undefined + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('python'); + }); + + test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); + const event = TypeMoq.Mock.ofType(); + event + .setup(e => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup(emitter => emitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await interpreterPathService.onDidChangeConfiguration(event.object); + _didChangeInterpreterEmitter.verifyAll(); + event.verifyAll(); + }); + + test('If some other setting changed, no event is fired', async () => { + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); + const event = TypeMoq.Mock.ofType(); + event + .setup(e => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup(emitter => emitter.fire(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await interpreterPathService.onDidChangeConfiguration(event.object); + _didChangeInterpreterEmitter.verifyAll(); + event.verifyAll(); + }); + + test('Ensure on interpreter change captures the fired event with the correct arguments', async () => { + const deferred = createDeferred(); + const interpreterConfigurationScope = { uri: undefined, configTarget: ConfigurationTarget.Global }; + interpreterPathService.onDidChange(i => { + expect(i).to.equal(interpreterConfigurationScope); + deferred.resolve(true); + }); + interpreterPathService._didChangeInterpreterEmitter.fire(interpreterConfigurationScope); + const eventCaptured = await Promise.race([deferred.promise, sleep(1000).then(() => false)]); + expect(eventCaptured).to.equal(true, 'Event should be captured'); + }); +}); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index e0f46c007e32..ffca231c437e 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -6,14 +6,63 @@ import * as path from 'path'; import { SemVer } from 'semver'; import { instance, mock } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { ClipboardService } from '../../client/common/application/clipboard'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; +import { CustomEditorService } from '../../client/common/application/customEditorService'; +import { DebugService } from '../../client/common/application/debugService'; +import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + ICustomEditorService, + IDebugService, + IDocumentManager, + ILiveShareApi, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { ConfigurationService } from '../../client/common/configuration/service'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EditorUtils } from '../../client/common/editor'; +import { ExperimentsManager } from '../../client/common/experiments'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from '../../client/common/insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../client/common/insidersBuild/types'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; import { PipInstaller } from '../../client/common/installer/pipInstaller'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { IModuleInstaller } from '../../client/common/installer/types'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; +import { FileDownloader } from '../../client/common/net/fileDownloader'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { NugetService } from '../../client/common/nuget/nugetService'; +import { INugetService } from '../../client/common/nuget/types'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { PathUtils } from '../../client/common/platform/pathUtils'; @@ -22,18 +71,53 @@ import { IFileSystem, IPlatformService } from '../../client/common/platform/type import { CurrentProcess } from '../../client/common/process/currentProcess'; import { ProcessLogger } from '../../client/common/process/logger'; import { IProcessLogger, IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; import { TerminalHelper } from '../../client/common/terminal/helper'; -import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from '../../client/common/terminal/types'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalService, + ITerminalServiceFactory, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, IConfigurationService, + ICryptoUtils, ICurrentProcess, + IEditorUtils, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IFileDownloader, + IHttpClient, IInstaller, + IInterpreterPathService, IPathUtils, IPersistentStateFactory, IPythonSettings, + IRandom, IsWindows } from '../../client/common/types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Architecture } from '../../client/common/utils/platform'; +import { Random } from '../../client/common/utils/random'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { INotebookExecutionLogger } from '../../client/datascience/types'; import { ICondaService, IInterpreterLocatorService, @@ -48,6 +132,8 @@ import { InterpeterHashProviderFactory } from '../../client/interpreter/locators import { InterpreterFilter } from '../../client/interpreter/locators/services/interpreterFilter'; import { WindowsStoreInterpreter } from '../../client/interpreter/locators/services/windowsStoreInterpreter'; import { IServiceContainer } from '../../client/ioc/types'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; import { getExtensionSettings, PYTHON_PATH, rootWorkspaceUri } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; @@ -69,7 +155,7 @@ const info: PythonInterpreter = { }; suite('Module Installer', () => { - [undefined, Uri.file(__filename)].forEach((resource) => { + [undefined, Uri.file(__filename)].forEach(resource => { let ioc: UnitTestIocContainer; let mockTerminalService: TypeMoq.IMock; let condaService: TypeMoq.IMock; @@ -106,13 +192,13 @@ suite('Module Installer', () => { mockTerminalService = TypeMoq.Mock.ofType(); mockTerminalFactory = TypeMoq.Mock.ofType(); mockTerminalFactory - .setup((t) => t.getTerminalService(TypeMoq.It.isValue(resource))) + .setup(t => t.getTerminalService(TypeMoq.It.isValue(resource))) .returns(() => mockTerminalService.object) .verifiable(TypeMoq.Times.atLeastOnce()); // If resource is provided, then ensure we do not invoke without the resource. mockTerminalFactory - .setup((t) => t.getTerminalService(TypeMoq.It.isAny())) - .callback((passedInResource) => expect(passedInResource).to.be.equal(resource)) + .setup(t => t.getTerminalService(TypeMoq.It.isAny())) + .callback(passedInResource => expect(passedInResource).to.be.equal(resource)) .returns(() => mockTerminalService.object); ioc.serviceManager.addSingletonInstance( ITerminalServiceFactory, @@ -137,11 +223,7 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton(IPlatformService, PlatformService); ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); - const workspaceService = TypeMoq.Mock.ofType(); - ioc.serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); - const http = TypeMoq.Mock.ofType(); - http.setup((h) => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns(() => ''); - workspaceService.setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))).returns(() => http.object); + ioc.serviceManager.addSingletonInstance(IWorkspaceService, new WorkspaceService()); ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance(IsWindows, false); @@ -153,6 +235,106 @@ suite('Module Installer', () => { InterpeterHashProviderFactory ); ioc.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); + + ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); + ioc.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); + ioc.serviceManager.addSingleton(IExtensions, Extensions); + ioc.serviceManager.addSingleton(IRandom, Random); + ioc.serviceManager.addSingleton(IApplicationShell, ApplicationShell); + ioc.serviceManager.addSingleton(IClipboard, ClipboardService); + ioc.serviceManager.addSingleton(ICommandManager, CommandManager); + ioc.serviceManager.addSingleton(IDocumentManager, DocumentManager); + ioc.serviceManager.addSingleton(IDebugService, DebugService); + ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); + ioc.serviceManager.addSingleton(IBrowserService, BrowserService); + ioc.serviceManager.addSingleton(IHttpClient, HttpClient); + ioc.serviceManager.addSingleton(IFileDownloader, FileDownloader); + ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); + ioc.serviceManager.addSingleton(INugetService, NugetService); + ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); + ioc.serviceManager.addSingleton( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler + ); + ioc.serviceManager.addSingleton(ILiveShareApi, LiveShareApi); + ioc.serviceManager.addSingleton(ICryptoUtils, CryptoUtils); + ioc.serviceManager.addSingleton(IExperimentsManager, ExperimentsManager); + + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda + ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv + ); + ioc.serviceManager.addSingleton( + IFeatureDeprecationManager, + FeatureDeprecationManager + ); + + ioc.serviceManager.addSingleton( + IAsyncDisposableRegistry, + AsyncDisposableRegistry + ); + ioc.serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); + ioc.serviceManager.addSingleton(IImportTracker, ImportTracker); + ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + ioc.serviceManager.addBinding(IImportTracker, INotebookExecutionLogger); + ioc.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); + ioc.serviceManager.addSingleton(IShellDetector, SettingsShellDetector); + ioc.serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); + ioc.serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); + ioc.serviceManager.addSingleton(IInsiderExtensionPrompt, InsidersExtensionPrompt); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + InsidersExtensionService + ); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler + ); + ioc.serviceManager.addSingleton( + IExtensionChannelService, + ExtensionChannelService + ); + ioc.serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionChannel.off + ); + ioc.serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersDailyChannelRule, + ExtensionChannel.daily + ); + ioc.serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersWeeklyChannelRule, + ExtensionChannel.weekly + ); + ioc.serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugSessionTelemetry + ); + ioc.serviceManager.addSingleton(ICustomEditorService, CustomEditorService); } async function resetSettings(): Promise { const configService = ioc.serviceManager.get(IConfigurationService); @@ -180,9 +362,7 @@ suite('Module Installer', () => { new MockModuleInstaller('mock', true) ); const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator - .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([])); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); ioc.serviceManager.addSingletonInstance( IInterpreterLocatorService, mockInterpreterLocator.object, @@ -209,15 +389,15 @@ suite('Module Installer', () => { const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); - const condaInstaller = moduleInstallers.find((item) => item.displayName === 'Conda')!; + const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); - const mockInstaller = moduleInstallers.find((item) => item.displayName === 'mock')!; + const mockInstaller = moduleInstallers.find(item => item.displayName === 'mock')!; expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); await expect(mockInstaller.isSupported()).to.eventually.equal(true, 'mock is not supported'); }); @@ -230,7 +410,7 @@ suite('Module Installer', () => { const pythonPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .setup(p => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([ { @@ -271,7 +451,7 @@ suite('Module Installer', () => { const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); }); @@ -280,16 +460,16 @@ suite('Module Installer', () => { const configService = TypeMoq.Mock.ofType(); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); const settings = TypeMoq.Mock.ofType(); const pythonPath = 'pythonABC'; - settings.setup((s) => s.pythonPath).returns(() => pythonPath); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); condaService - .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(true)); const condaInstaller = new CondaInstaller(serviceContainer.object); @@ -300,16 +480,16 @@ suite('Module Installer', () => { const configService = TypeMoq.Mock.ofType(); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); const settings = TypeMoq.Mock.ofType(); const pythonPath = 'pythonABC'; - settings.setup((s) => s.pythonPath).returns(() => pythonPath); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); condaService - .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(false)); const condaInstaller = new CondaInstaller(serviceContainer.object); @@ -321,7 +501,7 @@ suite('Module Installer', () => { const interpreterPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .setup(p => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Unknown }])); ioc.serviceManager.addSingletonInstance( IInterpreterLocatorService, @@ -340,25 +520,25 @@ suite('Module Installer', () => { path: PYTHON_PATH }; interpreterService - .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) + .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(interpreter)); const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); let argsSent: string[] = []; mockTerminalService - .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((_cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())) // tslint:disable-next-line:no-any .returns(() => Promise.resolve({ type: InterpreterType.Unknown } as any)); @@ -375,7 +555,7 @@ suite('Module Installer', () => { const interpreterPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .setup(p => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Conda }])); ioc.serviceManager.addSingletonInstance( IInterpreterLocatorService, @@ -391,13 +571,13 @@ suite('Module Installer', () => { const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); let argsSent: string[] = []; mockTerminalService - .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((_cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); @@ -415,7 +595,7 @@ suite('Module Installer', () => { test(`Validate pipenv install arguments ${resourceTestNameSuffix}`, async () => { const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .setup(p => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([{ ...info, path: 'interpreterPath', type: InterpreterType.VirtualEnv }]) ); @@ -427,14 +607,14 @@ suite('Module Installer', () => { const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find((item) => item.displayName === 'pipenv')!; + const pipInstaller = moduleInstallers.find(item => item.displayName === 'pipenv')!; expect(pipInstaller).not.to.be.an('undefined', 'pipenv installer not found'); let argsSent: string[] = []; let command: string | undefined; mockTerminalService - .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((cmd: string, args: string[]) => { argsSent = args; command = cmd; diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 64eb1ad60222..7df62d4a805a 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -7,60 +7,15 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { execFile } from 'child_process'; import * as fs from 'fs-extra'; -import { Container } from 'inversify'; import { EOL } from 'os'; import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { ConfigurationTarget, Disposable, Memento, OutputChannel, 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 { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; -import { PersistentStateFactory } from '../../../client/common/persistentState'; -import { IS_WINDOWS } from '../../../client/common/platform/constants'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { PathUtils } from '../../../client/common/platform/pathUtils'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; -import { CurrentProcess } from '../../../client/common/process/currentProcess'; -import { ProcessLogger } from '../../../client/common/process/logger'; -import { registerTypes as processRegisterTypes } from '../../../client/common/process/serviceRegistry'; -import { IProcessLogger, IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; -import { - GLOBAL_MEMENTO, - IConfigurationService, - ICurrentProcess, - IDisposableRegistry, - IMemento, - IOutputChannel, - IPathUtils, - IPersistentStateFactory, - IsWindows, - WORKSPACE_MEMENTO -} from '../../../client/common/types'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; import { clearCache } from '../../../client/common/utils/cacheUtils'; import { OSType } from '../../../client/common/utils/platform'; -import { registerTypes as variablesRegisterTypes } from '../../../client/common/variables/serviceRegistry'; -import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService -} from '../../../client/interpreter/autoSelection/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { InterpreterHashProvider } from '../../../client/interpreter/locators/services/hashProvider'; -import { InterpeterHashProviderFactory } from '../../../client/interpreter/locators/services/hashProviderFactory'; -import { InterpreterFilter } from '../../../client/interpreter/locators/services/interpreterFilter'; -import { WindowsStoreInterpreter } from '../../../client/interpreter/locators/services/windowsStoreInterpreter'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceContainer } from '../../../client/ioc/types'; import { clearPythonPathInWorkspaceFolder, getExtensionSettings, isOs, isPythonVersion } from '../../common'; -import { MockOutputChannel } from '../../mockClasses'; -import { MockAutoSelectionService } from '../../mocks/autoSelector'; -import { MockMemento } from '../../mocks/mementos'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; use(chaiAsPromised); @@ -71,7 +26,6 @@ const workspace4PyFile = Uri.file(path.join(workspace4Path.fsPath, 'one.py')); // tslint:disable-next-line:max-func-body-length suite('PythonExecutableService', () => { - let cont: Container; let serviceContainer: IServiceContainer; let configService: IConfigurationService; let pythonExecFactory: IPythonExecutionFactory; @@ -82,69 +36,10 @@ suite('PythonExecutableService', () => { this.skip(); } await clearPythonPathInWorkspaceFolder(workspace4Path); - await initialize(); + serviceContainer = (await initialize()).serviceContainer; }); setup(async () => { - cont = new Container(); - serviceContainer = new ServiceContainer(cont); - const serviceManager = new ServiceManager(cont); - - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance(IDisposableRegistry, []); - serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); - const standardOutputChannel = new MockOutputChannel('Python'); - serviceManager.addSingletonInstance( - IOutputChannel, - standardOutputChannel, - STANDARD_OUTPUT_CHANNEL - ); - serviceManager.addSingleton(IPathUtils, PathUtils); - serviceManager.addSingleton(ICurrentProcess, CurrentProcess); - serviceManager.addSingleton(IConfigurationService, ConfigurationService); - serviceManager.addSingleton(IPlatformService, PlatformService); - serviceManager.addSingleton(IWorkspaceService, WorkspaceService); - serviceManager.addSingleton(IFileSystem, FileSystem); - serviceManager.addSingleton(IProcessLogger, ProcessLogger); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService - ); - serviceManager.addSingleton( - IInterpreterAutoSeletionProxyService, - MockAutoSelectionService - ); - serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); - serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); - serviceManager.addSingleton( - InterpeterHashProviderFactory, - InterpeterHashProviderFactory - ); - serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); - serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); - serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); - - serviceManager.addSingleton(ICondaService, CondaService); - - processRegisterTypes(serviceManager); - variablesRegisterTypes(serviceManager); - - const mockInterpreterService = mock(InterpreterService); - when(mockInterpreterService.hasInterpreters).thenResolve(false); - serviceManager.addSingletonInstance(IInterpreterService, instance(mockInterpreterService)); - - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); - when( - mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything(), anything()) - ).thenResolve(); - serviceManager.addSingletonInstance( - IEnvironmentActivationService, - instance(mockEnvironmentActivationService) - ); - - configService = serviceManager.get(IConfigurationService); + configService = serviceContainer.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); @@ -153,8 +48,6 @@ suite('PythonExecutableService', () => { }); suiteTeardown(closeActiveWindows); teardown(async () => { - cont.unbindAll(); - cont.unload(); await closeActiveWindows(); await clearPythonPathInWorkspaceFolder(workspace4Path); await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index 85c79b22efc2..14079dc24529 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -1,186 +1,192 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable: no-any - -import { expect } from 'chai'; -import * as typemoq from 'typemoq'; -import { IExtensionSingleActivationService } from '../../client/activation/types'; -import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DebugService } from '../../client/common/application/debugService'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { Extensions } from '../../client/common/application/extensions'; -import { LanguageService } from '../../client/common/application/languageService'; -import { TerminalManager } from '../../client/common/application/terminalManager'; -import { - IApplicationEnvironment, - IApplicationShell, - ICommandManager, - IDebugService, - IDocumentManager, - ILanguageService, - ILiveShareApi, - ITerminalManager, - IWorkspaceService -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { CryptoUtils } from '../../client/common/crypto'; -import { EditorUtils } from '../../client/common/editor'; -import { ExperimentsManager } from '../../client/common/experiments'; -import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; -import { - ExtensionInsidersDailyChannelRule, - ExtensionInsidersOffChannelRule, - ExtensionInsidersWeeklyChannelRule -} from '../../client/common/insidersBuild/downloadChannelRules'; -import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; -import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; -import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; -import { - ExtensionChannel, - IExtensionChannelRule, - IExtensionChannelService, - IInsiderExtensionPrompt -} from '../../client/common/insidersBuild/types'; -import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { BrowserService } from '../../client/common/net/browser'; -import { HttpClient } from '../../client/common/net/httpClient'; -import { NugetService } from '../../client/common/nuget/nugetService'; -import { INugetService } from '../../client/common/nuget/types'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { registerTypes } from '../../client/common/serviceRegistry'; -import { TerminalActivator } from '../../client/common/terminal/activator'; -import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; -import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; -import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; -import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; -import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; -import { TerminalServiceFactory } from '../../client/common/terminal/factory'; -import { TerminalHelper } from '../../client/common/terminal/helper'; -import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; -import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; -import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; -import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; -import { - IShellDetector, - ITerminalActivationCommandProvider, - ITerminalActivationHandler, - ITerminalActivator, - ITerminalHelper, - ITerminalServiceFactory, - TerminalActivationProviders -} from '../../client/common/terminal/types'; -import { - IAsyncDisposableRegistry, - IBrowserService, - IConfigurationService, - ICryptoUtils, - ICurrentProcess, - IEditorUtils, - IExperimentsManager, - IExtensions, - IFeatureDeprecationManager, - IHttpClient, - IInstaller, - IPathUtils, - IPersistentStateFactory, - IRandom -} from '../../client/common/types'; -import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { Random } from '../../client/common/utils/random'; -import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; -import { IServiceManager } from '../../client/ioc/types'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IImportTracker } from '../../client/telemetry/types'; - -suite('Common - Service Registry', () => { - test('Registrations', () => { - const serviceManager = typemoq.Mock.ofType(); - - [ - [IExtensions, Extensions], - [IRandom, Random], - [IPersistentStateFactory, PersistentStateFactory], - [ITerminalServiceFactory, TerminalServiceFactory], - [IPathUtils, PathUtils], - [IApplicationShell, ApplicationShell], - [ICurrentProcess, CurrentProcess], - [IInstaller, ProductInstaller], - [ICommandManager, CommandManager], - [IConfigurationService, ConfigurationService], - [IWorkspaceService, WorkspaceService], - [IDocumentManager, DocumentManager], - [ITerminalManager, TerminalManager], - [IDebugService, DebugService], - [IApplicationEnvironment, ApplicationEnvironment], - [ILanguageService, LanguageService], - [IBrowserService, BrowserService], - [IHttpClient, HttpClient], - [IEditorUtils, EditorUtils], - [INugetService, NugetService], - [ITerminalActivator, TerminalActivator], - [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], - [ILiveShareApi, LiveShareApi], - [ICryptoUtils, CryptoUtils], - [IExperimentsManager, ExperimentsManager], - [ITerminalHelper, TerminalHelper], - [ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv], - [ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish], - [ - ITerminalActivationCommandProvider, - CommandPromptAndPowerShell, - TerminalActivationProviders.commandPromptAndPowerShell - ], - [ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda], - [ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv], - [IFeatureDeprecationManager, FeatureDeprecationManager], - [IAsyncDisposableRegistry, AsyncDisposableRegistry], - [IMultiStepInputFactory, MultiStepInputFactory], - [IImportTracker, ImportTracker], - [IShellDetector, TerminalNameShellDetector], - [IShellDetector, SettingsShellDetector], - [IShellDetector, UserEnvironmentShellDetector], - [IShellDetector, VSCEnvironmentShellDetector], - [IInsiderExtensionPrompt, InsidersExtensionPrompt], - [IExtensionSingleActivationService, InsidersExtensionService], - [IExtensionChannelService, ExtensionChannelService], - [IExtensionChannelRule, ExtensionInsidersOffChannelRule, ExtensionChannel.off], - [IExtensionChannelRule, ExtensionInsidersDailyChannelRule, ExtensionChannel.daily], - [IExtensionChannelRule, ExtensionInsidersWeeklyChannelRule, ExtensionChannel.weekly] - ].forEach((mapping) => { - if (mapping.length === 2) { - serviceManager - .setup((s) => - s.addSingleton( - typemoq.It.isValue(mapping[0] as any), - typemoq.It.is((value) => mapping[1] === value) - ) - ) - .verifiable(typemoq.Times.atLeastOnce()); - } else { - serviceManager - .setup((s) => - s.addSingleton( - typemoq.It.isValue(mapping[0] as any), - typemoq.It.isAny(), - typemoq.It.isValue(mapping[2] as any) - ) - ) - .callback((_, cls) => expect(cls).to.equal(mapping[1])) - .verifiable(typemoq.Times.once()); - } - }); - - registerTypes(serviceManager.object); - serviceManager.verifyAll(); - }); -}); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-any + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DebugService } from '../../client/common/application/debugService'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { LanguageService } from '../../client/common/application/languageService'; +import { TerminalManager } from '../../client/common/application/terminalManager'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IDebugService, + IDocumentManager, + ILanguageService, + ILiveShareApi, + ITerminalManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EditorUtils } from '../../client/common/editor'; +import { ExperimentsManager } from '../../client/common/experiments'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from '../../client/common/insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../client/common/insidersBuild/types'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { NugetService } from '../../client/common/nuget/nugetService'; +import { INugetService } from '../../client/common/nuget/types'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { registerTypes } from '../../client/common/serviceRegistry'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalServiceFactory } from '../../client/common/terminal/factory'; +import { TerminalHelper } from '../../client/common/terminal/helper'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalServiceFactory, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, + IConfigurationService, + ICryptoUtils, + ICurrentProcess, + IEditorUtils, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IHttpClient, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IRandom +} from '../../client/common/types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Random } from '../../client/common/utils/random'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { IServiceManager } from '../../client/ioc/types'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; + +suite('Common - Service Registry', () => { + test('Registrations', () => { + const serviceManager = typemoq.Mock.ofType(); + + [ + [IActiveResourceService, ActiveResourceService], + [IInterpreterPathService, InterpreterPathService], + [IExtensions, Extensions], + [IRandom, Random], + [IPersistentStateFactory, PersistentStateFactory], + [ITerminalServiceFactory, TerminalServiceFactory], + [IPathUtils, PathUtils], + [IApplicationShell, ApplicationShell], + [ICurrentProcess, CurrentProcess], + [IInstaller, ProductInstaller], + [ICommandManager, CommandManager], + [IConfigurationService, ConfigurationService], + [IWorkspaceService, WorkspaceService], + [IDocumentManager, DocumentManager], + [ITerminalManager, TerminalManager], + [IDebugService, DebugService], + [IApplicationEnvironment, ApplicationEnvironment], + [ILanguageService, LanguageService], + [IBrowserService, BrowserService], + [IHttpClient, HttpClient], + [IEditorUtils, EditorUtils], + [INugetService, NugetService], + [ITerminalActivator, TerminalActivator], + [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], + [ILiveShareApi, LiveShareApi], + [ICryptoUtils, CryptoUtils], + [IExperimentsManager, ExperimentsManager], + [ITerminalHelper, TerminalHelper], + [ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv], + [ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish], + [ + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ], + [ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda], + [ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv], + [IFeatureDeprecationManager, FeatureDeprecationManager], + [IAsyncDisposableRegistry, AsyncDisposableRegistry], + [IMultiStepInputFactory, MultiStepInputFactory], + [IImportTracker, ImportTracker], + [IShellDetector, TerminalNameShellDetector], + [IShellDetector, SettingsShellDetector], + [IShellDetector, UserEnvironmentShellDetector], + [IShellDetector, VSCEnvironmentShellDetector], + [IInsiderExtensionPrompt, InsidersExtensionPrompt], + [IExtensionSingleActivationService, InsidersExtensionService], + [IExtensionChannelService, ExtensionChannelService], + [IExtensionChannelRule, ExtensionInsidersOffChannelRule, ExtensionChannel.off], + [IExtensionChannelRule, ExtensionInsidersDailyChannelRule, ExtensionChannel.daily], + [IExtensionChannelRule, ExtensionInsidersWeeklyChannelRule, ExtensionChannel.weekly] + ].forEach(mapping => { + if (mapping.length === 2) { + serviceManager + .setup(s => + s.addSingleton( + typemoq.It.isValue(mapping[0] as any), + typemoq.It.is(value => mapping[1] === value) + ) + ) + .verifiable(typemoq.Times.atLeastOnce()); + } else { + serviceManager + .setup(s => + s.addSingleton( + typemoq.It.isValue(mapping[0] as any), + typemoq.It.isAny(), + typemoq.It.isValue(mapping[2] as any) + ) + ) + .callback((_, cls) => expect(cls).to.equal(mapping[1])) + .verifiable(typemoq.Times.once()); + } + }); + + registerTypes(serviceManager.object); + serviceManager.verifyAll(); + }); +}); diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index efe64d280c16..a377e78a51be 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -7,11 +7,15 @@ import { expect } from 'chai'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; +import { DeprecatePythonPath } from '../../../../client/common/experimentGroups'; import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IExperimentsManager } from '../../../../client/common/types'; import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../ciConstants'; import { PYTHON_PATH, + resetGlobalInterpreterPathSetting, restorePythonPathInWorkspaceRoot, + setGlobalInterpreterPath, setPythonPathInWorkspaceRoot, updateSetting, waitForCondition @@ -53,6 +57,7 @@ suite('Activation of Environments in Terminal', () => { }; let terminalSettings: any; let pythonSettings: any; + let experiments: IExperimentsManager; suiteSetup(async () => { envPaths = await fs.readJson(envsLocation); terminalSettings = vscode.workspace.getConfiguration('terminal', vscode.workspace.workspaceFolders![0].uri); @@ -60,7 +65,7 @@ suite('Activation of Environments in Terminal', () => { defaultShell.Windows = terminalSettings.inspect('integrated.shell.windows').globalValue; defaultShell.Linux = terminalSettings.inspect('integrated.shell.linux').globalValue; await terminalSettings.update('integrated.shell.linux', '/bin/bash', vscode.ConfigurationTarget.Global); - await initialize(); + experiments = (await initialize()).serviceContainer.get(IExperimentsManager); }); setup(async () => { @@ -101,7 +106,11 @@ suite('Activation of Environments in Terminal', () => { ); await terminalSettings.update('integrated.shell.linux', defaultShell.Linux, vscode.ConfigurationTarget.Global); await pythonSettings.update('condaPath', undefined, vscode.ConfigurationTarget.Workspace); - await restorePythonPathInWorkspaceRoot(); + if (experiments.inExperiment(DeprecatePythonPath.experiment)) { + await resetGlobalInterpreterPathSetting(); + } else { + await restorePythonPathInWorkspaceRoot(); + } } /** @@ -142,7 +151,11 @@ suite('Activation of Environments in Terminal', () => { vscode.workspace.workspaceFolders![0].uri, vscode.ConfigurationTarget.WorkspaceFolder ); - await setPythonPathInWorkspaceRoot(envPath); + if (experiments.inExperiment(DeprecatePythonPath.experiment)) { + await setGlobalInterpreterPath(envPath); + } else { + await setPythonPathInWorkspaceRoot(envPath); + } const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); expect(fileSystem.arePathsSame(content, envPath)).to.equal(true, 'Environment not activated'); } diff --git a/src/test/common/utils/cacheUtils.unit.test.ts b/src/test/common/utils/cacheUtils.unit.test.ts index b68e3164c7b2..b6859a82342d 100644 --- a/src/test/common/utils/cacheUtils.unit.test.ts +++ b/src/test/common/utils/cacheUtils.unit.test.ts @@ -123,7 +123,7 @@ suite('Common Utils - CacheUtils', () => { const pythonPath = 'Some Python Path'; const vsc = createMockVSC(pythonPath); const resource = Uri.parse('a'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); @@ -136,7 +136,7 @@ suite('Common Utils - CacheUtils', () => { const pythonPath = 'Some Python Path'; const vsc = createMockVSC(pythonPath); const resource = Uri.parse('a'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); @@ -153,7 +153,7 @@ suite('Common Utils - CacheUtils', () => { const pythonPath = 'Some Python Path'; const vsc = createMockVSC(pythonPath); const resource = Uri.parse('a'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); @@ -170,7 +170,7 @@ suite('Common Utils - CacheUtils', () => { const pythonPath = 'Some Python Path'; const vsc = createMockVSC(pythonPath); const resource = Uri.parse('a'); - const cache = new TestInMemoryInterpreterSpecificCache('Something', 100, [resource], vsc); + const cache = new TestInMemoryInterpreterSpecificCache('Something', 100, [resource], undefined, vsc); expect(cache.hasData).to.be.equal(false, 'Must not have any data before caching.'); cache.data = scenario.dataToStore; @@ -204,7 +204,7 @@ suite('Common Utils - CacheUtils', () => { const resource = Uri.parse('a'); (vsc.workspace as any).workspaceFolders = [{ index: 0, name: '1', uri: Uri.parse('wkfolder') }]; vsc.workspace.getWorkspaceFolder = () => vsc.workspace.workspaceFolders![0]; - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); @@ -218,8 +218,14 @@ suite('Common Utils - CacheUtils', () => { const vsc = createMockVSC(pythonPath); const resource = Uri.parse('a'); const anotherResource = Uri.parse('b'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - const cache2 = new InMemoryInterpreterSpecificCache('Something', 10000, [anotherResource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + const cache2 = new InMemoryInterpreterSpecificCache( + 'Something', + 10000, + [anotherResource], + undefined, + vsc + ); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); @@ -238,8 +244,14 @@ suite('Common Utils - CacheUtils', () => { const anotherResource = Uri.parse('b'); (vsc.workspace as any).workspaceFolders = [{ index: 0, name: '1', uri: Uri.parse('wkfolder') }]; vsc.workspace.getWorkspaceFolder = () => vsc.workspace.workspaceFolders![0]; - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - const cache2 = new InMemoryInterpreterSpecificCache('Something', 10000, [anotherResource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + const cache2 = new InMemoryInterpreterSpecificCache( + 'Something', + 10000, + [anotherResource], + undefined, + vsc + ); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); @@ -264,8 +276,14 @@ suite('Common Utils - CacheUtils', () => { const index = res.fsPath === resource.fsPath ? 0 : 1; return vsc.workspace.workspaceFolders![index]; }; - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - const cache2 = new InMemoryInterpreterSpecificCache('Something', 10000, [anotherResource], vsc); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + const cache2 = new InMemoryInterpreterSpecificCache( + 'Something', + 10000, + [anotherResource], + undefined, + vsc + ); expect(cache.hasData).to.be.equal(false, 'Must not have any data'); expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); diff --git a/src/test/common/utils/decorators.unit.test.ts b/src/test/common/utils/decorators.unit.test.ts index f4a2a60cb199..8ff3c81c0ca6 100644 --- a/src/test/common/utils/decorators.unit.test.ts +++ b/src/test/common/utils/decorators.unit.test.ts @@ -5,15 +5,8 @@ import { expect, use } from 'chai'; import * as chaiPromise from 'chai-as-promised'; -import { Uri } from 'vscode'; -import { Resource } from '../../../client/common/types'; import { clearCache } from '../../../client/common/utils/cacheUtils'; -import { - cache, - cacheResourceSpecificInterpreterData, - makeDebounceAsyncDecorator, - makeDebounceDecorator -} from '../../../client/common/utils/decorators'; +import { cache, makeDebounceAsyncDecorator, makeDebounceDecorator } from '../../../client/common/utils/decorators'; import { sleep } from '../../core'; use(chaiPromise); @@ -24,93 +17,6 @@ suite('Common Utils - Decorators', function () { // at the precise time prescribed. // tslint:disable-next-line: no-invalid-this this.retries(3); - suite('Cache', () => { - setup(clearCache); - teardown(clearCache); - function createMockVSC(pythonPath: string): typeof import('vscode') { - return { - workspace: { - getConfiguration: () => { - return { - get: () => { - return pythonPath; - }, - inspect: () => { - return { globalValue: pythonPath }; - } - }; - }, - getWorkspaceFolder: () => { - return; - } - }, - Uri: Uri - } as any; - } - test('Result must be cached when using cache decorator', async () => { - const vsc = createMockVSC(''); - class TestClass { - public invoked = false; - @cacheResourceSpecificInterpreterData('Something', 100000, vsc) - public async doSomething(_resource: Resource, a: number, b: number): Promise { - this.invoked = true; - return a + b; - } - } - - const cls = new TestClass(); - const uri = Uri.parse('a'); - const uri2 = Uri.parse('b'); - - let result = await cls.doSomething(uri, 1, 2); - expect(result).to.equal(3); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - - cls.invoked = false; - let result2 = await cls.doSomething(uri2, 2, 3); - expect(result2).to.equal(5); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - - cls.invoked = false; - result = await cls.doSomething(uri, 1, 2); - result2 = await cls.doSomething(uri2, 2, 3); - expect(result).to.equal(3); - expect(result2).to.equal(5); - expect(cls.invoked).to.equal(false, 'Must not be invoked'); - }); - test('Cache result must be cleared when cache expires', async () => { - const vsc = createMockVSC(''); - class TestClass { - public invoked = false; - @cacheResourceSpecificInterpreterData('Something', 100, vsc) - public async doSomething(_resource: Resource, a: number, b: number): Promise { - this.invoked = true; - return a + b; - } - } - - const cls = new TestClass(); - const uri = Uri.parse('a'); - let result = await cls.doSomething(uri, 1, 2); - - expect(result).to.equal(3); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - - cls.invoked = false; - result = await cls.doSomething(uri, 1, 2); - - expect(result).to.equal(3); - expect(cls.invoked).to.equal(false, 'Must not be invoked'); - - await sleep(110); - - cls.invoked = false; - result = await cls.doSomething(uri, 1, 2); - - expect(result).to.equal(3); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - }); - }); suite('Cache Decorator', () => { const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; diff --git a/src/test/common/variables/envVarsProvider.multiroot.test.ts b/src/test/common/variables/envVarsProvider.multiroot.test.ts index 362285a9cd36..f8c9a3be94a7 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -91,7 +91,8 @@ suite('Multiroot Environment Variables Provider', () => { new PlatformService(), workspaceService, cfgService, - mockProcess + mockProcess, + ioc.serviceContainer ); } diff --git a/src/test/common/variables/environmentVariablesProvider.unit.test.ts b/src/test/common/variables/environmentVariablesProvider.unit.test.ts index 5c477f1c4959..5faa09400240 100644 --- a/src/test/common/variables/environmentVariablesProvider.unit.test.ts +++ b/src/test/common/variables/environmentVariablesProvider.unit.test.ts @@ -17,10 +17,13 @@ import { PlatformService } from '../../../client/common/platform/platformService import { IPlatformService } from '../../../client/common/platform/types'; import { CurrentProcess } from '../../../client/common/process/currentProcess'; import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; import { clearCache } from '../../../client/common/utils/cacheUtils'; import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { noop } from '../../core'; @@ -33,6 +36,7 @@ suite('Multiroot Environment Variables Provider', () => { let configuration: IConfigurationService; let currentProcess: ICurrentProcess; let settings: IPythonSettings; + let serviceContainer: IServiceContainer; setup(() => { envVarsService = mock(EnvironmentVariablesService); @@ -41,6 +45,7 @@ suite('Multiroot Environment Variables Provider', () => { configuration = mock(ConfigurationService); currentProcess = mock(CurrentProcess); settings = mock(PythonSettings); + serviceContainer = mock(ServiceContainer); when(configuration.getSettings(anything())).thenReturn(instance(settings)); when(workspace.onDidChangeConfiguration).thenReturn(noop as any); @@ -50,7 +55,8 @@ suite('Multiroot Environment Variables Provider', () => { instance(platform), instance(workspace), instance(configuration), - instance(currentProcess) + instance(currentProcess), + instance(serviceContainer) ); sinon.stub(EnvFileTelemetry, 'sendFileCreationTelemetry').returns(); @@ -264,5 +270,60 @@ suite('Multiroot Environment Variables Provider', () => { verify(platform.pathVariableName).atLeast(1); assert.deepEqual(vars, envFileVars); }); + + test(`Getting environment variables which are already cached does not reinvoke the method ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + assert.deepEqual(vars, {}); + + await provider.getEnvironmentVariables(workspaceUri); + + // Verify that the contents of `_getEnvironmentVariables()` method are only invoked once + verify(configuration.getSettings(anything())).once(); + assert.deepEqual(vars, {}); + }); + + test(`Cache result must be cleared when cache expires ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + provider = new EnvironmentVariablesProvider( + instance(envVarsService), + [], + instance(platform), + instance(workspace), + instance(configuration), + instance(currentProcess), + instance(serviceContainer), + 100 + ); + const vars = await provider.getEnvironmentVariables(workspaceUri); + + assert.deepEqual(vars, {}); + + await sleep(110); + await provider.getEnvironmentVariables(workspaceUri); + + // Verify that the contents of `_getEnvironmentVariables()` method are invoked twice + verify(configuration.getSettings(anything())).twice(); + assert.deepEqual(vars, {}); + }); }); }); diff --git a/src/test/configuration/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector.unit.test.ts index 665a07e48ee3..2bc3ad18b467 100644 --- a/src/test/configuration/interpreterSelector.unit.test.ts +++ b/src/test/configuration/interpreterSelector.unit.test.ts @@ -222,8 +222,9 @@ suite('Interpreters - selector', () => { workspace.verifyAll(); pythonPathUpdater.verifyAll(); }); - test('Update workspace folder settings when there is one workspace folder', async () => { + test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + workspace.setup(w => w.workspaceFile).returns(() => undefined); const selectedItem: IInterpreterQuickPickItem = { description: '', detail: '', @@ -459,9 +460,10 @@ suite('Interpreters - selector', () => { workspace.verifyAll(); pythonPathUpdater.verifyAll(); }); - test('Update workspace folder settings when there is one workspace folder', async () => { + test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; workspace.setup(w => w.workspaceFolders).returns(() => [folder]); + workspace.setup(w => w.workspaceFile).returns(() => undefined); selector.getSuggestions = () => Promise.resolve([]); pythonPathUpdater diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 5224efb359ad..39d101a136ce 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -103,6 +103,7 @@ import { } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IInstallationChannelManager, IProductPathService, IProductService } from '../../client/common/installer/types'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { IS_WINDOWS } from '../../client/common/platform/constants'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { RegistryImplementation } from '../../client/common/platform/registry'; @@ -143,6 +144,7 @@ import { IExtensionContext, IExtensions, IInstaller, + IInterpreterPathService, IMemento, IOutputChannel, IPathUtils, @@ -472,7 +474,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const services = require('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices') as any; if (services.StaticServices) { const keys = Object.keys(services.StaticServices); - keys.forEach((k) => { + keys.forEach(k => { const service = services.StaticServices[k] as any; if (service && service._value && service._value.dispose) { if (typeof service._value.dispose === 'function') { @@ -550,6 +552,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(IThemeFinder, ThemeFinder); this.serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); this.serviceManager.addSingleton(IStatusProvider, StatusProvider); + this.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); this.serviceManager.addSingletonInstance( IAsyncDisposableRegistry, this.asyncRegistry @@ -605,7 +608,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { ServerPreload ); const mockExtensionContext = TypeMoq.Mock.ofType(); - mockExtensionContext.setup((m) => m.globalStoragePath).returns(() => os.tmpdir()); + mockExtensionContext.setup(m => m.globalStoragePath).returns(() => os.tmpdir()); this.serviceManager.addSingletonInstance(IExtensionContext, mockExtensionContext.object); const mockServerSelector = mock(JupyterServerSelector); @@ -815,10 +818,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const configurationService = TypeMoq.Mock.ofType(); this.datascience = TypeMoq.Mock.ofType(); - configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(this.getSettings.bind(this)); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(this.getSettings.bind(this)); const startTime = Date.now(); - this.datascience.setup((d) => d.activationStartTime).returns(() => startTime); + this.datascience.setup(d => d.activationStartTime).returns(() => startTime); this.serviceManager.addSingleton( IEnvironmentVariablesProvider, @@ -906,7 +909,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } const interpreterDisplay = TypeMoq.Mock.ofType(); - interpreterDisplay.setup((i) => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); // Create our jupyter mock if necessary if (this.shouldMockJupyter) { @@ -1022,9 +1025,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Don't use conda at all when mocking const condaService = TypeMoq.Mock.ofType(); this.serviceManager.addSingletonInstance(ICondaService, condaService.object); - condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(false)); - condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - condaService.setup((c) => c.condaEnvironmentsFile).returns(() => undefined); + condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(false)); + condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); this.serviceManager.addSingleton( IVirtualEnvironmentsSearchPathProvider, @@ -1080,23 +1083,23 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } }; - appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); + appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve('')); appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); appShell - .setup((a) => + .setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) .returns((_a1: string, a2: string, _a3: string, _a4: string) => Promise.resolve(a2)); appShell - .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .setup(a => a.showSaveDialog(TypeMoq.It.isAny())) .returns(() => Promise.resolve(Uri.file('test.ipynb'))); - appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - appShell.setup((a) => a.showInputBox(TypeMoq.It.isAny())).returns(() => Promise.resolve('Python')); + appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + appShell.setup(a => a.showInputBox(TypeMoq.It.isAny())).returns(() => Promise.resolve('Python')); const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); @@ -1117,7 +1120,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { IExtensionSingleActivationService ); - await Promise.all(activationServices.map((a) => a.activate())); + await Promise.all(activationServices.map(a => a.activate())); // Then force our interpreter to be one that supports jupyter (unless in a mock state when we don't have to) if (!this.mockJupyter) { @@ -1205,9 +1208,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return DataScienceIocContainer.jupyterInterpreters; } const list = await this.get(IInterpreterService).getInterpreters(undefined); - const promises = list.map((f) => this.hasJupyter(f).then((b) => (b ? f : undefined))); + const promises = list.map(f => this.hasJupyter(f).then(b => (b ? f : undefined))); const resolved = await Promise.all(promises); - DataScienceIocContainer.jupyterInterpreters = resolved.filter((r) => r) as PythonInterpreter[]; + DataScienceIocContainer.jupyterInterpreters = resolved.filter(r => r) as PythonInterpreter[]; return DataScienceIocContainer.jupyterInterpreters; } @@ -1218,7 +1221,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } public addResourceToFolder(resource: Uri, folderPath: string) { - let folder = this.workspaceFolders.find((f) => f.uri.fsPath === folderPath); + let folder = this.workspaceFolders.find(f => f.uri.fsPath === folderPath); if (!folder) { folder = this.addWorkspaceFolder(folderPath); } @@ -1262,7 +1265,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } if (this.extraListeners.length) { - this.extraListeners.forEach((e) => e(msg.type, msg.payload)); + this.extraListeners.forEach(e => e(msg.type, msg.payload)); } if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { this.wrapperCreatedPromise.resolve(); @@ -1287,7 +1290,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { private createWebPanel(): IWebPanel { const webPanel = mock(WebPanel); - when(webPanel.postMessage(anything())).thenCall((m) => { + when(webPanel.postMessage(anything())).thenCall(m => { // tslint:disable-next-line: no-require-imports const reactHelpers = require('./reactHelpers') as typeof import('./reactHelpers'); const message = reactHelpers.createMessageEvent(m); @@ -1313,7 +1316,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // This needs to be async because we are being called in the ctor of the webpanel. It can't // handle some messages during the ctor. setTimeout(() => { - this.missedMessages.forEach((m) => + this.missedMessages.forEach(m => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop() ); }, 0); @@ -1432,7 +1435,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { private getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { if (uri) { - return this.workspaceFolders.find((w) => w.ownedResources.has(uri.toString())); + return this.workspaceFolders.find(w => w.ownedResources.has(uri.toString())); } return undefined; } diff --git a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts index cdb262f788ff..73b9d647c5d7 100644 --- a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts +++ b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts @@ -9,10 +9,18 @@ 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 { DeprecatePythonPath } from '../../../../client/common/experimentGroups'; +import { ExperimentsManager } from '../../../../client/common/experiments'; +import { InterpreterPathService } from '../../../../client/common/interpreterPathService'; 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 { + IExperimentsManager, + IInterpreterPathService, + 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'; @@ -25,6 +33,8 @@ suite('Interpreters - Auto Selection - Settings Rule', () => { let fs: IFileSystem; let state: PersistentState; let workspaceService: IWorkspaceService; + let experimentsManager: IExperimentsManager; + let interpreterPathService: IInterpreterPathService; class SettingsInterpretersAutoSelectionRuleTest extends SettingsInterpretersAutoSelectionRule { public async onAutoSelectInterpreter( resource: Resource, @@ -38,6 +48,9 @@ suite('Interpreters - Auto Selection - Settings Rule', () => { state = mock(PersistentState); fs = mock(FileSystem); workspaceService = mock(WorkspaceService); + experimentsManager = mock(ExperimentsManager); + interpreterPathService = mock(InterpreterPathService); + when(experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).thenReturn(undefined); when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn( instance(state) @@ -45,11 +58,47 @@ suite('Interpreters - Auto Selection - Settings Rule', () => { rule = new SettingsInterpretersAutoSelectionRuleTest( instance(fs), instance(stateFactory), - instance(workspaceService) + instance(workspaceService), + instance(experimentsManager), + instance(interpreterPathService) ); }); - test('Invoke next rule if python Path in user settings is default', async () => { + test('If in experiment, invoke next rule if python Path in user settings is default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = {}; + + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(interpreterPathService.inspect(undefined)).thenReturn(pythonPathInConfig as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('If in experiment, invoke next rule if python Path in user settings is not defined', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'python' }; + + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(interpreterPathService.inspect(undefined)).thenReturn(pythonPathInConfig as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('If in experiment, must not Invoke next rule if python Path in user settings is not default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'something else' }; + + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(interpreterPathService.inspect(undefined)).thenReturn(pythonPathInConfig as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.exit); + }); + + test('If not in experiment, invoke next rule if python Path in user settings is default', async () => { const manager = mock(InterpreterAutoSelectionService); const pythonPathInConfig = {}; const pythonPath = { inspect: () => pythonPathInConfig }; @@ -60,7 +109,7 @@ suite('Interpreters - Auto Selection - Settings Rule', () => { expect(nextAction).to.be.equal(NextAction.runNextRule); }); - test('Invoke next rule if python Path in user settings is not defined', async () => { + test('If not in experiment, 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 }; @@ -71,7 +120,7 @@ suite('Interpreters - Auto Selection - Settings Rule', () => { expect(nextAction).to.be.equal(NextAction.runNextRule); }); - test('Must not Invoke next rule if python Path in user settings is not default', async () => { + test('If not in experiment, 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 }; diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts index 254858a0220d..18d4424f8f2a 100644 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts @@ -13,11 +13,19 @@ 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 { DeprecatePythonPath } from '../../../../client/common/experimentGroups'; +import { ExperimentsManager } from '../../../../client/common/experiments'; +import { InterpreterPathService } from '../../../../client/common/interpreterPathService'; 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 { + IExperimentsManager, + IInterpreterPathService, + 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'; @@ -46,6 +54,8 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { let virtualEnvLocator: IInterpreterLocatorService; let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; let workspaceService: IWorkspaceService; + let experimentsManager: IExperimentsManager; + let interpreterPathService: IInterpreterPathService; class WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest extends WorkspaceVirtualEnvInterpretersAutoSelectionRule { public async setGlobalInterpreter( interpreter?: PythonInterpreter, @@ -73,6 +83,8 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { workspaceService = mock(WorkspaceService); virtualEnvLocator = mock(KnownPathsService); pythonPathUpdaterService = mock(PythonPathUpdaterService); + experimentsManager = mock(ExperimentsManager); + interpreterPathService = mock(InterpreterPathService); when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn( instance(state) @@ -85,7 +97,9 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { instance(workspaceService), instance(pythonPathUpdaterService), instance(pipEnvLocator), - instance(virtualEnvLocator) + instance(virtualEnvLocator), + instance(experimentsManager), + instance(interpreterPathService) ); }); test('Invoke next rule if there is no workspace', async () => { @@ -147,6 +161,38 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { verify(helper.getActiveWorkspaceUri(anything())).once(); pythonPathInConfig.verifyAll(); }); + test('If in experiment, use new API to fetch 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); + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).thenReturn(undefined); + when(interpreterPathService.inspect(folderUri)).thenReturn(pythonPathInConfig.object); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(someUri, manager); + + verify(nextRule.autoSelectInterpreter(someUri, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + verify(interpreterPathService.inspect(folderUri)).once(); + pythonPathInConfig.verifyAll(); + }); test('Does not update settings when there is no interpreter', async () => { await rule.cacheSelectedInterpreter(undefined, {} as any); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index f2c51a0f3fc4..556bc0a7752d 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -5,8 +5,16 @@ import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Disposable, StatusBarAlignment, StatusBarItem, Uri, WorkspaceFolder } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IPathUtils, IPythonSettings } from '../../client/common/types'; +import { + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + IPathUtils, + IPythonSettings +} from '../../client/common/types'; +import { Interpreters } from '../../client/common/utils/localize'; import { Architecture } from '../../client/common/utils/platform'; import { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; @@ -49,6 +57,7 @@ suite('Interpreters Display', () => { let interpreterDisplay: IInterpreterDisplay; let interpreterHelper: TypeMoq.IMock; let pathUtils: TypeMoq.IMock; + let output: TypeMoq.IMock; let autoSelection: IInterpreterAutoSelectionService; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); @@ -63,53 +72,57 @@ suite('Interpreters Display', () => { pythonSettings = TypeMoq.Mock.ofType(); configurationService = TypeMoq.Mock.ofType(); pathUtils = TypeMoq.Mock.ofType(); + output = TypeMoq.Mock.ofType(); autoSelection = mock(InterpreterAutoSelectionService); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), STANDARD_OUTPUT_CHANNEL)) + .returns(() => output.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))) .returns(() => applicationShell.object); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))) .returns(() => interpreterService.object); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))) + .setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))) .returns(() => virtualEnvMgr.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configurationService.object); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))) .returns(() => interpreterHelper.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))) + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))) .returns(() => instance(autoSelection)); applicationShell - .setup((a) => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))) + .setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))) .returns(() => statusBar.object); - pathUtils.setup((p) => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p) => p); + pathUtils.setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(p => p); interpreterDisplay = new InterpreterDisplay(serviceContainer.object); }); function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { if (workspaceFolder) { const mockFolder = TypeMoq.Mock.ofType(); - mockFolder.setup((w) => w.uri).returns(() => workspaceFolder); + mockFolder.setup(w => w.uri).returns(() => workspaceFolder); workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))) + .setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))) .returns(() => mockFolder.object); } else { - workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); } } - test('Sattusbar must be created and have command name initialized', () => { - statusBar.verify((s) => (s.command = TypeMoq.It.isValue('python.setInterpreter')), TypeMoq.Times.once()); + test('Statusbar must be created and have command name initialized', () => { + statusBar.verify(s => (s.command = TypeMoq.It.isValue('python.setInterpreter')), TypeMoq.Times.once()); expect(disposableRegistry).to.be.lengthOf.above(0); expect(disposableRegistry).contain(statusBar.object); }); @@ -125,17 +138,46 @@ suite('Interpreters Display', () => { setupWorkspaceFolder(resource, workspaceFolder); when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); interpreterService - .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve([])); interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(activeInterpreter)); await interpreterDisplay.refresh(resource); verify(autoSelection.autoSelectInterpreter(anything())).once(); - statusBar.verify((s) => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); - statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!), TypeMoq.Times.once()); + statusBar.verify(s => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); + statusBar.verify(s => (s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!), TypeMoq.Times.atLeastOnce()); + }); + test('Log the output channel if displayed needs to be updated with a new interpreter', async () => { + const resource = Uri.file('x'); + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonInterpreter = { + ...info, + displayName: 'Dummy_Display_Name', + type: InterpreterType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + pathUtils + .setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => activeInterpreter.path); + setupWorkspaceFolder(resource, workspaceFolder); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + interpreterService + .setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve([])); + interpreterService + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(activeInterpreter)); + output + .setup(o => o.appendLine(Interpreters.pythonInterpreterPath().format(activeInterpreter.path))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterDisplay.refresh(resource); + + output.verifyAll(); }); test('If interpreter is not identified then tooltip should point to python Path', async () => { const resource = Uri.file('x'); @@ -149,13 +191,13 @@ suite('Interpreters Display', () => { path: pythonPath } as any) as PythonInterpreter; interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(pythonInterpreter)); await interpreterDisplay.refresh(resource); - statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)), TypeMoq.Times.once()); - statusBar.verify((s) => (s.text = TypeMoq.It.isValue(displayName)), TypeMoq.Times.once()); + statusBar.verify(s => (s.tooltip = TypeMoq.It.isValue(pythonPath)), TypeMoq.Times.atLeastOnce()); + statusBar.verify(s => (s.text = TypeMoq.It.isValue(displayName)), TypeMoq.Times.once()); }); test('If interpreter file does not exist then update status bar accordingly', async () => { const resource = Uri.file('x'); @@ -164,26 +206,26 @@ suite('Interpreters Display', () => { setupWorkspaceFolder(resource, workspaceFolder); // tslint:disable-next-line:no-any interpreterService - .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve([{} as any])); interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(undefined)); - configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); - fileSystem.setup((f) => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); interpreterHelper - .setup((v) => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) + .setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(undefined)); virtualEnvMgr - .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve('')); await interpreterDisplay.refresh(resource); - statusBar.verify((s) => (s.color = TypeMoq.It.isValue('yellow')), TypeMoq.Times.once()); + statusBar.verify(s => (s.color = TypeMoq.It.isValue('yellow')), TypeMoq.Times.once()); statusBar.verify( - (s) => (s.text = TypeMoq.It.isValue('$(alert) Select Python Interpreter')), + s => (s.text = TypeMoq.It.isValue('$(alert) Select Python Interpreter')), TypeMoq.Times.once() ); }); @@ -198,16 +240,16 @@ suite('Interpreters Display', () => { companyDisplayName: 'Company Name', path: pythonPath }; - fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); virtualEnvMgr - .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve('')); interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) .returns(() => Promise.resolve(activeInterpreter)) .verifiable(TypeMoq.Times.once()); interpreterHelper - .setup((i) => i.getActiveWorkspaceUri(undefined)) + .setup(i => i.getActiveWorkspaceUri(undefined)) .returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }) @@ -217,7 +259,7 @@ suite('Interpreters Display', () => { interpreterHelper.verifyAll(); interpreterService.verifyAll(); - statusBar.verify((s) => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); - statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)!), TypeMoq.Times.once()); + statusBar.verify(s => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); + statusBar.verify(s => (s.tooltip = TypeMoq.It.isValue(pythonPath)!), TypeMoq.Times.atLeastOnce()); }); }); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index dd12a384234c..4655d8d59da9 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -12,14 +12,18 @@ import * as md5 from 'md5'; import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; +import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { DeprecatePythonPath } from '../../client/common/experimentGroups'; import { getArchitectureDisplayName } from '../../client/common/platform/registry'; import { IFileSystem } from '../../client/common/platform/types'; import { IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; import { IConfigurationService, IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, + InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory, IPythonSettings @@ -65,6 +69,8 @@ suite('Interpreters service', () => { let pythonExecutionFactory: TypeMoq.IMock; let pythonExecutionService: TypeMoq.IMock; let configService: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + let experimentsManager: TypeMoq.IMock; let pythonSettings: TypeMoq.IMock; let hashProviderFactory: TypeMoq.IMock; @@ -73,6 +79,8 @@ suite('Interpreters service', () => { serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); + experimentsManager = TypeMoq.Mock.ofType(); + interpreterPathService = TypeMoq.Mock.ofType(); updater = TypeMoq.Mock.ofType(); helper = TypeMoq.Mock.ofType(); locator = TypeMoq.Mock.ofType(); @@ -119,6 +127,11 @@ suite('Interpreters service', () => { INTERPRETER_LOCATOR_SERVICE ); serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance(IExperimentsManager, experimentsManager.object); + serviceManager.addSingletonInstance( + IInterpreterPathService, + interpreterPathService.object + ); serviceManager.addSingletonInstance(IInterpreterDisplay, interpreterDisplay.object); serviceManager.addSingletonInstance( IVirtualEnvironmentManager, @@ -180,6 +193,12 @@ suite('Interpreters service', () => { const service = new InterpreterService(serviceContainer, hashProviderFactory.object); const documentManager = TypeMoq.Mock.ofType(); + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + workspace.setup(w => w.hasWorkspaceFolders).returns(() => true); + workspace.setup(w => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let activeTextEditorChangeHandler: Function | undefined; documentManager .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -205,6 +224,12 @@ suite('Interpreters service', () => { const service = new InterpreterService(serviceContainer, hashProviderFactory.object); const documentManager = TypeMoq.Mock.ofType(); + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + workspace.setup(w => w.hasWorkspaceFolders).returns(() => true); + workspace.setup(w => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let activeTextEditorChangeHandler: Function | undefined; documentManager .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -220,6 +245,95 @@ suite('Interpreters service', () => { interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); }); + + test('If user belongs to Deprecate Pythonpath experiment, register the correct handler', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const documentManager = TypeMoq.Mock.ofType(); + + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + workspace.setup(w => w.hasWorkspaceFolders).returns(() => true); + workspace.setup(w => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let interpreterPathServiceHandler: Function | undefined; + documentManager + .setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { dispose: noop }; + }); + const i: InterpreterConfigurationScope = { + uri: Uri.parse('a'), + configTarget: ConfigurationTarget.Workspace + }; + configService.reset(); + configService + .setup(c => c.getSettings()) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + configService + .setup(c => c.getSettings(i.uri)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + interpreterPathService + .setup(d => d.onDidChange(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback(cb => (interpreterPathServiceHandler = cb)) + .returns(() => { + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + service.initialize(); + expect(interpreterPathServiceHandler).to.not.equal(undefined, 'Handler not set'); + + interpreterPathServiceHandler!(i); + + // Ensure correct handler was invoked + configService.verifyAll(); + }); + + test('If stored setting is an empty string, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const resource = Uri.parse('a'); + service._pythonPathSetting = ''; + configService.reset(); + configService.setup(c => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + interpreterDisplay + .setup(i => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + }); + + test('If stored setting is not equal to current interpreter path setting, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const resource = Uri.parse('a'); + service._pythonPathSetting = 'stored setting'; + configService.reset(); + configService.setup(c => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + interpreterDisplay + .setup(i => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + }); + + test('If stored setting is equal to current interpreter path setting, do not refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const resource = Uri.parse('a'); + service._pythonPathSetting = 'setting'; + configService.reset(); + configService.setup(c => c.getSettings(resource)).returns(() => ({ pythonPath: 'setting' } as any)); + interpreterDisplay + .setup(i => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + }); }); suite('Get Interpreter Details', () => { diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts deleted file mode 100644 index 86f695bde851..000000000000 --- a/src/test/interpreters/pythonPathUpdater.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { IPythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -// tslint:disable:no-invalid-template-strings max-func-body-length - -suite('Python Path Settings Updater', () => { - let serviceContainer: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let updaterServiceFactory: IPythonPathUpdaterServiceFactory; - function setupMocks() { - serviceContainer = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); - } - function setupConfigProvider(resource?: Uri): TypeMoq.IMock { - const workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) - .returns(() => workspaceConfig.object); - return workspaceConfig; - } - suite('Global', () => { - setup(setupMocks); - test('Python Path should not be updated when current pythonPath is the same', async () => { - const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); - const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(); - workspaceConfig - .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) - .returns(() => { - // tslint:disable-next-line:no-any - return { globalValue: pythonPath } as any; - }); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never() - ); - }); - test('Python Path should be updated when current pythonPath is different', async () => { - const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); - const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(); - workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => - w.update( - TypeMoq.It.isValue('pythonPath'), - TypeMoq.It.isValue(pythonPath), - TypeMoq.It.isValue(true) - ), - TypeMoq.Times.once() - ); - }); - }); - - suite('WorkspaceFolder', () => { - setup(setupMocks); - test('Python Path should not be updated when current pythonPath is the same', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); - const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig - .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) - .returns(() => { - // tslint:disable-next-line:no-any - return { workspaceFolderValue: pythonPath } as any; - }); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never() - ); - }); - test('Python Path should be updated when current pythonPath is different', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); - const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => - w.update( - TypeMoq.It.isValue('pythonPath'), - TypeMoq.It.isValue(pythonPath), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder) - ), - TypeMoq.Times.once() - ); - }); - test('Python Path should be truncated for worspace-relative paths', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); - const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; - const expectedPythonPath = path.join('env', 'bin', 'python'); - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => - w.update( - TypeMoq.It.isValue('pythonPath'), - TypeMoq.It.isValue(expectedPythonPath), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder) - ), - TypeMoq.Times.once() - ); - }); - }); - suite('Workspace (multiroot scenario)', () => { - setup(setupMocks); - test('Python Path should not be updated when current pythonPath is the same', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); - const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig - .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) - .returns(() => { - // tslint:disable-next-line:no-any - return { workspaceValue: pythonPath } as any; - }); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never() - ); - }); - test('Python Path should be updated when current pythonPath is different', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); - const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => - w.update( - TypeMoq.It.isValue('pythonPath'), - TypeMoq.It.isValue(pythonPath), - TypeMoq.It.isValue(false) - ), - TypeMoq.Times.once() - ); - }); - test('Python Path should be truncated for workspace-relative paths', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); - const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; - const expectedPythonPath = path.join('env', 'bin', 'python'); - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify( - (w) => - w.update( - TypeMoq.It.isValue('pythonPath'), - TypeMoq.It.isValue(expectedPythonPath), - TypeMoq.It.isValue(false) - ), - TypeMoq.Times.once() - ); - }); - }); -}); diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts new file mode 100644 index 000000000000..bd22ea0bc726 --- /dev/null +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -0,0 +1,347 @@ +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { DeprecatePythonPath } from '../../client/common/experimentGroups'; +import { IExperimentsManager, IInterpreterPathService } from '../../client/common/types'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { IPythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +// tslint:disable:no-invalid-template-strings max-func-body-length + +suite('Python Path Settings Updater', () => { + let serviceContainer: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let experimentsManager: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + let updaterServiceFactory: IPythonPathUpdaterServiceFactory; + function setupMocks(inExperiment: boolean = false) { + serviceContainer = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); + experimentsManager.setup(e => e.inExperiment(TypeMoq.It.isAny())).returns(() => inExperiment); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterPathService = TypeMoq.Mock.ofType(); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IExperimentsManager))) + .returns(() => experimentsManager.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); + updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); + } + function setupConfigProvider(resource?: Uri): TypeMoq.IMock { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService + .setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + suite('When not in Deprecate PythonPath experiment', async () => { + suite('Global', () => { + setup(() => setupMocks(false)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => { + // tslint:disable-next-line:no-any + return { globalValue: pythonPath } as any; + }); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(pythonPath), + TypeMoq.It.isValue(true) + ), + TypeMoq.Times.once() + ); + }); + }); + + suite('WorkspaceFolder', () => { + setup(() => setupMocks(false)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig + .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => { + // tslint:disable-next-line:no-any + return { workspaceFolderValue: pythonPath } as any; + }); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(pythonPath), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder) + ), + TypeMoq.Times.once() + ); + }); + test('Python Path should be truncated for worspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(expectedPythonPath), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder) + ), + TypeMoq.Times.once() + ); + }); + }); + suite('Workspace (multiroot scenario)', () => { + setup(() => setupMocks(false)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig + .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => { + // tslint:disable-next-line:no-any + return { workspaceValue: pythonPath } as any; + }); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(pythonPath), + TypeMoq.It.isValue(false) + ), + TypeMoq.Times.once() + ); + }); + test('Python Path should be truncated for workspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + w => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(expectedPythonPath), + TypeMoq.It.isValue(false) + ), + TypeMoq.Times.once() + ); + }); + }); + }); + + suite('When in Deprecate PythonPath experiment', async () => { + suite('Global', () => { + setup(() => setupMocks(true)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + interpreterPathService + .setup(i => i.inspect(undefined)) + .returns(() => { + return { globalValue: pythonPath }; + }); + interpreterPathService + .setup(i => i.update(undefined, ConfigurationTarget.Global, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + interpreterPathService.setup(i => i.inspect(undefined)).returns(() => ({})); + + interpreterPathService + .setup(i => i.update(undefined, ConfigurationTarget.Global, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + }); + + suite('WorkspaceFolder', () => { + setup(() => setupMocks(true)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + interpreterPathService + .setup(i => i.inspect(workspaceFolder)) + .returns(() => ({ + workspaceFolderValue: pythonPath + })); + interpreterPathService + .setup(i => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup(i => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be truncated for workspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup(i => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, expectedPythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + }); + suite('Workspace (multiroot scenario)', () => { + setup(() => setupMocks(true)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + interpreterPathService + .setup(i => i.inspect(workspaceFolder)) + .returns(() => ({ workspaceValue: pythonPath })); + interpreterPathService + .setup(i => i.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + + interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup(i => i.update(workspaceFolder, ConfigurationTarget.Workspace, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + + interpreterPathService.verifyAll(); + }); + test('Python Path should be truncated for workspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + + interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup(i => i.update(workspaceFolder, ConfigurationTarget.Workspace, expectedPythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + + interpreterPathService.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/startupTelemetry.unit.test.ts b/src/test/startupTelemetry.unit.test.ts new file mode 100644 index 000000000000..ec8b554a43c4 --- /dev/null +++ b/src/test/startupTelemetry.unit.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../client/common/application/types'; +import { DeprecatePythonPath } from '../client/common/experimentGroups'; +import { IExperimentsManager, IInterpreterPathService } from '../client/common/types'; +import { IServiceContainer } from '../client/ioc/types'; +import { hasUserDefinedPythonPath } from '../client/startupTelemetry'; + +suite('Startup Telemetry - hasUserDefinedPythonPath()', async () => { + const resource = Uri.parse('a'); + let serviceContainer: TypeMoq.IMock; + let experimentsManager: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); + interpreterPathService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + experimentsManager + .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + serviceContainer.setup(s => s.get(IExperimentsManager)).returns(() => experimentsManager.object); + serviceContainer.setup(s => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup(s => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + }); + + function setupConfigProvider(): TypeMoq.IMock { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService + .setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + [undefined, 'python'].forEach(globalValue => { + [undefined, 'python'].forEach(workspaceValue => { + [undefined, 'python'].forEach(workspaceFolderValue => { + test(`Return false if using settings equals {globalValue: ${globalValue}, workspaceValue: ${workspaceValue}, workspaceFolderValue: ${workspaceFolderValue}}`, () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig + .setup(w => w.inspect('pythonPath')) + // tslint:disable-next-line: no-any + .returns(() => ({ globalValue, workspaceValue, workspaceFolderValue } as any)); + const result = hasUserDefinedPythonPath(resource, serviceContainer.object); + expect(result).to.equal(false, 'Should be false'); + }); + }); + }); + }); + + test('Return true if using setting value equals something else', () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup(w => w.inspect('pythonPath')).returns(() => ({ globalValue: 'something else' } as any)); + const result = hasUserDefinedPythonPath(resource, serviceContainer.object); + expect(result).to.equal(true, 'Should be true'); + }); + + test('If in Deprecate PythonPath experiment, use the new API to inspect settings', () => { + experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + interpreterPathService + .setup(i => i.inspect(resource)) + .returns(() => ({})) + .verifiable(TypeMoq.Times.once()); + hasUserDefinedPythonPath(resource, serviceContainer.object); + interpreterPathService.verifyAll(); + }); +}); From 05ba3ed3eb37f731ea1573c7d416ce32701f2979 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 7 Apr 2020 22:34:14 +0530 Subject: [PATCH 3/7] Prompt when an "unsafe" workspace python environment is to be autoselected (#10430) * Implement prompt when an unsafe workspace python environment is to be autoselected * Code reviews * Refactor pythonPath stuff into a private method * Use variable instead of hardcoding 'python' string * Added tests for src\client\interpreter\autoSelection\interpreterSecurity\interpreterSecurityCommands.ts * Don't register command in the constructor * Refactored code * Updated tests * Code reviews * Moved bannerLabelYes/No to Common() namespace * Update content for news entry * Corrected activation manager tests * Fix single workspace CI tests * Fix multiroot tests + Code reviews * Rename Interpreter Security clear command * Fix tests * Added intellisense for pythonpath experiment * Fix multiroot tests * Rename `Reset interpreter` command to `Clear workspace interpreter setting * Fix bug with interpreter security storage --- experiments.json | 13 +- news/1 Enhancements/10879.md | 1 + news/1 Enhancements/10912.md | 1 + package.json | 22 +- package.nls.json | 9 +- src/client/activation/activationManager.ts | 53 +++- src/client/common/application/commands.ts | 3 +- src/client/common/configSettings.ts | 163 ++++++----- src/client/common/configuration/service.ts | 11 +- src/client/common/constants.ts | 3 +- src/client/common/experimentGroups.ts | 3 - src/client/common/utils/localize.ts | 9 +- src/client/datascience/shiftEnterBanner.ts | 5 +- .../interpreter/autoSelection/constants.ts | 10 + src/client/interpreter/autoSelection/index.ts | 12 +- .../interpreterEvaluation.ts | 92 ++++++ .../interpreterSecurityService.ts | 53 ++++ .../interpreterSecurityStorage.ts | 99 +++++++ .../autoSelection/rules/workspaceEnv.ts | 25 +- src/client/interpreter/autoSelection/types.ts | 26 +- .../configuration/interpreterSelector.ts | 2 +- src/client/interpreter/helpers.ts | 10 +- src/client/interpreter/serviceRegistry.ts | 15 +- .../virtualEnvs/condaInheritEnvPrompt.ts | 8 +- .../virtualEnvs/virtualEnvPrompt.ts | 8 +- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 13 + .../activation/activationManager.unit.test.ts | 238 +++++++++++++--- .../configSettings.pythonPath.unit.test.ts | 97 ++++++- src/test/common/configuration/service.test.ts | 17 +- .../common/configuration/service.unit.test.ts | 22 ++ src/test/common/installer.test.ts | 1 + src/test/common/moduleInstaller.test.ts | 1 + .../envVarsProvider.multiroot.test.ts | 1 + src/test/format/extension.format.test.ts | 1 + src/test/format/extension.sort.test.ts | 1 + .../autoSelection/index.unit.test.ts | 20 +- .../interpreterEvaluation.unit.test.ts | 261 ++++++++++++++++++ .../interpreterSecurityService.unit.test.ts | 168 +++++++++++ .../interpreterSecurityStorage.unit.test.ts | 160 +++++++++++ .../rules/workspaceEnv.unit.test.ts | 63 ++--- .../interpreters/serviceRegistry.unit.test.ts | 12 +- .../condaInheritEnvPrompt.unit.test.ts | 108 ++++---- .../virtualEnvs/virtualEnvPrompt.unit.test.ts | 32 +-- src/test/linters/lint.multiroot.test.ts | 1 + .../extension.refactor.extract.method.test.ts | 1 + src/test/testing/debugger.test.ts | 1 + .../nosetest/nosetest.disovery.test.ts | 1 + .../testing/nosetest/nosetest.run.test.ts | 1 + src/test/testing/nosetest/nosetest.test.ts | 1 + .../testing/pytest/pytest.discovery.test.ts | 1 + src/test/testing/pytest/pytest.run.test.ts | 1 + src/test/testing/pytest/pytest.test.ts | 1 + src/test/testing/rediscover.test.ts | 1 + src/test/testing/serviceRegistry.ts | 17 ++ .../testing/stoppingDiscoverAndTest.test.ts | 1 + .../unittest/unittest.discovery.test.ts | 1 + .../testing/unittest/unittest.run.test.ts | 1 + src/test/testing/unittest/unittest.test.ts | 1 + 59 files changed, 1572 insertions(+), 332 deletions(-) create mode 100644 news/1 Enhancements/10879.md create mode 100644 news/1 Enhancements/10912.md create mode 100644 src/client/interpreter/autoSelection/constants.ts create mode 100644 src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts create mode 100644 src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts create mode 100644 src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts create mode 100644 src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts create mode 100644 src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts create mode 100644 src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts diff --git a/experiments.json b/experiments.json index a77f0700847d..79154fa0a51b 100644 --- a/experiments.json +++ b/experiments.json @@ -160,6 +160,17 @@ "salt": "EnableIPyWidgets", "min": 0, "max": 0 + }, + { + "name": "DeprecatePythonPath - experiment", + "salt": "DeprecatePythonPath", + "min": 0, + "max": 0 + }, + { + "name": "DeprecatePythonPath - control", + "salt": "DeprecatePythonPath", + "min": 100, + "max": 100 } - ] diff --git a/news/1 Enhancements/10879.md b/news/1 Enhancements/10879.md new file mode 100644 index 000000000000..63825b5e906e --- /dev/null +++ b/news/1 Enhancements/10879.md @@ -0,0 +1 @@ +Prompt when an "untrusted" workspace Python environment is to be auto selected. diff --git a/news/1 Enhancements/10912.md b/news/1 Enhancements/10912.md new file mode 100644 index 000000000000..34afd920706d --- /dev/null +++ b/news/1 Enhancements/10912.md @@ -0,0 +1 @@ +Added a command to reset "untrusted" interpreters storage. diff --git a/package.json b/package.json index 97afe3a23211..5b6d5dae5644 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,8 @@ "onCommand:python.switchOffInsidersChannel", "onCommand:python.switchToDailyChannel", "onCommand:python.switchToWeeklyChannel", - "onCommand:python.resetPythonInterpreter", + "onCommand:python.clearWorkspaceInterpreter", + "onCommand:python.resetInterpreterSecurityStorage", "onCommand:python.datascience.createnewnotebook", "onCommand:python.datascience.showhistorypane", "onCommand:python.datascience.importnotebook", @@ -226,8 +227,13 @@ "category": "Python" }, { - "command": "python.resetPythonInterpreter", - "title": "%python.command.python.resetPythonInterpreter.title%", + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "category": "Python" + }, + { + "command": "python.resetInterpreterSecurityStorage", + "title": "%python.command.python.resetInterpreterSecurityStorage.title%", "category": "Python" }, { @@ -760,8 +766,13 @@ "when": "config.python.insidersChannel != 'weekly'" }, { - "command": "python.resetPythonInterpreter", - "title": "%python.command.python.resetPythonInterpreter.title%", + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "category": "Python" + }, + { + "command": "python.resetInterpreterSecurityStorage", + "title": "%python.command.python.resetInterpreterSecurityStorage.title%", "category": "Python" }, { @@ -1611,6 +1622,7 @@ "UseTerminalToGetActivatedEnvVars - experiment", "CollectLSRequestTiming - experiment", "CollectNodeLSRequestTiming - experiment", + "DeprecatePythonPath - experiment", "All" ] }, diff --git a/package.nls.json b/package.nls.json index 7561efe83563..3b255b6f29e0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,7 +10,8 @@ "python.command.python.switchOffInsidersChannel.title": "Switch to Default Channel", "python.command.python.switchToDailyChannel.title": "Switch to Insiders Daily Channel", "python.command.python.switchToWeeklyChannel.title": "Switch to Insiders Weekly Channel", - "python.command.python.resetPythonInterpreter.title": "Reset Python Interpreter", + "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", + "python.command.python.resetInterpreterSecurityStorage.title": "Reset stored info for untrusted Interpreters", "python.command.python.refactorExtractVariable.title": "Extract Variable", "python.command.python.refactorExtractMethod.title": "Extract Method", "python.command.python.viewOutput.title": "Show Output", @@ -135,13 +136,17 @@ "Interpreters.entireWorkspace": "Entire workspace", "Interpreters.pythonInterpreterPath": "Python interpreter path: {0}", "Interpreters.LoadingInterpreters": "Loading Python Interpreters", + "Interpreters.unsafeInterpreterMessage": "We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.", "Interpreters.condaInheritEnvMessage": "We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change \"terminal.integrated.inheritEnv\" to false in your user settings.", "Logging.CurrentWorkingDirectory": "cwd:", + "Common.bannerLabelYes": "Yes", + "Common.bannerLabelNo": "No", "Common.doNotShowAgain": "Do not show again", "Common.reload": "Reload", "Common.moreInfo": "More Info", "Common.and": "and", "Common.install": "Install", + "Common.learnMore": "Learn more", "OutputChannelNames.languageServer": "Python Language Server", "OutputChannelNames.python": "Python", "OutputChannelNames.pythonTest": "Python Test Log", @@ -169,8 +174,6 @@ "DataScienceSurveyBanner.bannerLabelYes": "Yes, take survey now", "DataScienceSurveyBanner.bannerLabelNo": "No, thanks", "InteractiveShiftEnterBanner.bannerMessage": "Would you like to run code in the 'Python Interactive' window (an IPython console) for 'shift-enter'? Select 'No' to continue to run code in the Python Terminal. This can be changed later in settings.", - "InteractiveShiftEnterBanner.bannerLabelYes": "Yes", - "InteractiveShiftEnterBanner.bannerLabelNo": "No", "DataScience.restartingKernelStatus": "Restarting IPython Kernel", "DataScience.executingCode": "Executing Cell", "DataScience.collapseAll": "Collapse all cell inputs", diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index f0de8f49b4c7..8d99473f31b0 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -7,11 +7,13 @@ import { inject, injectable, multiInject } from 'inversify'; import { TextDocument } from 'vscode'; import { IApplicationDiagnostics } from '../application/types'; import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_LANGUAGE } from '../common/constants'; +import { DEFAULT_INTERPRETER_SETTING, PYTHON_LANGUAGE } from '../common/constants'; +import { DeprecatePythonPath } from '../common/experimentGroups'; import { traceDecorators } from '../common/logger'; import { IFileSystem } from '../common/platform/types'; -import { IDisposable, Resource } from '../common/types'; -import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types'; +import { IDisposable, IExperimentsManager, IInterpreterPathService, Resource } from '../common/types'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { IInterpreterAutoSelectionService, IInterpreterSecurityService } from '../interpreter/autoSelection/types'; import { IInterpreterService } from '../interpreter/contracts'; import { sendActivationTelemetry } from '../telemetry/envFileTelemetry'; import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService } from './types'; @@ -19,6 +21,7 @@ import { IExtensionActivationManager, IExtensionActivationService, IExtensionSin @injectable() export class ExtensionActivationManager implements IExtensionActivationManager { public readonly activatedWorkspaces = new Set(); + protected readonly isInterpreterSetForWorkspacePromises = new Map>(); private readonly disposables: IDisposable[] = []; private docOpenedHandler?: IDisposable; constructor( @@ -31,7 +34,10 @@ export class ExtensionActivationManager implements IExtensionActivationManager { @inject(IApplicationDiagnostics) private readonly appDiagnostics: IApplicationDiagnostics, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IFileSystem) private readonly fileSystem: IFileSystem, - @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + @inject(IInterpreterSecurityService) private readonly interpreterSecurityService: IInterpreterSecurityService ) {} public dispose() { @@ -48,7 +54,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { await this.initialize(); // Activate all activation services together. await Promise.all([ - Promise.all(this.singleActivationServices.map((item) => item.activate())), + Promise.all(this.singleActivationServices.map(item => item.activate())), this.activateWorkspace(this.activeResourceService.getActiveResource()) ]); await this.autoSelection.autoSelectInterpreter(undefined); @@ -66,7 +72,8 @@ export class ExtensionActivationManager implements IExtensionActivationManager { await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource); await this.autoSelection.autoSelectInterpreter(resource); - await Promise.all(this.activationServices.map((item) => item.activate(resource))); + await this.evaluateAutoSelectedInterpreterSafety(resource); + await Promise.all(this.activationServices.map(item => item.activate(resource))); await this.appDiagnostics.performPreStartupHealthCheck(resource); } public async initialize() { @@ -88,8 +95,38 @@ export class ExtensionActivationManager implements IExtensionActivationManager { const folder = this.workspaceService.getWorkspaceFolder(doc.uri); this.activateWorkspace(folder ? folder.uri : undefined).ignoreErrors(); } + + public async evaluateAutoSelectedInterpreterSafety(resource: Resource) { + if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { + const workspaceKey = this.getWorkspaceKey(resource); + const interpreterSettingValue = this.interpreterPathService.get(resource); + if (interpreterSettingValue.length === 0 || interpreterSettingValue === DEFAULT_INTERPRETER_SETTING) { + // Setting is not set, extension will use the autoselected value. Make sure it's safe. + const interpreter = this.autoSelection.getAutoSelectedInterpreter(resource); + if (interpreter) { + const isInterpreterSetForWorkspace = createDeferred(); + this.isInterpreterSetForWorkspacePromises.set(workspaceKey, isInterpreterSetForWorkspace); + await Promise.race([ + isInterpreterSetForWorkspace.promise, + this.interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter, resource) + ]); + } + } else { + // Resolve any concurrent calls waiting on the promise + if (this.isInterpreterSetForWorkspacePromises.has(workspaceKey)) { + this.isInterpreterSetForWorkspacePromises.get(workspaceKey)!.resolve(); + this.isInterpreterSetForWorkspacePromises.delete(workspaceKey); + } + } + } + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + } + protected addHandlers() { this.disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); + this.disposables.push( + this.interpreterPathService.onDidChange(i => this.evaluateAutoSelectedInterpreterSafety(i.uri)) + ); } protected addRemoveDocOpenedHandlers() { if (this.hasMultipleWorkspaces()) { @@ -105,11 +142,11 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } protected onWorkspaceFoldersChanged() { //If an activated workspace folder was removed, delete its key - const workspaceKeys = this.workspaceService.workspaceFolders!.map((workspaceFolder) => + const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspaceKey(workspaceFolder.uri) ); const activatedWkspcKeys = Array.from(this.activatedWorkspaces.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); if (activatedWkspcFoldersRemoved.length > 0) { for (const folder of activatedWkspcFoldersRemoved) { this.activatedWorkspaces.delete(folder); diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 698e26e07e8a..5d803bfb9309 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -22,7 +22,8 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; interface ICommandNameWithoutArgumentTypeMapping { [Commands.SwitchToInsidersDaily]: []; [Commands.SwitchToInsidersWeekly]: []; - [Commands.ResetPythonInterpreter]: []; + [Commands.ClearWorkspaceInterpreter]: []; + [Commands.ResetInterpreterSecurityStorage]: []; [Commands.SwitchOffInsidersChannel]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 793b46198043..cb6f64e303fa 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -14,7 +14,7 @@ import { } from 'vscode'; import { LanguageServerType } from '../activation/types'; import '../common/extensions'; -import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types'; +import { IInterpreterAutoSeletionProxyService, IInterpreterSecurityService } from '../interpreter/autoSelection/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { IWorkspaceService } from './application/types'; @@ -47,6 +47,41 @@ const untildify = require('untildify'); // tslint:disable-next-line:completed-docs export class PythonSettings implements IPythonSettings { + public get onDidChange(): Event { + return this.changed.event; + } + + public get pythonPath(): string { + return this._pythonPath; + } + public set pythonPath(value: string) { + if (this._pythonPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._pythonPath = this.getPythonExecutable(value); + } catch (ex) { + this._pythonPath = value; + } + } + + public get defaultInterpreterPath(): string { + return this._defaultInterpreterPath; + } + public set defaultInterpreterPath(value: string) { + if (this._defaultInterpreterPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._defaultInterpreterPath = this.getPythonExecutable(value); + } catch (ex) { + this._defaultInterpreterPath = value; + } + } private static pythonSettings: Map = new Map(); public downloadLanguageServer = true; public jediEnabled = true; @@ -82,16 +117,14 @@ export class PythonSettings implements IPythonSettings { private _pythonPath = ''; private _defaultInterpreterPath = ''; private readonly workspace: IWorkspaceService; - public get onDidChange(): Event { - return this.changed.event; - } constructor( workspaceFolder: Resource, private readonly interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, workspace?: IWorkspaceService, private readonly experimentsManager?: IExperimentsManager, - private readonly interpreterPathService?: IInterpreterPathService + private readonly interpreterPathService?: IInterpreterPathService, + private readonly interpreterSecurityService?: IInterpreterSecurityService ) { this.workspace = workspace || new WorkspaceService(); this.workspaceRoot = workspaceFolder; @@ -103,7 +136,8 @@ export class PythonSettings implements IPythonSettings { interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, workspace?: IWorkspaceService, experimentsManager?: IExperimentsManager, - interpreterPathService?: IInterpreterPathService + interpreterPathService?: IInterpreterPathService, + interpreterSecurityService?: IInterpreterSecurityService ): PythonSettings { workspace = workspace || new WorkspaceService(); const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; @@ -115,7 +149,8 @@ export class PythonSettings implements IPythonSettings { interpreterAutoSelectionService, workspace, experimentsManager, - interpreterPathService + interpreterPathService, + interpreterSecurityService ); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); // Pass null to avoid VSC from complaining about not passing in a value. @@ -167,35 +202,7 @@ export class PythonSettings implements IPythonSettings { const workspaceRoot = this.workspaceRoot?.fsPath; const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace); - /** - * Note that while calling `IExperimentsManager.inExperiment()`, we assume `IExperimentsManager.activate()` is already called. - * That's not true here, as this method is often called in the constructor,which runs before `.activate()` methods. - * But we can still use it here for this particular experiment. Reason being that this experiment only changes - * `pythonPath` setting, and I've checked that `pythonPath` setting is not accessed anywhere in the constructor. - */ - if (this.experimentsManager && this.interpreterPathService) { - if (this.experimentsManager.inExperiment(DeprecatePythonPath.experiment)) { - this.pythonPath = systemVariables.resolveAny(this.interpreterPathService.get(this.workspaceRoot))!; - } else { - this.pythonPath = systemVariables.resolveAny(pythonSettings.get('pythonPath'))!; - } - this.experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control); - } else { - 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.workspaceRoot) { - this.interpreterAutoSelectionService - .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) - .ignoreErrors(); - } - this.pythonPath = autoSelectedPythonInterpreter ? autoSelectedPythonInterpreter.path : this.pythonPath; - } - this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot); + this.pythonPath = this.getPythonPath(pythonSettings, systemVariables, workspaceRoot); // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion const defaultInterpreterPath = systemVariables.resolveAny(pythonSettings.get('defaultInterpreterPath')); @@ -524,37 +531,6 @@ export class PythonSettings implements IPythonSettings { this.insidersChannel = pythonSettings.get('insidersChannel')!; } - public get pythonPath(): string { - return this._pythonPath; - } - public set pythonPath(value: string) { - if (this._pythonPath === value) { - return; - } - // Add support for specifying just the directory where the python executable will be located. - // E.g. virtual directory name. - try { - this._pythonPath = this.getPythonExecutable(value); - } catch (ex) { - this._pythonPath = value; - } - } - - public get defaultInterpreterPath(): string { - return this._defaultInterpreterPath; - } - public set defaultInterpreterPath(value: string) { - if (this._defaultInterpreterPath === value) { - return; - } - // Add support for specifying just the directory where the python executable will be located. - // E.g. virtual directory name. - try { - this._defaultInterpreterPath = this.getPythonExecutable(value); - } catch (ex) { - this._defaultInterpreterPath = value; - } - } protected getPythonExecutable(pythonPath: string) { return getPythonExecutable(pythonPath); } @@ -582,6 +558,9 @@ export class PythonSettings implements IPythonSettings { this.disposables.push( this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)) ); + if (this.interpreterSecurityService) { + this.disposables.push(this.interpreterSecurityService.onDidChangeSafeInterpreters(onDidChange.bind(this))); + } this.disposables.push( this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { if (event.affectsConfiguration('python')) { @@ -602,6 +581,56 @@ export class PythonSettings implements IPythonSettings { protected debounceChangeNotification() { this.changed.fire(); } + + private getPythonPath( + pythonSettings: WorkspaceConfiguration, + systemVariables: SystemVariables, + workspaceRoot: string | undefined + ) { + /** + * Note that while calling `IExperimentsManager.inExperiment()`, we assume `IExperimentsManager.activate()` is already called. + * That's not true here, as this method is often called in the constructor,which runs before `.activate()` methods. + * But we can still use it here for this particular experiment. Reason being that this experiment only changes + * `pythonPath` setting, and I've checked that `pythonPath` setting is not accessed anywhere in the constructor. + */ + const inExperiment = this.experimentsManager?.inExperiment(DeprecatePythonPath.experiment); + this.experimentsManager?.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + // Use the interpreter path service if in the experiment otherwise use the normal settings + this.pythonPath = systemVariables.resolveAny( + inExperiment && this.interpreterPathService + ? this.interpreterPathService.get(this.workspaceRoot) + : pythonSettings.get('pythonPath') + )!; + if (this.pythonPath.length === 0 || this.pythonPath === 'python') { + const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter( + this.workspaceRoot + ); + if (inExperiment && this.interpreterSecurityService) { + if ( + autoSelectedPythonInterpreter && + this.interpreterSecurityService.isSafe(autoSelectedPythonInterpreter) && + this.workspaceRoot + ) { + this.pythonPath = autoSelectedPythonInterpreter.path; + this.interpreterAutoSelectionService + .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) + .ignoreErrors(); + } + } else { + if (autoSelectedPythonInterpreter && this.workspaceRoot) { + this.pythonPath = autoSelectedPythonInterpreter.path; + this.interpreterAutoSelectionService + .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) + .ignoreErrors(); + } + } + } + if (inExperiment && this.pythonPath === DEFAULT_INTERPRETER_SETTING) { + // This is to ensure that we ask users to select an interpreter in case auto selected interpreter is not safe to select + this.pythonPath = ''; + } + return getAbsolutePath(this.pythonPath, workspaceRoot); + } } function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): string { diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index e6da5ab0c19d..00a8c789f204 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -3,7 +3,10 @@ import { inject, injectable } from 'inversify'; import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { IInterpreterAutoSeletionProxyService } from '../../interpreter/autoSelection/types'; +import { + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from '../../interpreter/autoSelection/types'; import { IServiceContainer } from '../../ioc/types'; import { IWorkspaceService } from '../application/types'; import { PythonSettings } from '../configSettings'; @@ -23,12 +26,16 @@ export class ConfigurationService implements IConfigurationService { ); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); const experiments = this.serviceContainer.get(IExperimentsManager); + const interpreterSecurityService = this.serviceContainer.get( + IInterpreterSecurityService + ); return PythonSettings.getInstance( resource, InterpreterAutoSelectionService, this.workspaceService, experiments, - interpreterPathService + interpreterPathService, + interpreterSecurityService ); } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index c573d052475a..9e95e9a0c89c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -63,7 +63,8 @@ export namespace Commands { export const SwitchToInsidersDaily = 'python.switchToDailyChannel'; export const SwitchToInsidersWeekly = 'python.switchToWeeklyChannel'; export const PickLocalProcess = 'python.pickLocalProcess'; - export const ResetPythonInterpreter = 'python.resetPythonInterpreter'; + export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; + export const ResetInterpreterSecurityStorage = 'python.resetInterpreterSecurityStorage'; } export namespace Octicons { export const Test_Pass = '$(check)'; diff --git a/src/client/common/experimentGroups.ts b/src/client/common/experimentGroups.ts index 6f14c5c3bdfc..d98f18c409ce 100644 --- a/src/client/common/experimentGroups.ts +++ b/src/client/common/experimentGroups.ts @@ -85,9 +85,6 @@ export enum EnableIPyWidgets { /* * Experiment to check whether the extension should deprecate `python.pythonPath` setting - - * Note: 'DeprecatePythonPath - experiment' string is directly used in `src\test\common.ts` instead - * of accessing through the object. Be sure to remove it when removing the experiment. */ export enum DeprecatePythonPath { control = 'DeprecatePythonPath - control', diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index d658303c562b..86e972070afc 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -59,6 +59,8 @@ export namespace Diagnostics { } export namespace Common { + export const bannerLabelYes = localize('Common.bannerLabelYes', 'Yes'); + export const bannerLabelNo = localize('Common.bannerLabelNo', 'No'); export const canceled = localize('Common.canceled', 'Canceled'); export const cancel = localize('Common.cancel', 'Cancel'); export const gotIt = localize('Common.gotIt', 'Got it!'); @@ -70,6 +72,7 @@ export namespace Common { export const doNotShowAgain = localize('Common.doNotShowAgain', 'Do not show again'); export const reload = localize('Common.reload', 'Reload'); export const moreInfo = localize('Common.moreInfo', 'More Info'); + export const learnMore = localize('Common.learnMore', 'Learn more'); export const and = localize('Common.and', 'and'); } @@ -136,6 +139,10 @@ export namespace Interpreters { 'Interpreters.condaInheritEnvMessage', 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings.' ); + export const unsafeInterpreterMessage = localize( + 'Interpreters.unsafeInterpreterMessage', + 'We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.' + ); export const environmentPromptMessage = localize( 'Interpreters.environmentPromptMessage', 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?' @@ -208,8 +215,6 @@ export namespace InteractiveShiftEnterBanner { 'InteractiveShiftEnterBanner.bannerMessage', 'Would you like shift-enter to send code to the new Interactive Window experience?' ); - export const bannerLabelYes = localize('InteractiveShiftEnterBanner.bannerLabelYes', 'Yes'); - export const bannerLabelNo = localize('InteractiveShiftEnterBanner.bannerLabelNo', 'No'); } export namespace DataScienceSurveyBanner { diff --git a/src/client/datascience/shiftEnterBanner.ts b/src/client/datascience/shiftEnterBanner.ts index e717011f69cc..7fadde77a3a6 100644 --- a/src/client/datascience/shiftEnterBanner.ts +++ b/src/client/datascience/shiftEnterBanner.ts @@ -28,10 +28,7 @@ export class InteractiveShiftEnterBanner implements IPythonExtensionBanner { private initialized?: boolean; private disabledInCurrentSession: boolean = false; private bannerMessage: string = localize.InteractiveShiftEnterBanner.bannerMessage(); - private bannerLabels: string[] = [ - localize.InteractiveShiftEnterBanner.bannerLabelYes(), - localize.InteractiveShiftEnterBanner.bannerLabelNo() - ]; + private bannerLabels: string[] = [localize.Common.bannerLabelYes(), localize.Common.bannerLabelNo()]; constructor( @inject(IApplicationShell) private appShell: IApplicationShell, diff --git a/src/client/interpreter/autoSelection/constants.ts b/src/client/interpreter/autoSelection/constants.ts new file mode 100644 index 000000000000..0410458482c0 --- /dev/null +++ b/src/client/interpreter/autoSelection/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const unsafeInterpreterPromptKey = 'unsafeInterpreterPromptKey'; +export const unsafeInterpretersKey = 'unsafeInterpretersKey'; +export const safeInterpretersKey = 'safeInterpretersKey'; +export const flaggedWorkspacesKeysStorageKey = 'flaggedWorkspacesKeysStorageKey'; +export const learnMoreOnInterpreterSecurityURI = 'https://aka.ms/AA7jfor'; diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index 0696901fd9cc..1d59b1bb8387 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -18,7 +18,8 @@ import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService } from './types'; const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; @@ -54,7 +55,8 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio @named(AutoSelectionRule.workspaceVirtualEnvs) workspaceInterpreter: IInterpreterAutoSelectionRule, @inject(IInterpreterAutoSeletionProxyService) proxy: IInterpreterAutoSeletionProxyService, - @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterSecurityService) private readonly interpreterSecurityService: IInterpreterSecurityService ) { // 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). @@ -114,6 +116,12 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio // 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 interpreter = this._getAutoSelectedInterpreter(resource); + // Unless the interpreter is marked as unsafe, return interpreter. + return interpreter && this.interpreterSecurityService.isSafe(interpreter) === false ? undefined : interpreter; + } + + public _getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { const workspaceState = this.getWorkspaceState(resource); if (workspaceState && workspaceState.value) { return workspaceState.value; diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts new file mode 100644 index 000000000000..21d53dc10bf7 --- /dev/null +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { IBrowserService, Resource } from '../../../common/types'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IInterpreterHelper, PythonInterpreter } from '../../contracts'; +import { isInterpreterLocatedInWorkspace } from '../../helpers'; +import { learnMoreOnInterpreterSecurityURI } from '../constants'; +import { IInterpreterEvaluation, IInterpreterSecurityStorage } from '../types'; + +const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.learnMore(), Common.doNotShowAgain()]; +const telemetrySelections: ['Yes', 'No', 'Learn more', 'Do not show again'] = [ + 'Yes', + 'No', + 'Learn more', + 'Do not show again' +]; + +@injectable() +export class InterpreterEvaluation implements IInterpreterEvaluation { + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IBrowserService) private browserService: IBrowserService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterSecurityStorage) private readonly interpreterSecurityStorage: IInterpreterSecurityStorage + ) {} + + public async evaluateIfInterpreterIsSafe(interpreter: PythonInterpreter, resource: Resource): Promise { + const activeWorkspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + if (!activeWorkspaceUri) { + return true; + } + const isSafe = this.inferValueUsingCurrentState(interpreter, resource); + return isSafe !== undefined ? isSafe : this._inferValueUsingPrompt(activeWorkspaceUri); + } + + public inferValueUsingCurrentState(interpreter: PythonInterpreter, resource: Resource) { + const activeWorkspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + if (!activeWorkspaceUri) { + return true; + } + if (!isInterpreterLocatedInWorkspace(interpreter, activeWorkspaceUri)) { + return true; + } + const isSafe = this.interpreterSecurityStorage.hasUserApprovedWorkspaceInterpreters(activeWorkspaceUri).value; + if (isSafe !== undefined) { + return isSafe; + } + if (!this.interpreterSecurityStorage.unsafeInterpreterPromptEnabled.value) { + // If the prompt is disabled, assume all environments are safe from now on. + return true; + } + } + + public async _inferValueUsingPrompt(activeWorkspaceUri: Uri): Promise { + const areInterpretersInWorkspaceSafe = this.interpreterSecurityStorage.hasUserApprovedWorkspaceInterpreters( + activeWorkspaceUri + ); + await this.interpreterSecurityStorage.storeKeyForWorkspace(activeWorkspaceUri); + let selection = await this.showPromptAndGetSelection(); + while (selection === Common.learnMore()) { + this.browserService.launch(learnMoreOnInterpreterSecurityURI); + selection = await this.showPromptAndGetSelection(); + } + if (!selection || selection === Common.bannerLabelNo()) { + await areInterpretersInWorkspaceSafe.updateValue(false); + return false; + } else if (selection === Common.doNotShowAgain()) { + await this.interpreterSecurityStorage.unsafeInterpreterPromptEnabled.updateValue(false); + } + await areInterpretersInWorkspaceSafe.updateValue(true); + return true; + } + + private async showPromptAndGetSelection(): Promise { + const selection = await this.appShell.showInformationMessage( + Interpreters.unsafeInterpreterMessage(), + ...prompts + ); + sendTelemetryEvent(EventName.UNSAFE_INTERPRETER_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + }); + return selection; + } +} diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts new file mode 100644 index 000000000000..b495405587e0 --- /dev/null +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { Resource } from '../../../common/types'; +import { PythonInterpreter } from '../../contracts'; +import { IInterpreterEvaluation, IInterpreterSecurityService, IInterpreterSecurityStorage } from '../types'; + +@injectable() +export class InterpreterSecurityService implements IInterpreterSecurityService { + public _didSafeInterpretersChange = new EventEmitter(); + constructor( + @inject(IInterpreterSecurityStorage) private readonly interpreterSecurityStorage: IInterpreterSecurityStorage, + @inject(IInterpreterEvaluation) private readonly interpreterEvaluation: IInterpreterEvaluation + ) {} + + public isSafe(interpreter: PythonInterpreter, resource?: Resource): boolean | undefined { + const unsafeInterpreters = this.interpreterSecurityStorage.unsafeInterpreters.value; + if (unsafeInterpreters.includes(interpreter.path)) { + return false; + } + const safeInterpreters = this.interpreterSecurityStorage.safeInterpreters.value; + if (safeInterpreters.includes(interpreter.path)) { + return true; + } + return this.interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + } + + public async evaluateAndRecordInterpreterSafety(interpreter: PythonInterpreter, resource: Resource): Promise { + const unsafeInterpreters = this.interpreterSecurityStorage.unsafeInterpreters.value; + const safeInterpreters = this.interpreterSecurityStorage.safeInterpreters.value; + if (unsafeInterpreters.includes(interpreter.path) || safeInterpreters.includes(interpreter.path)) { + return; + } + const isSafe = await this.interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + if (isSafe) { + await this.interpreterSecurityStorage.safeInterpreters.updateValue([interpreter.path, ...safeInterpreters]); + } else { + await this.interpreterSecurityStorage.unsafeInterpreters.updateValue([ + interpreter.path, + ...unsafeInterpreters + ]); + } + this._didSafeInterpretersChange.fire(); + } + + public get onDidChangeSafeInterpreters(): Event { + return this._didSafeInterpretersChange.event; + } +} diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts new file mode 100644 index 000000000000..ea89c7d7d810 --- /dev/null +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; +import { Commands } from '../../../common/constants'; +import { IDisposable, IDisposableRegistry, IPersistentState, IPersistentStateFactory } from '../../../common/types'; +import { + flaggedWorkspacesKeysStorageKey, + safeInterpretersKey, + unsafeInterpreterPromptKey, + unsafeInterpretersKey +} from '../constants'; +import { IInterpreterSecurityStorage } from '../types'; + +@injectable() +export class InterpreterSecurityStorage implements IInterpreterSecurityStorage { + public get unsafeInterpreterPromptEnabled(): IPersistentState { + return this._unsafeInterpreterPromptEnabled; + } + public get unsafeInterpreters(): IPersistentState { + return this._unsafeInterpreters; + } + public get safeInterpreters(): IPersistentState { + return this._safeInterpreters; + } + private _unsafeInterpreterPromptEnabled: IPersistentState; + private _unsafeInterpreters: IPersistentState; + private _safeInterpreters: IPersistentState; + private flaggedWorkspacesKeysStorage: IPersistentState; + + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[] + ) { + this._unsafeInterpreterPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + unsafeInterpreterPromptKey, + true + ); + this._unsafeInterpreters = this.persistentStateFactory.createGlobalPersistentState( + unsafeInterpretersKey, + [] + ); + this._safeInterpreters = this.persistentStateFactory.createGlobalPersistentState( + safeInterpretersKey, + [] + ); + this.flaggedWorkspacesKeysStorage = this.persistentStateFactory.createGlobalPersistentState( + flaggedWorkspacesKeysStorageKey, + [] + ); + } + + public hasUserApprovedWorkspaceInterpreters(resource: Uri): IPersistentState { + return this.persistentStateFactory.createGlobalPersistentState( + this._getKeyForWorkspace(resource), + undefined + ); + } + + public async activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand( + Commands.ResetInterpreterSecurityStorage, + this.resetInterpreterSecurityStorage.bind(this) + ) + ); + } + + public async resetInterpreterSecurityStorage(): Promise { + this.flaggedWorkspacesKeysStorage.value.forEach(async key => { + const areInterpretersInWorkspaceSafe = this.persistentStateFactory.createGlobalPersistentState< + boolean | undefined + >(key, undefined); + await areInterpretersInWorkspaceSafe.updateValue(undefined); + }); + await this.flaggedWorkspacesKeysStorage.updateValue([]); + await this._safeInterpreters.updateValue([]); + await this._unsafeInterpreters.updateValue([]); + await this._unsafeInterpreterPromptEnabled.updateValue(true); + } + + public _getKeyForWorkspace(resource: Uri): string { + return `ARE_INTERPRETERS_SAFE_FOR_WS_${this.workspaceService.getWorkspaceFolderIdentifier(resource)}`; + } + + public async storeKeyForWorkspace(resource: Uri): Promise { + const key = this._getKeyForWorkspace(resource); + const flaggedWorkspacesKeys = this.flaggedWorkspacesKeysStorage.value; + if (!flaggedWorkspacesKeys.includes(key)) { + await this.flaggedWorkspacesKeysStorage.updateValue([key, ...flaggedWorkspacesKeys]); + } + } +} diff --git a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts index f8e262bc827c..7233dce8308e 100644 --- a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts +++ b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts @@ -12,7 +12,6 @@ import { IFileSystem, IPlatformService } from '../../../common/platform/types'; import { IExperimentsManager, IInterpreterPathService, 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, @@ -31,8 +30,6 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe @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, @@ -59,7 +56,7 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe : pythonConfig.inspect('pythonPath')!; this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); // If user has defined custom values in settings for this workspace folder, then use that. - if (pythonPathInConfig.workspaceFolderValue) { + if (pythonPathInConfig.workspaceFolderValue || pythonPathInConfig.workspaceValue) { return NextAction.runNextRule; } const pipEnvPromise = createDeferredFromPromise( @@ -82,7 +79,7 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe bestInterpreter = this.helper.getBestInterpreter(pipEnvList.concat(virtualEnvList)); } if (bestInterpreter && manager) { - await this.cacheSelectedInterpreter(workspacePath.folderUri, bestInterpreter); + await super.cacheSelectedInterpreter(workspacePath.folderUri, bestInterpreter); await manager.setWorkspaceInterpreter(workspacePath.folderUri!, bestInterpreter); } @@ -114,22 +111,4 @@ export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleSe return fsPathToCompare.startsWith(workspacePath); }); } - protected async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { - // We should never clear settings in user settings.json. - if (!interpreter) { - await super.cacheSelectedInterpreter(resource, interpreter); - return; - } - const activeWorkspace = this.helper.getActiveWorkspaceUri(resource); - 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 index 2bc24f46a139..9914266e38cb 100644 --- a/src/client/interpreter/autoSelection/types.ts +++ b/src/client/interpreter/autoSelection/types.ts @@ -4,7 +4,8 @@ 'use strict'; import { Event, Uri } from 'vscode'; -import { Resource } from '../../common/types'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IPersistentState, Resource } from '../../common/types'; import { PythonInterpreter } from '../contracts'; export const IInterpreterAutoSeletionProxyService = Symbol('IInterpreterAutoSeletionProxyService'); @@ -29,6 +30,7 @@ export const IInterpreterAutoSelectionService = Symbol('IInterpreterAutoSelectio export interface IInterpreterAutoSelectionService extends IInterpreterAutoSeletionProxyService { readonly onDidChangeAutoSelectedInterpreter: Event; autoSelectInterpreter(resource: Resource): Promise; + getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; setGlobalInterpreter(interpreter: PythonInterpreter | undefined): Promise; } @@ -48,3 +50,25 @@ export interface IInterpreterAutoSelectionRule { autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise; getPreviouslyAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; } + +export const IInterpreterSecurityService = Symbol('IInterpreterSecurityService'); +export interface IInterpreterSecurityService { + readonly onDidChangeSafeInterpreters: Event; + evaluateAndRecordInterpreterSafety(interpreter: PythonInterpreter, resource: Resource): Promise; + isSafe(interpreter: PythonInterpreter, resource?: Resource): boolean | undefined; +} + +export const IInterpreterSecurityStorage = Symbol('IInterpreterSecurityStorage'); +export interface IInterpreterSecurityStorage extends IExtensionSingleActivationService { + readonly unsafeInterpreterPromptEnabled: IPersistentState; + readonly unsafeInterpreters: IPersistentState; + readonly safeInterpreters: IPersistentState; + hasUserApprovedWorkspaceInterpreters(resource: Uri): IPersistentState; + storeKeyForWorkspace(resource: Uri): Promise; +} + +export const IInterpreterEvaluation = Symbol('IInterpreterEvaluation'); +export interface IInterpreterEvaluation { + evaluateIfInterpreterIsSafe(interpreter: PythonInterpreter, resource: Resource): Promise; + inferValueUsingCurrentState(interpreter: PythonInterpreter, resource: Resource): boolean | undefined; +} diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 4f677690967e..e9caf2207d4c 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -44,7 +44,7 @@ export class InterpreterSelector implements IInterpreterSelector { this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)) ); this.disposables.push( - this.commandManager.registerCommand(Commands.ResetPythonInterpreter, this.resetInterpreter.bind(this)) + this.commandManager.registerCommand(Commands.ClearWorkspaceInterpreter, this.resetInterpreter.bind(this)) ); this.disposables.push( this.commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this)) diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index 0cfb575265e9..84f6a30a4894 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,8 +1,9 @@ import { inject, injectable } from 'inversify'; import { compare } from 'semver'; -import { ConfigurationTarget } from 'vscode'; +import { ConfigurationTarget, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; import { traceError } from '../common/logger'; +import { FileSystemPaths } from '../common/platform/fs-paths'; import { InterpreterInfomation, IPythonExecutionFactory } from '../common/process/types'; import { IPersistentStateFactory, Resource } from '../common/types'; import { IServiceContainer } from '../ioc/types'; @@ -24,6 +25,13 @@ export function getFirstNonEmptyLineFromMultilineString(stdout: string) { return lines.length > 0 ? lines[0] : ''; } +export function isInterpreterLocatedInWorkspace(interpreter: PythonInterpreter, activeWorkspaceUri: Uri) { + const fileSystemPaths = FileSystemPaths.withDefaults(); + const interpreterPath = fileSystemPaths.normCase(interpreter.path); + const resourcePath = fileSystemPaths.normCase(activeWorkspaceUri.fsPath); + return interpreterPath.startsWith(resourcePath); +} + @injectable() export class InterpreterHelper implements IInterpreterHelper { private readonly persistentFactory: IPersistentStateFactory; diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 275126c503a0..715bae6d0d4f 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -10,6 +10,9 @@ import { EnvironmentActivationService } from './activation/service'; import { TerminalEnvironmentActivationService } from './activation/terminalEnvironmentActivationService'; import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; +import { InterpreterEvaluation } from './autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from './autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from './autoSelection/interpreterSecurity/interpreterSecurityStorage'; import { InterpreterAutoSeletionProxyService } from './autoSelection/proxy'; import { CachedInterpretersAutoSelectionRule } from './autoSelection/rules/cached'; import { CurrentPathInterpretersAutoSelectionRule } from './autoSelection/rules/currentPath'; @@ -21,7 +24,10 @@ import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService + IInterpreterAutoSeletionProxyService, + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage } from './autoSelection/types'; import { InterpreterComparer } from './configuration/interpreterComparer'; import { InterpreterSelector } from './configuration/interpreterSelector'; @@ -105,6 +111,13 @@ import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt'; */ // tslint:disable-next-line: max-func-body-length export function registerInterpreterTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterSecurityStorage + ); + serviceManager.addSingleton(IInterpreterEvaluation, InterpreterEvaluation); + serviceManager.addSingleton(IInterpreterSecurityStorage, InterpreterSecurityStorage); + serviceManager.addSingleton(IInterpreterSecurityService, InterpreterSecurityService); serviceManager.addSingleton( IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters diff --git a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts index e1f87bc7485c..69caa081a181 100644 --- a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts @@ -8,7 +8,7 @@ import { IApplicationShell, IWorkspaceService } from '../../common/application/t import { traceDecorators, traceError } from '../../common/logger'; import { IPlatformService } from '../../common/platform/types'; import { IBrowserService, IPersistentStateFactory } from '../../common/types'; -import { Common, InteractiveShiftEnterBanner, Interpreters } from '../../common/utils/localize'; +import { Common, Interpreters } from '../../common/utils/localize'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService, InterpreterType } from '../contracts'; @@ -49,11 +49,7 @@ export class CondaInheritEnvPrompt implements IExtensionActivationService { if (!notificationPromptEnabled.value) { return; } - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.moreInfo() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.moreInfo()]; const telemetrySelections: ['Yes', 'No', 'More Info'] = ['Yes', 'No', 'More Info']; const selection = await this.appShell.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts); sendTelemetryEvent(EventName.CONDA_INHERIT_ENV_PROMPT, undefined, { diff --git a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts index 474284bc1b67..00b7d293c4c5 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -8,7 +8,7 @@ import { IApplicationShell } from '../../common/application/types'; import { traceDecorators } from '../../common/logger'; import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; import { sleep } from '../../common/utils/async'; -import { Common, InteractiveShiftEnterBanner, Interpreters } from '../../common/utils/localize'; +import { Common, Interpreters } from '../../common/utils/localize'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IPythonPathUpdaterServiceManager } from '../configuration/types'; @@ -66,11 +66,7 @@ export class VirtualEnvironmentPrompt implements IExtensionActivationService { if (!notificationPromptEnabled.value) { return; } - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.doNotShowAgain() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; const telemetrySelections: ['Yes', 'No', 'Ignore'] = ['Yes', 'No', 'Ignore']; const selection = await this.appShell.showInformationMessage( Interpreters.environmentPromptMessage(), diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 98e1f110c626..7d6f206d6fbf 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -31,6 +31,7 @@ export enum EventName { TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION', PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', + UNSAFE_INTERPRETER_PROMPT = 'UNSAFE_INTERPRETER_PROMPT', INSIDERS_RELOAD_PROMPT = 'INSIDERS_RELOAD_PROMPT', INSIDERS_PROMPT = 'INSIDERS_PROMPT', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 292ac71f9385..0af3d8a0e759 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1023,6 +1023,19 @@ export interface IEventNamePropertyMapping { */ selection: 'Yes' | 'No' | 'More Info' | undefined; }; + /** + * Telemetry event sent with details when user clicks the prompt with the following message + * `Prompt message` :- 'We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.' + */ + [EventName.UNSAFE_INTERPRETER_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `Learn more` When 'More Info' option is selected + * `Do not show again` When 'Do not show again' option is selected + */ + selection: 'Yes' | 'No' | 'Learn more' | 'Do not show again' | undefined; + }; /** * Telemetry event sent with details when user clicks a button in the virtual environment prompt. * `Prompt message` :- 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?' diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index aff3f922f18f..c864ab466da9 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -5,7 +5,7 @@ import { assert, expect } from 'chai'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { TextDocument, Uri } from 'vscode'; import { ExtensionActivationManager } from '../../client/activation/activationManager'; @@ -16,10 +16,18 @@ import { ActiveResourceService } from '../../client/common/application/activeRes import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { DeprecatePythonPath } from '../../client/common/experimentGroups'; +import { ExperimentsManager } from '../../client/common/experiments'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { IFileSystem } from '../../client/common/platform/types'; -import { IDisposable } from '../../client/common/types'; -import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; +import { IDisposable, IExperimentsManager, IInterpreterPathService } from '../../client/common/types'; +import { createDeferred, createDeferredFromPromise } from '../../client/common/utils/async'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { + IInterpreterAutoSelectionService, + IInterpreterSecurityService +} from '../../client/interpreter/autoSelection/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { InterpreterService } from '../../client/interpreter/interpreterService'; import * as EnvFileTelemetry from '../../client/telemetry/envFileTelemetry'; @@ -48,10 +56,16 @@ suite('Language Server Activation - ActivationManager', () => { let interpreterService: IInterpreterService; let activeResourceService: IActiveResourceService; let documentManager: typemoq.IMock; + let interpreterSecurityService: IInterpreterSecurityService; + let interpreterPathService: typemoq.IMock; + let experiments: IExperimentsManager; let activationService1: IExtensionActivationService; let activationService2: IExtensionActivationService; let fileSystem: IFileSystem; setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + experiments = mock(ExperimentsManager); + interpreterPathService = typemoq.Mock.ofType(); workspaceService = mock(WorkspaceService); activeResourceService = mock(ActiveResourceService); appDiagnostics = typemoq.Mock.ofType(); @@ -61,6 +75,9 @@ suite('Language Server Activation - ActivationManager', () => { activationService1 = mock(LanguageServerExtensionActivationService); activationService2 = mock(LanguageServerExtensionActivationService); fileSystem = mock(FileSystem); + interpreterPathService + .setup(i => i.onDidChange(typemoq.It.isAny())) + .returns(() => typemoq.Mock.ofType().object); managerTest = new ExtensionActivationManagerTest( [instance(activationService1), instance(activationService2)], [], @@ -70,10 +87,14 @@ suite('Language Server Activation - ActivationManager', () => { appDiagnostics.object, instance(workspaceService), instance(fileSystem), - instance(activeResourceService) + instance(activeResourceService), + instance(experiments), + interpreterPathService.object, + instance(interpreterSecurityService) ); sinon.stub(EnvFileTelemetry, 'sendActivationTelemetry').resolves(); + managerTest.evaluateAutoSelectedInterpreterSafety = () => Promise.resolve(); }); teardown(() => { @@ -88,7 +109,7 @@ suite('Language Server Activation - ActivationManager', () => { when(workspaceService.hasWorkspaceFolders).thenReturn(true); const eventDef = () => disposable2.object; documentManager - .setup((d) => d.onDidOpenTextDocument) + .setup(d => d.onDidOpenTextDocument) .returns(() => eventDef) .verifiable(typemoq.Times.once()); @@ -100,8 +121,8 @@ suite('Language Server Activation - ActivationManager', () => { documentManager.verifyAll(); - disposable.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); - disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + disposable.setup(d => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); managerTest.dispose(); @@ -116,11 +137,11 @@ suite('Language Server Activation - ActivationManager', () => { when(workspaceService.hasWorkspaceFolders).thenReturn(true); const eventDef = () => disposable2.object; documentManager - .setup((d) => d.onDidOpenTextDocument) + .setup(d => d.onDidOpenTextDocument) .returns(() => eventDef) .verifiable(typemoq.Times.once()); - disposable.setup((d) => d.dispose()); - disposable2.setup((d) => d.dispose()); + disposable.setup(d => d.dispose()); + disposable2.setup(d => d.dispose()); await managerTest.initialize(); @@ -128,8 +149,8 @@ suite('Language Server Activation - ActivationManager', () => { verify(workspaceService.hasWorkspaceFolders).once(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); documentManager.verifyAll(); - disposable.verify((d) => d.dispose(), typemoq.Times.never()); - disposable2.verify((d) => d.dispose(), typemoq.Times.never()); + disposable.verify(d => d.dispose(), typemoq.Times.never()); + disposable2.verify(d => d.dispose(), typemoq.Times.never()); when(workspaceService.workspaceFolders).thenReturn([]); when(workspaceService.hasWorkspaceFolders).thenReturn(false); @@ -137,13 +158,13 @@ suite('Language Server Activation - ActivationManager', () => { await managerTest.initialize(); verify(workspaceService.hasWorkspaceFolders).twice(); - disposable.verify((d) => d.dispose(), typemoq.Times.never()); - disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + disposable.verify(d => d.dispose(), typemoq.Times.never()); + disposable2.verify(d => d.dispose(), typemoq.Times.once()); managerTest.dispose(); - disposable.verify((d) => d.dispose(), typemoq.Times.atLeast(1)); - disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + disposable.verify(d => d.dispose(), typemoq.Times.atLeast(1)); + disposable2.verify(d => d.dispose(), typemoq.Times.once()); }); test('Activate workspace specific to the resource in case of Multiple workspaces when a file is opened', async () => { const disposable1 = typemoq.Mock.ofType(); @@ -152,16 +173,16 @@ suite('Language Server Activation - ActivationManager', () => { let workspaceFoldersChangedHandler!: Function; const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType(); - document.setup((d) => d.uri).returns(() => documentUri); - document.setup((d) => d.languageId).returns(() => PYTHON_LANGUAGE); + document.setup(d => d.uri).returns(() => documentUri); + document.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { workspaceFoldersChangedHandler = cb; return disposable1.object; }); documentManager - .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((cb) => (fileOpenedHandler = cb)) + .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback(cb => (fileOpenedHandler = cb)) .returns(() => disposable2.object) .verifiable(typemoq.Times.once()); @@ -178,11 +199,11 @@ suite('Language Server Activation - ActivationManager', () => { when(activationService2.activate(resource)).thenResolve(); when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection - .setup((a) => a.autoSelectInterpreter(resource)) + .setup(a => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); appDiagnostics - .setup((a) => a.performPreStartupHealthCheck(resource)) + .setup(a => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); // Add workspaceFoldersChangedHandler @@ -211,11 +232,11 @@ suite('Language Server Activation - ActivationManager', () => { when(activationService2.activate(resource)).thenResolve(); when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection - .setup((a) => a.autoSelectInterpreter(resource)) + .setup(a => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); appDiagnostics - .setup((a) => a.performPreStartupHealthCheck(resource)) + .setup(a => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); @@ -231,11 +252,11 @@ suite('Language Server Activation - ActivationManager', () => { when(activationService2.activate(resource)).thenResolve(); when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection - .setup((a) => a.autoSelectInterpreter(resource)) + .setup(a => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); appDiagnostics - .setup((a) => a.performPreStartupHealthCheck(resource)) + .setup(a => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); @@ -293,15 +314,15 @@ suite('Language Server Activation - ActivationManager', () => { let workspaceFoldersChangedHandler!: Function; const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType(); - document.setup((d) => d.uri).returns(() => documentUri); + document.setup(d => d.uri).returns(() => documentUri); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { workspaceFoldersChangedHandler = cb; return disposable1.object; }); documentManager - .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((cb) => (docOpenedHandler = cb)) + .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback(cb => (docOpenedHandler = cb)) .returns(() => disposable2.object) .verifiable(typemoq.Times.once()); @@ -333,7 +354,7 @@ suite('Language Server Activation - ActivationManager', () => { //Removed no. of folders to one when(workspaceService.workspaceFolders).thenReturn([folder1]); when(workspaceService.hasWorkspaceFolders).thenReturn(true); - disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); workspaceFoldersChangedHandler.call(managerTest); @@ -352,6 +373,7 @@ suite('Language Server Activation - activate()', () => { let interpreterService: IInterpreterService; let activeResourceService: IActiveResourceService; let documentManager: typemoq.IMock; + let interpreterSecurityService: IInterpreterSecurityService; let activationService1: IExtensionActivationService; let activationService2: IExtensionActivationService; let fileSystem: IFileSystem; @@ -360,11 +382,17 @@ suite('Language Server Activation - activate()', () => { let activateWorkspace: sinon.SinonStub; let managerTest: ExtensionActivationManager; const resource = Uri.parse('a'); + let interpreterPathService: typemoq.IMock; + let experiments: IExperimentsManager; + setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + experiments = mock(ExperimentsManager); workspaceService = mock(WorkspaceService); activeResourceService = mock(ActiveResourceService); appDiagnostics = typemoq.Mock.ofType(); autoSelection = typemoq.Mock.ofType(); + interpreterPathService = typemoq.Mock.ofType(); interpreterService = mock(InterpreterService); documentManager = typemoq.Mock.ofType(); activationService1 = mock(LanguageServerExtensionActivationService); @@ -375,6 +403,9 @@ suite('Language Server Activation - activate()', () => { initialize.resolves(); activateWorkspace = sinon.stub(ExtensionActivationManager.prototype, 'activateWorkspace'); activateWorkspace.resolves(); + interpreterPathService + .setup(i => i.onDidChange(typemoq.It.isAny())) + .returns(() => typemoq.Mock.ofType().object); managerTest = new ExtensionActivationManager( [instance(activationService1), instance(activationService2)], [singleActivationService.object], @@ -384,8 +415,12 @@ suite('Language Server Activation - activate()', () => { appDiagnostics.object, instance(workspaceService), instance(fileSystem), - instance(activeResourceService) + instance(activeResourceService), + instance(experiments), + interpreterPathService.object, + instance(interpreterSecurityService) ); + managerTest.evaluateAutoSelectedInterpreterSafety = () => Promise.resolve(); }); teardown(() => { @@ -394,11 +429,11 @@ suite('Language Server Activation - activate()', () => { test('Execution goes as expected if there are no errors', async () => { singleActivationService - .setup((s) => s.activate()) + .setup(s => s.activate()) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); autoSelection - .setup((a) => a.autoSelectInterpreter(undefined)) + .setup(a => a.autoSelectInterpreter(undefined)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); when(activeResourceService.getActiveResource()).thenReturn(resource); @@ -411,11 +446,11 @@ suite('Language Server Activation - activate()', () => { test('Throws error if execution fails', async () => { singleActivationService - .setup((s) => s.activate()) + .setup(s => s.activate()) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); autoSelection - .setup((a) => a.autoSelectInterpreter(undefined)) + .setup(a => a.autoSelectInterpreter(undefined)) .returns(() => Promise.reject(new Error('Kaboom'))) .verifiable(typemoq.Times.once()); when(activeResourceService.getActiveResource()).thenReturn(resource); @@ -423,3 +458,132 @@ suite('Language Server Activation - activate()', () => { await expect(promise).to.eventually.be.rejectedWith('Kaboom'); }); }); + +suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', () => { + let workspaceService: IWorkspaceService; + let appDiagnostics: typemoq.IMock; + let autoSelection: typemoq.IMock; + let interpreterService: IInterpreterService; + let activeResourceService: IActiveResourceService; + let documentManager: typemoq.IMock; + let activationService1: IExtensionActivationService; + let activationService2: IExtensionActivationService; + let fileSystem: IFileSystem; + let managerTest: ExtensionActivationManager; + const resource = Uri.parse('a'); + let interpreterSecurityService: IInterpreterSecurityService; + let interpreterPathService: IInterpreterPathService; + let experiments: IExperimentsManager; + setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + experiments = mock(ExperimentsManager); + fileSystem = mock(FileSystem); + interpreterPathService = mock(InterpreterPathService); + workspaceService = mock(WorkspaceService); + activeResourceService = mock(ActiveResourceService); + appDiagnostics = typemoq.Mock.ofType(); + autoSelection = typemoq.Mock.ofType(); + interpreterService = mock(InterpreterService); + documentManager = typemoq.Mock.ofType(); + activationService1 = mock(LanguageServerExtensionActivationService); + activationService2 = mock(LanguageServerExtensionActivationService); + when(experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).thenReturn(undefined); + managerTest = new ExtensionActivationManager( + [instance(activationService1), instance(activationService2)], + [], + documentManager.object, + instance(interpreterService), + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + instance(experiments), + instance(interpreterPathService), + instance(interpreterSecurityService) + ); + }); + + test(`If in Deprecate PythonPath experiment, and setting is not set, fetch autoselected interpreter but don't evaluate it if it equals 'undefined'`, async () => { + const interpreter = undefined; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup(a => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.once()); + when(interpreterPathService.get(resource)).thenReturn('python'); + when(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).never(); + }); + + ['', 'python'].forEach(setting => { + test(`If in Deprecate PythonPath experiment, and setting equals '${setting}', fetch autoselected interpreter and evaluate it`, async () => { + const interpreter = { path: 'pythonPath' }; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup(a => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.once()); + when(interpreterPathService.get(resource)).thenReturn('python'); + when( + interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource) + ).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).once(); + }); + }); + + test(`If in Deprecate PythonPath experiment, and setting is not set, fetch autoselected interpreter but don't evaluate it if it equals 'undefined'`, async () => { + const interpreter = undefined; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup(a => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.once()); + when(interpreterPathService.get(resource)).thenReturn('python'); + when(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).never(); + }); + + test(`If in Deprecate PythonPath experiment, and setting is set, simply return`, async () => { + const interpreter = undefined; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup(a => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.never()); + when(interpreterPathService.get(resource)).thenReturn('settingSetToSomePath'); + when(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).never(); + }); + + test(`If in Deprecate PythonPath experiment, if setting is set during evaluation, don't wait for the evaluation to finish to resolve method promise`, async () => { + const interpreter = { path: 'pythonPath' }; + const evaluateIfInterpreterIsSafeDeferred = createDeferred(); + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection.setup(a => a.getAutoSelectedInterpreter(resource)).returns(() => interpreter as any); + when(interpreterPathService.get(resource)).thenReturn('python'); + when(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).thenReturn( + evaluateIfInterpreterIsSafeDeferred.promise + ); + const deferredPromise = createDeferredFromPromise(managerTest.evaluateAutoSelectedInterpreterSafety(resource)); + expect(deferredPromise.completed).to.equal(false, 'Promise should not be resolved yet'); + reset(interpreterPathService); + when(interpreterPathService.get(resource)).thenReturn('settingSetToSomePath'); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + await sleep(1); + expect(deferredPromise.completed).to.equal(true, 'Promise should be resolved'); + }); +}); diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index da5d194afcb5..d813910518b0 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -17,6 +17,7 @@ import { DeprecatePythonPath } from '../../../client/common/experimentGroups'; import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { IInterpreterSecurityService } from '../../../client/interpreter/autoSelection/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; const untildify = require('untildify'); @@ -43,7 +44,7 @@ suite('Python Settings - pythonPath', () => { interpreterPathService = typemoq.Mock.ofType(); experimentsManager = typemoq.Mock.ofType(); workspaceService = typemoq.Mock.ofType(); - pythonSettings.setup(p => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); + pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); }); teardown(() => { if (configSettings) { @@ -56,7 +57,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); const pythonPath = 'This is the python Path'; pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -67,7 +68,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); const pythonPath = `~${path.sep}This is the python Path`; pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -79,7 +80,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(workspaceFolderUri, new MockAutoSelectionService()); const pythonPath = `.${path.sep}This is the python Path`; pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); @@ -93,7 +94,7 @@ suite('Python Settings - pythonPath', () => { const workspaceFolderToken = '${workspaceFolder}'; const pythonPath = `${workspaceFolderToken}${path.sep}This is the python Path`; pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -106,7 +107,7 @@ suite('Python Settings - pythonPath', () => { configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); const pythonPath = 'python'; pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -122,7 +123,7 @@ suite('Python Settings - pythonPath', () => { when(selectionService.setWorkspaceInterpreter(workspaceFolderUri, anything())).thenResolve(); configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => 'python') .verifiable(typemoq.Times.atLeast(1)); configSettings.update(pythonSettings.object); @@ -130,6 +131,72 @@ suite('Python Settings - pythonPath', () => { expect(configSettings.pythonPath).to.be.equal(pythonPath); verify(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).once(); }); + test("If user is in Deprecate Python Path experiment and we don't have a custom python path, get the autoselected interpreter and use it if it's safe", () => { + const resource = Uri.parse('a'); + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter: any = { path: pythonPath }; + const selectionService = mock(MockAutoSelectionService); + const interpreterSecurityService = typemoq.Mock.ofType(); + when(selectionService.getAutoSelectedInterpreter(resource)).thenReturn(interpreter); + interpreterSecurityService.setup((i) => i.isSafe(interpreter)).returns(() => true); + when(selectionService.setWorkspaceInterpreter(resource, anything())).thenResolve(); + configSettings = new CustomPythonSettings( + resource, + instance(selectionService), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object, + interpreterSecurityService.object + ); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => true) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + verify(selectionService.getAutoSelectedInterpreter(resource)).once(); + }); + test("If user is in Deprecate Python Path experiment and we don't have a custom python path, get the autoselected interpreter and but don't use it if it's not safe", () => { + const resource = Uri.parse('a'); + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter: any = { path: pythonPath }; + const selectionService = mock(MockAutoSelectionService); + const interpreterSecurityService = typemoq.Mock.ofType(); + when(selectionService.getAutoSelectedInterpreter(resource)).thenReturn(interpreter); + interpreterSecurityService.setup((i) => i.isSafe(interpreter)).returns(() => false); + when(selectionService.setWorkspaceInterpreter(resource, anything())).thenResolve(); + configSettings = new CustomPythonSettings( + resource, + instance(selectionService), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object, + interpreterSecurityService.object + ); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => true) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal('a'); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + verify(selectionService.getAutoSelectedInterpreter(resource)).once(); + }); test('If user is in Deprecate Python Path experiment, use the new API to fetch Python Path', () => { const resource = Uri.parse('a'); configSettings = new CustomPythonSettings( @@ -140,17 +207,17 @@ suite('Python Settings - pythonPath', () => { interpreterPathService.object ); const pythonPath = 'This is the new API python Path'; - pythonSettings.setup(p => p.get(typemoq.It.isValue('pythonPath'))).verifiable(typemoq.Times.never()); + pythonSettings.setup((p) => p.get(typemoq.It.isValue('pythonPath'))).verifiable(typemoq.Times.never()); experimentsManager - .setup(e => e.inExperiment(DeprecatePythonPath.experiment)) + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) .returns(() => true) .verifiable(typemoq.Times.once()); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined) .verifiable(typemoq.Times.once()); interpreterPathService - .setup(i => i.get(resource)) + .setup((i) => i.get(resource)) .returns(() => pythonPath) .verifiable(typemoq.Times.once()); configSettings.update(pythonSettings.object); @@ -171,18 +238,18 @@ suite('Python Settings - pythonPath', () => { ); const pythonPath = 'This is the settings python Path'; pythonSettings - .setup(p => p.get(typemoq.It.isValue('pythonPath'))) + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) .returns(() => pythonPath) .verifiable(typemoq.Times.atLeastOnce()); experimentsManager - .setup(e => e.inExperiment(DeprecatePythonPath.experiment)) + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) .returns(() => false) .verifiable(typemoq.Times.once()); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined) .verifiable(typemoq.Times.once()); - interpreterPathService.setup(i => i.get(resource)).verifiable(typemoq.Times.never()); + interpreterPathService.setup((i) => i.get(resource)).verifiable(typemoq.Times.never()); configSettings.update(pythonSettings.object); expect(configSettings.pythonPath).to.be.equal(pythonPath); diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts index 79f21f7dfabd..5bbd24a9fa8d 100644 --- a/src/test/common/configuration/service.test.ts +++ b/src/test/common/configuration/service.test.ts @@ -3,31 +3,26 @@ import { expect } from 'chai'; import { workspace } from 'vscode'; import { IAsyncDisposableRegistry, IConfigurationService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; import { getExtensionSettings } from '../../common'; import { initialize } from '../../initialize'; -import { UnitTestIocContainer } from '../../testing/serviceRegistry'; // tslint:disable-next-line:max-func-body-length suite('Configuration Service', () => { - let ioc: UnitTestIocContainer; - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); + let serviceContainer: IServiceContainer; + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; }); - teardown(() => ioc.dispose()); test('Ensure same instance of settings return', () => { const workspaceUri = workspace.workspaceFolders![0].uri; - const settings = ioc.serviceContainer - .get(IConfigurationService) - .getSettings(workspaceUri); + const settings = serviceContainer.get(IConfigurationService).getSettings(workspaceUri); const instanceIsSame = settings === getExtensionSettings(workspaceUri); expect(instanceIsSame).to.be.equal(true, 'Incorrect settings'); }); test('Ensure async registry works', async () => { - const asyncRegistry = ioc.serviceContainer.get(IAsyncDisposableRegistry); + const asyncRegistry = serviceContainer.get(IAsyncDisposableRegistry); let disposed = false; const disposable = { dispose(): Promise { diff --git a/src/test/common/configuration/service.unit.test.ts b/src/test/common/configuration/service.unit.test.ts index 8238e6702149..547438905190 100644 --- a/src/test/common/configuration/service.unit.test.ts +++ b/src/test/common/configuration/service.unit.test.ts @@ -3,12 +3,18 @@ 'use strict'; +import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { DeprecatePythonPath } from '../../../client/common/experimentGroups'; import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; +import { + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from '../../../client/interpreter/autoSelection/types'; import { IServiceContainer } from '../../../client/ioc/types'; suite('Configuration Service', () => { @@ -17,9 +23,11 @@ suite('Configuration Service', () => { let interpreterPathService: TypeMoq.IMock; let experimentsManager: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; + let interpreterSecurityService: TypeMoq.IMock; let configService: ConfigurationService; setup(() => { workspaceService = TypeMoq.Mock.ofType(); + interpreterSecurityService = TypeMoq.Mock.ofType(); workspaceService .setup(w => w.getWorkspaceFolder(resource)) .returns(() => ({ @@ -47,6 +55,20 @@ suite('Configuration Service', () => { return workspaceConfig; } + test('Fetching settings goes as expected', () => { + const interpreterAutoSelectionProxyService = TypeMoq.Mock.ofType(); + serviceContainer + .setup(s => s.get(IInterpreterSecurityService)) + .returns(() => interpreterSecurityService.object) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup(s => s.get(IInterpreterAutoSeletionProxyService)) + .returns(() => interpreterAutoSelectionProxyService.object) + .verifiable(TypeMoq.Times.once()); + const settings = configService.getSettings(); + expect(settings).to.be.instanceOf(PythonSettings); + }); + test('Do not update global settings if global value is already equal to the new value', async () => { experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 1d1aa4b63d56..35800178a8fa 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -164,6 +164,7 @@ suite('Installer', () => { ioc.registerVariableTypes(); ioc.registerLinterTypes(); ioc.registerFormatterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index ffca231c437e..4c0c20307aee 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -184,6 +184,7 @@ suite('Module Installer', () => { ioc.registerVariableTypes(); ioc.registerLinterTypes(); ioc.registerFormatterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); ioc.serviceManager.addSingleton(IProcessLogger, ProcessLogger); diff --git a/src/test/common/variables/envVarsProvider.multiroot.test.ts b/src/test/common/variables/envVarsProvider.multiroot.test.ts index f8c9a3be94a7..4044f271ba76 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -53,6 +53,7 @@ suite('Multiroot Environment Variables Provider', () => { ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); + ioc.registerInterpreterStorageTypes(); const mockEnvironmentActivationService = mock(EnvironmentActivationService); when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts index 331b2e5eefa7..d35f33d1bf4b 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -82,6 +82,7 @@ suite('Formatting - General', () => { ioc.registerVariableTypes(); ioc.registerUnitTestTypes(); ioc.registerFormatterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); ioc.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts index adb740fe1aa9..bf266f798cca 100644 --- a/src/test/format/extension.sort.test.ts +++ b/src/test/format/extension.sort.test.ts @@ -55,6 +55,7 @@ suite('Sorting', () => { ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index 1fd642cab281..369fb71c5d93 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -7,7 +7,7 @@ import { expect } from 'chai'; import { SemVer } from 'semver'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; @@ -17,6 +17,7 @@ import { IFileSystem } from '../../../client/common/platform/types'; import { IPersistentStateFactory, Resource } from '../../../client/common/types'; import { createDeferred } from '../../../client/common/utils/async'; import { InterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection'; +import { InterpreterSecurityService } from '../../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; import { InterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/proxy'; import { CachedInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/cached'; import { CurrentPathInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/currentPath'; @@ -26,7 +27,8 @@ import { WindowsRegistryInterpretersAutoSelectionRule } from '../../../client/in import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/workspaceEnv'; import { IInterpreterAutoSelectionRule, - IInterpreterAutoSeletionProxyService + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService } from '../../../client/interpreter/autoSelection/types'; import { IInterpreterHelper, PythonInterpreter } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; @@ -47,6 +49,7 @@ suite('Interpreters - Auto Selection', () => { let state: PersistentState; let helper: IInterpreterHelper; let proxy: IInterpreterAutoSeletionProxyService; + let interpreterSecurityService: IInterpreterSecurityService; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { public initializeStore(resource: Resource): Promise { return super.initializeStore(resource); @@ -59,6 +62,7 @@ suite('Interpreters - Auto Selection', () => { } } setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); workspaceService = mock(WorkspaceService); stateFactory = mock(PersistentStateFactory); state = mock(PersistentState); @@ -71,6 +75,7 @@ suite('Interpreters - Auto Selection', () => { workspaceInterpreter = mock(WorkspaceVirtualEnvInterpretersAutoSelectionRule); helper = mock(InterpreterHelper); proxy = mock(InterpreterAutoSeletionProxyService); + when(interpreterSecurityService.isSafe(anything())).thenReturn(undefined); autoSelectionService = new InterpreterAutoSelectionServiceTest( instance(workspaceService), @@ -83,7 +88,8 @@ suite('Interpreters - Auto Selection', () => { instance(userDefinedInterpreter), instance(workspaceInterpreter), instance(proxy), - instance(helper) + instance(helper), + instance(interpreterSecurityService) ); }); @@ -216,6 +222,14 @@ suite('Interpreters - Auto Selection', () => { expect(selectedInterpreter).to.deep.equal(interpreterInfo); expect(eventFired).to.deep.equal(false, 'event fired'); }); + test('If interpreter chosen is unsafe, return `undefined` as the autoselected interpreter', async () => { + const interpreterInfo = { path: 'pythonPath' } as any; + autoSelectionService._getAutoSelectedInterpreter = () => interpreterInfo; + reset(interpreterSecurityService); + when(interpreterSecurityService.isSafe(interpreterInfo)).thenReturn(false); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + expect(selectedInterpreter).to.equal(undefined, 'Should be undefined'); + }); 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'; diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts new file mode 100644 index 000000000000..cc5e4660b6f5 --- /dev/null +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as Typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { IBrowserService, IPersistentState } from '../../../../client/common/types'; +import { Common, Interpreters } from '../../../../client/common/utils/localize'; +import { learnMoreOnInterpreterSecurityURI } from '../../../../client/interpreter/autoSelection/constants'; +import { InterpreterEvaluation } from '../../../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { IInterpreterSecurityStorage } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; + +const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.learnMore(), Common.doNotShowAgain()]; + +suite('Interpreter Evaluation', () => { + const resource = Uri.parse('a'); + let applicationShell: Typemoq.IMock; + let browserService: Typemoq.IMock; + let interpreterHelper: Typemoq.IMock; + let interpreterSecurityStorage: Typemoq.IMock; + let unsafeInterpreterPromptEnabled: Typemoq.IMock>; + let areInterpretersInWorkspaceSafe: Typemoq.IMock>; + let interpreterEvaluation: InterpreterEvaluation; + setup(() => { + applicationShell = Typemoq.Mock.ofType(); + browserService = Typemoq.Mock.ofType(); + interpreterHelper = Typemoq.Mock.ofType(); + interpreterSecurityStorage = Typemoq.Mock.ofType(); + unsafeInterpreterPromptEnabled = Typemoq.Mock.ofType>(); + areInterpretersInWorkspaceSafe = Typemoq.Mock.ofType>(); + interpreterSecurityStorage + .setup(i => i.hasUserApprovedWorkspaceInterpreters(resource)) + .returns(() => areInterpretersInWorkspaceSafe.object); + interpreterSecurityStorage + .setup(i => i.unsafeInterpreterPromptEnabled) + .returns(() => unsafeInterpreterPromptEnabled.object); + interpreterSecurityStorage.setup(i => i.storeKeyForWorkspace(resource)).returns(() => Promise.resolve()); + interpreterEvaluation = new InterpreterEvaluation( + applicationShell.object, + browserService.object, + interpreterHelper.object, + interpreterSecurityStorage.object + ); + }); + + suite('Method evaluateIfInterpreterIsSafe()', () => { + test('If no workspaces are opened, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper.setup(i => i.getActiveWorkspaceUri(resource)).returns(() => undefined); + const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('If method inferValueFromStorage() returns a defined value, return the value', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper + .setup(i => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + // tslint:disable-next-line: no-any + interpreterEvaluation.inferValueUsingCurrentState = () => 'storageValue' as any; + const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + expect(isSafe).to.equal('storageValue'); + }); + + test('If method inferValueFromStorage() returns a undefined value, infer the value using the prompt and return it', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper + .setup(i => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + interpreterEvaluation.inferValueUsingCurrentState = () => undefined; + // tslint:disable-next-line: no-any + interpreterEvaluation._inferValueUsingPrompt = () => 'promptValue' as any; + const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + expect(isSafe).to.equal('promptValue'); + }); + }); + + suite('Method inferValueUsingStorage()', () => { + test('If no workspaces are opened, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper.setup(i => i.getActiveWorkspaceUri(resource)).returns(() => undefined); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('If interpreter is stored outside the workspace, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper + .setup(i => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('If interpreter is stored in the workspace but method _areInterpretersInWorkspaceSafe() returns a defined value, return the value', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; + interpreterHelper + .setup(i => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + areInterpretersInWorkspaceSafe + .setup(i => i.value) + // tslint:disable-next-line: no-any + .returns(() => 'areInterpretersInWorkspaceSafeValue' as any); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal('areInterpretersInWorkspaceSafeValue'); + }); + + test('If prompt has been disabled, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; + interpreterHelper + .setup(i => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + areInterpretersInWorkspaceSafe.setup(i => i.value).returns(() => undefined); + unsafeInterpreterPromptEnabled.setup(s => s.value).returns(() => false); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('Otherwise return `undefined`', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; + interpreterHelper + .setup(i => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + areInterpretersInWorkspaceSafe.setup(i => i.value).returns(() => undefined); + unsafeInterpreterPromptEnabled.setup(s => s.value).returns(() => true); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(undefined, 'Should be undefined'); + }); + }); + + suite('Method _inferValueUsingPrompt()', () => { + test('Active workspace key is stored in security storage', async () => { + interpreterSecurityStorage + .setup(i => i.storeKeyForWorkspace(resource)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + await interpreterEvaluation._inferValueUsingPrompt(resource); + interpreterSecurityStorage.verifyAll(); + }); + test('If `Learn more` is selected, launch URL & keep showing the prompt again until user clicks some other option', async () => { + let promptDisplayCount = 0; + // Select `Learn more` 2 times, then select something else the 3rd time. + const showInformationMessage = () => { + promptDisplayCount += 1; + return Promise.resolve(promptDisplayCount < 3 ? Common.learnMore() : 'Some other option'); + }; + applicationShell + .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(showInformationMessage) + .verifiable(Typemoq.Times.exactly(3)); + browserService + .setup(b => b.launch(learnMoreOnInterpreterSecurityURI)) + .returns(() => undefined) + .verifiable(Typemoq.Times.exactly(2)); + + await interpreterEvaluation._inferValueUsingPrompt(resource); + + applicationShell.verifyAll(); + browserService.verifyAll(); + }); + + test('If `No` is selected, update the areInterpretersInWorkspaceSafe storage to unsafe and return false', async () => { + applicationShell + .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo())) + .verifiable(Typemoq.Times.once()); + areInterpretersInWorkspaceSafe + .setup(i => i.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + + const result = await interpreterEvaluation._inferValueUsingPrompt(resource); + expect(result).to.equal(false, 'Should be false'); + + applicationShell.verifyAll(); + areInterpretersInWorkspaceSafe.verifyAll(); + }); + + test('If `Yes` is selected, update the areInterpretersInWorkspaceSafe storage to safe and return true', async () => { + applicationShell + .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes())) + .verifiable(Typemoq.Times.once()); + areInterpretersInWorkspaceSafe + .setup(i => i.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + + const result = await interpreterEvaluation._inferValueUsingPrompt(resource); + expect(result).to.equal(true, 'Should be true'); + + applicationShell.verifyAll(); + areInterpretersInWorkspaceSafe.verifyAll(); + }); + + test('If no selection is made, update the areInterpretersInWorkspaceSafe storage to unsafe and return false', async () => { + applicationShell + .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + areInterpretersInWorkspaceSafe + .setup(i => i.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + + const result = await interpreterEvaluation._inferValueUsingPrompt(resource); + expect(result).to.equal(false, 'Should be false'); + + applicationShell.verifyAll(); + areInterpretersInWorkspaceSafe.verifyAll(); + }); + }); +}); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts new file mode 100644 index 000000000000..b2cb16cdde45 --- /dev/null +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as Typemoq from 'typemoq'; +import { EventEmitter, Uri } from 'vscode'; +import { IPersistentState } from '../../../../client/common/types'; +import { createDeferred, sleep } from '../../../../client/common/utils/async'; +import { InterpreterSecurityService } from '../../../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { + IInterpreterEvaluation, + IInterpreterSecurityStorage +} from '../../../../client/interpreter/autoSelection/types'; + +suite('Interpreter Security service', () => { + const safeInterpretersList = ['safe1', 'safe2']; + const unsafeInterpretersList = ['unsafe1', 'unsafe2']; + const resource = Uri.parse('a'); + let interpreterSecurityStorage: Typemoq.IMock; + let interpreterEvaluation: Typemoq.IMock; + let unsafeInterpreters: Typemoq.IMock>; + let safeInterpreters: Typemoq.IMock>; + let interpreterSecurityService: InterpreterSecurityService; + setup(() => { + interpreterEvaluation = Typemoq.Mock.ofType(); + unsafeInterpreters = Typemoq.Mock.ofType>(); + safeInterpreters = Typemoq.Mock.ofType>(); + interpreterSecurityStorage = Typemoq.Mock.ofType(); + safeInterpreters.setup(s => s.value).returns(() => safeInterpretersList); + unsafeInterpreters.setup(s => s.value).returns(() => unsafeInterpretersList); + interpreterSecurityStorage.setup(p => p.unsafeInterpreters).returns(() => unsafeInterpreters.object); + interpreterSecurityStorage.setup(p => p.safeInterpreters).returns(() => safeInterpreters.object); + interpreterSecurityService = new InterpreterSecurityService( + interpreterSecurityStorage.object, + interpreterEvaluation.object + ); + }); + + suite('Method isSafe()', () => { + test('Returns `true` if interpreter is in the safe interpreters list', () => { + // tslint:disable-next-line: no-any + let isSafe = interpreterSecurityService.isSafe({ path: 'safe1' } as any); + expect(isSafe).to.equal(true, ''); + // tslint:disable-next-line: no-any + isSafe = interpreterSecurityService.isSafe({ path: 'safe2' } as any); + expect(isSafe).to.equal(true, ''); + }); + + test('Returns `false` if interpreter is in the unsafe intepreters list', () => { + // tslint:disable-next-line: no-any + let isSafe = interpreterSecurityService.isSafe({ path: 'unsafe1' } as any); + expect(isSafe).to.equal(false, ''); + // tslint:disable-next-line: no-any + isSafe = interpreterSecurityService.isSafe({ path: 'unsafe2' } as any); + expect(isSafe).to.equal(false, ''); + }); + + test('Returns `undefined` if interpreter is not in either of these lists', () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'random' } as any; + interpreterEvaluation + .setup(i => i.inferValueUsingCurrentState(interpreter, resource)) + // tslint:disable-next-line: no-any + .returns(() => 'value' as any) + .verifiable(Typemoq.Times.once()); + const isSafe = interpreterSecurityService.isSafe(interpreter, resource); + expect(isSafe).to.equal('value', ''); + interpreterEvaluation.verifyAll(); + }); + }); + + suite('Method evaluateInterpreterSafety()', () => { + test("If interpreter to be evaluated already exists in the safe intepreters list, simply return and don't evaluate", async () => { + const interpreter = { path: 'safe2' }; + interpreterEvaluation + .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .verifiable(Typemoq.Times.never()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + }); + + test("If interpreter to be evaluated already exists in the unsafe intepreters list, simply return and don't evaluate", async () => { + const interpreter = { path: 'unsafe1' }; + interpreterEvaluation + .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .verifiable(Typemoq.Times.never()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + }); + + test('If interpreter to be evaluated does not exists in the either of the intepreters list, evaluate the interpreters', async () => { + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + }); + + test('If interpreter is evaluated to be safe, add it in the safe interpreters list', async () => { + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(Typemoq.Times.once()); + safeInterpreters + .setup(s => s.updateValue(['notInEitherLists', ...safeInterpretersList])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + safeInterpreters.verifyAll(); + }); + + test('If interpreter is evaluated to be unsafe, add it in the unsafe interpreters list', async () => { + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(Typemoq.Times.once()); + unsafeInterpreters + .setup(s => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + unsafeInterpreters.verifyAll(); + }); + + test('Ensure an event is fired at the end of the method execution', async () => { + const _didSafeInterpretersChange = Typemoq.Mock.ofType>(); + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .returns(() => Promise.resolve(false)); + unsafeInterpreters + .setup(s => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) + .returns(() => Promise.resolve()); + interpreterSecurityService._didSafeInterpretersChange = _didSafeInterpretersChange.object; + _didSafeInterpretersChange + .setup(d => d.fire()) + .returns(() => undefined) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + unsafeInterpreters.verifyAll(); + _didSafeInterpretersChange.verifyAll(); + }); + }); + + test('Ensure onDidChangeSafeInterpreters() method captures the fired event', async () => { + const deferred = createDeferred(); + interpreterSecurityService.onDidChangeSafeInterpreters(() => { + deferred.resolve(true); + }); + interpreterSecurityService._didSafeInterpretersChange.fire(); + const eventCaptured = await Promise.race([deferred.promise, sleep(1000).then(() => false)]); + expect(eventCaptured).to.equal(true, 'Event should be captured'); + }); +}); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts new file mode 100644 index 000000000000..aed4de7f8d2b --- /dev/null +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as Typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { IDisposable, IPersistentState, IPersistentStateFactory } from '../../../../client/common/types'; +import { + flaggedWorkspacesKeysStorageKey, + safeInterpretersKey, + unsafeInterpreterPromptKey, + unsafeInterpretersKey +} from '../../../../client/interpreter/autoSelection/constants'; +import { InterpreterSecurityStorage } from '../../../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; + +suite('Interpreter Security Storage', () => { + const resource = Uri.parse('a'); + let persistentStateFactory: Typemoq.IMock; + let interpreterSecurityStorage: InterpreterSecurityStorage; + let unsafeInterpreters: Typemoq.IMock>; + let safeInterpreters: Typemoq.IMock>; + let flaggedWorkspacesKeysStorage: Typemoq.IMock>; + let commandManager: Typemoq.IMock; + let workspaceService: Typemoq.IMock; + let areInterpretersInWorkspaceSafe: Typemoq.IMock>; + let unsafeInterpreterPromptEnabled: Typemoq.IMock>; + setup(() => { + persistentStateFactory = Typemoq.Mock.ofType(); + unsafeInterpreters = Typemoq.Mock.ofType>(); + safeInterpreters = Typemoq.Mock.ofType>(); + flaggedWorkspacesKeysStorage = Typemoq.Mock.ofType>(); + unsafeInterpreterPromptEnabled = Typemoq.Mock.ofType>(); + commandManager = Typemoq.Mock.ofType(); + workspaceService = Typemoq.Mock.ofType(); + areInterpretersInWorkspaceSafe = Typemoq.Mock.ofType>(); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(unsafeInterpretersKey, [])) + .returns(() => unsafeInterpreters.object); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(safeInterpretersKey, [])) + .returns(() => safeInterpreters.object); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(unsafeInterpreterPromptKey, true)) + .returns(() => unsafeInterpreterPromptEnabled.object); + persistentStateFactory + .setup(p => p.createGlobalPersistentState(flaggedWorkspacesKeysStorageKey, [])) + .returns(() => flaggedWorkspacesKeysStorage.object); + interpreterSecurityStorage = new InterpreterSecurityStorage( + persistentStateFactory.object, + workspaceService.object, + commandManager.object, + [] + ); + }); + + test('Command is registered in the activate() method', async () => { + commandManager + .setup(c => c.registerCommand(Commands.ResetInterpreterSecurityStorage, Typemoq.It.isAny())) + .returns(() => Typemoq.Mock.ofType().object) + .verifiable(Typemoq.Times.once()); + + await interpreterSecurityStorage.activate(); + + commandManager.verifyAll(); + }); + + test('Flagged workspace keys are stored correctly', async () => { + flaggedWorkspacesKeysStorage + .setup(f => f.value) + .returns(() => ['workspace1Key']) + .verifiable(Typemoq.Times.once()); + const workspace2 = Uri.parse('2'); + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(workspace2)).returns(() => workspace2.fsPath); + + const workspace2Key = interpreterSecurityStorage._getKeyForWorkspace(workspace2); + + flaggedWorkspacesKeysStorage + .setup(f => f.updateValue(['workspace1Key', workspace2Key])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + + await interpreterSecurityStorage.storeKeyForWorkspace(workspace2); + + expect(workspace2Key).to.equal(`ARE_INTERPRETERS_SAFE_FOR_WS_${workspace2.fsPath}`); + }); + + test('All kinds of storages are cleared upon invoking the command', async () => { + const areInterpretersInWorkspace1Safe = Typemoq.Mock.ofType>(); + const areInterpretersInWorkspace2Safe = Typemoq.Mock.ofType>(); + + flaggedWorkspacesKeysStorage.setup(f => f.value).returns(() => ['workspace1Key', 'workspace2Key']); + safeInterpreters + .setup(s => s.updateValue([])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + unsafeInterpreters + .setup(s => s.updateValue([])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + unsafeInterpreterPromptEnabled + .setup(s => s.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + persistentStateFactory + .setup(p => p.createGlobalPersistentState('workspace1Key', undefined)) + .returns(() => areInterpretersInWorkspace1Safe.object); + areInterpretersInWorkspace1Safe + .setup(s => s.updateValue(undefined)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + persistentStateFactory + .setup(p => p.createGlobalPersistentState('workspace2Key', undefined)) + .returns(() => areInterpretersInWorkspace2Safe.object); + areInterpretersInWorkspace2Safe + .setup(s => s.updateValue(undefined)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + + await interpreterSecurityStorage.resetInterpreterSecurityStorage(); + + areInterpretersInWorkspace1Safe.verifyAll(); + areInterpretersInWorkspace2Safe.verifyAll(); + safeInterpreters.verifyAll(); + unsafeInterpreterPromptEnabled.verifyAll(); + unsafeInterpreters.verifyAll(); + }); + + test('Method areInterpretersInWorkspaceSafe() returns the areInterpretersInWorkspaceSafe storage', () => { + workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup(p => + p.createGlobalPersistentState( + `ARE_INTERPRETERS_SAFE_FOR_WS_${resource.fsPath}`, + undefined + ) + ) + .returns(() => areInterpretersInWorkspaceSafe.object); + const result = interpreterSecurityStorage.hasUserApprovedWorkspaceInterpreters(resource); + assert(areInterpretersInWorkspaceSafe.object === result); + }); + + test('Get unsafeInterpreterPromptEnabled() returns the unsafeInterpreterPromptEnabled storage', () => { + const result = interpreterSecurityStorage.unsafeInterpreterPromptEnabled; + assert(unsafeInterpreterPromptEnabled.object === result); + }); + + test('Get unsafeInterpreters() returns the unsafeInterpreters storage', () => { + const result = interpreterSecurityStorage.unsafeInterpreters; + assert(unsafeInterpreters.object === result); + }); + + test('Get safeInterpreters() returns the safeInterpreters storage', () => { + const result = interpreterSecurityStorage.safeInterpreters; + assert(safeInterpreters.object === result); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts index 18d4424f8f2a..33d1aa7a2e0b 100644 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts @@ -32,18 +32,16 @@ import { InterpreterAutoSelectionService } from '../../../../client/interpreter/ 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 + PythonInterpreter } 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', () => { + type PythonPathInConfig = { workspaceFolderValue: string; workspaceValue: string }; let rule: WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest; let stateFactory: IPersistentStateFactory; let fs: IFileSystem; @@ -52,7 +50,6 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { let platform: IPlatformService; let pipEnvLocator: IInterpreterLocatorService; let virtualEnvLocator: IInterpreterLocatorService; - let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; let workspaceService: IWorkspaceService; let experimentsManager: IExperimentsManager; let interpreterPathService: IInterpreterPathService; @@ -82,7 +79,6 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { pipEnvLocator = mock(KnownPathsService); workspaceService = mock(WorkspaceService); virtualEnvLocator = mock(KnownPathsService); - pythonPathUpdaterService = mock(PythonPathUpdaterService); experimentsManager = mock(ExperimentsManager); interpreterPathService = mock(InterpreterPathService); @@ -95,7 +91,6 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { instance(stateFactory), instance(platform), instance(workspaceService), - instance(pythonPathUpdaterService), instance(pipEnvLocator), instance(virtualEnvLocator), instance(experimentsManager), @@ -136,7 +131,6 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { 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 @@ -164,7 +158,6 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { test('If in experiment, use new API to fetch 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 @@ -193,38 +186,6 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { verify(interpreterPathService.inspect(folderUri)).once(); pythonPathInConfig.verifyAll(); }); - test('Does not update settings when there is no interpreter', async () => { - await rule.cacheSelectedInterpreter(undefined, {} as any); - - verify(pythonPathUpdaterService.updatePythonPath(anything(), anything(), anything(), anything())).never(); - }); - test('Does not update 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); @@ -333,13 +294,16 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { }); 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()); + pythonPathInConfig + .setup(p => p.workspaceValue) + .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); @@ -364,13 +328,16 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { }); 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()); + pythonPathInConfig + .setup(p => p.workspaceValue) + .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); @@ -395,13 +362,16 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { }); test('Wait for virtualEnv if pipEnv completes without any interpreters', 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()); + pythonPathInConfig + .setup(p => p.workspaceValue) + .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); @@ -426,13 +396,16 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { }); test('Wait for pipEnv if VirtualEnv completes without any interpreters', 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()); + pythonPathInConfig + .setup(p => p.workspaceValue) + .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); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 0547d111de6e..bbc12abfd496 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -11,6 +11,9 @@ import { EnvironmentActivationService } from '../../client/interpreter/activatio import { TerminalEnvironmentActivationService } from '../../client/interpreter/activation/terminalEnvironmentActivationService'; import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; import { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; +import { InterpreterEvaluation } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; import { InterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/proxy'; import { CachedInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/cached'; import { CurrentPathInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/currentPath'; @@ -22,7 +25,10 @@ import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, - IInterpreterAutoSeletionProxyService + IInterpreterAutoSeletionProxyService, + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage } from '../../client/interpreter/autoSelection/types'; import { InterpreterComparer } from '../../client/interpreter/configuration/interpreterComparer'; import { InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; @@ -111,6 +117,10 @@ suite('Interpreters - Service Registry', () => { registerTypes(instance(serviceManager)); [ + [IExtensionSingleActivationService, InterpreterSecurityStorage], + [IInterpreterEvaluation, InterpreterEvaluation], + [IInterpreterSecurityStorage, InterpreterSecurityStorage], + [IInterpreterSecurityService, InterpreterSecurityService], [IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters], [IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global'], [IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'], diff --git a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts index 94c7b84ea96f..cc465e8f6c29 100644 --- a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts @@ -13,7 +13,7 @@ import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IPlatformService } from '../../../client/common/platform/types'; import { IBrowserService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; import { createDeferred, createDeferredFromPromise, sleep } from '../../../client/common/utils/async'; -import { Common, InteractiveShiftEnterBanner, Interpreters } from '../../../client/common/utils/localize'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; import { IInterpreterService, InterpreterType } from '../../../client/interpreter/contracts'; import { CondaInheritEnvPrompt, @@ -68,11 +68,11 @@ suite('Conda Inherit Env Prompt', async () => { ); const workspaceConfig = TypeMoq.Mock.ofType(); interpreterService - .setup((is) => is.getActiveInterpreter(resource)) + .setup(is => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(undefined) as any) .verifiable(TypeMoq.Times.never()); workspaceService - .setup((ws) => ws.getConfiguration('terminal', resource)) + .setup(ws => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -82,7 +82,7 @@ suite('Conda Inherit Env Prompt', async () => { }); test('Returns false if on Windows', async () => { platformService - .setup((ps) => ps.isWindows) + .setup(ps => ps.isWindows) .returns(() => true) .verifiable(TypeMoq.Times.once()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -96,15 +96,15 @@ suite('Conda Inherit Env Prompt', async () => { }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup((ps) => ps.isWindows) + .setup(ps => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup((is) => is.getActiveInterpreter(resource)) + .setup(is => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal', resource)) + .setup(ws => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -115,15 +115,15 @@ suite('Conda Inherit Env Prompt', async () => { test('Returns false if no active interpreter is present', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup((ps) => ps.isWindows) + .setup(ps => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup((is) => is.getActiveInterpreter(resource)) + .setup(is => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal', resource)) + .setup(ws => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -137,19 +137,19 @@ suite('Conda Inherit Env Prompt', async () => { }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup((ps) => ps.isWindows) + .setup(ps => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup((is) => is.getActiveInterpreter(resource)) + .setup(is => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal', resource)) + .setup(ws => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup((ws) => ws.inspect('integrated.inheritEnv')) + .setup(ws => ws.inspect('integrated.inheritEnv')) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -182,26 +182,26 @@ suite('Conda Inherit Env Prompt', async () => { workspaceFolderValue: false } } - ].forEach((testParams) => { + ].forEach(testParams => { test(testParams.name, async () => { const interpreter = { type: InterpreterType.Conda }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup((ps) => ps.isWindows) + .setup(ps => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup((is) => is.getActiveInterpreter(resource)) + .setup(is => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal', resource)) + .setup(ws => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup((ws) => ws.inspect('integrated.inheritEnv')) + .setup(ws => ws.inspect('integrated.inheritEnv')) .returns(() => testParams.settings as any); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); expect(result).to.equal(false, 'Prompt should not be shown'); @@ -220,18 +220,18 @@ suite('Conda Inherit Env Prompt', async () => { }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup((ps) => ps.isWindows) + .setup(ps => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup((is) => is.getActiveInterpreter(resource)) + .setup(is => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal', resource)) + .setup(ws => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); - workspaceConfig.setup((ws) => ws.inspect('integrated.inheritEnv')).returns(() => settings as any); + workspaceConfig.setup(ws => ws.inspect('integrated.inheritEnv')).returns(() => settings as any); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); expect(result).to.equal(true, 'Prompt should be shown'); expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); @@ -348,11 +348,7 @@ suite('Conda Inherit Env Prompt', async () => { }); suite('Method promptAndUpdate()', () => { - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.moreInfo() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.moreInfo()]; setup(() => { workspaceService = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -376,11 +372,11 @@ suite('Conda Inherit Env Prompt', async () => { test('Does not display prompt if it is disabled', async () => { notificationPromptEnabled - .setup((n) => n.value) + .setup(n => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -391,27 +387,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Do nothing if no option is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup((n) => n.value) + .setup(n => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal')) + .setup(ws => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); notificationPromptEnabled - .setup((n) => n.updateValue(false)) + .setup(n => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .setup(b => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -424,27 +420,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Update terminal settings if `Yes` is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup((n) => n.value) + .setup(n => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) - .returns(() => Promise.resolve(InteractiveShiftEnterBanner.bannerLabelYes())) + .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes())) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal')) + .setup(ws => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); notificationPromptEnabled - .setup((n) => n.updateValue(false)) + .setup(n => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .setup(b => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -457,27 +453,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Disable notification prompt if `No` is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup((n) => n.value) + .setup(n => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) - .returns(() => Promise.resolve(InteractiveShiftEnterBanner.bannerLabelNo())) + .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo())) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal')) + .setup(ws => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); notificationPromptEnabled - .setup((n) => n.updateValue(false)) + .setup(n => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .setup(b => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -490,27 +486,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Launch browser if `More info` option is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup((n) => n.value) + .setup(n => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(Common.moreInfo())) .verifiable(TypeMoq.Times.once()); workspaceService - .setup((ws) => ws.getConfiguration('terminal')) + .setup(ws => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); notificationPromptEnabled - .setup((n) => n.updateValue(false)) + .setup(n => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); browserService - .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .setup(b => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); await condaInheritEnvPrompt.promptAndUpdate(); diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index d3ed199313cc..84d5a5faa209 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -10,7 +10,7 @@ import { ApplicationShell } from '../../../client/common/application/application import { IApplicationShell } from '../../../client/common/application/types'; import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { Common, InteractiveShiftEnterBanner } from '../../../client/common/utils/localize'; +import { Common } from '../../../client/common/utils/localize'; import { PythonPathUpdaterService } from '../../../client/interpreter/configuration/pythonPathUpdaterService'; import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; import { @@ -67,11 +67,7 @@ suite('Virtual Environment Prompt', () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; const interpreter2 = { path: 'path/to/interpreter2' }; - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.doNotShowAgain() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; const notificationPromptEnabled = TypeMoq.Mock.ofType>(); // tslint:disable:no-any when(locator.getInterpreters(resource)).thenResolve([interpreter1, interpreter2] as any); @@ -93,11 +89,7 @@ suite('Virtual Environment Prompt', () => { test("If user selects 'Yes', python path is updated", async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.doNotShowAgain() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; const notificationPromptEnabled = TypeMoq.Mock.ofType>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( notificationPromptEnabled.object @@ -130,11 +122,7 @@ suite('Virtual Environment Prompt', () => { test("If user selects 'No', no operation is performed", async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.doNotShowAgain() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; const notificationPromptEnabled = TypeMoq.Mock.ofType>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( notificationPromptEnabled.object @@ -172,11 +160,7 @@ suite('Virtual Environment Prompt', () => { test("If user selects 'Do not show again', prompt is disabled", async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.doNotShowAgain() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; const notificationPromptEnabled = TypeMoq.Mock.ofType>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( notificationPromptEnabled.object @@ -198,11 +182,7 @@ suite('Virtual Environment Prompt', () => { test('If prompt is disabled, no notification is shown', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [ - InteractiveShiftEnterBanner.bannerLabelYes(), - InteractiveShiftEnterBanner.bannerLabelNo(), - Common.doNotShowAgain() - ]; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; const notificationPromptEnabled = TypeMoq.Mock.ofType>(); when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( notificationPromptEnabled.object diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index 2a8630575d41..302361099680 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -55,6 +55,7 @@ suite('Multiroot Linting', () => { ioc.registerVariableTypes(); ioc.registerFileSystemTypes(); ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); ioc.serviceManager.addSingleton(ICondaService, CondaService); ioc.serviceManager.addSingleton( diff --git a/src/test/refactor/extension.refactor.extract.method.test.ts b/src/test/refactor/extension.refactor.extract.method.test.ts index 9a47914d827d..c5dba01879b8 100644 --- a/src/test/refactor/extension.refactor.extract.method.test.ts +++ b/src/test/refactor/extension.refactor.extract.method.test.ts @@ -90,6 +90,7 @@ suite('Method Extraction', () => { ioc.registerCommonTypes(); ioc.registerProcessTypes(); ioc.registerVariableTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/testing/debugger.test.ts b/src/test/testing/debugger.test.ts index 8687a8e9ab67..98238dd7d8b8 100644 --- a/src/test/testing/debugger.test.ts +++ b/src/test/testing/debugger.test.ts @@ -90,6 +90,7 @@ suite('Unit Tests - debugging', () => { ioc.registerTestsHelper(); ioc.registerTestManagers(); ioc.registerMockUnitTestSocketServer(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.add(IArgumentsHelper, ArgumentsHelper); ioc.serviceManager.add(ITestRunner, TestRunner); ioc.serviceManager.add(IXUnitParser, XUnitParser); diff --git a/src/test/testing/nosetest/nosetest.disovery.test.ts b/src/test/testing/nosetest/nosetest.disovery.test.ts index db8858bb756d..42d0196de443 100644 --- a/src/test/testing/nosetest/nosetest.disovery.test.ts +++ b/src/test/testing/nosetest/nosetest.disovery.test.ts @@ -78,6 +78,7 @@ suite('Unit Tests - nose - discovery with mocked process output', () => { ioc.registerVariableTypes(); ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/testing/nosetest/nosetest.run.test.ts b/src/test/testing/nosetest/nosetest.run.test.ts index 539049219624..e6e26f1addf0 100644 --- a/src/test/testing/nosetest/nosetest.run.test.ts +++ b/src/test/testing/nosetest/nosetest.run.test.ts @@ -75,6 +75,7 @@ suite('Unit Tests - nose - run against actual python process', () => { ioc.registerMockProcessTypes(); ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(ICondaService, CondaService); ioc.serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); diff --git a/src/test/testing/nosetest/nosetest.test.ts b/src/test/testing/nosetest/nosetest.test.ts index 70aeb981757e..54d3d29f5a7a 100644 --- a/src/test/testing/nosetest/nosetest.test.ts +++ b/src/test/testing/nosetest/nosetest.test.ts @@ -66,6 +66,7 @@ suite('Unit Tests - nose - discovery against actual python process', () => { ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(ICondaService, CondaService); } diff --git a/src/test/testing/pytest/pytest.discovery.test.ts b/src/test/testing/pytest/pytest.discovery.test.ts index 803f68fe845e..97711e355ff2 100644 --- a/src/test/testing/pytest/pytest.discovery.test.ts +++ b/src/test/testing/pytest/pytest.discovery.test.ts @@ -131,6 +131,7 @@ suite('Unit Tests - pytest - discovery with mocked process output', () => { // Mocks. ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/testing/pytest/pytest.run.test.ts b/src/test/testing/pytest/pytest.run.test.ts index bd202163fd04..9d64bfba9581 100644 --- a/src/test/testing/pytest/pytest.run.test.ts +++ b/src/test/testing/pytest/pytest.run.test.ts @@ -442,6 +442,7 @@ suite('Unit Tests - pytest - run with mocked process output', () => { ioc.registerVariableTypes(); // Mocks. ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/testing/pytest/pytest.test.ts b/src/test/testing/pytest/pytest.test.ts index 00e06217019b..0dc439b1afe2 100644 --- a/src/test/testing/pytest/pytest.test.ts +++ b/src/test/testing/pytest/pytest.test.ts @@ -45,6 +45,7 @@ suite('Unit Tests - pytest - discovery against actual python process', () => { ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(ICondaService, CondaService); } diff --git a/src/test/testing/rediscover.test.ts b/src/test/testing/rediscover.test.ts index 70394949426e..44c3df297215 100644 --- a/src/test/testing/rediscover.test.ts +++ b/src/test/testing/rediscover.test.ts @@ -53,6 +53,7 @@ suite('Unit Tests re-discovery', () => { ioc.registerProcessTypes(); ioc.registerVariableTypes(); ioc.registerUnitTestTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/testing/serviceRegistry.ts b/src/test/testing/serviceRegistry.ts index a79bc840831c..0a9df7793822 100644 --- a/src/test/testing/serviceRegistry.ts +++ b/src/test/testing/serviceRegistry.ts @@ -18,6 +18,16 @@ import { INotebookImporter, INotebookServer } from '../../client/datascience/types'; +import { InterpreterEvaluation } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; +import { + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; import { IServiceContainer } from '../../client/ioc/types'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/testing/common/constants'; import { TestContextService } from '../../client/testing/common/services/contextService'; @@ -153,6 +163,13 @@ export class UnitTestIocContainer extends IocContainer { }); } + public registerInterpreterStorageTypes() { + this.serviceManager.add(IInterpreterSecurityStorage, InterpreterSecurityStorage); + this.serviceManager.add(IInterpreterSecurityService, InterpreterSecurityService); + this.serviceManager.add(IInterpreterEvaluation, InterpreterEvaluation); + this.serviceManager.add(IInterpreterHelper, InterpreterHelper); + } + public registerTestManagerService() { this.serviceManager.addFactory(ITestManagerServiceFactory, (context) => { return (workspaceFolder: Uri) => { diff --git a/src/test/testing/stoppingDiscoverAndTest.test.ts b/src/test/testing/stoppingDiscoverAndTest.test.ts index 466941ae50ec..e8c1c1603869 100644 --- a/src/test/testing/stoppingDiscoverAndTest.test.ts +++ b/src/test/testing/stoppingDiscoverAndTest.test.ts @@ -53,6 +53,7 @@ suite('Unit Tests Stopping Discovery and Runner', () => { ioc.registerTestStorage(); ioc.registerTestsHelper(); ioc.registerTestDiagnosticServices(); + ioc.registerInterpreterStorageTypes(); } test('Running tests should not stop existing discovery', async () => { diff --git a/src/test/testing/unittest/unittest.discovery.test.ts b/src/test/testing/unittest/unittest.discovery.test.ts index cd5fc08e1f34..b88027b42789 100644 --- a/src/test/testing/unittest/unittest.discovery.test.ts +++ b/src/test/testing/unittest/unittest.discovery.test.ts @@ -60,6 +60,7 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => { // Mocks. ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, diff --git a/src/test/testing/unittest/unittest.run.test.ts b/src/test/testing/unittest/unittest.run.test.ts index df97d22554cd..85428c185634 100644 --- a/src/test/testing/unittest/unittest.run.test.ts +++ b/src/test/testing/unittest/unittest.run.test.ts @@ -108,6 +108,7 @@ suite('Unit Tests - unittest - run with mocked process output', () => { ioc.registerTestsHelper(); ioc.registerTestStorage(); ioc.registerTestVisitors(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.add(IArgumentsService, ArgumentsService, UNITTEST_PROVIDER); ioc.serviceManager.add(IArgumentsHelper, ArgumentsHelper); ioc.serviceManager.add(ITestManagerRunner, TestManagerRunner, UNITTEST_PROVIDER); diff --git a/src/test/testing/unittest/unittest.test.ts b/src/test/testing/unittest/unittest.test.ts index 0b8ae0b9b337..0a184b5b679e 100644 --- a/src/test/testing/unittest/unittest.test.ts +++ b/src/test/testing/unittest/unittest.test.ts @@ -52,6 +52,7 @@ suite('Unit Tests - unittest - discovery against actual python process', () => { ioc.registerVariableTypes(); ioc.registerUnitTestTypes(); ioc.registerProcessTypes(); + ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(ICondaService, instance(mock(CondaService))); ioc.serviceManager.addSingletonInstance( IInterpreterService, From 76a3d6ca8b22828edcddade98da6c816c5342b77 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 7 Apr 2020 12:43:36 -0700 Subject: [PATCH 4/7] Correct prettier errors --- src/client/activation/activationManager.ts | 10 +- .../checks/macPythonInterpreter.ts | 12 +- src/client/common/experiments.ts | 10 +- src/client/common/utils/decorators.ts | 30 +-- .../interpreterSecurityStorage.ts | 2 +- .../configuration/interpreterSelector.ts | 2 +- src/client/interpreter/interpreterService.ts | 20 +- .../activation/activationManager.unit.test.ts | 80 ++++---- .../checks/macPythonInterpreter.unit.test.ts | 186 ++++++++++-------- .../configSettings.unit.test.ts | 54 ++--- .../common/configuration/service.unit.test.ts | 58 +++--- src/test/common/experiments.unit.test.ts | 110 +++++------ .../interpreterPathService.unit.test.ts | 116 +++++------ src/test/common/moduleInstaller.test.ts | 68 +++---- src/test/common/serviceRegistry.unit.test.ts | 8 +- .../interpreterSelector.unit.test.ts | 88 ++++----- .../datascience/dataScienceIocContainer.ts | 46 ++--- .../interpreterEvaluation.unit.test.ts | 50 ++--- .../interpreterSecurityService.unit.test.ts | 30 +-- .../interpreterSecurityStorage.unit.test.ts | 36 ++-- .../rules/workspaceEnv.unit.test.ts | 10 +- src/test/interpreters/display.unit.test.ts | 86 ++++---- .../interpreterService.unit.test.ts | 46 ++--- .../pythonPathUpdaterFactory.unit.test.ts | 78 ++++---- .../condaInheritEnvPrompt.unit.test.ts | 96 ++++----- src/test/startupTelemetry.unit.test.ts | 30 +-- 26 files changed, 693 insertions(+), 669 deletions(-) diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index 8d99473f31b0..e4517144a720 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -54,7 +54,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { await this.initialize(); // Activate all activation services together. await Promise.all([ - Promise.all(this.singleActivationServices.map(item => item.activate())), + Promise.all(this.singleActivationServices.map((item) => item.activate())), this.activateWorkspace(this.activeResourceService.getActiveResource()) ]); await this.autoSelection.autoSelectInterpreter(undefined); @@ -73,7 +73,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { await this.autoSelection.autoSelectInterpreter(resource); await this.evaluateAutoSelectedInterpreterSafety(resource); - await Promise.all(this.activationServices.map(item => item.activate(resource))); + await Promise.all(this.activationServices.map((item) => item.activate(resource))); await this.appDiagnostics.performPreStartupHealthCheck(resource); } public async initialize() { @@ -125,7 +125,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { protected addHandlers() { this.disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); this.disposables.push( - this.interpreterPathService.onDidChange(i => this.evaluateAutoSelectedInterpreterSafety(i.uri)) + this.interpreterPathService.onDidChange((i) => this.evaluateAutoSelectedInterpreterSafety(i.uri)) ); } protected addRemoveDocOpenedHandlers() { @@ -142,11 +142,11 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } protected onWorkspaceFoldersChanged() { //If an activated workspace folder was removed, delete its key - const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => + const workspaceKeys = this.workspaceService.workspaceFolders!.map((workspaceFolder) => this.getWorkspaceKey(workspaceFolder.uri) ); const activatedWkspcKeys = Array.from(this.activatedWorkspaces.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); if (activatedWkspcFoldersRemoved.length > 0) { for (const folder of activatedWkspcFoldersRemoved) { this.activatedWorkspaces.delete(folder); diff --git a/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/src/client/application/diagnostics/checks/macPythonInterpreter.ts index f409c473a454..26344f41a607 100644 --- a/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -102,7 +102,7 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { } const interpreters = await this.interpreterService.getInterpreters(resource); - if (interpreters.filter(i => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { + if (interpreters.filter((i) => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { return [ new InvalidMacPythonInterpreterDiagnostic( DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, @@ -127,7 +127,7 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { DiagnosticCommandPromptHandlerServiceId ); await Promise.all( - diagnostics.map(async diagnostic => { + diagnostics.map(async (diagnostic) => { const canHandle = await this.canHandle(diagnostic); const shouldIgnore = await this.filterService.shouldIgnoreDiagnostic(diagnostic.code); if (!canHandle || shouldIgnore) { @@ -144,7 +144,7 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); const experiments = this.serviceContainer.get(IExperimentsManager); if (experiments.inExperiment(DeprecatePythonPath.experiment)) { - disposables.push(interpreterPathService.onDidChange(i => this.onDidChangeConfiguration(undefined, i))); + disposables.push(interpreterPathService.onDidChange((i) => this.onDidChangeConfiguration(undefined, i))); } experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); @@ -157,9 +157,9 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { if (event) { const workspaceService = this.serviceContainer.get(IWorkspaceService); const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders - ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) + ? workspaceService.workspaceFolders!.map((workspace) => workspace.uri) : [undefined]; - const workspaceUriIndex = workspacesUris.findIndex(uri => + const workspaceUriIndex = workspacesUris.findIndex((uri) => event.affectsConfiguration('python.pythonPath', uri) ); if (workspaceUriIndex === -1) { @@ -182,7 +182,7 @@ export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { this.timeOut = setTimeout(() => { this.timeOut = undefined; this.diagnose(workspaceUri) - .then(diagnostics => this.handle(diagnostics)) + .then((diagnostics) => this.handle(diagnostics)) .ignoreErrors(); }, this.changeThrottleTimeout); } diff --git a/src/client/common/experiments.ts b/src/client/common/experiments.ts index d2372dbc4339..f4655f63f36f 100644 --- a/src/client/common/experiments.ts +++ b/src/client/common/experiments.ts @@ -139,7 +139,7 @@ export class ExperimentsManager implements IExperimentsManager { return false; } this.sendTelemetryIfInExperiment(experimentName); - return this.userExperiments.find(exp => exp.name === experimentName) ? true : false; + return this.userExperiments.find((exp) => exp.name === experimentName) ? true : false; } /** @@ -180,7 +180,7 @@ export class ExperimentsManager implements IExperimentsManager { @traceDecorators.error('Failed to send telemetry when user is in experiment') public sendTelemetryIfInExperiment(experimentName: string): void { - if (this.userExperiments.find(exp => exp.name === experimentName)) { + if (this.userExperiments.find((exp) => exp.name === experimentName)) { sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS, undefined, { expName: experimentName }); } } @@ -223,7 +223,7 @@ export class ExperimentsManager implements IExperimentsManager { throw new Error('Machine ID should be a string'); } let hash: number; - if (oldExperimentSalts.find(oldSalt => oldSalt === salt)) { + if (oldExperimentSalts.find((oldSalt) => oldSalt === salt)) { hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'SHA512'); } else { hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'FNV'); @@ -339,7 +339,7 @@ export class ExperimentsManager implements IExperimentsManager { this._experimentsOptedOutFrom[i] = ''; } } - this._experimentsOptedInto = this._experimentsOptedInto.filter(exp => exp !== ''); - this._experimentsOptedOutFrom = this._experimentsOptedOutFrom.filter(exp => exp !== ''); + this._experimentsOptedInto = this._experimentsOptedInto.filter((exp) => exp !== ''); + this._experimentsOptedOutFrom = this._experimentsOptedOutFrom.filter((exp) => exp !== ''); } } diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts index 10da3870d363..e0e85bfded1b 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -57,7 +57,7 @@ export function debounceAsync(wait?: number) { export function makeDebounceDecorator(wait?: number) { // tslint:disable-next-line:no-any no-function-expression - return function(_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { // We could also make use of _debounce() options. For instance, // the following causes the original method to be called // immediately: @@ -72,7 +72,7 @@ export function makeDebounceDecorator(wait?: number) { const options = {}; const originalMethod = descriptor.value!; const debounced = _debounce( - function(this: any) { + function (this: any) { return originalMethod.apply(this, arguments as any); }, wait, @@ -84,7 +84,7 @@ export function makeDebounceDecorator(wait?: number) { export function makeDebounceAsyncDecorator(wait?: number) { // tslint:disable-next-line:no-any no-function-expression - return function(_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { type StateInformation = { started: boolean; deferred: Deferred | undefined; @@ -94,7 +94,7 @@ export function makeDebounceAsyncDecorator(wait?: number) { const state: StateInformation = { started: false, deferred: undefined, timer: undefined }; // Lets defer execution using a setTimeout for the given time. - (descriptor as any).value = function(this: any) { + (descriptor as any).value = function (this: any) { const existingDeferred: Deferred | undefined = state.deferred; if (existingDeferred && state.started) { return existingDeferred.promise; @@ -112,11 +112,11 @@ export function makeDebounceAsyncDecorator(wait?: number) { state.started = true; originalMethod .apply(this) - .then(r => { + .then((r) => { state.started = false; deferred.resolve(r); }) - .catch(ex => { + .catch((ex) => { state.started = false; deferred.reject(ex); }); @@ -141,7 +141,7 @@ export function clearCachedResourceSpecificIngterpreterData( type PromiseFunctionWithAnyArgs = (...any: any) => Promise; const cacheStoreForMethods = getGlobalCacheStore(); export function cache(expiryDurationMs: number) { - return function( + return function ( target: Object, propertyName: string, descriptor: TypedPropertyDescriptor @@ -149,7 +149,7 @@ export function cache(expiryDurationMs: number) { const originalMethod = descriptor.value!; const className = 'constructor' in target && target.constructor.name ? target.constructor.name : ''; const keyPrefix = `Cache_Method_Output_${className}.${propertyName}`; - descriptor.value = async function(...args: any) { + descriptor.value = async function (...args: any) { if (isTestExecution()) { return originalMethod.apply(this, args) as Promise; } @@ -161,7 +161,9 @@ export function cache(expiryDurationMs: number) { } const promise = originalMethod.apply(this, args) as Promise; promise - .then(result => cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryDurationMs })) + .then((result) => + cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryDurationMs }) + ) .ignoreErrors(); return promise; }; @@ -177,18 +179,18 @@ export function cache(expiryDurationMs: number) { */ export function swallowExceptions(scopeName: string) { // tslint:disable-next-line:no-any no-function-expression - return function(_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { + return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value!; const errorMessage = `Python Extension (Error in ${scopeName}, method:${propertyName}):`; // tslint:disable-next-line:no-any no-function-expression - descriptor.value = function(...args: any[]) { + descriptor.value = function (...args: any[]) { try { // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any const result = originalMethod.apply(this, args); // If method being wrapped returns a promise then wait and swallow errors. if (result && typeof result.then === 'function' && typeof result.catch === 'function') { - return (result as Promise).catch(error => { + return (result as Promise).catch((error) => { if (isTestExecution()) { return; } @@ -209,10 +211,10 @@ export function swallowExceptions(scopeName: string) { type PromiseFunction = (...any: any[]) => Promise; export function displayProgress(title: string, location = ProgressLocation.Window) { - return function(_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function (_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value!; // tslint:disable-next-line:no-any no-function-expression - descriptor.value = async function(...args: any[]) { + descriptor.value = async function (...args: any[]) { const progressOptions: ProgressOptions = { location, title }; // tslint:disable-next-line:no-invalid-this const promise = originalMethod.apply(this, args); diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts index ea89c7d7d810..f75317a859cb 100644 --- a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts @@ -73,7 +73,7 @@ export class InterpreterSecurityStorage implements IInterpreterSecurityStorage { } public async resetInterpreterSecurityStorage(): Promise { - this.flaggedWorkspacesKeysStorage.value.forEach(async key => { + this.flaggedWorkspacesKeysStorage.value.forEach(async (key) => { const areInterpretersInWorkspaceSafe = this.persistentStateFactory.createGlobalPersistentState< boolean | undefined >(key, undefined); diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index e9caf2207d4c..d1b03882f461 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -174,7 +174,7 @@ export class InterpreterSelector implements IInterpreterSelector { type WorkspaceSelectionQuickPickItem = QuickPickItem & { uri: Uri }; const quickPickItems: WorkspaceSelectionQuickPickItem[] = [ - ...this.workspaceService.workspaceFolders.map(w => ({ + ...this.workspaceService.workspaceFolders.map((w) => ({ label: w.name, description: path.dirname(w.uri.fsPath), uri: w.uri diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 33f08370bfc1..81d6330d0aaa 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -84,23 +84,23 @@ 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)) + documentManager.onDidChangeActiveTextEditor((e) => (e ? this.refresh(e.document.uri) : undefined)) ); const workspaceService = this.serviceContainer.get(IWorkspaceService); const pySettings = this.configService.getSettings(); this._pythonPathSetting = pySettings.pythonPath; if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { disposables.push( - this.interpreterPathService.onDidChange(i => { + this.interpreterPathService.onDidChange((i) => { this._onConfigChanged(i.uri); }) ); } else { const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders - ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) + ? workspaceService.workspaceFolders!.map((workspace) => workspace.uri) : [undefined]; - const disposable = workspaceService.onDidChangeConfiguration(e => { - const workspaceUriIndex = workspacesUris.findIndex(uri => + const disposable = workspaceService.onDidChangeConfiguration((e) => { + const workspaceUriIndex = workspacesUris.findIndex((uri) => e.affectsConfiguration('python.pythonPath', uri) ); const workspaceUri = workspaceUriIndex === -1 ? undefined : workspacesUris[workspaceUriIndex]; @@ -116,8 +116,8 @@ export class InterpreterService implements Disposable, IInterpreterService { const interpreters = await this.locator.getInterpreters(resource); await Promise.all( interpreters - .filter(item => !item.displayName) - .map(async item => { + .filter((item) => !item.displayName) + .map(async (item) => { item.displayName = await this.getDisplayName(item, resource); // Keep information up to date with latest details. if (!item.cachedEntry) { @@ -176,7 +176,7 @@ export class InterpreterService implements Disposable, IInterpreterService { // This is the preferred approach, hence the delay in option 1. const option2 = (async () => { const interpreters = await this.getInterpreters(resource); - const found = interpreters.find(i => fs.arePathsSame(i.path, pythonPath)); + const found = interpreters.find((i) => fs.arePathsSame(i.path, pythonPath)); if (found) { // Cache the interpreter info, only if we get the data from interpretr list. // tslint:disable-next-line:no-any @@ -257,13 +257,13 @@ export class InterpreterService implements Disposable, IInterpreterService { this._pythonPathSetting = pySettings.pythonPath; this.didChangeInterpreterEmitter.fire(); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); - interpreterDisplay.refresh().catch(ex => traceError('Python Extension: display.refresh', ex)); + interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); } }; protected async getInterepreterFileHash(pythonPath: string): Promise { return this.hashProviderFactory .create({ pythonPath }) - .then(provider => provider.getInterpreterHash(pythonPath)); + .then((provider) => provider.getInterpreterHash(pythonPath)); } protected async updateCachedInterpreterInformation(info: PythonInterpreter, resource: Resource): Promise { const key = JSON.stringify(info); diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index c864ab466da9..4defb6da89e0 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -76,7 +76,7 @@ suite('Language Server Activation - ActivationManager', () => { activationService2 = mock(LanguageServerExtensionActivationService); fileSystem = mock(FileSystem); interpreterPathService - .setup(i => i.onDidChange(typemoq.It.isAny())) + .setup((i) => i.onDidChange(typemoq.It.isAny())) .returns(() => typemoq.Mock.ofType().object); managerTest = new ExtensionActivationManagerTest( [instance(activationService1), instance(activationService2)], @@ -109,7 +109,7 @@ suite('Language Server Activation - ActivationManager', () => { when(workspaceService.hasWorkspaceFolders).thenReturn(true); const eventDef = () => disposable2.object; documentManager - .setup(d => d.onDidOpenTextDocument) + .setup((d) => d.onDidOpenTextDocument) .returns(() => eventDef) .verifiable(typemoq.Times.once()); @@ -121,8 +121,8 @@ suite('Language Server Activation - ActivationManager', () => { documentManager.verifyAll(); - disposable.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); + disposable.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); managerTest.dispose(); @@ -137,11 +137,11 @@ suite('Language Server Activation - ActivationManager', () => { when(workspaceService.hasWorkspaceFolders).thenReturn(true); const eventDef = () => disposable2.object; documentManager - .setup(d => d.onDidOpenTextDocument) + .setup((d) => d.onDidOpenTextDocument) .returns(() => eventDef) .verifiable(typemoq.Times.once()); - disposable.setup(d => d.dispose()); - disposable2.setup(d => d.dispose()); + disposable.setup((d) => d.dispose()); + disposable2.setup((d) => d.dispose()); await managerTest.initialize(); @@ -149,8 +149,8 @@ suite('Language Server Activation - ActivationManager', () => { verify(workspaceService.hasWorkspaceFolders).once(); verify(workspaceService.onDidChangeWorkspaceFolders).once(); documentManager.verifyAll(); - disposable.verify(d => d.dispose(), typemoq.Times.never()); - disposable2.verify(d => d.dispose(), typemoq.Times.never()); + disposable.verify((d) => d.dispose(), typemoq.Times.never()); + disposable2.verify((d) => d.dispose(), typemoq.Times.never()); when(workspaceService.workspaceFolders).thenReturn([]); when(workspaceService.hasWorkspaceFolders).thenReturn(false); @@ -158,13 +158,13 @@ suite('Language Server Activation - ActivationManager', () => { await managerTest.initialize(); verify(workspaceService.hasWorkspaceFolders).twice(); - disposable.verify(d => d.dispose(), typemoq.Times.never()); - disposable2.verify(d => d.dispose(), typemoq.Times.once()); + disposable.verify((d) => d.dispose(), typemoq.Times.never()); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); managerTest.dispose(); - disposable.verify(d => d.dispose(), typemoq.Times.atLeast(1)); - disposable2.verify(d => d.dispose(), typemoq.Times.once()); + disposable.verify((d) => d.dispose(), typemoq.Times.atLeast(1)); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); }); test('Activate workspace specific to the resource in case of Multiple workspaces when a file is opened', async () => { const disposable1 = typemoq.Mock.ofType(); @@ -173,16 +173,16 @@ suite('Language Server Activation - ActivationManager', () => { let workspaceFoldersChangedHandler!: Function; const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType(); - document.setup(d => d.uri).returns(() => documentUri); - document.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE); + document.setup((d) => d.uri).returns(() => documentUri); + document.setup((d) => d.languageId).returns(() => PYTHON_LANGUAGE); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { workspaceFoldersChangedHandler = cb; return disposable1.object; }); documentManager - .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback(cb => (fileOpenedHandler = cb)) + .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => (fileOpenedHandler = cb)) .returns(() => disposable2.object) .verifiable(typemoq.Times.once()); @@ -199,11 +199,11 @@ suite('Language Server Activation - ActivationManager', () => { when(activationService2.activate(resource)).thenResolve(); when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection - .setup(a => a.autoSelectInterpreter(resource)) + .setup((a) => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); appDiagnostics - .setup(a => a.performPreStartupHealthCheck(resource)) + .setup((a) => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); // Add workspaceFoldersChangedHandler @@ -232,11 +232,11 @@ suite('Language Server Activation - ActivationManager', () => { when(activationService2.activate(resource)).thenResolve(); when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection - .setup(a => a.autoSelectInterpreter(resource)) + .setup((a) => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); appDiagnostics - .setup(a => a.performPreStartupHealthCheck(resource)) + .setup((a) => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); @@ -252,11 +252,11 @@ suite('Language Server Activation - ActivationManager', () => { when(activationService2.activate(resource)).thenResolve(); when(interpreterService.getInterpreters(anything())).thenResolve(); autoSelection - .setup(a => a.autoSelectInterpreter(resource)) + .setup((a) => a.autoSelectInterpreter(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); appDiagnostics - .setup(a => a.performPreStartupHealthCheck(resource)) + .setup((a) => a.performPreStartupHealthCheck(resource)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); @@ -314,15 +314,15 @@ suite('Language Server Activation - ActivationManager', () => { let workspaceFoldersChangedHandler!: Function; const documentUri = Uri.file('a'); const document = typemoq.Mock.ofType(); - document.setup(d => d.uri).returns(() => documentUri); + document.setup((d) => d.uri).returns(() => documentUri); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { workspaceFoldersChangedHandler = cb; return disposable1.object; }); documentManager - .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback(cb => (docOpenedHandler = cb)) + .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => (docOpenedHandler = cb)) .returns(() => disposable2.object) .verifiable(typemoq.Times.once()); @@ -354,7 +354,7 @@ suite('Language Server Activation - ActivationManager', () => { //Removed no. of folders to one when(workspaceService.workspaceFolders).thenReturn([folder1]); when(workspaceService.hasWorkspaceFolders).thenReturn(true); - disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); workspaceFoldersChangedHandler.call(managerTest); @@ -404,7 +404,7 @@ suite('Language Server Activation - activate()', () => { activateWorkspace = sinon.stub(ExtensionActivationManager.prototype, 'activateWorkspace'); activateWorkspace.resolves(); interpreterPathService - .setup(i => i.onDidChange(typemoq.It.isAny())) + .setup((i) => i.onDidChange(typemoq.It.isAny())) .returns(() => typemoq.Mock.ofType().object); managerTest = new ExtensionActivationManager( [instance(activationService1), instance(activationService2)], @@ -429,11 +429,11 @@ suite('Language Server Activation - activate()', () => { test('Execution goes as expected if there are no errors', async () => { singleActivationService - .setup(s => s.activate()) + .setup((s) => s.activate()) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); autoSelection - .setup(a => a.autoSelectInterpreter(undefined)) + .setup((a) => a.autoSelectInterpreter(undefined)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); when(activeResourceService.getActiveResource()).thenReturn(resource); @@ -446,11 +446,11 @@ suite('Language Server Activation - activate()', () => { test('Throws error if execution fails', async () => { singleActivationService - .setup(s => s.activate()) + .setup((s) => s.activate()) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); autoSelection - .setup(a => a.autoSelectInterpreter(undefined)) + .setup((a) => a.autoSelectInterpreter(undefined)) .returns(() => Promise.reject(new Error('Kaboom'))) .verifiable(typemoq.Times.once()); when(activeResourceService.getActiveResource()).thenReturn(resource); @@ -509,7 +509,7 @@ suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); autoSelection - .setup(a => a.getAutoSelectedInterpreter(resource)) + .setup((a) => a.getAutoSelectedInterpreter(resource)) .returns(() => interpreter as any) .verifiable(typemoq.Times.once()); when(interpreterPathService.get(resource)).thenReturn('python'); @@ -519,13 +519,13 @@ suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).never(); }); - ['', 'python'].forEach(setting => { + ['', 'python'].forEach((setting) => { test(`If in Deprecate PythonPath experiment, and setting equals '${setting}', fetch autoselected interpreter and evaluate it`, async () => { const interpreter = { path: 'pythonPath' }; when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); autoSelection - .setup(a => a.getAutoSelectedInterpreter(resource)) + .setup((a) => a.getAutoSelectedInterpreter(resource)) .returns(() => interpreter as any) .verifiable(typemoq.Times.once()); when(interpreterPathService.get(resource)).thenReturn('python'); @@ -543,7 +543,7 @@ suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); autoSelection - .setup(a => a.getAutoSelectedInterpreter(resource)) + .setup((a) => a.getAutoSelectedInterpreter(resource)) .returns(() => interpreter as any) .verifiable(typemoq.Times.once()); when(interpreterPathService.get(resource)).thenReturn('python'); @@ -558,7 +558,7 @@ suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); autoSelection - .setup(a => a.getAutoSelectedInterpreter(resource)) + .setup((a) => a.getAutoSelectedInterpreter(resource)) .returns(() => interpreter as any) .verifiable(typemoq.Times.never()); when(interpreterPathService.get(resource)).thenReturn('settingSetToSomePath'); @@ -573,7 +573,7 @@ suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', const evaluateIfInterpreterIsSafeDeferred = createDeferred(); when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); - autoSelection.setup(a => a.getAutoSelectedInterpreter(resource)).returns(() => interpreter as any); + autoSelection.setup((a) => a.getAutoSelectedInterpreter(resource)).returns(() => interpreter as any); when(interpreterPathService.get(resource)).thenReturn('python'); when(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).thenReturn( evaluateIfInterpreterIsSafeDeferred.promise diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index fb5cc2b8ee76..eaa9315341d3 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -60,7 +60,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { serviceContainer = typemoq.Mock.ofType(); messageHandler = typemoq.Mock.ofType>(); serviceContainer - .setup(s => + .setup((s) => s.get( typemoq.It.isValue(IDiagnosticHandlerService), typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) @@ -69,31 +69,33 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .returns(() => messageHandler.object); commandFactory = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) .returns(() => commandFactory.object); settings = typemoq.Mock.ofType(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); const configService = typemoq.Mock.ofType(); - configService.setup(c => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + configService.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IConfigurationService))) + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) .returns(() => configService.object); interpreterService = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IInterpreterService))) + .setup((s) => s.get(typemoq.It.isValue(IInterpreterService))) .returns(() => interpreterService.object); platformService = typemoq.Mock.ofType(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); helper = typemoq.Mock.ofType(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); filterService = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object); platformService - .setup(p => p.isMac) + .setup((p) => p.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); return serviceContainer.object; @@ -120,7 +122,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { ]) { const diagnostic = typemoq.Mock.ofType(); diagnostic - .setup(d => d.code) + .setup((d) => d.code) .returns(() => code) .verifiable(typemoq.Times.atLeastOnce()); @@ -132,7 +134,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { const diagnostic = typemoq.Mock.ofType(); diagnostic - .setup(d => d.code) + .setup((d) => d.code) .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); @@ -143,7 +145,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { test('Should return empty diagnostics if not a Mac', async () => { platformService.reset(); platformService - .setup(p => p.isMac) + .setup((p) => p.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); @@ -153,7 +155,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { }); test('Should return empty diagnostics if installer check is disabled', async () => { settings - .setup(s => s.disableInstallationChecks) + .setup((s) => s.disableInstallationChecks) .returns(() => true) .verifiable(typemoq.Times.once()); @@ -164,25 +166,25 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { }); test('Should return empty diagnostics if there are interpreters, one is selected, and platform is not mac', async () => { settings - .setup(s => s.disableInstallationChecks) + .setup((s) => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.hasInterpreters) + .setup((i) => i.hasInterpreters) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) + .setup((i) => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([{} as any])) .verifiable(typemoq.Times.never()); interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) .verifiable(typemoq.Times.once()); platformService - .setup(i => i.isMac) + .setup((i) => i.isMac) .returns(() => false) .verifiable(typemoq.Times.once()); @@ -194,29 +196,29 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { }); test('Should return empty diagnostics if there are interpreters, platform is mac and selected interpreter is not default mac interpreter', async () => { settings - .setup(s => s.disableInstallationChecks) + .setup((s) => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.hasInterpreters) + .setup((i) => i.hasInterpreters) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) + .setup((i) => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([{} as any])) .verifiable(typemoq.Times.never()); interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) .verifiable(typemoq.Times.once()); platformService - .setup(i => i.isMac) + .setup((i) => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isAny())) + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isAny())) .returns(() => false) .verifiable(typemoq.Times.once()); @@ -229,25 +231,25 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { }); test('Should return diagnostic if there are no other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { settings - .setup(s => s.disableInstallationChecks) + .setup((s) => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) + .setup((i) => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([{ path: pythonPath } as any, { path: pythonPath } as any])) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) .verifiable(typemoq.Times.once()); platformService - .setup(i => i.isMac) + .setup((i) => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); @@ -269,11 +271,11 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { test('Should return diagnostic if there are other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { const nonMacStandardInterpreter = 'Non Mac Std Interpreter'; settings - .setup(s => s.disableInstallationChecks) + .setup((s) => s.disableInstallationChecks) .returns(() => false) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) + .setup((i) => i.getInterpreters(typemoq.It.isAny())) .returns(() => Promise.resolve([ { path: pythonPath } as any, @@ -283,19 +285,19 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { ) .verifiable(typemoq.Times.once()); platformService - .setup(i => i.isMac) + .setup((i) => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) .returns(() => false) .verifiable(typemoq.Times.atLeastOnce()); interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) @@ -325,12 +327,12 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { const cmdIgnore = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler - .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -341,7 +343,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .returns(() => cmd) .verifiable(typemoq.Times.once()); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -373,12 +375,12 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { const cmdIgnore = ({} as any) as IDiagnosticCommand; let messagePrompt: MessageCommandPrompt | undefined; messageHandler - .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -390,7 +392,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .returns(() => cmdLearn) .verifiable(typemoq.Times.once()); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -402,7 +404,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .returns(() => cmdDownload) .verifiable(typemoq.Times.once()); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith>({ @@ -432,7 +434,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { ); filterService - .setup(f => + .setup((f) => f.shouldIgnoreDiagnostic( typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic) ) @@ -440,10 +442,10 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); commandFactory - .setup(f => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); messageHandler - .setup(f => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); await diagnosticService.handle([diagnostic]); @@ -473,13 +475,15 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { } as any; const serviceContainerObject = createContainer(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); diagnosticService = new (class extends InvalidMacPythonInterpreterService { protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; @@ -493,13 +497,15 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { let invoked = false; const workspaceService = { onDidChangeConfiguration: noop } as any; const serviceContainerObject = createContainer(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); diagnosticService = new (class extends InvalidMacPythonInterpreterService { protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; @@ -514,22 +520,24 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; workspaceService - .setup(w => w.hasWorkspaceFolders) + .setup((w) => w.hasWorkspaceFolders) .returns(() => true) .verifiable(typemoq.Times.once()); workspaceService - .setup(w => w.workspaceFolders) + .setup((w) => w.workspaceFolders) .returns(() => [{ uri: '' }] as any) .verifiable(typemoq.Times.once()); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { constructor( arg1: IServiceContainer, @@ -553,7 +561,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { ); event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); @@ -570,19 +578,21 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { test('Diagnostics are checked with correct interpreter config uri when path changes and only config uri is passed', async () => { const configUri = Uri.parse('i'); const interpreterConfigurationScope = typemoq.Mock.ofType(); - interpreterConfigurationScope.setup(i => i.uri).returns(() => Uri.parse('i')); + interpreterConfigurationScope.setup((i) => i.uri).returns(() => Uri.parse('i')); const workspaceService = typemoq.Mock.ofType(); const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; serviceContainer - .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { constructor( arg1: IServiceContainer, @@ -620,14 +630,16 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { const workspaceService = typemoq.Mock.ofType(); const serviceContainerObject = createContainer(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { constructor( arg1: IServiceContainer, @@ -658,22 +670,24 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; workspaceService - .setup(w => w.hasWorkspaceFolders) + .setup((w) => w.hasWorkspaceFolders) .returns(() => true) .verifiable(typemoq.Times.once()); workspaceService - .setup(w => w.workspaceFolders) + .setup((w) => w.workspaceFolders) .returns(() => [{ uri: '' }] as any) .verifiable(typemoq.Times.once()); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { constructor( arg1: IServiceContainer, @@ -697,7 +711,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { ); event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) .returns(() => true) .verifiable(typemoq.Times.atLeastOnce()); @@ -718,22 +732,24 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { const interpreterPathService = typemoq.Mock.ofType(); interpreterPathService - .setup(d => d.onDidChange(typemoq.It.isAny(), typemoq.It.isAny())) - .callback(cb => (interpreterPathServiceHandler = cb)) + .setup((d) => d.onDidChange(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => (interpreterPathServiceHandler = cb)) .returns(() => { return { dispose: noop }; }); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IInterpreterPathService))) + .setup((s) => s.get(typemoq.It.isValue(IInterpreterPathService))) .returns(() => interpreterPathService.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); const experiments = typemoq.Mock.ofType(); serviceContainer - .setup(s => s.get(typemoq.It.isValue(IExperimentsManager))) + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) .returns(() => experiments.object); - experiments.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); - experiments.setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).returns(() => undefined); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); diagnosticService = new (class extends InvalidMacPythonInterpreterService {})( serviceContainerObject, undefined as any, diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 0f7e7ad55ac8..bfe108f2997c 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -69,16 +69,16 @@ suite('Python Settings', async () => { 'defaultInterpreterPath' ]) { config - .setup(c => c.get(name)) + .setup((c) => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } if (sourceSettings.jediEnabled) { - config.setup(c => c.get('jediPath')).returns(() => sourceSettings.jediPath); + config.setup((c) => c.get('jediPath')).returns(() => sourceSettings.jediPath); } for (const name of ['venvFolders']) { config - .setup(c => c.get(name)) + .setup((c) => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } @@ -86,42 +86,42 @@ suite('Python Settings', async () => { // boolean settings for (const name of ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer']) { config - .setup(c => c.get(name, true)) + .setup((c) => c.get(name, true)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } for (const name of ['disableInstallationCheck', 'globalModuleInstallation']) { config - .setup(c => c.get(name)) + .setup((c) => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } // number settings if (sourceSettings.jediEnabled) { - config.setup(c => c.get('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); + config.setup((c) => c.get('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); } // Language server type settings - config.setup(c => c.get('languageServer')).returns(() => sourceSettings.languageServer); + config.setup((c) => c.get('languageServer')).returns(() => sourceSettings.languageServer); // "any" settings // tslint:disable-next-line:no-any - config.setup(c => c.get('devOptions')).returns(() => sourceSettings.devOptions); + config.setup((c) => c.get('devOptions')).returns(() => sourceSettings.devOptions); // complex settings - config.setup(c => c.get('linting')).returns(() => sourceSettings.linting); - config.setup(c => c.get('analysis')).returns(() => sourceSettings.analysis); - config.setup(c => c.get('sortImports')).returns(() => sourceSettings.sortImports); - config.setup(c => c.get('formatting')).returns(() => sourceSettings.formatting); - config.setup(c => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); + config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); + config.setup((c) => c.get('analysis')).returns(() => sourceSettings.analysis); + config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); + config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); + config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config - .setup(c => c.get('workspaceSymbols')) + .setup((c) => c.get('workspaceSymbols')) .returns(() => sourceSettings.workspaceSymbols); - config.setup(c => c.get('testing')).returns(() => sourceSettings.testing); - config.setup(c => c.get('terminal')).returns(() => sourceSettings.terminal); - config.setup(c => c.get('dataScience')).returns(() => sourceSettings.datascience); - config.setup(c => c.get('experiments')).returns(() => sourceSettings.experiments); + config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); + config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); + config.setup((c) => c.get('dataScience')).returns(() => sourceSettings.datascience); + config.setup((c) => c.get('experiments')).returns(() => sourceSettings.experiments); } function testIfValueIsUpdated(settingName: string, value: any) { @@ -147,21 +147,21 @@ suite('Python Settings', async () => { 'poetryPath', 'insidersChannel', 'defaultInterpreterPath' - ].forEach(async settingName => { + ].forEach(async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); }); }); suite('Boolean settings', async () => { ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( - async settingName => { + async (settingName) => { testIfValueIsUpdated(settingName, true); } ); }); suite('Number settings', async () => { - ['jediMemoryLimit'].forEach(async settingName => { + ['jediMemoryLimit'].forEach(async (settingName) => { testIfValueIsUpdated(settingName, 1001); }); }); @@ -171,7 +171,7 @@ suite('Python Settings', async () => { expected.condaPath = 'spam'; initializeConfig(expected); config - .setup(c => c.get('condaPath')) + .setup((c) => c.get('condaPath')) .returns(() => expected.condaPath) .verifiable(TypeMoq.Times.once()); @@ -186,7 +186,7 @@ suite('Python Settings', async () => { expected.condaPath = path.join('~', 'anaconda3', 'bin', 'conda'); initializeConfig(expected); config - .setup(c => c.get('condaPath')) + .setup((c) => c.get('condaPath')) .returns(() => expected.condaPath) .verifiable(TypeMoq.Times.once()); @@ -206,7 +206,7 @@ suite('Python Settings', async () => { }; initializeConfig(expected); config - .setup(c => c.get('experiments')) + .setup((c) => c.get('experiments')) .returns(() => expected.experiments) .verifiable(TypeMoq.Times.once()); @@ -237,7 +237,7 @@ suite('Python Settings', async () => { expected.formatting.blackPath = 'spam'; initializeConfig(expected); config - .setup(c => c.get('formatting')) + .setup((c) => c.get('formatting')) .returns(() => expected.formatting) .verifiable(TypeMoq.Times.once()); @@ -264,7 +264,7 @@ suite('Python Settings', async () => { expected.formatting.blackPath = 'spam'; initializeConfig(expected); config - .setup(c => c.get('formatting')) + .setup((c) => c.get('formatting')) .returns(() => expected.formatting) .verifiable(TypeMoq.Times.once()); @@ -319,7 +319,7 @@ suite('Python Settings', async () => { }; initializeConfig(expected); config - .setup(c => c.get('experiments')) + .setup((c) => c.get('experiments')) .returns(() => expected.experiments) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/common/configuration/service.unit.test.ts b/src/test/common/configuration/service.unit.test.ts index 547438905190..87fceaad4a7c 100644 --- a/src/test/common/configuration/service.unit.test.ts +++ b/src/test/common/configuration/service.unit.test.ts @@ -29,7 +29,7 @@ suite('Configuration Service', () => { workspaceService = TypeMoq.Mock.ofType(); interpreterSecurityService = TypeMoq.Mock.ofType(); workspaceService - .setup(w => w.getWorkspaceFolder(resource)) + .setup((w) => w.getWorkspaceFolder(resource)) .returns(() => ({ uri: resource, index: 0, @@ -39,18 +39,18 @@ suite('Configuration Service', () => { serviceContainer = TypeMoq.Mock.ofType(); experimentsManager = TypeMoq.Mock.ofType(); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined); - serviceContainer.setup(s => s.get(IWorkspaceService)).returns(() => workspaceService.object); - serviceContainer.setup(s => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); - serviceContainer.setup(s => s.get(IExperimentsManager)).returns(() => experimentsManager.object); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + serviceContainer.setup((s) => s.get(IExperimentsManager)).returns(() => experimentsManager.object); configService = new ConfigurationService(serviceContainer.object); }); function setupConfigProvider(): TypeMoq.IMock { const workspaceConfig = TypeMoq.Mock.ofType(); workspaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) .returns(() => workspaceConfig.object); return workspaceConfig; } @@ -58,11 +58,11 @@ suite('Configuration Service', () => { test('Fetching settings goes as expected', () => { const interpreterAutoSelectionProxyService = TypeMoq.Mock.ofType(); serviceContainer - .setup(s => s.get(IInterpreterSecurityService)) + .setup((s) => s.get(IInterpreterSecurityService)) .returns(() => interpreterSecurityService.object) .verifiable(TypeMoq.Times.once()); serviceContainer - .setup(s => s.get(IInterpreterAutoSeletionProxyService)) + .setup((s) => s.get(IInterpreterAutoSeletionProxyService)) .returns(() => interpreterAutoSelectionProxyService.object) .verifiable(TypeMoq.Times.once()); const settings = configService.getSettings(); @@ -70,12 +70,12 @@ suite('Configuration Service', () => { }); test('Do not update global settings if global value is already equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); // tslint:disable-next-line: no-any - workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); workspaceConfig - .setup(w => w.update('setting', 'globalValue', ConfigurationTarget.Global)) + .setup((w) => w.update('setting', 'globalValue', ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -85,12 +85,12 @@ suite('Configuration Service', () => { }); test('Update global settings if global value is not equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); // tslint:disable-next-line: no-any - workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); workspaceConfig - .setup(w => w.update('setting', 'newGlobalValue', ConfigurationTarget.Global)) + .setup((w) => w.update('setting', 'newGlobalValue', ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -100,12 +100,12 @@ suite('Configuration Service', () => { }); test('Do not update workspace settings if workspace value is already equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); // tslint:disable-next-line: no-any - workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); workspaceConfig - .setup(w => w.update('setting', 'workspaceValue', ConfigurationTarget.Workspace)) + .setup((w) => w.update('setting', 'workspaceValue', ConfigurationTarget.Workspace)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -115,12 +115,12 @@ suite('Configuration Service', () => { }); test('Update workspace settings if workspace value is not equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); // tslint:disable-next-line: no-any - workspaceConfig.setup(w => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); workspaceConfig - .setup(w => w.update('setting', 'newWorkspaceValue', ConfigurationTarget.Workspace)) + .setup((w) => w.update('setting', 'newWorkspaceValue', ConfigurationTarget.Workspace)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -130,14 +130,14 @@ suite('Configuration Service', () => { }); test('Do not update workspace folder settings if workspace folder value is already equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); workspaceConfig - .setup(w => w.inspect('setting')) + .setup((w) => w.inspect('setting')) // tslint:disable-next-line: no-any .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); workspaceConfig - .setup(w => w.update('setting', 'workspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .setup((w) => w.update('setting', 'workspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -152,14 +152,14 @@ suite('Configuration Service', () => { }); test('Update workspace folder settings if workspace folder value is not equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); workspaceConfig - .setup(w => w.inspect('setting')) + .setup((w) => w.inspect('setting')) // tslint:disable-next-line: no-any .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); workspaceConfig - .setup(w => w.update('setting', 'newWorkspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .setup((w) => w.update('setting', 'newWorkspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -174,13 +174,13 @@ suite('Configuration Service', () => { }); test('If in Deprecate PythonPath experiment & setting to update is `python.pythonPath`, update settings using new API if stored value is not equal to the new value', async () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); interpreterPathService - .setup(w => w.inspect(resource)) + .setup((w) => w.inspect(resource)) // tslint:disable-next-line: no-any .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); interpreterPathService - .setup(w => w.update(resource, ConfigurationTarget.WorkspaceFolder, 'newWorkspaceFolderValue')) + .setup((w) => w.update(resource, ConfigurationTarget.WorkspaceFolder, 'newWorkspaceFolderValue')) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/common/experiments.unit.test.ts b/src/test/common/experiments.unit.test.ts index 6d412d044b7b..b36425f366f6 100644 --- a/src/test/common/experiments.unit.test.ts +++ b/src/test/common/experiments.unit.test.ts @@ -66,8 +66,8 @@ suite('A/B experiments', () => { experiments = TypeMoq.Mock.ofType(); const settings = mock(PythonSettings); when(settings.experiments).thenReturn(experiments.object); - experiments.setup(e => e.optInto).returns(() => []); - experiments.setup(e => e.optOutFrom).returns(() => []); + experiments.setup((e) => e.optInto).returns(() => []); + experiments.setup((e) => e.optOutFrom).returns(() => []); when(configurationService.getSettings(undefined)).thenReturn(instance(settings)); fs = mock(FileSystem); when( @@ -114,7 +114,7 @@ suite('A/B experiments', () => { test('Initializing experiments does not download experiments if storage is valid and contains experiments', async () => { isDownloadedStorageValid - .setup(n => n.value) + .setup((n) => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); @@ -126,15 +126,15 @@ suite('A/B experiments', () => { test('If storage has expired, initializing experiments downloads the experiments, but does not store them if they are invalid or incomplete', async () => { const abExperiments = [{ name: 'experiment1', salt: 'salt', max: 100 }]; isDownloadedStorageValid - .setup(n => n.value) + .setup((n) => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup(n => n.updateValue(true)) + .setup((n) => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); downloadedExperimentsStorage - .setup(n => n.updateValue(abExperiments)) + .setup((n) => n.updateValue(abExperiments)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -146,15 +146,15 @@ suite('A/B experiments', () => { test('If storage has expired, initializing experiments downloads the experiments, and stores them if they are valid', async () => { isDownloadedStorageValid - .setup(n => n.value) + .setup((n) => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup(n => n.updateValue(true)) + .setup((n) => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -165,15 +165,15 @@ suite('A/B experiments', () => { test('If downloading experiments fails with error, the storage is left as it is', async () => { isDownloadedStorageValid - .setup(n => n.value) + .setup((n) => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup(n => n.updateValue(true)) + .setup((n) => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); downloadedExperimentsStorage - .setup(n => n.updateValue(anything())) + .setup((n) => n.updateValue(anything())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -191,7 +191,7 @@ suite('A/B experiments', () => { const initializeInBackground = sinon.stub(ExperimentsManager.prototype, 'initializeInBackground'); initializeInBackground.callsFake(() => Promise.resolve()); experiments - .setup(e => e.enabled) + .setup((e) => e.enabled) .returns(() => enabled) .verifiable(TypeMoq.Times.atLeastOnce()); @@ -304,15 +304,15 @@ suite('A/B experiments', () => { test('Ensure experiment storage is updated to contain the latest downloaded experiments', async () => { downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) .verifiable(TypeMoq.Times.atLeastOnce()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -339,20 +339,20 @@ suite('A/B experiments', () => { ); downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -380,16 +380,16 @@ suite('A/B experiments', () => { ); downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); @@ -416,11 +416,11 @@ suite('A/B experiments', () => { instance(configurationService) ); downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -435,11 +435,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve(fileContent); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -467,11 +467,11 @@ suite('A/B experiments', () => { instance(configurationService) ); downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -486,11 +486,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve(fileContent); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -523,11 +523,11 @@ suite('A/B experiments', () => { }); test('If checking the existence of config file fails', async () => { downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -536,11 +536,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve('fileContent'); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -554,11 +554,11 @@ suite('A/B experiments', () => { test('If reading config file fails', async () => { downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -567,11 +567,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenThrow(error); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -585,11 +585,11 @@ suite('A/B experiments', () => { test('If config file does not exist', async () => { downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -597,11 +597,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve('fileContent'); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -615,11 +615,11 @@ suite('A/B experiments', () => { test('If parsing file or updating storage fails', async () => { downloadedExperimentsStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) + .setup((n) => n.updateValue(undefined)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); @@ -634,11 +634,11 @@ suite('A/B experiments', () => { when(fs.readFile(anything())).thenResolve(fileContent); experimentStorage - .setup(n => n.value) + .setup((n) => n.value) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.reject(error)) .verifiable(TypeMoq.Times.once()); @@ -674,7 +674,7 @@ suite('A/B experiments', () => { } ]; - testsForInExperiment.forEach(testParams => { + testsForInExperiment.forEach((testParams) => { test(testParams.testName, async () => { expManager.userExperiments = testParams.userExperiments; expect(expManager.inExperiment(testParams.experimentName)).to.equal( @@ -716,7 +716,7 @@ suite('A/B experiments', () => { ]; suite('Function IsUserInRange()', () => { - testsForIsUserInRange.forEach(testParams => { + testsForIsUserInRange.forEach((testParams) => { test(testParams.testName, async () => { when(appEnvironment.machineId).thenReturn('101'); if (testParams.machineIdError) { @@ -864,9 +864,9 @@ suite('A/B experiments', () => { ]; suite('Function populateUserExperiments', async () => { - testsForPopulateUserExperiments.forEach(testParams => { + testsForPopulateUserExperiments.forEach((testParams) => { test(testParams.testName, async () => { - experimentStorage.setup(n => n.value).returns(() => testParams.experimentStorageValue); + experimentStorage.setup((n) => n.value).returns(() => testParams.experimentStorageValue); when(appEnvironment.machineId).thenReturn('101'); if (testParams.hash) { when(crypto.createHash(anything(), 'number', anything())).thenReturn(testParams.hash); @@ -932,7 +932,7 @@ suite('A/B experiments', () => { ]; suite('Function areExperimentsValid()', () => { - testsForAreExperimentsValid.forEach(testParams => { + testsForAreExperimentsValid.forEach((testParams) => { test(testParams.testName, () => { expect(expManager.areExperimentsValid(testParams.experiments as any)).to.equal( testParams.expectedResult @@ -1013,15 +1013,15 @@ suite('A/B experiments', () => { test('If storage as parameter is passed in as argument to function downloadAndStoreExperiments(), download experiments into that storage', async () => { downloadedExperimentsStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) + .setup((n) => n.updateValue(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); isDownloadedStorageValid - .setup(n => n.updateValue(true)) + .setup((n) => n.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); when(httpClient.getJSON(configUri, false)).thenResolve([ diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts index a0e6d5595dde..091ffdbfb4d0 100644 --- a/src/test/common/interpreterPathService.unit.test.ts +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -29,23 +29,23 @@ suite('Interpreter Path Service', async () => { const event = TypeMoq.Mock.ofType>(); workspaceService = TypeMoq.Mock.ofType(); workspaceService - .setup(w => w.getWorkspaceFolder(resource)) + .setup((w) => w.getWorkspaceFolder(resource)) .returns(() => ({ uri: resource, name: 'Workspacefolder', index: 0 })); - workspaceService.setup(w => w.getWorkspaceFolder(resourceOutsideOfWorkspace)).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolder(resourceOutsideOfWorkspace)).returns(() => undefined); persistentStateFactory = TypeMoq.Mock.ofType(); - workspaceService.setup(w => w.onDidChangeConfiguration).returns(() => event.object); + workspaceService.setup((w) => w.onDidChangeConfiguration).returns(() => event.object); interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); }); test('Global settings are correctly updated', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); workspaceConfig - .setup(w => w.update('defaultInterpreterPath', interpreterPath, true)) + .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -57,14 +57,14 @@ suite('Interpreter Path Service', async () => { test('Workspace settings are correctly updated if a folder is directly opened', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); - workspaceService.setup(w => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.once()); persistentState - .setup(p => p.updateValue(interpreterPath)) + .setup((p) => p.updateValue(interpreterPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -77,17 +77,17 @@ suite('Interpreter Path Service', async () => { test('Ensure the correct event is fired if Workspace settings are updated', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); - workspaceService.setup(w => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object); - persistentState.setup(p => p.updateValue(interpreterPath)).returns(() => Promise.resolve()); + persistentState.setup((p) => p.updateValue(interpreterPath)).returns(() => Promise.resolve()); const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; _didChangeInterpreterEmitter - .setup(emitter => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.Workspace })) + .setup((emitter) => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.Workspace })) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); @@ -100,14 +100,14 @@ suite('Interpreter Path Service', async () => { const workspaceFileUri = Uri.parse('path/to/workspaceFile'); const expectedSettingKey = `WORKSPACE_INTERPRETER_PATH_${workspaceFileUri.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); - workspaceService.setup(w => w.workspaceFile).returns(() => workspaceFileUri); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.once()); persistentState - .setup(p => p.updateValue(interpreterPath)) + .setup((p) => p.updateValue(interpreterPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -120,13 +120,13 @@ suite('Interpreter Path Service', async () => { test('Workspace folder settings are correctly updated in case of multiroot folders', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.once()); persistentState - .setup(p => p.updateValue(interpreterPath)) + .setup((p) => p.updateValue(interpreterPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -139,20 +139,20 @@ suite('Interpreter Path Service', async () => { test('Ensure the correct event is fired if Workspace folder settings are updated', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.once()); persistentState - .setup(p => p.updateValue(interpreterPath)) + .setup((p) => p.updateValue(interpreterPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; _didChangeInterpreterEmitter - .setup(emitter => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.WorkspaceFolder })) + .setup((emitter) => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.WorkspaceFolder })) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); @@ -164,13 +164,13 @@ suite('Interpreter Path Service', async () => { test('Updating workspace settings throws error if no workspace is opened', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.never()); persistentState - .setup(p => p.updateValue(interpreterPath)) + .setup((p) => p.updateValue(interpreterPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -188,13 +188,13 @@ suite('Interpreter Path Service', async () => { test('Updating workspace folder settings throws error if no workspace is opened', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.never()); persistentState - .setup(p => p.updateValue(interpreterPath)) + .setup((p) => p.updateValue(interpreterPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -211,9 +211,9 @@ suite('Interpreter Path Service', async () => { test('Inspecting settings returns as expected if no workspace is opened', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); workspaceConfig - .setup(w => w.inspect('defaultInterpreterPath')) + .setup((w) => w.inspect('defaultInterpreterPath')) .returns( () => ({ @@ -222,9 +222,9 @@ suite('Interpreter Path Service', async () => { } as any) ); const persistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => persistentState.object) .verifiable(TypeMoq.Times.never()); @@ -242,11 +242,11 @@ suite('Interpreter Path Service', async () => { const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const workspaceConfig = TypeMoq.Mock.ofType(); // No workspace file is present if a folder is directly opened - workspaceService.setup(w => w.workspaceFile).returns(() => undefined); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); - workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); workspaceConfig - .setup(w => w.inspect('defaultInterpreterPath')) + .setup((w) => w.inspect('defaultInterpreterPath')) .returns( () => ({ @@ -255,14 +255,14 @@ suite('Interpreter Path Service', async () => { } as any) ); const workspaceFolderPersistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => workspaceFolderPersistentState.object); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedSettingKey, undefined)) .returns(() => workspaceFolderPersistentState.object); - workspaceFolderPersistentState.setup(p => p.value).returns(() => 'workspaceFolderValue'); + workspaceFolderPersistentState.setup((p) => p.value).returns(() => 'workspaceFolderValue'); const settings = interpreterPathService.inspect(resource); @@ -279,11 +279,11 @@ suite('Interpreter Path Service', async () => { const expectedWorkspaceFolderSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; const workspaceConfig = TypeMoq.Mock.ofType(); // A workspace file is present in case of multiroot workspace folders - workspaceService.setup(w => w.workspaceFile).returns(() => workspaceFileUri); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); - workspaceService.setup(w => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); workspaceConfig - .setup(w => w.inspect('defaultInterpreterPath')) + .setup((w) => w.inspect('defaultInterpreterPath')) .returns( () => ({ @@ -293,15 +293,17 @@ suite('Interpreter Path Service', async () => { ); const workspaceFolderPersistentState = TypeMoq.Mock.ofType>(); const workspacePersistentState = TypeMoq.Mock.ofType>(); - workspaceService.setup(w => w.workspaceFolders).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedWorkspaceFolderSettingKey, undefined)) + .setup((p) => + p.createGlobalPersistentState(expectedWorkspaceFolderSettingKey, undefined) + ) .returns(() => workspaceFolderPersistentState.object); persistentStateFactory - .setup(p => p.createGlobalPersistentState(expectedWorkspaceSettingKey, undefined)) + .setup((p) => p.createGlobalPersistentState(expectedWorkspaceSettingKey, undefined)) .returns(() => workspacePersistentState.object); - workspaceFolderPersistentState.setup(p => p.value).returns(() => 'workspaceFolderValue'); - workspacePersistentState.setup(p => p.value).returns(() => 'workspaceValue'); + workspaceFolderPersistentState.setup((p) => p.value).returns(() => 'workspaceFolderValue'); + workspacePersistentState.setup((p) => p.value).returns(() => 'workspaceValue'); const settings = interpreterPathService.inspect(resource); @@ -366,12 +368,12 @@ suite('Interpreter Path Service', async () => { const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); const event = TypeMoq.Mock.ofType(); event - .setup(e => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .setup((e) => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) .returns(() => true) .verifiable(TypeMoq.Times.once()); interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; _didChangeInterpreterEmitter - .setup(emitter => emitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global })) + .setup((emitter) => emitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global })) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); await interpreterPathService.onDidChangeConfiguration(event.object); @@ -383,12 +385,12 @@ suite('Interpreter Path Service', async () => { const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType>(); const event = TypeMoq.Mock.ofType(); event - .setup(e => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .setup((e) => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; _didChangeInterpreterEmitter - .setup(emitter => emitter.fire(TypeMoq.It.isAny())) + .setup((emitter) => emitter.fire(TypeMoq.It.isAny())) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await interpreterPathService.onDidChangeConfiguration(event.object); @@ -399,7 +401,7 @@ suite('Interpreter Path Service', async () => { test('Ensure on interpreter change captures the fired event with the correct arguments', async () => { const deferred = createDeferred(); const interpreterConfigurationScope = { uri: undefined, configTarget: ConfigurationTarget.Global }; - interpreterPathService.onDidChange(i => { + interpreterPathService.onDidChange((i) => { expect(i).to.equal(interpreterConfigurationScope); deferred.resolve(true); }); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 4c0c20307aee..261eb8794d87 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -155,7 +155,7 @@ const info: PythonInterpreter = { }; suite('Module Installer', () => { - [undefined, Uri.file(__filename)].forEach(resource => { + [undefined, Uri.file(__filename)].forEach((resource) => { let ioc: UnitTestIocContainer; let mockTerminalService: TypeMoq.IMock; let condaService: TypeMoq.IMock; @@ -193,13 +193,13 @@ suite('Module Installer', () => { mockTerminalService = TypeMoq.Mock.ofType(); mockTerminalFactory = TypeMoq.Mock.ofType(); mockTerminalFactory - .setup(t => t.getTerminalService(TypeMoq.It.isValue(resource))) + .setup((t) => t.getTerminalService(TypeMoq.It.isValue(resource))) .returns(() => mockTerminalService.object) .verifiable(TypeMoq.Times.atLeastOnce()); // If resource is provided, then ensure we do not invoke without the resource. mockTerminalFactory - .setup(t => t.getTerminalService(TypeMoq.It.isAny())) - .callback(passedInResource => expect(passedInResource).to.be.equal(resource)) + .setup((t) => t.getTerminalService(TypeMoq.It.isAny())) + .callback((passedInResource) => expect(passedInResource).to.be.equal(resource)) .returns(() => mockTerminalService.object); ioc.serviceManager.addSingletonInstance( ITerminalServiceFactory, @@ -363,7 +363,9 @@ suite('Module Installer', () => { new MockModuleInstaller('mock', true) ); const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + mockInterpreterLocator + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([])); ioc.serviceManager.addSingletonInstance( IInterpreterLocatorService, mockInterpreterLocator.object, @@ -390,15 +392,15 @@ suite('Module Installer', () => { const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); - const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; + const condaInstaller = moduleInstallers.find((item) => item.displayName === 'Conda')!; expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); - const mockInstaller = moduleInstallers.find(item => item.displayName === 'mock')!; + const mockInstaller = moduleInstallers.find((item) => item.displayName === 'mock')!; expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); await expect(mockInstaller.isSupported()).to.eventually.equal(true, 'mock is not supported'); }); @@ -411,7 +413,7 @@ suite('Module Installer', () => { const pythonPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup(p => p.getInterpreters(TypeMoq.It.isAny())) + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([ { @@ -452,7 +454,7 @@ suite('Module Installer', () => { const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); }); @@ -461,16 +463,16 @@ suite('Module Installer', () => { const configService = TypeMoq.Mock.ofType(); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); const settings = TypeMoq.Mock.ofType(); const pythonPath = 'pythonABC'; - settings.setup(s => s.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); condaService - .setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(true)); const condaInstaller = new CondaInstaller(serviceContainer.object); @@ -481,16 +483,16 @@ suite('Module Installer', () => { const configService = TypeMoq.Mock.ofType(); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); const settings = TypeMoq.Mock.ofType(); const pythonPath = 'pythonABC'; - settings.setup(s => s.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); condaService - .setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(false)); const condaInstaller = new CondaInstaller(serviceContainer.object); @@ -502,7 +504,7 @@ suite('Module Installer', () => { const interpreterPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup(p => p.getInterpreters(TypeMoq.It.isAny())) + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Unknown }])); ioc.serviceManager.addSingletonInstance( IInterpreterLocatorService, @@ -521,25 +523,25 @@ suite('Module Installer', () => { path: PYTHON_PATH }; interpreterService - .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) + .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(interpreter)); const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); let argsSent: string[] = []; mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((_cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) // tslint:disable-next-line:no-any .returns(() => Promise.resolve({ type: InterpreterType.Unknown } as any)); @@ -556,7 +558,7 @@ suite('Module Installer', () => { const interpreterPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup(p => p.getInterpreters(TypeMoq.It.isAny())) + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Conda }])); ioc.serviceManager.addSingletonInstance( IInterpreterLocatorService, @@ -572,13 +574,13 @@ suite('Module Installer', () => { const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); let argsSent: string[] = []; mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((_cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); @@ -596,7 +598,7 @@ suite('Module Installer', () => { test(`Validate pipenv install arguments ${resourceTestNameSuffix}`, async () => { const mockInterpreterLocator = TypeMoq.Mock.ofType(); mockInterpreterLocator - .setup(p => p.getInterpreters(TypeMoq.It.isAny())) + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) .returns(() => Promise.resolve([{ ...info, path: 'interpreterPath', type: InterpreterType.VirtualEnv }]) ); @@ -608,14 +610,14 @@ suite('Module Installer', () => { const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'pipenv')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'pipenv')!; expect(pipInstaller).not.to.be.an('undefined', 'pipenv installer not found'); let argsSent: string[] = []; let command: string | undefined; mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((cmd: string, args: string[]) => { argsSent = args; command = cmd; diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index 14079dc24529..62bab1a4a092 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -162,19 +162,19 @@ suite('Common - Service Registry', () => { [IExtensionChannelRule, ExtensionInsidersOffChannelRule, ExtensionChannel.off], [IExtensionChannelRule, ExtensionInsidersDailyChannelRule, ExtensionChannel.daily], [IExtensionChannelRule, ExtensionInsidersWeeklyChannelRule, ExtensionChannel.weekly] - ].forEach(mapping => { + ].forEach((mapping) => { if (mapping.length === 2) { serviceManager - .setup(s => + .setup((s) => s.addSingleton( typemoq.It.isValue(mapping[0] as any), - typemoq.It.is(value => mapping[1] === value) + typemoq.It.is((value) => mapping[1] === value) ) ) .verifiable(typemoq.Times.atLeastOnce()); } else { serviceManager - .setup(s => + .setup((s) => s.addSingleton( typemoq.It.isValue(mapping[0] as any), typemoq.It.isAny(), diff --git a/src/test/configuration/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector.unit.test.ts index 2bc3ad18b467..035c580f5f10 100644 --- a/src/test/configuration/interpreterSelector.unit.test.ts +++ b/src/test/configuration/interpreterSelector.unit.test.ts @@ -109,11 +109,11 @@ suite('Interpreters - selector', () => { workspace = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); fileSystem - .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) .returns((a: string, b: string) => a === b); - configurationService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - comparer.setup(c => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); + comparer.setup((c) => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); selector = new TestInterpreterSelector( interpreterService.object, workspace.object, @@ -128,7 +128,7 @@ suite('Interpreters - selector', () => { ); }); - [true, false].forEach(isWindows => { + [true, false].forEach((isWindows) => { test(`Suggestions (${isWindows ? 'Windows' : 'Non-Windows'})`, async () => { const interpreterSelector = new InterpreterSelector( interpreterService.object, @@ -150,12 +150,12 @@ suite('Interpreters - selector', () => { { displayName: '2 (virtualenv)', path: 'c:/path2/path2', type: InterpreterType.VirtualEnv }, { displayName: '3', path: 'c:/path2/path2', type: InterpreterType.Unknown }, { displayName: '4', path: 'c:/path4/path4', type: InterpreterType.Conda } - ].map(item => { + ].map((item) => { return { ...info, ...item }; }); interpreterService - .setup(x => x.getInterpreters(TypeMoq.It.isAny())) - .returns(() => new Promise(resolve => resolve(initial))); + .setup((x) => x.getInterpreters(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve) => resolve(initial))); const actual = await interpreterSelector.getSuggestions(undefined); @@ -187,7 +187,7 @@ suite('Interpreters - selector', () => { // tslint:disable-next-line: max-func-body-length suite('Test method setInterpreter()', async () => { test('Update Global settings when there are no workspaces', async () => { - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); const selectedItem: IInterpreterQuickPickItem = { description: '', detail: '', @@ -197,15 +197,15 @@ suite('Interpreters - selector', () => { interpreter: {} as any }; - workspace.setup(w => w.workspaceFolders).returns(() => undefined); + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); selector.getSuggestions = () => Promise.resolve([]); appShell - .setup(s => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(selectedItem)) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.Global), @@ -223,8 +223,8 @@ suite('Interpreters - selector', () => { pythonPathUpdater.verifyAll(); }); test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); - workspace.setup(w => w.workspaceFile).returns(() => undefined); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + workspace.setup((w) => w.workspaceFile).returns(() => undefined); const selectedItem: IInterpreterQuickPickItem = { description: '', detail: '', @@ -235,15 +235,15 @@ suite('Interpreters - selector', () => { }; const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder]); selector.getSuggestions = () => Promise.resolve([]); appShell - .setup(s => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(selectedItem)) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), @@ -261,7 +261,7 @@ suite('Interpreters - selector', () => { pythonPathUpdater.verifyAll(); }); test('Update selected workspace folder settings when there is more than one workspace folder', async () => { - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); const selectedItem: IInterpreterQuickPickItem = { description: '', detail: '', @@ -271,7 +271,7 @@ suite('Interpreters - selector', () => { interpreter: {} as any }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); const expectedItems = [ { label: 'one', @@ -290,11 +290,11 @@ suite('Interpreters - selector', () => { ]; selector.getSuggestions = () => Promise.resolve([]); appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue([]), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue([]), TypeMoq.It.isAny())) .returns(() => Promise.resolve(selectedItem)) .verifiable(TypeMoq.Times.once()); appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ label: 'two', @@ -304,7 +304,7 @@ suite('Interpreters - selector', () => { ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), @@ -322,7 +322,7 @@ suite('Interpreters - selector', () => { pythonPathUpdater.verifyAll(); }); test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); const selectedItem: IInterpreterQuickPickItem = { description: '', detail: '', @@ -332,7 +332,7 @@ suite('Interpreters - selector', () => { interpreter: {} as any }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); const expectedItems = [ { label: 'one', @@ -351,13 +351,13 @@ suite('Interpreters - selector', () => { ]; selector.getSuggestions = () => Promise.resolve([selectedItem]); appShell - .setup(s => + .setup((s) => s.showQuickPick(TypeMoq.It.isValue([selectedItem]), TypeMoq.It.isAny()) ) .returns(() => Promise.resolve(selectedItem)) .verifiable(TypeMoq.Times.once()); appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ label: Interpreters.entireWorkspace(), @@ -366,7 +366,7 @@ suite('Interpreters - selector', () => { ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(selectedItem.path), TypeMoq.It.isValue(ConfigurationTarget.Workspace), @@ -393,11 +393,11 @@ suite('Interpreters - selector', () => { interpreter: {} as any }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); selector.getSuggestions = () => Promise.resolve([]); appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue([]), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue([]), TypeMoq.It.isAny())) .returns(() => Promise.resolve(selectedItem)) .verifiable(TypeMoq.Times.never()); @@ -419,11 +419,11 @@ suite('Interpreters - selector', () => { ]; appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) .returns(() => Promise.resolve()) @@ -440,10 +440,10 @@ suite('Interpreters - selector', () => { // tslint:disable-next-line: max-func-body-length suite('Test method resetInterpreter()', async () => { test('Update Global settings when there are no workspaces', async () => { - workspace.setup(w => w.workspaceFolders).returns(() => undefined); + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.Global), @@ -462,12 +462,12 @@ suite('Interpreters - selector', () => { }); test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder]); - workspace.setup(w => w.workspaceFile).returns(() => undefined); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder]); + workspace.setup((w) => w.workspaceFile).returns(() => undefined); selector.getSuggestions = () => Promise.resolve([]); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), @@ -485,7 +485,7 @@ suite('Interpreters - selector', () => { pythonPathUpdater.verifyAll(); }); test('Update selected workspace folder settings when there is more than one workspace folder', async () => { - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); const expectedItems = [ { label: 'one', @@ -503,7 +503,7 @@ suite('Interpreters - selector', () => { } ]; appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ label: 'two', @@ -513,7 +513,7 @@ suite('Interpreters - selector', () => { ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), @@ -531,7 +531,7 @@ suite('Interpreters - selector', () => { pythonPathUpdater.verifyAll(); }); test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); const expectedItems = [ { label: 'one', @@ -549,7 +549,7 @@ suite('Interpreters - selector', () => { } ]; appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ label: Interpreters.entireWorkspace(), @@ -558,7 +558,7 @@ suite('Interpreters - selector', () => { ) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath( TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(ConfigurationTarget.Workspace), @@ -576,7 +576,7 @@ suite('Interpreters - selector', () => { pythonPathUpdater.verifyAll(); }); test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); const expectedItems = [ { @@ -596,11 +596,11 @@ suite('Interpreters - selector', () => { ]; appShell - .setup(s => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); pythonPathUpdater - .setup(p => + .setup((p) => p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) .returns(() => Promise.resolve()) diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 39d101a136ce..3b09e4285183 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -474,7 +474,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const services = require('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices') as any; if (services.StaticServices) { const keys = Object.keys(services.StaticServices); - keys.forEach(k => { + keys.forEach((k) => { const service = services.StaticServices[k] as any; if (service && service._value && service._value.dispose) { if (typeof service._value.dispose === 'function') { @@ -608,7 +608,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { ServerPreload ); const mockExtensionContext = TypeMoq.Mock.ofType(); - mockExtensionContext.setup(m => m.globalStoragePath).returns(() => os.tmpdir()); + mockExtensionContext.setup((m) => m.globalStoragePath).returns(() => os.tmpdir()); this.serviceManager.addSingletonInstance(IExtensionContext, mockExtensionContext.object); const mockServerSelector = mock(JupyterServerSelector); @@ -818,10 +818,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const configurationService = TypeMoq.Mock.ofType(); this.datascience = TypeMoq.Mock.ofType(); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(this.getSettings.bind(this)); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(this.getSettings.bind(this)); const startTime = Date.now(); - this.datascience.setup(d => d.activationStartTime).returns(() => startTime); + this.datascience.setup((d) => d.activationStartTime).returns(() => startTime); this.serviceManager.addSingleton( IEnvironmentVariablesProvider, @@ -909,7 +909,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } const interpreterDisplay = TypeMoq.Mock.ofType(); - interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + interpreterDisplay.setup((i) => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); // Create our jupyter mock if necessary if (this.shouldMockJupyter) { @@ -1025,9 +1025,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Don't use conda at all when mocking const condaService = TypeMoq.Mock.ofType(); this.serviceManager.addSingletonInstance(ICondaService, condaService.object); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(false)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); + condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(false)); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + condaService.setup((c) => c.condaEnvironmentsFile).returns(() => undefined); this.serviceManager.addSingleton( IVirtualEnvironmentsSearchPathProvider, @@ -1083,23 +1083,23 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } }; - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); appShell - .setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve('')); appShell - .setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); appShell - .setup(a => + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) .returns((_a1: string, a2: string, _a3: string, _a4: string) => Promise.resolve(a2)); appShell - .setup(a => a.showSaveDialog(TypeMoq.It.isAny())) + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) .returns(() => Promise.resolve(Uri.file('test.ipynb'))); - appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - appShell.setup(a => a.showInputBox(TypeMoq.It.isAny())).returns(() => Promise.resolve('Python')); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + appShell.setup((a) => a.showInputBox(TypeMoq.It.isAny())).returns(() => Promise.resolve('Python')); const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); @@ -1120,7 +1120,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { IExtensionSingleActivationService ); - await Promise.all(activationServices.map(a => a.activate())); + await Promise.all(activationServices.map((a) => a.activate())); // Then force our interpreter to be one that supports jupyter (unless in a mock state when we don't have to) if (!this.mockJupyter) { @@ -1208,9 +1208,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return DataScienceIocContainer.jupyterInterpreters; } const list = await this.get(IInterpreterService).getInterpreters(undefined); - const promises = list.map(f => this.hasJupyter(f).then(b => (b ? f : undefined))); + const promises = list.map((f) => this.hasJupyter(f).then((b) => (b ? f : undefined))); const resolved = await Promise.all(promises); - DataScienceIocContainer.jupyterInterpreters = resolved.filter(r => r) as PythonInterpreter[]; + DataScienceIocContainer.jupyterInterpreters = resolved.filter((r) => r) as PythonInterpreter[]; return DataScienceIocContainer.jupyterInterpreters; } @@ -1221,7 +1221,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } public addResourceToFolder(resource: Uri, folderPath: string) { - let folder = this.workspaceFolders.find(f => f.uri.fsPath === folderPath); + let folder = this.workspaceFolders.find((f) => f.uri.fsPath === folderPath); if (!folder) { folder = this.addWorkspaceFolder(folderPath); } @@ -1265,7 +1265,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } if (this.extraListeners.length) { - this.extraListeners.forEach(e => e(msg.type, msg.payload)); + this.extraListeners.forEach((e) => e(msg.type, msg.payload)); } if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { this.wrapperCreatedPromise.resolve(); @@ -1290,7 +1290,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { private createWebPanel(): IWebPanel { const webPanel = mock(WebPanel); - when(webPanel.postMessage(anything())).thenCall(m => { + when(webPanel.postMessage(anything())).thenCall((m) => { // tslint:disable-next-line: no-require-imports const reactHelpers = require('./reactHelpers') as typeof import('./reactHelpers'); const message = reactHelpers.createMessageEvent(m); @@ -1316,7 +1316,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // This needs to be async because we are being called in the ctor of the webpanel. It can't // handle some messages during the ctor. setTimeout(() => { - this.missedMessages.forEach(m => + this.missedMessages.forEach((m) => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop() ); }, 0); @@ -1435,7 +1435,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { private getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { if (uri) { - return this.workspaceFolders.find(w => w.ownedResources.has(uri.toString())); + return this.workspaceFolders.find((w) => w.ownedResources.has(uri.toString())); } return undefined; } diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts index cc5e4660b6f5..9db6976bdf3b 100644 --- a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts @@ -33,12 +33,12 @@ suite('Interpreter Evaluation', () => { unsafeInterpreterPromptEnabled = Typemoq.Mock.ofType>(); areInterpretersInWorkspaceSafe = Typemoq.Mock.ofType>(); interpreterSecurityStorage - .setup(i => i.hasUserApprovedWorkspaceInterpreters(resource)) + .setup((i) => i.hasUserApprovedWorkspaceInterpreters(resource)) .returns(() => areInterpretersInWorkspaceSafe.object); interpreterSecurityStorage - .setup(i => i.unsafeInterpreterPromptEnabled) + .setup((i) => i.unsafeInterpreterPromptEnabled) .returns(() => unsafeInterpreterPromptEnabled.object); - interpreterSecurityStorage.setup(i => i.storeKeyForWorkspace(resource)).returns(() => Promise.resolve()); + interpreterSecurityStorage.setup((i) => i.storeKeyForWorkspace(resource)).returns(() => Promise.resolve()); interpreterEvaluation = new InterpreterEvaluation( applicationShell.object, browserService.object, @@ -51,7 +51,7 @@ suite('Interpreter Evaluation', () => { test('If no workspaces are opened, return true', async () => { // tslint:disable-next-line: no-any const interpreter = { path: 'interpreterPath' } as any; - interpreterHelper.setup(i => i.getActiveWorkspaceUri(resource)).returns(() => undefined); + interpreterHelper.setup((i) => i.getActiveWorkspaceUri(resource)).returns(() => undefined); const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); expect(isSafe).to.equal(true, 'Should be true'); }); @@ -60,7 +60,7 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any const interpreter = { path: 'interpreterPath' } as any; interpreterHelper - .setup(i => i.getActiveWorkspaceUri(resource)) + .setup((i) => i.getActiveWorkspaceUri(resource)) .returns( () => ({ @@ -78,7 +78,7 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any const interpreter = { path: 'interpreterPath' } as any; interpreterHelper - .setup(i => i.getActiveWorkspaceUri(resource)) + .setup((i) => i.getActiveWorkspaceUri(resource)) .returns( () => ({ @@ -98,7 +98,7 @@ suite('Interpreter Evaluation', () => { test('If no workspaces are opened, return true', async () => { // tslint:disable-next-line: no-any const interpreter = { path: 'interpreterPath' } as any; - interpreterHelper.setup(i => i.getActiveWorkspaceUri(resource)).returns(() => undefined); + interpreterHelper.setup((i) => i.getActiveWorkspaceUri(resource)).returns(() => undefined); const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); expect(isSafe).to.equal(true, 'Should be true'); }); @@ -107,7 +107,7 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any const interpreter = { path: 'interpreterPath' } as any; interpreterHelper - .setup(i => i.getActiveWorkspaceUri(resource)) + .setup((i) => i.getActiveWorkspaceUri(resource)) .returns( () => ({ @@ -123,7 +123,7 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; interpreterHelper - .setup(i => i.getActiveWorkspaceUri(resource)) + .setup((i) => i.getActiveWorkspaceUri(resource)) .returns( () => ({ @@ -132,7 +132,7 @@ suite('Interpreter Evaluation', () => { } as any) ); areInterpretersInWorkspaceSafe - .setup(i => i.value) + .setup((i) => i.value) // tslint:disable-next-line: no-any .returns(() => 'areInterpretersInWorkspaceSafeValue' as any); const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); @@ -143,7 +143,7 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; interpreterHelper - .setup(i => i.getActiveWorkspaceUri(resource)) + .setup((i) => i.getActiveWorkspaceUri(resource)) .returns( () => ({ @@ -151,8 +151,8 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any } as any) ); - areInterpretersInWorkspaceSafe.setup(i => i.value).returns(() => undefined); - unsafeInterpreterPromptEnabled.setup(s => s.value).returns(() => false); + areInterpretersInWorkspaceSafe.setup((i) => i.value).returns(() => undefined); + unsafeInterpreterPromptEnabled.setup((s) => s.value).returns(() => false); const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); expect(isSafe).to.equal(true, 'Should be true'); }); @@ -161,7 +161,7 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; interpreterHelper - .setup(i => i.getActiveWorkspaceUri(resource)) + .setup((i) => i.getActiveWorkspaceUri(resource)) .returns( () => ({ @@ -169,8 +169,8 @@ suite('Interpreter Evaluation', () => { // tslint:disable-next-line: no-any } as any) ); - areInterpretersInWorkspaceSafe.setup(i => i.value).returns(() => undefined); - unsafeInterpreterPromptEnabled.setup(s => s.value).returns(() => true); + areInterpretersInWorkspaceSafe.setup((i) => i.value).returns(() => undefined); + unsafeInterpreterPromptEnabled.setup((s) => s.value).returns(() => true); const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); expect(isSafe).to.equal(undefined, 'Should be undefined'); }); @@ -179,7 +179,7 @@ suite('Interpreter Evaluation', () => { suite('Method _inferValueUsingPrompt()', () => { test('Active workspace key is stored in security storage', async () => { interpreterSecurityStorage - .setup(i => i.storeKeyForWorkspace(resource)) + .setup((i) => i.storeKeyForWorkspace(resource)) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); await interpreterEvaluation._inferValueUsingPrompt(resource); @@ -193,11 +193,11 @@ suite('Interpreter Evaluation', () => { return Promise.resolve(promptDisplayCount < 3 ? Common.learnMore() : 'Some other option'); }; applicationShell - .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) .returns(showInformationMessage) .verifiable(Typemoq.Times.exactly(3)); browserService - .setup(b => b.launch(learnMoreOnInterpreterSecurityURI)) + .setup((b) => b.launch(learnMoreOnInterpreterSecurityURI)) .returns(() => undefined) .verifiable(Typemoq.Times.exactly(2)); @@ -209,11 +209,11 @@ suite('Interpreter Evaluation', () => { test('If `No` is selected, update the areInterpretersInWorkspaceSafe storage to unsafe and return false', async () => { applicationShell - .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) .returns(() => Promise.resolve(Common.bannerLabelNo())) .verifiable(Typemoq.Times.once()); areInterpretersInWorkspaceSafe - .setup(i => i.updateValue(false)) + .setup((i) => i.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(Typemoq.Times.once()); @@ -226,11 +226,11 @@ suite('Interpreter Evaluation', () => { test('If `Yes` is selected, update the areInterpretersInWorkspaceSafe storage to safe and return true', async () => { applicationShell - .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) .returns(() => Promise.resolve(Common.bannerLabelYes())) .verifiable(Typemoq.Times.once()); areInterpretersInWorkspaceSafe - .setup(i => i.updateValue(true)) + .setup((i) => i.updateValue(true)) .returns(() => Promise.resolve(undefined)) .verifiable(Typemoq.Times.once()); @@ -243,11 +243,11 @@ suite('Interpreter Evaluation', () => { test('If no selection is made, update the areInterpretersInWorkspaceSafe storage to unsafe and return false', async () => { applicationShell - .setup(a => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(Typemoq.Times.once()); areInterpretersInWorkspaceSafe - .setup(i => i.updateValue(false)) + .setup((i) => i.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(Typemoq.Times.once()); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts index b2cb16cdde45..aecb047c260e 100644 --- a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts @@ -28,10 +28,10 @@ suite('Interpreter Security service', () => { unsafeInterpreters = Typemoq.Mock.ofType>(); safeInterpreters = Typemoq.Mock.ofType>(); interpreterSecurityStorage = Typemoq.Mock.ofType(); - safeInterpreters.setup(s => s.value).returns(() => safeInterpretersList); - unsafeInterpreters.setup(s => s.value).returns(() => unsafeInterpretersList); - interpreterSecurityStorage.setup(p => p.unsafeInterpreters).returns(() => unsafeInterpreters.object); - interpreterSecurityStorage.setup(p => p.safeInterpreters).returns(() => safeInterpreters.object); + safeInterpreters.setup((s) => s.value).returns(() => safeInterpretersList); + unsafeInterpreters.setup((s) => s.value).returns(() => unsafeInterpretersList); + interpreterSecurityStorage.setup((p) => p.unsafeInterpreters).returns(() => unsafeInterpreters.object); + interpreterSecurityStorage.setup((p) => p.safeInterpreters).returns(() => safeInterpreters.object); interpreterSecurityService = new InterpreterSecurityService( interpreterSecurityStorage.object, interpreterEvaluation.object @@ -61,7 +61,7 @@ suite('Interpreter Security service', () => { // tslint:disable-next-line: no-any const interpreter = { path: 'random' } as any; interpreterEvaluation - .setup(i => i.inferValueUsingCurrentState(interpreter, resource)) + .setup((i) => i.inferValueUsingCurrentState(interpreter, resource)) // tslint:disable-next-line: no-any .returns(() => 'value' as any) .verifiable(Typemoq.Times.once()); @@ -75,7 +75,7 @@ suite('Interpreter Security service', () => { test("If interpreter to be evaluated already exists in the safe intepreters list, simply return and don't evaluate", async () => { const interpreter = { path: 'safe2' }; interpreterEvaluation - .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) .verifiable(Typemoq.Times.never()); // tslint:disable-next-line: no-any await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); @@ -85,7 +85,7 @@ suite('Interpreter Security service', () => { test("If interpreter to be evaluated already exists in the unsafe intepreters list, simply return and don't evaluate", async () => { const interpreter = { path: 'unsafe1' }; interpreterEvaluation - .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) .verifiable(Typemoq.Times.never()); // tslint:disable-next-line: no-any await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); @@ -95,7 +95,7 @@ suite('Interpreter Security service', () => { test('If interpreter to be evaluated does not exists in the either of the intepreters list, evaluate the interpreters', async () => { const interpreter = { path: 'notInEitherLists' }; interpreterEvaluation - .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) .verifiable(Typemoq.Times.once()); // tslint:disable-next-line: no-any await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); @@ -105,11 +105,11 @@ suite('Interpreter Security service', () => { test('If interpreter is evaluated to be safe, add it in the safe interpreters list', async () => { const interpreter = { path: 'notInEitherLists' }; interpreterEvaluation - .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) .returns(() => Promise.resolve(true)) .verifiable(Typemoq.Times.once()); safeInterpreters - .setup(s => s.updateValue(['notInEitherLists', ...safeInterpretersList])) + .setup((s) => s.updateValue(['notInEitherLists', ...safeInterpretersList])) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); // tslint:disable-next-line: no-any @@ -121,11 +121,11 @@ suite('Interpreter Security service', () => { test('If interpreter is evaluated to be unsafe, add it in the unsafe interpreters list', async () => { const interpreter = { path: 'notInEitherLists' }; interpreterEvaluation - .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) .returns(() => Promise.resolve(false)) .verifiable(Typemoq.Times.once()); unsafeInterpreters - .setup(s => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) + .setup((s) => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); // tslint:disable-next-line: no-any @@ -138,14 +138,14 @@ suite('Interpreter Security service', () => { const _didSafeInterpretersChange = Typemoq.Mock.ofType>(); const interpreter = { path: 'notInEitherLists' }; interpreterEvaluation - .setup(i => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) .returns(() => Promise.resolve(false)); unsafeInterpreters - .setup(s => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) + .setup((s) => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) .returns(() => Promise.resolve()); interpreterSecurityService._didSafeInterpretersChange = _didSafeInterpretersChange.object; _didSafeInterpretersChange - .setup(d => d.fire()) + .setup((d) => d.fire()) .returns(() => undefined) .verifiable(Typemoq.Times.once()); // tslint:disable-next-line: no-any diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts index aed4de7f8d2b..7fd4a4779308 100644 --- a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts @@ -38,16 +38,16 @@ suite('Interpreter Security Storage', () => { workspaceService = Typemoq.Mock.ofType(); areInterpretersInWorkspaceSafe = Typemoq.Mock.ofType>(); persistentStateFactory - .setup(p => p.createGlobalPersistentState(unsafeInterpretersKey, [])) + .setup((p) => p.createGlobalPersistentState(unsafeInterpretersKey, [])) .returns(() => unsafeInterpreters.object); persistentStateFactory - .setup(p => p.createGlobalPersistentState(safeInterpretersKey, [])) + .setup((p) => p.createGlobalPersistentState(safeInterpretersKey, [])) .returns(() => safeInterpreters.object); persistentStateFactory - .setup(p => p.createGlobalPersistentState(unsafeInterpreterPromptKey, true)) + .setup((p) => p.createGlobalPersistentState(unsafeInterpreterPromptKey, true)) .returns(() => unsafeInterpreterPromptEnabled.object); persistentStateFactory - .setup(p => p.createGlobalPersistentState(flaggedWorkspacesKeysStorageKey, [])) + .setup((p) => p.createGlobalPersistentState(flaggedWorkspacesKeysStorageKey, [])) .returns(() => flaggedWorkspacesKeysStorage.object); interpreterSecurityStorage = new InterpreterSecurityStorage( persistentStateFactory.object, @@ -59,7 +59,7 @@ suite('Interpreter Security Storage', () => { test('Command is registered in the activate() method', async () => { commandManager - .setup(c => c.registerCommand(Commands.ResetInterpreterSecurityStorage, Typemoq.It.isAny())) + .setup((c) => c.registerCommand(Commands.ResetInterpreterSecurityStorage, Typemoq.It.isAny())) .returns(() => Typemoq.Mock.ofType().object) .verifiable(Typemoq.Times.once()); @@ -70,16 +70,16 @@ suite('Interpreter Security Storage', () => { test('Flagged workspace keys are stored correctly', async () => { flaggedWorkspacesKeysStorage - .setup(f => f.value) + .setup((f) => f.value) .returns(() => ['workspace1Key']) .verifiable(Typemoq.Times.once()); const workspace2 = Uri.parse('2'); - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(workspace2)).returns(() => workspace2.fsPath); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(workspace2)).returns(() => workspace2.fsPath); const workspace2Key = interpreterSecurityStorage._getKeyForWorkspace(workspace2); flaggedWorkspacesKeysStorage - .setup(f => f.updateValue(['workspace1Key', workspace2Key])) + .setup((f) => f.updateValue(['workspace1Key', workspace2Key])) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); @@ -92,31 +92,31 @@ suite('Interpreter Security Storage', () => { const areInterpretersInWorkspace1Safe = Typemoq.Mock.ofType>(); const areInterpretersInWorkspace2Safe = Typemoq.Mock.ofType>(); - flaggedWorkspacesKeysStorage.setup(f => f.value).returns(() => ['workspace1Key', 'workspace2Key']); + flaggedWorkspacesKeysStorage.setup((f) => f.value).returns(() => ['workspace1Key', 'workspace2Key']); safeInterpreters - .setup(s => s.updateValue([])) + .setup((s) => s.updateValue([])) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); unsafeInterpreters - .setup(s => s.updateValue([])) + .setup((s) => s.updateValue([])) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); unsafeInterpreterPromptEnabled - .setup(s => s.updateValue(true)) + .setup((s) => s.updateValue(true)) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); persistentStateFactory - .setup(p => p.createGlobalPersistentState('workspace1Key', undefined)) + .setup((p) => p.createGlobalPersistentState('workspace1Key', undefined)) .returns(() => areInterpretersInWorkspace1Safe.object); areInterpretersInWorkspace1Safe - .setup(s => s.updateValue(undefined)) + .setup((s) => s.updateValue(undefined)) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); persistentStateFactory - .setup(p => p.createGlobalPersistentState('workspace2Key', undefined)) + .setup((p) => p.createGlobalPersistentState('workspace2Key', undefined)) .returns(() => areInterpretersInWorkspace2Safe.object); areInterpretersInWorkspace2Safe - .setup(s => s.updateValue(undefined)) + .setup((s) => s.updateValue(undefined)) .returns(() => Promise.resolve()) .verifiable(Typemoq.Times.once()); @@ -130,9 +130,9 @@ suite('Interpreter Security Storage', () => { }); test('Method areInterpretersInWorkspaceSafe() returns the areInterpretersInWorkspaceSafe storage', () => { - workspaceService.setup(w => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); persistentStateFactory - .setup(p => + .setup((p) => p.createGlobalPersistentState( `ARE_INTERPRETERS_SAFE_FOR_WS_${resource.fsPath}`, undefined diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts index 33d1aa7a2e0b..e3780bd261aa 100644 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts @@ -161,7 +161,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { const pythonPathInConfig = typemoq.Mock.ofType(); const pythonPathValue = 'Hello there.exe'; pythonPathInConfig - .setup(p => p.workspaceFolderValue) + .setup((p) => p.workspaceFolderValue) .returns(() => pythonPathValue) .verifiable(typemoq.Times.once()); @@ -301,7 +301,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { .returns(() => undefined as any) .verifiable(typemoq.Times.once()); pythonPathInConfig - .setup(p => p.workspaceValue) + .setup((p) => p.workspaceValue) .returns(() => undefined as any) .verifiable(typemoq.Times.once()); when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); @@ -335,7 +335,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { .returns(() => undefined as any) .verifiable(typemoq.Times.once()); pythonPathInConfig - .setup(p => p.workspaceValue) + .setup((p) => p.workspaceValue) .returns(() => undefined as any) .verifiable(typemoq.Times.once()); when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); @@ -369,7 +369,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { .returns(() => undefined as any) .verifiable(typemoq.Times.once()); pythonPathInConfig - .setup(p => p.workspaceValue) + .setup((p) => p.workspaceValue) .returns(() => undefined as any) .verifiable(typemoq.Times.once()); when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); @@ -403,7 +403,7 @@ suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { .returns(() => undefined as any) .verifiable(typemoq.Times.once()); pythonPathInConfig - .setup(p => p.workspaceValue) + .setup((p) => p.workspaceValue) .returns(() => undefined as any) .verifiable(typemoq.Times.once()); when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index 556bc0a7752d..3b441d803c7f 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -76,53 +76,53 @@ suite('Interpreters Display', () => { autoSelection = mock(InterpreterAutoSelectionService); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), STANDARD_OUTPUT_CHANNEL)) + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), STANDARD_OUTPUT_CHANNEL)) .returns(() => output.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))) + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) .returns(() => applicationShell.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))) + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) .returns(() => interpreterService.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))) + .setup((c) => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))) .returns(() => virtualEnvMgr.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configurationService.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) .returns(() => interpreterHelper.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))) + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))) .returns(() => instance(autoSelection)); applicationShell - .setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))) + .setup((a) => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))) .returns(() => statusBar.object); - pathUtils.setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(p => p); + pathUtils.setup((p) => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p) => p); interpreterDisplay = new InterpreterDisplay(serviceContainer.object); }); function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { if (workspaceFolder) { const mockFolder = TypeMoq.Mock.ofType(); - mockFolder.setup(w => w.uri).returns(() => workspaceFolder); + mockFolder.setup((w) => w.uri).returns(() => workspaceFolder); workspaceService - .setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))) + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))) .returns(() => mockFolder.object); } else { - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); } } test('Statusbar must be created and have command name initialized', () => { - statusBar.verify(s => (s.command = TypeMoq.It.isValue('python.setInterpreter')), TypeMoq.Times.once()); + statusBar.verify((s) => (s.command = TypeMoq.It.isValue('python.setInterpreter')), TypeMoq.Times.once()); expect(disposableRegistry).to.be.lengthOf.above(0); expect(disposableRegistry).contain(statusBar.object); }); @@ -138,17 +138,17 @@ suite('Interpreters Display', () => { setupWorkspaceFolder(resource, workspaceFolder); when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); interpreterService - .setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve([])); interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(activeInterpreter)); await interpreterDisplay.refresh(resource); verify(autoSelection.autoSelectInterpreter(anything())).once(); - statusBar.verify(s => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); - statusBar.verify(s => (s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!), TypeMoq.Times.atLeastOnce()); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!), TypeMoq.Times.atLeastOnce()); }); test('Log the output channel if displayed needs to be updated with a new interpreter', async () => { const resource = Uri.file('x'); @@ -160,18 +160,18 @@ suite('Interpreters Display', () => { path: path.join('user', 'development', 'env', 'bin', 'python') }; pathUtils - .setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((p) => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => activeInterpreter.path); setupWorkspaceFolder(resource, workspaceFolder); when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); interpreterService - .setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve([])); interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(activeInterpreter)); output - .setup(o => o.appendLine(Interpreters.pythonInterpreterPath().format(activeInterpreter.path))) + .setup((o) => o.appendLine(Interpreters.pythonInterpreterPath().format(activeInterpreter.path))) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); @@ -191,13 +191,13 @@ suite('Interpreters Display', () => { path: pythonPath } as any) as PythonInterpreter; interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(pythonInterpreter)); await interpreterDisplay.refresh(resource); - statusBar.verify(s => (s.tooltip = TypeMoq.It.isValue(pythonPath)), TypeMoq.Times.atLeastOnce()); - statusBar.verify(s => (s.text = TypeMoq.It.isValue(displayName)), TypeMoq.Times.once()); + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)), TypeMoq.Times.atLeastOnce()); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(displayName)), TypeMoq.Times.once()); }); test('If interpreter file does not exist then update status bar accordingly', async () => { const resource = Uri.file('x'); @@ -206,26 +206,26 @@ suite('Interpreters Display', () => { setupWorkspaceFolder(resource, workspaceFolder); // tslint:disable-next-line:no-any interpreterService - .setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve([{} as any])); interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) .returns(() => Promise.resolve(undefined)); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + fileSystem.setup((f) => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); interpreterHelper - .setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) + .setup((v) => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve(undefined)); virtualEnvMgr - .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve('')); await interpreterDisplay.refresh(resource); - statusBar.verify(s => (s.color = TypeMoq.It.isValue('yellow')), TypeMoq.Times.once()); + statusBar.verify((s) => (s.color = TypeMoq.It.isValue('yellow')), TypeMoq.Times.once()); statusBar.verify( - s => (s.text = TypeMoq.It.isValue('$(alert) Select Python Interpreter')), + (s) => (s.text = TypeMoq.It.isValue('$(alert) Select Python Interpreter')), TypeMoq.Times.once() ); }); @@ -240,16 +240,16 @@ suite('Interpreters Display', () => { companyDisplayName: 'Company Name', path: pythonPath }; - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); virtualEnvMgr - .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) .returns(() => Promise.resolve('')); interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) .returns(() => Promise.resolve(activeInterpreter)) .verifiable(TypeMoq.Times.once()); interpreterHelper - .setup(i => i.getActiveWorkspaceUri(undefined)) + .setup((i) => i.getActiveWorkspaceUri(undefined)) .returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }) @@ -259,7 +259,7 @@ suite('Interpreters Display', () => { interpreterHelper.verifyAll(); interpreterService.verifyAll(); - statusBar.verify(s => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); - statusBar.verify(s => (s.tooltip = TypeMoq.It.isValue(pythonPath)!), TypeMoq.Times.atLeastOnce()); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)!), TypeMoq.Times.atLeastOnce()); }); }); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 4655d8d59da9..e362068ae95f 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -193,12 +193,12 @@ suite('Interpreters service', () => { const service = new InterpreterService(serviceContainer, hashProviderFactory.object); const documentManager = TypeMoq.Mock.ofType(); - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined); - workspace.setup(w => w.hasWorkspaceFolders).returns(() => true); - workspace.setup(w => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + workspace.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let activeTextEditorChangeHandler: Function | undefined; documentManager .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -224,12 +224,12 @@ suite('Interpreters service', () => { const service = new InterpreterService(serviceContainer, hashProviderFactory.object); const documentManager = TypeMoq.Mock.ofType(); - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined); - workspace.setup(w => w.hasWorkspaceFolders).returns(() => true); - workspace.setup(w => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + workspace.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let activeTextEditorChangeHandler: Function | undefined; documentManager .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -250,15 +250,15 @@ suite('Interpreters service', () => { const service = new InterpreterService(serviceContainer, hashProviderFactory.object); const documentManager = TypeMoq.Mock.ofType(); - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined); - workspace.setup(w => w.hasWorkspaceFolders).returns(() => true); - workspace.setup(w => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + workspace.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); let interpreterPathServiceHandler: Function | undefined; documentManager - .setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => { return { dispose: noop }; }); @@ -268,16 +268,16 @@ suite('Interpreters service', () => { }; configService.reset(); configService - .setup(c => c.getSettings()) + .setup((c) => c.getSettings()) .returns(() => pythonSettings.object) .verifiable(TypeMoq.Times.once()); configService - .setup(c => c.getSettings(i.uri)) + .setup((c) => c.getSettings(i.uri)) .returns(() => pythonSettings.object) .verifiable(TypeMoq.Times.once()); interpreterPathService - .setup(d => d.onDidChange(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback(cb => (interpreterPathServiceHandler = cb)) + .setup((d) => d.onDidChange(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((cb) => (interpreterPathServiceHandler = cb)) .returns(() => { return { dispose: noop }; }); @@ -298,9 +298,9 @@ suite('Interpreters service', () => { const resource = Uri.parse('a'); service._pythonPathSetting = ''; configService.reset(); - configService.setup(c => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); interpreterDisplay - .setup(i => i.refresh()) + .setup((i) => i.refresh()) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); service._onConfigChanged(resource); @@ -312,9 +312,9 @@ suite('Interpreters service', () => { const resource = Uri.parse('a'); service._pythonPathSetting = 'stored setting'; configService.reset(); - configService.setup(c => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); interpreterDisplay - .setup(i => i.refresh()) + .setup((i) => i.refresh()) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); service._onConfigChanged(resource); @@ -326,9 +326,9 @@ suite('Interpreters service', () => { const resource = Uri.parse('a'); service._pythonPathSetting = 'setting'; configService.reset(); - configService.setup(c => c.getSettings(resource)).returns(() => ({ pythonPath: 'setting' } as any)); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'setting' } as any)); interpreterDisplay - .setup(i => i.refresh()) + .setup((i) => i.refresh()) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); service._onConfigChanged(resource); diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts index bd22ea0bc726..e59e379cbc34 100644 --- a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -20,26 +20,26 @@ suite('Python Path Settings Updater', () => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); experimentsManager = TypeMoq.Mock.ofType(); - experimentsManager.setup(e => e.inExperiment(TypeMoq.It.isAny())).returns(() => inExperiment); + experimentsManager.setup((e) => e.inExperiment(TypeMoq.It.isAny())).returns(() => inExperiment); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined); interpreterPathService = TypeMoq.Mock.ofType(); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IExperimentsManager))) + .setup((c) => c.get(TypeMoq.It.isValue(IExperimentsManager))) .returns(() => experimentsManager.object); serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IInterpreterPathService))) + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterPathService))) .returns(() => interpreterPathService.object); updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); } function setupConfigProvider(resource?: Uri): TypeMoq.IMock { const workspaceConfig = TypeMoq.Mock.ofType(); workspaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) .returns(() => workspaceConfig.object); return workspaceConfig; } @@ -52,7 +52,7 @@ suite('Python Path Settings Updater', () => { const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; const workspaceConfig = setupConfigProvider(); workspaceConfig - .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) .returns(() => { // tslint:disable-next-line:no-any return { globalValue: pythonPath } as any; @@ -60,7 +60,7 @@ suite('Python Path Settings Updater', () => { await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never() ); }); @@ -68,11 +68,11 @@ suite('Python Path Settings Updater', () => { const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; const workspaceConfig = setupConfigProvider(); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => + (w) => w.update( TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), @@ -92,7 +92,7 @@ suite('Python Path Settings Updater', () => { const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; const workspaceConfig = setupConfigProvider(workspaceFolder); workspaceConfig - .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) .returns(() => { // tslint:disable-next-line:no-any return { workspaceFolderValue: pythonPath } as any; @@ -100,7 +100,7 @@ suite('Python Path Settings Updater', () => { await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never() ); }); @@ -110,11 +110,11 @@ suite('Python Path Settings Updater', () => { const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => + (w) => w.update( TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), @@ -130,11 +130,11 @@ suite('Python Path Settings Updater', () => { const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; const expectedPythonPath = path.join('env', 'bin', 'python'); const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => + (w) => w.update( TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(expectedPythonPath), @@ -153,7 +153,7 @@ suite('Python Path Settings Updater', () => { const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; const workspaceConfig = setupConfigProvider(workspaceFolder); workspaceConfig - .setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) .returns(() => { // tslint:disable-next-line:no-any return { workspaceValue: pythonPath } as any; @@ -161,7 +161,7 @@ suite('Python Path Settings Updater', () => { await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never() ); }); @@ -171,11 +171,11 @@ suite('Python Path Settings Updater', () => { const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => + (w) => w.update( TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), @@ -191,11 +191,11 @@ suite('Python Path Settings Updater', () => { const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; const expectedPythonPath = path.join('env', 'bin', 'python'); const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); await updater.updatePythonPath(pythonPath); workspaceConfig.verify( - w => + (w) => w.update( TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(expectedPythonPath), @@ -213,12 +213,12 @@ suite('Python Path Settings Updater', () => { test('Python Path should not be updated when current pythonPath is the same', async () => { const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; interpreterPathService - .setup(i => i.inspect(undefined)) + .setup((i) => i.inspect(undefined)) .returns(() => { return { globalValue: pythonPath }; }); interpreterPathService - .setup(i => i.update(undefined, ConfigurationTarget.Global, pythonPath)) + .setup((i) => i.update(undefined, ConfigurationTarget.Global, pythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -228,10 +228,10 @@ suite('Python Path Settings Updater', () => { }); test('Python Path should be updated when current pythonPath is different', async () => { const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; - interpreterPathService.setup(i => i.inspect(undefined)).returns(() => ({})); + interpreterPathService.setup((i) => i.inspect(undefined)).returns(() => ({})); interpreterPathService - .setup(i => i.update(undefined, ConfigurationTarget.Global, pythonPath)) + .setup((i) => i.update(undefined, ConfigurationTarget.Global, pythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); @@ -247,12 +247,12 @@ suite('Python Path Settings Updater', () => { const workspaceFolder = Uri.file(workspaceFolderPath); const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; interpreterPathService - .setup(i => i.inspect(workspaceFolder)) + .setup((i) => i.inspect(workspaceFolder)) .returns(() => ({ workspaceFolderValue: pythonPath })); interpreterPathService - .setup(i => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); @@ -263,9 +263,9 @@ suite('Python Path Settings Updater', () => { const workspaceFolderPath = path.join('user', 'desktop', 'development'); const workspaceFolder = Uri.file(workspaceFolderPath); const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); interpreterPathService - .setup(i => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -279,10 +279,10 @@ suite('Python Path Settings Updater', () => { const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; const expectedPythonPath = path.join('env', 'bin', 'python'); const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); interpreterPathService - .setup(i => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, expectedPythonPath)) + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, expectedPythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -298,10 +298,10 @@ suite('Python Path Settings Updater', () => { const workspaceFolder = Uri.file(workspaceFolderPath); const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; interpreterPathService - .setup(i => i.inspect(workspaceFolder)) + .setup((i) => i.inspect(workspaceFolder)) .returns(() => ({ workspaceValue: pythonPath })); interpreterPathService - .setup(i => i.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((i) => i.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); @@ -314,9 +314,9 @@ suite('Python Path Settings Updater', () => { const workspaceFolder = Uri.file(workspaceFolderPath); const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); interpreterPathService - .setup(i => i.update(workspaceFolder, ConfigurationTarget.Workspace, pythonPath)) + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.Workspace, pythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -331,9 +331,9 @@ suite('Python Path Settings Updater', () => { const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; const expectedPythonPath = path.join('env', 'bin', 'python'); - interpreterPathService.setup(i => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); interpreterPathService - .setup(i => i.update(workspaceFolder, ConfigurationTarget.Workspace, expectedPythonPath)) + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.Workspace, expectedPythonPath)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts index cc465e8f6c29..257b3e170567 100644 --- a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts @@ -68,11 +68,11 @@ suite('Conda Inherit Env Prompt', async () => { ); const workspaceConfig = TypeMoq.Mock.ofType(); interpreterService - .setup(is => is.getActiveInterpreter(resource)) + .setup((is) => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(undefined) as any) .verifiable(TypeMoq.Times.never()); workspaceService - .setup(ws => ws.getConfiguration('terminal', resource)) + .setup((ws) => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -82,7 +82,7 @@ suite('Conda Inherit Env Prompt', async () => { }); test('Returns false if on Windows', async () => { platformService - .setup(ps => ps.isWindows) + .setup((ps) => ps.isWindows) .returns(() => true) .verifiable(TypeMoq.Times.once()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -96,15 +96,15 @@ suite('Conda Inherit Env Prompt', async () => { }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup(ps => ps.isWindows) + .setup((ps) => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup(is => is.getActiveInterpreter(resource)) + .setup((is) => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal', resource)) + .setup((ws) => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -115,15 +115,15 @@ suite('Conda Inherit Env Prompt', async () => { test('Returns false if no active interpreter is present', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup(ps => ps.isWindows) + .setup((ps) => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup(is => is.getActiveInterpreter(resource)) + .setup((is) => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal', resource)) + .setup((ws) => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -137,19 +137,19 @@ suite('Conda Inherit Env Prompt', async () => { }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup(ps => ps.isWindows) + .setup((ps) => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup(is => is.getActiveInterpreter(resource)) + .setup((is) => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal', resource)) + .setup((ws) => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup(ws => ws.inspect('integrated.inheritEnv')) + .setup((ws) => ws.inspect('integrated.inheritEnv')) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); @@ -182,26 +182,26 @@ suite('Conda Inherit Env Prompt', async () => { workspaceFolderValue: false } } - ].forEach(testParams => { + ].forEach((testParams) => { test(testParams.name, async () => { const interpreter = { type: InterpreterType.Conda }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup(ps => ps.isWindows) + .setup((ps) => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup(is => is.getActiveInterpreter(resource)) + .setup((is) => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal', resource)) + .setup((ws) => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup(ws => ws.inspect('integrated.inheritEnv')) + .setup((ws) => ws.inspect('integrated.inheritEnv')) .returns(() => testParams.settings as any); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); expect(result).to.equal(false, 'Prompt should not be shown'); @@ -220,18 +220,18 @@ suite('Conda Inherit Env Prompt', async () => { }; const workspaceConfig = TypeMoq.Mock.ofType(); platformService - .setup(ps => ps.isWindows) + .setup((ps) => ps.isWindows) .returns(() => false) .verifiable(TypeMoq.Times.once()); interpreterService - .setup(is => is.getActiveInterpreter(resource)) + .setup((is) => is.getActiveInterpreter(resource)) .returns(() => Promise.resolve(interpreter) as any) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal', resource)) + .setup((ws) => ws.getConfiguration('terminal', resource)) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); - workspaceConfig.setup(ws => ws.inspect('integrated.inheritEnv')).returns(() => settings as any); + workspaceConfig.setup((ws) => ws.inspect('integrated.inheritEnv')).returns(() => settings as any); const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); expect(result).to.equal(true, 'Prompt should be shown'); expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); @@ -372,11 +372,11 @@ suite('Conda Inherit Env Prompt', async () => { test('Does not display prompt if it is disabled', async () => { notificationPromptEnabled - .setup(n => n.value) + .setup((n) => n.value) .returns(() => false) .verifiable(TypeMoq.Times.once()); appShell - .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -387,27 +387,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Do nothing if no option is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup(n => n.value) + .setup((n) => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal')) + .setup((ws) => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); notificationPromptEnabled - .setup(n => n.updateValue(false)) + .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); browserService - .setup(b => b.launch('https://aka.ms/AA66i8f')) + .setup((b) => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -420,27 +420,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Update terminal settings if `Yes` is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup(n => n.value) + .setup((n) => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(Common.bannerLabelYes())) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal')) + .setup((ws) => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); notificationPromptEnabled - .setup(n => n.updateValue(false)) + .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); browserService - .setup(b => b.launch('https://aka.ms/AA66i8f')) + .setup((b) => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -453,27 +453,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Disable notification prompt if `No` is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup(n => n.value) + .setup((n) => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(Common.bannerLabelNo())) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal')) + .setup((ws) => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); notificationPromptEnabled - .setup(n => n.updateValue(false)) + .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); browserService - .setup(b => b.launch('https://aka.ms/AA66i8f')) + .setup((b) => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.never()); await condaInheritEnvPrompt.promptAndUpdate(); @@ -486,27 +486,27 @@ suite('Conda Inherit Env Prompt', async () => { test('Launch browser if `More info` option is selected', async () => { const workspaceConfig = TypeMoq.Mock.ofType(); notificationPromptEnabled - .setup(n => n.value) + .setup((n) => n.value) .returns(() => true) .verifiable(TypeMoq.Times.once()); appShell - .setup(a => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) .returns(() => Promise.resolve(Common.moreInfo())) .verifiable(TypeMoq.Times.once()); workspaceService - .setup(ws => ws.getConfiguration('terminal')) + .setup((ws) => ws.getConfiguration('terminal')) .returns(() => workspaceConfig.object) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup(wc => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); notificationPromptEnabled - .setup(n => n.updateValue(false)) + .setup((n) => n.updateValue(false)) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.never()); browserService - .setup(b => b.launch('https://aka.ms/AA66i8f')) + .setup((b) => b.launch('https://aka.ms/AA66i8f')) .returns(() => undefined) .verifiable(TypeMoq.Times.once()); await condaInheritEnvPrompt.promptAndUpdate(); diff --git a/src/test/startupTelemetry.unit.test.ts b/src/test/startupTelemetry.unit.test.ts index ec8b554a43c4..828a85a000ff 100644 --- a/src/test/startupTelemetry.unit.test.ts +++ b/src/test/startupTelemetry.unit.test.ts @@ -24,30 +24,32 @@ suite('Startup Telemetry - hasUserDefinedPythonPath()', async () => { interpreterPathService = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); experimentsManager - .setup(e => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) .returns(() => undefined); - serviceContainer.setup(s => s.get(IExperimentsManager)).returns(() => experimentsManager.object); - serviceContainer.setup(s => s.get(IWorkspaceService)).returns(() => workspaceService.object); - serviceContainer.setup(s => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + serviceContainer.setup((s) => s.get(IExperimentsManager)).returns(() => experimentsManager.object); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); }); function setupConfigProvider(): TypeMoq.IMock { const workspaceConfig = TypeMoq.Mock.ofType(); workspaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) .returns(() => workspaceConfig.object); return workspaceConfig; } - [undefined, 'python'].forEach(globalValue => { - [undefined, 'python'].forEach(workspaceValue => { - [undefined, 'python'].forEach(workspaceFolderValue => { + [undefined, 'python'].forEach((globalValue) => { + [undefined, 'python'].forEach((workspaceValue) => { + [undefined, 'python'].forEach((workspaceFolderValue) => { test(`Return false if using settings equals {globalValue: ${globalValue}, workspaceValue: ${workspaceValue}, workspaceFolderValue: ${workspaceFolderValue}}`, () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => false); const workspaceConfig = setupConfigProvider(); // tslint:disable-next-line: no-any workspaceConfig - .setup(w => w.inspect('pythonPath')) + .setup((w) => w.inspect('pythonPath')) // tslint:disable-next-line: no-any .returns(() => ({ globalValue, workspaceValue, workspaceFolderValue } as any)); const result = hasUserDefinedPythonPath(resource, serviceContainer.object); @@ -58,18 +60,18 @@ suite('Startup Telemetry - hasUserDefinedPythonPath()', async () => { }); test('Return true if using setting value equals something else', () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); const workspaceConfig = setupConfigProvider(); // tslint:disable-next-line: no-any - workspaceConfig.setup(w => w.inspect('pythonPath')).returns(() => ({ globalValue: 'something else' } as any)); + workspaceConfig.setup((w) => w.inspect('pythonPath')).returns(() => ({ globalValue: 'something else' } as any)); const result = hasUserDefinedPythonPath(resource, serviceContainer.object); expect(result).to.equal(true, 'Should be true'); }); test('If in Deprecate PythonPath experiment, use the new API to inspect settings', () => { - experimentsManager.setup(e => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); interpreterPathService - .setup(i => i.inspect(resource)) + .setup((i) => i.inspect(resource)) .returns(() => ({})) .verifiable(TypeMoq.Times.once()); hasUserDefinedPythonPath(resource, serviceContainer.object); From 5f4ce884caef85fb396528223a8e00e304eb3580 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 8 Apr 2020 00:58:54 -0700 Subject: [PATCH 5/7] Edit news entries appropriately --- news/1 Enhancements/10325.md | 2 +- news/1 Enhancements/10374.md | 2 +- news/1 Enhancements/10879.md | 2 +- news/1 Enhancements/10912.md | 2 +- news/1 Enhancements/11021.md | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 news/1 Enhancements/11021.md diff --git a/news/1 Enhancements/10325.md b/news/1 Enhancements/10325.md index f532b69c0c2a..8634bbe9aee2 100644 --- a/news/1 Enhancements/10325.md +++ b/news/1 Enhancements/10325.md @@ -1 +1 @@ -Use new interpreter storage supporting multiroot workspaces when in PythonPath experiment. +Use new interpreter storage supporting multiroot workspaces when in Deprecate PythonPath experiment. diff --git a/news/1 Enhancements/10374.md b/news/1 Enhancements/10374.md index 1baf92bbcc75..2e59cc5c0620 100644 --- a/news/1 Enhancements/10374.md +++ b/news/1 Enhancements/10374.md @@ -1 +1 @@ -Added a command to reset value of Python interpreter. +Added a command `Clear Workspace Interpreter Setting` to clear value of Python interpreter from workspace settings. diff --git a/news/1 Enhancements/10879.md b/news/1 Enhancements/10879.md index 63825b5e906e..bd19fd67f63e 100644 --- a/news/1 Enhancements/10879.md +++ b/news/1 Enhancements/10879.md @@ -1 +1 @@ -Prompt when an "untrusted" workspace Python environment is to be auto selected. +Prompt when an "untrusted" workspace Python environment is to be auto selected when in Deprecate PythonPath experiment. diff --git a/news/1 Enhancements/10912.md b/news/1 Enhancements/10912.md index 34afd920706d..efd1d1c4996d 100644 --- a/news/1 Enhancements/10912.md +++ b/news/1 Enhancements/10912.md @@ -1 +1 @@ -Added a command to reset "untrusted" interpreters storage. +Added a command `Reset stored info for untrusted Interpreters` to reset "untrusted" interpreters storage when in Deprecate PythonPath experiment. diff --git a/news/1 Enhancements/11021.md b/news/1 Enhancements/11021.md new file mode 100644 index 000000000000..b0ec55221aac --- /dev/null +++ b/news/1 Enhancements/11021.md @@ -0,0 +1 @@ +Added a user setting `python.defaultInterpreterPath` to set up the default interpreter path when in Deprecate PythonPath experiment. From dc0e23f6a7ee647593dde68b882e550b71aab91f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 8 Apr 2020 09:31:22 -0700 Subject: [PATCH 6/7] Git rebase --- package.json | 1 + src/client/common/configSettings.ts | 7 +++++-- .../configSettings/configSettings.pythonPath.unit.test.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5b6d5dae5644..ff86923d5ce1 100644 --- a/package.json +++ b/package.json @@ -1599,6 +1599,7 @@ "UseTerminalToGetActivatedEnvVars - experiment", "CollectLSRequestTiming - experiment", "CollectNodeLSRequestTiming - experiment", + "DeprecatePythonPath - experiment", "All" ] }, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index cb6f64e303fa..e95e67ed5a37 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -17,6 +17,7 @@ import '../common/extensions'; import { IInterpreterAutoSeletionProxyService, IInterpreterSecurityService } from '../interpreter/autoSelection/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; import { IWorkspaceService } from './application/types'; import { WorkspaceService } from './application/workspace'; import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; @@ -242,8 +243,10 @@ export class PythonSettings implements IPythonSettings { } this.languageServer = systemVariables.resolveAny(ls)!; - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.envFile = systemVariables.resolveAny(pythonSettings.get('envFile'))!; + const envFileSetting = pythonSettings.get('envFile'); + this.envFile = systemVariables.resolveAny(envFileSetting)!; + sendSettingTelemetry(this.workspace, envFileSetting); + // tslint:disable-next-line:no-any // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index d813910518b0..b15ebfff705e 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -16,8 +16,8 @@ import { PythonSettings } from '../../../client/common/configSettings'; import { DeprecatePythonPath } from '../../../client/common/experimentGroups'; import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; -import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { IInterpreterSecurityService } from '../../../client/interpreter/autoSelection/types'; +import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; const untildify = require('untildify'); From 1bc48d086c0581057cc00a75148a8c512857b217 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 8 Apr 2020 10:52:14 -0700 Subject: [PATCH 7/7] Code reviews --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 3b255b6f29e0..c929edcf5089 100644 --- a/package.nls.json +++ b/package.nls.json @@ -11,7 +11,7 @@ "python.command.python.switchToDailyChannel.title": "Switch to Insiders Daily Channel", "python.command.python.switchToWeeklyChannel.title": "Switch to Insiders Weekly Channel", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", - "python.command.python.resetInterpreterSecurityStorage.title": "Reset stored info for untrusted Interpreters", + "python.command.python.resetInterpreterSecurityStorage.title": "Reset Stored Info for Untrusted Interpreters", "python.command.python.refactorExtractVariable.title": "Extract Variable", "python.command.python.refactorExtractMethod.title": "Extract Method", "python.command.python.viewOutput.title": "Show Output",