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

90 statements  

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

1"""Integration tests for shfmt tool definition. 

2 

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

4They verify the ShfmtPlugin 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 shfmt is not installed 

21pytestmark = pytest.mark.skipif( 

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

23 reason="shfmt not installed", 

24) 

25 

26 

27@pytest.fixture 

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

29 """Create a temporary shell script with formatting issues. 

30 

31 Creates a file containing shell code with formatting issues that shfmt 

32 should detect, including: 

33 - Inconsistent indentation 

34 - Missing spaces 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_script.sh" 

43 file_path.write_text( 

44 """\ 

45#!/bin/bash 

46if [ "$1" = "test" ];then 

47echo "hello" 

48 echo "world" 

49fi 

50""", 

51 ) 

52 return str(file_path) 

53 

54 

55@pytest.fixture 

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

57 """Create a temporary shell script with no formatting issues. 

58 

59 Creates a file containing properly formatted shell code that should pass 

60 shfmt checking 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_script.sh" 

69 file_path.write_text( 

70 """\ 

71#!/bin/bash 

72 

73# A clean shell script 

74say_hello() { 

75 echo "Hello, World!" 

76} 

77 

78if [ -n "$1" ]; then 

79 say_hello 

80fi 

81""", 

82 ) 

83 return str(file_path) 

84 

85 

86@pytest.fixture 

87def temp_shell_file_complex_issues(tmp_path: Path) -> str: 

88 """Create a temporary shell script with multiple formatting issues. 

89 

90 Creates a file containing code with various formatting issues that shfmt 

91 should fix, including: 

92 - No space after semicolon in for loop 

93 - Inconsistent indentation 

94 - Binary operator at end of line instead of start 

95 

96 Args: 

97 tmp_path: Pytest fixture providing a temporary directory. 

98 

99 Returns: 

100 Path to the created file as a string. 

101 """ 

102 file_path = tmp_path / "complex_script.sh" 

103 file_path.write_text( 

104 """\ 

105#!/bin/bash 

106for i in 1 2 3;do 

107echo $i 

108done 

109if [ "$1" = "a" ] || 

110[ "$1" = "b" ]; then 

111 echo "match" 

112fi 

113""", 

114 ) 

115 return str(file_path) 

116 

117 

118# --- Tests for ShfmtPlugin definition --- 

119 

120 

121@pytest.mark.parametrize( 

122 ("attr", "expected"), 

123 [ 

124 ("name", "shfmt"), 

125 ("can_fix", True), 

126 ], 

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

128) 

129def test_definition_attributes( 

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

131 attr: str, 

132 expected: object, 

133) -> None: 

134 """Verify ShfmtPlugin definition has correct attribute values. 

135 

136 Tests that the plugin definition exposes the expected values for 

137 name and can_fix attributes. 

138 

139 Args: 

140 get_plugin: Fixture factory to get plugin instances. 

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

142 expected: The expected value of the attribute. 

143 """ 

144 shfmt_plugin = get_plugin("shfmt") 

145 assert_that(getattr(shfmt_plugin.definition, attr)).is_equal_to(expected) 

146 

147 

148def test_definition_file_patterns( 

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

150) -> None: 

151 """Verify ShfmtPlugin definition includes shell file patterns. 

152 

153 Tests that the plugin is configured to handle shell files (*.sh, *.bash, *.ksh). 

154 

155 Args: 

156 get_plugin: Fixture factory to get plugin instances. 

157 """ 

158 shfmt_plugin = get_plugin("shfmt") 

159 assert_that(shfmt_plugin.definition.file_patterns).contains("*.sh") 

160 assert_that(shfmt_plugin.definition.file_patterns).contains("*.bash") 

161 

162 

163def test_definition_has_version_command( 

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

165) -> None: 

166 """Verify ShfmtPlugin definition has a version command. 

167 

168 Tests that the plugin exposes a version command for checking 

169 the installed shfmt version. 

170 

171 Args: 

172 get_plugin: Fixture factory to get plugin instances. 

173 """ 

174 shfmt_plugin = get_plugin("shfmt") 

175 assert_that(shfmt_plugin.definition.version_command).is_not_none() 

176 

177 

178# --- Integration tests for shfmt check command --- 

179 

180 

181def test_check_file_with_issues( 

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

183 temp_shell_file_with_issues: str, 

184) -> None: 

185 """Verify shfmt check detects formatting issues in problematic files. 

186 

187 Runs shfmt on a file containing formatting issues and verifies that 

188 issues are found. 

189 

190 Args: 

191 get_plugin: Fixture factory to get plugin instances. 

192 temp_shell_file_with_issues: Path to file with formatting issues. 

193 """ 

194 shfmt_plugin = get_plugin("shfmt") 

195 result = shfmt_plugin.check([temp_shell_file_with_issues], {}) 

196 

197 assert_that(result).is_not_none() 

198 assert_that(result.name).is_equal_to("shfmt") 

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

200 

201 

202def test_check_clean_file( 

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

204 temp_shell_file_clean: str, 

205) -> None: 

206 """Verify shfmt check passes on clean files. 

207 

208 Runs shfmt on a clean file and verifies no issues are found. 

209 

210 Args: 

211 get_plugin: Fixture factory to get plugin instances. 

212 temp_shell_file_clean: Path to clean file. 

213 """ 

214 shfmt_plugin = get_plugin("shfmt") 

215 result = shfmt_plugin.check([temp_shell_file_clean], {}) 

216 

217 assert_that(result).is_not_none() 

218 assert_that(result.name).is_equal_to("shfmt") 

219 assert_that(result.success).is_true() 

220 

221 

222def test_check_empty_directory( 

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

224 tmp_path: Path, 

225) -> None: 

226 """Verify shfmt check handles empty directories gracefully. 

227 

228 Runs shfmt on an empty directory and verifies a result is returned 

229 with zero issues. 

230 

231 Args: 

232 get_plugin: Fixture factory to get plugin instances. 

233 tmp_path: Pytest fixture providing a temporary directory. 

234 """ 

235 shfmt_plugin = get_plugin("shfmt") 

236 result = shfmt_plugin.check([str(tmp_path)], {}) 

237 

238 assert_that(result).is_not_none() 

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

240 

241 

242# --- Integration tests for shfmt fix command --- 

243 

244 

245def test_fix_formats_file( 

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

247 temp_shell_file_with_issues: str, 

248) -> None: 

249 """Verify shfmt fix reformats files with formatting issues. 

250 

251 Runs shfmt fix on a file with formatting issues and verifies 

252 the file content changes. 

253 

254 Args: 

255 get_plugin: Fixture factory to get plugin instances. 

256 temp_shell_file_with_issues: Path to file with formatting issues. 

257 """ 

258 shfmt_plugin = get_plugin("shfmt") 

259 original = Path(temp_shell_file_with_issues).read_text() 

260 

261 result = shfmt_plugin.fix([temp_shell_file_with_issues], {}) 

262 

263 assert_that(result).is_not_none() 

264 assert_that(result.name).is_equal_to("shfmt") 

265 

266 new_content = Path(temp_shell_file_with_issues).read_text() 

267 assert_that(new_content).is_not_equal_to(original) 

268 

269 

270def test_fix_complex_file( 

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

272 temp_shell_file_complex_issues: str, 

273) -> None: 

274 """Verify shfmt fix handles complex formatting issues. 

275 

276 Runs shfmt fix on a file with multiple formatting issues and verifies 

277 fixes are applied. 

278 

279 Args: 

280 get_plugin: Fixture factory to get plugin instances. 

281 temp_shell_file_complex_issues: Path to file with complex issues. 

282 """ 

283 shfmt_plugin = get_plugin("shfmt") 

284 original = Path(temp_shell_file_complex_issues).read_text() 

285 

286 result = shfmt_plugin.fix([temp_shell_file_complex_issues], {}) 

287 

288 assert_that(result).is_not_none() 

289 

290 new_content = Path(temp_shell_file_complex_issues).read_text() 

291 assert_that(new_content).is_not_equal_to(original) 

292 

293 

294def test_fix_clean_file_unchanged( 

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

296 temp_shell_file_clean: str, 

297) -> None: 

298 """Verify shfmt fix doesn't change already formatted files. 

299 

300 Runs shfmt fix on a clean file and verifies the content stays the same. 

301 

302 Args: 

303 get_plugin: Fixture factory to get plugin instances. 

304 temp_shell_file_clean: Path to clean file. 

305 """ 

306 shfmt_plugin = get_plugin("shfmt") 

307 original = Path(temp_shell_file_clean).read_text() 

308 

309 result = shfmt_plugin.fix([temp_shell_file_clean], {}) 

310 

311 assert_that(result).is_not_none() 

312 assert_that(result.success).is_true() 

313 

314 new_content = Path(temp_shell_file_clean).read_text() 

315 assert_that(new_content).is_equal_to(original) 

316 

317 

318# --- Integration tests for shfmt check with various options --- 

319 

320 

321@pytest.mark.parametrize( 

322 ("option_name", "option_value"), 

323 [ 

324 ("indent", 4), 

325 ("binary_next_line", True), 

326 ("switch_case_indent", True), 

327 ("space_redirects", True), 

328 ("language_dialect", "bash"), 

329 ("simplify", True), 

330 ], 

331 ids=[ 

332 "indent", 

333 "binary_next_line", 

334 "switch_case_indent", 

335 "space_redirects", 

336 "language_dialect", 

337 "simplify", 

338 ], 

339) 

340def test_check_with_options( 

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

342 temp_shell_file_with_issues: str, 

343 option_name: str, 

344 option_value: object, 

345) -> None: 

346 """Verify shfmt check works with various configuration options. 

347 

348 Runs shfmt with different options configured and verifies the 

349 check completes successfully. 

350 

351 Args: 

352 get_plugin: Fixture factory to get plugin instances. 

353 temp_shell_file_with_issues: Path to file with formatting issues. 

354 option_name: Name of the option to set. 

355 option_value: Value to set for the option. 

356 """ 

357 shfmt_plugin = get_plugin("shfmt") 

358 shfmt_plugin.set_options(**{option_name: option_value}) 

359 result = shfmt_plugin.check([temp_shell_file_with_issues], {}) 

360 

361 assert_that(result).is_not_none() 

362 assert_that(result.name).is_equal_to("shfmt") 

363 

364 

365# --- Tests for ShfmtPlugin.set_options method --- 

366 

367 

368@pytest.mark.parametrize( 

369 ("option_name", "option_value", "expected"), 

370 [ 

371 ("indent", 4, 4), 

372 ("indent", 0, 0), 

373 ("binary_next_line", True, True), 

374 ("switch_case_indent", True, True), 

375 ("space_redirects", True, True), 

376 ("language_dialect", "bash", "bash"), 

377 ("language_dialect", "posix", "posix"), 

378 ("simplify", True, True), 

379 ], 

380 ids=[ 

381 "indent_spaces", 

382 "indent_tabs", 

383 "binary_next_line", 

384 "switch_case_indent", 

385 "space_redirects", 

386 "dialect_bash", 

387 "dialect_posix", 

388 "simplify", 

389 ], 

390) 

391def test_set_options( 

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

393 option_name: str, 

394 option_value: object, 

395 expected: object, 

396) -> None: 

397 """Verify ShfmtPlugin.set_options correctly sets various options. 

398 

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

400 

401 Args: 

402 get_plugin: Fixture factory to get plugin instances. 

403 option_name: Name of the option to set. 

404 option_value: Value to set for the option. 

405 expected: Expected value when retrieving the option. 

406 """ 

407 shfmt_plugin = get_plugin("shfmt") 

408 shfmt_plugin.set_options(**{option_name: option_value}) 

409 assert_that(shfmt_plugin.options.get(option_name)).is_equal_to(expected) 

410 

411 

412def test_invalid_language_dialect( 

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

414) -> None: 

415 """Verify ShfmtPlugin.set_options rejects invalid language_dialect values. 

416 

417 Tests that invalid language_dialect values raise ValueError. 

418 

419 Args: 

420 get_plugin: Fixture factory to get plugin instances. 

421 """ 

422 shfmt_plugin = get_plugin("shfmt") 

423 with pytest.raises(ValueError, match="Invalid language_dialect"): 

424 shfmt_plugin.set_options(language_dialect="invalid")