from __future__ import annotations import platform import urllib.request from pathlib import Path from unittest.mock import patch import pytest from click import ClickException from click.testing import CliRunner import devx.tools.install_tools as install_tools class TestArch: def test_amd64(self) -> None: with patch.object(platform, "machine", return_value="x86_64"): assert install_tools._arch() == "amd64" def test_arm64(self) -> None: with patch.object(platform, "machine", return_value="aarch64"): assert install_tools._arch() == "arm64" def test_unsupported(self) -> None: with patch.object(platform, "machine", return_value="riscv64"): with pytest.raises(ClickException): install_tools._arch() class TestIsInstalled: def test_on_path(self) -> None: with patch("shutil.which", return_value="/usr/bin/actionlint"): assert install_tools._is_installed("actionlint") is True def test_in_target_dir(self, tmp_path: Path) -> None: with patch.object(install_tools, "TARGET_DIR", tmp_path): (tmp_path / "actionlint").touch() with patch("shutil.which", return_value=None): assert install_tools._is_installed("actionlint") is True def test_not_installed(self, tmp_path: Path) -> None: with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch("shutil.which", return_value=None): assert install_tools._is_installed("actionlint") is False class TestDownload: def test_download(self, tmp_path: Path) -> None: dest = tmp_path / "file.bin" class _FakeResponse: def __init__(self) -> None: self._sent = False def __enter__(self) -> _FakeResponse: return self def __exit__(self, *args: object) -> None: pass def read(self, n: int = -1) -> bytes: if self._sent: return b"" self._sent = True return b"data" with patch("urllib.request.urlopen", return_value=_FakeResponse()) as mock_urlopen: install_tools._download("https://example.com/file", dest) mock_urlopen.assert_called_once() call_args = mock_urlopen.call_args req = call_args.args[0] assert isinstance(req, urllib.request.Request) assert req.get_header("User-agent") == "devx/install-tools" assert dest.read_bytes() == b"data" class TestDownloadBinary: def test_download(self, tmp_path: Path) -> None: dest = tmp_path / "act_runner" def _write_file(url: str, path: Path) -> tuple[str, None]: Path(path).write_bytes(b"binary") return str(path), None with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(install_tools, "_download", side_effect=_write_file): result = install_tools._download_binary("https://example.com/act_runner", "act_runner") assert result == dest assert dest.exists() assert dest.stat().st_mode & 0o111 class TestDownloadAndExtractTarball: def test_extract(self, tmp_path: Path) -> None: import tarfile # Create a fake tarball with a binary tarball_path = tmp_path / "archive.tar.gz" binary_content = b"fake binary" with tarfile.open(tarball_path, "w:gz") as tar: import io info = tarfile.TarInfo(name="actionlint") info.size = len(binary_content) tar.addfile(info, io.BytesIO(binary_content)) target_dir = tmp_path / "bin" target_dir.mkdir() with patch.object(install_tools, "TARGET_DIR", target_dir): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): result = install_tools._download_and_extract_tarball( "https://example.com/actionlint.tar.gz", "actionlint" ) assert result == target_dir / "actionlint" assert result.exists() assert result.read_bytes() == binary_content def test_binary_not_found(self, tmp_path: Path) -> None: import tarfile tarball_path = tmp_path / "archive.tar.gz" with tarfile.open(tarball_path, "w:gz") as tar: import io info = tarfile.TarInfo(name="other_file") info.size = 0 tar.addfile(info, io.BytesIO(b"")) target_dir = tmp_path / "bin" target_dir.mkdir() with patch.object(install_tools, "TARGET_DIR", target_dir): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): with pytest.raises(ClickException, match="not found in archive"): install_tools._download_and_extract_tarball("https://example.com/actionlint.tar.gz", "actionlint") class TestInstallActionlint: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_actionlint() is True def test_install(self, tmp_path: Path) -> None: import tarfile tarball_path = tmp_path / "archive.tar.gz" binary_content = b"fake actionlint" with tarfile.open(tarball_path, "w:gz") as tar: import io info = tarfile.TarInfo(name="actionlint") info.size = len(binary_content) tar.addfile(info, io.BytesIO(binary_content)) with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): assert install_tools.install_actionlint() is True assert (tmp_path / "actionlint").exists() class TestInstallGitCliff: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_git_cliff() is True def test_install(self, tmp_path: Path) -> None: import tarfile tarball_path = tmp_path / "archive.tar.gz" binary_content = b"fake git-cliff" with tarfile.open(tarball_path, "w:gz") as tar: import io info = tarfile.TarInfo(name="git-cliff") info.size = len(binary_content) tar.addfile(info, io.BytesIO(binary_content)) with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): assert install_tools.install_git_cliff() is True assert (tmp_path / "git-cliff").exists() class TestInstallActRunner: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_act_runner() is True def test_install(self, tmp_path: Path) -> None: def _write_file(url: str, path: Path) -> tuple[str, None]: Path(path).write_bytes(b"binary") return str(path), None with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object(install_tools, "_download", side_effect=_write_file): assert install_tools.install_act_runner() is True assert (tmp_path / "act_runner").exists() class TestInstallTea: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_tea() is True def test_install(self, tmp_path: Path) -> None: def _write_file(url: str, path: Path) -> tuple[str, None]: Path(path).write_bytes(b"binary") return str(path), None with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object(install_tools, "_download", side_effect=_write_file): assert install_tools.install_tea() is True assert (tmp_path / "tea").exists() def test_install_fallback_to_second_url(self, tmp_path: Path) -> None: """First URL fails (403), second URL succeeds.""" call_count = [0] def _download_side_effect(url: str, dest: Path) -> None: call_count[0] += 1 if call_count[0] == 1: raise OSError("HTTP Error 403: Forbidden") Path(dest).write_bytes(b"binary") with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object(install_tools, "_download", side_effect=_download_side_effect): assert install_tools.install_tea() is True assert (tmp_path / "tea").exists() assert call_count[0] == 2 def test_install_all_urls_fail(self, tmp_path: Path) -> None: """All URLs fail — should raise ClickException.""" with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object(install_tools, "_download", side_effect=OSError("403 Forbidden")): with pytest.raises(ClickException, match="Failed to download tea"): install_tools.install_tea() class TestInstallHadolint: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_hadolint() is True def test_install(self, tmp_path: Path) -> None: def _write_file(url: str, path: Path) -> tuple[str, None]: Path(path).write_bytes(b"binary") return str(path), None with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object(install_tools, "_download", side_effect=_write_file): assert install_tools.install_hadolint() is True assert (tmp_path / "hadolint").exists() class TestInstallTofu: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_tofu() is True def test_install(self, tmp_path: Path) -> None: import io import tarfile tarball_path = tmp_path / "archive.tar.gz" binary_content = b"fake tofu" with tarfile.open(tarball_path, "w:gz") as tar: info = tarfile.TarInfo(name="tofu") info.size = len(binary_content) tar.addfile(info, io.BytesIO(binary_content)) with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object(platform, "system", return_value="Linux"): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): assert install_tools.install_tofu() is True assert (tmp_path / "tofu").exists() class TestInstallVale: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_vale() is True def test_install(self, tmp_path: Path) -> None: import io import tarfile tarball_path = tmp_path / "archive.tar.gz" binary_content = b"fake vale" with tarfile.open(tarball_path, "w:gz") as tar: info = tarfile.TarInfo(name="vale") info.size = len(binary_content) tar.addfile(info, io.BytesIO(binary_content)) with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(platform, "machine", return_value="x86_64"): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): assert install_tools.install_vale() is True assert (tmp_path / "vale").exists() class TestInstallPromtool: def test_already_installed(self) -> None: with patch.object(install_tools, "_is_installed", return_value=True): assert install_tools.install_promtool() is True def test_install(self, tmp_path: Path) -> None: import io import tarfile tarball_path = tmp_path / "archive.tar.gz" binary_content = b"fake promtool" with tarfile.open(tarball_path, "w:gz") as tar: info = tarfile.TarInfo(name="promtool") info.size = len(binary_content) tar.addfile(info, io.BytesIO(binary_content)) with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch.object(install_tools, "_arch", return_value="amd64"): with patch.object( install_tools, "_download", side_effect=lambda url, dest: Path(dest).write_bytes(tarball_path.read_bytes()), ): assert install_tools.install_promtool() is True assert (tmp_path / "promtool").exists() def test_url_contains_version(self, tmp_path: Path) -> None: """Verify the download URL includes the correct promtool version.""" captured_url = [] def fake_extract(url: str, binary_name: str) -> Path: captured_url.append(url) return tmp_path / binary_name with patch.object(install_tools, "_is_installed", return_value=False): with patch.object(install_tools, "_download_and_extract_tarball", side_effect=fake_extract): install_tools.install_promtool() assert any(f"v{install_tools.PROMTOOL_VERSION}" in url for url in captured_url) class TestListTools: def test_list(self, tmp_path: Path) -> None: with patch.object(install_tools, "TARGET_DIR", tmp_path): with patch("shutil.which", return_value=None): with patch.object(install_tools, "TOOL_NAMES", ["actionlint", "git-cliff", "act_runner"]): install_tools.list_tools() class TestInstallTool: def test_actionlint(self) -> None: with patch.object(install_tools, "install_actionlint", return_value=True) as mock: assert install_tools._install_tool("actionlint") is True mock.assert_called_once() def test_git_cliff(self) -> None: with patch.object(install_tools, "install_git_cliff", return_value=True) as mock: assert install_tools._install_tool("git-cliff") is True mock.assert_called_once() def test_act_runner(self) -> None: with patch.object(install_tools, "install_act_runner", return_value=True) as mock: assert install_tools._install_tool("act_runner") is True mock.assert_called_once() def test_tea(self) -> None: with patch.object(install_tools, "install_tea", return_value=True) as mock: assert install_tools._install_tool("tea") is True mock.assert_called_once() def test_hadolint(self) -> None: with patch.object(install_tools, "install_hadolint", return_value=True) as mock: assert install_tools._install_tool("hadolint") is True mock.assert_called_once() def test_tofu(self) -> None: with patch.object(install_tools, "install_tofu", return_value=True) as mock: assert install_tools._install_tool("tofu") is True mock.assert_called_once() def test_vale(self) -> None: with patch.object(install_tools, "install_vale", return_value=True) as mock: assert install_tools._install_tool("vale") is True mock.assert_called_once() def test_promtool(self) -> None: with patch.object(install_tools, "install_promtool", return_value=True) as mock: assert install_tools._install_tool("promtool") is True mock.assert_called_once() def test_unknown_tool(self) -> None: with pytest.raises(ClickException, match="Unknown tool"): install_tools._install_tool("unknown") class TestMain: def test_list_status(self) -> None: runner = CliRunner() with patch.object(install_tools, "_is_installed", return_value=True): result = runner.invoke(install_tools.main, ["--list"]) assert result.exit_code == 0 assert "actionlint" in result.output def test_install_all(self) -> None: runner = CliRunner() with patch.object(install_tools, "_install_tool", return_value=True) as mock_install: result = runner.invoke(install_tools.main, []) assert result.exit_code == 0 assert mock_install.call_count == 8 def test_install_specific_tool(self) -> None: runner = CliRunner() with patch.object(install_tools, "_install_tool", return_value=True) as mock_install: result = runner.invoke(install_tools.main, ["--tool", "actionlint"]) assert result.exit_code == 0 mock_install.assert_called_once_with("actionlint") def test_install_multiple_specific_tools(self) -> None: runner = CliRunner() with patch.object(install_tools, "_install_tool", return_value=True) as mock_install: result = runner.invoke(install_tools.main, ["--tool", "git-cliff", "--tool", "tea"]) assert result.exit_code == 0 assert mock_install.call_count == 2 def test_install_failure(self) -> None: runner = CliRunner() with patch.object(install_tools, "_install_tool", side_effect=Exception("network error")): result = runner.invoke(install_tools.main, ["--tool", "actionlint"]) assert result.exit_code != 0 def test_path_reminder(self, monkeypatch: pytest.MonkeyPatch) -> None: """When TARGET_DIR is not in PATH, a reminder is printed.""" monkeypatch.setenv("PATH", "/usr/bin:/bin") runner = CliRunner() with patch.object(install_tools, "_install_tool", return_value=True): result = runner.invoke(install_tools.main, []) assert result.exit_code == 0 assert "Add" in result.output assert "PATH" in result.output def test_no_path_reminder_when_in_path(self, monkeypatch: pytest.MonkeyPatch) -> None: """When TARGET_DIR is in PATH, no reminder is printed.""" target_dir = str(install_tools.TARGET_DIR) monkeypatch.setenv("PATH", f"/usr/bin:{target_dir}:/bin") runner = CliRunner() with patch.object(install_tools, "_install_tool", return_value=True): result = runner.invoke(install_tools.main, []) assert result.exit_code == 0 assert "Add" not in result.output