Coverage for tests / unit / tools / executor / test_tool_executor_post_checks.py: 96%

83 statements  

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

1"""Unit tests for executor post-check behavior (e.g., Black as post-check).""" 

2 

3from __future__ import annotations 

4 

5import json 

6from collections.abc import Callable 

7from dataclasses import dataclass, field 

8from typing import TYPE_CHECKING, Any 

9 

10import pytest 

11from assertpy import assert_that 

12 

13if TYPE_CHECKING: 

14 pass 

15 

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 tool stub returning a pre-baked ToolResult.""" 

36 

37 def __init__(self, name: str, can_fix: bool, result: ToolResult) -> None: 

38 """Initialize the fake tool. 

39 

40 Args: 

41 name: Tool name. 

42 can_fix: Whether fixes are supported. 

43 result: Result object to return from check/fix. 

44 """ 

45 self.name = name 

46 self._definition = FakeToolDefinition(name=name, can_fix=can_fix) 

47 self._result = result 

48 self.options: dict[str, object] = {} 

49 

50 @property 

51 def definition(self) -> FakeToolDefinition: 

52 """Return the tool definition. 

53 

54 Returns: 

55 FakeToolDefinition containing tool metadata. 

56 """ 

57 return self._definition 

58 

59 @property 

60 def can_fix(self) -> bool: 

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

62 

63 Returns: 

64 True if the tool can fix issues. 

65 """ 

66 return self._definition.can_fix 

67 

68 def reset_options(self) -> None: 

69 """Reset options to defaults (stub for testing).""" 

70 self.options = {} 

71 

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

73 """Record option values provided to the tool stub. 

74 

75 Args: 

76 **kwargs: Arbitrary options to store for assertions. 

77 """ 

78 self.options.update(kwargs) 

79 

80 def check( 

81 self, 

82 paths: list[str], 

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

84 ) -> ToolResult: 

85 """Return the stored result for a check invocation. 

86 

87 Args: 

88 paths: Target paths (ignored in stub). 

89 options: Optional tool options. 

90 

91 Returns: 

92 ToolResult: Pre-baked result instance. 

93 """ 

94 return self._result 

95 

96 def fix( 

97 self, 

98 paths: list[str], 

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

100 ) -> ToolResult: 

101 """Return the stored result for a fix invocation. 

102 

103 Args: 

104 paths: Target paths (ignored in stub). 

105 options: Optional tool options. 

106 

107 Returns: 

108 ToolResult: Pre-baked result instance. 

109 """ 

110 return self._result 

111 

112 

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

114 import lintro.utils.console as cl 

115 

116 class SilentLogger: 

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

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

119 return None 

120 

121 return _ 

122 

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

124 

125 

126def _setup_main_tool(monkeypatch: pytest.MonkeyPatch) -> FakeTool: 

127 """Configure the main (ruff) tool and output manager stubs. 

128 

129 Args: 

130 monkeypatch: Pytest monkeypatch fixture. 

131 

132 Returns: 

133 FakeTool: Configured ruff tool stub. 

134 """ 

135 import lintro.utils.tool_executor as te 

136 

137 ok = ToolResult(name="ruff", success=True, output="", issues_count=0) 

138 ruff = FakeTool("ruff", can_fix=True, result=ok) 

139 

140 def fake_get_tools( 

141 tools: str | None, 

142 action: str, 

143 **_kwargs: object, 

144 ) -> ToolsToRunResult: 

145 return ToolsToRunResult(to_run=["ruff"]) 

146 

147 monkeypatch.setattr(te, "get_tools_to_run", fake_get_tools, raising=True) 

148 monkeypatch.setattr(tool_manager, "get_tool", lambda name: ruff, raising=True) 

149 

150 def noop_write_reports_from_results( 

151 self: object, 

152 results: list[ToolResult], 

153 ) -> None: 

154 return None 

155 

156 monkeypatch.setattr( 

157 OutputManager, 

158 "write_reports_from_results", 

159 noop_write_reports_from_results, 

160 raising=True, 

161 ) 

162 

163 return ruff 

164 

165 

166def test_post_checks_missing_tool_is_skipped_gracefully( 

167 monkeypatch: pytest.MonkeyPatch, 

168 capsys: pytest.CaptureFixture[str], 

169) -> None: 

170 """When post-check tool is unavailable, it is skipped gracefully. 

171 

172 Post-checks are optional when the tool cannot be resolved from the tool 

173 manager. The main tool (ruff) should run, and the missing post-check tool 

174 (black) should not appear in results. 

175 

176 Args: 

177 monkeypatch: Pytest fixture to modify objects during the test. 

178 capsys: Pytest fixture to capture stdout/stderr for assertions. 

179 """ 

180 _stub_logger(monkeypatch) 

181 ruff_local = _setup_main_tool(monkeypatch) 

182 

183 import lintro.utils.config as cfg 

184 import lintro.utils.post_checks as pc 

185 import lintro.utils.tool_executor as te 

186 

187 def post_check_config(): 

188 return {"enabled": True, "tools": ["black"], "enforce_failure": True} 

189 

190 # Patch in all modules that import load_post_checks_config 

191 monkeypatch.setattr(cfg, "load_post_checks_config", post_check_config, raising=True) 

192 monkeypatch.setattr(te, "load_post_checks_config", post_check_config, raising=True) 

193 monkeypatch.setattr(pc, "load_post_checks_config", post_check_config, raising=True) 

194 

195 # Fail only for the post-check tool (black); allow main ruff to run 

196 def fail_get_tool(name: str) -> FakeTool: 

197 if name == "black": 

198 raise RuntimeError("black not available") 

199 return ruff_local 

200 

201 monkeypatch.setattr(tool_manager, "get_tool", fail_get_tool, raising=True) 

202 

203 code = run_lint_tools_simple( 

204 action="check", 

205 paths=["."], 

206 tools="all", 

207 tool_options=None, 

208 exclude=None, 

209 include_venv=False, 

210 group_by="auto", 

211 output_format="json", 

212 verbose=False, 

213 raw_output=False, 

214 ) 

215 out = capsys.readouterr().out 

216 data = json.loads(out) 

217 results = data.get("results", []) 

218 tool_names = [result["tool"] for result in results] 

219 # Main tool (ruff) should run successfully 

220 assert_that("ruff" in tool_names).is_true() 

221 # Post-check (black) should be skipped, not appear in results 

222 assert_that("black" in tool_names).is_false() 

223 # Exit code should be success (0) since main tool passed and post-check was skipped 

224 assert_that(code).is_equal_to(0)