from __future__ import annotations import platform 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" def _write_file(url: str, path: Path) -> tuple[str, None]: Path(path).write_bytes(b"data") return str(path), None with patch("urllib.request.urlretrieve", side_effect=_write_file) as mock_retrieve: install_tools._download("https://example.com/file", dest) mock_retrieve.assert_called_once() 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() 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 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_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 == 6 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