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

1"""Unit tests for tool-specific config loaders. 

2 

3This module contains function-based pytest tests for tool-specific config 

4loaders including ruff, mypy, bandit, and black. 

5""" 

6 

7from __future__ import annotations 

8 

9from pathlib import Path 

10from typing import Any 

11from unittest.mock import MagicMock, patch 

12 

13import pytest 

14from assertpy import assert_that 

15 

16from lintro.utils.config import ( 

17 load_bandit_config, 

18 load_black_config, 

19 load_mypy_config, 

20 load_ruff_config, 

21) 

22 

23# ============================================================================= 

24# Fixtures 

25# ============================================================================= 

26 

27 

28@pytest.fixture 

29def mock_load_tool_config() -> Any: 

30 """Factory fixture for mocking load_tool_config_from_pyproject. 

31 

32 Returns: 

33 Function that creates a patch context manager with the given return value. 

34 """ 

35 

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 ) 

41 

42 return _create_mock 

43 

44 

45# ============================================================================= 

46# Tests for load_ruff_config 

47# ============================================================================= 

48 

49 

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. 

52 

53 Ruff's lint.select, lint.ignore, lint.extend-select, and lint.extend-ignore 

54 should be flattened to top-level keys with underscores. 

55 

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() 

70 

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"]) 

76 

77 

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. 

82 

83 When lint is not a dict, select and other lint keys should not be present. 

84 

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() 

90 

91 assert_that(result).does_not_contain_key("select") 

92 assert_that(result).does_not_contain_key("ignore") 

93 

94 

95def test_load_ruff_config_handles_empty_config(mock_load_tool_config: Any) -> None: 

96 """Verify load_ruff_config handles empty configuration. 

97 

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() 

103 

104 assert_that(result).is_instance_of(dict) 

105 

106 

107# ============================================================================= 

108# Tests for tool-specific config loaders (bandit, black) 

109# ============================================================================= 

110 

111 

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. 

138 

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() 

150 

151 assert_that(result).is_equal_to(expected) 

152 assert_that(result).is_instance_of(dict) 

153 

154 

155# ============================================================================= 

156# Tests for load_mypy_config 

157# ============================================================================= 

158 

159 

160def test_load_mypy_config_from_pyproject(tmp_path: Path) -> None: 

161 """Verify mypy config loads correctly from pyproject.toml. 

162 

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") 

168 

169 config, path = load_mypy_config(base_dir=tmp_path) 

170 

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) 

174 

175 

176def test_load_mypy_config_from_mypy_ini(tmp_path: Path) -> None: 

177 """Verify mypy config loads correctly from mypy.ini. 

178 

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") 

184 

185 config, path = load_mypy_config(base_dir=tmp_path) 

186 

187 assert_that(config).contains_key("strict") 

188 assert_that(path).is_equal_to(mypy_ini) 

189 

190 

191def test_load_mypy_config_from_dot_mypy_ini(tmp_path: Path) -> None: 

192 """Verify mypy config loads correctly from .mypy.ini. 

193 

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") 

199 

200 config, path = load_mypy_config(base_dir=tmp_path) 

201 

202 assert_that(config).contains_key("warn_unused_ignores") 

203 assert_that(path).is_equal_to(dot_mypy_ini) 

204 

205 

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. 

208 

209 Args: 

210 tmp_path: Pytest temporary directory fixture. 

211 """ 

212 config, path = load_mypy_config(base_dir=tmp_path) 

213 

214 assert_that(config).is_empty() 

215 assert_that(config).is_instance_of(dict) 

216 assert_that(path).is_none() 

217 

218 

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 ) 

227 

228 load_mypy_config(base_dir=None) 

229 

230 mock_path.cwd.assert_called_once()