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

1"""Integration tests for rustfmt tool definition. 

2 

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

6 

7from __future__ import annotations 

8 

9import re 

10import shutil 

11import subprocess 

12from collections.abc import Callable 

13from pathlib import Path 

14from typing import TYPE_CHECKING 

15 

16import pytest 

17from assertpy import assert_that 

18from packaging.version import Version 

19 

20if TYPE_CHECKING: 

21 from lintro.plugins.base import BaseToolPlugin 

22 

23 

24def _get_rustfmt_version() -> Version | None: 

25 """Get the installed rustfmt version. 

26 

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 

46 

47 

48_RUSTFMT_MIN_VERSION = Version("1.8.0") 

49_installed_version = _get_rustfmt_version() 

50 

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) 

60 

61 

62@pytest.fixture 

63def temp_rust_project_with_issues(tmp_path: Path) -> str: 

64 """Create a temporary Rust project with formatting issues. 

65 

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 

70 

71 Args: 

72 tmp_path: Pytest fixture providing a temporary directory. 

73 

74 Returns: 

75 Path to the project directory as a string. 

76 """ 

77 project_dir = tmp_path / "rust_project" 

78 project_dir.mkdir() 

79 

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 ) 

89 

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 ) 

98 

99 return str(project_dir) 

100 

101 

102@pytest.fixture 

103def temp_rust_project_clean(tmp_path: Path) -> str: 

104 """Create a temporary Rust project with no formatting issues. 

105 

106 Creates a Cargo project containing properly formatted Rust code that 

107 should pass rustfmt checking without issues. 

108 

109 Args: 

110 tmp_path: Pytest fixture providing a temporary directory. 

111 

112 Returns: 

113 Path to the project directory as a string. 

114 """ 

115 project_dir = tmp_path / "rust_project_clean" 

116 project_dir.mkdir() 

117 

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 ) 

127 

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 ) 

140 

141 return str(project_dir) 

142 

143 

144@pytest.fixture 

145def temp_rust_project_complex_issues(tmp_path: Path) -> str: 

146 """Create a temporary Rust project with multiple formatting issues. 

147 

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 

153 

154 Args: 

155 tmp_path: Pytest fixture providing a temporary directory. 

156 

157 Returns: 

158 Path to the project directory as a string. 

159 """ 

160 project_dir = tmp_path / "rust_project_complex" 

161 project_dir.mkdir() 

162 

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 ) 

172 

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 ) 

182 

183 return str(project_dir) 

184 

185 

186# --- Tests for RustfmtPlugin definition --- 

187 

188 

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. 

203 

204 Tests that the plugin definition exposes the expected values for 

205 name and can_fix attributes. 

206 

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) 

214 

215 

216def test_definition_file_patterns( 

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

218) -> None: 

219 """Verify RustfmtPlugin definition includes Rust file patterns. 

220 

221 Tests that the plugin is configured to handle Rust files (*.rs). 

222 

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

228 

229 

230def test_definition_has_version_command( 

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

232) -> None: 

233 """Verify RustfmtPlugin definition has a version command. 

234 

235 Tests that the plugin exposes a version command for checking 

236 the installed rustfmt version. 

237 

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

243 

244 

245# --- Integration tests for rustfmt check command --- 

246 

247 

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. 

253 

254 Runs rustfmt on a project containing formatting issues and verifies that 

255 issues are found. 

256 

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], {}) 

263 

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) 

267 

268 

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. 

274 

275 Runs rustfmt on a clean project and verifies no issues are found. 

276 

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], {}) 

283 

284 assert_that(result).is_not_none() 

285 assert_that(result.name).is_equal_to("rustfmt") 

286 assert_that(result.success).is_true() 

287 

288 

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. 

294 

295 Runs rustfmt on an empty directory and verifies a result is returned 

296 with zero issues. 

297 

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)], {}) 

304 

305 assert_that(result).is_not_none() 

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

307 

308 

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. 

314 

315 Runs rustfmt on a directory with Rust files but no Cargo.toml. 

316 

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

324 

325 rustfmt_plugin = get_plugin("rustfmt") 

326 result = rustfmt_plugin.check([str(test_file)], {}) 

327 

328 assert_that(result).is_not_none() 

329 assert_that(result.output).contains("No Cargo.toml found") 

330 

331 

332# --- Integration tests for rustfmt fix command --- 

333 

334 

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. 

340 

341 Runs rustfmt fix on a project with formatting issues and verifies 

342 the files are reformatted. 

343 

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

352 

353 result = rustfmt_plugin.fix([temp_rust_project_with_issues], {}) 

354 

355 assert_that(result).is_not_none() 

356 assert_that(result.name).is_equal_to("rustfmt") 

357 

358 new_content = main_rs.read_text() 

359 assert_that(new_content).is_not_equal_to(original) 

360 

361 

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. 

367 

368 Runs rustfmt fix on a project with multiple formatting issues and verifies 

369 fixes are applied. 

370 

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

379 

380 result = rustfmt_plugin.fix([temp_rust_project_complex_issues], {}) 

381 

382 assert_that(result).is_not_none() 

383 

384 new_content = main_rs.read_text() 

385 assert_that(new_content).is_not_equal_to(original) 

386 

387 

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. 

393 

394 Runs rustfmt fix on a clean project and verifies the content stays the same. 

395 

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

404 

405 result = rustfmt_plugin.fix([temp_rust_project_clean], {}) 

406 

407 assert_that(result).is_not_none() 

408 assert_that(result.success).is_true() 

409 

410 new_content = main_rs.read_text() 

411 assert_that(new_content).is_equal_to(original) 

412 

413 

414# --- Tests for RustfmtPlugin.set_options method --- 

415 

416 

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. 

437 

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

439 

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) 

449 

450 

451def test_invalid_timeout( 

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

453) -> None: 

454 """Verify RustfmtPlugin.set_options rejects invalid timeout values. 

455 

456 Tests that invalid timeout values raise ValueError. 

457 

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)