Coverage for tests / unit / pytest / test_pytest_handlers.py: 100%

187 statements  

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

1"""Unit tests for pytest_handlers module.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from typing import Any 

7from unittest.mock import MagicMock, patch 

8 

9import pytest 

10from assertpy import assert_that 

11 

12from lintro.tools.implementations.pytest.pytest_handlers import ( 

13 handle_check_plugins, 

14 handle_collect_only, 

15 handle_fixture_info, 

16 handle_list_fixtures, 

17 handle_list_markers, 

18 handle_list_plugins, 

19 handle_parametrize_help, 

20) 

21 

22 

23@dataclass 

24class FakeToolDefinition: 

25 """Fake ToolDefinition for testing.""" 

26 

27 name: str = "pytest" 

28 

29 

30class FakePytestPlugin: 

31 """Fake PytestPlugin for testing handler functions.""" 

32 

33 def __init__(self) -> None: 

34 """Initialize fake plugin.""" 

35 self._definition = FakeToolDefinition() 

36 self._subprocess_success = True 

37 self._subprocess_output = "" 

38 self._executable_cmd: list[str] = ["pytest"] 

39 

40 @property 

41 def definition(self) -> FakeToolDefinition: 

42 """Return the tool definition. 

43 

44 Returns: 

45 The tool definition. 

46 """ 

47 return self._definition 

48 

49 def _get_executable_command(self, tool_name: str = "pytest") -> list[str]: 

50 """Return command to execute the tool. 

51 

52 Args: 

53 tool_name: Name of the tool to execute. 

54 

55 Returns: 

56 List of command arguments. 

57 """ 

58 return list(self._executable_cmd) 

59 

60 def _run_subprocess(self, cmd: list[str]) -> tuple[bool, str]: 

61 """Run subprocess and return success/output. 

62 

63 Args: 

64 cmd: Command to execute. 

65 

66 Returns: 

67 Tuple of (success, output). 

68 """ 

69 return self._subprocess_success, self._subprocess_output 

70 

71 

72@pytest.fixture 

73def fake_pytest_plugin() -> FakePytestPlugin: 

74 """Create a FakePytestPlugin instance for testing. 

75 

76 Returns: 

77 A FakePytestPlugin instance. 

78 """ 

79 return FakePytestPlugin() 

80 

81 

82# Tests for handle_list_plugins 

83 

84 

85@patch("lintro.tools.implementations.pytest.pytest_handlers.list_installed_plugins") 

86@patch("lintro.tools.implementations.pytest.pytest_handlers.get_pytest_version_info") 

87def test_list_plugins_with_results( 

88 mock_version: MagicMock, 

89 mock_plugins: MagicMock, 

90 fake_pytest_plugin: FakePytestPlugin, 

91) -> None: 

92 """List installed plugins with version info. 

93 

94 Args: 

95 mock_version: Mock for pytest version info. 

96 mock_plugins: Mock for installed plugins list. 

97 fake_pytest_plugin: The fake pytest plugin instance to test. 

98 """ 

99 mock_version.return_value = "pytest 7.0.0" 

100 mock_plugins.return_value = [ 

101 {"name": "pytest-cov", "version": "4.0.0"}, 

102 {"name": "pytest-mock", "version": "3.10.0"}, 

103 ] 

104 

105 result = handle_list_plugins(fake_pytest_plugin) # type: ignore[arg-type] 

106 

107 assert_that(result.success).is_true() 

108 assert_that(result.issues_count).is_equal_to(0) 

109 assert_that(result.output).is_not_none() 

110 assert_that(result.output).contains("pytest 7.0.0") 

111 assert_that(result.output).contains("Installed pytest plugins (2)") 

112 assert_that(result.output).contains("pytest-cov (4.0.0)") 

113 assert_that(result.output).contains("pytest-mock (3.10.0)") 

114 

115 

116@patch("lintro.tools.implementations.pytest.pytest_handlers.list_installed_plugins") 

117@patch("lintro.tools.implementations.pytest.pytest_handlers.get_pytest_version_info") 

118def test_list_plugins_no_plugins( 

119 mock_version: MagicMock, 

120 mock_plugins: MagicMock, 

121 fake_pytest_plugin: FakePytestPlugin, 

122) -> None: 

123 """Show message when no plugins found. 

124 

125 Args: 

126 mock_version: Mock for the version check function. 

127 mock_plugins: Mock for the plugins listing function. 

128 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

129 """ 

130 mock_version.return_value = "pytest 7.0.0" 

131 mock_plugins.return_value = [] 

132 

133 result = handle_list_plugins(fake_pytest_plugin) # type: ignore[arg-type] 

134 

135 assert_that(result.success).is_true() 

136 assert_that(result.output).is_not_none() 

137 assert_that(result.output).contains("No pytest plugins found") 

138 

139 

140# Tests for handle_check_plugins 

141 

142 

143@patch("lintro.tools.implementations.pytest.pytest_handlers.check_plugin_installed") 

144def test_check_all_plugins_installed( 

145 mock_check: MagicMock, 

146 fake_pytest_plugin: FakePytestPlugin, 

147) -> None: 

148 """All required plugins are installed. 

149 

150 Args: 

151 mock_check: Mock for the plugin installation check function. 

152 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

153 """ 

154 mock_check.return_value = True 

155 

156 result = handle_check_plugins(fake_pytest_plugin, "pytest-cov,pytest-mock") # type: ignore[arg-type] 

157 

158 assert_that(result.success).is_true() 

159 assert_that(result.issues_count).is_equal_to(0) 

160 assert_that(result.output).is_not_none() 

161 assert_that(result.output).contains("Installed plugins (2)") 

162 assert_that(result.output).contains("pytest-cov") 

163 assert_that(result.output).contains("pytest-mock") 

164 

165 

166@patch("lintro.tools.implementations.pytest.pytest_handlers.check_plugin_installed") 

167def test_check_missing_plugins( 

168 mock_check: MagicMock, 

169 fake_pytest_plugin: FakePytestPlugin, 

170) -> None: 

171 """Some required plugins are missing. 

172 

173 Args: 

174 mock_check: Mock for the plugin installation check function. 

175 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

176 """ 

177 mock_check.side_effect = lambda p: p == "pytest-cov" 

178 

179 result = handle_check_plugins(fake_pytest_plugin, "pytest-cov,pytest-xdist") # type: ignore[arg-type] 

180 

181 assert_that(result.success).is_false() 

182 assert_that(result.issues_count).is_equal_to(1) 

183 assert_that(result.output).is_not_none() 

184 assert_that(result.output).contains("Installed plugins (1)") 

185 assert_that(result.output).contains("Missing plugins (1)") 

186 assert_that(result.output).contains("pytest-xdist") 

187 assert_that(result.output).contains("pip install") 

188 

189 

190@patch("lintro.tools.implementations.pytest.pytest_handlers.check_plugin_installed") 

191def test_check_all_plugins_missing( 

192 mock_check: MagicMock, 

193 fake_pytest_plugin: FakePytestPlugin, 

194) -> None: 

195 """All required plugins are missing. 

196 

197 Args: 

198 mock_check: Mock for the plugin installation check function. 

199 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

200 """ 

201 mock_check.return_value = False 

202 

203 result = handle_check_plugins(fake_pytest_plugin, "pytest-cov,pytest-xdist") # type: ignore[arg-type] 

204 

205 assert_that(result.success).is_false() 

206 assert_that(result.issues_count).is_equal_to(2) 

207 assert_that(result.output).is_not_none() 

208 assert_that(result.output).contains("Missing plugins (2)") 

209 

210 

211@pytest.mark.parametrize( 

212 ("plugins_input", "expected_message"), 

213 [ 

214 (None, "required_plugins must be specified"), 

215 ("", "required_plugins must be specified"), 

216 ], 

217 ids=["none_plugins", "empty_plugins"], 

218) 

219def test_check_plugins_invalid_input( 

220 fake_pytest_plugin: FakePytestPlugin, 

221 plugins_input: str | None, 

222 expected_message: str, 

223) -> None: 

224 """Error when no plugins or empty plugins specified. 

225 

226 Args: 

227 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

228 plugins_input: The plugins input to test. 

229 expected_message: Expected error message in the result. 

230 """ 

231 result = handle_check_plugins(fake_pytest_plugin, plugins_input) # type: ignore[arg-type] 

232 

233 assert_that(result.success).is_false() 

234 

235 

236@patch("lintro.tools.implementations.pytest.pytest_handlers.check_plugin_installed") 

237def test_check_plugins_with_whitespace( 

238 mock_check: MagicMock, 

239 fake_pytest_plugin: FakePytestPlugin, 

240) -> None: 

241 """Handle whitespace in plugin list. 

242 

243 Args: 

244 mock_check: Mock for the plugin installation check function. 

245 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

246 """ 

247 mock_check.return_value = True 

248 

249 result = handle_check_plugins(fake_pytest_plugin, " pytest-cov , pytest-mock ") # type: ignore[arg-type] 

250 

251 assert_that(result.success).is_true() 

252 assert_that(result.output).is_not_none() 

253 assert_that("Installed plugins (2)" in result.output).is_true() # type: ignore[operator] # validated via is_not_none 

254 

255 

256# Tests for handle_collect_only 

257 

258 

259def test_collect_with_function_style_output( 

260 fake_pytest_plugin: FakePytestPlugin, 

261) -> None: 

262 """Parse <Function test_name> style output. 

263 

264 Args: 

265 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

266 """ 

267 fake_pytest_plugin._subprocess_output = """ 

268<Module tests/test_example.py> 

269 <Function test_one> 

270 <Function test_two> 

271""" 

272 result = handle_collect_only(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type] 

273 

274 assert_that(result.success).is_true() 

275 assert_that(result.output).is_not_none() 

276 assert_that(result.output).contains("Collected 2 test(s)") 

277 assert_that(result.output).contains("test_one") 

278 assert_that(result.output).contains("test_two") 

279 

280 

281def test_collect_with_double_colon_style(fake_pytest_plugin: FakePytestPlugin) -> None: 

282 """Parse test_file.py::test_name style output. 

283 

284 Args: 

285 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

286 """ 

287 fake_pytest_plugin._subprocess_output = """ 

288tests/test_example.py::test_one 

289tests/test_example.py::test_two 

290""" 

291 result = handle_collect_only(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type] 

292 

293 assert_that(result.success).is_true() 

294 assert_that(result.output).is_not_none() 

295 assert_that(result.output).contains("Collected 2 test(s)") 

296 assert_that(result.output).contains("test_one") 

297 assert_that(result.output).contains("test_two") 

298 

299 

300def test_collect_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None: 

301 """Return failure when subprocess fails. 

302 

303 Args: 

304 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

305 """ 

306 fake_pytest_plugin._subprocess_success = False 

307 fake_pytest_plugin._subprocess_output = "No tests found" 

308 

309 result = handle_collect_only(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type] 

310 

311 assert_that(result.success).is_false() 

312 assert_that(result.output).is_not_none() 

313 assert_that(result.output).contains("No tests found") 

314 

315 

316def test_collect_no_tests(fake_pytest_plugin: FakePytestPlugin) -> None: 

317 """Handle empty test collection. 

318 

319 Args: 

320 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

321 """ 

322 fake_pytest_plugin._subprocess_output = "no tests collected" 

323 

324 result = handle_collect_only(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type] 

325 

326 assert_that(result.success).is_true() 

327 assert_that(result.output).is_not_none() 

328 assert_that(result.output).contains("Collected 0 test(s)") 

329 

330 

331# Tests for handle_list_fixtures 

332 

333 

334def test_list_fixtures_success(fake_pytest_plugin: FakePytestPlugin) -> None: 

335 """List fixtures successfully. 

336 

337 Args: 

338 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

339 """ 

340 fake_pytest_plugin._subprocess_output = """ 

341tmp_path -- A tmp_path fixture 

342capsys -- Capture stdout/stderr 

343""" 

344 result = handle_list_fixtures(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type] 

345 

346 assert_that(result.success).is_true() 

347 assert_that(result.output).is_not_none() 

348 assert_that(result.output).contains("tmp_path") 

349 assert_that(result.output).contains("capsys") 

350 

351 

352def test_list_fixtures_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None: 

353 """Return failure when subprocess fails. 

354 

355 Args: 

356 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

357 """ 

358 fake_pytest_plugin._subprocess_success = False 

359 fake_pytest_plugin._subprocess_output = "Error occurred" 

360 

361 result = handle_list_fixtures(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type] 

362 

363 assert_that(result.success).is_false() 

364 

365 

366# Tests for handle_fixture_info 

367 

368 

369def test_fixture_info_found(fake_pytest_plugin: FakePytestPlugin) -> None: 

370 """Get info for specific fixture. 

371 

372 Args: 

373 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

374 """ 

375 fake_pytest_plugin._subprocess_output = """ 

376tmp_path -- Temp path fixture 

377 Return a temporary directory path object. 

378 

379capsys -- Capture fixture 

380 Capture stdout/stderr. 

381""" 

382 result = handle_fixture_info(fake_pytest_plugin, "tmp_path", ["tests/"]) # type: ignore[arg-type] 

383 

384 assert_that(result.success).is_true() 

385 assert_that(result.output).is_not_none() 

386 assert_that(result.output).contains("tmp_path") 

387 assert_that(result.output).contains("Temp path fixture") 

388 

389 

390def test_fixture_info_not_found(fake_pytest_plugin: FakePytestPlugin) -> None: 

391 """Show message when fixture not found. 

392 

393 Args: 

394 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

395 """ 

396 fake_pytest_plugin._subprocess_output = """ 

397capsys -- Capture fixture 

398""" 

399 result = handle_fixture_info(fake_pytest_plugin, "nonexistent", ["tests/"]) # type: ignore[arg-type] 

400 

401 assert_that(result.success).is_false() 

402 assert_that(result.output).is_not_none() 

403 assert_that(result.output).contains("'nonexistent' not found") 

404 

405 

406def test_fixture_info_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None: 

407 """Return failure when subprocess fails. 

408 

409 Args: 

410 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

411 """ 

412 fake_pytest_plugin._subprocess_success = False 

413 fake_pytest_plugin._subprocess_output = "Error" 

414 

415 result = handle_fixture_info(fake_pytest_plugin, "tmp_path", ["tests/"]) # type: ignore[arg-type] 

416 

417 assert_that(result.success).is_false() 

418 

419 

420def test_fixture_info_with_suffix_char(fake_pytest_plugin: FakePytestPlugin) -> None: 

421 """Handle fixture name with suffix character. 

422 

423 Args: 

424 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

425 """ 

426 fake_pytest_plugin._subprocess_output = """ 

427tmp_path: 

428 Return a temporary directory path object. 

429 

430other_fixture -- Other 

431""" 

432 result = handle_fixture_info(fake_pytest_plugin, "tmp_path", ["tests/"]) # type: ignore[arg-type] 

433 

434 assert_that(result.success).is_true() 

435 assert_that(result.output).is_not_none() 

436 assert_that(result.output).contains("tmp_path") 

437 

438 

439# Tests for handle_list_markers 

440 

441 

442def test_list_markers_success(fake_pytest_plugin: FakePytestPlugin) -> None: 

443 """List markers successfully. 

444 

445 Args: 

446 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

447 """ 

448 fake_pytest_plugin._subprocess_output = """ 

449@pytest.mark.slow: marks tests as slow 

450@pytest.mark.skip: skip test 

451""" 

452 result = handle_list_markers(fake_pytest_plugin) # type: ignore[arg-type] 

453 

454 assert_that(result.success).is_true() 

455 assert_that(result.output).is_not_none() 

456 assert_that(result.output).contains("slow") 

457 assert_that(result.output).contains("skip") 

458 

459 

460def test_list_markers_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None: 

461 """Return failure when subprocess fails. 

462 

463 Args: 

464 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

465 """ 

466 fake_pytest_plugin._subprocess_success = False 

467 fake_pytest_plugin._subprocess_output = "Error" 

468 

469 result = handle_list_markers(fake_pytest_plugin) # type: ignore[arg-type] 

470 

471 assert_that(result.success).is_false() 

472 

473 

474# Tests for handle_parametrize_help 

475 

476 

477def test_parametrize_help_output(fake_pytest_plugin: FakePytestPlugin) -> None: 

478 """Return parametrization help text. 

479 

480 Args: 

481 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

482 """ 

483 result = handle_parametrize_help(fake_pytest_plugin) # type: ignore[arg-type] 

484 

485 assert_that(result.success).is_true() 

486 assert_that(result.issues_count).is_equal_to(0) 

487 assert_that(result.output).is_not_none() 

488 assert_that(result.output).contains("Pytest Parametrization Help") 

489 assert_that(result.output).contains("@pytest.mark.parametrize") 

490 assert_that(result.output).contains("Basic Usage") 

491 assert_that(result.output).contains("Example:") 

492 

493 

494def test_parametrize_help_contains_doc_link( 

495 fake_pytest_plugin: FakePytestPlugin, 

496) -> None: 

497 """Help text contains documentation link. 

498 

499 Args: 

500 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

501 """ 

502 result = handle_parametrize_help(fake_pytest_plugin) # type: ignore[arg-type] 

503 

504 assert_that(result.output).is_not_none() 

505 assert_that(result.output).contains("docs.pytest.org") 

506 

507 

508# Exception handling tests using parametrize 

509 

510 

511@pytest.mark.parametrize( 

512 ("handler_func", "handler_args", "expected_error_message"), 

513 [ 

514 (handle_collect_only, (["tests/"],), "Error collecting tests"), 

515 (handle_list_fixtures, (["tests/"],), "Error listing fixtures"), 

516 (handle_fixture_info, ("tmp_path", ["tests/"]), "Error getting fixture info"), 

517 (handle_list_markers, (), "Error listing markers"), 

518 ], 

519 ids=[ 

520 "collect_only_exception", 

521 "list_fixtures_exception", 

522 "fixture_info_exception", 

523 "list_markers_exception", 

524 ], 

525) 

526def test_handler_exception_handling( 

527 fake_pytest_plugin: FakePytestPlugin, 

528 handler_func: Any, 

529 handler_args: tuple[Any, ...], 

530 expected_error_message: str, 

531) -> None: 

532 """Handle exceptions gracefully across all handler functions. 

533 

534 Args: 

535 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance. 

536 handler_func: The handler function being tested. 

537 handler_args: Arguments to pass to the handler function. 

538 expected_error_message: Expected error message in the result. 

539 """ 

540 

541 def raise_error(cmd: list[str]) -> tuple[bool, str]: 

542 raise RuntimeError("Subprocess error") 

543 

544 fake_pytest_plugin._run_subprocess = raise_error # type: ignore[method-assign] 

545 

546 result = handler_func(fake_pytest_plugin, *handler_args) 

547 

548 assert_that(result.success).is_false() 

549 assert_that(result.output).is_not_none() 

550 assert_that(expected_error_message in result.output).is_true()