Coverage for tests / unit / cli_utils / commands / test_setup_command.py: 100%

127 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

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

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6from typing import Any 

7from unittest.mock import MagicMock, patch 

8 

9import pytest 

10from assertpy import assert_that 

11from click.testing import CliRunner 

12 

13from lintro.cli_utils.commands.setup import _generate_config, setup_command 

14from lintro.utils.project_detection import ( 

15 detect_package_managers, 

16 detect_project_languages, 

17) 

18 

19# ── detect_project_languages ───────────────────────────────────────── 

20 

21 

22def test_detect_python(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

23 """Detect Python when pyproject.toml exists.""" 

24 (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n") 

25 monkeypatch.chdir(tmp_path) 

26 

27 langs = detect_project_languages() 

28 assert_that(langs).contains("python") 

29 

30 

31def test_detect_javascript(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

32 """Detect JavaScript when package.json exists.""" 

33 (tmp_path / "package.json").write_text('{"name":"x"}') 

34 monkeypatch.chdir(tmp_path) 

35 

36 langs = detect_project_languages() 

37 assert_that(langs).contains("javascript") 

38 

39 

40def test_detect_typescript_via_tsconfig( 

41 tmp_path: Path, 

42 monkeypatch: pytest.MonkeyPatch, 

43) -> None: 

44 """Detect TypeScript when tsconfig.json exists.""" 

45 (tmp_path / "package.json").write_text('{"name":"x"}') 

46 (tmp_path / "tsconfig.json").write_text("{}") 

47 monkeypatch.chdir(tmp_path) 

48 

49 langs = detect_project_languages() 

50 assert_that(langs).contains("typescript") 

51 

52 

53def test_detect_rust(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

54 """Detect Rust when Cargo.toml exists.""" 

55 (tmp_path / "Cargo.toml").write_text('[package]\nname = "x"\n') 

56 monkeypatch.chdir(tmp_path) 

57 

58 langs = detect_project_languages() 

59 assert_that(langs).contains("rust") 

60 

61 

62def test_detect_shell(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

63 """Detect Shell when .sh files exist in scripts/ directory.""" 

64 scripts = tmp_path / "scripts" 

65 scripts.mkdir() 

66 (scripts / "deploy.sh").write_text("#!/bin/bash\n") 

67 monkeypatch.chdir(tmp_path) 

68 

69 langs = detect_project_languages() 

70 assert_that(langs).contains("shell") 

71 

72 

73def test_detect_docker(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

74 """Detect Docker when Dockerfile exists.""" 

75 (tmp_path / "Dockerfile").write_text("FROM python:3.13\n") 

76 monkeypatch.chdir(tmp_path) 

77 

78 langs = detect_project_languages() 

79 assert_that(langs).contains("docker") 

80 

81 

82def test_detect_github_actions( 

83 tmp_path: Path, 

84 monkeypatch: pytest.MonkeyPatch, 

85) -> None: 

86 """Detect GitHub Actions when .github/workflows/ exists.""" 

87 (tmp_path / ".github" / "workflows").mkdir(parents=True) 

88 monkeypatch.chdir(tmp_path) 

89 

90 langs = detect_project_languages() 

91 assert_that(langs).contains("github_actions") 

92 

93 

94def test_detect_multiple_languages( 

95 tmp_path: Path, 

96 monkeypatch: pytest.MonkeyPatch, 

97) -> None: 

98 """Detect multiple languages from various indicators.""" 

99 (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n") 

100 (tmp_path / "Dockerfile").write_text("FROM python:3.13\n") 

101 (tmp_path / ".github" / "workflows").mkdir(parents=True) 

102 monkeypatch.chdir(tmp_path) 

103 

104 langs = detect_project_languages() 

105 assert_that(langs).contains("python", "docker", "github_actions") 

106 

107 

108def test_detect_empty_project( 

109 tmp_path: Path, 

110 monkeypatch: pytest.MonkeyPatch, 

111) -> None: 

112 """Return empty list for a project with no indicator files.""" 

113 monkeypatch.chdir(tmp_path) 

114 

115 langs = detect_project_languages() 

116 assert_that(langs).is_empty() 

117 

118 

119# ── detect_package_managers ────────────────────────────────────────── 

120 

121 

122def test_detect_uv_manager( 

123 tmp_path: Path, 

124 monkeypatch: pytest.MonkeyPatch, 

125) -> None: 

126 """Detect uv when pyproject.toml exists and uv is available.""" 

127 (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n") 

128 monkeypatch.chdir(tmp_path) 

129 

130 with patch( 

131 "lintro.utils.project_detection.shutil.which", 

132 side_effect=lambda n: "/bin/uv" if n == "uv" else None, 

133 ): 

134 managers = detect_package_managers() 

135 

136 assert_that(managers).contains_key("uv") 

137 

138 

139def test_detect_pip_manager_no_uv( 

140 tmp_path: Path, 

141 monkeypatch: pytest.MonkeyPatch, 

142) -> None: 

143 """Fall back to pip when uv is not available.""" 

144 (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n") 

145 monkeypatch.chdir(tmp_path) 

146 

147 with patch("lintro.utils.project_detection.shutil.which", return_value=None): 

148 managers = detect_package_managers() 

149 

150 assert_that(managers).contains_key("pip") 

151 

152 

153def test_detect_bun_manager( 

154 tmp_path: Path, 

155 monkeypatch: pytest.MonkeyPatch, 

156) -> None: 

157 """Detect bun when package.json exists and bun is available.""" 

158 (tmp_path / "package.json").write_text('{"name":"x"}') 

159 monkeypatch.chdir(tmp_path) 

160 

161 with patch( 

162 "lintro.utils.project_detection.shutil.which", 

163 side_effect=lambda n: "/bin/bun" if n == "bun" else None, 

164 ): 

165 managers = detect_package_managers() 

166 

167 assert_that(managers).contains_key("bun") 

168 

169 

170# ── _generate_config ───────────────────────────────────────────────── 

171 

172 

173def test_generate_config_structure() -> None: 

174 """Generated YAML contains expected sections.""" 

175 config = _generate_config(["ruff", "mypy"], ["python"]) 

176 

177 assert_that(config).contains("enforce:") 

178 assert_that(config).contains("execution:") 

179 assert_that(config).contains("tools:") 

180 assert_that(config).contains("ruff:") 

181 assert_that(config).contains("mypy:") 

182 

183 

184def test_generate_config_python_line_length() -> None: 

185 """Python projects get line_length in enforce section.""" 

186 config = _generate_config(["ruff"], ["python"]) 

187 assert_that(config).contains("line_length: 88") 

188 

189 

190def test_generate_config_no_python() -> None: 

191 """Non-Python projects omit line_length.""" 

192 config = _generate_config(["hadolint"], ["docker"]) 

193 assert_that(config).does_not_contain("line_length") 

194 

195 

196# ── CLI invocation ─────────────────────────────────────────────────── 

197 

198 

199def _patch_setup_deps() -> tuple[Any, Any, Any, Any]: 

200 """Common patches for setup CLI tests.""" 

201 registry = MagicMock() 

202 registry.profile_names = ["minimal", "recommended", "complete", "ci"] 

203 registry.profiles = {} 

204 registry.tools_for_profile.return_value = [] 

205 ctx = MagicMock() 

206 

207 return ( 

208 patch( 

209 "lintro.cli_utils.commands.setup.ToolRegistry.load", 

210 return_value=registry, 

211 ), 

212 patch( 

213 "lintro.cli_utils.commands.setup.RuntimeContext.detect", 

214 return_value=ctx, 

215 ), 

216 patch( 

217 "lintro.cli_utils.commands.setup.detect_project_languages", 

218 return_value=["python"], 

219 ), 

220 patch( 

221 "lintro.cli_utils.commands.setup.detect_package_managers", 

222 return_value={"uv": "pyproject.toml"}, 

223 ), 

224 ) 

225 

226 

227def test_setup_dry_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

228 """--dry-run does not write a config file.""" 

229 monkeypatch.chdir(tmp_path) 

230 runner = CliRunner() 

231 p1, p2, p3, p4 = _patch_setup_deps() 

232 

233 with p1, p2, p3, p4: 

234 result = runner.invoke( 

235 setup_command, 

236 ["--profile", "minimal", "--yes", "--dry-run"], 

237 ) 

238 

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

240 assert_that((tmp_path / ".lintro-config.yaml").exists()).is_false() 

241 

242 

243def test_setup_profile_yes(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 

244 """--profile minimal --yes runs non-interactively.""" 

245 monkeypatch.chdir(tmp_path) 

246 runner = CliRunner() 

247 p1, p2, p3, p4 = _patch_setup_deps() 

248 

249 with p1, p2, p3, p4: 

250 result = runner.invoke( 

251 setup_command, 

252 ["--profile", "minimal", "--yes", "--skip-install"], 

253 ) 

254 

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

256 assert_that((tmp_path / ".lintro-config.yaml").exists()).is_true() 

257 

258 

259def test_setup_invalid_profile( 

260 tmp_path: Path, 

261 monkeypatch: pytest.MonkeyPatch, 

262) -> None: 

263 """Invalid profile name raises an error.""" 

264 monkeypatch.chdir(tmp_path) 

265 runner = CliRunner() 

266 p1, p2, p3, p4 = _patch_setup_deps() 

267 

268 with p1, p2, p3, p4: 

269 result = runner.invoke(setup_command, ["--profile", "nonexistent", "--yes"]) 

270 

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

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

273 

274 

275def test_setup_skip_install( 

276 tmp_path: Path, 

277 monkeypatch: pytest.MonkeyPatch, 

278) -> None: 

279 """--skip-install skips the installer entirely.""" 

280 monkeypatch.chdir(tmp_path) 

281 runner = CliRunner() 

282 p1, p2, p3, p4 = _patch_setup_deps() 

283 

284 with p1, p2, p3, p4: 

285 result = runner.invoke( 

286 setup_command, 

287 ["--profile", "minimal", "--yes", "--skip-install"], 

288 ) 

289 

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