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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Integration tests for Oxlint tool definition.
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"""
7from __future__ import annotations
9import shutil
10import subprocess
11from collections.abc import Callable
12from pathlib import Path
13from typing import TYPE_CHECKING
15import pytest
16from assertpy import assert_that
18if TYPE_CHECKING:
19 from lintro.plugins.base import BaseToolPlugin
22def oxlint_is_available() -> bool:
23 """Check if oxlint is installed and actually works.
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.
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
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
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)
69@pytest.fixture
70def temp_js_file_with_issues(tmp_path: Path) -> str:
71 """Create a temporary JavaScript file with lint issues.
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
79 Args:
80 tmp_path: Pytest fixture providing a temporary directory.
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;
91if (someVar == 2) {
92 console.log('test');
93}
95debugger;
96""",
97 )
98 return str(file_path)
101@pytest.fixture
102def temp_js_file_clean(tmp_path: Path) -> str:
103 """Create a temporary JavaScript file with no lint issues.
105 Creates a file containing clean JavaScript code that should pass
106 Oxlint linting without issues.
108 Args:
109 tmp_path: Pytest fixture providing a temporary directory.
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 */
121/**
122 * Returns a greeting.
123 * @returns {string} Greeting message.
124 */
125function hello() {
126 return "Hello, World!";
127}
129hello();
130""",
131 )
132 return str(file_path)
135@pytest.fixture
136def temp_ts_file_with_issues(tmp_path: Path) -> str:
137 """Create a temporary TypeScript file with lint issues.
139 Creates a file containing TypeScript code with lint issues that Oxlint
140 should detect.
142 Args:
143 tmp_path: Pytest fixture providing a temporary directory.
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";
154function empty(): void {}
156debugger;
157""",
158 )
159 return str(file_path)
162# --- Tests for OxlintPlugin definition ---
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.
180 Tests that the plugin definition exposes the expected values for
181 name and can_fix attributes.
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)
192def test_definition_file_patterns(
193 get_plugin: Callable[[str], BaseToolPlugin],
194) -> None:
195 """Verify OxlintPlugin definition includes JavaScript/TypeScript file patterns.
197 Tests that the plugin is configured to handle JS/TS files (*.js, *.ts, *.jsx, *.tsx).
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")
220def test_definition_has_version_command(
221 get_plugin: Callable[[str], BaseToolPlugin],
222) -> None:
223 """Verify OxlintPlugin definition has a version command.
225 Tests that the plugin exposes a version command for checking
226 the installed Oxlint version.
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()
235# --- Integration tests for oxlint check command ---
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.
244 Runs Oxlint on a file containing lint issues and verifies that
245 issues are found.
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], {})
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)
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.
265 Runs Oxlint on a clean file and verifies no issues are found.
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], {})
274 assert_that(result).is_not_none()
275 assert_that(result.name).is_equal_to("oxlint")
276 assert_that(result.success).is_true()
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.
285 Runs Oxlint on a TypeScript file and verifies issues are found.
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], {})
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)
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.
305 Runs Oxlint on an empty directory and verifies a result is returned
306 with zero issues.
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)], {})
315 assert_that(result).is_not_none()
316 assert_that(result.issues_count).is_equal_to(0)
319# --- Integration tests for oxlint fix command ---
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.
328 Runs Oxlint fix on a file with fixable issues and verifies
329 the file content changes.
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 )
346 oxlint_plugin = get_plugin("oxlint")
347 original_content = file_path.read_text()
348 result = oxlint_plugin.fix([str(file_path)], {})
350 assert_that(result).is_not_none()
351 assert_that(result.name).is_equal_to("oxlint")
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()
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)
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.
369 Runs Oxlint fix on a clean file and verifies the content stays the same.
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()
378 result = oxlint_plugin.fix([temp_js_file_clean], {})
380 assert_that(result).is_not_none()
381 assert_that(result.success).is_true()
383 new_content = Path(temp_js_file_clean).read_text()
384 assert_that(new_content).is_equal_to(original)
387# --- Tests for OxlintPlugin.set_options method ---
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.
407 Tests that plugin options can be set and retrieved correctly.
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)
420def test_set_exclude_patterns(
421 get_plugin: Callable[[str], BaseToolPlugin],
422) -> None:
423 """Verify OxlintPlugin.set_options correctly sets exclude_patterns.
425 Tests that exclude patterns can be set and retrieved correctly.
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")
436# --- Integration tests for new oxlint options ---
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.
455 Tests that config and tsconfig options can be set and retrieved correctly.
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)
468def test_set_deny_option(
469 get_plugin: Callable[[str], BaseToolPlugin],
470) -> None:
471 """Verify OxlintPlugin.set_options correctly sets deny option.
473 Tests that deny rules can be set and retrieved correctly.
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 )
485def test_set_allow_option(
486 get_plugin: Callable[[str], BaseToolPlugin],
487) -> None:
488 """Verify OxlintPlugin.set_options correctly sets allow option.
490 Tests that allow rules can be set and retrieved correctly.
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"])
500def test_set_warn_option(
501 get_plugin: Callable[[str], BaseToolPlugin],
502) -> None:
503 """Verify OxlintPlugin.set_options correctly sets warn option.
505 Tests that warn rules can be set and retrieved correctly.
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"])
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.
521 Tests that denying a rule causes it to be reported as an error.
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 )
538 oxlint_plugin = get_plugin("oxlint")
539 oxlint_plugin.set_options(deny=["no-debugger"])
540 result = oxlint_plugin.check([str(file_path)], {})
542 # Should detect the debugger statement
543 assert_that(result).is_not_none()
544 assert_that(result.issues_count).is_greater_than(0)