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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for tool_config_generator module."""
3from pathlib import Path
5import pytest
6from assertpy import assert_that
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
23def test_returns_empty_when_no_enforce_settings() -> None:
24 """Should return empty list when no enforce settings."""
25 lintro_config = LintroConfig()
27 args = get_enforce_cli_args(
28 tool_name="ruff",
29 lintro_config=lintro_config,
30 )
32 assert_that(args).is_empty()
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 )
41 args = get_enforce_cli_args(
42 tool_name="black",
43 lintro_config=lintro_config,
44 )
46 assert_that(args).is_equal_to(["--line-length", "88"])
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 )
55 args = get_enforce_cli_args(
56 tool_name="ruff",
57 lintro_config=lintro_config,
58 )
60 assert_that(args).is_equal_to(["--target-version", "py312"])
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 )
72 args = get_enforce_cli_args(
73 tool_name="ruff",
74 lintro_config=lintro_config,
75 )
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")
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 )
89 args = get_enforce_cli_args(
90 tool_name="mypy",
91 lintro_config=lintro_config,
92 )
94 assert_that(args).is_equal_to(["--python-version", "3.13"])
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")
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 )
108 args = get_enforce_cli_args(
109 tool_name="yamllint",
110 lintro_config=lintro_config,
111 )
113 # yamllint doesn't support --line-length CLI flag
114 assert_that(args).is_empty()
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 )
124 assert_that(args).is_empty()
127def test_markdownlint_config_uses_correct_suffix() -> None:
128 """Should use .markdownlint-cli2.jsonc suffix for markdownlint.
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 )
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)
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 )
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)
161# =============================================================================
162# Key transformation tests
163# =============================================================================
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 )
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 }
183 transformed = _transform_keys_for_native_config(defaults, "hadolint")
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"])
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 }
200 transformed = _transform_keys_for_native_config(defaults, "hadolint")
202 assert_that(transformed).contains_key("ignored")
203 assert_that(transformed).contains_key("custom_key")
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 }
213 transformed = _transform_keys_for_native_config(defaults, "prettier")
215 assert_that(transformed).is_equal_to(defaults)
218def test_hadolint_config_file_has_correct_keys() -> None:
219 """Should write hadolint config with camelCase keys."""
220 import yaml
222 defaults = {
223 "ignored": [],
224 "trusted_registries": ["docker.io", "gcr.io"],
225 }
227 config_path = _write_defaults_config(
228 defaults=defaults,
229 tool_name="hadolint",
230 config_format=ConfigFormat.YAML,
231 )
233 try:
234 content = config_path.read_text()
235 parsed = yaml.safe_load(content)
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)
247# =============================================================================
248# Oxlint and Oxfmt config tests
249# =============================================================================
252def test_get_defaults_injection_args_oxlint(tmp_path: Path) -> None:
253 """Should return correct config args for oxlint.
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)
262 assert_that(args).is_equal_to(["--config", str(config_path)])
265def test_get_defaults_injection_args_oxfmt(tmp_path: Path) -> None:
266 """Should return correct config args for oxfmt.
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)
275 assert_that(args).is_equal_to(["--config", str(config_path)])
278def test_has_native_config_oxlint(
279 tmp_path: Path,
280 monkeypatch: pytest.MonkeyPatch,
281) -> None:
282 """Should detect oxlint native config file.
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": {}}')
291 assert_that(has_native_config("oxlint")).is_true()
294def test_has_native_config_oxfmt(
295 tmp_path: Path,
296 monkeypatch: pytest.MonkeyPatch,
297) -> None:
298 """Should detect oxfmt native config file.
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}')
307 assert_that(has_native_config("oxfmt")).is_true()
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.
316 Args:
317 tmp_path: Pytest temporary directory fixture.
318 monkeypatch: Pytest monkeypatch fixture.
319 """
320 monkeypatch.chdir(tmp_path)
322 assert_that(has_native_config("oxlint")).is_false()
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.
331 Args:
332 tmp_path: Pytest temporary directory fixture.
333 monkeypatch: Pytest monkeypatch fixture.
334 """
335 monkeypatch.chdir(tmp_path)
337 assert_that(has_native_config("oxfmt")).is_false()
340# =============================================================================
341# Prettier builtin defaults tests
342# =============================================================================
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.
351 Args:
352 tmp_path: Pytest temporary directory fixture.
353 monkeypatch: Pytest monkeypatch fixture.
354 """
355 monkeypatch.chdir(tmp_path)
356 lintro_config = LintroConfig()
358 config_path = generate_defaults_config("prettier", lintro_config)
360 assert_that(config_path).is_not_none()
361 assert config_path is not None
362 import json
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)
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.
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 )
385 config_path = generate_defaults_config("prettier", lintro_config)
387 assert_that(config_path).is_not_none()
388 assert config_path is not None
389 import json
391 content = json.loads(config_path.read_text())
392 assert_that(content["proseWrap"]).is_equal_to("never")
393 config_path.unlink(missing_ok=True)
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.
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 )
411 config_path = generate_defaults_config("prettier", lintro_config)
413 assert_that(config_path).is_not_none()
414 assert config_path is not None
415 import json
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)
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.
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()
439 config_path = generate_defaults_config("prettier", lintro_config)
441 assert_that(config_path).is_none()
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.
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("{}")
457 assert_that(has_native_config("prettier")).is_true()
460def test_get_defaults_injection_args_prettier(tmp_path: Path) -> None:
461 """Should return --no-config --config args for prettier.
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)
470 assert_that(args).is_equal_to(["--no-config", "--config", str(config_path)])