From e36b27c58993420c0eb5f902f9bea2cd7803399e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Apr 2023 16:25:23 -0700 Subject: [PATCH] Add `microvenv` support for non-windows platforms (#20985) This PR contains: 1. `microvenv` fallback if `venv` is not available (implemented in python with tests) 2. Updates to telemetry to include microvenv. Closes https://github.com/microsoft/vscode-python/issues/20905 --- pythonFiles/create_conda.py | 2 +- pythonFiles/create_microvenv.py | 97 ++++ pythonFiles/create_venv.py | 32 +- pythonFiles/tests/test_create_microvenv.py | 60 +++ pythonFiles/tests/test_create_venv.py | 35 +- requirements.in | 3 + requirements.txt | 8 +- src/client/common/utils/localize.ts | 4 + .../provider/venvProgressAndTelemetry.ts | 415 ++++++++++++------ src/client/telemetry/index.ts | 14 +- .../venvProgressAndTelemetry.unit.test.ts | 54 +++ 11 files changed, 570 insertions(+), 154 deletions(-) create mode 100644 pythonFiles/create_microvenv.py create mode 100644 pythonFiles/tests/test_create_microvenv.py create mode 100644 src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts diff --git a/pythonFiles/create_conda.py b/pythonFiles/create_conda.py index 9a34de47d51f..15320a8a1ce6 100644 --- a/pythonFiles/create_conda.py +++ b/pythonFiles/create_conda.py @@ -9,7 +9,7 @@ from typing import Optional, Sequence, Union CONDA_ENV_NAME = ".conda" -CWD = pathlib.PurePath(os.getcwd()) +CWD = pathlib.Path.cwd() class VenvError(Exception): diff --git a/pythonFiles/create_microvenv.py b/pythonFiles/create_microvenv.py new file mode 100644 index 000000000000..3dfd554a3012 --- /dev/null +++ b/pythonFiles/create_microvenv.py @@ -0,0 +1,97 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import os +import pathlib +import subprocess +import sys +import urllib.request as url_lib +from typing import Optional, Sequence + +VENV_NAME = ".venv" +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +CWD = pathlib.Path.cwd() + + +class MicroVenvError(Exception): + pass + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) + except subprocess.CalledProcessError: + raise MicroVenvError(error_message) + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + + parser.add_argument( + "--install-pip", + action="store_true", + default=False, + help="Install pip into the virtual environment.", + ) + + parser.add_argument( + "--name", + default=VENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + return parser.parse_args(argv) + + +def create_microvenv(name: str): + run_process( + [sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name], + "CREATE_MICROVENV.MICROVENV_FAILED_CREATION", + ) + + +def download_pip_pyz(name: str): + url = "https://bootstrap.pypa.io/pip/pip.pyz" + print("CREATE_MICROVENV.DOWNLOADING_PIP") + + try: + with url_lib.urlopen(url) as response: + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + with open(pip_pyz_path, "wb") as out_file: + data = response.read() + out_file.write(data) + out_file.flush() + except Exception: + raise MicroVenvError("CREATE_MICROVENV.DOWNLOAD_PIP_FAILED") + + +def install_pip(name: str): + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + executable = os.fspath(CWD / name / "bin" / "python") + print("CREATE_MICROVENV.INSTALLING_PIP") + run_process( + [executable, pip_pyz_path, "install", "pip"], + "CREATE_MICROVENV.INSTALL_PIP_FAILED", + ) + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + print("CREATE_MICROVENV.CREATING_MICROVENV") + create_microvenv(args.name) + print("CREATE_MICROVENV.CREATED_MICROVENV") + + if args.install_pip: + download_pip_pyz(args.name) + install_pip(args.name) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index 2a2768c993ae..8ebafdfca32c 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -10,7 +10,8 @@ from typing import List, Optional, Sequence, Union VENV_NAME = ".venv" -CWD = pathlib.PurePath(os.getcwd()) +CWD = pathlib.Path.cwd() +MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py" class VenvError(Exception): @@ -130,22 +131,39 @@ def main(argv: Optional[Sequence[str]] = None) -> None: argv = [] args = parse_args(argv) + use_micro_venv = False if not is_installed("venv"): - raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + if sys.platform == "win32": + raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + else: + use_micro_venv = True pip_installed = is_installed("pip") deps_needed = args.requirements or args.extras or args.toml - if deps_needed and not pip_installed: + if deps_needed and not pip_installed and not use_micro_venv: raise VenvError("CREATE_VENV.PIP_NOT_FOUND") if venv_exists(args.name): venv_path = get_venv_path(args.name) print(f"EXISTING_VENV:{venv_path}") else: - run_process( - [sys.executable, "-m", "venv", args.name], - "CREATE_VENV.VENV_FAILED_CREATION", - ) + if use_micro_venv: + run_process( + [ + sys.executable, + os.fspath(MICROVENV_SCRIPT_PATH), + "--install-pip", + "--name", + args.name, + ], + "CREATE_VENV.MICROVENV_FAILED_CREATION", + ) + pip_installed = True + else: + 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: diff --git a/pythonFiles/tests/test_create_microvenv.py b/pythonFiles/tests/test_create_microvenv.py new file mode 100644 index 000000000000..26a57dda26a1 --- /dev/null +++ b/pythonFiles/tests/test_create_microvenv.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import importlib +import os +import sys + +import create_microvenv +import pytest + + +def test_create_microvenv(): + importlib.reload(create_microvenv) + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + assert args == [ + sys.executable, + os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"), + create_microvenv.VENV_NAME, + ] + assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION" + + create_microvenv.run_process = run_process + + create_microvenv.main() + assert run_process_called == True + + +def test_create_microvenv_with_pip(): + importlib.reload(create_microvenv) + + download_pip_pyz_called = False + + def download_pip_pyz(name): + nonlocal download_pip_pyz_called + download_pip_pyz_called = True + assert name == create_microvenv.VENV_NAME + + create_microvenv.download_pip_pyz = download_pip_pyz + + run_process_called = False + + def run_process(args, error_message): + if "install" in args and "pip" in args: + nonlocal run_process_called + run_process_called = True + pip_pyz_path = os.fspath( + create_microvenv.CWD / create_microvenv.VENV_NAME / "pip.pyz" + ) + executable = os.fspath( + create_microvenv.CWD / create_microvenv.VENV_NAME / "bin" / "python" + ) + assert args == [executable, pip_pyz_path, "install", "pip"] + assert error_message == "CREATE_MICROVENV.INSTALL_PIP_FAILED" + + create_microvenv.run_process = run_process + create_microvenv.main(["--install-pip"]) diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index 95ec863373d8..2949a18ebd40 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -2,13 +2,46 @@ # Licensed under the MIT License. import importlib +import os import sys import create_venv import pytest -def test_venv_not_installed(): +@pytest.mark.skipif( + sys.platform == "win32", reason="Windows does not have micro venv fallback." +) +def test_venv_not_installed_unix(): + importlib.reload(create_venv) + create_venv.is_installed = lambda module: module != "venv" + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + if "--install-pip" in args: + run_process_called = True + assert args == [ + sys.executable, + os.fspath(create_venv.MICROVENV_SCRIPT_PATH), + "--install-pip", + "--name", + ".test_venv", + ] + assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION" + + create_venv.run_process = run_process + + create_venv.main(["--name", ".test_venv"]) + + # run_process is called when the venv does not exist + assert run_process_called == True + + +@pytest.mark.skipif( + sys.platform != "win32", reason="Windows does not have microvenv fallback." +) +def test_venv_not_installed_windows(): importlib.reload(create_venv) create_venv.is_installed = lambda module: module != "venv" with pytest.raises(create_venv.VenvError) as e: diff --git a/requirements.in b/requirements.in index c394c0feb0cf..8b76e392917e 100644 --- a/requirements.in +++ b/requirements.in @@ -5,3 +5,6 @@ # Unittest test adapter typing-extensions==4.5.0 + +# Fallback env creator for debian +microvenv diff --git a/requirements.txt b/requirements.txt index 1cdb049c430a..07145e1832d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,13 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # pip-compile --generate-hashes requirements.in # +microvenv==2023.2.0 \ + --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ + --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 + # via -r requirements.in typing-extensions==4.5.0 \ --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 509287d87533..10c70c8c6cd0 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -438,7 +438,11 @@ export namespace CreateEnv { export namespace Venv { export const creating = l10n.t('Creating venv...'); + export const creatingMicrovenv = l10n.t('Creating microvenv...'); export const created = l10n.t('Environment created...'); + export const existing = l10n.t('Using existing environment...'); + export const downloadingPip = l10n.t('Downloading pip...'); + export const installingPip = l10n.t('Installing pip...'); export const upgradingPip = l10n.t('Upgrading pip...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts index f9de4a94689a..b8efff3afbd8 100644 --- a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -8,151 +8,294 @@ 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 UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; -export const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; -export const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; +const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; +const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; +const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; +const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; +const UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; +const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; +const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; +const CREATING_MICROVENV_MARKER = 'CREATE_MICROVENV.CREATING_MICROVENV'; +const CREATE_MICROVENV_FAILED_MARKER = 'CREATE_VENV.MICROVENV_FAILED_CREATION'; +const CREATE_MICROVENV_FAILED_MARKER2 = 'CREATE_MICROVENV.MICROVENV_FAILED_CREATION'; +const MICROVENV_CREATED_MARKER = 'CREATE_MICROVENV.CREATED_MICROVENV'; +const INSTALLING_PIP_MARKER = 'CREATE_MICROVENV.INSTALLING_PIP'; +const INSTALL_PIP_FAILED_MARKER = 'CREATE_MICROVENV.INSTALL_PIP_FAILED'; +const DOWNLOADING_PIP_MARKER = 'CREATE_MICROVENV.DOWNLOADING_PIP'; +const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_MICROVENV.DOWNLOAD_PIP_FAILED'; export class VenvProgressAndTelemetry { - private venvCreatedReported = false; + private readonly processed = new Set(); - private venvOrPipMissingReported = false; - - private venvUpgradingPipReported = false; - - private venvUpgradedPipReported = false; - - private venvFailedReported = false; - - private venvInstallingPackagesReported = false; - - private venvInstallingPackagesFailedReported = false; - - private venvInstalledPackagesReported = false; + private readonly reportActions = new Map string | undefined>([ + [ + VENV_CREATED_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.created }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + return undefined; + }, + ], + [ + VENV_EXISTING_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.existing }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLING_REQUIREMENTS, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLING_PYPROJECT, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + PIP_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noPip', + }); + return PIP_NOT_INSTALLED_MARKER; + }, + ], + [ + VENV_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noVenv', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], + [ + INSTALL_REQUIREMENTS_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return INSTALL_REQUIREMENTS_FAILED_MARKER; + }, + ], + [ + INSTALL_PYPROJECT_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return INSTALL_PYPROJECT_FAILED_MARKER; + }, + ], + [ + CREATE_VENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'other', + }); + return CREATE_VENV_FAILED_MARKER; + }, + ], + [ + VENV_ALREADY_EXISTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLED_REQUIREMENTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLED_PYPROJECT_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + UPGRADED_PIP_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + [ + UPGRADE_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return UPGRADE_PIP_FAILED_MARKER; + }, + ], + [ + DOWNLOADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.downloadingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return undefined; + }, + ], + [ + DOWNLOAD_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return DOWNLOAD_PIP_FAILED_MARKER; + }, + ], + [ + INSTALLING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return undefined; + }, + ], + [ + INSTALL_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return INSTALL_PIP_FAILED_MARKER; + }, + ], + [ + CREATING_MICROVENV_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.creatingMicrovenv }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'microvenv', + pythonVersion: undefined, + }); + return undefined; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER2, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER2; + }, + ], + [ + MICROVENV_CREATED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'microvenv', + reason: 'created', + }); + return undefined; + }, + ], + [ + UPGRADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.upgradingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'microvenv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + ]); private lastError: string | undefined = undefined; 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', - }); - this.lastError = VENV_NOT_INSTALLED_MARKER; - } else if (!this.venvOrPipMissingReported && output.includes(PIP_NOT_INSTALLED_MARKER)) { - this.venvOrPipMissingReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { - environmentType: 'venv', - reason: 'noPip', - }); - this.lastError = PIP_NOT_INSTALLED_MARKER; - } else if (!this.venvUpgradingPipReported && output.includes(UPGRADING_PIP_MARKER)) { - this.venvUpgradingPipReported = true; - this.progress.report({ - message: CreateEnv.Venv.upgradingPip, - }); - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { - environmentType: 'venv', - using: 'pipUpgrade', - }); - } else if (!this.venvFailedReported && output.includes(CREATE_VENV_FAILED_MARKER)) { - this.venvFailedReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { - environmentType: 'venv', - reason: 'other', - }); - this.lastError = CREATE_VENV_FAILED_MARKER; - } 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(UPGRADE_PIP_FAILED_MARKER)) { - this.venvInstallingPackagesFailedReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { - environmentType: 'venv', - using: 'pipUpgrade', - }); - this.lastError = UPGRADE_PIP_FAILED_MARKER; - } 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', - }); - this.lastError = INSTALL_REQUIREMENTS_FAILED_MARKER; - } 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', - }); - this.lastError = INSTALL_PYPROJECT_FAILED_MARKER; - } else if (!this.venvUpgradedPipReported && output.includes(UPGRADED_PIP_MARKER)) { - this.venvUpgradedPipReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { - environmentType: 'venv', - using: 'pipUpgrade', - }); - } 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', - }); - } - } - public getLastError(): string | undefined { return this.lastError; } + + public process(output: string): void { + const keys: string[] = Array.from(this.reportActions.keys()); + + for (const key of keys) { + if (output.includes(key) && !this.processed.has(key)) { + const action = this.reportActions.get(key); + if (action) { + const err = action(this.progress); + if (err) { + this.lastError = err; + } + } + this.processed.add(key); + } + } + } } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 973e55302635..df2b454cbe27 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2014,7 +2014,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_CREATING]: { - environmentType: 'venv' | 'conda'; + environmentType: 'venv' | 'conda' | 'microvenv'; pythonVersion: string | undefined; }; /** @@ -2027,7 +2027,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_CREATED]: { - environmentType: 'venv' | 'conda'; + environmentType: 'venv' | 'conda' | 'microvenv'; reason: 'created' | 'existing'; }; /** @@ -2040,7 +2040,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_FAILED]: { - environmentType: 'venv' | 'conda'; + environmentType: 'venv' | 'conda' | 'microvenv'; reason: 'noVenv' | 'noPip' | 'other'; }; /** @@ -2053,8 +2053,8 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { - environmentType: 'venv' | 'conda'; - using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade'; + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade' | 'pipInstall' | 'pipDownload'; }; /** * Telemetry event sent after installing packages. @@ -2079,8 +2079,8 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED]: { - environmentType: 'venv' | 'conda'; - using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipDownload' | 'pipInstall'; }; /** * Telemetry event sent when a linter or formatter extension is already installed. diff --git a/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts new file mode 100644 index 000000000000..ecb7d1434ada --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts @@ -0,0 +1,54 @@ +// 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 { + VENV_CREATED_MARKER, + VenvProgressAndTelemetry, +} from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import * as telemetry from '../../../../client/telemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Venv Progress and Telemetry', () => { + let sendTelemetryEventStub: sinon.SinonStub; + let progressReporterMock: typemoq.IMock; + + setup(() => { + sendTelemetryEventStub = sinon.stub(telemetry, 'sendTelemetryEvent'); + progressReporterMock = typemoq.Mock.ofType(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure telemetry event and progress are sent', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); + + test('Do not trigger telemetry event the second time', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); +});