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

1"""Tests for lintro.tools.core.install_strategies package.""" 

2 

3from __future__ import annotations 

4 

5from assertpy import assert_that 

6 

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 

14 

15PM = PackageManager 

16 

17# --------------------------------------------------------------------------- 

18# Helpers 

19# --------------------------------------------------------------------------- 

20 

21 

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. 

28 

29 Args: 

30 managers: Set of available package managers. 

31 install_context: How lintro was installed. 

32 

33 Returns: 

34 An InstallEnvironment instance. 

35 """ 

36 return InstallEnvironment( 

37 install_context=install_context, 

38 available_managers=managers, 

39 ) 

40 

41 

42# --------------------------------------------------------------------------- 

43# InstallEnvironment 

44# --------------------------------------------------------------------------- 

45 

46 

47def test_install_environment_has_true() -> None: 

48 """Return True when the manager is present in available_managers.""" 

49 env = _make_env() 

50 

51 assert_that(env.has(PM.UV)).is_true() 

52 

53 

54def test_install_environment_has_false() -> None: 

55 """Return False when available_managers is empty.""" 

56 env = _make_env(managers=frozenset()) 

57 

58 assert_that(env.has(PM.UV)).is_false() 

59 

60 

61# --------------------------------------------------------------------------- 

62# Registry 

63# --------------------------------------------------------------------------- 

64 

65 

66def test_registry_contains_all_five() -> None: 

67 """Strategy registry has entries for pip, npm, binary, cargo, and rustup.""" 

68 registry = strategy_registry() 

69 

70 assert_that(registry).contains_key("pip", "npm", "binary", "cargo", "rustup") 

71 

72 

73def test_get_strategy_pip() -> None: 

74 """get_strategy('pip') returns a strategy with install_type 'pip'.""" 

75 strategy = get_strategy("pip") 

76 

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

81 

82 

83def test_get_strategy_unknown_returns_none() -> None: 

84 """get_strategy returns None for an unregistered install type.""" 

85 result = get_strategy("magic") 

86 

87 assert_that(result).is_none() 

88 

89 

90# --------------------------------------------------------------------------- 

91# PipStrategy 

92# --------------------------------------------------------------------------- 

93 

94 

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 

102 

103 result = strategy.install_hint(env, "ruff", "0.14.0", "ruff", None) 

104 

105 assert_that(result).is_equal_to("uv pip install 'ruff>=0.14.0'") 

106 

107 

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 

115 

116 result = strategy.install_hint(env, "ruff", "0.14.0", "ruff", None) 

117 

118 assert_that(result).is_equal_to("pip install 'ruff>=0.14.0'") 

119 

120 

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 

131 

132 result = strategy.install_hint( 

133 env, 

134 "markdownlint", 

135 "0.12.0", 

136 "markdownlint-cli2", 

137 None, 

138 ) 

139 

140 assert_that(result).is_equal_to("brew install markdownlint-cli2") 

141 

142 

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 

150 

151 result = strategy.upgrade_hint(env, "ruff", "0.14.0", "ruff", None) 

152 

153 assert_that(result).is_equal_to("uv pip install --upgrade 'ruff>=0.14.0'") 

154 

155 

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 

166 

167 result = strategy.upgrade_hint( 

168 env, 

169 "markdownlint", 

170 "0.12.0", 

171 "markdownlint-cli2", 

172 None, 

173 ) 

174 

175 assert_that(result).is_equal_to("brew upgrade markdownlint-cli2") 

176 

177 

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 

185 

186 result = strategy.check_prerequisites(env, "ruff") 

187 

188 assert_that(result).is_none() 

189 

190 

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 

198 

199 result = strategy.check_prerequisites(env, "ruff") 

200 

201 assert_that(result).is_equal_to("uv/pip not available") 

202 

203 

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 

214 

215 # Prerequisites pass for a tool with a brew formula 

216 assert_that(strategy.check_prerequisites(env, "markdownlint")).is_none() 

217 

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

227 

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 ) 

232 

233 

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 

241 

242 assert_that(strategy.is_available(env)).is_true() 

243 

244 

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 

252 

253 assert_that(strategy.is_available(env)).is_false() 

254 

255 

256# --------------------------------------------------------------------------- 

257# NpmStrategy 

258# --------------------------------------------------------------------------- 

259 

260 

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 

268 

269 result = strategy.install_hint(env, "prettier", "3.2.0", "prettier", None) 

270 

271 assert_that(result).is_equal_to("bun add -g prettier@3.2.0") 

272 

273 

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 

281 

282 result = strategy.install_hint(env, "prettier", "3.2.0", "prettier", None) 

283 

284 assert_that(result).is_equal_to("npm install -g prettier@3.2.0") 

285 

286 

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 

294 

295 result = strategy.upgrade_hint(env, "prettier", "3.2.0", "prettier", None) 

296 

297 assert_that(result).is_equal_to("bun add -g prettier@3.2.0") 

298 

299 

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 

307 

308 result = strategy.check_prerequisites(env, "prettier") 

309 

310 assert_that(result).is_none() 

311 

312 

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 

320 

321 result = strategy.check_prerequisites(env, "prettier") 

322 

323 assert_that(result).is_equal_to("bun/npm not available (install Node.js first)") 

324 

325 

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 

336 

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

346 

347 

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 

358 

359 result = strategy.check_prerequisites(env, "prettier") 

360 assert_that(result).is_equal_to("bun/npm not available (install Node.js first)") 

361 

362 

363# --------------------------------------------------------------------------- 

364# BinaryStrategy 

365# --------------------------------------------------------------------------- 

366 

367 

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 

375 

376 result = strategy.install_hint(env, "hadolint", "2.12.0", "hadolint", None) 

377 

378 assert_that(result).is_equal_to("brew install hadolint") 

379 

380 

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 

388 

389 result = strategy.install_hint(env, "hadolint", "2.12.0", "hadolint", None) 

390 

391 assert_that(result).starts_with("See https://") 

392 assert_that(result).contains("hadolint") 

393 

394 

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 

402 

403 result = strategy.upgrade_hint(env, "hadolint", "2.12.0", "hadolint", None) 

404 

405 assert_that(result).is_equal_to("brew upgrade hadolint") 

406 

407 

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 

415 

416 result = strategy.check_prerequisites(env, "hadolint") 

417 

418 assert_that(result).is_none() 

419 

420 

421# --------------------------------------------------------------------------- 

422# CargoStrategy 

423# --------------------------------------------------------------------------- 

424 

425 

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 

433 

434 result = strategy.install_hint( 

435 env, 

436 "cargo-audit", 

437 "0.20.0", 

438 "cargo-audit", 

439 None, 

440 ) 

441 

442 assert_that(result).is_equal_to("cargo install cargo-audit") 

443 

444 

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 

452 

453 result = strategy.upgrade_hint( 

454 env, 

455 "cargo-audit", 

456 "0.20.0", 

457 "cargo-audit", 

458 None, 

459 ) 

460 

461 assert_that(result).is_equal_to("cargo install --force cargo-audit") 

462 

463 

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 

471 

472 result = strategy.check_prerequisites(env, "cargo-audit") 

473 

474 assert_that(result).is_none() 

475 

476 

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 

484 

485 result = strategy.check_prerequisites(env, "cargo-audit") 

486 

487 assert_that(result).is_equal_to("cargo not available (install Rust first)") 

488 

489 

490# --------------------------------------------------------------------------- 

491# RustupStrategy 

492# --------------------------------------------------------------------------- 

493 

494 

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 

502 

503 result = strategy.install_hint(env, "clippy", "0.1.0", None, "clippy") 

504 

505 assert_that(result).is_equal_to("rustup component add clippy") 

506 

507 

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 

515 

516 result = strategy.install_hint(env, "rustfmt", "1.0.0", None, None) 

517 

518 assert_that(result).is_equal_to("rustup toolchain install stable") 

519 

520 

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 

528 

529 result = strategy.upgrade_hint(env, "clippy", "0.1.0", None, "clippy") 

530 

531 assert_that(result).is_equal_to("rustup update stable") 

532 

533 

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 

541 

542 result = strategy.check_prerequisites(env, "clippy") 

543 

544 assert_that(result).is_none() 

545 

546 

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 

554 

555 result = strategy.check_prerequisites(env, "clippy") 

556 

557 assert_that(result).is_equal_to("clippy: rustup not available (install Rust first)") 

558 

559 

560# --------------------------------------------------------------------------- 

561# BREW_FORMULA_NAMES 

562# --------------------------------------------------------------------------- 

563 

564 

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 ) 

571 

572 

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