Coverage for tests / unit / tools / test_helpers.py: 40%
81 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"""Shared test utilities for tool plugin tests.
3This module provides helper functions and utilities that complement
4the fixtures in conftest.py. These helpers are designed for reuse
5across multiple tool test modules.
6"""
8from __future__ import annotations
10from contextlib import contextmanager
11from dataclasses import dataclass, field
12from typing import TYPE_CHECKING, Any
13from unittest.mock import MagicMock, patch
15from assertpy import assert_that
17from lintro.enums.tool_name import ToolName
18from lintro.models.core.tool_result import ToolResult
20if TYPE_CHECKING:
21 from collections.abc import Generator
23 from lintro.plugins.base import BaseToolPlugin
26# =============================================================================
27# Sample dataclasses for test data
28# =============================================================================
31@dataclass
32class SampleIssue:
33 """Sample issue for testing tool output parsing.
35 This provides a simple, reusable issue structure that can be
36 customized for different test scenarios.
38 Attributes:
39 file: The file path where the issue was found.
40 line: The line number of the issue.
41 column: The column number of the issue.
42 code: The issue code/identifier.
43 message: The issue description.
44 severity: The severity level of the issue.
45 """
47 file: str = "src/main.py"
48 line: int = 10
49 column: int = 1
50 code: str = "E001"
51 message: str = "Test error message"
52 severity: str = "error"
55@dataclass
56class SampleToolConfig:
57 """Sample tool configuration for testing.
59 Provides default values that can be overridden for specific test cases.
61 Attributes:
62 priority: Execution priority for the tool.
63 file_patterns: Glob patterns for matching files.
64 timeout: Execution timeout in seconds.
65 options: Additional tool-specific options.
66 """
68 priority: int = 50
69 file_patterns: list[str] = field(default_factory=lambda: ["*.py"])
70 timeout: int = 30
71 options: dict[str, Any] = field(default_factory=dict)
74# =============================================================================
75# Assertion helpers
76# =============================================================================
79def assert_tool_result_success(
80 result: ToolResult,
81 expected_name: ToolName | str | None = None,
82 expected_issues_count: int = 0,
83) -> None:
84 """Assert a tool result indicates success.
86 Args:
87 result: The ToolResult to verify.
88 expected_name: Expected tool name (optional).
89 expected_issues_count: Expected issue count (default 0).
91 Example:
92 result = plugin.check(files, options)
93 assert_tool_result_success(result, ToolName.RUFF)
94 """
95 assert_that(result.success).is_true()
96 assert_that(result.issues_count).is_equal_to(expected_issues_count)
98 if expected_name is not None:
99 assert_that(result.name).is_equal_to(expected_name)
102def assert_tool_result_failure(
103 result: ToolResult,
104 expected_name: ToolName | str | None = None,
105 min_issues: int = 1,
106) -> None:
107 """Assert a tool result indicates failure with issues.
109 Args:
110 result: The ToolResult to verify.
111 expected_name: Expected tool name (optional).
112 min_issues: Minimum expected issue count (default 1).
114 Example:
115 result = plugin.check(files, options)
116 assert_tool_result_failure(result, ToolName.RUFF, min_issues=3)
117 """
118 assert_that(result.success).is_false()
119 assert_that(result.issues_count).is_greater_than_or_equal_to(min_issues)
121 if expected_name is not None:
122 assert_that(result.name).is_equal_to(expected_name)
125def assert_tool_result_timeout(
126 result: ToolResult,
127 expected_name: ToolName | str | None = None,
128) -> None:
129 """Assert a tool result indicates a timeout occurred.
131 Args:
132 result: The ToolResult to verify.
133 expected_name: Expected tool name (optional).
135 Example:
136 result = plugin.check(files, options)
137 assert_tool_result_timeout(result, ToolName.RUFF)
138 """
139 assert_that(result.success).is_false()
140 assert_that(result.output).is_not_none()
141 assert_that(result.output.lower() if result.output else "").contains("timeout")
143 if expected_name is not None:
144 assert_that(result.name).is_equal_to(expected_name)
147def assert_tool_result_skipped(
148 result: ToolResult,
149 expected_name: ToolName | str | None = None,
150) -> None:
151 """Assert a tool result indicates the check was skipped.
153 Args:
154 result: The ToolResult to verify.
155 expected_name: Expected tool name (optional).
157 Example:
158 result = plugin.check(files, options)
159 assert_tool_result_skipped(result, ToolName.RUFF)
160 """
161 assert_that(result.success).is_true()
162 assert_that(result.issues_count).is_equal_to(0)
164 if expected_name is not None:
165 assert_that(result.name).is_equal_to(expected_name)
168# =============================================================================
169# Mock creation helpers
170# =============================================================================
173def create_mock_subprocess_result(
174 stdout: str = "",
175 stderr: str = "",
176 returncode: int = 0,
177) -> MagicMock:
178 """Create a mock subprocess result with specified values.
180 Args:
181 stdout: Standard output content.
182 stderr: Standard error content.
183 returncode: Process return code.
185 Returns:
186 A MagicMock configured as a subprocess result.
188 Example:
189 mock_result = create_mock_subprocess_result(
190 stdout="All checks passed",
191 returncode=0,
192 )
193 """
194 mock_result = MagicMock()
195 mock_result.stdout = stdout
196 mock_result.stderr = stderr
197 mock_result.returncode = returncode
198 return mock_result
201def create_mock_tool_result(
202 name: ToolName | str = ToolName.RUFF,
203 success: bool = True,
204 issues_count: int = 0,
205 output: str = "",
206 issues: list[Any] | None = None,
207) -> MagicMock:
208 """Create a mock ToolResult with specified values.
210 Args:
211 name: Tool name.
212 success: Whether the check succeeded.
213 issues_count: Number of issues found.
214 output: Tool output string.
215 issues: List of parsed issues.
217 Returns:
218 A MagicMock configured as a ToolResult.
220 Example:
221 mock_result = create_mock_tool_result(
222 name=ToolName.MYPY,
223 success=False,
224 issues_count=5,
225 )
226 """
227 result = MagicMock()
228 result.name = name
229 result.success = success
230 result.issues_count = issues_count
231 result.output = output
232 result.issues = issues if issues is not None else []
233 return result
236# =============================================================================
237# Context managers for patching
238# =============================================================================
241@contextmanager
242def patch_plugin_for_check_test(
243 plugin: BaseToolPlugin,
244 subprocess_result: tuple[bool, str],
245 files: list[str] | None = None,
246 timeout: int = 30,
247 cwd: str = "/test/project",
248) -> Generator[MagicMock, None, None]:
249 """Context manager for patching a plugin to test the check method.
251 This helper patches _prepare_execution and _run_subprocess to allow
252 testing the check method without actual subprocess calls.
254 Args:
255 plugin: The plugin instance to patch.
256 subprocess_result: Tuple of (success, output) for subprocess mock.
257 files: List of file paths (optional).
258 timeout: Timeout value for execution context.
259 cwd: Working directory for execution context.
261 Yields:
262 MagicMock: The mock execution context.
264 Example:
265 with patch_plugin_for_check_test(ruff_plugin, (True, "")) as ctx:
266 result = ruff_plugin.check(["test.py"], {})
267 assert result.success
268 """
269 from lintro.plugins.base import ExecutionContext
271 mock_ctx = MagicMock(spec=ExecutionContext)
272 mock_ctx.files = files if files is not None else ["test.py"]
273 mock_ctx.rel_files = files if files is not None else ["test.py"]
274 mock_ctx.cwd = cwd
275 mock_ctx.timeout = timeout
276 mock_ctx.should_skip = False
277 mock_ctx.early_result = None
279 with (
280 patch.object(plugin, "_prepare_execution", return_value=mock_ctx),
281 patch.object(plugin, "_run_subprocess", return_value=subprocess_result),
282 ):
283 yield mock_ctx
286@contextmanager
287def patch_plugin_for_fix_test(
288 plugin: BaseToolPlugin,
289 subprocess_result: tuple[bool, str],
290 files: list[str] | None = None,
291 timeout: int = 30,
292 cwd: str = "/test/project",
293) -> Generator[MagicMock, None, None]:
294 """Context manager for patching a plugin to test the fix method.
296 Similar to patch_plugin_for_check_test but configured for fix operations.
298 Args:
299 plugin: The plugin instance to patch.
300 subprocess_result: Tuple of (success, output) for subprocess mock.
301 files: List of file paths (optional).
302 timeout: Timeout value for execution context.
303 cwd: Working directory for execution context.
305 Yields:
306 MagicMock: The mock execution context.
308 Example:
309 with patch_plugin_for_fix_test(ruff_plugin, (True, "1 fixed")) as ctx:
310 result = ruff_plugin.fix(["test.py"], {})
311 assert result.success
312 """
313 from lintro.plugins.base import ExecutionContext
315 mock_ctx = MagicMock(spec=ExecutionContext)
316 mock_ctx.files = files if files is not None else ["test.py"]
317 mock_ctx.rel_files = files if files is not None else ["test.py"]
318 mock_ctx.cwd = cwd
319 mock_ctx.timeout = timeout
320 mock_ctx.should_skip = False
321 mock_ctx.early_result = None
323 with (
324 patch.object(plugin, "_prepare_execution", return_value=mock_ctx),
325 patch.object(plugin, "_run_subprocess", return_value=subprocess_result),
326 ):
327 yield mock_ctx