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

1"""Shared test utilities for tool plugin tests. 

2 

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

7 

8from __future__ import annotations 

9 

10from contextlib import contextmanager 

11from dataclasses import dataclass, field 

12from typing import TYPE_CHECKING, Any 

13from unittest.mock import MagicMock, patch 

14 

15from assertpy import assert_that 

16 

17from lintro.enums.tool_name import ToolName 

18from lintro.models.core.tool_result import ToolResult 

19 

20if TYPE_CHECKING: 

21 from collections.abc import Generator 

22 

23 from lintro.plugins.base import BaseToolPlugin 

24 

25 

26# ============================================================================= 

27# Sample dataclasses for test data 

28# ============================================================================= 

29 

30 

31@dataclass 

32class SampleIssue: 

33 """Sample issue for testing tool output parsing. 

34 

35 This provides a simple, reusable issue structure that can be 

36 customized for different test scenarios. 

37 

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

46 

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" 

53 

54 

55@dataclass 

56class SampleToolConfig: 

57 """Sample tool configuration for testing. 

58 

59 Provides default values that can be overridden for specific test cases. 

60 

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

67 

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) 

72 

73 

74# ============================================================================= 

75# Assertion helpers 

76# ============================================================================= 

77 

78 

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. 

85 

86 Args: 

87 result: The ToolResult to verify. 

88 expected_name: Expected tool name (optional). 

89 expected_issues_count: Expected issue count (default 0). 

90 

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) 

97 

98 if expected_name is not None: 

99 assert_that(result.name).is_equal_to(expected_name) 

100 

101 

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. 

108 

109 Args: 

110 result: The ToolResult to verify. 

111 expected_name: Expected tool name (optional). 

112 min_issues: Minimum expected issue count (default 1). 

113 

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) 

120 

121 if expected_name is not None: 

122 assert_that(result.name).is_equal_to(expected_name) 

123 

124 

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. 

130 

131 Args: 

132 result: The ToolResult to verify. 

133 expected_name: Expected tool name (optional). 

134 

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

142 

143 if expected_name is not None: 

144 assert_that(result.name).is_equal_to(expected_name) 

145 

146 

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. 

152 

153 Args: 

154 result: The ToolResult to verify. 

155 expected_name: Expected tool name (optional). 

156 

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) 

163 

164 if expected_name is not None: 

165 assert_that(result.name).is_equal_to(expected_name) 

166 

167 

168# ============================================================================= 

169# Mock creation helpers 

170# ============================================================================= 

171 

172 

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. 

179 

180 Args: 

181 stdout: Standard output content. 

182 stderr: Standard error content. 

183 returncode: Process return code. 

184 

185 Returns: 

186 A MagicMock configured as a subprocess result. 

187 

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 

199 

200 

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. 

209 

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. 

216 

217 Returns: 

218 A MagicMock configured as a ToolResult. 

219 

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 

234 

235 

236# ============================================================================= 

237# Context managers for patching 

238# ============================================================================= 

239 

240 

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. 

250 

251 This helper patches _prepare_execution and _run_subprocess to allow 

252 testing the check method without actual subprocess calls. 

253 

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. 

260 

261 Yields: 

262 MagicMock: The mock execution context. 

263 

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 

270 

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 

278 

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 

284 

285 

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. 

295 

296 Similar to patch_plugin_for_check_test but configured for fix operations. 

297 

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. 

304 

305 Yields: 

306 MagicMock: The mock execution context. 

307 

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 

314 

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 

322 

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