From 9c740b9557d62e21b062a35ff4a945835fe45c64 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 14 Aug 2023 17:50:34 -0700 Subject: [PATCH] Show notification reaffirming Python extension still handles activation when in `pythonTerminalEnvVarActivation` experiment (#21802) Closes https://github.com/microsoft/vscode-python/issues/21793 Only show notification when terminal prompt does not already indicate that env is activated. --- src/client/activation/extensionSurvey.ts | 4 +- src/client/common/experiments/helpers.ts | 6 + src/client/common/utils/localize.ts | 5 +- .../terminalEnvVarCollectionPrompt.ts | 80 +++++++ .../terminalEnvVarCollectionService.ts | 65 +++++- src/client/interpreter/activation/types.ts | 8 + src/client/interpreter/serviceRegistry.ts | 12 +- src/client/pythonEnvironments/info/index.ts | 3 +- src/client/telemetry/index.ts | 6 +- .../activation/extensionSurvey.unit.test.ts | 2 +- .../checks/macPythonInterpreter.unit.test.ts | 2 +- .../common/installer/installer.unit.test.ts | 12 +- ...erminalEnvVarCollectionPrompt.unit.test.ts | 203 ++++++++++++++++++ ...rminalEnvVarCollectionService.unit.test.ts | 157 ++++++++++++++ .../virtualEnvs/virtualEnvPrompt.unit.test.ts | 2 +- 15 files changed, 545 insertions(+), 22 deletions(-) create mode 100644 src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts create mode 100644 src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index 6d1d784237ba..11b581a27252 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -83,10 +83,10 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @traceDecoratorError('Failed to display prompt for extension survey') public async showSurvey() { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; - const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [ + const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [ 'Yes', 'Maybe later', - 'Do not show again', + "Don't show again", ]; const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts); sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index 04da948fd15d..bae96b222eb6 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -3,10 +3,16 @@ 'use strict'; +import { env, workspace } from 'vscode'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; +import { isTestExecution } from '../constants'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { + if (!isTestExecution() && workspace.workspaceFile && env.remoteName) { + // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + return false; + } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { return false; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b3af0c476957..3ec1829201f8 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -63,7 +63,7 @@ export namespace Common { export const openOutputPanel = l10n.t('Show output'); export const noIWillDoItLater = l10n.t('No, I will do it later'); export const notNow = l10n.t('Not now'); - export const doNotShowAgain = l10n.t('Do not show again'); + export const doNotShowAgain = l10n.t("Don't show again"); export const reload = l10n.t('Reload'); export const moreInfo = l10n.t('More Info'); export const learnMore = l10n.t('Learn more'); @@ -198,6 +198,9 @@ export namespace Interpreters { ); export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); + export const terminalEnvVarCollectionPrompt = l10n.t( + 'The Python extension automatically activates all terminals using the selected environment. You can hover over the terminal tab to see more information about the activation. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts new file mode 100644 index 000000000000..7833f34ce2fb --- /dev/null +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IPersistentStateFactory, +} from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ITerminalEnvVarCollectionService } from './types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; + +export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; + +@injectable() +export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(ITerminalEnvVarCollectionService) + private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + this.disposableRegistry.push( + this.terminalManager.onDidOpenTerminal(async (terminal) => { + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + return; + } + if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) { + // No need to show notification if terminal prompt already indicates when env is activated. + return; + } + await this.notifyUsers(); + }), + ); + } + + private async notifyUsers(): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalEnvCollectionPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.doNotShowAgain]; + const selection = await this.appShell.showInformationMessage( + Interpreters.terminalEnvVarCollectionPrompt, + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 2e5c26be0222..a45306132bf4 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -23,13 +23,15 @@ import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceVerbose } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; -import { IEnvironmentActivationService } from './types'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; +import { TerminalShellType } from '../../common/terminal/types'; +import { OSType } from '../../common/utils/platform'; @injectable() -export class TerminalEnvVarCollectionService implements IExtensionActivationService { +export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false, @@ -127,6 +129,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } + await this.trackTerminalPrompt(shell, resource, env); this.processEnvVars = undefined; return; } @@ -146,6 +149,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (prevValue !== value) { if (value !== undefined) { if (key === 'PS1') { + // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, @@ -165,6 +169,61 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; + + await this.trackTerminalPrompt(shell, resource, env); + } + + private isPromptSet = new Map(); + + // eslint-disable-next-line class-methods-use-this + public isTerminalPromptSetCorrectly(resource?: Resource): boolean { + const workspaceFolder = this.getWorkspaceFolder(resource); + return !!this.isPromptSet.get(workspaceFolder?.index); + } + + /** + * Call this once we know terminal prompt is set correctly for terminal owned by this resource. + */ + private terminalPromptIsCorrect(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.set(key, true); + } + + private terminalPromptIsUnknown(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.delete(key); + } + + /** + * Tracks whether prompt for terminal was correctly set. + */ + private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) { + this.terminalPromptIsUnknown(resource); + if (!env) { + this.terminalPromptIsCorrect(resource); + return; + } + // Prompts for these shells cannot be set reliably using variables + const exceptionShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + TerminalShellType.zsh, // TODO: Remove this once https://github.com/microsoft/vscode/issues/188875 is fixed + ]; + const customShellType = identifyShellFromShellPath(shell); + if (exceptionShells.includes(customShellType)) { + return; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldPS1BeSet = interpreter?.type !== undefined; + if (shouldPS1BeSet && !env.PS1) { + // PS1 should be set but no PS1 was set. + return; + } + } + this.terminalPromptIsCorrect(resource); } private async handleMicroVenv(resource: Resource) { @@ -178,7 +237,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.replace( 'PATH', `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, - { applyAtShellIntegration: true }, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, ); return; } diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index e00ef9b62b3f..2b364cbeb862 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,3 +21,11 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 04af15415b04..018e7abfdc46 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,8 +6,9 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; +import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService } from './activation/types'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -109,8 +110,13 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - IExtensionActivationService, + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService, ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalEnvVarCollectionPrompt, + ); } diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 60628f61314e..ee2ff9d7cc22 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -4,6 +4,7 @@ 'use strict'; import { Architecture } from '../../common/utils/platform'; +import { PythonEnvType } from '../base/info'; import { PythonVersion } from './pythonVersion'; /** @@ -85,7 +86,7 @@ export type PythonEnvironment = InterpreterInformation & { envName?: string; envPath?: string; cachedEntry?: boolean; - type?: string; + type?: PythonEnvType; }; /** diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 3fcc177edea7..a068f21d2327 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -950,7 +950,7 @@ export interface IEventNamePropertyMapping { tool?: LinterId; /** * `select` When 'Select linter' option is selected - * `disablePrompt` When 'Do not show again' option is selected + * `disablePrompt` When "Don't show again" option is selected * `install` When 'Install' option is selected * * @type {('select' | 'disablePrompt' | 'install')} @@ -1374,7 +1374,7 @@ export interface IEventNamePropertyMapping { /** * `Yes` When 'Yes' option is selected * `No` When 'No' option is selected - * `Ignore` When 'Do not show again' option is clicked + * `Ignore` When "Don't show again" option is clicked * * @type {('Yes' | 'No' | 'Ignore' | undefined)} */ @@ -1571,7 +1571,7 @@ export interface IEventNamePropertyMapping { /** * Carries the selection of user when they are asked to take the extension survey */ - selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined; + selection: 'Yes' | 'Maybe later' | "Don't show again" | undefined; }; /** * Telemetry event sent when starting REPL diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index 6449eae24f31..ba96b917aff3 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -355,7 +355,7 @@ suite('Extension survey prompt - showSurvey()', () => { platformService.verifyAll(); }); - test("Disable prompt if 'Do not show again' option is clicked", async () => { + test('Disable prompt if "Don\'t show again" option is clicked', async () => { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index f75375520ec8..ba2436d0ffeb 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -220,7 +220,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ { prompt: 'Select Python Interpreter', command: cmd }, - { prompt: 'Do not show again', command: cmdIgnore }, + { prompt: "Don't show again", command: cmdIgnore }, ]); }); test('Should not display a message if No Interpreters diagnostic has been ignored', async () => { diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts index bdd7ab32a028..38b9d9174472 100644 --- a/src/test/common/installer/installer.unit.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -307,10 +307,10 @@ suite('Module Installer only', () => { TypeMoq.It.isAnyString(), TypeMoq.It.isValue('Install'), TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), + TypeMoq.It.isValue("Don't show again"), ), ) - .returns(async () => 'Do not show again') + .returns(async () => "Don't show again") .verifiable(TypeMoq.Times.once()); const persistVal = TypeMoq.Mock.ofType>(); let mockPersistVal = false; @@ -367,7 +367,7 @@ suite('Module Installer only', () => { TypeMoq.It.isAnyString(), TypeMoq.It.isValue('Install'), TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), + TypeMoq.It.isValue("Don't show again"), ), ) .returns(async () => undefined) @@ -864,7 +864,7 @@ suite('Module Installer only', () => { test('Ensure 3 options for pylint', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; await installer.promptToInstallImplementation(product, resource); @@ -875,7 +875,7 @@ suite('Module Installer only', () => { }); test('Ensure select linter command is invoked', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; when( appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), @@ -892,7 +892,7 @@ suite('Module Installer only', () => { }); test('If install button is selected, install linter and return response', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; when( appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts new file mode 100644 index 000000000000..8edaa00dcf73 --- /dev/null +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; +import { + IConfigurationService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../client/common/types'; +import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; +import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; + +suite('Terminal Environment Variable Collection Prompt', () => { + let shell: IApplicationShell; + let terminalManager: ITerminalManager; + let experimentService: IExperimentService; + let activeResourceService: IActiveResourceService; + let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; + let persistentStateFactory: IPersistentStateFactory; + let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEventEmitter: EventEmitter; + let notificationEnabled: IPersistentState; + let configurationService: IConfigurationService; + const prompts = [Common.doNotShowAgain]; + const message = Interpreters.terminalEnvVarCollectionPrompt; + + setup(async () => { + shell = mock(); + terminalManager = mock(); + experimentService = mock(); + activeResourceService = mock(); + persistentStateFactory = mock(); + terminalEnvVarCollectionService = mock(); + configurationService = mock(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock>(); + terminalEventEmitter = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); + terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + instance(shell), + instance(persistentStateFactory), + instance(terminalManager), + [], + instance(activeResourceService), + instance(terminalEnvVarCollectionService), + instance(configurationService), + instance(experimentService), + ); + }); + + test('Show notification when a new terminal is opened for which there is no prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).once(); + }); + + test('Do not show notification if automatic terminal activation is turned off', async () => { + reset(configurationService); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: false, + }, + } as unknown) as IPythonSettings); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(false); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Do not disable notification if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(undefined)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).never(); + }); +}); diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 73c9e82274de..86823f16a8c8 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -35,6 +35,8 @@ import { TerminalEnvVarCollectionService } from '../../../client/interpreter/act import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -262,6 +264,161 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); + test('Correct track that prompt was set for non-Windows bash where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was not set for non-Windows where PS1 is not set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set correctly for global interpreters', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: undefined, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(undefined); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for Windows when not using powershell', async () => { + when(platform.osType).thenReturn(OSType.Windows); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'cmd'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for Windows when using powershell', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'powershell'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { when( environmentActivationService.getActivatedEnvironmentVariables( diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index 9671e393dc43..2ad67831c455 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -223,7 +223,7 @@ suite('Virtual Environment Prompt', () => { notificationPromptEnabled.verifyAll(); }); - test("If user selects 'Do not show again', prompt is disabled", async () => { + test('If user selects "Don\'t show again", prompt is disabled', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain];