Coverage for tests / integration / tools / test_ruff_integration.py: 100%

104 statements  

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

1"""Integration tests for Ruff tool definition. 

2 

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

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

5""" 

6 

7from __future__ import annotations 

8 

9import shutil 

10from collections.abc import Callable 

11from pathlib import Path 

12from typing import TYPE_CHECKING 

13 

14import pytest 

15from assertpy import assert_that 

16 

17if TYPE_CHECKING: 

18 from lintro.plugins.base import BaseToolPlugin 

19 

20# Skip all tests if ruff is not installed 

21pytestmark = pytest.mark.skipif( 

22 shutil.which("ruff") is None, 

23 reason="ruff not installed", 

24) 

25 

26 

27@pytest.fixture 

28def temp_python_file_with_issues(tmp_path: Path) -> str: 

29 """Create a temporary Python file with lint issues. 

30 

31 Creates a file containing code with lint issues that Ruff 

32 should detect, including: 

33 - Unused imports 

34 - Missing whitespace around operators 

35 

36 Args: 

37 tmp_path: Pytest fixture providing a temporary directory. 

38 

39 Returns: 

40 Path to the created file as a string. 

41 """ 

42 file_path = tmp_path / "test_file.py" 

43 file_path.write_text( 

44 """\ 

45import os 

46import sys # unused import 

47x=1 # missing whitespace around operator 

48def foo(): 

49 pass 

50""", 

51 ) 

52 return str(file_path) 

53 

54 

55@pytest.fixture 

56def temp_python_file_clean(tmp_path: Path) -> str: 

57 """Create a temporary Python file with no lint issues. 

58 

59 Creates a file containing clean Python code that should pass 

60 Ruff linting without issues. 

61 

62 Args: 

63 tmp_path: Pytest fixture providing a temporary directory. 

64 

65 Returns: 

66 Path to the created file as a string. 

67 """ 

68 file_path = tmp_path / "clean_file.py" 

69 file_path.write_text( 

70 """\ 

71\"\"\"A clean module.\"\"\" 

72 

73 

74def hello() -> str: 

75 \"\"\"Return a greeting.\"\"\" 

76 return "Hello, World!" 

77""", 

78 ) 

79 return str(file_path) 

80 

81 

82@pytest.fixture 

83def temp_python_file_formatting_issues(tmp_path: Path) -> str: 

84 """Create a temporary Python file with formatting issues. 

85 

86 Creates a file containing code with formatting issues that Ruff format 

87 should fix, including: 

88 - Missing spaces around operators 

89 - Missing blank lines between functions 

90 - Long line that should be wrapped 

91 

92 Args: 

93 tmp_path: Pytest fixture providing a temporary directory. 

94 

95 Returns: 

96 Path to the created file as a string. 

97 """ 

98 file_path = tmp_path / "format_file.py" 

99 file_path.write_text( 

100 """\ 

101def foo(a,b,c): 

102 x=a+b+c 

103 return x 

104def bar(x,y): 

105 z=x*y 

106 return z 

107very_long_variable_name={"key1":"value1","key2":"value2","key3":"value3","key4":"value4"} 

108""", 

109 ) 

110 return str(file_path) 

111 

112 

113# --- Tests for RuffPlugin definition --- 

114 

115 

116@pytest.mark.parametrize( 

117 ("attr", "expected"), 

118 [ 

119 ("name", "ruff"), 

120 ("can_fix", True), 

121 ], 

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

123) 

124def test_definition_attributes( 

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

126 attr: str, 

127 expected: object, 

128) -> None: 

129 """Verify RuffPlugin definition has correct attribute values. 

130 

131 Tests that the plugin definition exposes the expected values for 

132 name and can_fix attributes. 

133 

134 Args: 

135 get_plugin: Fixture factory to get plugin instances. 

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

137 expected: The expected value of the attribute. 

138 """ 

139 ruff_plugin = get_plugin("ruff") 

140 assert_that(getattr(ruff_plugin.definition, attr)).is_equal_to(expected) 

141 

142 

143def test_definition_file_patterns(get_plugin: Callable[[str], BaseToolPlugin]) -> None: 

144 """Verify RuffPlugin definition includes Python file patterns. 

145 

146 Tests that the plugin is configured to handle Python files (*.py). 

147 

148 Args: 

149 get_plugin: Fixture factory to get plugin instances. 

150 """ 

151 ruff_plugin = get_plugin("ruff") 

152 assert_that(ruff_plugin.definition.file_patterns).contains("*.py") 

153 

154 

155def test_definition_has_version_command( 

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

157) -> None: 

158 """Verify RuffPlugin definition has a version command. 

159 

160 Tests that the plugin exposes a version command for checking 

161 the installed Ruff version. 

162 

163 Args: 

164 get_plugin: Fixture factory to get plugin instances. 

165 """ 

166 ruff_plugin = get_plugin("ruff") 

167 assert_that(ruff_plugin.definition.version_command).is_not_none() 

168 

169 

170# --- Integration tests for ruff check command --- 

171 

172 

173def test_check_file_with_issues( 

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

175 temp_python_file_with_issues: str, 

176) -> None: 

177 """Verify Ruff check detects lint issues in problematic files. 

178 

179 Runs Ruff on a file containing lint issues and verifies that 

180 issues are found. 

181 

182 Args: 

183 get_plugin: Fixture factory to get plugin instances. 

184 temp_python_file_with_issues: Path to file with lint issues. 

185 """ 

186 ruff_plugin = get_plugin("ruff") 

187 result = ruff_plugin.check([temp_python_file_with_issues], {}) 

188 

189 assert_that(result).is_not_none() 

190 assert_that(result.name).is_equal_to("ruff") 

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

192 

193 

194def test_check_clean_file( 

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

196 temp_python_file_clean: str, 

197) -> None: 

198 """Verify Ruff check passes on clean files. 

199 

200 Runs Ruff on a clean file and verifies no issues are found. 

201 

202 Args: 

203 get_plugin: Fixture factory to get plugin instances. 

204 temp_python_file_clean: Path to clean file. 

205 """ 

206 ruff_plugin = get_plugin("ruff") 

207 result = ruff_plugin.check([temp_python_file_clean], {}) 

208 

209 assert_that(result).is_not_none() 

210 assert_that(result.name).is_equal_to("ruff") 

211 assert_that(result.success).is_true() 

212 

213 

214def test_check_nonexistent_file( 

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

216 tmp_path: Path, 

217) -> None: 

218 """Verify Ruff check raises error for nonexistent files. 

219 

220 Attempts to run Ruff on a nonexistent file and verifies that 

221 a FileNotFoundError is raised. 

222 

223 Args: 

224 get_plugin: Fixture factory to get plugin instances. 

225 tmp_path: Pytest fixture providing a temporary directory. 

226 """ 

227 ruff_plugin = get_plugin("ruff") 

228 nonexistent = str(tmp_path / "nonexistent.py") 

229 with pytest.raises(FileNotFoundError): 

230 ruff_plugin.check([nonexistent], {}) 

231 

232 

233def test_check_empty_directory( 

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

235 tmp_path: Path, 

236) -> None: 

237 """Verify Ruff check handles empty directories gracefully. 

238 

239 Runs Ruff on an empty directory and verifies a result is returned 

240 with zero issues. 

241 

242 Args: 

243 get_plugin: Fixture factory to get plugin instances. 

244 tmp_path: Pytest fixture providing a temporary directory. 

245 """ 

246 ruff_plugin = get_plugin("ruff") 

247 result = ruff_plugin.check([str(tmp_path)], {}) 

248 

249 assert_that(result).is_not_none() 

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

251 

252 

253# --- Integration tests for ruff fix command --- 

254 

255 

256def test_fix_formats_file( 

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

258 temp_python_file_formatting_issues: str, 

259) -> None: 

260 """Verify Ruff fix reformats files with formatting issues. 

261 

262 Runs Ruff fix on a file with formatting issues and verifies 

263 the file content changes. 

264 

265 Args: 

266 get_plugin: Fixture factory to get plugin instances. 

267 temp_python_file_formatting_issues: Path to file with formatting issues. 

268 """ 

269 ruff_plugin = get_plugin("ruff") 

270 original = Path(temp_python_file_formatting_issues).read_text() 

271 

272 result = ruff_plugin.fix([temp_python_file_formatting_issues], {}) 

273 

274 assert_that(result).is_not_none() 

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

276 

277 new_content = Path(temp_python_file_formatting_issues).read_text() 

278 assert_that(new_content).is_not_equal_to(original) 

279 

280 

281def test_fix_removes_unused_imports( 

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

283 tmp_path: Path, 

284) -> None: 

285 """Verify Ruff fix removes unused imports when configured. 

286 

287 Runs Ruff fix with F401 rule selected on a file with unused imports 

288 and verifies fixes are applied. 

289 

290 Args: 

291 get_plugin: Fixture factory to get plugin instances. 

292 tmp_path: Pytest fixture providing a temporary directory. 

293 """ 

294 ruff_plugin = get_plugin("ruff") 

295 file_path = tmp_path / "unused_import.py" 

296 file_path.write_text("import os\nimport sys\n\nx = 1\n") 

297 

298 ruff_plugin.set_options(select=["F401"]) 

299 

300 result = ruff_plugin.fix([str(file_path)], {}) 

301 

302 assert_that(result).is_not_none() 

303 

304 

305# --- Integration tests for ruff check with various options --- 

306 

307 

308@pytest.mark.parametrize( 

309 ("option_name", "option_value"), 

310 [ 

311 ("select", ["F401"]), 

312 ("ignore", ["E501", "F401"]), 

313 ("line_length", 120), 

314 ], 

315 ids=["select_rules", "ignore_rules", "line_length"], 

316) 

317def test_check_with_options( 

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

319 temp_python_file_with_issues: str, 

320 option_name: str, 

321 option_value: object, 

322) -> None: 

323 """Verify Ruff check works with various configuration options. 

324 

325 Runs Ruff with different options configured and verifies the 

326 check completes successfully. 

327 

328 Args: 

329 get_plugin: Fixture factory to get plugin instances. 

330 temp_python_file_with_issues: Path to file with lint issues. 

331 option_name: Name of the option to set. 

332 option_value: Value to set for the option. 

333 """ 

334 ruff_plugin = get_plugin("ruff") 

335 ruff_plugin.set_options(**{option_name: option_value}) 

336 result = ruff_plugin.check([temp_python_file_with_issues], {}) 

337 

338 assert_that(result).is_not_none() 

339 assert_that(result.name).is_equal_to("ruff") 

340 

341 

342# --- Integration tests for ruff fix with various options --- 

343 

344 

345def test_fix_with_unsafe_fixes( 

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

347 tmp_path: Path, 

348) -> None: 

349 """Verify Ruff fix works with unsafe fixes enabled. 

350 

351 Runs Ruff fix with unsafe_fixes option enabled and verifies 

352 the fix completes. 

353 

354 Args: 

355 get_plugin: Fixture factory to get plugin instances. 

356 tmp_path: Pytest fixture providing a temporary directory. 

357 """ 

358 ruff_plugin = get_plugin("ruff") 

359 file_path = tmp_path / "unsafe_fix.py" 

360 file_path.write_text("import os\nimport sys\n\nx = 1\n") 

361 

362 ruff_plugin.set_options(unsafe_fixes=True, select=["F401"]) 

363 result = ruff_plugin.fix([str(file_path)], {}) 

364 

365 assert_that(result).is_not_none() 

366 

367 

368def test_fix_with_format_disabled( 

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

370 tmp_path: Path, 

371) -> None: 

372 """Verify Ruff fix works with formatting disabled. 

373 

374 Runs Ruff fix with format option disabled and verifies 

375 the fix completes. 

376 

377 Args: 

378 get_plugin: Fixture factory to get plugin instances. 

379 tmp_path: Pytest fixture providing a temporary directory. 

380 """ 

381 ruff_plugin = get_plugin("ruff") 

382 file_path = tmp_path / "no_format.py" 

383 file_path.write_text("x=1\ny=2\n") 

384 

385 ruff_plugin.set_options(format=False) 

386 result = ruff_plugin.fix([str(file_path)], {}) 

387 

388 assert_that(result).is_not_none() 

389 

390 

391# --- Tests for RuffPlugin.set_options method --- 

392 

393 

394@pytest.mark.parametrize( 

395 ("option_name", "option_value", "expected"), 

396 [ 

397 ("line_length", 100, 100), 

398 ("select", ["E", "F"], ["E", "F"]), 

399 ("ignore", ["E501"], ["E501"]), 

400 ("unsafe_fixes", True, True), 

401 ], 

402 ids=["line_length", "select_rules", "ignore_rules", "unsafe_fixes"], 

403) 

404def test_set_options( 

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

406 option_name: str, 

407 option_value: object, 

408 expected: object, 

409) -> None: 

410 """Verify RuffPlugin.set_options correctly sets various options. 

411 

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

413 

414 Args: 

415 get_plugin: Fixture factory to get plugin instances. 

416 option_name: Name of the option to set. 

417 option_value: Value to set for the option. 

418 expected: Expected value when retrieving the option. 

419 """ 

420 ruff_plugin = get_plugin("ruff") 

421 ruff_plugin.set_options(**{option_name: option_value}) 

422 assert_that(ruff_plugin.options.get(option_name)).is_equal_to(expected) 

423 

424 

425@pytest.mark.parametrize( 

426 ("option_value", "error_match"), 

427 [ 

428 ("not an int", "must be an integer"), 

429 (-1, None), 

430 ], 

431 ids=["invalid_type", "negative_value"], 

432) 

433def test_invalid_line_length( 

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

435 option_value: object, 

436 error_match: str | None, 

437) -> None: 

438 """Verify RuffPlugin.set_options rejects invalid line_length values. 

439 

440 Tests that invalid line_length values raise ValueError. 

441 

442 Args: 

443 get_plugin: Fixture factory to get plugin instances. 

444 option_value: Invalid value to set for line_length. 

445 error_match: Expected error message pattern, or None for any ValueError. 

446 """ 

447 ruff_plugin = get_plugin("ruff") 

448 if error_match: 

449 with pytest.raises(ValueError, match=error_match): 

450 ruff_plugin.set_options(line_length=option_value) 

451 else: 

452 with pytest.raises(ValueError): 

453 ruff_plugin.set_options(line_length=option_value)