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
« 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)."""
3from __future__ import annotations
5import json
6from collections.abc import Callable
7from dataclasses import dataclass, field
8from typing import TYPE_CHECKING, Any
10import pytest
11from assertpy import assert_that
13if TYPE_CHECKING:
14 pass
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
23@dataclass
24class FakeToolDefinition:
25 """Fake ToolDefinition for testing."""
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)
34class FakeTool:
35 """Simple tool stub returning a pre-baked ToolResult."""
37 def __init__(self, name: str, can_fix: bool, result: ToolResult) -> None:
38 """Initialize the fake tool.
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] = {}
50 @property
51 def definition(self) -> FakeToolDefinition:
52 """Return the tool definition.
54 Returns:
55 FakeToolDefinition containing tool metadata.
56 """
57 return self._definition
59 @property
60 def can_fix(self) -> bool:
61 """Return whether the tool can fix issues.
63 Returns:
64 True if the tool can fix issues.
65 """
66 return self._definition.can_fix
68 def reset_options(self) -> None:
69 """Reset options to defaults (stub for testing)."""
70 self.options = {}
72 def set_options(self, **kwargs: Any) -> None:
73 """Record option values provided to the tool stub.
75 Args:
76 **kwargs: Arbitrary options to store for assertions.
77 """
78 self.options.update(kwargs)
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.
87 Args:
88 paths: Target paths (ignored in stub).
89 options: Optional tool options.
91 Returns:
92 ToolResult: Pre-baked result instance.
93 """
94 return self._result
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.
103 Args:
104 paths: Target paths (ignored in stub).
105 options: Optional tool options.
107 Returns:
108 ToolResult: Pre-baked result instance.
109 """
110 return self._result
113def _stub_logger(monkeypatch: pytest.MonkeyPatch) -> None:
114 import lintro.utils.console as cl
116 class SilentLogger:
117 def __getattr__(self, name: str) -> Callable[..., None]:
118 def _(*a: Any, **k: Any) -> None:
119 return None
121 return _
123 monkeypatch.setattr(cl, "create_logger", lambda *_a, **_k: SilentLogger())
126def _setup_main_tool(monkeypatch: pytest.MonkeyPatch) -> FakeTool:
127 """Configure the main (ruff) tool and output manager stubs.
129 Args:
130 monkeypatch: Pytest monkeypatch fixture.
132 Returns:
133 FakeTool: Configured ruff tool stub.
134 """
135 import lintro.utils.tool_executor as te
137 ok = ToolResult(name="ruff", success=True, output="", issues_count=0)
138 ruff = FakeTool("ruff", can_fix=True, result=ok)
140 def fake_get_tools(
141 tools: str | None,
142 action: str,
143 **_kwargs: object,
144 ) -> ToolsToRunResult:
145 return ToolsToRunResult(to_run=["ruff"])
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)
150 def noop_write_reports_from_results(
151 self: object,
152 results: list[ToolResult],
153 ) -> None:
154 return None
156 monkeypatch.setattr(
157 OutputManager,
158 "write_reports_from_results",
159 noop_write_reports_from_results,
160 raising=True,
161 )
163 return ruff
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.
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.
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)
183 import lintro.utils.config as cfg
184 import lintro.utils.post_checks as pc
185 import lintro.utils.tool_executor as te
187 def post_check_config():
188 return {"enabled": True, "tools": ["black"], "enforce_failure": True}
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)
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
201 monkeypatch.setattr(tool_manager, "get_tool", fail_get_tool, raising=True)
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)