Public Access
493 lines
20 KiB
Python
493 lines
20 KiB
Python
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
|