From 9721b45f874b3898de564c4f007f0dbc5633d6aa Mon Sep 17 00:00:00 2001 From: juftin Date: Tue, 20 Feb 2024 23:31:29 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20uv=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 60 ++++++++++++++++++++-------- tests/test_cli.py | 6 ++- tests/test_installer.py | 20 +++++----- tests/test_integration.py | 73 ++++++++++++++++++++++------------- tests/test_integration_cli.py | 18 ++++++--- tests/test_plugin.py | 62 ++++++++++++++++++++++++++++- 6 files changed, 178 insertions(+), 61 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index af27d04..4b3b4b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,13 +2,15 @@ Shared fixtures for tests. """ +from __future__ import annotations + import contextlib import os import pathlib import shutil from dataclasses import dataclass, field from subprocess import CompletedProcess -from typing import Dict, Generator, Type +from typing import Generator from unittest.mock import patch import pytest @@ -19,7 +21,6 @@ from hatch.utils.fs import Path, temp_directory from hatch.utils.platform import Platform -from hatch_pip_compile.installer import PipInstaller, PipSyncInstaller, PluginInstaller from hatch_pip_compile.plugin import PipCompileEnvironment @@ -120,16 +121,20 @@ def __post_init__(self) -> None: self.default_environment = self.reload_environment("default") self.test_environment = self.reload_environment("test") - def reload_environment(self, environment: str) -> PipCompileEnvironment: + def reload_environment(self, environment: str | PipCompileEnvironment) -> PipCompileEnvironment: """ Reload a new environment given the current state of the isolated project """ + if isinstance(environment, PipCompileEnvironment): + environment_name = environment.name + else: + environment_name = environment new_project = Project(self.isolation) return PipCompileEnvironment( root=self.isolation, metadata=new_project.metadata, - name=environment, - config=new_project.config.envs[environment], + name=environment_name, + config=new_project.config.envs[environment_name], matrix_variables={}, data_directory=self.isolated_data_dir, isolated_data_directory=self.isolated_data_dir, @@ -155,6 +160,36 @@ def chdir(self) -> Generator[None, None, None]: finally: os.chdir(current_dir) + def update_environment_resolver( + self, environment: str | PipCompileEnvironment, resolver: str + ) -> PipCompileEnvironment: + """ + Update the environment resolver + """ + if isinstance(environment, PipCompileEnvironment): + environment_name = environment.name + else: + environment_name = environment + self.toml_doc["tool"]["hatch"]["envs"][environment_name]["pip-compile-resolver"] = resolver + self.update_pyproject() + return self.reload_environment(environment_name) + + def update_environment_installer( + self, environment: str | PipCompileEnvironment, installer: str + ) -> PipCompileEnvironment: + """ + Update the environment installer + """ + if isinstance(environment, PipCompileEnvironment): + environment_name = environment.name + else: + environment_name = environment + self.toml_doc["tool"]["hatch"]["envs"][environment_name][ + "pip-compile-installer" + ] = installer + self.update_pyproject() + return self.reload_environment(environment_name) + @pytest.fixture def pip_compile( @@ -176,20 +211,13 @@ def pip_compile( ) -@pytest.fixture -def installer_dict() -> Dict[str, Type[PluginInstaller]]: - """ - Installer dictionary for parametrized tests - """ - return { - "pip": PipInstaller, - "pip-sync": PipSyncInstaller, - } - - @pytest.fixture(autouse=True) def pip_compile_disable(monkeypatch: pytest.MonkeyPatch) -> None: """ Delete the PIP_COMPILE_DISABLE environment variable """ monkeypatch.delenv("PIP_COMPILE_DISABLE", raising=False) + + +resolver_param = pytest.mark.parametrize("resolver", ["pip-compile", "uv"]) +installer_param = pytest.mark.parametrize("installer", ["pip", "pip-sync", "uv"]) diff --git a/tests/test_cli.py b/tests/test_cli.py index d63ee19..e2868e2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -122,9 +122,11 @@ def test_cli_upgrade_packages(pip_compile: PipCompileFixture) -> None: """ runner = CliRunner() with runner.isolated_filesystem(temp_dir=pip_compile.isolation): - result = runner.invoke(cli=cli, args=["--upgrade-package", "requests"]) + result = runner.invoke( + cli=cli, args=["--upgrade-package", "requests", "--upgrade-package", "urllib3"] + ) assert result.exit_code == 0 - assert "hatch-pip-compile: Upgrading packages: requests" in result.output + assert "hatch-pip-compile: Upgrading packages: requests, urllib3" in result.output def test_cli_upgrade_test(pip_compile: PipCompileFixture) -> None: diff --git a/tests/test_installer.py b/tests/test_installer.py index 96bc488..420cfb2 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -2,14 +2,12 @@ Installation Tests """ -from typing import Dict, Type from unittest.mock import Mock import pytest from hatch_pip_compile.exceptions import HatchPipCompileError -from hatch_pip_compile.installer import PluginInstaller -from tests.conftest import PipCompileFixture +from tests.conftest import PipCompileFixture, installer_param def test_pip_install_dependencies(mock_check_command: Mock, pip_compile: PipCompileFixture) -> None: @@ -33,17 +31,17 @@ def test_pip_install_dependencies(mock_check_command: Mock, pip_compile: PipComp assert call_args == expected_call -@pytest.mark.parametrize("installer", ["pip", "pip-sync"]) -def test_installer_type( - installer: str, installer_dict: Dict[str, Type[PluginInstaller]], pip_compile: PipCompileFixture -) -> None: +@installer_param +def test_installer_type(installer: str, pip_compile: PipCompileFixture) -> None: """ Test the `pip-compile-installer` configuration option """ - pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-installer"] = installer - pip_compile.update_pyproject() - updated_environment = pip_compile.reload_environment("default") - assert isinstance(updated_environment.installer, installer_dict[installer]) + updated_environment = pip_compile.update_environment_installer( + environment="default", installer=installer + ) + assert isinstance( + updated_environment.installer, updated_environment.dependency_installers[installer] + ) def test_installer_unknown(pip_compile: PipCompileFixture) -> None: diff --git a/tests/test_integration.py b/tests/test_integration.py index 285aa9b..27ffcf1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -7,14 +7,12 @@ """ import sys -from typing import Dict, Type import hatch import packaging.requirements import pytest -from hatch_pip_compile.installer import PluginInstaller -from tests.conftest import PipCompileFixture +from tests.conftest import PipCompileFixture, installer_param, resolver_param try: match_major, hatch_minor, _ = hatch._version.__version__.split(".") @@ -22,22 +20,29 @@ match_major, hatch_minor, _ = hatch.__about__.__version__.split(".") -@pytest.mark.parametrize("installer", ["pip", "pip-sync"]) +@installer_param +@resolver_param def test_new_dependency( - installer: str, installer_dict: Dict[str, Type[PluginInstaller]], pip_compile: PipCompileFixture + installer: str, + pip_compile: PipCompileFixture, + resolver: str, ) -> None: """ Test adding a new dependency """ if installer == "pip-sync" and sys.platform == "win32": # pragma: no cover pytest.skip("Flaky test on Windows") - original_requirements = pip_compile.default_environment.piptools_lock.read_header_requirements() + default_env = pip_compile.update_environment_resolver(environment="default", resolver=resolver) + original_requirements = default_env.piptools_lock.read_header_requirements() assert original_requirements == [packaging.requirements.Requirement("hatch")] pip_compile.toml_doc["project"]["dependencies"] = ["requests"] - pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-installer"] = installer pip_compile.update_pyproject() - updated_environment = pip_compile.reload_environment("default") - assert isinstance(updated_environment.installer, installer_dict[installer]) + updated_environment = pip_compile.update_environment_installer( + environment=default_env, installer=installer + ) + assert isinstance( + updated_environment.installer, updated_environment.dependency_installers[installer] + ) assert updated_environment.dependencies == ["requests"] pip_compile.application.prepare_environment(environment=updated_environment) assert updated_environment.lockfile_up_to_date is True @@ -52,20 +57,28 @@ def test_new_dependency( assert "requests" in result.stdout.decode() -@pytest.mark.parametrize("installer", ["pip", "pip-sync"]) +@installer_param +@resolver_param def test_delete_dependencies( - installer: str, installer_dict: Dict[str, Type[PluginInstaller]], pip_compile: PipCompileFixture + installer: str, + pip_compile: PipCompileFixture, + resolver: str, ) -> None: """ Test deleting all dependencies also deletes the lockfile """ if installer == "pip-sync" and sys.platform == "win32": # pragma: no cover pytest.skip("Flaky test on Windows") + pip_compile.update_environment_resolver( + environment=pip_compile.default_environment, resolver=resolver + ) pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-installer"] = installer pip_compile.toml_doc["project"]["dependencies"] = [] pip_compile.update_pyproject() updated_environment = pip_compile.reload_environment("default") - assert isinstance(updated_environment.installer, installer_dict[installer]) + assert isinstance( + updated_environment.installer, updated_environment.dependency_installers[installer] + ) assert updated_environment.dependencies == [] assert updated_environment.lockfile_up_to_date is False pip_compile.application.prepare_environment(environment=updated_environment) @@ -75,10 +88,12 @@ def test_delete_dependencies( assert updated_environment.piptools_lock_file.exists() is False -def test_create_constraint_environment(pip_compile: PipCompileFixture) -> None: +@resolver_param +def test_create_constraint_environment(pip_compile: PipCompileFixture, resolver: str) -> None: """ Syncing an environment with a constraint env also syncs the constraint env """ + pip_compile.update_environment_resolver(environment="default", resolver=resolver) original_requirements = pip_compile.default_environment.piptools_lock.read_header_requirements() assert original_requirements == [packaging.requirements.Requirement("hatch")] pip_compile.toml_doc["project"]["dependencies"] = ["requests"] @@ -96,40 +111,44 @@ def test_create_constraint_environment(pip_compile: PipCompileFixture) -> None: assert "pytest" in result.stdout.decode() -def test_dependency_uninstalled(pip_compile: PipCompileFixture) -> None: +@resolver_param +def test_dependency_uninstalled(pip_compile: PipCompileFixture, resolver: str) -> None: """ An environment is prepared, then a dependency is uninstalled, the environment should be out of sync even though the lockfile is good """ - pip_compile.application.prepare_environment(environment=pip_compile.test_environment) - list_result = pip_compile.test_environment.plugin_check_command( + test_env = pip_compile.update_environment_resolver(environment="test", resolver=resolver) + pip_compile.application.prepare_environment(environment=test_env) + list_result = test_env.plugin_check_command( command=["python", "-m", "pip", "list"], capture_output=True, ) assert "pytest" in list_result.stdout.decode() - assert pip_compile.test_environment.dependencies_in_sync() is True - pip_compile.test_environment.plugin_check_command( + assert test_env.dependencies_in_sync() is True + test_env.plugin_check_command( command=["python", "-m", "pip", "uninstall", "pytest", "pytest-cov", "-y"], ) - new_list_result = pip_compile.test_environment.plugin_check_command( + new_list_result = test_env.plugin_check_command( command=["python", "-m", "pip", "list"], capture_output=True, ) assert "pytest" not in new_list_result.stdout.decode() - assert pip_compile.test_environment.lockfile_up_to_date is True - assert pip_compile.test_environment.dependencies_in_sync() is False + assert test_env.lockfile_up_to_date is True + assert test_env.dependencies_in_sync() is False -def test_lockfile_missing(pip_compile: PipCompileFixture) -> None: +@resolver_param +def test_lockfile_missing(pip_compile: PipCompileFixture, resolver: str) -> None: """ Lockfile missing on previously prepared environment """ # Prepare the test environment, assert it is in sync - pip_compile.application.prepare_environment(environment=pip_compile.test_environment) - assert pip_compile.test_environment.dependencies_in_sync() is True + test_env = pip_compile.update_environment_resolver(environment="test", resolver=resolver) + pip_compile.application.prepare_environment(environment=test_env) + assert test_env.dependencies_in_sync() is True # Delete the lockfile, assert environment is in sync but lockfile is missing - pip_compile.test_environment.piptools_lock_file.unlink() + test_env.piptools_lock_file.unlink() updated_environment = pip_compile.reload_environment("test") list_result = updated_environment.plugin_check_command( command=["python", "-m", "pip", "list"], @@ -156,10 +175,12 @@ def test_check_dependency_hash_creates_lock(pip_compile: PipCompileFixture) -> N assert updated_environment.piptools_lock_file.exists() is True -def test_dependencies_in_sync(pip_compile: PipCompileFixture) -> None: +@resolver_param +def test_dependencies_in_sync(pip_compile: PipCompileFixture, resolver: str) -> None: """ Test the `dependencies_in_sync` method """ + pip_compile.update_environment_resolver(environment="default", resolver=resolver) pip_compile.default_environment.create() assert pip_compile.default_environment.lockfile_up_to_date is True assert pip_compile.default_environment.dependencies_in_sync() is False diff --git a/tests/test_integration_cli.py b/tests/test_integration_cli.py index acbae2f..c4a099a 100644 --- a/tests/test_integration_cli.py +++ b/tests/test_integration_cli.py @@ -8,18 +8,21 @@ from click.testing import CliRunner from hatch_pip_compile.exceptions import HatchPipCompileError -from tests.conftest import PipCompileFixture +from tests.conftest import PipCompileFixture, resolver_param +@resolver_param @pytest.mark.parametrize("environment_name", ["default", "test", "lint", "docs", "misc"]) def test_invoke_environment_creates_env( - pip_compile: PipCompileFixture, environment_name: str + pip_compile: PipCompileFixture, environment_name: str, resolver: str ) -> None: """ Test using the CLI runner """ runner = CliRunner() - environment = pip_compile.reload_environment(environment=environment_name) + environment = pip_compile.update_environment_resolver( + environment=environment_name, resolver=resolver + ) venv = environment.virtual_env.directory assert not venv.exists() with runner.isolated_filesystem(pip_compile.isolation): @@ -98,13 +101,18 @@ def test_missing_lockfile_after_prepared(pip_compile: PipCompileFixture) -> None assert environment.piptools_lock_file.exists() +@resolver_param @pytest.mark.parametrize("environment_name", ["default", "test", "lint"]) -def test_pip_compile_disable_cli(pip_compile: PipCompileFixture, environment_name: str) -> None: +def test_pip_compile_disable_cli( + pip_compile: PipCompileFixture, environment_name: str, resolver: str +) -> None: """ Test that the `PIP_COMPILE_DISABLE` environment variable raises an error """ runner = CliRunner() - environment = pip_compile.reload_environment(environment=environment_name) + environment = pip_compile.update_environment_resolver( + environment=environment_name, resolver=resolver + ) environment.piptools_lock_file.unlink(missing_ok=True) with runner.isolated_filesystem(pip_compile.isolation): result = runner.invoke( diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 93c14b8..fa7573a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -7,6 +7,7 @@ import pytest from hatch_pip_compile.exceptions import HatchPipCompileError +from hatch_pip_compile.resolver import PipCompileResolver, UvResolver from tests.conftest import PipCompileFixture @@ -94,9 +95,31 @@ def test_pip_compile_cli(mock_check_command: Mock, pip_compile: PipCompileFixtur "piptools", "compile", "--quiet", - "--strip-extras", "--no-header", "--resolver=backtracking", + "--strip-extras", + "--output-file", + ] + call_args = list(mock_check_command.call_args)[0][0][:-2] + assert call_args == expected_call + + +def test_uv_pip_compile_cli(mock_check_command: Mock, pip_compile: PipCompileFixture) -> None: + """ + Test the `pip_compile_cli` method is called with the expected arguments (uv) + """ + pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-resolver"] = "uv" + pip_compile.update_pyproject() + default_environment = pip_compile.reload_environment("default") + default_environment.pip_compile_cli() + expected_call = [ + "python", + "-m", + "uv", + "pip", + "compile", + "--quiet", + "--no-header", "--output-file", ] call_args = list(mock_check_command.call_args)[0][0][:-2] @@ -173,3 +196,40 @@ def test_prepare_environment(pip_compile: PipCompileFixture, environment_name: s assert not environment.piptools_lock_file.exists() assert environment.dependencies_in_sync() is True assert environment.lockfile_up_to_date is True + + +def test_bad_resolver(pip_compile: PipCompileFixture) -> None: + """ + Test the `pip-compile-resolver` option on invalid resolver + """ + pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-resolver"] = "unknown" + pip_compile.update_pyproject() + with pytest.raises(HatchPipCompileError, match="Invalid pip-compile-resolver"): + pip_compile.reload_environment("default").prepare_environment() + + +def test_resolver_instance_default(pip_compile: PipCompileFixture) -> None: + """ + Test the `pip-compile-resolver` option on default resolver + """ + assert isinstance(pip_compile.default_environment.resolver, PipCompileResolver) + + +def test_resolver_instance_pip_compile(pip_compile: PipCompileFixture) -> None: + """ + Test that the `PipCompileResolver` is used + """ + pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-resolver"] = "pip-compile" + pip_compile.update_pyproject() + environment = pip_compile.reload_environment("default") + assert isinstance(environment.resolver, PipCompileResolver) + + +def test_resolver_instance_uv(pip_compile: PipCompileFixture) -> None: + """ + Test that the `UVResolver` is used + """ + pip_compile.toml_doc["tool"]["hatch"]["envs"]["default"]["pip-compile-resolver"] = "uv" + pip_compile.update_pyproject() + environment = pip_compile.reload_environment("default") + assert isinstance(environment.resolver, UvResolver)