Coverage for tests / integration / tools / test_rustfmt_integration.py: 97%
123 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 rustfmt tool definition.
3These tests require rustfmt and cargo to be installed and available in PATH.
4They verify the RustfmtPlugin definition, check command, fix command, and set_options method.
5"""
7from __future__ import annotations
9import re
10import shutil
11import subprocess
12from collections.abc import Callable
13from pathlib import Path
14from typing import TYPE_CHECKING
16import pytest
17from assertpy import assert_that
18from packaging.version import Version
20if TYPE_CHECKING:
21 from lintro.plugins.base import BaseToolPlugin
24def _get_rustfmt_version() -> Version | None:
25 """Get the installed rustfmt version.
27 Returns:
28 Version object or None if not installed or version cannot be determined.
29 """
30 if shutil.which("rustfmt") is None:
31 return None
32 try:
33 result = subprocess.run(
34 ["rustfmt", "--version"],
35 capture_output=True,
36 text=True,
37 timeout=10,
38 )
39 # Output format: "rustfmt <version>-<channel> (<commit-hash> <date>)"
40 match = re.search(r"(\d+\.\d+\.\d+)", result.stdout)
41 if match:
42 return Version(match.group(1))
43 except (subprocess.SubprocessError, ValueError):
44 pass
45 return None
48_RUSTFMT_MIN_VERSION = Version("1.8.0")
49_installed_version = _get_rustfmt_version()
51# Skip all tests if rustfmt is not installed or version is below minimum
52pytestmark = pytest.mark.skipif(
53 shutil.which("rustfmt") is None
54 or shutil.which("cargo") is None
55 or _installed_version is None
56 or _installed_version < _RUSTFMT_MIN_VERSION,
57 reason=f"rustfmt >= {_RUSTFMT_MIN_VERSION} or cargo not installed "
58 f"(found: {_installed_version})",
59)
62@pytest.fixture
63def temp_rust_project_with_issues(tmp_path: Path) -> str:
64 """Create a temporary Rust project with formatting issues.
66 Creates a Cargo project containing Rust code with formatting issues that
67 rustfmt should detect, including:
68 - Missing spaces around braces
69 - Inconsistent formatting
71 Args:
72 tmp_path: Pytest fixture providing a temporary directory.
74 Returns:
75 Path to the project directory as a string.
76 """
77 project_dir = tmp_path / "rust_project"
78 project_dir.mkdir()
80 # Create Cargo.toml
81 (project_dir / "Cargo.toml").write_text(
82 """\
83[package]
84name = "test_project"
85version = "0.1.0"
86edition = "2021"
87""",
88 )
90 # Create src directory with poorly formatted code
91 src_dir = project_dir / "src"
92 src_dir.mkdir()
93 (src_dir / "main.rs").write_text(
94 """\
95fn main(){let x=1;let y=2;println!("{} {}",x,y);}
96""",
97 )
99 return str(project_dir)
102@pytest.fixture
103def temp_rust_project_clean(tmp_path: Path) -> str:
104 """Create a temporary Rust project with no formatting issues.
106 Creates a Cargo project containing properly formatted Rust code that
107 should pass rustfmt checking without issues.
109 Args:
110 tmp_path: Pytest fixture providing a temporary directory.
112 Returns:
113 Path to the project directory as a string.
114 """
115 project_dir = tmp_path / "rust_project_clean"
116 project_dir.mkdir()
118 # Create Cargo.toml
119 (project_dir / "Cargo.toml").write_text(
120 """\
121[package]
122name = "test_project"
123version = "0.1.0"
124edition = "2021"
125""",
126 )
128 # Create src directory with well-formatted code
129 src_dir = project_dir / "src"
130 src_dir.mkdir()
131 (src_dir / "main.rs").write_text(
132 """\
133fn main() {
134 let x = 1;
135 let y = 2;
136 println!("{} {}", x, y);
137}
138""",
139 )
141 return str(project_dir)
144@pytest.fixture
145def temp_rust_project_complex_issues(tmp_path: Path) -> str:
146 """Create a temporary Rust project with multiple formatting issues.
148 Creates a Cargo project containing code with various formatting issues
149 that rustfmt should fix, including:
150 - Missing spaces around operators
151 - Incorrect indentation
152 - Missing newlines
154 Args:
155 tmp_path: Pytest fixture providing a temporary directory.
157 Returns:
158 Path to the project directory as a string.
159 """
160 project_dir = tmp_path / "rust_project_complex"
161 project_dir.mkdir()
163 # Create Cargo.toml
164 (project_dir / "Cargo.toml").write_text(
165 """\
166[package]
167name = "test_project"
168version = "0.1.0"
169edition = "2021"
170""",
171 )
173 # Create src directory with multiple formatting issues
174 src_dir = project_dir / "src"
175 src_dir.mkdir()
176 (src_dir / "main.rs").write_text(
177 """\
178fn main(){if true{println!("yes");}else{println!("no");}}
179fn helper(x:i32,y:i32)->i32{x+y}
180""",
181 )
183 return str(project_dir)
186# --- Tests for RustfmtPlugin definition ---
189@pytest.mark.parametrize(
190 ("attr", "expected"),
191 [
192 ("name", "rustfmt"),
193 ("can_fix", True),
194 ],
195 ids=["name", "can_fix"],
196)
197def test_definition_attributes(
198 get_plugin: Callable[[str], BaseToolPlugin],
199 attr: str,
200 expected: object,
201) -> None:
202 """Verify RustfmtPlugin definition has correct attribute values.
204 Tests that the plugin definition exposes the expected values for
205 name and can_fix attributes.
207 Args:
208 get_plugin: Fixture factory to get plugin instances.
209 attr: The attribute name to check on the definition.
210 expected: The expected value of the attribute.
211 """
212 rustfmt_plugin = get_plugin("rustfmt")
213 assert_that(getattr(rustfmt_plugin.definition, attr)).is_equal_to(expected)
216def test_definition_file_patterns(
217 get_plugin: Callable[[str], BaseToolPlugin],
218) -> None:
219 """Verify RustfmtPlugin definition includes Rust file patterns.
221 Tests that the plugin is configured to handle Rust files (*.rs).
223 Args:
224 get_plugin: Fixture factory to get plugin instances.
225 """
226 rustfmt_plugin = get_plugin("rustfmt")
227 assert_that(rustfmt_plugin.definition.file_patterns).contains("*.rs")
230def test_definition_has_version_command(
231 get_plugin: Callable[[str], BaseToolPlugin],
232) -> None:
233 """Verify RustfmtPlugin definition has a version command.
235 Tests that the plugin exposes a version command for checking
236 the installed rustfmt version.
238 Args:
239 get_plugin: Fixture factory to get plugin instances.
240 """
241 rustfmt_plugin = get_plugin("rustfmt")
242 assert_that(rustfmt_plugin.definition.version_command).is_not_none()
245# --- Integration tests for rustfmt check command ---
248def test_check_project_with_issues(
249 get_plugin: Callable[[str], BaseToolPlugin],
250 temp_rust_project_with_issues: str,
251) -> None:
252 """Verify rustfmt check detects formatting issues in problematic projects.
254 Runs rustfmt on a project containing formatting issues and verifies that
255 issues are found.
257 Args:
258 get_plugin: Fixture factory to get plugin instances.
259 temp_rust_project_with_issues: Path to project with formatting issues.
260 """
261 rustfmt_plugin = get_plugin("rustfmt")
262 result = rustfmt_plugin.check([temp_rust_project_with_issues], {})
264 assert_that(result).is_not_none()
265 assert_that(result.name).is_equal_to("rustfmt")
266 assert_that(result.issues_count).is_greater_than(0)
269def test_check_clean_project(
270 get_plugin: Callable[[str], BaseToolPlugin],
271 temp_rust_project_clean: str,
272) -> None:
273 """Verify rustfmt check passes on clean projects.
275 Runs rustfmt on a clean project and verifies no issues are found.
277 Args:
278 get_plugin: Fixture factory to get plugin instances.
279 temp_rust_project_clean: Path to clean project.
280 """
281 rustfmt_plugin = get_plugin("rustfmt")
282 result = rustfmt_plugin.check([temp_rust_project_clean], {})
284 assert_that(result).is_not_none()
285 assert_that(result.name).is_equal_to("rustfmt")
286 assert_that(result.success).is_true()
289def test_check_empty_directory(
290 get_plugin: Callable[[str], BaseToolPlugin],
291 tmp_path: Path,
292) -> None:
293 """Verify rustfmt check handles empty directories gracefully.
295 Runs rustfmt on an empty directory and verifies a result is returned
296 with zero issues.
298 Args:
299 get_plugin: Fixture factory to get plugin instances.
300 tmp_path: Pytest fixture providing a temporary directory.
301 """
302 rustfmt_plugin = get_plugin("rustfmt")
303 result = rustfmt_plugin.check([str(tmp_path)], {})
305 assert_that(result).is_not_none()
306 assert_that(result.issues_count).is_equal_to(0)
309def test_check_no_cargo_toml(
310 get_plugin: Callable[[str], BaseToolPlugin],
311 tmp_path: Path,
312) -> None:
313 """Verify rustfmt check handles projects without Cargo.toml.
315 Runs rustfmt on a directory with Rust files but no Cargo.toml.
317 Args:
318 get_plugin: Fixture factory to get plugin instances.
319 tmp_path: Pytest fixture providing a temporary directory.
320 """
321 # Create a Rust file without Cargo.toml
322 test_file = tmp_path / "main.rs"
323 test_file.write_text("fn main() {}\n")
325 rustfmt_plugin = get_plugin("rustfmt")
326 result = rustfmt_plugin.check([str(test_file)], {})
328 assert_that(result).is_not_none()
329 assert_that(result.output).contains("No Cargo.toml found")
332# --- Integration tests for rustfmt fix command ---
335def test_fix_formats_project(
336 get_plugin: Callable[[str], BaseToolPlugin],
337 temp_rust_project_with_issues: str,
338) -> None:
339 """Verify rustfmt fix reformats projects with formatting issues.
341 Runs rustfmt fix on a project with formatting issues and verifies
342 the files are reformatted.
344 Args:
345 get_plugin: Fixture factory to get plugin instances.
346 temp_rust_project_with_issues: Path to project with formatting issues.
347 """
348 rustfmt_plugin = get_plugin("rustfmt")
349 project_path = Path(temp_rust_project_with_issues)
350 main_rs = project_path / "src" / "main.rs"
351 original = main_rs.read_text()
353 result = rustfmt_plugin.fix([temp_rust_project_with_issues], {})
355 assert_that(result).is_not_none()
356 assert_that(result.name).is_equal_to("rustfmt")
358 new_content = main_rs.read_text()
359 assert_that(new_content).is_not_equal_to(original)
362def test_fix_complex_project(
363 get_plugin: Callable[[str], BaseToolPlugin],
364 temp_rust_project_complex_issues: str,
365) -> None:
366 """Verify rustfmt fix handles complex formatting issues.
368 Runs rustfmt fix on a project with multiple formatting issues and verifies
369 fixes are applied.
371 Args:
372 get_plugin: Fixture factory to get plugin instances.
373 temp_rust_project_complex_issues: Path to project with complex issues.
374 """
375 rustfmt_plugin = get_plugin("rustfmt")
376 project_path = Path(temp_rust_project_complex_issues)
377 main_rs = project_path / "src" / "main.rs"
378 original = main_rs.read_text()
380 result = rustfmt_plugin.fix([temp_rust_project_complex_issues], {})
382 assert_that(result).is_not_none()
384 new_content = main_rs.read_text()
385 assert_that(new_content).is_not_equal_to(original)
388def test_fix_clean_project_unchanged(
389 get_plugin: Callable[[str], BaseToolPlugin],
390 temp_rust_project_clean: str,
391) -> None:
392 """Verify rustfmt fix doesn't change already formatted projects.
394 Runs rustfmt fix on a clean project and verifies the content stays the same.
396 Args:
397 get_plugin: Fixture factory to get plugin instances.
398 temp_rust_project_clean: Path to clean project.
399 """
400 rustfmt_plugin = get_plugin("rustfmt")
401 project_path = Path(temp_rust_project_clean)
402 main_rs = project_path / "src" / "main.rs"
403 original = main_rs.read_text()
405 result = rustfmt_plugin.fix([temp_rust_project_clean], {})
407 assert_that(result).is_not_none()
408 assert_that(result.success).is_true()
410 new_content = main_rs.read_text()
411 assert_that(new_content).is_equal_to(original)
414# --- Tests for RustfmtPlugin.set_options method ---
417@pytest.mark.parametrize(
418 ("option_name", "option_value", "expected"),
419 [
420 ("timeout", 30, 30),
421 ("timeout", 60, 60),
422 ("timeout", 120, 120),
423 ],
424 ids=[
425 "timeout_30",
426 "timeout_60",
427 "timeout_120",
428 ],
429)
430def test_set_options(
431 get_plugin: Callable[[str], BaseToolPlugin],
432 option_name: str,
433 option_value: object,
434 expected: object,
435) -> None:
436 """Verify RustfmtPlugin.set_options correctly sets various options.
438 Tests that plugin options can be set and retrieved correctly.
440 Args:
441 get_plugin: Fixture factory to get plugin instances.
442 option_name: Name of the option to set.
443 option_value: Value to set for the option.
444 expected: Expected value when retrieving the option.
445 """
446 rustfmt_plugin = get_plugin("rustfmt")
447 rustfmt_plugin.set_options(**{option_name: option_value})
448 assert_that(rustfmt_plugin.options.get(option_name)).is_equal_to(expected)
451def test_invalid_timeout(
452 get_plugin: Callable[[str], BaseToolPlugin],
453) -> None:
454 """Verify RustfmtPlugin.set_options rejects invalid timeout values.
456 Tests that invalid timeout values raise ValueError.
458 Args:
459 get_plugin: Fixture factory to get plugin instances.
460 """
461 rustfmt_plugin = get_plugin("rustfmt")
462 with pytest.raises(ValueError, match="must be positive"):
463 rustfmt_plugin.set_options(timeout=-1)