From 78758fcc0ec7097ff11d983addf692e2ba526cca Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Wed, 30 Oct 2024 14:04:59 +0100 Subject: [PATCH 01/14] adding basic tests and size limiting logic for plugin --- anaconda_conda_telemetry/hooks.py | 263 ++++++++++++++++++++++++------ tests/test_hooks.py | 68 ++++++++ tests/test_placeholder.py | 2 - 3 files changed, 280 insertions(+), 53 deletions(-) create mode 100644 tests/test_hooks.py delete mode 100644 tests/test_placeholder.py diff --git a/anaconda_conda_telemetry/hooks.py b/anaconda_conda_telemetry/hooks.py index f78fe96..f7b1b25 100644 --- a/anaconda_conda_telemetry/hooks.py +++ b/anaconda_conda_telemetry/hooks.py @@ -1,39 +1,53 @@ -from conda import __version__ as CONDA_VERSION +from __future__ import annotations + +import functools +import logging +import sys +import time +import typing + from conda.base.context import context +from conda.cli.main_list import list_packages from conda.common.url import mask_anaconda_token from conda.models.channel import all_channel_urls -from conda.plugins import hookimpl, CondaPostCommand -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchSpanProcessor, - # ConsoleSpanExporter, -) -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from conda.plugins import hookimpl, CondaRequestHeader try: from conda_build import __version__ as CONDA_BUILD_VERSION except ImportError: CONDA_BUILD_VERSION = "n/a" -# Tracer -provider = TracerProvider() -exporter = OTLPSpanExporter( - endpoint="http://localhost:4317/", - insecure=True, -) -processor = BatchSpanProcessor(exporter) -provider.add_span_processor(processor) +if typing.TYPE_CHECKING: + from collections.abc import Iterator + +logger = logging.getLogger(__name__) -# Enable when debugging output is desired -# processor_console = BatchSpanProcessor(ConsoleSpanExporter()) -# provider.add_span_processor(processor_console) +#: Field separator for request header +FIELD_SEPARATOR = ";" -# Sets the global default tracer provider -trace.set_tracer_provider(provider) +#: Size limit in bytes for the payload in the request header +SIZE_LIMIT = 7_000 -# Creates a tracer from the global tracer provider -tracer = trace.get_tracer("anaconda-conda-telemetry") +#: Prefix for all custom headers submitted via this plugin +HEADER_PREFIX = "Anaconda-Telemetry" + +#: Hosts we want to submit request headers to +REQUEST_HEADER_HOSTS = {"repo.anaconda.com", "conda.anaconda.org"} + + +def timer(func): + @functools.wraps(func) + def wrapper_timer(*args, **kwargs): + tic = time.perf_counter() + value = func(*args, **kwargs) + toc = time.perf_counter() + elapsed_time = toc - tic + logger.info( + f"function: {func.__name__}; duration (seconds): {elapsed_time:0.4f}" + ) + return value + + return wrapper_timer def get_virtual_packages() -> tuple[str, ...]: @@ -54,35 +68,182 @@ def get_channel_urls() -> tuple[str, ...]: return tuple(mask_anaconda_token(c) for c in channels) -def submit_telemetry_data(command: str): +def get_conda_command() -> str | None: """ - Submits telemetry data to the configured data collector + Use ``sys.argv`` to determine the conda command that is current being run """ - with tracer.start_as_current_span("post_command_hook") as current_span: - current_span.set_attribute( - "python_implementation", context.python_implementation_name_version[0] - ) - current_span.set_attribute( - "python_version", context.python_implementation_name_version[1] - ) - current_span.set_attribute("conda_version", CONDA_VERSION) - current_span.set_attribute("solver_version", context.solver_user_agent()) - current_span.set_attribute("conda_build_version", CONDA_BUILD_VERSION) - current_span.set_attribute("virtual_packages", get_virtual_packages()) - current_span.set_attribute( - "platform_system", context.platform_system_release[0] - ) - current_span.set_attribute( - "platform_release", context.platform_system_release[1] - ) - current_span.set_attribute("channel_urls", get_channel_urls()) - current_span.set_attribute("conda_command", command) + if len(sys.argv) > 2: + return sys.argv[1] -@hookimpl -def conda_post_commands(): - yield CondaPostCommand( - name="post-command-submit-telemetry-data", - action=submit_telemetry_data, - run_for=["install", "remove", "update", "create"], +def get_package_list() -> tuple[str, ...]: + """ + Retrieve the list of packages in the current environment + """ + _, packages = list_packages(context.active_prefix, format="canonical") + + return packages + + +def get_search_term() -> str: + """ + Retrieve the search term being used when search command is run + """ + return context._argparse_args.match_spec + + +def get_install_arguments() -> tuple[str, ...]: + """ + Get the position argument which have specified via the ``install`` or ``create`` commands + """ + return context._argparse_args.packages + + +@timer +@functools.lru_cache(None) +def get_sys_info_header_value() -> str: + """ + Return ``;`` delimited string of extra system information + """ + telemetry_data = { + "conda_build_version": CONDA_BUILD_VERSION, + "conda_command": get_conda_command(), + } + + return FIELD_SEPARATOR.join( + f"{key}:{value}" for key, value in telemetry_data.items() ) + + +@timer +@functools.lru_cache(None) +def get_channel_urls_header_value() -> str: + """ + Return ``FIELD_SEPARATOR`` delimited string of channel URLs + """ + return FIELD_SEPARATOR.join(get_channel_urls()) + + +@timer +@functools.lru_cache(None) +def get_virtual_packages_header_value() -> str: + """ + Return ``FIELD_SEPARATOR`` delimited string of virtual packages + """ + return FIELD_SEPARATOR.join(get_virtual_packages()) + + +@timer +@functools.lru_cache(None) +def get_install_arguments_header_value() -> str: + """ + Return ``FIELD_SEPARATOR`` delimited string of channel URLs + """ + return FIELD_SEPARATOR.join(get_install_arguments()) + + +@timer +@functools.lru_cache(None) +def get_installed_packages_header_value() -> str: + """ + Return ``FIELD_SEPARATOR`` delimited string of install arguments + """ + return FIELD_SEPARATOR.join(get_package_list()) + + +class HeaderWrapper(typing.NamedTuple): + """ + Object that wraps ``CondaRequestHeader`` and adds a ``size_limit`` field + """ + + header: CondaRequestHeader + size_limit: int + + +def validate_headers( + custom_headers: list[HeaderWrapper], +) -> Iterator[CondaRequestHeader]: + """ + Makes sure that all headers combined are not larger than ``SIZE_LIMIT``. + + Any headers over their individual limits will be truncated. + """ + total_max_size = sum(header.size_limit for header in custom_headers) + assert total_max_size <= SIZE_LIMIT + + for wrapper in custom_headers: + wrapper.header.value = wrapper.header.value[: wrapper.size_limit] + yield wrapper.header + + +@hookimpl +def conda_request_headers(): + custom_headers = [ + HeaderWrapper( + header=CondaRequestHeader( + name=f"{HEADER_PREFIX}-Sys-Info", + description="Custom headers used to submit telemetry data", + value=get_sys_info_header_value(), + hosts=REQUEST_HEADER_HOSTS, + ), + size_limit=500, + ), + HeaderWrapper( + header=CondaRequestHeader( + name=f"{HEADER_PREFIX}-Channels", + description="Header which exposes the channel URLs currently in use", + value=get_channel_urls_header_value(), + hosts=REQUEST_HEADER_HOSTS, + ), + size_limit=500, + ), + HeaderWrapper( + header=CondaRequestHeader( + name=f"{HEADER_PREFIX}-Virtual-Pkgs", + description="Header which exposes the virtual packages currently in use", + value=get_virtual_packages_header_value(), + hosts=REQUEST_HEADER_HOSTS, + ), + size_limit=500, + ), + HeaderWrapper( + header=CondaRequestHeader( + name=f"{HEADER_PREFIX}-Packages", + description="Header which exposes the currently installed packages", + value=get_installed_packages_header_value(), + hosts=REQUEST_HEADER_HOSTS, + ), + size_limit=5_000, + ), + ] + + command = get_conda_command() + + if command == "search": + custom_headers.append( + HeaderWrapper( + header=CondaRequestHeader( + name=f"{HEADER_PREFIX}-Search", + description="Header which exposes what is being searched for", + value=get_search_term(), + hosts=REQUEST_HEADER_HOSTS, + ), + size_limit=500, + ) + ) + + if command in {"install", "create"}: + custom_headers.append( + HeaderWrapper( + header=CondaRequestHeader( + name=f"{HEADER_PREFIX}-Install", + description="Header which exposes what is currently being installed as " + "specified on the command line", + value=get_install_arguments_header_value(), + hosts=REQUEST_HEADER_HOSTS, + ), + size_limit=500, + ) + ) + + yield from validate_headers(custom_headers) diff --git a/tests/test_hooks.py b/tests/test_hooks.py new file mode 100644 index 0000000..b12cf81 --- /dev/null +++ b/tests/test_hooks.py @@ -0,0 +1,68 @@ +from anaconda_conda_telemetry.hooks import conda_request_headers, HEADER_PREFIX + + +def test_conda_request_header_default_headers(): + """ + Ensure default headers are returned + """ + headers = {header.name: header for header in tuple(conda_request_headers())} + + expected_header_names_values = { + f"{HEADER_PREFIX}-Sys-Info": "", + f"{HEADER_PREFIX}-Channels": "", + f"{HEADER_PREFIX}-Virtual-Pkgs": "", + f"{HEADER_PREFIX}-Packages": "", + } + expected_header_names = {key for key, _ in expected_header_names_values.items()} + + assert len(set(headers.keys()).intersection(expected_header_names)) == len( + expected_header_names + ) + + +def test_conda_request_header_with_search(monkeypatch, mocker): + """ + Ensure default headers are returned when conda search is invoked + """ + monkeypatch.setattr("sys.argv", ["conda", "search", "package"]) + mock_argparse_args = mocker.MagicMock(match_spec="package") + mocker.patch( + "anaconda_conda_telemetry.hooks.context._argparse_args", mock_argparse_args + ) + + header_names = {header.name for header in tuple(conda_request_headers())} + expected_header_names = { + f"{HEADER_PREFIX}-Sys-Info", + f"{HEADER_PREFIX}-Channels", + f"{HEADER_PREFIX}-Virtual-Pkgs", + f"{HEADER_PREFIX}-Packages", + f"{HEADER_PREFIX}-Search", + } + + assert len(header_names.intersection(expected_header_names)) == len( + expected_header_names + ) + + +def test_conda_request_header_with_install(monkeypatch, mocker): + """ + Ensure default headers are returned when conda search is invoked + """ + monkeypatch.setattr("sys.argv", ["conda", "install", "package"]) + mock_argparse_args = mocker.MagicMock(packages=["package"]) + mocker.patch( + "anaconda_conda_telemetry.hooks.context._argparse_args", mock_argparse_args + ) + + header_names = {header.name for header in tuple(conda_request_headers())} + expected_header_names = { + f"{HEADER_PREFIX}-Sys-Info", + f"{HEADER_PREFIX}-Channels", + f"{HEADER_PREFIX}-Virtual-Pkgs", + f"{HEADER_PREFIX}-Packages", + f"{HEADER_PREFIX}-Install", + } + + assert len(header_names.intersection(expected_header_names)) == len( + expected_header_names + ) diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py deleted file mode 100644 index e02870c..0000000 --- a/tests/test_placeholder.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_placeholder(): - assert 1 + 1 == 2 From 4d909d4a6f4c166866153f6cdccf4fe3fea7422e Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Wed, 30 Oct 2024 14:07:00 +0100 Subject: [PATCH 02/14] fixing test runner --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0fb8869..dbdfdf2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: echo $CONDA/bin >> $GITHUB_PATH - name: Install dependencies (conda) run: | - conda env update --file environment.yml --name base + conda env update --file environment.yaml --name base conda install --file requirements.dev.txt - name: Install conda-basic-auth run: | From 53210418fdc5b5a3485cfcb42fac8eed8b6c7943 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Wed, 30 Oct 2024 14:08:12 +0100 Subject: [PATCH 03/14] removing otel dependencies --- environment.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/environment.yaml b/environment.yaml index 16ac6ca..556da6a 100644 --- a/environment.yaml +++ b/environment.yaml @@ -3,6 +3,3 @@ channels: dependencies: - python>=3.8 - conda-canary/label/dev::conda - - opentelemetry-api - - opentelemetry-sdk - - opentelemetry-exporter-otlp-proto-grpc From 518fd50aaeef567c760b8dff930f1357d430eaf4 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Wed, 30 Oct 2024 14:13:08 +0100 Subject: [PATCH 04/14] updating test runner --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dbdfdf2..07fcd77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,9 +30,10 @@ jobs: run: | conda env update --file environment.yaml --name base conda install --file requirements.dev.txt - - name: Install conda-basic-auth + - name: Install anaconda-conda-telemetry run: | pip install -e . - name: Test with pytest run: | + conda activate pytest --doctest-modules From 3e758eb15ab764987cc4d1b1420728a4dfa25d9f Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Wed, 30 Oct 2024 15:32:27 +0100 Subject: [PATCH 05/14] modifying test to make it pass on GH actions --- .github/workflows/tests.yml | 1 - tests/test_hooks.py | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07fcd77..1e0ae75 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,5 +35,4 @@ jobs: pip install -e . - name: Test with pytest run: | - conda activate pytest --doctest-modules diff --git a/tests/test_hooks.py b/tests/test_hooks.py index b12cf81..9f6e73f 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,6 +1,27 @@ +import pytest + from anaconda_conda_telemetry.hooks import conda_request_headers, HEADER_PREFIX +@pytest.fixture(autouse=True) +def packages(mocker): + """ + Mocks ``anaconda_conda_telemetry.hooks.list_packages`` + """ + packages = [ + "defaults/osx-arm64::sqlite-3.45.3-h80987f9_0", + "defaults/osx-arm64::pcre2-10.42-hb066dcc_1", + "defaults/osx-arm64::libxml2-2.13.1-h0b34f26_2", + ] + + def mock_list_packages(*args, **kwargs): + return 0, packages + + mocker.patch("anaconda_conda_telemetry.hooks.list_packages", mock_list_packages) + + return packages + + def test_conda_request_header_default_headers(): """ Ensure default headers are returned From d58fb7b6c5ff17d8529fbe574ebe0a1c388b52b1 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:10:28 +0100 Subject: [PATCH 06/14] updating testing matrix to include more OSes --- .github/workflows/tests.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e0ae75..e8ef5c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,11 +10,11 @@ on: jobs: build: - - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: python-version: ["3.8", "3.12"] + os: ["macos-latest", "ubuntu-latest", "windows-latest"] steps: - uses: actions/checkout@v3 @@ -22,10 +22,11 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH + - uses: conda-incubator/setup-miniconda@v3 + name: Setup Miniconda + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} - name: Install dependencies (conda) run: | conda env update --file environment.yaml --name base From 75c7b3f7c7dcce319263c2091efabe76fdafe100 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:18:22 +0100 Subject: [PATCH 07/14] remove pip install of anaconda-conda-telemetry from tests --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8ef5c1..aa7b4eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,9 +31,6 @@ jobs: run: | conda env update --file environment.yaml --name base conda install --file requirements.dev.txt - - name: Install anaconda-conda-telemetry - run: | - pip install -e . - name: Test with pytest run: | pytest --doctest-modules From f7df774dc184e9454e354932169cc611045be790 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:22:01 +0100 Subject: [PATCH 08/14] changes --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aa7b4eb..5a8093e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: python-version: ["3.8", "3.12"] - os: ["macos-latest", "ubuntu-latest", "windows-latest"] + os: ["macos-latest", "ubuntu-latest"] steps: - uses: actions/checkout@v3 @@ -31,6 +31,8 @@ jobs: run: | conda env update --file environment.yaml --name base conda install --file requirements.dev.txt + - name: Print environment variables + run: env | sort - name: Test with pytest run: | pytest --doctest-modules From 120f9f4fd37b2e5269c9eb43b70b0a8cc4671edc Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:24:42 +0100 Subject: [PATCH 09/14] CI test runner changes --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5a8093e..0f9d44a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,8 +31,10 @@ jobs: run: | conda env update --file environment.yaml --name base conda install --file requirements.dev.txt - - name: Print environment variables - run: env | sort + - name: Conda info + run: | + conda info + conda list - name: Test with pytest run: | pytest --doctest-modules From 51ab54aead7965ab4b031b6e85fc2c418ff2de5a Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:27:30 +0100 Subject: [PATCH 10/14] CI test runner changes --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0f9d44a..2b61f0b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,4 +37,4 @@ jobs: conda list - name: Test with pytest run: | - pytest --doctest-modules + conda run pytest --doctest-modules From 0fda63c1029d0e78c96c481f43cd712b4412da57 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:31:07 +0100 Subject: [PATCH 11/14] CI test runner changes --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b61f0b..d3353c7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: python-version: ["3.8", "3.12"] - os: ["macos-latest", "ubuntu-latest"] + os: ["macos-latest", "ubuntu-latest", "windows-latest"] steps: - uses: actions/checkout@v3 From d235ffd84652fd348a8fbeb4070c9d4b76032e8a Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Mon, 4 Nov 2024 14:37:37 +0100 Subject: [PATCH 12/14] CI test runner changes --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3353c7..00e7dc2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,12 +29,12 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies (conda) run: | - conda env update --file environment.yaml --name base - conda install --file requirements.dev.txt + conda env update --file environment.yaml --name test + conda install --name test --file requirements.dev.txt - name: Conda info run: | conda info - conda list + conda list --name test - name: Test with pytest run: | - conda run pytest --doctest-modules + conda run --name test pytest --doctest-modules From d0e43f003074cd319015bdbb44249f42a281722f Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Tue, 5 Nov 2024 12:17:47 +0100 Subject: [PATCH 13/14] making timer noop when not in INFO mode or lower --- anaconda_conda_telemetry/hooks.py | 55 ++++++++++++++++-------- tests/test_hooks.py | 69 +++++++++++++++++++++++-------- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/anaconda_conda_telemetry/hooks.py b/anaconda_conda_telemetry/hooks.py index f7b1b25..e3aa665 100644 --- a/anaconda_conda_telemetry/hooks.py +++ b/anaconda_conda_telemetry/hooks.py @@ -2,7 +2,6 @@ import functools import logging -import sys import time import typing @@ -31,6 +30,24 @@ #: Prefix for all custom headers submitted via this plugin HEADER_PREFIX = "Anaconda-Telemetry" +#: Name of the virtual package header +HEADER_VIRTUAL_PACKAGES = f"{HEADER_PREFIX}-Virtual-Packages" + +#: Name of the channels header +HEADER_CHANNELS = f"{HEADER_PREFIX}-Channels" + +#: Name of the packages header +HEADER_PACKAGES = f"{HEADER_PREFIX}-Packages" + +#: Name of the search header +HEADER_SEARCH = f"{HEADER_PREFIX}-Search" + +#: Name of the install header +HEADER_INSTALL = f"{HEADER_PREFIX}-Install" + +#: Name of the sys info header +HEADER_SYS_INFO = f"{HEADER_PREFIX}-Sys-Info" + #: Hosts we want to submit request headers to REQUEST_HEADER_HOSTS = {"repo.anaconda.com", "conda.anaconda.org"} @@ -38,14 +55,17 @@ def timer(func): @functools.wraps(func) def wrapper_timer(*args, **kwargs): - tic = time.perf_counter() - value = func(*args, **kwargs) - toc = time.perf_counter() - elapsed_time = toc - tic - logger.info( - f"function: {func.__name__}; duration (seconds): {elapsed_time:0.4f}" - ) - return value + if logger.getEffectiveLevel() <= logging.INFO: + tic = time.perf_counter() + value = func(*args, **kwargs) + toc = time.perf_counter() + elapsed_time = toc - tic + logger.info( + f"function: {func.__name__}; duration (seconds): {elapsed_time:0.4f}" + ) + return value + + return func(*args, **kwargs) return wrapper_timer @@ -68,12 +88,11 @@ def get_channel_urls() -> tuple[str, ...]: return tuple(mask_anaconda_token(c) for c in channels) -def get_conda_command() -> str | None: +def get_conda_command() -> str: """ Use ``sys.argv`` to determine the conda command that is current being run """ - if len(sys.argv) > 2: - return sys.argv[1] + return context._argparse_args.cmd def get_package_list() -> tuple[str, ...]: @@ -181,7 +200,7 @@ def conda_request_headers(): custom_headers = [ HeaderWrapper( header=CondaRequestHeader( - name=f"{HEADER_PREFIX}-Sys-Info", + name=HEADER_SYS_INFO, description="Custom headers used to submit telemetry data", value=get_sys_info_header_value(), hosts=REQUEST_HEADER_HOSTS, @@ -190,7 +209,7 @@ def conda_request_headers(): ), HeaderWrapper( header=CondaRequestHeader( - name=f"{HEADER_PREFIX}-Channels", + name=HEADER_CHANNELS, description="Header which exposes the channel URLs currently in use", value=get_channel_urls_header_value(), hosts=REQUEST_HEADER_HOSTS, @@ -199,7 +218,7 @@ def conda_request_headers(): ), HeaderWrapper( header=CondaRequestHeader( - name=f"{HEADER_PREFIX}-Virtual-Pkgs", + name=HEADER_VIRTUAL_PACKAGES, description="Header which exposes the virtual packages currently in use", value=get_virtual_packages_header_value(), hosts=REQUEST_HEADER_HOSTS, @@ -208,7 +227,7 @@ def conda_request_headers(): ), HeaderWrapper( header=CondaRequestHeader( - name=f"{HEADER_PREFIX}-Packages", + name=HEADER_PACKAGES, description="Header which exposes the currently installed packages", value=get_installed_packages_header_value(), hosts=REQUEST_HEADER_HOSTS, @@ -223,7 +242,7 @@ def conda_request_headers(): custom_headers.append( HeaderWrapper( header=CondaRequestHeader( - name=f"{HEADER_PREFIX}-Search", + name=HEADER_SEARCH, description="Header which exposes what is being searched for", value=get_search_term(), hosts=REQUEST_HEADER_HOSTS, @@ -236,7 +255,7 @@ def conda_request_headers(): custom_headers.append( HeaderWrapper( header=CondaRequestHeader( - name=f"{HEADER_PREFIX}-Install", + name=HEADER_INSTALL, description="Header which exposes what is currently being installed as " "specified on the command line", value=get_install_arguments_header_value(), diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 9f6e73f..715f12c 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,6 +1,17 @@ +import logging + import pytest -from anaconda_conda_telemetry.hooks import conda_request_headers, HEADER_PREFIX +from anaconda_conda_telemetry.hooks import ( + conda_request_headers, + HEADER_INSTALL, + HEADER_CHANNELS, + HEADER_SYS_INFO, + HEADER_VIRTUAL_PACKAGES, + HEADER_PACKAGES, + HEADER_SEARCH, + timer, +) @pytest.fixture(autouse=True) @@ -22,17 +33,21 @@ def mock_list_packages(*args, **kwargs): return packages -def test_conda_request_header_default_headers(): +def test_conda_request_header_default_headers(mocker): """ Ensure default headers are returned """ + mock_argparse_args = mocker.MagicMock(match_spec="package", cmd="search") + mocker.patch( + "anaconda_conda_telemetry.hooks.context._argparse_args", mock_argparse_args + ) headers = {header.name: header for header in tuple(conda_request_headers())} expected_header_names_values = { - f"{HEADER_PREFIX}-Sys-Info": "", - f"{HEADER_PREFIX}-Channels": "", - f"{HEADER_PREFIX}-Virtual-Pkgs": "", - f"{HEADER_PREFIX}-Packages": "", + HEADER_SYS_INFO: "", + HEADER_CHANNELS: "", + HEADER_PACKAGES: "", + HEADER_VIRTUAL_PACKAGES: "", } expected_header_names = {key for key, _ in expected_header_names_values.items()} @@ -46,18 +61,18 @@ def test_conda_request_header_with_search(monkeypatch, mocker): Ensure default headers are returned when conda search is invoked """ monkeypatch.setattr("sys.argv", ["conda", "search", "package"]) - mock_argparse_args = mocker.MagicMock(match_spec="package") + mock_argparse_args = mocker.MagicMock(match_spec="package", cmd="search") mocker.patch( "anaconda_conda_telemetry.hooks.context._argparse_args", mock_argparse_args ) header_names = {header.name for header in tuple(conda_request_headers())} expected_header_names = { - f"{HEADER_PREFIX}-Sys-Info", - f"{HEADER_PREFIX}-Channels", - f"{HEADER_PREFIX}-Virtual-Pkgs", - f"{HEADER_PREFIX}-Packages", - f"{HEADER_PREFIX}-Search", + HEADER_SYS_INFO, + HEADER_CHANNELS, + HEADER_PACKAGES, + HEADER_VIRTUAL_PACKAGES, + HEADER_SEARCH, } assert len(header_names.intersection(expected_header_names)) == len( @@ -70,20 +85,38 @@ def test_conda_request_header_with_install(monkeypatch, mocker): Ensure default headers are returned when conda search is invoked """ monkeypatch.setattr("sys.argv", ["conda", "install", "package"]) - mock_argparse_args = mocker.MagicMock(packages=["package"]) + mock_argparse_args = mocker.MagicMock(packages=["package"], cmd="install") mocker.patch( "anaconda_conda_telemetry.hooks.context._argparse_args", mock_argparse_args ) header_names = {header.name for header in tuple(conda_request_headers())} expected_header_names = { - f"{HEADER_PREFIX}-Sys-Info", - f"{HEADER_PREFIX}-Channels", - f"{HEADER_PREFIX}-Virtual-Pkgs", - f"{HEADER_PREFIX}-Packages", - f"{HEADER_PREFIX}-Install", + HEADER_SYS_INFO, + HEADER_CHANNELS, + HEADER_PACKAGES, + HEADER_VIRTUAL_PACKAGES, + HEADER_INSTALL, } assert len(header_names.intersection(expected_header_names)) == len( expected_header_names ) + + +def test_timer_in_info_mode(caplog): + """ + Ensure the timer decorator works and logs the time taken in INFO mode + """ + caplog.set_level(logging.INFO) + + @timer + def test(): + return 1 + + assert test() == 1 + + assert caplog.records[0].levelname == "INFO" + + assert "INFO anaconda_conda_telemetry.hooks" in caplog.text + assert "function: test; duration (seconds):" in caplog.text From aa8715a4ab18690d610cf6bbfd3ab9ba2e7180e3 Mon Sep 17 00:00:00 2001 From: Travis Hathaway Date: Tue, 5 Nov 2024 12:33:03 +0100 Subject: [PATCH 14/14] adding a better message to the assertion error --- anaconda_conda_telemetry/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/anaconda_conda_telemetry/hooks.py b/anaconda_conda_telemetry/hooks.py index e3aa665..737be26 100644 --- a/anaconda_conda_telemetry/hooks.py +++ b/anaconda_conda_telemetry/hooks.py @@ -188,7 +188,9 @@ def validate_headers( Any headers over their individual limits will be truncated. """ total_max_size = sum(header.size_limit for header in custom_headers) - assert total_max_size <= SIZE_LIMIT + assert ( + total_max_size <= SIZE_LIMIT + ), f"Total header size limited to {SIZE_LIMIT}. Exceeded with {total_max_size=}" for wrapper in custom_headers: wrapper.header.value = wrapper.header.value[: wrapper.size_limit]