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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Unit tests for pytest_handlers module."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import Any
7from unittest.mock import MagicMock, patch
9import pytest
10from assertpy import assert_that
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)
23@dataclass
24class FakeToolDefinition:
25 """Fake ToolDefinition for testing."""
27 name: str = "pytest"
30class FakePytestPlugin:
31 """Fake PytestPlugin for testing handler functions."""
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"]
40 @property
41 def definition(self) -> FakeToolDefinition:
42 """Return the tool definition.
44 Returns:
45 The tool definition.
46 """
47 return self._definition
49 def _get_executable_command(self, tool_name: str = "pytest") -> list[str]:
50 """Return command to execute the tool.
52 Args:
53 tool_name: Name of the tool to execute.
55 Returns:
56 List of command arguments.
57 """
58 return list(self._executable_cmd)
60 def _run_subprocess(self, cmd: list[str]) -> tuple[bool, str]:
61 """Run subprocess and return success/output.
63 Args:
64 cmd: Command to execute.
66 Returns:
67 Tuple of (success, output).
68 """
69 return self._subprocess_success, self._subprocess_output
72@pytest.fixture
73def fake_pytest_plugin() -> FakePytestPlugin:
74 """Create a FakePytestPlugin instance for testing.
76 Returns:
77 A FakePytestPlugin instance.
78 """
79 return FakePytestPlugin()
82# Tests for handle_list_plugins
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.
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 ]
105 result = handle_list_plugins(fake_pytest_plugin) # type: ignore[arg-type]
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)")
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.
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 = []
133 result = handle_list_plugins(fake_pytest_plugin) # type: ignore[arg-type]
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")
140# Tests for handle_check_plugins
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.
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
156 result = handle_check_plugins(fake_pytest_plugin, "pytest-cov,pytest-mock") # type: ignore[arg-type]
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")
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.
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"
179 result = handle_check_plugins(fake_pytest_plugin, "pytest-cov,pytest-xdist") # type: ignore[arg-type]
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")
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.
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
203 result = handle_check_plugins(fake_pytest_plugin, "pytest-cov,pytest-xdist") # type: ignore[arg-type]
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)")
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.
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]
233 assert_that(result.success).is_false()
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.
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
249 result = handle_check_plugins(fake_pytest_plugin, " pytest-cov , pytest-mock ") # type: ignore[arg-type]
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
256# Tests for handle_collect_only
259def test_collect_with_function_style_output(
260 fake_pytest_plugin: FakePytestPlugin,
261) -> None:
262 """Parse <Function test_name> style output.
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]
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")
281def test_collect_with_double_colon_style(fake_pytest_plugin: FakePytestPlugin) -> None:
282 """Parse test_file.py::test_name style output.
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]
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")
300def test_collect_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None:
301 """Return failure when subprocess fails.
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"
309 result = handle_collect_only(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type]
311 assert_that(result.success).is_false()
312 assert_that(result.output).is_not_none()
313 assert_that(result.output).contains("No tests found")
316def test_collect_no_tests(fake_pytest_plugin: FakePytestPlugin) -> None:
317 """Handle empty test collection.
319 Args:
320 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance.
321 """
322 fake_pytest_plugin._subprocess_output = "no tests collected"
324 result = handle_collect_only(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type]
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)")
331# Tests for handle_list_fixtures
334def test_list_fixtures_success(fake_pytest_plugin: FakePytestPlugin) -> None:
335 """List fixtures successfully.
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]
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")
352def test_list_fixtures_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None:
353 """Return failure when subprocess fails.
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"
361 result = handle_list_fixtures(fake_pytest_plugin, ["tests/"]) # type: ignore[arg-type]
363 assert_that(result.success).is_false()
366# Tests for handle_fixture_info
369def test_fixture_info_found(fake_pytest_plugin: FakePytestPlugin) -> None:
370 """Get info for specific fixture.
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.
379capsys -- Capture fixture
380 Capture stdout/stderr.
381"""
382 result = handle_fixture_info(fake_pytest_plugin, "tmp_path", ["tests/"]) # type: ignore[arg-type]
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")
390def test_fixture_info_not_found(fake_pytest_plugin: FakePytestPlugin) -> None:
391 """Show message when fixture not found.
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]
401 assert_that(result.success).is_false()
402 assert_that(result.output).is_not_none()
403 assert_that(result.output).contains("'nonexistent' not found")
406def test_fixture_info_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None:
407 """Return failure when subprocess fails.
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"
415 result = handle_fixture_info(fake_pytest_plugin, "tmp_path", ["tests/"]) # type: ignore[arg-type]
417 assert_that(result.success).is_false()
420def test_fixture_info_with_suffix_char(fake_pytest_plugin: FakePytestPlugin) -> None:
421 """Handle fixture name with suffix character.
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.
430other_fixture -- Other
431"""
432 result = handle_fixture_info(fake_pytest_plugin, "tmp_path", ["tests/"]) # type: ignore[arg-type]
434 assert_that(result.success).is_true()
435 assert_that(result.output).is_not_none()
436 assert_that(result.output).contains("tmp_path")
439# Tests for handle_list_markers
442def test_list_markers_success(fake_pytest_plugin: FakePytestPlugin) -> None:
443 """List markers successfully.
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]
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")
460def test_list_markers_subprocess_failure(fake_pytest_plugin: FakePytestPlugin) -> None:
461 """Return failure when subprocess fails.
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"
469 result = handle_list_markers(fake_pytest_plugin) # type: ignore[arg-type]
471 assert_that(result.success).is_false()
474# Tests for handle_parametrize_help
477def test_parametrize_help_output(fake_pytest_plugin: FakePytestPlugin) -> None:
478 """Return parametrization help text.
480 Args:
481 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance.
482 """
483 result = handle_parametrize_help(fake_pytest_plugin) # type: ignore[arg-type]
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:")
494def test_parametrize_help_contains_doc_link(
495 fake_pytest_plugin: FakePytestPlugin,
496) -> None:
497 """Help text contains documentation link.
499 Args:
500 fake_pytest_plugin: Fixture providing a FakePytestPlugin instance.
501 """
502 result = handle_parametrize_help(fake_pytest_plugin) # type: ignore[arg-type]
504 assert_that(result.output).is_not_none()
505 assert_that(result.output).contains("docs.pytest.org")
508# Exception handling tests using parametrize
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.
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 """
541 def raise_error(cmd: list[str]) -> tuple[bool, str]:
542 raise RuntimeError("Subprocess error")
544 fake_pytest_plugin._run_subprocess = raise_error # type: ignore[method-assign]
546 result = handler_func(fake_pytest_plugin, *handler_args)
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()