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

1"""Tests for the ``lintro install`` CLI command.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6from unittest.mock import MagicMock, patch 

7 

8from assertpy import assert_that 

9from click.testing import CliRunner 

10 

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 

14 

15# ── Helpers ────────────────────────────────────────────────────────── 

16 

17 

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 ) 

28 

29 

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 

39 

40 

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 ) 

54 

55 

56# ── CLI invocation ─────────────────────────────────────────────────── 

57 

58 

59def test_install_all_already_installed() -> None: 

60 """Exit 0 when all tools are already installed.""" 

61 runner = CliRunner() 

62 p1, p2 = _patches() 

63 

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, []) 

75 

76 assert_that(result.exit_code).is_equal_to(0) 

77 assert_that(result.output).contains("already installed") 

78 

79 

80def test_install_specific_tools() -> None: 

81 """Install specific tool names passed as positional args.""" 

82 runner = CliRunner() 

83 p1, p2 = _patches() 

84 

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

99 

100 assert_that(result.exit_code).is_equal_to(0) 

101 assert_that(result.output).contains("ruff") 

102 

103 

104def test_install_dry_run() -> None: 

105 """--dry-run shows plan without executing.""" 

106 runner = CliRunner() 

107 p1, p2 = _patches() 

108 

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

120 

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

125 

126 

127def test_install_conflicting_selectors() -> None: 

128 """Tools + --profile raises UsageError.""" 

129 runner = CliRunner() 

130 p1, p2 = _patches() 

131 

132 with p1, p2: 

133 result = runner.invoke(install_command, ["ruff", "--profile", "minimal"]) 

134 

135 assert_that(result.exit_code).is_not_equal_to(0) 

136 assert_that(result.output).contains("Cannot combine") 

137 

138 

139def test_install_unknown_tool_name() -> None: 

140 """Unknown tool name raises UsageError.""" 

141 runner = CliRunner() 

142 p1, p2 = _patches() 

143 

144 with p1, p2: 

145 result = runner.invoke(install_command, ["nonexistent"]) 

146 

147 assert_that(result.exit_code).is_not_equal_to(0) 

148 assert_that(result.output).contains("Unknown tools") 

149 

150 

151def test_install_unknown_profile() -> None: 

152 """Unknown profile name raises UsageError.""" 

153 runner = CliRunner() 

154 p1, p2 = _patches() 

155 

156 with p1, p2: 

157 result = runner.invoke(install_command, ["--profile", "nonexistent"]) 

158 

159 assert_that(result.exit_code).is_not_equal_to(0) 

160 assert_that(result.output).contains("Unknown profile") 

161 

162 

163def test_install_all_flag() -> None: 

164 """--all resolves to the 'complete' profile.""" 

165 runner = CliRunner() 

166 p1, p2 = _patches() 

167 

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

178 

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

184 

185 

186def test_install_failure_exit_1() -> None: 

187 """Failed installs produce exit code 1.""" 

188 runner = CliRunner() 

189 p1, p2 = _patches() 

190 

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

205 

206 assert_that(result.exit_code).is_equal_to(1) 

207 

208 

209# ── _detect_languages ──────────────────────────────────────────────── 

210 

211 

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 

215 

216 result = _detect_languages() 

217 assert_that(result).is_instance_of(list)