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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for the ``lintro setup`` CLI command."""
3from __future__ import annotations
5from pathlib import Path
6from typing import Any
7from unittest.mock import MagicMock, patch
9import pytest
10from assertpy import assert_that
11from click.testing import CliRunner
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)
19# ── detect_project_languages ─────────────────────────────────────────
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)
27 langs = detect_project_languages()
28 assert_that(langs).contains("python")
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)
36 langs = detect_project_languages()
37 assert_that(langs).contains("javascript")
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)
49 langs = detect_project_languages()
50 assert_that(langs).contains("typescript")
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)
58 langs = detect_project_languages()
59 assert_that(langs).contains("rust")
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)
69 langs = detect_project_languages()
70 assert_that(langs).contains("shell")
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)
78 langs = detect_project_languages()
79 assert_that(langs).contains("docker")
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)
90 langs = detect_project_languages()
91 assert_that(langs).contains("github_actions")
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)
104 langs = detect_project_languages()
105 assert_that(langs).contains("python", "docker", "github_actions")
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)
115 langs = detect_project_languages()
116 assert_that(langs).is_empty()
119# ── detect_package_managers ──────────────────────────────────────────
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)
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()
136 assert_that(managers).contains_key("uv")
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)
147 with patch("lintro.utils.project_detection.shutil.which", return_value=None):
148 managers = detect_package_managers()
150 assert_that(managers).contains_key("pip")
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)
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()
167 assert_that(managers).contains_key("bun")
170# ── _generate_config ─────────────────────────────────────────────────
173def test_generate_config_structure() -> None:
174 """Generated YAML contains expected sections."""
175 config = _generate_config(["ruff", "mypy"], ["python"])
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:")
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")
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")
196# ── CLI invocation ───────────────────────────────────────────────────
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()
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 )
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()
233 with p1, p2, p3, p4:
234 result = runner.invoke(
235 setup_command,
236 ["--profile", "minimal", "--yes", "--dry-run"],
237 )
239 assert_that(result.exit_code).is_equal_to(0)
240 assert_that((tmp_path / ".lintro-config.yaml").exists()).is_false()
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()
249 with p1, p2, p3, p4:
250 result = runner.invoke(
251 setup_command,
252 ["--profile", "minimal", "--yes", "--skip-install"],
253 )
255 assert_that(result.exit_code).is_equal_to(0)
256 assert_that((tmp_path / ".lintro-config.yaml").exists()).is_true()
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()
268 with p1, p2, p3, p4:
269 result = runner.invoke(setup_command, ["--profile", "nonexistent", "--yes"])
271 assert_that(result.exit_code).is_not_equal_to(0)
272 assert_that(result.output).contains("Unknown profile")
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()
284 with p1, p2, p3, p4:
285 result = runner.invoke(
286 setup_command,
287 ["--profile", "minimal", "--yes", "--skip-install"],
288 )
290 assert_that(result.exit_code).is_equal_to(0)