Coverage for tests / unit / compatibility / test_compatibility_ruff_black.py: 99%

78 statements  

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

1"""Compatibility tests ensuring Ruff/Black policy interactions.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Callable 

6from dataclasses import dataclass, field 

7from typing import TYPE_CHECKING, Any 

8 

9import pytest 

10from assertpy import assert_that 

11 

12if TYPE_CHECKING: 

13 pass 

14 

15from lintro.enums.tool_name import ToolName 

16from lintro.models.core.tool_result import ToolResult 

17from lintro.tools import tool_manager 

18from lintro.utils.execution.tool_configuration import ToolsToRunResult 

19from lintro.utils.output import OutputManager 

20from lintro.utils.tool_executor import run_lint_tools_simple 

21 

22 

23@dataclass 

24class FakeToolDefinition: 

25 """Fake ToolDefinition for testing.""" 

26 

27 name: str 

28 can_fix: bool = False 

29 description: str = "" 

30 file_patterns: list[str] = field(default_factory=list) 

31 native_configs: list[str] = field(default_factory=list) 

32 

33 

34class FakeTool: 

35 """Simple stub representing a tool with check/fix capability.""" 

36 

37 def __init__(self, name: ToolName, can_fix: bool) -> None: 

38 """Initialize stub tool. 

39 

40 Args: 

41 name: Tool name. 

42 can_fix: Whether the tool can apply fixes. 

43 """ 

44 self.name = name 

45 self._definition = FakeToolDefinition(name=str(name), can_fix=can_fix) 

46 self.options: dict[str, Any] = {} 

47 

48 @property 

49 def definition(self) -> FakeToolDefinition: 

50 """Return the tool definition. 

51 

52 Returns: 

53 FakeToolDefinition containing tool metadata. 

54 """ 

55 return self._definition 

56 

57 @property 

58 def can_fix(self) -> bool: 

59 """Return whether the tool can fix issues. 

60 

61 Returns: 

62 True if the tool can fix issues. 

63 """ 

64 return self._definition.can_fix 

65 

66 def set_options(self, **kwargs: Any) -> None: 

67 """Record provided options for later assertions. 

68 

69 Args: 

70 **kwargs: Arbitrary option key-value pairs forwarded by the runner. 

71 """ 

72 self.options.update(kwargs) 

73 

74 def reset_options(self) -> None: 

75 """Reset options to defaults (no-op for stub).""" 

76 self.options.clear() 

77 

78 def check( 

79 self, 

80 paths: list[str], 

81 options: dict[str, Any] | None = None, 

82 ) -> ToolResult: 

83 """Return a successful empty result for lint checks. 

84 

85 Args: 

86 paths: Target file or directory paths to check. 

87 options: Optional tool options. 

88 

89 Returns: 

90 ToolResult indicating success with zero issues. 

91 """ 

92 return ToolResult(name=self.name, success=True, output="", issues_count=0) 

93 

94 def fix( 

95 self, 

96 paths: list[str], 

97 options: dict[str, Any] | None = None, 

98 ) -> ToolResult: 

99 """Return a successful empty result for fixes. 

100 

101 Args: 

102 paths: Target file or directory paths to fix. 

103 options: Optional tool options. 

104 

105 Returns: 

106 ToolResult indicating success with zero issues. 

107 """ 

108 return ToolResult(name=self.name, success=True, output="", issues_count=0) 

109 

110 

111def _stub_logger(monkeypatch: pytest.MonkeyPatch) -> None: 

112 """Silence console logger for deterministic tests. 

113 

114 Args: 

115 monkeypatch: Pytest monkeypatch fixture for patching objects. 

116 """ 

117 import lintro.utils.console as cl 

118 

119 class SilentLogger: 

120 def __getattr__(self, name: str) -> Callable[..., None]: 

121 def _(*a: Any, **k: Any) -> None: 

122 return None 

123 

124 return _ 

125 

126 monkeypatch.setattr(cl, "create_logger", lambda *_a, **_k: SilentLogger()) 

127 

128 

129def _setup_tools(monkeypatch: pytest.MonkeyPatch) -> tuple[FakeTool, FakeTool]: 

130 """Prepare stubbed tool manager and output manager plumbing. 

131 

132 Args: 

133 monkeypatch: Pytest monkeypatch fixture for patching objects. 

134 

135 Returns: 

136 Tuple of stubbed Ruff and Black tool instances. 

137 """ 

138 import lintro.utils.tool_executor as te 

139 

140 ruff = FakeTool(ToolName.RUFF, can_fix=True) 

141 black = FakeTool(ToolName.BLACK, can_fix=True) 

142 tool_map = {ToolName.RUFF: ruff, ToolName.BLACK: black} 

143 

144 def fake_get_tools( 

145 _tools: str | None, 

146 _action: str, 

147 *, 

148 ignore_conflicts: bool = False, # noqa: ARG001 — must match caller kwarg name 

149 ) -> ToolsToRunResult: 

150 """Return tool names for ruff and black in order. 

151 

152 Args: 

153 _tools: Optional tool selection string (ignored in tests). 

154 _action: Runner action being executed (ignored in tests). 

155 ignore_conflicts: Whether to ignore tool conflicts (ignored in tests). 

156 

157 Returns: 

158 ToolsToRunResult with ruff and black tools. 

159 """ 

160 return ToolsToRunResult(to_run=[ToolName.RUFF, ToolName.BLACK]) 

161 

162 # Patch get_tools_to_run in the tool_executor module where it's imported 

163 monkeypatch.setattr(te, "get_tools_to_run", fake_get_tools) 

164 monkeypatch.setattr( 

165 tool_manager, 

166 "get_tool", 

167 lambda name: tool_map[ToolName(name.lower())], 

168 ) 

169 

170 def noop_write_reports_from_results( 

171 self: object, 

172 results: list[ToolResult], 

173 ) -> None: 

174 """No-op writer used to avoid filesystem interaction. 

175 

176 Args: 

177 self: Output manager instance under test. 

178 results: Aggregated tool results to write. 

179 

180 Returns: 

181 None. 

182 """ 

183 return None 

184 

185 monkeypatch.setattr( 

186 OutputManager, 

187 "write_reports_from_results", 

188 noop_write_reports_from_results, 

189 ) 

190 

191 return ruff, black 

192 

193 

194def test_ruff_formatting_disabled_when_black_present( 

195 monkeypatch: pytest.MonkeyPatch, 

196) -> None: 

197 """Black present: Ruff formatting should be disabled by default. 

198 

199 Args: 

200 monkeypatch: Pytest monkeypatch fixture for patching objects. 

201 """ 

202 _stub_logger(monkeypatch) 

203 ruff, black = _setup_tools(monkeypatch) 

204 

205 code = run_lint_tools_simple( 

206 action="fmt", 

207 paths=["."], 

208 tools="all", 

209 tool_options=None, 

210 exclude=None, 

211 include_venv=False, 

212 group_by="auto", 

213 output_format="grid", 

214 verbose=False, 

215 raw_output=False, 

216 ) 

217 

218 assert_that(code).is_equal_to(0) 

219 assert_that(ruff.options.get("format")).is_false() 

220 

221 

222def test_ruff_formatting_respects_cli_override( 

223 monkeypatch: pytest.MonkeyPatch, 

224) -> None: 

225 """CLI options should re-enable Ruff format and format_check. 

226 

227 Args: 

228 monkeypatch: Pytest monkeypatch fixture for patching objects. 

229 """ 

230 _stub_logger(monkeypatch) 

231 ruff, black = _setup_tools(monkeypatch) 

232 

233 code = run_lint_tools_simple( 

234 action="fmt", 

235 paths=["."], 

236 tools="all", 

237 tool_options="ruff:format=True,ruff:format_check=True", 

238 exclude=None, 

239 include_venv=False, 

240 group_by="auto", 

241 output_format="grid", 

242 verbose=False, 

243 raw_output=False, 

244 ) 

245 

246 assert_that(code).is_equal_to(0) 

247 assert_that(ruff.options.get("format")).is_true() 

248 assert_that(ruff.options.get("format_check")).is_true() 

249 

250 

251def test_ruff_format_check_disabled_in_check_when_black_present( 

252 monkeypatch: pytest.MonkeyPatch, 

253) -> None: 

254 """Black present: Ruff format_check should be disabled in check. 

255 

256 Args: 

257 monkeypatch: Pytest monkeypatch fixture for patching objects. 

258 """ 

259 _stub_logger(monkeypatch) 

260 ruff, black = _setup_tools(monkeypatch) 

261 

262 code = run_lint_tools_simple( 

263 action="check", 

264 paths=["."], 

265 tools="all", 

266 tool_options=None, 

267 exclude=None, 

268 include_venv=False, 

269 group_by="auto", 

270 output_format="grid", 

271 verbose=False, 

272 raw_output=False, 

273 ) 

274 

275 assert_that(code).is_equal_to(0) 

276 assert_that(ruff.options.get("format_check")).is_false()