Coverage for tests / config / test_tool_config_generator.py: 100%

156 statements  

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

1"""Tests for tool_config_generator module.""" 

2 

3from pathlib import Path 

4 

5import pytest 

6from assertpy import assert_that 

7 

8from lintro.config.enforce_config import EnforceConfig 

9from lintro.config.lintro_config import LintroConfig 

10from lintro.config.tool_config_generator import ( 

11 NATIVE_KEY_MAPPINGS, 

12 _convert_python_version_for_mypy, 

13 _transform_keys_for_native_config, 

14 _write_defaults_config, 

15 generate_defaults_config, 

16 get_defaults_injection_args, 

17 get_enforce_cli_args, 

18 has_native_config, 

19) 

20from lintro.enums.config_format import ConfigFormat 

21 

22 

23def test_returns_empty_when_no_enforce_settings() -> None: 

24 """Should return empty list when no enforce settings.""" 

25 lintro_config = LintroConfig() 

26 

27 args = get_enforce_cli_args( 

28 tool_name="ruff", 

29 lintro_config=lintro_config, 

30 ) 

31 

32 assert_that(args).is_empty() 

33 

34 

35def test_injects_line_length_for_black() -> None: 

36 """Should inject --line-length for black.""" 

37 lintro_config = LintroConfig( 

38 enforce=EnforceConfig(line_length=88), 

39 ) 

40 

41 args = get_enforce_cli_args( 

42 tool_name="black", 

43 lintro_config=lintro_config, 

44 ) 

45 

46 assert_that(args).is_equal_to(["--line-length", "88"]) 

47 

48 

49def test_injects_target_version_for_ruff() -> None: 

50 """Should inject --target-version for ruff.""" 

51 lintro_config = LintroConfig( 

52 enforce=EnforceConfig(target_python="py312"), 

53 ) 

54 

55 args = get_enforce_cli_args( 

56 tool_name="ruff", 

57 lintro_config=lintro_config, 

58 ) 

59 

60 assert_that(args).is_equal_to(["--target-version", "py312"]) 

61 

62 

63def test_injects_both_line_length_and_target_version() -> None: 

64 """Should inject both settings when both are set.""" 

65 lintro_config = LintroConfig( 

66 enforce=EnforceConfig( 

67 line_length=100, 

68 target_python="py313", 

69 ), 

70 ) 

71 

72 args = get_enforce_cli_args( 

73 tool_name="ruff", 

74 lintro_config=lintro_config, 

75 ) 

76 

77 assert_that(args).contains("--line-length") 

78 assert_that(args).contains("100") 

79 assert_that(args).contains("--target-version") 

80 assert_that(args).contains("py313") 

81 

82 

83def test_converts_target_version_format_for_mypy() -> None: 

84 """Should convert py313 format to 3.13 for mypy.""" 

85 lintro_config = LintroConfig( 

86 enforce=EnforceConfig(target_python="py313"), 

87 ) 

88 

89 args = get_enforce_cli_args( 

90 tool_name="mypy", 

91 lintro_config=lintro_config, 

92 ) 

93 

94 assert_that(args).is_equal_to(["--python-version", "3.13"]) 

95 

96 

97def test_convert_python_version_helper_handles_plain_version() -> None: 

98 """Should return plain version unchanged when already numeric.""" 

99 assert_that(_convert_python_version_for_mypy("3.12")).is_equal_to("3.12") 

100 

101 

102def test_returns_empty_for_unsupported_tool() -> None: 

103 """Should return empty list for tools without CLI mappings.""" 

104 lintro_config = LintroConfig( 

105 enforce=EnforceConfig(line_length=100), 

106 ) 

107 

108 args = get_enforce_cli_args( 

109 tool_name="yamllint", 

110 lintro_config=lintro_config, 

111 ) 

112 

113 # yamllint doesn't support --line-length CLI flag 

114 assert_that(args).is_empty() 

115 

116 

117def test_returns_empty_for_none_path() -> None: 

118 """Should return empty list when no config path.""" 

119 args = get_defaults_injection_args( 

120 tool_name="prettier", 

121 config_path=None, 

122 ) 

123 

124 assert_that(args).is_empty() 

125 

126 

127def test_markdownlint_config_uses_correct_suffix() -> None: 

128 """Should use .markdownlint-cli2.jsonc suffix for markdownlint. 

129 

130 markdownlint-cli2 v0.17+ requires config files to follow strict naming 

131 conventions. The temp config file must end with a recognized suffix. 

132 """ 

133 config_path = _write_defaults_config( 

134 defaults={"config": {"MD013": {"line_length": 100}}}, 

135 tool_name="markdownlint", 

136 config_format=ConfigFormat.JSON, 

137 ) 

138 

139 try: 

140 assert_that(str(config_path)).ends_with(".markdownlint-cli2.jsonc") 

141 assert_that(config_path.exists()).is_true() 

142 finally: 

143 config_path.unlink(missing_ok=True) 

144 

145 

146def test_generic_tool_config_uses_json_suffix() -> None: 

147 """Should use .json suffix for tools without special requirements.""" 

148 config_path = _write_defaults_config( 

149 defaults={"some": "config"}, 

150 tool_name="prettier", 

151 config_format=ConfigFormat.JSON, 

152 ) 

153 

154 try: 

155 assert_that(str(config_path)).ends_with(".json") 

156 assert_that(config_path.exists()).is_true() 

157 finally: 

158 config_path.unlink(missing_ok=True) 

159 

160 

161# ============================================================================= 

162# Key transformation tests 

163# ============================================================================= 

164 

165 

166def test_hadolint_key_mapping_exists() -> None: 

167 """Should have key mappings defined for hadolint.""" 

168 assert_that(NATIVE_KEY_MAPPINGS).contains_key("hadolint") 

169 hadolint_mappings = NATIVE_KEY_MAPPINGS["hadolint"] 

170 assert_that(hadolint_mappings).contains_key("trusted_registries") 

171 assert_that(hadolint_mappings["trusted_registries"]).is_equal_to( 

172 "trustedRegistries", 

173 ) 

174 

175 

176def test_transform_keys_converts_hadolint_trusted_registries() -> None: 

177 """Should convert trusted_registries to trustedRegistries for hadolint.""" 

178 defaults = { 

179 "ignored": ["DL3006"], 

180 "trusted_registries": ["docker.io", "gcr.io"], 

181 } 

182 

183 transformed = _transform_keys_for_native_config(defaults, "hadolint") 

184 

185 assert_that(transformed).contains_key("trustedRegistries") 

186 assert_that(transformed).does_not_contain_key("trusted_registries") 

187 assert_that(transformed["trustedRegistries"]).is_equal_to(["docker.io", "gcr.io"]) 

188 # "ignored" should remain unchanged 

189 assert_that(transformed).contains_key("ignored") 

190 assert_that(transformed["ignored"]).is_equal_to(["DL3006"]) 

191 

192 

193def test_transform_keys_preserves_unmapped_keys() -> None: 

194 """Should preserve keys that have no mapping.""" 

195 defaults = { 

196 "ignored": ["DL3006"], 

197 "custom_key": "value", 

198 } 

199 

200 transformed = _transform_keys_for_native_config(defaults, "hadolint") 

201 

202 assert_that(transformed).contains_key("ignored") 

203 assert_that(transformed).contains_key("custom_key") 

204 

205 

206def test_transform_keys_returns_unchanged_for_unknown_tool() -> None: 

207 """Should return defaults unchanged for tools without mappings.""" 

208 defaults = { 

209 "some_key": "value", 

210 "another_key": 123, 

211 } 

212 

213 transformed = _transform_keys_for_native_config(defaults, "prettier") 

214 

215 assert_that(transformed).is_equal_to(defaults) 

216 

217 

218def test_hadolint_config_file_has_correct_keys() -> None: 

219 """Should write hadolint config with camelCase keys.""" 

220 import yaml 

221 

222 defaults = { 

223 "ignored": [], 

224 "trusted_registries": ["docker.io", "gcr.io"], 

225 } 

226 

227 config_path = _write_defaults_config( 

228 defaults=defaults, 

229 tool_name="hadolint", 

230 config_format=ConfigFormat.YAML, 

231 ) 

232 

233 try: 

234 content = config_path.read_text() 

235 parsed = yaml.safe_load(content) 

236 

237 # Should have camelCase key 

238 assert_that(parsed).contains_key("trustedRegistries") 

239 assert_that(parsed).does_not_contain_key("trusted_registries") 

240 assert_that(parsed["trustedRegistries"]).is_equal_to(["docker.io", "gcr.io"]) 

241 # ignored should remain as-is 

242 assert_that(parsed).contains_key("ignored") 

243 finally: 

244 config_path.unlink(missing_ok=True) 

245 

246 

247# ============================================================================= 

248# Oxlint and Oxfmt config tests 

249# ============================================================================= 

250 

251 

252def test_get_defaults_injection_args_oxlint(tmp_path: Path) -> None: 

253 """Should return correct config args for oxlint. 

254 

255 Args: 

256 tmp_path: Pytest temporary directory fixture. 

257 """ 

258 config_path = tmp_path / "test.json" 

259 config_path.write_text("{}") 

260 args = get_defaults_injection_args("oxlint", config_path) 

261 

262 assert_that(args).is_equal_to(["--config", str(config_path)]) 

263 

264 

265def test_get_defaults_injection_args_oxfmt(tmp_path: Path) -> None: 

266 """Should return correct config args for oxfmt. 

267 

268 Args: 

269 tmp_path: Pytest temporary directory fixture. 

270 """ 

271 config_path = tmp_path / "test.json" 

272 config_path.write_text("{}") 

273 args = get_defaults_injection_args("oxfmt", config_path) 

274 

275 assert_that(args).is_equal_to(["--config", str(config_path)]) 

276 

277 

278def test_has_native_config_oxlint( 

279 tmp_path: Path, 

280 monkeypatch: pytest.MonkeyPatch, 

281) -> None: 

282 """Should detect oxlint native config file. 

283 

284 Args: 

285 tmp_path: Pytest temporary directory fixture. 

286 monkeypatch: Pytest monkeypatch fixture. 

287 """ 

288 monkeypatch.chdir(tmp_path) 

289 (tmp_path / ".oxlintrc.json").write_text('{"rules": {}}') 

290 

291 assert_that(has_native_config("oxlint")).is_true() 

292 

293 

294def test_has_native_config_oxfmt( 

295 tmp_path: Path, 

296 monkeypatch: pytest.MonkeyPatch, 

297) -> None: 

298 """Should detect oxfmt native config file. 

299 

300 Args: 

301 tmp_path: Pytest temporary directory fixture. 

302 monkeypatch: Pytest monkeypatch fixture. 

303 """ 

304 monkeypatch.chdir(tmp_path) 

305 (tmp_path / ".oxfmtrc.json").write_text('{"printWidth": 100}') 

306 

307 assert_that(has_native_config("oxfmt")).is_true() 

308 

309 

310def test_has_native_config_oxlint_not_found( 

311 tmp_path: Path, 

312 monkeypatch: pytest.MonkeyPatch, 

313) -> None: 

314 """Should return False when no oxlint config exists. 

315 

316 Args: 

317 tmp_path: Pytest temporary directory fixture. 

318 monkeypatch: Pytest monkeypatch fixture. 

319 """ 

320 monkeypatch.chdir(tmp_path) 

321 

322 assert_that(has_native_config("oxlint")).is_false() 

323 

324 

325def test_has_native_config_oxfmt_not_found( 

326 tmp_path: Path, 

327 monkeypatch: pytest.MonkeyPatch, 

328) -> None: 

329 """Should return False when no oxfmt config exists. 

330 

331 Args: 

332 tmp_path: Pytest temporary directory fixture. 

333 monkeypatch: Pytest monkeypatch fixture. 

334 """ 

335 monkeypatch.chdir(tmp_path) 

336 

337 assert_that(has_native_config("oxfmt")).is_false() 

338 

339 

340# ============================================================================= 

341# Prettier builtin defaults tests 

342# ============================================================================= 

343 

344 

345def test_prettier_builtin_defaults_applied_when_no_user_defaults( 

346 tmp_path: Path, 

347 monkeypatch: pytest.MonkeyPatch, 

348) -> None: 

349 """Should generate config with builtin defaults when user sets none. 

350 

351 Args: 

352 tmp_path: Pytest temporary directory fixture. 

353 monkeypatch: Pytest monkeypatch fixture. 

354 """ 

355 monkeypatch.chdir(tmp_path) 

356 lintro_config = LintroConfig() 

357 

358 config_path = generate_defaults_config("prettier", lintro_config) 

359 

360 assert_that(config_path).is_not_none() 

361 assert config_path is not None 

362 import json 

363 

364 content = json.loads(config_path.read_text()) 

365 assert_that(content).contains_key("proseWrap") 

366 assert_that(content["proseWrap"]).is_equal_to("always") 

367 config_path.unlink(missing_ok=True) 

368 

369 

370def test_prettier_user_defaults_override_builtin_defaults( 

371 tmp_path: Path, 

372 monkeypatch: pytest.MonkeyPatch, 

373) -> None: 

374 """Should let user defaults override builtin defaults. 

375 

376 Args: 

377 tmp_path: Pytest temporary directory fixture. 

378 monkeypatch: Pytest monkeypatch fixture. 

379 """ 

380 monkeypatch.chdir(tmp_path) 

381 lintro_config = LintroConfig( 

382 defaults={"prettier": {"proseWrap": "never"}}, 

383 ) 

384 

385 config_path = generate_defaults_config("prettier", lintro_config) 

386 

387 assert_that(config_path).is_not_none() 

388 assert config_path is not None 

389 import json 

390 

391 content = json.loads(config_path.read_text()) 

392 assert_that(content["proseWrap"]).is_equal_to("never") 

393 config_path.unlink(missing_ok=True) 

394 

395 

396def test_prettier_user_defaults_merged_with_builtin_defaults( 

397 tmp_path: Path, 

398 monkeypatch: pytest.MonkeyPatch, 

399) -> None: 

400 """Should merge user defaults on top of builtin defaults. 

401 

402 Args: 

403 tmp_path: Pytest temporary directory fixture. 

404 monkeypatch: Pytest monkeypatch fixture. 

405 """ 

406 monkeypatch.chdir(tmp_path) 

407 lintro_config = LintroConfig( 

408 defaults={"prettier": {"tabWidth": 4}}, 

409 ) 

410 

411 config_path = generate_defaults_config("prettier", lintro_config) 

412 

413 assert_that(config_path).is_not_none() 

414 assert config_path is not None 

415 import json 

416 

417 content = json.loads(config_path.read_text()) 

418 # Builtin default should be present 

419 assert_that(content["proseWrap"]).is_equal_to("always") 

420 # User default should also be present 

421 assert_that(content["tabWidth"]).is_equal_to(4) 

422 config_path.unlink(missing_ok=True) 

423 

424 

425def test_prettier_native_config_skips_defaults_generation( 

426 tmp_path: Path, 

427 monkeypatch: pytest.MonkeyPatch, 

428) -> None: 

429 """Should skip defaults generation when native config exists. 

430 

431 Args: 

432 tmp_path: Pytest temporary directory fixture. 

433 monkeypatch: Pytest monkeypatch fixture. 

434 """ 

435 monkeypatch.chdir(tmp_path) 

436 (tmp_path / ".prettierrc").write_text("{}") 

437 lintro_config = LintroConfig() 

438 

439 config_path = generate_defaults_config("prettier", lintro_config) 

440 

441 assert_that(config_path).is_none() 

442 

443 

444def test_has_native_config_prettier_detects_prettierrc( 

445 tmp_path: Path, 

446 monkeypatch: pytest.MonkeyPatch, 

447) -> None: 

448 """Should detect .prettierrc as native config. 

449 

450 Args: 

451 tmp_path: Pytest temporary directory fixture. 

452 monkeypatch: Pytest monkeypatch fixture. 

453 """ 

454 monkeypatch.chdir(tmp_path) 

455 (tmp_path / ".prettierrc").write_text("{}") 

456 

457 assert_that(has_native_config("prettier")).is_true() 

458 

459 

460def test_get_defaults_injection_args_prettier(tmp_path: Path) -> None: 

461 """Should return --no-config --config args for prettier. 

462 

463 Args: 

464 tmp_path: Pytest temporary directory fixture. 

465 """ 

466 config_path = tmp_path / "test.json" 

467 config_path.write_text("{}") 

468 args = get_defaults_injection_args("prettier", config_path) 

469 

470 assert_that(args).is_equal_to(["--no-config", "--config", str(config_path)])