Files
devx/tests/unit/test_setup.py
T
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 60fd11419c feat: extract reusable dev/CI tools from GRM into devx package
Port core modules (config, exceptions, i18n, api_clients, gitea_cli),
14 CI scripts, 6 dev tools, 5 molecule tools, CLI entry point, workflows,
Makefile, tests (784 tests, 100% coverage), and documentation from GRM.

The devx package is published to the Gitea PyPI registry and consumed
by GRM, infra, and other oblachno-oss projects as a pip dependency.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-22 17:01:20 +02:00

170 lines
6.1 KiB
Python

"""Unit tests for devx.tools.setup."""
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner
from devx.tools.setup import (
_install_pre_commit_hooks,
_install_python_deps,
_run,
_verify,
main,
)
class TestRun:
@patch("devx.tools.setup.subprocess.run")
def test_run_success(self, mock_run: MagicMock) -> None:
_run(["echo", "hello"])
mock_run.assert_called_once_with(["echo", "hello"], check=True)
@patch("devx.tools.setup.subprocess.run")
def test_run_failure_raises(self, mock_run: MagicMock) -> None:
mock_run.side_effect = subprocess.CalledProcessError(1, ["echo", "hello"])
with pytest.raises(subprocess.CalledProcessError):
_run(["echo", "hello"])
class TestInstallPythonDeps:
@patch("devx.tools.setup._run")
def test_install_dev(self, mock_run: MagicMock) -> None:
_install_python_deps(".venv/bin", "dev")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"])
@patch("devx.tools.setup._run")
def test_install_ci(self, mock_run: MagicMock) -> None:
_install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"])
@patch("devx.tools.setup._run")
def test_install_custom_extras(self, mock_run: MagicMock) -> None:
_install_python_deps(".venv/bin", "ci,lint")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"])
class TestInstallPreCommitHooks:
@patch("devx.tools.setup._run")
def test_installs_all_hook_types(self, mock_run: MagicMock) -> None:
_install_pre_commit_hooks(".venv/bin")
assert mock_run.call_count == 3
hook_types = [call.args[0][3] for call in mock_run.call_args_list]
assert "pre-commit" in hook_types
assert "commit-msg" in hook_types
assert "pre-push" in hook_types
class TestVerify:
@patch("devx.tools.setup.subprocess.run")
def test_verify_runs_tools(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0, stdout="devx 0.1.0", stderr="")
_verify(".venv/bin")
# Should try to run --version for devx and pre-commit
assert mock_run.call_count >= 1
@patch("devx.tools.setup.subprocess.run")
def test_verify_handles_missing_tools(self, mock_run: MagicMock) -> None:
mock_run.side_effect = FileNotFoundError("not found")
_verify(".venv/bin") # Should not raise
@patch("devx.tools.setup.subprocess.run")
def test_verify_handles_timeout(self, mock_run: MagicMock) -> None:
mock_run.side_effect = subprocess.TimeoutExpired(cmd="devx", timeout=10)
_verify(".venv/bin") # Should not raise
class TestMain:
@patch("devx.tools.setup._verify")
@patch("devx.tools.setup._install_pre_commit_hooks")
@patch("devx.tools.setup._install_python_deps")
def test_main_success(
self,
mock_install_deps: MagicMock,
mock_install_hooks: MagicMock,
mock_verify: MagicMock,
tmp_path: Path,
) -> None:
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
runner = CliRunner()
result = runner.invoke(main, ["--bin", str(bin_dir)])
assert result.exit_code == 0
mock_install_deps.assert_called_once()
mock_install_hooks.assert_called_once()
mock_verify.assert_called_once()
@patch("devx.tools.setup._verify")
@patch("devx.tools.setup._install_pre_commit_hooks")
@patch("devx.tools.setup._install_python_deps")
def test_main_no_pre_commit(
self,
mock_install_deps: MagicMock,
mock_install_hooks: MagicMock,
mock_verify: MagicMock,
tmp_path: Path,
) -> None:
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
runner = CliRunner()
result = runner.invoke(main, ["--bin", str(bin_dir), "--no-pre-commit"])
assert result.exit_code == 0
mock_install_deps.assert_called_once()
mock_install_hooks.assert_not_called()
@patch("devx.tools.setup._verify")
@patch("devx.tools.setup._install_pre_commit_hooks")
@patch("devx.tools.setup._install_python_deps")
def test_main_custom_extras(
self,
mock_install_deps: MagicMock,
mock_install_hooks: MagicMock,
mock_verify: MagicMock,
tmp_path: Path,
) -> None:
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
runner = CliRunner()
result = runner.invoke(main, ["--bin", str(bin_dir), "--extras", "ci"])
assert result.exit_code == 0
mock_install_deps.assert_called_once_with(str(bin_dir), "ci")
@patch("devx.tools.setup._verify")
@patch("devx.tools.setup._install_pre_commit_hooks")
@patch("devx.tools.setup._install_python_deps")
def test_main_no_tea_login_noop(
self,
mock_install_deps: MagicMock,
mock_install_hooks: MagicMock,
mock_verify: MagicMock,
tmp_path: Path,
) -> None:
"""--no-tea-login is a no-op (kept for backwards compatibility)."""
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
runner = CliRunner()
result = runner.invoke(main, ["--bin", str(bin_dir), "--no-tea-login"])
assert result.exit_code == 0
def test_main_missing_bin_dir(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--bin", "/nonexistent/path"])
assert result.exit_code != 0
assert "Bin directory not found" in result.output
def test_main_module_block(tmp_path: Path) -> None:
"""Test the __main__ block execution."""
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
with patch.dict("os.environ", {}, clear=True):
with patch("devx.tools.setup._install_python_deps") as mock_deps:
with patch("devx.tools.setup._install_pre_commit_hooks"):
with patch("devx.tools.setup._verify"):
runner = CliRunner()
result = runner.invoke(main, ["--bin", str(bin_dir)])
assert result.exit_code == 0
mock_deps.assert_called_once()