Coverage for tests / unit / utils / async_tool_executor / test_run_tools_parallel.py: 100%
57 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"""Tests for AsyncToolExecutor.run_tools_parallel method."""
3from __future__ import annotations
5import asyncio
6from typing import Any, cast
8from assertpy import assert_that
10from lintro.enums.action import Action
11from lintro.models.core.tool_result import ToolResult
12from lintro.plugins.base import BaseToolPlugin
13from lintro.utils.async_tool_executor import AsyncToolExecutor
15from .conftest import MockToolDefinition, MockToolPlugin
18def test_run_tools_parallel_success(executor: AsyncToolExecutor) -> None:
19 """Test parallel execution of multiple tools.
21 Args:
22 executor: AsyncToolExecutor fixture.
23 """
24 # MockToolPlugin implements the same interface as BaseToolPlugin for testing
25 tools = cast(
26 list[tuple[str, BaseToolPlugin]],
27 [
28 ("tool1", MockToolPlugin(definition=MockToolDefinition(name="tool1"))),
29 ("tool2", MockToolPlugin(definition=MockToolDefinition(name="tool2"))),
30 ("tool3", MockToolPlugin(definition=MockToolDefinition(name="tool3"))),
31 ],
32 )
34 async def run_test() -> Any:
35 return await executor.run_tools_parallel(
36 tools,
37 paths=["."],
38 action=Action.CHECK,
39 )
41 results = asyncio.run(run_test())
43 assert_that(results).is_length(3)
44 for _name, result in results:
45 assert_that(result.success).is_true()
48def test_run_tools_parallel_with_failures(executor: AsyncToolExecutor) -> None:
49 """Test parallel execution handles partial failures.
51 Args:
52 executor: AsyncToolExecutor fixture.
53 """
54 # MockToolPlugin implements the same interface as BaseToolPlugin for testing
55 tools = cast(
56 list[tuple[str, BaseToolPlugin]],
57 [
58 (
59 "success_tool",
60 MockToolPlugin(definition=MockToolDefinition(name="success_tool")),
61 ),
62 (
63 "failing_tool",
64 MockToolPlugin(
65 definition=MockToolDefinition(name="failing_tool"),
66 check_result=ToolResult(
67 name="failing_tool",
68 success=False,
69 output="Failed",
70 issues_count=3,
71 ),
72 ),
73 ),
74 ],
75 )
77 async def run_test() -> Any:
78 return await executor.run_tools_parallel(
79 tools,
80 paths=["."],
81 action=Action.CHECK,
82 )
84 results = asyncio.run(run_test())
86 assert_that(results).is_length(2)
87 result_dict = dict(results)
88 assert_that(result_dict["success_tool"].success).is_true()
89 assert_that(result_dict["failing_tool"].success).is_false()
92def test_run_tools_parallel_empty_list(executor: AsyncToolExecutor) -> None:
93 """Test parallel execution with empty tools list.
95 Args:
96 executor: AsyncToolExecutor fixture.
97 """
99 async def run_test() -> Any:
100 return await executor.run_tools_parallel(
101 tools=[],
102 paths=["."],
103 action=Action.CHECK,
104 )
106 results = asyncio.run(run_test())
108 assert_that(results).is_empty()
111def test_run_tools_parallel_single_tool(executor: AsyncToolExecutor) -> None:
112 """Test parallel execution with single tool.
114 Args:
115 executor: AsyncToolExecutor fixture.
116 """
117 # MockToolPlugin implements the same interface as BaseToolPlugin for testing
118 tools = cast(
119 list[tuple[str, BaseToolPlugin]],
120 [
121 ("single", MockToolPlugin(definition=MockToolDefinition(name="single"))),
122 ],
123 )
125 async def run_test() -> Any:
126 return await executor.run_tools_parallel(
127 tools,
128 paths=["."],
129 action=Action.CHECK,
130 )
132 results = asyncio.run(run_test())
134 assert_that(results).is_length(1)
135 assert_that(results[0][0]).is_equal_to("single")
138def test_run_tools_parallel_with_options_per_tool(
139 executor: AsyncToolExecutor,
140) -> None:
141 """Test parallel execution passes correct options to each tool.
143 Args:
144 executor: AsyncToolExecutor fixture.
145 """
146 tool1_opts: dict[str, Any] = {}
147 tool2_opts: dict[str, Any] = {}
149 def check1(
150 paths: list[str],
151 options: dict[str, Any] | None = None,
152 ) -> ToolResult:
153 nonlocal tool1_opts
154 tool1_opts = options or {}
155 return ToolResult(name="tool1", success=True, output="", issues_count=0)
157 def check2(
158 paths: list[str],
159 options: dict[str, Any] | None = None,
160 ) -> ToolResult:
161 nonlocal tool2_opts
162 tool2_opts = options or {}
163 return ToolResult(name="tool2", success=True, output="", issues_count=0)
165 tool1 = MockToolPlugin(definition=MockToolDefinition(name="tool1"))
166 # Use object.__setattr__ to bypass dataclass method assignment restriction
167 object.__setattr__(tool1, "check", check1)
168 tool2 = MockToolPlugin(definition=MockToolDefinition(name="tool2"))
169 object.__setattr__(tool2, "check", check2)
171 # MockToolPlugin implements the same interface as BaseToolPlugin for testing
172 async def run_test() -> Any:
173 return await executor.run_tools_parallel(
174 tools=cast(
175 list[tuple[str, BaseToolPlugin]],
176 [("tool1", tool1), ("tool2", tool2)],
177 ),
178 paths=["."],
179 action=Action.CHECK,
180 options_per_tool={
181 "tool1": {"option_a": "value_a"},
182 "tool2": {"option_b": "value_b"},
183 },
184 )
186 asyncio.run(run_test())
188 assert_that(tool1_opts).contains_key("option_a")
189 assert_that(tool2_opts).contains_key("option_b")
190 assert_that(tool1_opts).does_not_contain_key("option_b")