diff --git a/pythonFiles/create_conda.py b/pythonFiles/create_conda.py index 0e48ee6b2286..9a34de47d51f 100644 --- a/pythonFiles/create_conda.py +++ b/pythonFiles/create_conda.py @@ -85,6 +85,7 @@ def install_packages(env_path: str) -> None: ], "CREATE_CONDA.FAILED_INSTALL_YML", ) + print("CREATE_CONDA.INSTALLED_YML") def add_gitignore(name: str) -> None: @@ -100,7 +101,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: argv = [] args = parse_args(argv) - if not conda_env_exists(args.name): + if conda_env_exists(args.name): + env_path = get_conda_env_path(args.name) + print(f"EXISTING_CONDA_ENV:{env_path}") + else: run_process( [ sys.executable, @@ -114,12 +118,11 @@ def main(argv: Optional[Sequence[str]] = None) -> None: ], "CREATE_CONDA.ENV_FAILED_CREATION", ) + env_path = get_conda_env_path(args.name) + print(f"CREATED_CONDA_ENV:{env_path}") if args.git_ignore: add_gitignore(args.name) - env_path = get_conda_env_path(args.name) - print(f"CREATED_CONDA_ENV:{env_path}") - if args.install: install_packages(env_path) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index 4d9b551798e1..1f31abc5cc87 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -51,7 +51,7 @@ def file_exists(path: Union[str, pathlib.PurePath]) -> bool: def venv_exists(name: str) -> bool: - return os.path.exists(CWD / name) + return os.path.exists(CWD / name) and file_exists(get_venv_path(name)) def run_process(args: Sequence[str], error_message: str) -> None: @@ -72,9 +72,6 @@ def get_venv_path(name: str) -> str: def install_packages(venv_path: str) -> None: - if not is_installed("pip"): - raise VenvError("CREATE_VENV.PIP_NOT_FOUND") - requirements = os.fspath(CWD / "requirements.txt") pyproject = os.fspath(CWD / "pyproject.toml") @@ -89,12 +86,14 @@ def install_packages(venv_path: str) -> None: [venv_path, "-m", "pip", "install", "-r", requirements], "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", ) + print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS") elif file_exists(pyproject): print(f"VENV_INSTALLING_PYPROJECT: {pyproject}") run_process( [venv_path, "-m", "pip", "install", "-e", ".[extras]"], "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT", ) + print("CREATE_VENV.PIP_INSTALLED_PYPROJECT") def add_gitignore(name: str) -> None: @@ -110,20 +109,27 @@ def main(argv: Optional[Sequence[str]] = None) -> None: argv = [] args = parse_args(argv) - if is_installed("venv"): - if not venv_exists(args.name): - run_process( - [sys.executable, "-m", "venv", args.name], - "CREATE_VENV.VENV_FAILED_CREATION", - ) - if args.git_ignore: - add_gitignore(args.name) + if not is_installed("venv"): + raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + + if args.install and not is_installed("pip"): + raise VenvError("CREATE_VENV.PIP_NOT_FOUND") + + if venv_exists(args.name): venv_path = get_venv_path(args.name) - print(f"CREATED_VENV:{venv_path}") - if args.install: - install_packages(venv_path) + print(f"EXISTING_VENV:{venv_path}") else: - raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + run_process( + [sys.executable, "-m", "venv", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) + venv_path = get_venv_path(args.name) + print(f"CREATED_VENV:{venv_path}") + if args.git_ignore: + add_gitignore(args.name) + + if args.install: + install_packages(venv_path) if __name__ == "__main__": diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 0ce9d5420c32..e1e2f8d71184 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -552,10 +552,12 @@ export namespace SwitchToDefaultLS { } export namespace CreateEnv { + export const informEnvCreation = localize( + 'createEnv.informEnvCreation', + 'We have selected the following environment:', + ); export const statusTitle = localize('createEnv.statusTitle', 'Creating environment'); export const statusStarting = localize('createEnv.statusStarting', 'Starting...'); - export const statusError = localize('createEnv.statusError', 'Error.'); - export const statusDone = localize('createEnv.statusDone', 'Done.'); export const hasVirtualEnv = localize('createEnv.hasVirtualEnv', 'Workspace folder contains a virtual environment'); @@ -564,21 +566,23 @@ export namespace CreateEnv { 'Please open a directory when creating an environment using venv.', ); - export const pickWorkspaceTitle = localize( - 'createEnv.workspaceQuickPick.title', + export const pickWorkspacePlaceholder = localize( + 'createEnv.workspaceQuickPick.placeholder', 'Select a workspace to create environment', ); - export const providersQuickPickTitle = localize('createEnv.providersQuickPick.title', 'Select an environment type'); + export const providersQuickPickPlaceholder = localize( + 'createEnv.providersQuickPick.placeholder', + 'Select an environment type', + ); export namespace Venv { export const creating = localize('createEnv.venv.creating', 'Creating venv...'); export const created = localize('createEnv.venv.created', 'Environment created...'); export const installingPackages = localize('createEnv.venv.installingPackages', 'Installing packages...'); - export const waitingForPython = localize('createEnv.venv.waitingForPython', 'Waiting on Python selection...'); - export const waitingForWorkspace = localize( - 'createEnv.venv.waitingForWorkspace', - 'Waiting on workspace selection...', + export const errorCreatingEnvironment = localize( + 'createEnv.venv.errorCreatingEnvironment', + 'Error while creating virtual environment.', ); export const selectPythonQuickPickTitle = localize( 'createEnv.venv.basePython.title', @@ -588,6 +592,7 @@ export namespace CreateEnv { 'createEnv.venv.description', 'Creates a `.venv` virtual environment in the current workspace', ); + export const error = localize('createEnv.venv.error', 'Creating virtual environment failed with error.'); } export namespace Conda { @@ -601,20 +606,11 @@ export namespace CreateEnv { 'createEnv.conda.errorCreatingEnvironment', 'Error while creating conda environment.', ); - export const waitingForWorkspace = localize( - 'createEnv.conda.waitingForWorkspace', - 'Waiting on workspace selection...', - ); - export const waitingForPython = localize( - 'createEnv.conda.waitingForPython', - 'Waiting on Python version selection...', - ); - export const selectPythonQuickPickTitle = localize( - 'createEnv.conda.pythonSelection.title', + export const selectPythonQuickPickPlaceholder = localize( + 'createEnv.conda.pythonSelection.placeholder', 'Please select the version of Python to install in the environment', ); - export const searching = localize('createEnv.conda.searching', 'Searching for conda (base)...'); - export const creating = localize('createEnv.venv.creating', 'Running conda create...'); + export const creating = localize('createEnv.conda.creating', 'Creating conda environment...'); export const providerDescription = localize( 'createEnv.conda.description', 'Creates a `.conda` Conda environment in the current workspace', diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 7def82abc752..07a4b4c4acc6 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -38,6 +38,23 @@ export function showErrorMessage(message: string, ...items: any[]): Thenable< return window.showErrorMessage(message, ...items); } +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showInformationMessage(message: string, ...items: any[]): Thenable { + return window.showInformationMessage(message, ...items); +} + export function withProgress( options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 3d2e026d7da4..d479fe49f944 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -21,7 +21,13 @@ import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './c import { Commands, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IExtensions, IOutputChannel } from './common/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExtensions, + IInterpreterPathService, + IOutputChannel, +} from './common/types'; import { noop } from './common/utils/misc'; import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; @@ -97,11 +103,14 @@ export async function activateComponents( return Promise.all([legacyActivationResult, ...promises]); } -export function activateFeatures(ext: ExtensionState, components: Components): void { +export function activateFeatures(ext: ExtensionState, _components: Components): void { const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( IInterpreterQuickPick, ); - registerCreateEnvironmentFeatures(ext.disposables, components.pythonEnvs, interpreterQuickPick); + const interpreterPathService: IInterpreterPathService = ext.legacyIOC.serviceContainer.get( + IInterpreterPathService, + ); + registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService); } /// ////////////////////////// diff --git a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts index 889e0205747b..7ed18c0e8b2a 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -6,9 +6,9 @@ import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell } from '../../common/application/types'; import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; -import { sleep } from '../../common/utils/async'; import { Common, Interpreters } from '../../common/utils/localize'; import { traceDecoratorError, traceVerbose } from '../../logging'; +import { isCreatingEnvironment } from '../../pythonEnvironments/creation/createEnvApi'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -38,8 +38,9 @@ export class VirtualEnvironmentPrompt implements IExtensionActivationService { @traceDecoratorError('Error in event handler for detection of new environment') protected async handleNewEnvironment(resource: Uri): Promise { - // Wait for a while, to ensure environment gets created and is accessible (as this is slow on Windows) - await sleep(1000); + if (isCreatingEnvironment()) { + return; + } const interpreters = await this.pyenvs.getWorkspaceVirtualEnvInterpreters(resource); const interpreter = Array.isArray(interpreters) && interpreters.length > 0 diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index 6aaee99e1f36..b2dc97882e23 100644 --- a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -51,7 +51,7 @@ export async function pickWorkspaceFolder( const selected = await showQuickPick( getWorkspacesForQuickPick(workspaces), { - title: CreateEnv.pickWorkspaceTitle, + placeHolder: CreateEnv.pickWorkspacePlaceholder, ignoreFocusOut: true, canPickMany: options?.allowMultiSelect, }, diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index 2546a858ced9..76263dd9315a 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -1,16 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable } from 'vscode'; +import { ConfigurationTarget, Disposable } from 'vscode'; import { Commands } from '../../common/constants'; -import { IDisposableRegistry } from '../../common/types'; +import { IDisposableRegistry, IInterpreterPathService } from '../../common/types'; import { registerCommand } from '../../common/vscodeApis/commandApis'; import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; -import { IDiscoveryAPI } from '../base/locator'; -import { handleCreateEnvironmentCommand } from './createEnvQuickPick'; +import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; import { VenvCreationProvider } from './provider/venvCreationProvider'; -import { CreateEnvironmentOptions, CreateEnvironmentProvider } from './types'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; class CreateEnvironmentProviders { private _createEnvProviders: CreateEnvironmentProvider[] = []; @@ -41,20 +42,30 @@ export function registerCreateEnvironmentProvider(provider: CreateEnvironmentPro }); } +export const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreatingEnvironment } = getCreationEvents(); + export function registerCreateEnvironmentFeatures( disposables: IDisposableRegistry, - discoveryApi: IDiscoveryAPI, interpreterQuickPick: IInterpreterQuickPick, + interpreterPathService: IInterpreterPathService, ): void { disposables.push( registerCommand( Commands.Create_Environment, - (options?: CreateEnvironmentOptions): Promise => { + (options?: CreateEnvironmentOptions): Promise => { const providers = _createEnvironmentProviders.getAll(); return handleCreateEnvironmentCommand(providers, options); }, ), ); - disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(discoveryApi, interpreterQuickPick))); + disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick))); disposables.push(registerCreateEnvironmentProvider(condaCreationProvider())); + disposables.push( + onCreateEnvironmentExited(async (e: CreateEnvironmentResult | undefined) => { + if (e && e.path) { + await interpreterPathService.update(e.uri, ConfigurationTarget.WorkspaceFolder, e.path); + showInformationMessage(`${CreateEnv.informEnvCreation} ${e.path}`); + } + }), + ); } diff --git a/src/client/pythonEnvironments/creation/createEnvQuickPick.ts b/src/client/pythonEnvironments/creation/createEnvQuickPick.ts deleted file mode 100644 index de71aa84cd06..000000000000 --- a/src/client/pythonEnvironments/creation/createEnvQuickPick.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License - -import { QuickPickItem } from 'vscode'; -import { CreateEnv } from '../../common/utils/localize'; -import { showQuickPick } from '../../common/vscodeApis/windowApis'; -import { traceError } from '../../logging'; -import { createEnvironment } from './createEnvironment'; -import { CreateEnvironmentOptions, CreateEnvironmentProvider } from './types'; - -interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { - id: string; -} - -async function showCreateEnvironmentQuickPick( - providers: readonly CreateEnvironmentProvider[], -): Promise { - const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ - label: p.name, - description: p.description, - id: p.id, - })); - const selected = await showQuickPick(items, { - title: CreateEnv.providersQuickPickTitle, - matchOnDescription: true, - ignoreFocusOut: true, - }); - - if (selected) { - const selections = providers.filter((p) => p.id === selected.id); - if (selections.length > 0) { - return selections[0]; - } - } - return undefined; -} - -export async function handleCreateEnvironmentCommand( - providers: readonly CreateEnvironmentProvider[], - options?: CreateEnvironmentOptions, -): Promise { - if (providers.length === 1) { - return createEnvironment(providers[0], options); - } - if (providers.length > 1) { - const provider = await showCreateEnvironmentQuickPick(providers); - if (provider) { - return createEnvironment(provider, options); - } - } else { - traceError('No Environment Creation providers were registered.'); - } - return undefined; -} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index a07555273604..7489da89f123 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -1,48 +1,101 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import { CancellationToken, ProgressLocation } from 'vscode'; -import { withProgress } from '../../common/vscodeApis/windowApis'; +import { Event, EventEmitter, QuickPickItem } from 'vscode'; +import { CreateEnv } from '../../common/utils/localize'; +import { showQuickPick } from '../../common/vscodeApis/windowApis'; import { traceError } from '../../logging'; -import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from './types'; -import { Common, CreateEnv } from '../../common/utils/localize'; -import { Commands } from '../../common/constants'; +import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; -export async function createEnvironment( +const onCreateEnvironmentStartedEvent = new EventEmitter(); +const onCreateEnvironmentExitedEvent = new EventEmitter(); + +let startedEventCount = 0; + +function isBusyCreatingEnvironment(): boolean { + return startedEventCount > 0; +} + +function fireStartedEvent(): void { + onCreateEnvironmentStartedEvent.fire(); + startedEventCount += 1; +} + +function fireExitedEvent(result: CreateEnvironmentResult | undefined): void { + onCreateEnvironmentExitedEvent.fire(result); + startedEventCount -= 1; +} + +export function getCreationEvents(): { + onCreateEnvironmentStarted: Event; + onCreateEnvironmentExited: Event; + isCreatingEnvironment: () => boolean; +} { + return { + onCreateEnvironmentStarted: onCreateEnvironmentStartedEvent.event, + onCreateEnvironmentExited: onCreateEnvironmentExitedEvent.event, + isCreatingEnvironment: isBusyCreatingEnvironment, + }; +} + +async function createEnvironment( provider: CreateEnvironmentProvider, options: CreateEnvironmentOptions = { ignoreSourceControl: true, installPackages: true, }, -): Promise { - return withProgress( - { - location: ProgressLocation.Notification, - title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, - cancellable: true, - }, - async (progress: CreateEnvironmentProgress, token: CancellationToken) => { - let hasError = false; - progress.report({ - message: CreateEnv.statusStarting, - }); - try { - const result = await provider.createEnvironment(options, progress, token); - return result; - } catch (ex) { - traceError(ex); - hasError = true; - progress.report({ - message: CreateEnv.statusError, - }); - throw ex; - } finally { - if (!hasError) { - progress.report({ - message: CreateEnv.statusDone, - }); - } - } - }, - ); +): Promise { + let result: CreateEnvironmentResult | undefined; + try { + fireStartedEvent(); + result = await provider.createEnvironment(options); + } finally { + fireExitedEvent(result); + } + return result; +} + +interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { + id: string; +} + +async function showCreateEnvironmentQuickPick( + providers: readonly CreateEnvironmentProvider[], +): Promise { + const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ + label: p.name, + description: p.description, + id: p.id, + })); + const selected = await showQuickPick(items, { + placeHolder: CreateEnv.providersQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selected) { + const selections = providers.filter((p) => p.id === selected.id); + if (selections.length > 0) { + return selections[0]; + } + } + return undefined; +} + +export async function handleCreateEnvironmentCommand( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions, +): Promise { + if (providers.length === 1) { + return createEnvironment(providers[0], options); + } + if (providers.length > 1) { + const provider = await showCreateEnvironmentQuickPick(providers); + if (provider) { + return createEnvironment(provider, options); + } + } else { + traceError('No Environment Creation providers were registered.'); + } + return undefined; } diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 38da9038c59a..3845a9ce8dad 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -1,22 +1,32 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, WorkspaceFolder } from 'vscode'; +import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; -import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { traceError, traceLog } from '../../../logging'; -import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from '../types'; +import { + CreateEnvironmentOptions, + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; -import { CreateEnv } from '../../../common/utils/localize'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { getConda, pickPythonVersion } from './condaUtils'; import { showErrorMessageWithLogs } from '../common/commonUtils'; - -export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; -export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; +import { withProgress } from '../../../common/vscodeApis/windowApis'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { + CondaProgressAndTelemetry, + CONDA_ENV_CREATED_MARKER, + CONDA_ENV_EXISTING_MARKER, +} from './condaProgressAndTelemetry'; function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -44,13 +54,33 @@ function generateCommandArgs(version?: string, options?: CreateEnvironmentOption return command; } +function getCondaEnvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER) || s.startsWith(CONDA_ENV_EXISTING_MARKER))[0]; + if (envPath.includes(CONDA_ENV_CREATED_MARKER)) { + return envPath.substring(CONDA_ENV_CREATED_MARKER.length); + } + return envPath.substring(CONDA_ENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + async function createCondaEnv( workspace: WorkspaceFolder, command: string, args: string[], - progress?: CreateEnvironmentProgress, + progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { + progress.report({ + message: CreateEnv.Conda.creating, + }); + const deferred = createDeferred(); let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; if (getOSType() === OSType.Windows) { @@ -78,35 +108,20 @@ async function createCondaEnv( }, }); + const progressAndTelemetry = new CondaProgressAndTelemetry(progress); let condaEnvPath: string | undefined; out.subscribe( (value) => { const output = value.out.splitLines().join('\r\n'); traceLog(output); - if (output.includes(CONDA_ENV_CREATED_MARKER)) { - progress?.report({ - message: CreateEnv.Conda.created, - }); - try { - const envPath = output - .split(/\r?\n/g) - .map((s) => s.trim()) - .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER))[0]; - condaEnvPath = envPath.substring(CONDA_ENV_CREATED_MARKER.length); - } catch (ex) { - traceError('Parsing out environment path failed.'); - condaEnvPath = undefined; - } - } else if (output.includes(CONDA_INSTALLING_YML)) { - progress?.report({ - message: CreateEnv.Conda.installingPackages, - }); + if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { + condaEnvPath = getCondaEnvFromOutput(output); } + progressAndTelemetry.process(output); }, async (error) => { traceError('Error while running conda env creation script: ', error); deferred.reject(error); - await showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); }, () => { dispose(); @@ -139,42 +154,65 @@ function getExecutableCommand(condaPath: string): string { return path.join(path.dirname(condaPath), 'python'); } -async function createEnvironment( - options?: CreateEnvironmentOptions, - progress?: CreateEnvironmentProgress, - token?: CancellationToken, -): Promise { - progress?.report({ - message: CreateEnv.Conda.searching, - }); +async function createEnvironment(options?: CreateEnvironmentOptions): Promise { const conda = await getConda(); if (!conda) { return undefined; } - progress?.report({ - message: CreateEnv.Conda.waitingForWorkspace, - }); - const workspace = (await pickWorkspaceFolder({ token })) as WorkspaceFolder | undefined; + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; if (!workspace) { traceError('Workspace was not selected or found for creating virtual env.'); return undefined; } - progress?.report({ - message: CreateEnv.Conda.waitingForPython, - }); - const version = await pickPythonVersion(token); + const version = await pickPythonVersion(); if (!version) { traceError('Conda environments for use with python extension require Python.'); return undefined; } - progress?.report({ - message: CreateEnv.Conda.creating, - }); - const args = generateCommandArgs(version, options); - return createCondaEnv(workspace, getExecutableCommand(conda), args, progress, token); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => { + let hasError = false; + + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'conda', + pythonVersion: version, + }); + envPath = await createCondaEnv( + workspace, + getExecutableCommand(conda), + generateCommandArgs(version, options), + progress, + token, + ); + } catch (ex) { + traceError(ex); + hasError = true; + throw ex; + } finally { + if (hasError) { + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + } + } + return { path: envPath, uri: workspace.uri }; + }, + ); } export function condaCreationProvider(): CreateEnvironmentProvider { diff --git a/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts new file mode 100644 index 000000000000..49707c8ae31e --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; +export const CONDA_ENV_EXISTING_MARKER = 'EXISTING_CONDA_ENV:'; +export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; +export const CREATE_CONDA_FAILED_MARKER = 'CREATE_CONDA.ENV_FAILED_CREATION'; +export const CREATE_CONDA_INSTALLED_YML = 'CREATE_CONDA.INSTALLED_YML'; +export const CREATE_FAILED_INSTALL_YML = 'CREATE_CONDA.FAILED_INSTALL_YML'; + +export class CondaProgressAndTelemetry { + private condaCreatedReported = false; + + private condaFailedReported = false; + + private condaInstallingPackagesReported = false; + + private condaInstallingPackagesFailedReported = false; + + private condaInstalledPackagesReported = false; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public process(output: string): void { + if (!this.condaCreatedReported && output.includes(CONDA_ENV_CREATED_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } else if (!this.condaCreatedReported && output.includes(CONDA_ENV_EXISTING_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'existing', + }); + } else if (!this.condaFailedReported && output.includes(CREATE_CONDA_FAILED_MARKER)) { + this.condaFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'conda', + reason: 'other', + }); + } else if (!this.condaInstallingPackagesReported && output.includes(CONDA_INSTALLING_YML)) { + this.condaInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Conda.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } else if (!this.condaInstallingPackagesFailedReported && output.includes(CREATE_FAILED_INSTALL_YML)) { + this.condaInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } else if (!this.condaInstalledPackagesReported && output.includes(CREATE_CONDA_INSTALLED_YML)) { + this.condaInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } + } +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 9496d01a07fe..256bdb6b01fb 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -22,14 +22,14 @@ export async function getConda(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.7', '3.8', '3.9', '3.10'].map((v) => ({ + const items: QuickPickItem[] = ['3.10', '3.9', '3.8', '3.7'].map((v) => ({ label: `Python`, description: v, })); const version = await showQuickPick( items, { - title: CreateEnv.Conda.selectPythonQuickPickTitle, + placeHolder: CreateEnv.Conda.selectPythonQuickPickPlaceholder, }, token, ); diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index fbdc73f39258..8d9a677dc3f2 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -1,27 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, WorkspaceFolder } from 'vscode'; -import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import * as os from 'os'; +import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { CreateEnv } from '../../../common/utils/localize'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { traceError, traceLog } from '../../../logging'; -import { PythonEnvKind } from '../../base/info'; -import { IDiscoveryAPI } from '../../base/locator'; -import { CreateEnvironmentOptions, CreateEnvironmentProgress, CreateEnvironmentProvider } from '../types'; +import { + CreateEnvironmentOptions, + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; import { EnvironmentType, PythonEnvironment } from '../../info'; - -export const VENV_CREATED_MARKER = 'CREATED_VENV:'; -export const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; -export const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; -export const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; -export const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; -export const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; -export const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +import { withProgress } from '../../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; +import { showErrorMessageWithLogs } from '../common/commonUtils'; function generateCommandArgs(options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -44,16 +45,37 @@ function generateCommandArgs(options?: CreateEnvironmentOptions): string[] { return command; } +function getVenvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(VENV_CREATED_MARKER) || s.startsWith(VENV_EXISTING_MARKER))[0]; + if (envPath.includes(VENV_CREATED_MARKER)) { + return envPath.substring(VENV_CREATED_MARKER.length); + } + return envPath.substring(VENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + async function createVenv( workspace: WorkspaceFolder, command: string, args: string[], - progress?: CreateEnvironmentProgress, + progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { - progress?.report({ + progress.report({ message: CreateEnv.Venv.creating, }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'venv', + pythonVersion: undefined, + }); + const deferred = createDeferred(); traceLog('Running Env creation script: ', [command, ...args]); const { out, dispose } = execObservable(command, args, { @@ -62,30 +84,16 @@ async function createVenv( cwd: workspace.uri.fsPath, }); + const progressAndTelemetry = new VenvProgressAndTelemetry(progress); let venvPath: string | undefined; out.subscribe( (value) => { - const output = value.out.split(/\r?\n/g).join('\r\n'); + const output = value.out.split(/\r?\n/g).join(os.EOL); traceLog(output); - if (output.includes(VENV_CREATED_MARKER)) { - progress?.report({ - message: CreateEnv.Venv.created, - }); - try { - const envPath = output - .split(/\r?\n/g) - .map((s) => s.trim()) - .filter((s) => s.startsWith(VENV_CREATED_MARKER))[0]; - venvPath = envPath.substring(VENV_CREATED_MARKER.length); - } catch (ex) { - traceError('Parsing out environment path failed.'); - venvPath = undefined; - } - } else if (output.includes(INSTALLING_REQUIREMENTS) || output.includes(INSTALLING_PYPROJECT)) { - progress?.report({ - message: CreateEnv.Venv.installingPackages, - }); + if (output.includes(VENV_CREATED_MARKER) || output.includes(VENV_EXISTING_MARKER)) { + venvPath = getVenvFromOutput(output); } + progressAndTelemetry.process(output); }, (error) => { traceError('Error while running venv creation script: ', error); @@ -102,38 +110,15 @@ async function createVenv( } export class VenvCreationProvider implements CreateEnvironmentProvider { - constructor( - private readonly discoveryApi: IDiscoveryAPI, - private readonly interpreterQuickPick: IInterpreterQuickPick, - ) {} - - public async createEnvironment( - options?: CreateEnvironmentOptions, - progress?: CreateEnvironmentProgress, - token?: CancellationToken, - ): Promise { - progress?.report({ - message: CreateEnv.Venv.waitingForWorkspace, - }); - - const workspace = (await pickWorkspaceFolder({ token })) as WorkspaceFolder | undefined; + constructor(private readonly interpreterQuickPick: IInterpreterQuickPick) {} + + public async createEnvironment(options?: CreateEnvironmentOptions): Promise { + const workspace = (await pickWorkspaceFolder()) as WorkspaceFolder | undefined; if (workspace === undefined) { traceError('Workspace was not selected or found for creating virtual environment.'); return undefined; } - progress?.report({ - message: CreateEnv.Venv.waitingForPython, - }); - const interpreters = this.discoveryApi.getEnvs({ - kinds: [PythonEnvKind.MicrosoftStore, PythonEnvKind.OtherGlobal], - }); - - const args = generateCommandArgs(options); - if (interpreters.length === 1) { - return createVenv(workspace, interpreters[0].executable.filename, args, progress, token); - } - const interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( workspace.uri, (i: PythonEnvironment) => @@ -141,7 +126,46 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { ); if (interpreter) { - return createVenv(workspace, interpreter, args, progress, token); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => { + let hasError = false; + + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter) { + envPath = await createVenv( + workspace, + interpreter, + generateCommandArgs(options), + progress, + token, + ); + } + } catch (ex) { + traceError(ex); + hasError = true; + throw ex; + } finally { + if (hasError) { + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + } + } + + return { path: envPath, uri: workspace.uri }; + }, + ); } traceError('Virtual env creation requires an interpreter.'); diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts new file mode 100644 index 000000000000..423fb16b3110 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const VENV_CREATED_MARKER = 'CREATED_VENV:'; +export const VENV_EXISTING_MARKER = 'EXISTING_VENV:'; +export const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +export const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +export const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +export const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +export const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +export const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +export const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; +export const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; +export const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; +export const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; +export const PIP_UPGRADE_FAILED_MARKER = 'CREATE_VENV.PIP_UPGRADE_FAILED'; + +export class VenvProgressAndTelemetry { + private venvCreatedReported = false; + + private venvOrPipMissingReported = false; + + private venvFailedReported = false; + + private venvInstallingPackagesReported = false; + + private venvInstallingPackagesFailedReported = false; + + private venvInstalledPackagesReported = false; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public process(output: string): void { + if (!this.venvCreatedReported && output.includes(VENV_CREATED_MARKER)) { + this.venvCreatedReported = true; + this.progress.report({ + message: CreateEnv.Venv.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + } else if (!this.venvCreatedReported && output.includes(VENV_EXISTING_MARKER)) { + this.venvCreatedReported = true; + this.progress.report({ + message: CreateEnv.Venv.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + } else if (!this.venvOrPipMissingReported && output.includes(VENV_NOT_INSTALLED_MARKER)) { + this.venvOrPipMissingReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noVenv', + }); + } else if (!this.venvOrPipMissingReported && output.includes(PIP_NOT_INSTALLED_MARKER)) { + this.venvOrPipMissingReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noPip', + }); + } else if (!this.venvFailedReported && output.includes(CREATE_VENV_FAILED_MARKER)) { + this.venvFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'other', + }); + } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_REQUIREMENTS)) { + this.venvInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Venv.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_PYPROJECT)) { + this.venvInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Venv.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + } else if (!this.venvInstallingPackagesFailedReported && output.includes(PIP_UPGRADE_FAILED_MARKER)) { + this.venvInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_REQUIREMENTS_FAILED_MARKER)) { + this.venvInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_PYPROJECT_FAILED_MARKER)) { + this.venvInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_REQUIREMENTS_MARKER)) { + this.venvInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_PYPROJECT_MARKER)) { + this.venvInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + } + } +} diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts index 9e9a31799d09..6c844b8cfd02 100644 --- a/src/client/pythonEnvironments/creation/types.ts +++ b/src/client/pythonEnvironments/creation/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import { CancellationToken, Progress } from 'vscode'; +import { Progress, Uri } from 'vscode'; export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} @@ -10,12 +10,13 @@ export interface CreateEnvironmentOptions { ignoreSourceControl?: boolean; } +export interface CreateEnvironmentResult { + path: string | undefined; + uri: Uri | undefined; +} + export interface CreateEnvironmentProvider { - createEnvironment( - options?: CreateEnvironmentOptions, - progress?: CreateEnvironmentProgress, - token?: CancellationToken, - ): Promise; + createEnvironment(options?: CreateEnvironmentOptions): Promise; name: string; description: string; id: string; diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 4a611fcf3e7f..7f6f8b9c58a3 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -100,6 +100,13 @@ export enum EventName { TENSORBOARD_TORCH_PROFILER_IMPORT = 'TENSORBOARD.TORCH_PROFILER_IMPORT', TENSORBOARD_JUMP_TO_SOURCE_REQUEST = 'TENSORBOARD_JUMP_TO_SOURCE_REQUEST', TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND = 'TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND', + + ENVIRONMENT_CREATING = 'ENVIRONMENT.CREATING', + ENVIRONMENT_CREATED = 'ENVIRONMENT.CREATED', + ENVIRONMENT_FAILED = 'ENVIRONMENT.FAILED', + ENVIRONMENT_INSTALLING_PACKAGES = 'ENVIRONMENT.INSTALLING_PACKAGES', + ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', + ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', } export enum PlatformErrors { diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 1d1b2e076c13..4e4e9ba39649 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1974,18 +1974,97 @@ export interface IEventNamePropertyMapping { "tensorboard_jump_to_source_file_not_found" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND]: never | undefined; + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; + /** + * Telemetry event sent before creating an environment. + */ /* __GDPR__ - "query-expfeature" : { - "owner": "luabud", - "comment": "Logs queries to the experiment service by feature for metric calculations", - "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } - } - */ - /* __GDPR__ - "call-tas-error" : { - "owner": "luabud", - "comment": "Logs when calls to the experiment service fails", - "errortype": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Type of error when calling TAS (ServerError, NoResponse, etc.)"} - } - */ + "environment.creating" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "pythonVersion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CREATING]: { + environmentType: 'venv' | 'conda'; + pythonVersion: string | undefined; + }; + /** + * Telemetry event sent after creating an environment, but before attempting package installation. + */ + /* __GDPR__ + "environment.created" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CREATED]: { + environmentType: 'venv' | 'conda'; + reason: 'created' | 'existing'; + }; + /** + * Telemetry event sent if creating an environment failed. + */ + /* __GDPR__ + "environment.failed" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_FAILED]: { + environmentType: 'venv' | 'conda'; + reason: 'noVenv' | 'noPip' | 'other'; + }; + /** + * Telemetry event sent before installing packages. + */ + /* __GDPR__ + "environment.installing_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { + environmentType: 'venv' | 'conda'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + }; + /** + * Telemetry event sent after installing packages. + */ + /* __GDPR__ + "environment.installed_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLED_PACKAGES]: { + environmentType: 'venv' | 'conda'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + }; + /** + * Telemetry event sent if installing packages failed. + */ + /* __GDPR__ + "environment.installing_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED]: { + environmentType: 'venv' | 'conda'; + using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + }; + /* __GDPR__ + "query-expfeature" : { + "owner": "luabud", + "comment": "Logs queries to the experiment service by feature for metric calculations", + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } + } + */ + /* __GDPR__ + "call-tas-error" : { + "owner": "luabud", + "comment": "Logs when calls to the experiment service fails", + "errortype": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Type of error when calling TAS (ServerError, NoResponse, etc.)"} + } + */ } diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index 129776cc6a15..9671e393dc43 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -3,8 +3,9 @@ 'use strict'; -import { anything, deepEqual, instance, mock, reset, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; +import { anything, deepEqual, instance, mock, reset, verify, when } from 'ts-mockito'; import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { ApplicationShell } from '../../../client/common/application/applicationShell'; import { IApplicationShell } from '../../../client/common/application/types'; @@ -17,6 +18,7 @@ import { IComponentAdapter, IInterpreterHelper, IInterpreterService } from '../. import { InterpreterHelper } from '../../../client/interpreter/helpers'; import { VirtualEnvironmentPrompt } from '../../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as createEnvApi from '../../../client/pythonEnvironments/creation/createEnvApi'; suite('Virtual Environment Prompt', () => { class VirtualEnvironmentPromptTest extends VirtualEnvironmentPrompt { @@ -36,12 +38,15 @@ suite('Virtual Environment Prompt', () => { let componentAdapter: IComponentAdapter; let interpreterService: IInterpreterService; let environmentPrompt: VirtualEnvironmentPromptTest; + let isCreatingEnvironmentStub: sinon.SinonStub; setup(() => { persistentStateFactory = mock(PersistentStateFactory); helper = mock(InterpreterHelper); pythonPathUpdaterService = mock(PythonPathUpdaterService); componentAdapter = mock(); interpreterService = mock(); + isCreatingEnvironmentStub = sinon.stub(createEnvApi, 'isCreatingEnvironment'); + isCreatingEnvironmentStub.returns(false); when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ id: 'selected', path: 'path/to/selected', @@ -59,6 +64,10 @@ suite('Virtual Environment Prompt', () => { ); }); + teardown(() => { + sinon.restore(); + }); + test('User is notified if interpreter exists and only python path to global interpreter is specified in settings', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; @@ -256,4 +265,17 @@ suite('Virtual Environment Prompt', () => { verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); verify(appShell.showInformationMessage(anything(), ...prompts)).never(); }); + + test('If environment is being created, no notification is shown', async () => { + isCreatingEnvironmentStub.reset(); + isCreatingEnvironmentStub.returns(true); + + const resource = Uri.file('a'); + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + + await environmentPrompt.handleNewEnvironment(resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).never(); + verify(appShell.showInformationMessage(anything(), ...prompts)).never(); + }); }); diff --git a/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts deleted file mode 100644 index 165dff8c6b2b..000000000000 --- a/src/test/pythonEnvironments/creation/createEnvQuickPick.unit.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as typemoq from 'typemoq'; -import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import * as createEnv from '../../../client/pythonEnvironments/creation/createEnvironment'; -import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvQuickPick'; -import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; - -suite('Create Environment Command Handler Tests', () => { - let showQuickPickStub: sinon.SinonStub; - let createEnvironmentStub: sinon.SinonStub; - - setup(() => { - showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); - createEnvironmentStub = sinon.stub(createEnv, 'createEnvironment'); - }); - - teardown(() => { - sinon.restore(); - }); - - test('No providers registered', async () => { - await handleCreateEnvironmentCommand([]); - - assert.isTrue(showQuickPickStub.notCalled); - assert.isTrue(createEnvironmentStub.notCalled); - }); - - test('Single environment creation provider registered', async () => { - const provider = typemoq.Mock.ofType(); - provider.setup((p) => p.name).returns(() => 'test'); - provider.setup((p) => p.id).returns(() => 'test-id'); - provider.setup((p) => p.description).returns(() => 'test-description'); - - await handleCreateEnvironmentCommand([provider.object]); - - assert.isTrue(showQuickPickStub.notCalled); - createEnvironmentStub.calledOnceWithExactly(provider.object, undefined); - }); - - test('Multiple environment creation providers registered', async () => { - const provider1 = typemoq.Mock.ofType(); - provider1.setup((p) => p.name).returns(() => 'test1'); - provider1.setup((p) => p.id).returns(() => 'test-id1'); - provider1.setup((p) => p.description).returns(() => 'test-description1'); - - const provider2 = typemoq.Mock.ofType(); - provider2.setup((p) => p.name).returns(() => 'test2'); - provider2.setup((p) => p.id).returns(() => 'test-id2'); - provider2.setup((p) => p.description).returns(() => 'test-description2'); - - showQuickPickStub.resolves({ - id: 'test-id2', - label: 'test2', - description: 'test-description2', - }); - - provider1.setup((p) => (p as any).then).returns(() => undefined); - provider2.setup((p) => (p as any).then).returns(() => undefined); - await handleCreateEnvironmentCommand([provider1.object, provider2.object]); - - assert.isTrue(showQuickPickStub.calledOnce); - createEnvironmentStub.calledOnceWithExactly(provider2.object, undefined); - }); -}); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts index 0e94e81ab38a..9c8e1af42b9a 100644 --- a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -1,71 +1,126 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { ProgressLocation, ProgressOptions } from 'vscode'; -import { Common, CreateEnv } from '../../../client/common/utils/localize'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import { createEnvironment } from '../../../client/pythonEnvironments/creation/createEnvironment'; -import { - CreateEnvironmentProgress, - CreateEnvironmentProvider, -} from '../../../client/pythonEnvironments/creation/types'; -import { Commands } from '../../../client/common/constants'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { onCreateEnvironmentStarted } from '../../../client/pythonEnvironments/creation/createEnvApi'; chaiUse(chaiAsPromised); suite('Create Environments Tests', () => { - let withProgressStub: sinon.SinonStub; - let progressMock: typemoq.IMock; + let showQuickPickStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let startedEventTriggered = false; + let exitedEventTriggered = false; setup(() => { - progressMock = typemoq.Mock.ofType(); - withProgressStub = sinon.stub(windowApis, 'withProgress'); - withProgressStub.callsFake(async (options: ProgressOptions, task) => { - assert.deepEqual(options, { - location: ProgressLocation.Notification, - title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, - cancellable: true, - }); - - await task(progressMock.object, undefined); - }); + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + startedEventTriggered = false; + exitedEventTriggered = false; + disposables.push( + onCreateEnvironmentStarted(() => { + startedEventTriggered = true; + }), + ); + disposables.push( + onCreateEnvironmentStarted(() => { + exitedEventTriggered = true; + }), + ); }); teardown(() => { - progressMock.reset(); sinon.restore(); + disposables.forEach((d) => d.dispose()); }); test('Successful environment creation', async () => { const provider = typemoq.Mock.ofType(); - provider - .setup((p) => p.createEnvironment(typemoq.It.isAny(), progressMock.object, undefined)) - .returns(() => Promise.resolve(undefined)); - progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusDone })).verifiable(typemoq.Times.once()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusError })).verifiable(typemoq.Times.never()); - await createEnvironment(provider.object); - - progressMock.verifyAll(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); provider.verifyAll(); }); test('Environment creation error', async () => { const provider = typemoq.Mock.ofType(); - provider - .setup((p) => p.createEnvironment(typemoq.It.isAny(), progressMock.object, undefined)) - .returns(() => Promise.reject()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusDone })).verifiable(typemoq.Times.never()); - progressMock.setup((p) => p.report({ message: CreateEnv.statusError })).verifiable(typemoq.Times.once()); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.reject()); + provider.setup((p) => (p as any).then).returns(() => undefined); - await assert.isRejected(createEnvironment(provider.object)); + await assert.isRejected(handleCreateEnvironmentCommand([provider.object])); - progressMock.verifyAll(); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); provider.verifyAll(); }); + + test('No providers registered', async () => { + await handleCreateEnvironmentCommand([]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isFalse(startedEventTriggered); + assert.isFalse(exitedEventTriggered); + }); + + test('Single environment creation provider registered', async () => { + const provider = typemoq.Mock.ofType(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Multiple environment creation providers registered', async () => { + const provider1 = typemoq.Mock.ofType(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); }); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index fffcd2511a15..65b5affcbf8f 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -5,29 +5,35 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import { assert, use as chaiUse } from 'chai'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { CreateEnvironmentProvider } from '../../../../client/pythonEnvironments/creation/types'; +import * as typemoq from 'typemoq'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; import { - condaCreationProvider, - CONDA_ENV_CREATED_MARKER, -} from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/types'; +import { condaCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as condaUtils from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; import { Output } from '../../../../client/common/process/types'; import { createDeferred } from '../../../../client/common/utils/async'; import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { CONDA_ENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/condaProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; chaiUse(chaiAsPromised); suite('Conda Creation provider tests', () => { let condaProvider: CreateEnvironmentProvider; + let progressMock: typemoq.IMock; let getCondaStub: sinon.SinonStub; let pickPythonVersionStub: sinon.SinonStub; let pickWorkspaceFolderStub: sinon.SinonStub; let execObservableStub: sinon.SinonStub; - + let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; setup(() => { @@ -35,9 +41,12 @@ suite('Conda Creation provider tests', () => { getCondaStub = sinon.stub(condaUtils, 'getConda'); pickPythonVersionStub = sinon.stub(condaUtils, 'pickPythonVersion'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -72,11 +81,12 @@ suite('Conda Creation provider tests', () => { test('Create conda environment', async () => { getCondaStub.resolves('/usr/bin/conda/conda_bin/conda'); - pickWorkspaceFolderStub.resolves({ + const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', index: 0, - }); + }; + pickWorkspaceFolderStub.resolves(workspace1); pickPythonVersionStub.resolves('3.10'); const deferred = createDeferred(); @@ -100,6 +110,18 @@ suite('Conda Creation provider tests', () => { }; }); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + const promise = condaProvider.createEnvironment(); await deferred.promise; assert.isDefined(_next); @@ -107,7 +129,8 @@ suite('Conda Creation provider tests', () => { _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - assert.strictEqual(await promise, 'new_environment'); + assert.deepStrictEqual(await promise, { path: 'new_environment', uri: workspace1.uri }); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); test('Create conda environment failed', async () => { @@ -140,11 +163,24 @@ suite('Conda Creation provider tests', () => { }; }); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + const promise = condaProvider.createEnvironment(); await deferred.promise; assert.isDefined(_error); _error!('bad arguments'); _complete!(); await assert.isRejected(promise); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index e30c86e78a4f..1fb959f228ea 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -5,80 +5,46 @@ import * as path from 'path'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import * as sinon from 'sinon'; -import { Uri } from 'vscode'; -import { CreateEnvironmentProvider } from '../../../../client/pythonEnvironments/creation/types'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; import { - VenvCreationProvider, - VENV_CREATED_MARKER, -} from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; -import { IDiscoveryAPI } from '../../../../client/pythonEnvironments/base/locator'; + CreateEnvironmentProgress, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/types'; +import { VenvCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; import { IInterpreterQuickPick } from '../../../../client/interpreter/configuration/types'; import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; -import { PythonEnvKind, PythonEnvSource } from '../../../../client/pythonEnvironments/base/info'; -import { Architecture } from '../../../../client/common/utils/platform'; import { createDeferred } from '../../../../client/common/utils/async'; import { Output } from '../../../../client/common/process/types'; +import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; chaiUse(chaiAsPromised); -const python37 = { - name: 'Python 3.7', - kind: PythonEnvKind.System, - location: '/usr/bin/python3.7', - source: [PythonEnvSource.PathEnvVar], - executable: { - filename: '/usr/bin/python3.7', - ctime: 0, - mtime: 0, - sysPrefix: '', - }, - version: { - major: 3, - minor: 7, - micro: 7, - }, - arch: Architecture.x64, - distro: { - org: 'python', - }, -}; -const python38 = { - name: 'Python 3.8', - kind: PythonEnvKind.System, - location: '/usr/bin/python3.8', - source: [PythonEnvSource.PathEnvVar], - executable: { - filename: '/usr/bin/python3.8', - ctime: 0, - mtime: 0, - sysPrefix: '', - }, - version: { - major: 3, - minor: 8, - micro: 8, - }, - arch: Architecture.x64, - distro: { - org: 'python', - }, -}; - suite('venv Creation provider tests', () => { let venvProvider: CreateEnvironmentProvider; let pickWorkspaceFolderStub: sinon.SinonStub; - let discoveryApi: typemoq.IMock; let interpreterQuickPick: typemoq.IMock; + let progressMock: typemoq.IMock; let execObservableStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); - discoveryApi = typemoq.Mock.ofType(); interpreterQuickPick = typemoq.Mock.ofType(); - venvProvider = new VenvCreationProvider(discoveryApi.object, interpreterQuickPick.object); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + progressMock = typemoq.Mock.ofType(); + venvProvider = new VenvCreationProvider(interpreterQuickPick.object); }); teardown(() => { @@ -99,39 +65,27 @@ suite('venv Creation provider tests', () => { index: 0, }); - // Return multiple envs here to force user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python37, python38]) - .verifiable(typemoq.Times.once()); - interpreterQuickPick .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(typemoq.Times.once()); assert.isUndefined(await venvProvider.createEnvironment()); - discoveryApi.verifyAll(); interpreterQuickPick.verifyAll(); }); - test('Create venv with single global python', async () => { - pickWorkspaceFolderStub.resolves({ + test('Create venv with python selected by user', async () => { + const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', index: 0, - }); - - // Return single env here to skip user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python38]) - .verifiable(typemoq.Times.once()); + }; + pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.never()); + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); const deferred = createDeferred(); let _next: undefined | ((value: Output) => void); @@ -154,56 +108,17 @@ suite('venv Creation provider tests', () => { }; }); - const promise = venvProvider.createEnvironment(); - await deferred.promise; - assert.isDefined(_next); - assert.isDefined(_complete); - - _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); - _complete!(); - assert.strictEqual(await promise, 'new_environment'); - discoveryApi.verifyAll(); - interpreterQuickPick.verifyAll(); - }); - - test('Create venv with multiple global python', async () => { - pickWorkspaceFolderStub.resolves({ - uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), - name: 'workspace1', - index: 0, - }); - - // Return single env here to skip user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python37, python38]) - .verifiable(typemoq.Times.once()); - - interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(python38.executable.filename)) - .verifiable(typemoq.Times.once()); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); - const deferred = createDeferred(); - let _next: undefined | ((value: Output) => void); - let _complete: undefined | (() => void); - execObservableStub.callsFake(() => { - deferred.resolve(); - return { - proc: undefined, - out: { - subscribe: ( - next?: (value: Output) => void, - _error?: (error: unknown) => void, - complete?: () => void, - ) => { - _next = next; - _complete = complete; - }, - }, - dispose: () => undefined, - }; - }); + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); const promise = venvProvider.createEnvironment(); await deferred.promise; @@ -212,9 +127,12 @@ suite('venv Creation provider tests', () => { _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - assert.strictEqual(await promise, 'new_environment'); - discoveryApi.verifyAll(); + + const actual = await promise; + assert.deepStrictEqual(actual, { path: 'new_environment', uri: workspace1.uri }); interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); test('Create venv failed', async () => { @@ -224,16 +142,10 @@ suite('venv Creation provider tests', () => { index: 0, }); - // Return single env here to skip user selection. - discoveryApi - .setup((d) => d.getEnvs(typemoq.It.isAny())) - .returns(() => [python38]) - .verifiable(typemoq.Times.once()); - interpreterQuickPick - .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.never()); + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); const deferred = createDeferred(); let _error: undefined | ((error: unknown) => void); @@ -256,11 +168,24 @@ suite('venv Creation provider tests', () => { }; }); + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + const promise = venvProvider.createEnvironment(); await deferred.promise; assert.isDefined(_error); _error!('bad arguments'); _complete!(); await assert.isRejected(promise); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); });