Coverage for tests / unit / tools / core / test_install_strategies.py: 100%
243 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 lintro.tools.core.install_strategies package."""
3from __future__ import annotations
5from assertpy import assert_that
7from lintro.enums.install_context import InstallContext, PackageManager
8from lintro.tools.core.install_strategies import (
9 InstallEnvironment,
10 get_strategy,
11 strategy_registry,
12)
13from lintro.tools.core.install_strategies.brew_names import BREW_FORMULA_NAMES
15PM = PackageManager
17# ---------------------------------------------------------------------------
18# Helpers
19# ---------------------------------------------------------------------------
22def _make_env(
23 *,
24 managers: frozenset[PackageManager] = frozenset(PackageManager),
25 install_context: InstallContext = InstallContext.PIP,
26) -> InstallEnvironment:
27 """Build an InstallEnvironment with explicit manager set.
29 Args:
30 managers: Set of available package managers.
31 install_context: How lintro was installed.
33 Returns:
34 An InstallEnvironment instance.
35 """
36 return InstallEnvironment(
37 install_context=install_context,
38 available_managers=managers,
39 )
42# ---------------------------------------------------------------------------
43# InstallEnvironment
44# ---------------------------------------------------------------------------
47def test_install_environment_has_true() -> None:
48 """Return True when the manager is present in available_managers."""
49 env = _make_env()
51 assert_that(env.has(PM.UV)).is_true()
54def test_install_environment_has_false() -> None:
55 """Return False when available_managers is empty."""
56 env = _make_env(managers=frozenset())
58 assert_that(env.has(PM.UV)).is_false()
61# ---------------------------------------------------------------------------
62# Registry
63# ---------------------------------------------------------------------------
66def test_registry_contains_all_five() -> None:
67 """Strategy registry has entries for pip, npm, binary, cargo, and rustup."""
68 registry = strategy_registry()
70 assert_that(registry).contains_key("pip", "npm", "binary", "cargo", "rustup")
73def test_get_strategy_pip() -> None:
74 """get_strategy('pip') returns a strategy with install_type 'pip'."""
75 strategy = get_strategy("pip")
77 assert_that(strategy).is_not_none()
78 # narrow Optional for mypy — assertpy does not perform type narrowing
79 assert strategy is not None # noqa: S101
80 assert_that(strategy.install_type()).is_equal_to("pip")
83def test_get_strategy_unknown_returns_none() -> None:
84 """get_strategy returns None for an unregistered install type."""
85 result = get_strategy("magic")
87 assert_that(result).is_none()
90# ---------------------------------------------------------------------------
91# PipStrategy
92# ---------------------------------------------------------------------------
95def test_pip_install_hint_with_uv() -> None:
96 """Prefer 'uv pip install' when uv is available."""
97 env = _make_env(managers=frozenset({PM.UV}))
98 strategy = get_strategy("pip")
99 assert_that(strategy).is_not_none()
100 # narrow Optional for mypy — assertpy does not perform type narrowing
101 assert strategy is not None # noqa: S101
103 result = strategy.install_hint(env, "ruff", "0.14.0", "ruff", None)
105 assert_that(result).is_equal_to("uv pip install 'ruff>=0.14.0'")
108def test_pip_install_hint_without_uv() -> None:
109 """Fall back to 'pip install' when uv is absent but pip is present."""
110 env = _make_env(managers=frozenset({PM.PIP}))
111 strategy = get_strategy("pip")
112 assert_that(strategy).is_not_none()
113 # narrow Optional for mypy — assertpy does not perform type narrowing
114 assert strategy is not None # noqa: S101
116 result = strategy.install_hint(env, "ruff", "0.14.0", "ruff", None)
118 assert_that(result).is_equal_to("pip install 'ruff>=0.14.0'")
121def test_pip_install_hint_homebrew_context() -> None:
122 """Use brew install for mapped tools under HOMEBREW_FULL context."""
123 env = _make_env(
124 managers=frozenset({PM.BREW, PM.UV}),
125 install_context=InstallContext.HOMEBREW_FULL,
126 )
127 strategy = get_strategy("pip")
128 assert_that(strategy).is_not_none()
129 # narrow Optional for mypy — assertpy does not perform type narrowing
130 assert strategy is not None # noqa: S101
132 result = strategy.install_hint(
133 env,
134 "markdownlint",
135 "0.12.0",
136 "markdownlint-cli2",
137 None,
138 )
140 assert_that(result).is_equal_to("brew install markdownlint-cli2")
143def test_pip_upgrade_hint() -> None:
144 """Generate upgrade hint with --upgrade flag when uv is available."""
145 env = _make_env(managers=frozenset({PM.UV}))
146 strategy = get_strategy("pip")
147 assert_that(strategy).is_not_none()
148 # narrow Optional for mypy — assertpy does not perform type narrowing
149 assert strategy is not None # noqa: S101
151 result = strategy.upgrade_hint(env, "ruff", "0.14.0", "ruff", None)
153 assert_that(result).is_equal_to("uv pip install --upgrade 'ruff>=0.14.0'")
156def test_pip_upgrade_hint_homebrew() -> None:
157 """Use brew upgrade for mapped tools under HOMEBREW_FULL context."""
158 env = _make_env(
159 managers=frozenset({PM.BREW, PM.UV}),
160 install_context=InstallContext.HOMEBREW_FULL,
161 )
162 strategy = get_strategy("pip")
163 assert_that(strategy).is_not_none()
164 # narrow Optional for mypy — assertpy does not perform type narrowing
165 assert strategy is not None # noqa: S101
167 result = strategy.upgrade_hint(
168 env,
169 "markdownlint",
170 "0.12.0",
171 "markdownlint-cli2",
172 None,
173 )
175 assert_that(result).is_equal_to("brew upgrade markdownlint-cli2")
178def test_pip_check_prerequisites_met() -> None:
179 """Return None when uv is available."""
180 env = _make_env(managers=frozenset({PM.UV}))
181 strategy = get_strategy("pip")
182 assert_that(strategy).is_not_none()
183 # narrow Optional for mypy — assertpy does not perform type narrowing
184 assert strategy is not None # noqa: S101
186 result = strategy.check_prerequisites(env, "ruff")
188 assert_that(result).is_none()
191def test_pip_check_prerequisites_not_met() -> None:
192 """Return skip reason when neither uv nor pip is available."""
193 env = _make_env(managers=frozenset())
194 strategy = get_strategy("pip")
195 assert_that(strategy).is_not_none()
196 # narrow Optional for mypy — assertpy does not perform type narrowing
197 assert strategy is not None # noqa: S101
199 result = strategy.check_prerequisites(env, "ruff")
201 assert_that(result).is_equal_to("uv/pip not available")
204def test_pip_brew_only_non_homebrew_context() -> None:
205 """Brew-only env with PIP context: prereq passes for mapped tools, hint uses brew."""
206 env = _make_env(
207 managers=frozenset({PM.BREW}),
208 install_context=InstallContext.PIP,
209 )
210 strategy = get_strategy("pip")
211 assert_that(strategy).is_not_none()
212 # narrow Optional for mypy — assertpy does not perform type narrowing
213 assert strategy is not None # noqa: S101
215 # Prerequisites pass for a tool with a brew formula
216 assert_that(strategy.check_prerequisites(env, "markdownlint")).is_none()
218 # Install hint uses brew (not pip) since pip is unavailable
219 hint = strategy.install_hint(
220 env,
221 "markdownlint",
222 "0.22.0",
223 "markdownlint-cli2",
224 None,
225 )
226 assert_that(hint).is_equal_to("brew install markdownlint-cli2")
228 # Unmapped tool without pip/uv fails prereqs
229 assert_that(strategy.check_prerequisites(env, "ruff")).is_equal_to(
230 "uv/pip not available",
231 )
234def test_pip_is_available_true() -> None:
235 """Return True when uv is available."""
236 env = _make_env(managers=frozenset({PM.UV}))
237 strategy = get_strategy("pip")
238 assert_that(strategy).is_not_none()
239 # narrow Optional for mypy — assertpy does not perform type narrowing
240 assert strategy is not None # noqa: S101
242 assert_that(strategy.is_available(env)).is_true()
245def test_pip_is_available_false() -> None:
246 """Return False when no relevant package manager is available."""
247 env = _make_env(managers=frozenset())
248 strategy = get_strategy("pip")
249 assert_that(strategy).is_not_none()
250 # narrow Optional for mypy — assertpy does not perform type narrowing
251 assert strategy is not None # noqa: S101
253 assert_that(strategy.is_available(env)).is_false()
256# ---------------------------------------------------------------------------
257# NpmStrategy
258# ---------------------------------------------------------------------------
261def test_npm_install_hint_with_bun() -> None:
262 """Prefer 'bun add -g' when bun is available."""
263 env = _make_env(managers=frozenset({PM.BUN}))
264 strategy = get_strategy("npm")
265 assert_that(strategy).is_not_none()
266 # narrow Optional for mypy — assertpy does not perform type narrowing
267 assert strategy is not None # noqa: S101
269 result = strategy.install_hint(env, "prettier", "3.2.0", "prettier", None)
271 assert_that(result).is_equal_to("bun add -g prettier@3.2.0")
274def test_npm_install_hint_without_bun() -> None:
275 """Fall back to 'npm install -g' when bun is absent."""
276 env = _make_env(managers=frozenset({PM.NPM}))
277 strategy = get_strategy("npm")
278 assert_that(strategy).is_not_none()
279 # narrow Optional for mypy — assertpy does not perform type narrowing
280 assert strategy is not None # noqa: S101
282 result = strategy.install_hint(env, "prettier", "3.2.0", "prettier", None)
284 assert_that(result).is_equal_to("npm install -g prettier@3.2.0")
287def test_npm_upgrade_hint_with_bun() -> None:
288 """Npm replaces on install, so upgrade hint matches install hint."""
289 env = _make_env(managers=frozenset({PM.BUN}))
290 strategy = get_strategy("npm")
291 assert_that(strategy).is_not_none()
292 # narrow Optional for mypy — assertpy does not perform type narrowing
293 assert strategy is not None # noqa: S101
295 result = strategy.upgrade_hint(env, "prettier", "3.2.0", "prettier", None)
297 assert_that(result).is_equal_to("bun add -g prettier@3.2.0")
300def test_npm_check_prerequisites_met() -> None:
301 """Return None when npm is available."""
302 env = _make_env(managers=frozenset({PM.NPM}))
303 strategy = get_strategy("npm")
304 assert_that(strategy).is_not_none()
305 # narrow Optional for mypy — assertpy does not perform type narrowing
306 assert strategy is not None # noqa: S101
308 result = strategy.check_prerequisites(env, "prettier")
310 assert_that(result).is_none()
313def test_npm_check_prerequisites_not_met() -> None:
314 """Return skip reason when neither bun nor npm is available."""
315 env = _make_env(managers=frozenset())
316 strategy = get_strategy("npm")
317 assert_that(strategy).is_not_none()
318 # narrow Optional for mypy — assertpy does not perform type narrowing
319 assert strategy is not None # noqa: S101
321 result = strategy.check_prerequisites(env, "prettier")
323 assert_that(result).is_equal_to("bun/npm not available (install Node.js first)")
326def test_npm_brew_only_mapped_tool() -> None:
327 """Brew-only env: mapped tool gets brew install hint and passes prereqs."""
328 env = _make_env(
329 managers=frozenset({PM.BREW}),
330 install_context=InstallContext.PIP,
331 )
332 strategy = get_strategy("npm")
333 assert_that(strategy).is_not_none()
334 # narrow Optional for mypy — assertpy does not perform type narrowing
335 assert strategy is not None # noqa: S101
337 assert_that(strategy.check_prerequisites(env, "markdownlint")).is_none()
338 hint = strategy.install_hint(
339 env,
340 "markdownlint",
341 "0.22.0",
342 "markdownlint-cli2",
343 None,
344 )
345 assert_that(hint).is_equal_to("brew install markdownlint-cli2")
348def test_npm_brew_only_unmapped_tool() -> None:
349 """Brew-only env: unmapped tool fails prereqs."""
350 env = _make_env(
351 managers=frozenset({PM.BREW}),
352 install_context=InstallContext.PIP,
353 )
354 strategy = get_strategy("npm")
355 assert_that(strategy).is_not_none()
356 # narrow Optional for mypy — assertpy does not perform type narrowing
357 assert strategy is not None # noqa: S101
359 result = strategy.check_prerequisites(env, "prettier")
360 assert_that(result).is_equal_to("bun/npm not available (install Node.js first)")
363# ---------------------------------------------------------------------------
364# BinaryStrategy
365# ---------------------------------------------------------------------------
368def test_binary_install_hint_with_brew() -> None:
369 """Generate brew install when Homebrew is available."""
370 env = _make_env(managers=frozenset({PM.BREW}))
371 strategy = get_strategy("binary")
372 assert_that(strategy).is_not_none()
373 # narrow Optional for mypy — assertpy does not perform type narrowing
374 assert strategy is not None # noqa: S101
376 result = strategy.install_hint(env, "hadolint", "2.12.0", "hadolint", None)
378 assert_that(result).is_equal_to("brew install hadolint")
381def test_binary_install_hint_without_brew() -> None:
382 """Fall back to GitHub search URL when brew is unavailable."""
383 env = _make_env(managers=frozenset())
384 strategy = get_strategy("binary")
385 assert_that(strategy).is_not_none()
386 # narrow Optional for mypy — assertpy does not perform type narrowing
387 assert strategy is not None # noqa: S101
389 result = strategy.install_hint(env, "hadolint", "2.12.0", "hadolint", None)
391 assert_that(result).starts_with("See https://")
392 assert_that(result).contains("hadolint")
395def test_binary_upgrade_hint_with_brew() -> None:
396 """Generate brew upgrade hint when Homebrew is available."""
397 env = _make_env(managers=frozenset({PM.BREW}))
398 strategy = get_strategy("binary")
399 assert_that(strategy).is_not_none()
400 # narrow Optional for mypy — assertpy does not perform type narrowing
401 assert strategy is not None # noqa: S101
403 result = strategy.upgrade_hint(env, "hadolint", "2.12.0", "hadolint", None)
405 assert_that(result).is_equal_to("brew upgrade hadolint")
408def test_binary_check_prerequisites_always_none() -> None:
409 """Binary strategy never fails prerequisite checks."""
410 env = _make_env(managers=frozenset())
411 strategy = get_strategy("binary")
412 assert_that(strategy).is_not_none()
413 # narrow Optional for mypy — assertpy does not perform type narrowing
414 assert strategy is not None # noqa: S101
416 result = strategy.check_prerequisites(env, "hadolint")
418 assert_that(result).is_none()
421# ---------------------------------------------------------------------------
422# CargoStrategy
423# ---------------------------------------------------------------------------
426def test_cargo_install_hint() -> None:
427 """Generate cargo install command for a crate."""
428 env = _make_env(managers=frozenset({PM.CARGO}))
429 strategy = get_strategy("cargo")
430 assert_that(strategy).is_not_none()
431 # narrow Optional for mypy — assertpy does not perform type narrowing
432 assert strategy is not None # noqa: S101
434 result = strategy.install_hint(
435 env,
436 "cargo-audit",
437 "0.20.0",
438 "cargo-audit",
439 None,
440 )
442 assert_that(result).is_equal_to("cargo install cargo-audit")
445def test_cargo_upgrade_hint() -> None:
446 """Generate cargo install --force command for upgrades."""
447 env = _make_env(managers=frozenset({PM.CARGO}))
448 strategy = get_strategy("cargo")
449 assert_that(strategy).is_not_none()
450 # narrow Optional for mypy — assertpy does not perform type narrowing
451 assert strategy is not None # noqa: S101
453 result = strategy.upgrade_hint(
454 env,
455 "cargo-audit",
456 "0.20.0",
457 "cargo-audit",
458 None,
459 )
461 assert_that(result).is_equal_to("cargo install --force cargo-audit")
464def test_cargo_check_prerequisites_met() -> None:
465 """Return None when cargo is available."""
466 env = _make_env(managers=frozenset({PM.CARGO}))
467 strategy = get_strategy("cargo")
468 assert_that(strategy).is_not_none()
469 # narrow Optional for mypy — assertpy does not perform type narrowing
470 assert strategy is not None # noqa: S101
472 result = strategy.check_prerequisites(env, "cargo-audit")
474 assert_that(result).is_none()
477def test_cargo_check_prerequisites_not_met() -> None:
478 """Return skip reason when cargo is not available."""
479 env = _make_env(managers=frozenset())
480 strategy = get_strategy("cargo")
481 assert_that(strategy).is_not_none()
482 # narrow Optional for mypy — assertpy does not perform type narrowing
483 assert strategy is not None # noqa: S101
485 result = strategy.check_prerequisites(env, "cargo-audit")
487 assert_that(result).is_equal_to("cargo not available (install Rust first)")
490# ---------------------------------------------------------------------------
491# RustupStrategy
492# ---------------------------------------------------------------------------
495def test_rustup_install_hint_with_component() -> None:
496 """Generate rustup component add when component is specified."""
497 env = _make_env(managers=frozenset({PM.RUSTUP}))
498 strategy = get_strategy("rustup")
499 assert_that(strategy).is_not_none()
500 # narrow Optional for mypy — assertpy does not perform type narrowing
501 assert strategy is not None # noqa: S101
503 result = strategy.install_hint(env, "clippy", "0.1.0", None, "clippy")
505 assert_that(result).is_equal_to("rustup component add clippy")
508def test_rustup_install_hint_without_component() -> None:
509 """Generate rustup toolchain install when no component is specified."""
510 env = _make_env(managers=frozenset({PM.RUSTUP}))
511 strategy = get_strategy("rustup")
512 assert_that(strategy).is_not_none()
513 # narrow Optional for mypy — assertpy does not perform type narrowing
514 assert strategy is not None # noqa: S101
516 result = strategy.install_hint(env, "rustfmt", "1.0.0", None, None)
518 assert_that(result).is_equal_to("rustup toolchain install stable")
521def test_rustup_upgrade_hint() -> None:
522 """Generate rustup update stable for upgrades."""
523 env = _make_env(managers=frozenset({PM.RUSTUP}))
524 strategy = get_strategy("rustup")
525 assert_that(strategy).is_not_none()
526 # narrow Optional for mypy — assertpy does not perform type narrowing
527 assert strategy is not None # noqa: S101
529 result = strategy.upgrade_hint(env, "clippy", "0.1.0", None, "clippy")
531 assert_that(result).is_equal_to("rustup update stable")
534def test_rustup_check_prerequisites_met() -> None:
535 """Return None when rustup is available."""
536 env = _make_env(managers=frozenset({PM.RUSTUP}))
537 strategy = get_strategy("rustup")
538 assert_that(strategy).is_not_none()
539 # narrow Optional for mypy — assertpy does not perform type narrowing
540 assert strategy is not None # noqa: S101
542 result = strategy.check_prerequisites(env, "clippy")
544 assert_that(result).is_none()
547def test_rustup_check_prerequisites_not_met() -> None:
548 """Return skip reason when rustup is not available."""
549 env = _make_env(managers=frozenset())
550 strategy = get_strategy("rustup")
551 assert_that(strategy).is_not_none()
552 # narrow Optional for mypy — assertpy does not perform type narrowing
553 assert strategy is not None # noqa: S101
555 result = strategy.check_prerequisites(env, "clippy")
557 assert_that(result).is_equal_to("clippy: rustup not available (install Rust first)")
560# ---------------------------------------------------------------------------
561# BREW_FORMULA_NAMES
562# ---------------------------------------------------------------------------
565def test_brew_formula_names_markdownlint() -> None:
566 """Verify markdownlint maps to markdownlint-cli2."""
567 assert_that(BREW_FORMULA_NAMES).contains_key("markdownlint")
568 assert_that(BREW_FORMULA_NAMES["markdownlint"]).is_equal_to(
569 "markdownlint-cli2",
570 )
573def test_brew_formula_names_osv_scanner() -> None:
574 """Verify osv_scanner maps to osv-scanner."""
575 assert_that(BREW_FORMULA_NAMES).contains_key("osv_scanner")
576 assert_that(BREW_FORMULA_NAMES["osv_scanner"]).is_equal_to("osv-scanner")