diff --git a/.config/dictionary.txt b/.config/dictionary.txt index 898f4f6..0d4f4ca 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -16,6 +16,9 @@ fileh fqcn levelname netcommon +platinclude +platlib +platstdlib purelib reqs setenv diff --git a/src/ansible_dev_environment/config.py b/src/ansible_dev_environment/config.py index 397b469..539ab73 100644 --- a/src/ansible_dev_environment/config.py +++ b/src/ansible_dev_environment/config.py @@ -65,7 +65,11 @@ def cache_dir(self: Config) -> Path: @property def venv(self: Config) -> Path: - """Return the virtual environment path.""" + """Return the virtual environment path. + + Raises: + SystemExit: If the virtual environment cannot be found. + """ if self.args.venv: return Path(self.args.venv).expanduser().resolve() venv_str = os.environ.get("VIRTUAL_ENV") @@ -73,7 +77,7 @@ def venv(self: Config) -> Path: return Path(venv_str).expanduser().resolve() err = "Failed to find a virtual environment." self._output.critical(err) - sys.exit(1) + raise SystemExit(1) # pragma: no cover # critical exits @property def venv_cache_dir(self: Config) -> Path: @@ -109,10 +113,13 @@ def interpreter(self: Config) -> Path: return Path(sys.executable) @property - def galaxy_bin(self: Config) -> Path | None: + def galaxy_bin(self: Config) -> Path: """Find the ansible galaxy command. Prefer the venv over the system package over the PATH. + + Raises: + SystemExit: If the command cannot be found. """ within_venv = self.venv_bindir / "ansible-galaxy" if within_venv.exists(): @@ -131,7 +138,7 @@ def galaxy_bin(self: Config) -> Path | None: return Path(last_resort) msg = "Failed to find ansible-galaxy." self._output.critical(msg) - return None + raise SystemExit(1) # pragma: no cover # critical exits def _set_interpreter( self: Config, diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 749871a..8d86648 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -3,14 +3,18 @@ from __future__ import annotations import argparse +import copy +import json import shutil +import subprocess from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pytest from ansible_dev_environment.config import Config +from ansible_dev_environment.utils import subprocess_run if TYPE_CHECKING: @@ -243,3 +247,350 @@ def _which(name: str) -> str | None: assert exc.value.code == 1 assert exist_called assert which_called + + +def test_venv_from_env_var( + monkeypatch: pytest.MonkeyPatch, + session_venv: Config, + output: Output, +) -> None: + """Test the venv property found in the environment variable. + + Reuse the venv from the session_venv fixture. + + Args: + monkeypatch: A pytest fixture for patching. + session_venv: The session venv fixture. + output: The output fixture. + """ + venv = session_venv.venv + + args = gen_args(venv="") + monkeypatch.setenv("VIRTUAL_ENV", str(venv)) + + config = Config(args=args, output=output, term_features=output.term_features) + config.init() + + assert config.venv == venv + + +def test_venv_not_found( + output: Output, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test the venv not found. + + Args: + output: The output fixture. + capsys: A pytest fixture for capturing stdout and stderr. + monkeypatch: A pytest fixture for patching. + """ + args = gen_args(venv="") + config = Config(args=args, output=output, term_features=output.term_features) + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + + with pytest.raises(SystemExit) as exc: + config.init() + + assert exc.value.code == 1 + assert "Failed to find a virtual environment." in capsys.readouterr().err + + +def test_venv_creation_failed( + tmp_path: Path, + output: Output, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test the venv creation failed. + + Args: + tmp_path: A temporary directory. + output: The output fixture. + monkeypatch: A pytest fixture for patching. + capsys: A pytest fixture for capturing stdout and stderr. + """ + args = gen_args(venv=str(tmp_path / "test_venv")) + + orig_subprocess_run = subprocess_run + + def mock_subprocess_run( + *args: Any, # noqa: ANN401 + **kwargs: Any, # noqa: ANN401 + ) -> subprocess.CompletedProcess[str]: + """Mock the subprocess.run function. + + Args: + *args: The positional arguments. + **kwargs: The keyword arguments. + + Raises: + subprocess.CalledProcessError: For the venv command + Returns: + The completed process. + + """ + if "venv" in kwargs["command"]: + raise subprocess.CalledProcessError(1, kwargs["command"]) + return orig_subprocess_run(*args, **kwargs) + + monkeypatch.setattr("ansible_dev_environment.config.subprocess_run", mock_subprocess_run) + config = Config(args=args, output=output, term_features=output.term_features) + + with pytest.raises(SystemExit) as exc: + config.init() + + assert exc.value.code == 1 + assert "Failed to create virtual environment" in capsys.readouterr().err + + +def test_venv_env_var_wrong( + output: Output, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Test VIRTUAL_ENV points to a non-existent venv. + + Args: + output: The output fixture. + capsys: A pytest fixture for capturing stdout and stderr. + monkeypatch: A pytest fixture for patching. + tmp_path: A temporary directory + """ + args = gen_args(venv="") + config = Config(args=args, output=output, term_features=output.term_features) + monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / "test_venv")) + + with pytest.raises(SystemExit) as exc: + config.init() + + assert exc.value.code == 1 + assert "Cannot find virtual environment" in capsys.readouterr().err + + +def test_venv_env_var_missing_interpreter( + output: Output, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Test VIRTUAL_ENV points to a directory without a python interpreter. + + Args: + output: The output fixture. + capsys: A pytest fixture for capturing stdout and stderr. + monkeypatch: A pytest fixture for patching. + tmp_path: A temporary directory + """ + args = gen_args(venv="") + config = Config(args=args, output=output, term_features=output.term_features) + venv = tmp_path / "test_venv" + venv.mkdir() + monkeypatch.setenv("VIRTUAL_ENV", str(venv)) + + with pytest.raises(SystemExit) as exc: + config.init() + + assert exc.value.code == 1 + assert "Cannot find interpreter" in capsys.readouterr().err + + +def test_sys_packages_path_fail_call( + session_venv: Config, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test the system site packages path. + + Args: + session_venv: The session venv fixture. + monkeypatch: A pytest fixture for patching. + capsys: A pytest fixture for capturing stdout and stderr. + """ + orig_subprocess_run = subprocess_run + + def mock_subprocess_run( + *args: Any, # noqa: ANN401 + **kwargs: Any, # noqa: ANN401 + ) -> subprocess.CompletedProcess[str]: + """Mock the subprocess.run function. + + Args: + *args: The positional arguments. + **kwargs: The keyword arguments. + + Raises: + subprocess.CalledProcessError: For the venv command + Returns: + The completed process. + + """ + if "sysconfig.get_paths" in kwargs["command"]: + raise subprocess.CalledProcessError(1, kwargs["command"]) + return orig_subprocess_run(*args, **kwargs) + + monkeypatch.setattr("ansible_dev_environment.config.subprocess_run", mock_subprocess_run) + + copied_config = copy.deepcopy(session_venv) + + with pytest.raises(SystemExit) as exc: + copied_config._set_site_pkg_path() + + assert exc.value.code == 1 + assert "Failed to find site packages path" in capsys.readouterr().err + + +def test_sys_packages_path_fail_invalid_json( + session_venv: Config, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test the system site packages path when the json is invalid. + + Args: + session_venv: The session venv fixture. + monkeypatch: A pytest fixture for patching. + capsys: A pytest fixture for capturing stdout and stderr. + """ + orig_subprocess_run = subprocess_run + + def mock_subprocess_run( + *args: Any, # noqa: ANN401 + **kwargs: Any, # noqa: ANN401 + ) -> subprocess.CompletedProcess[str]: + """Mock the subprocess.run function. + + Args: + *args: The positional arguments. + **kwargs: The keyword arguments. + + Returns: + The completed process. + + """ + if "sysconfig.get_paths" in kwargs["command"]: + return subprocess.CompletedProcess( + args=kwargs["command"], + returncode=0, + stdout="invalid json", + stderr="", + ) + return orig_subprocess_run(*args, **kwargs) + + monkeypatch.setattr("ansible_dev_environment.config.subprocess_run", mock_subprocess_run) + + copied_config = copy.deepcopy(session_venv) + + with pytest.raises(SystemExit) as exc: + copied_config._set_site_pkg_path() + + assert exc.value.code == 1 + assert "Failed to decode json" in capsys.readouterr().err + + +def test_sys_packages_path_fail_empty( + session_venv: Config, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test the system site packages path when the json is empty. + + Args: + session_venv: The session venv fixture. + monkeypatch: A pytest fixture for patching. + capsys: A pytest fixture for capturing stdout and stderr. + """ + orig_subprocess_run = subprocess_run + + def mock_subprocess_run( + *args: Any, # noqa: ANN401 + **kwargs: Any, # noqa: ANN401 + ) -> subprocess.CompletedProcess[str]: + """Mock the subprocess.run function. + + Args: + *args: The positional arguments. + **kwargs: The keyword arguments. + + Returns: + The completed process. + + """ + if "sysconfig.get_paths" in kwargs["command"]: + return subprocess.CompletedProcess( + args=kwargs["command"], + returncode=0, + stdout="[]", + stderr="", + ) + return orig_subprocess_run(*args, **kwargs) + + monkeypatch.setattr("ansible_dev_environment.config.subprocess_run", mock_subprocess_run) + + copied_config = copy.deepcopy(session_venv) + + with pytest.raises(SystemExit) as exc: + copied_config._set_site_pkg_path() + + assert exc.value.code == 1 + assert "Failed to find site packages path" in capsys.readouterr().err + + +def test_sys_packages_path_missing_purelib( + session_venv: Config, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test the system site packages path when the json is empty. + + Args: + session_venv: The session venv fixture. + monkeypatch: A pytest fixture for patching. + capsys: A pytest fixture for capturing stdout and stderr. + """ + orig_subprocess_run = subprocess_run + + def mock_subprocess_run( + *args: Any, # noqa: ANN401 + **kwargs: Any, # noqa: ANN401 + ) -> subprocess.CompletedProcess[str]: + """Mock the subprocess.run function. + + Args: + *args: The positional arguments. + **kwargs: The keyword arguments. + + Returns: + The completed process. + + """ + if "sysconfig.get_paths" in kwargs["command"]: + response = { + "stdlib": "/usr/lib64/python3.12", + "platstdlib": "/home/user/ansible-dev-environment/venv/lib64/python3.12", + "_purelib": "/home/user/ansible-dev-environment/venv/lib/python3.12/site-packages", + "platlib": "/home/user/ansible-dev-environment/venv/lib64/python3.12/site-packages", + "include": "/usr/include/python3.12", + "platinclude": "/usr/include/python3.12", + "scripts": "/home/user/ansible-dev-environment/venv/bin", + "data": "/home/user/github/ansible-dev-environment/venv", + } + return subprocess.CompletedProcess( + args=kwargs["command"], + returncode=0, + stdout=json.dumps(response), + stderr="", + ) + return orig_subprocess_run(*args, **kwargs) + + monkeypatch.setattr("ansible_dev_environment.config.subprocess_run", mock_subprocess_run) + + copied_config = copy.deepcopy(session_venv) + + with pytest.raises(SystemExit) as exc: + copied_config._set_site_pkg_path() + + assert exc.value.code == 1 + assert "Failed to find purelib in sysconfig paths" in capsys.readouterr().err