Coverage for tests / integration / tools / test_oxlint_integration.py: 97%

134 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Integration tests for Oxlint tool definition. 

2 

3These tests require oxlint to be installed and available in PATH. 

4They verify the OxlintPlugin definition, check command, fix command, and set_options method. 

5""" 

6 

7from __future__ import annotations 

8 

9import shutil 

10import subprocess 

11from collections.abc import Callable 

12from pathlib import Path 

13from typing import TYPE_CHECKING 

14 

15import pytest 

16from assertpy import assert_that 

17 

18if TYPE_CHECKING: 

19 from lintro.plugins.base import BaseToolPlugin 

20 

21 

22def oxlint_is_available() -> bool: 

23 """Check if oxlint is installed and actually works. 

24 

25 This is more robust than just checking shutil.which() because wrapper 

26 scripts may exist even when the underlying npm package isn't installed. 

27 We verify the tool works by actually linting a simple JavaScript snippet. 

28 

29 Returns: 

30 True if oxlint is available and functional, False otherwise. 

31 """ 

32 if shutil.which("oxlint") is None: 

33 return False 

34 try: 

35 # First check --version works 

36 version_result = subprocess.run( 

37 ["oxlint", "--version"], 

38 capture_output=True, 

39 timeout=10, 

40 check=False, 

41 ) 

42 if version_result.returncode != 0: 

43 return False 

44 

45 # Then verify it can actually lint code (catches missing npm packages) 

46 # oxlint returns 0 for clean files, non-zero for files with issues 

47 # Use --quiet to minimize output and lint valid code that should pass 

48 lint_result = subprocess.run( 

49 ["oxlint", "--stdin-filename", "test.js"], 

50 input=b"const x = 1;\n", 

51 capture_output=True, 

52 timeout=10, 

53 check=False, 

54 ) 

55 # returncode 0 = clean, 1 = issues found, other = error 

56 # We accept 0 or 1 as "working" - anything else is a tool failure 

57 return lint_result.returncode in (0, 1) 

58 except (subprocess.TimeoutExpired, OSError): 

59 return False 

60 

61 

62# Skip all tests if oxlint is not installed or not working 

63pytestmark = pytest.mark.skipif( 

64 not oxlint_is_available(), 

65 reason="oxlint not installed or not working", 

66) 

67 

68 

69@pytest.fixture 

70def temp_js_file_with_issues(tmp_path: Path) -> str: 

71 """Create a temporary JavaScript file with lint issues. 

72 

73 Creates a file containing code with lint issues that Oxlint 

74 should detect, including: 

75 - Unused variables 

76 - Use of debugger statement 

77 - Use of var instead of const/let 

78 

79 Args: 

80 tmp_path: Pytest fixture providing a temporary directory. 

81 

82 Returns: 

83 Path to the created file as a string. 

84 """ 

85 file_path = tmp_path / "test_file.js" 

86 file_path.write_text( 

87 """\ 

88// Test file with lint violations 

89var unused = 1; 

90 

91if (someVar == 2) { 

92 console.log('test'); 

93} 

94 

95debugger; 

96""", 

97 ) 

98 return str(file_path) 

99 

100 

101@pytest.fixture 

102def temp_js_file_clean(tmp_path: Path) -> str: 

103 """Create a temporary JavaScript file with no lint issues. 

104 

105 Creates a file containing clean JavaScript code that should pass 

106 Oxlint linting without issues. 

107 

108 Args: 

109 tmp_path: Pytest fixture providing a temporary directory. 

110 

111 Returns: 

112 Path to the created file as a string. 

113 """ 

114 file_path = tmp_path / "clean_file.js" 

115 file_path.write_text( 

116 """\ 

117/** 

118 * A clean module. 

119 */ 

120 

121/** 

122 * Returns a greeting. 

123 * @returns {string} Greeting message. 

124 */ 

125function hello() { 

126 return "Hello, World!"; 

127} 

128 

129hello(); 

130""", 

131 ) 

132 return str(file_path) 

133 

134 

135@pytest.fixture 

136def temp_ts_file_with_issues(tmp_path: Path) -> str: 

137 """Create a temporary TypeScript file with lint issues. 

138 

139 Creates a file containing TypeScript code with lint issues that Oxlint 

140 should detect. 

141 

142 Args: 

143 tmp_path: Pytest fixture providing a temporary directory. 

144 

145 Returns: 

146 Path to the created file as a string. 

147 """ 

148 file_path = tmp_path / "test_file.ts" 

149 file_path.write_text( 

150 """\ 

151// TypeScript file with violations 

152var unusedVar: string = "unused"; 

153 

154function empty(): void {} 

155 

156debugger; 

157""", 

158 ) 

159 return str(file_path) 

160 

161 

162# --- Tests for OxlintPlugin definition --- 

163 

164 

165@pytest.mark.parametrize( 

166 ("attr", "expected"), 

167 [ 

168 ("name", "oxlint"), 

169 ("can_fix", True), 

170 ], 

171 ids=["name", "can_fix"], 

172) 

173def test_definition_attributes( 

174 get_plugin: Callable[[str], BaseToolPlugin], 

175 attr: str, 

176 expected: object, 

177) -> None: 

178 """Verify OxlintPlugin definition has correct attribute values. 

179 

180 Tests that the plugin definition exposes the expected values for 

181 name and can_fix attributes. 

182 

183 Args: 

184 get_plugin: Fixture factory to get plugin instances. 

185 attr: The attribute name to check on the definition. 

186 expected: The expected value of the attribute. 

187 """ 

188 oxlint_plugin = get_plugin("oxlint") 

189 assert_that(getattr(oxlint_plugin.definition, attr)).is_equal_to(expected) 

190 

191 

192def test_definition_file_patterns( 

193 get_plugin: Callable[[str], BaseToolPlugin], 

194) -> None: 

195 """Verify OxlintPlugin definition includes JavaScript/TypeScript file patterns. 

196 

197 Tests that the plugin is configured to handle JS/TS files (*.js, *.ts, *.jsx, *.tsx). 

198 

199 Args: 

200 get_plugin: Fixture factory to get plugin instances. 

201 """ 

202 oxlint_plugin = get_plugin("oxlint") 

203 patterns = oxlint_plugin.definition.file_patterns 

204 # Core JS/TS patterns 

205 assert_that(patterns).contains("*.js") 

206 assert_that(patterns).contains("*.ts") 

207 assert_that(patterns).contains("*.jsx") 

208 assert_that(patterns).contains("*.tsx") 

209 # Module variants 

210 assert_that(patterns).contains("*.mjs") 

211 assert_that(patterns).contains("*.cjs") 

212 assert_that(patterns).contains("*.mts") 

213 assert_that(patterns).contains("*.cts") 

214 # Framework support 

215 assert_that(patterns).contains("*.vue") 

216 assert_that(patterns).contains("*.svelte") 

217 assert_that(patterns).contains("*.astro") 

218 

219 

220def test_definition_has_version_command( 

221 get_plugin: Callable[[str], BaseToolPlugin], 

222) -> None: 

223 """Verify OxlintPlugin definition has a version command. 

224 

225 Tests that the plugin exposes a version command for checking 

226 the installed Oxlint version. 

227 

228 Args: 

229 get_plugin: Fixture factory to get plugin instances. 

230 """ 

231 oxlint_plugin = get_plugin("oxlint") 

232 assert_that(oxlint_plugin.definition.version_command).is_not_none() 

233 

234 

235# --- Integration tests for oxlint check command --- 

236 

237 

238def test_check_file_with_issues( 

239 get_plugin: Callable[[str], BaseToolPlugin], 

240 temp_js_file_with_issues: str, 

241) -> None: 

242 """Verify Oxlint check detects lint issues in problematic files. 

243 

244 Runs Oxlint on a file containing lint issues and verifies that 

245 issues are found. 

246 

247 Args: 

248 get_plugin: Fixture factory to get plugin instances. 

249 temp_js_file_with_issues: Path to file with lint issues. 

250 """ 

251 oxlint_plugin = get_plugin("oxlint") 

252 result = oxlint_plugin.check([temp_js_file_with_issues], {}) 

253 

254 assert_that(result).is_not_none() 

255 assert_that(result.name).is_equal_to("oxlint") 

256 assert_that(result.issues_count).is_greater_than(0) 

257 

258 

259def test_check_clean_file( 

260 get_plugin: Callable[[str], BaseToolPlugin], 

261 temp_js_file_clean: str, 

262) -> None: 

263 """Verify Oxlint check passes on clean files. 

264 

265 Runs Oxlint on a clean file and verifies no issues are found. 

266 

267 Args: 

268 get_plugin: Fixture factory to get plugin instances. 

269 temp_js_file_clean: Path to clean file. 

270 """ 

271 oxlint_plugin = get_plugin("oxlint") 

272 result = oxlint_plugin.check([temp_js_file_clean], {}) 

273 

274 assert_that(result).is_not_none() 

275 assert_that(result.name).is_equal_to("oxlint") 

276 assert_that(result.success).is_true() 

277 

278 

279def test_check_typescript_file( 

280 get_plugin: Callable[[str], BaseToolPlugin], 

281 temp_ts_file_with_issues: str, 

282) -> None: 

283 """Verify Oxlint check works with TypeScript files. 

284 

285 Runs Oxlint on a TypeScript file and verifies issues are found. 

286 

287 Args: 

288 get_plugin: Fixture factory to get plugin instances. 

289 temp_ts_file_with_issues: Path to TypeScript file with issues. 

290 """ 

291 oxlint_plugin = get_plugin("oxlint") 

292 result = oxlint_plugin.check([temp_ts_file_with_issues], {}) 

293 

294 assert_that(result).is_not_none() 

295 assert_that(result.name).is_equal_to("oxlint") 

296 assert_that(result.issues_count).is_greater_than(0) 

297 

298 

299def test_check_empty_directory( 

300 get_plugin: Callable[[str], BaseToolPlugin], 

301 tmp_path: Path, 

302) -> None: 

303 """Verify Oxlint check handles empty directories gracefully. 

304 

305 Runs Oxlint on an empty directory and verifies a result is returned 

306 with zero issues. 

307 

308 Args: 

309 get_plugin: Fixture factory to get plugin instances. 

310 tmp_path: Pytest fixture providing a temporary directory. 

311 """ 

312 oxlint_plugin = get_plugin("oxlint") 

313 result = oxlint_plugin.check([str(tmp_path)], {}) 

314 

315 assert_that(result).is_not_none() 

316 assert_that(result.issues_count).is_equal_to(0) 

317 

318 

319# --- Integration tests for oxlint fix command --- 

320 

321 

322def test_fix_applies_fixes( 

323 get_plugin: Callable[[str], BaseToolPlugin], 

324 tmp_path: Path, 

325) -> None: 

326 """Verify Oxlint fix applies auto-fixes to files. 

327 

328 Runs Oxlint fix on a file with fixable issues and verifies 

329 the file content changes. 

330 

331 Args: 

332 get_plugin: Fixture factory to get plugin instances. 

333 tmp_path: Pytest fixture providing a temporary directory. 

334 """ 

335 # Create file with fixable issue (debugger statement) 

336 file_path = tmp_path / "fixable.js" 

337 file_path.write_text( 

338 """\ 

339function test() { 

340 debugger; 

341 return 1; 

342} 

343""", 

344 ) 

345 

346 oxlint_plugin = get_plugin("oxlint") 

347 original_content = file_path.read_text() 

348 result = oxlint_plugin.fix([str(file_path)], {}) 

349 

350 assert_that(result).is_not_none() 

351 assert_that(result.name).is_equal_to("oxlint") 

352 

353 # File may or may not change depending on what oxlint considers fixable 

354 # Just verify the fix operation completed successfully 

355 assert_that(result.initial_issues_count).is_not_none() 

356 

357 # If issues were fixed, file content should have changed 

358 if result.fixed_issues_count and result.fixed_issues_count > 0: 

359 new_content = file_path.read_text() 

360 assert_that(new_content).is_not_equal_to(original_content) 

361 

362 

363def test_fix_clean_file_unchanged( 

364 get_plugin: Callable[[str], BaseToolPlugin], 

365 temp_js_file_clean: str, 

366) -> None: 

367 """Verify Oxlint fix doesn't change already clean files. 

368 

369 Runs Oxlint fix on a clean file and verifies the content stays the same. 

370 

371 Args: 

372 get_plugin: Fixture factory to get plugin instances. 

373 temp_js_file_clean: Path to clean file. 

374 """ 

375 oxlint_plugin = get_plugin("oxlint") 

376 original = Path(temp_js_file_clean).read_text() 

377 

378 result = oxlint_plugin.fix([temp_js_file_clean], {}) 

379 

380 assert_that(result).is_not_none() 

381 assert_that(result.success).is_true() 

382 

383 new_content = Path(temp_js_file_clean).read_text() 

384 assert_that(new_content).is_equal_to(original) 

385 

386 

387# --- Tests for OxlintPlugin.set_options method --- 

388 

389 

390@pytest.mark.parametrize( 

391 ("option_name", "option_value", "expected"), 

392 [ 

393 ("timeout", 60, 60), 

394 ("quiet", True, True), 

395 ("verbose_fix_output", True, True), 

396 ], 

397 ids=["timeout", "quiet", "verbose_fix_output"], 

398) 

399def test_set_options( 

400 get_plugin: Callable[[str], BaseToolPlugin], 

401 option_name: str, 

402 option_value: object, 

403 expected: object, 

404) -> None: 

405 """Verify OxlintPlugin.set_options correctly sets various options. 

406 

407 Tests that plugin options can be set and retrieved correctly. 

408 

409 Args: 

410 get_plugin: Fixture factory to get plugin instances. 

411 option_name: Name of the option to set. 

412 option_value: Value to set for the option. 

413 expected: Expected value when retrieving the option. 

414 """ 

415 oxlint_plugin = get_plugin("oxlint") 

416 oxlint_plugin.set_options(**{option_name: option_value}) 

417 assert_that(oxlint_plugin.options.get(option_name)).is_equal_to(expected) 

418 

419 

420def test_set_exclude_patterns( 

421 get_plugin: Callable[[str], BaseToolPlugin], 

422) -> None: 

423 """Verify OxlintPlugin.set_options correctly sets exclude_patterns. 

424 

425 Tests that exclude patterns can be set and retrieved correctly. 

426 

427 Args: 

428 get_plugin: Fixture factory to get plugin instances. 

429 """ 

430 oxlint_plugin = get_plugin("oxlint") 

431 oxlint_plugin.set_options(exclude_patterns=["node_modules", "dist"]) 

432 assert_that(oxlint_plugin.exclude_patterns).contains("node_modules") 

433 assert_that(oxlint_plugin.exclude_patterns).contains("dist") 

434 

435 

436# --- Integration tests for new oxlint options --- 

437 

438 

439@pytest.mark.parametrize( 

440 ("option_name", "option_value", "expected"), 

441 [ 

442 ("config", ".oxlintrc.json", ".oxlintrc.json"), 

443 ("tsconfig", "tsconfig.json", "tsconfig.json"), 

444 ], 

445 ids=["config", "tsconfig"], 

446) 

447def test_set_config_options( 

448 get_plugin: Callable[[str], BaseToolPlugin], 

449 option_name: str, 

450 option_value: object, 

451 expected: object, 

452) -> None: 

453 """Verify OxlintPlugin.set_options correctly sets config options. 

454 

455 Tests that config and tsconfig options can be set and retrieved correctly. 

456 

457 Args: 

458 get_plugin: Fixture factory to get plugin instances. 

459 option_name: Name of the option to set. 

460 option_value: Value to set for the option. 

461 expected: Expected value when retrieving the option. 

462 """ 

463 oxlint_plugin = get_plugin("oxlint") 

464 oxlint_plugin.set_options(**{option_name: option_value}) 

465 assert_that(oxlint_plugin.options.get(option_name)).is_equal_to(expected) 

466 

467 

468def test_set_deny_option( 

469 get_plugin: Callable[[str], BaseToolPlugin], 

470) -> None: 

471 """Verify OxlintPlugin.set_options correctly sets deny option. 

472 

473 Tests that deny rules can be set and retrieved correctly. 

474 

475 Args: 

476 get_plugin: Fixture factory to get plugin instances. 

477 """ 

478 oxlint_plugin = get_plugin("oxlint") 

479 oxlint_plugin.set_options(deny=["no-debugger", "eqeqeq"]) 

480 assert_that(oxlint_plugin.options.get("deny")).is_equal_to( 

481 ["no-debugger", "eqeqeq"], 

482 ) 

483 

484 

485def test_set_allow_option( 

486 get_plugin: Callable[[str], BaseToolPlugin], 

487) -> None: 

488 """Verify OxlintPlugin.set_options correctly sets allow option. 

489 

490 Tests that allow rules can be set and retrieved correctly. 

491 

492 Args: 

493 get_plugin: Fixture factory to get plugin instances. 

494 """ 

495 oxlint_plugin = get_plugin("oxlint") 

496 oxlint_plugin.set_options(allow=["no-console"]) 

497 assert_that(oxlint_plugin.options.get("allow")).is_equal_to(["no-console"]) 

498 

499 

500def test_set_warn_option( 

501 get_plugin: Callable[[str], BaseToolPlugin], 

502) -> None: 

503 """Verify OxlintPlugin.set_options correctly sets warn option. 

504 

505 Tests that warn rules can be set and retrieved correctly. 

506 

507 Args: 

508 get_plugin: Fixture factory to get plugin instances. 

509 """ 

510 oxlint_plugin = get_plugin("oxlint") 

511 oxlint_plugin.set_options(warn=["complexity"]) 

512 assert_that(oxlint_plugin.options.get("warn")).is_equal_to(["complexity"]) 

513 

514 

515def test_deny_option_affects_check_output( 

516 get_plugin: Callable[[str], BaseToolPlugin], 

517 tmp_path: Path, 

518) -> None: 

519 """Verify deny option affects check output. 

520 

521 Tests that denying a rule causes it to be reported as an error. 

522 

523 Args: 

524 get_plugin: Fixture factory to get plugin instances. 

525 tmp_path: Pytest fixture providing a temporary directory. 

526 """ 

527 # Create file with debugger statement 

528 file_path = tmp_path / "test.js" 

529 file_path.write_text( 

530 """\ 

531function test() { 

532 debugger; 

533 return 1; 

534} 

535""", 

536 ) 

537 

538 oxlint_plugin = get_plugin("oxlint") 

539 oxlint_plugin.set_options(deny=["no-debugger"]) 

540 result = oxlint_plugin.check([str(file_path)], {}) 

541 

542 # Should detect the debugger statement 

543 assert_that(result).is_not_none() 

544 assert_that(result.issues_count).is_greater_than(0)