Coverage for tests / unit / config / test_config_tool_specific.py: 100%
67 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"""Unit tests for tool-specific config loaders.
3This module contains function-based pytest tests for tool-specific config
4loaders including ruff, mypy, bandit, and black.
5"""
7from __future__ import annotations
9from pathlib import Path
10from typing import Any
11from unittest.mock import MagicMock, patch
13import pytest
14from assertpy import assert_that
16from lintro.utils.config import (
17 load_bandit_config,
18 load_black_config,
19 load_mypy_config,
20 load_ruff_config,
21)
23# =============================================================================
24# Fixtures
25# =============================================================================
28@pytest.fixture
29def mock_load_tool_config() -> Any:
30 """Factory fixture for mocking load_tool_config_from_pyproject.
32 Returns:
33 Function that creates a patch context manager with the given return value.
34 """
36 def _create_mock(return_value: dict[str, Any]) -> Any:
37 return patch(
38 "lintro.utils.config.load_tool_config_from_pyproject",
39 return_value=return_value,
40 )
42 return _create_mock
45# =============================================================================
46# Tests for load_ruff_config
47# =============================================================================
50def test_load_ruff_config_flattens_lint_section(mock_load_tool_config: Any) -> None:
51 """Verify load_ruff_config flattens the lint section to top level.
53 Ruff's lint.select, lint.ignore, lint.extend-select, and lint.extend-ignore
54 should be flattened to top-level keys with underscores.
56 Args:
57 mock_load_tool_config: Factory fixture for mocking load_tool_config_from_pyproject.
58 """
59 mock_config = {
60 "line-length": 100,
61 "lint": {
62 "select": ["E", "F"],
63 "ignore": ["E501"],
64 "extend-select": ["W"],
65 "extend-ignore": ["E402"],
66 },
67 }
68 with mock_load_tool_config(mock_config):
69 result = load_ruff_config()
71 assert_that(result["select"]).is_equal_to(["E", "F"])
72 assert_that(result["select"]).is_length(2)
73 assert_that(result["ignore"]).is_equal_to(["E501"])
74 assert_that(result["extend_select"]).is_equal_to(["W"])
75 assert_that(result["extend_ignore"]).is_equal_to(["E402"])
78def test_load_ruff_config_handles_non_dict_lint_section(
79 mock_load_tool_config: Any,
80) -> None:
81 """Verify load_ruff_config handles non-dict lint section gracefully.
83 When lint is not a dict, select and other lint keys should not be present.
85 Args:
86 mock_load_tool_config: Factory fixture for mocking load_tool_config_from_pyproject.
87 """
88 with mock_load_tool_config({"lint": "invalid", "line-length": 88}):
89 result = load_ruff_config()
91 assert_that(result).does_not_contain_key("select")
92 assert_that(result).does_not_contain_key("ignore")
95def test_load_ruff_config_handles_empty_config(mock_load_tool_config: Any) -> None:
96 """Verify load_ruff_config handles empty configuration.
98 Args:
99 mock_load_tool_config: Factory fixture for mocking load_tool_config_from_pyproject.
100 """
101 with mock_load_tool_config({}):
102 result = load_ruff_config()
104 assert_that(result).is_instance_of(dict)
107# =============================================================================
108# Tests for tool-specific config loaders (bandit, black)
109# =============================================================================
112@pytest.mark.parametrize(
113 ("loader_func", "tool_name", "config_data", "expected"),
114 [
115 pytest.param(
116 load_bandit_config,
117 "bandit",
118 {"exclude_dirs": ["tests"]},
119 {"exclude_dirs": ["tests"]},
120 id="bandit-config",
121 ),
122 pytest.param(
123 load_black_config,
124 "black",
125 {"line-length": 88},
126 {"line-length": 88},
127 id="black-config",
128 ),
129 ],
130)
131def test_tool_config_loaders_return_correct_config(
132 loader_func: Any,
133 tool_name: str,
134 config_data: dict[str, Any],
135 expected: dict[str, Any],
136) -> None:
137 """Test that tool-specific config loaders correctly return tool configuration.
139 Args:
140 loader_func: The config loader function to test.
141 tool_name: Name of the tool (for documentation).
142 config_data: Mock config data to return.
143 expected: Expected result from the loader.
144 """
145 with patch(
146 "lintro.utils.config.load_tool_config_from_pyproject",
147 return_value=config_data,
148 ):
149 result = loader_func()
151 assert_that(result).is_equal_to(expected)
152 assert_that(result).is_instance_of(dict)
155# =============================================================================
156# Tests for load_mypy_config
157# =============================================================================
160def test_load_mypy_config_from_pyproject(tmp_path: Path) -> None:
161 """Verify mypy config loads correctly from pyproject.toml.
163 Args:
164 tmp_path: Pytest temporary directory fixture.
165 """
166 pyproject = tmp_path / "pyproject.toml"
167 pyproject.write_text("[tool.mypy]\nstrict = true\nwarn_return_any = true\n")
169 config, path = load_mypy_config(base_dir=tmp_path)
171 assert_that(config).is_equal_to({"strict": True, "warn_return_any": True})
172 assert_that(path).is_equal_to(pyproject)
173 assert_that(config).is_instance_of(dict)
176def test_load_mypy_config_from_mypy_ini(tmp_path: Path) -> None:
177 """Verify mypy config loads correctly from mypy.ini.
179 Args:
180 tmp_path: Pytest temporary directory fixture.
181 """
182 mypy_ini = tmp_path / "mypy.ini"
183 mypy_ini.write_text("[mypy]\nstrict = true\n")
185 config, path = load_mypy_config(base_dir=tmp_path)
187 assert_that(config).contains_key("strict")
188 assert_that(path).is_equal_to(mypy_ini)
191def test_load_mypy_config_from_dot_mypy_ini(tmp_path: Path) -> None:
192 """Verify mypy config loads correctly from .mypy.ini.
194 Args:
195 tmp_path: Pytest temporary directory fixture.
196 """
197 dot_mypy_ini = tmp_path / ".mypy.ini"
198 dot_mypy_ini.write_text("[mypy]\nwarn_unused_ignores = True\n")
200 config, path = load_mypy_config(base_dir=tmp_path)
202 assert_that(config).contains_key("warn_unused_ignores")
203 assert_that(path).is_equal_to(dot_mypy_ini)
206def test_load_mypy_config_returns_empty_when_no_config_file(tmp_path: Path) -> None:
207 """Verify load_mypy_config returns empty config when no files found.
209 Args:
210 tmp_path: Pytest temporary directory fixture.
211 """
212 config, path = load_mypy_config(base_dir=tmp_path)
214 assert_that(config).is_empty()
215 assert_that(config).is_instance_of(dict)
216 assert_that(path).is_none()
219def test_load_mypy_config_defaults_to_cwd_when_no_base_dir() -> None:
220 """Verify load_mypy_config defaults to current working directory."""
221 with patch("lintro.utils.config.Path") as mock_path:
222 mock_cwd = MagicMock()
223 mock_path.cwd.return_value = mock_cwd
224 mock_cwd.__truediv__ = MagicMock(
225 return_value=MagicMock(exists=MagicMock(return_value=False)),
226 )
228 load_mypy_config(base_dir=None)
230 mock_path.cwd.assert_called_once()