"""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()