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

1"""Tests for OxlintPlugin.fix method.""" 

2 

3from __future__ import annotations 

4 

5import pathlib 

6import subprocess 

7from pathlib import Path 

8from typing import TYPE_CHECKING 

9from unittest.mock import MagicMock, patch 

10 

11from assertpy import assert_that 

12 

13from lintro.parsers.oxlint.oxlint_issue import OxlintIssue 

14 

15if TYPE_CHECKING: 

16 from lintro.tools.definitions.oxlint import OxlintPlugin 

17 

18 

19def test_fix_success_all_fixed(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None: 

20 """Fix returns success when all issues fixed. 

21 

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

28 

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": []}' 

41 

42 call_count = 0 

43 

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) 

53 

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 

72 

73 result = oxlint_plugin.fix([str(test_file)], {}) 

74 

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) 

80 

81 

82def test_fix_partial_fix(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None: 

83 """Fix returns remaining issues when not all can be fixed. 

84 

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

91 

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

121 

122 call_count = 0 

123 

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) 

133 

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 

152 

153 result = oxlint_plugin.fix([str(test_file)], {}) 

154 

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) 

159 

160 

161def test_fix_timeout_on_initial_check( 

162 oxlint_plugin: OxlintPlugin, 

163 tmp_path: Path, 

164) -> None: 

165 """Fix handles timeout on initial check. 

166 

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

173 

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 

192 

193 result = oxlint_plugin.fix([str(test_file)], {}) 

194 

195 assert_that(result.success).is_false() 

196 assert_that(result.output).contains("timed out") 

197 

198 

199def test_fix_timeout_on_fix_command( 

200 oxlint_plugin: OxlintPlugin, 

201 tmp_path: Path, 

202) -> None: 

203 """Fix handles timeout on fix command. 

204 

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 

210 

211 test_file = pathlib.Path(tmp_path) / "test.js" 

212 test_file.write_text("var x = 1;\n") 

213 

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

225 

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 ) 

246 

247 call_count = 0 

248 

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) 

256 

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 

280 

281 result = oxlint_plugin.fix([str(test_file)], {}) 

282 

283 assert_that(result.success).is_false() 

284 assert_that(result.output).contains("timed out") 

285 

286 

287def test_fix_timeout_on_final_check( 

288 oxlint_plugin: OxlintPlugin, 

289 tmp_path: Path, 

290) -> None: 

291 """Fix handles timeout on final check. 

292 

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 

298 

299 test_file = pathlib.Path(tmp_path) / "test.js" 

300 test_file.write_text("var x = 1;\n") 

301 

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

313 

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 ) 

334 

335 call_count = 0 

336 

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) 

346 

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 

370 

371 result = oxlint_plugin.fix([str(test_file)], {}) 

372 

373 assert_that(result.success).is_false() 

374 assert_that(result.output).contains("timed out") 

375 

376 

377def test_fix_early_skip(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None: 

378 """Fix returns early when should_skip is True. 

379 

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 

385 

386 early_result = ToolResult( 

387 name="oxlint", 

388 success=True, 

389 output="No files to fix.", 

390 issues_count=0, 

391 issues=[], 

392 ) 

393 

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 

399 

400 result = oxlint_plugin.fix([str(tmp_path)], {}) 

401 

402 assert_that(result).is_same_as(early_result) 

403 

404 

405def test_fix_unfixable_issues(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None: 

406 """Fix reports unfixable issues correctly. 

407 

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

414 

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 

428 

429 call_count = 0 

430 

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) 

440 

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 

459 

460 result = oxlint_plugin.fix([str(test_file)], {}) 

461 

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

467 

468 

469def test_fix_no_issues(oxlint_plugin: OxlintPlugin, tmp_path: Path) -> None: 

470 """Fix returns success when no issues found. 

471 

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

478 

479 mock_output = '{"diagnostics": []}' 

480 

481 call_count = 0 

482 

483 def mock_run_subprocess(*args, **kwargs): 

484 nonlocal call_count 

485 call_count += 1 

486 return (True, mock_output) 

487 

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 

506 

507 result = oxlint_plugin.fix([str(test_file)], {}) 

508 

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)