Coverage for tests / unit / tools / oxlint / test_fix_method.py: 100%
188 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"""Tests for OxlintPlugin.fix method."""
3from __future__ import annotations
5import pathlib
6import subprocess
7from pathlib import Path
8from typing import TYPE_CHECKING
9from unittest.mock import MagicMock, patch
11from assertpy import assert_that
13from lintro.parsers.oxlint.oxlint_issue import OxlintIssue
15if TYPE_CHECKING:
16 from lintro.tools.definitions.oxlint import OxlintPlugin
19def test_fix_success_all_fixed(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None:
20 """Fix returns success when all issues fixed.
22 Args:
23 oxlint_plugin: The OxlintPlugin instance to test.
24 tmp_path: Temporary directory path for test files.
25 """
26 test_file = pathlib.Path(tmp_path) / "test.js"
27 test_file.write_text("var x = 1;\n")
29 initial_output = """{
30 "diagnostics": [
31 {
32 "message": "Use 'const' instead of 'var'.",
33 "code": "eslint(prefer-const)",
34 "severity": "warning",
35 "filename": "test.js",
36 "labels": [{"span": {"line": 1, "column": 1}}]
37 }
38 ]
39 }"""
40 final_output = '{"diagnostics": []}'
42 call_count = 0
44 def mock_run_subprocess(*args, **kwargs):
45 nonlocal call_count
46 call_count += 1
47 if call_count == 1:
48 return (False, initial_output)
49 elif call_count == 2:
50 return (True, "")
51 else:
52 return (True, final_output)
54 with (
55 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
56 patch.object(
57 oxlint_plugin,
58 "_run_subprocess",
59 side_effect=mock_run_subprocess,
60 ),
61 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
62 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
63 ):
64 mock_ctx = MagicMock()
65 mock_ctx.should_skip = False
66 mock_ctx.early_result = None
67 mock_ctx.timeout = 30
68 mock_ctx.cwd = str(tmp_path)
69 mock_ctx.rel_files = ["test.js"]
70 mock_ctx.files = [str(test_file)]
71 mock_prepare.return_value = mock_ctx
73 result = oxlint_plugin.fix([str(test_file)], {})
75 assert_that(result.name).is_equal_to("oxlint")
76 assert_that(result.success).is_true()
77 assert_that(result.initial_issues_count).is_equal_to(1)
78 assert_that(result.fixed_issues_count).is_equal_to(1)
79 assert_that(result.remaining_issues_count).is_equal_to(0)
82def test_fix_partial_fix(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None:
83 """Fix returns remaining issues when not all can be fixed.
85 Args:
86 oxlint_plugin: The OxlintPlugin instance to test.
87 tmp_path: Temporary directory path for test files.
88 """
89 test_file = pathlib.Path(tmp_path) / "test.js"
90 test_file.write_text("var x = 1;\n")
92 initial_output = """{
93 "diagnostics": [
94 {
95 "message": "Use 'const' instead of 'var'.",
96 "code": "eslint(prefer-const)",
97 "severity": "warning",
98 "filename": "test.js",
99 "labels": [{"span": {"line": 1, "column": 1}}]
100 },
101 {
102 "message": "Unused variable x.",
103 "code": "eslint(no-unused-vars)",
104 "severity": "warning",
105 "filename": "test.js",
106 "labels": [{"span": {"line": 1, "column": 5}}]
107 }
108 ]
109 }"""
110 final_output = """{
111 "diagnostics": [
112 {
113 "message": "Unused variable x.",
114 "code": "eslint(no-unused-vars)",
115 "severity": "warning",
116 "filename": "test.js",
117 "labels": [{"span": {"line": 1, "column": 7}}]
118 }
119 ]
120 }"""
122 call_count = 0
124 def mock_run_subprocess(*args, **kwargs):
125 nonlocal call_count
126 call_count += 1
127 if call_count == 1:
128 return (False, initial_output)
129 elif call_count == 2:
130 return (True, "")
131 else:
132 return (False, final_output)
134 with (
135 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
136 patch.object(
137 oxlint_plugin,
138 "_run_subprocess",
139 side_effect=mock_run_subprocess,
140 ),
141 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
142 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
143 ):
144 mock_ctx = MagicMock()
145 mock_ctx.should_skip = False
146 mock_ctx.early_result = None
147 mock_ctx.timeout = 30
148 mock_ctx.cwd = str(tmp_path)
149 mock_ctx.rel_files = ["test.js"]
150 mock_ctx.files = [str(test_file)]
151 mock_prepare.return_value = mock_ctx
153 result = oxlint_plugin.fix([str(test_file)], {})
155 assert_that(result.success).is_false()
156 assert_that(result.initial_issues_count).is_equal_to(2)
157 assert_that(result.fixed_issues_count).is_equal_to(1)
158 assert_that(result.remaining_issues_count).is_equal_to(1)
161def test_fix_timeout_on_initial_check(
162 oxlint_plugin: OxlintPlugin,
163 tmp_path: Path,
164) -> None:
165 """Fix handles timeout on initial check.
167 Args:
168 oxlint_plugin: The OxlintPlugin instance to test.
169 tmp_path: Temporary directory path for test files.
170 """
171 test_file = pathlib.Path(tmp_path) / "test.js"
172 test_file.write_text("const x = 1;\n")
174 with (
175 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
176 patch.object(
177 oxlint_plugin,
178 "_run_subprocess",
179 side_effect=subprocess.TimeoutExpired(cmd=["oxlint"], timeout=30),
180 ),
181 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
182 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
183 ):
184 mock_ctx = MagicMock()
185 mock_ctx.should_skip = False
186 mock_ctx.early_result = None
187 mock_ctx.timeout = 30
188 mock_ctx.cwd = str(tmp_path)
189 mock_ctx.rel_files = ["test.js"]
190 mock_ctx.files = [str(test_file)]
191 mock_prepare.return_value = mock_ctx
193 result = oxlint_plugin.fix([str(test_file)], {})
195 assert_that(result.success).is_false()
196 assert_that(result.output).contains("timed out")
199def test_fix_timeout_on_fix_command(
200 oxlint_plugin: OxlintPlugin,
201 tmp_path: Path,
202) -> None:
203 """Fix handles timeout on fix command.
205 Args:
206 oxlint_plugin: The OxlintPlugin instance to test.
207 tmp_path: Temporary directory path for test files.
208 """
209 from lintro.models.core.tool_result import ToolResult
211 test_file = pathlib.Path(tmp_path) / "test.js"
212 test_file.write_text("var x = 1;\n")
214 initial_output = """{
215 "diagnostics": [
216 {
217 "message": "Use 'const' instead of 'var'.",
218 "code": "eslint(prefer-const)",
219 "severity": "warning",
220 "filename": "test.js",
221 "labels": [{"span": {"line": 1, "column": 1}}]
222 }
223 ]
224 }"""
226 timeout_result = ToolResult(
227 name="oxlint",
228 success=False,
229 output="Oxlint execution timed out (30s limit exceeded).",
230 issues_count=1,
231 issues=[
232 OxlintIssue(
233 file="execution",
234 line=1,
235 column=1,
236 code="TIMEOUT",
237 message="Oxlint execution timed out",
238 severity="error",
239 fixable=False,
240 ),
241 ],
242 initial_issues_count=1,
243 fixed_issues_count=0,
244 remaining_issues_count=1,
245 )
247 call_count = 0
249 def mock_run_subprocess(*args, **kwargs):
250 nonlocal call_count
251 call_count += 1
252 if call_count == 1:
253 return (False, initial_output)
254 else:
255 raise subprocess.TimeoutExpired(cmd=["oxlint"], timeout=30)
257 with (
258 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
259 patch.object(
260 oxlint_plugin,
261 "_run_subprocess",
262 side_effect=mock_run_subprocess,
263 ),
264 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
265 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
266 patch.object(
267 oxlint_plugin,
268 "_create_timeout_result",
269 return_value=timeout_result,
270 ),
271 ):
272 mock_ctx = MagicMock()
273 mock_ctx.should_skip = False
274 mock_ctx.early_result = None
275 mock_ctx.timeout = 30
276 mock_ctx.cwd = str(tmp_path)
277 mock_ctx.rel_files = ["test.js"]
278 mock_ctx.files = [str(test_file)]
279 mock_prepare.return_value = mock_ctx
281 result = oxlint_plugin.fix([str(test_file)], {})
283 assert_that(result.success).is_false()
284 assert_that(result.output).contains("timed out")
287def test_fix_timeout_on_final_check(
288 oxlint_plugin: OxlintPlugin,
289 tmp_path: Path,
290) -> None:
291 """Fix handles timeout on final check.
293 Args:
294 oxlint_plugin: The OxlintPlugin instance to test.
295 tmp_path: Temporary directory path for test files.
296 """
297 from lintro.models.core.tool_result import ToolResult
299 test_file = pathlib.Path(tmp_path) / "test.js"
300 test_file.write_text("var x = 1;\n")
302 initial_output = """{
303 "diagnostics": [
304 {
305 "message": "Use 'const' instead of 'var'.",
306 "code": "eslint(prefer-const)",
307 "severity": "warning",
308 "filename": "test.js",
309 "labels": [{"span": {"line": 1, "column": 1}}]
310 }
311 ]
312 }"""
314 timeout_result = ToolResult(
315 name="oxlint",
316 success=False,
317 output="Oxlint execution timed out (30s limit exceeded).",
318 issues_count=1,
319 issues=[
320 OxlintIssue(
321 file="execution",
322 line=1,
323 column=1,
324 code="TIMEOUT",
325 message="Oxlint execution timed out",
326 severity="error",
327 fixable=False,
328 ),
329 ],
330 initial_issues_count=1,
331 fixed_issues_count=0,
332 remaining_issues_count=1,
333 )
335 call_count = 0
337 def mock_run_subprocess(*args, **kwargs):
338 nonlocal call_count
339 call_count += 1
340 if call_count == 1:
341 return (False, initial_output)
342 elif call_count == 2:
343 return (True, "")
344 else:
345 raise subprocess.TimeoutExpired(cmd=["oxlint"], timeout=30)
347 with (
348 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
349 patch.object(
350 oxlint_plugin,
351 "_run_subprocess",
352 side_effect=mock_run_subprocess,
353 ),
354 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
355 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
356 patch.object(
357 oxlint_plugin,
358 "_create_timeout_result",
359 return_value=timeout_result,
360 ),
361 ):
362 mock_ctx = MagicMock()
363 mock_ctx.should_skip = False
364 mock_ctx.early_result = None
365 mock_ctx.timeout = 30
366 mock_ctx.cwd = str(tmp_path)
367 mock_ctx.rel_files = ["test.js"]
368 mock_ctx.files = [str(test_file)]
369 mock_prepare.return_value = mock_ctx
371 result = oxlint_plugin.fix([str(test_file)], {})
373 assert_that(result.success).is_false()
374 assert_that(result.output).contains("timed out")
377def test_fix_early_skip(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None:
378 """Fix returns early when should_skip is True.
380 Args:
381 oxlint_plugin: The OxlintPlugin instance to test.
382 tmp_path: Temporary directory path for test files.
383 """
384 from lintro.models.core.tool_result import ToolResult
386 early_result = ToolResult(
387 name="oxlint",
388 success=True,
389 output="No files to fix.",
390 issues_count=0,
391 issues=[],
392 )
394 with patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare:
395 mock_ctx = MagicMock()
396 mock_ctx.should_skip = True
397 mock_ctx.early_result = early_result
398 mock_prepare.return_value = mock_ctx
400 result = oxlint_plugin.fix([str(tmp_path)], {})
402 assert_that(result).is_same_as(early_result)
405def test_fix_unfixable_issues(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None:
406 """Fix reports unfixable issues correctly.
408 Args:
409 oxlint_plugin: The OxlintPlugin instance to test.
410 tmp_path: Temporary directory path for test files.
411 """
412 test_file = pathlib.Path(tmp_path) / "test.js"
413 test_file.write_text("const x = 1;\n")
415 # No fixable issues - all issues remain after fix
416 initial_output = """{
417 "diagnostics": [
418 {
419 "message": "Unused variable x.",
420 "code": "eslint(no-unused-vars)",
421 "severity": "warning",
422 "filename": "test.js",
423 "labels": [{"span": {"line": 1, "column": 7}}]
424 }
425 ]
426 }"""
427 final_output = initial_output # Same issues remain
429 call_count = 0
431 def mock_run_subprocess(*args, **kwargs):
432 nonlocal call_count
433 call_count += 1
434 if call_count == 1:
435 return (False, initial_output)
436 elif call_count == 2:
437 return (True, "")
438 else:
439 return (False, final_output)
441 with (
442 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
443 patch.object(
444 oxlint_plugin,
445 "_run_subprocess",
446 side_effect=mock_run_subprocess,
447 ),
448 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
449 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
450 ):
451 mock_ctx = MagicMock()
452 mock_ctx.should_skip = False
453 mock_ctx.early_result = None
454 mock_ctx.timeout = 30
455 mock_ctx.cwd = str(tmp_path)
456 mock_ctx.rel_files = ["test.js"]
457 mock_ctx.files = [str(test_file)]
458 mock_prepare.return_value = mock_ctx
460 result = oxlint_plugin.fix([str(test_file)], {})
462 assert_that(result.success).is_false()
463 assert_that(result.initial_issues_count).is_equal_to(1)
464 assert_that(result.fixed_issues_count).is_equal_to(0)
465 assert_that(result.remaining_issues_count).is_equal_to(1)
466 assert_that(result.output).contains("cannot be auto-fixed")
469def test_fix_no_issues(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None:
470 """Fix returns success when no issues found.
472 Args:
473 oxlint_plugin: The OxlintPlugin instance to test.
474 tmp_path: Temporary directory path for test files.
475 """
476 test_file = pathlib.Path(tmp_path) / "test.js"
477 test_file.write_text("const x = 1;\nconsole.log(x);\n")
479 mock_output = '{"diagnostics": []}'
481 call_count = 0
483 def mock_run_subprocess(*args, **kwargs):
484 nonlocal call_count
485 call_count += 1
486 return (True, mock_output)
488 with (
489 patch.object(oxlint_plugin, "_prepare_execution") as mock_prepare,
490 patch.object(
491 oxlint_plugin,
492 "_run_subprocess",
493 side_effect=mock_run_subprocess,
494 ),
495 patch.object(oxlint_plugin, "_get_executable_command", return_value=["oxlint"]),
496 patch.object(oxlint_plugin, "_build_config_args", return_value=[]),
497 ):
498 mock_ctx = MagicMock()
499 mock_ctx.should_skip = False
500 mock_ctx.early_result = None
501 mock_ctx.timeout = 30
502 mock_ctx.cwd = str(tmp_path)
503 mock_ctx.rel_files = ["test.js"]
504 mock_ctx.files = [str(test_file)]
505 mock_prepare.return_value = mock_ctx
507 result = oxlint_plugin.fix([str(test_file)], {})
509 assert_that(result.success).is_true()
510 assert_that(result.initial_issues_count).is_equal_to(0)
511 assert_that(result.fixed_issues_count).is_equal_to(0)
512 assert_that(result.remaining_issues_count).is_equal_to(0)