Coverage for tests / unit / cli_utils / commands / test_install_command.py: 100%
96 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for the ``lintro install`` CLI command."""
3from __future__ import annotations
5from typing import Any
6from unittest.mock import MagicMock, patch
8from assertpy import assert_that
9from click.testing import CliRunner
11from lintro.cli_utils.commands.install import install_command
12from lintro.tools.core.tool_installer import InstallPlan, InstallResult
13from lintro.tools.core.tool_registry import ManifestTool
15# ── Helpers ──────────────────────────────────────────────────────────
18def _make_tool(name: str = "ruff", version: str = "0.14.0") -> ManifestTool:
19 """Build a ManifestTool for testing."""
20 return ManifestTool(
21 name=name,
22 version=version,
23 install_type="pip",
24 tier="tools",
25 category="bundled",
26 version_command=(name, "--version"),
27 )
30def _mock_registry() -> MagicMock:
31 """Build a mock ToolRegistry."""
32 registry = MagicMock()
33 registry.profile_names = ["minimal", "recommended", "complete", "ci"]
34 registry.__contains__ = lambda self, name: name in ("ruff", "mypy")
35 registry.all_tools.return_value = [_make_tool("ruff"), _make_tool("mypy")]
36 registry.get.side_effect = _make_tool
37 registry.tools_for_profile.return_value = [_make_tool("ruff")]
38 return registry
41def _patches() -> tuple[Any, Any]:
42 """Common patches for install CLI tests."""
43 registry = _mock_registry()
44 return (
45 patch(
46 "lintro.cli_utils.commands.install.ToolRegistry.load",
47 return_value=registry,
48 ),
49 patch(
50 "lintro.cli_utils.commands.install.RuntimeContext.detect",
51 return_value=MagicMock(),
52 ),
53 )
56# ── CLI invocation ───────────────────────────────────────────────────
59def test_install_all_already_installed() -> None:
60 """Exit 0 when all tools are already installed."""
61 runner = CliRunner()
62 p1, p2 = _patches()
64 plan = InstallPlan(already_ok=[_make_tool()])
65 with (
66 p1,
67 p2,
68 patch(
69 "lintro.cli_utils.commands.install.ToolInstaller",
70 ) as mock_cls,
71 patch("lintro.cli_utils.commands.install._detect_languages", return_value=[]),
72 ):
73 mock_cls.return_value.plan.return_value = plan
74 result = runner.invoke(install_command, [])
76 assert_that(result.exit_code).is_equal_to(0)
77 assert_that(result.output).contains("already installed")
80def test_install_specific_tools() -> None:
81 """Install specific tool names passed as positional args."""
82 runner = CliRunner()
83 p1, p2 = _patches()
85 tool = _make_tool()
86 plan = InstallPlan(to_install=[(tool, "pip install ruff>=0.14.0")])
87 with (
88 p1,
89 p2,
90 patch(
91 "lintro.cli_utils.commands.install.ToolInstaller",
92 ) as mock_cls,
93 ):
94 mock_cls.return_value.plan.return_value = plan
95 mock_cls.return_value.execute.return_value = [
96 InstallResult(tool=tool, success=True, message="OK", duration_seconds=1.0),
97 ]
98 result = runner.invoke(install_command, ["ruff"])
100 assert_that(result.exit_code).is_equal_to(0)
101 assert_that(result.output).contains("ruff")
104def test_install_dry_run() -> None:
105 """--dry-run shows plan without executing."""
106 runner = CliRunner()
107 p1, p2 = _patches()
109 tool = _make_tool()
110 plan = InstallPlan(to_install=[(tool, "pip install ruff>=0.14.0")])
111 with (
112 p1,
113 p2,
114 patch(
115 "lintro.cli_utils.commands.install.ToolInstaller",
116 ) as mock_cls,
117 ):
118 mock_cls.return_value.plan.return_value = plan
119 result = runner.invoke(install_command, ["ruff", "--dry-run"])
121 assert_that(result.exit_code).is_equal_to(0)
122 assert_that(result.output).contains("Dry run")
123 # execute should NOT have been called
124 mock_cls.return_value.execute.assert_not_called()
127def test_install_conflicting_selectors() -> None:
128 """Tools + --profile raises UsageError."""
129 runner = CliRunner()
130 p1, p2 = _patches()
132 with p1, p2:
133 result = runner.invoke(install_command, ["ruff", "--profile", "minimal"])
135 assert_that(result.exit_code).is_not_equal_to(0)
136 assert_that(result.output).contains("Cannot combine")
139def test_install_unknown_tool_name() -> None:
140 """Unknown tool name raises UsageError."""
141 runner = CliRunner()
142 p1, p2 = _patches()
144 with p1, p2:
145 result = runner.invoke(install_command, ["nonexistent"])
147 assert_that(result.exit_code).is_not_equal_to(0)
148 assert_that(result.output).contains("Unknown tools")
151def test_install_unknown_profile() -> None:
152 """Unknown profile name raises UsageError."""
153 runner = CliRunner()
154 p1, p2 = _patches()
156 with p1, p2:
157 result = runner.invoke(install_command, ["--profile", "nonexistent"])
159 assert_that(result.exit_code).is_not_equal_to(0)
160 assert_that(result.output).contains("Unknown profile")
163def test_install_all_flag() -> None:
164 """--all resolves to the 'complete' profile."""
165 runner = CliRunner()
166 p1, p2 = _patches()
168 plan = InstallPlan(already_ok=[_make_tool()])
169 with (
170 p1,
171 p2,
172 patch(
173 "lintro.cli_utils.commands.install.ToolInstaller",
174 ) as mock_cls,
175 ):
176 mock_cls.return_value.plan.return_value = plan
177 result = runner.invoke(install_command, ["--all"])
179 assert_that(result.exit_code).is_equal_to(0)
180 # Verify the plan was called with profile="complete"
181 assert_that(
182 mock_cls.return_value.plan.call_args.kwargs["profile"],
183 ).is_equal_to("complete")
186def test_install_failure_exit_1() -> None:
187 """Failed installs produce exit code 1."""
188 runner = CliRunner()
189 p1, p2 = _patches()
191 tool = _make_tool()
192 plan = InstallPlan(to_install=[(tool, "pip install ruff>=0.14.0")])
193 with (
194 p1,
195 p2,
196 patch(
197 "lintro.cli_utils.commands.install.ToolInstaller",
198 ) as mock_cls,
199 ):
200 mock_cls.return_value.plan.return_value = plan
201 mock_cls.return_value.execute.return_value = [
202 InstallResult(tool=tool, success=False, message="Command failed"),
203 ]
204 result = runner.invoke(install_command, ["ruff"])
206 assert_that(result.exit_code).is_equal_to(1)
209# ── _detect_languages ────────────────────────────────────────────────
212def test_detect_languages_returns_list() -> None:
213 """_detect_languages returns a list without raising."""
214 from lintro.cli_utils.commands.install import _detect_languages
216 result = _detect_languages()
217 assert_that(result).is_instance_of(list)