Coverage for lintro / tools / core / timeout_utils.py: 100%
26 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"""Shared timeout handling utilities for tool implementations.
3This module provides standardized timeout handling across different tools,
4ensuring consistent behavior and error messages for subprocess timeouts.
5"""
7import subprocess # nosec B404 - used safely with shell disabled
8from dataclasses import dataclass, field
9from typing import Any
11from loguru import logger
14@dataclass
15class TimeoutResult:
16 """Timeout result structure."""
18 success: bool
19 output: str
20 issues_count: int
21 issues: list[Any] = field(default_factory=list)
22 timed_out: bool = True
23 timeout_seconds: int = 0
26def run_subprocess_with_timeout(
27 tool: Any,
28 cmd: list[str],
29 timeout: int | None = None,
30 cwd: str | None = None,
31 tool_name: str | None = None,
32) -> tuple[bool, str]:
33 """Run a subprocess command with timeout handling.
35 This is a wrapper around tool._run_subprocess that provides consistent
36 timeout error handling and messaging across different tools.
38 Args:
39 tool: Tool instance with _run_subprocess method.
40 cmd: Command to run.
41 timeout: Timeout in seconds. If None, uses tool's default timeout.
42 cwd: Working directory for command execution.
43 tool_name: Name of the tool for error messages. If None, uses tool.name.
45 Returns:
46 tuple[bool, str]: (success, output) where success is True if command
47 succeeded without timeout, and output contains command output or
48 timeout error message.
50 Raises:
51 subprocess.TimeoutExpired: If command times out (re-raised with context).
52 """
53 tool_name = tool_name or tool.definition.name
55 try:
56 success, output = tool._run_subprocess(cmd=cmd, timeout=timeout, cwd=cwd)
57 return bool(success), str(output)
58 except subprocess.TimeoutExpired as e:
59 # Re-raise with more context for the calling tool
60 actual_timeout = timeout or tool.options.get("timeout", tool._default_timeout)
61 timeout_msg = (
62 f"{tool_name} execution timed out ({actual_timeout}s limit exceeded).\n\n"
63 "This may indicate:\n"
64 " - Large codebase taking too long to process\n"
65 " - Need to increase timeout via --tool-options timeout=N\n"
66 " - Command hanging due to external dependencies\n"
67 )
68 logger.warning(timeout_msg)
70 # Create a new TimeoutExpired with enhanced message
71 raise subprocess.TimeoutExpired(
72 cmd=cmd,
73 timeout=actual_timeout,
74 output=timeout_msg,
75 ) from e
78def get_timeout_value(tool: Any, default_timeout: int | None = None) -> int:
79 """Get timeout value from tool options with fallback to default.
81 Args:
82 tool: Tool instance with options.
83 default_timeout: Default timeout if not specified in options.
85 Returns:
86 int: Timeout value in seconds.
87 """
88 if default_timeout is None:
89 default_timeout = getattr(tool, "_default_timeout", 300)
91 return int(tool.options.get("timeout", default_timeout))
94def create_timeout_result(
95 tool: Any,
96 timeout: int,
97 cmd: list[str] | None = None,
98 tool_name: str | None = None,
99) -> TimeoutResult:
100 """Create a standardized timeout result.
102 Args:
103 tool: Tool instance.
104 timeout: Timeout value that was exceeded.
105 cmd: Optional command that timed out.
106 tool_name: Optional tool name override.
108 Returns:
109 TimeoutResult: Result dataclass with timeout information.
110 """
111 tool_name = tool_name or tool.definition.name
113 return TimeoutResult(
114 success=False,
115 output=(
116 f"{tool_name} execution timed out ({timeout}s limit exceeded).\n\n"
117 "This may indicate:\n"
118 " - Large codebase taking too long to process\n"
119 " - Need to increase timeout via --tool-options timeout=N\n"
120 " - Command hanging due to external dependencies\n"
121 ),
122 issues_count=1, # Count timeout as execution failure
123 issues=[],
124 timed_out=True,
125 timeout_seconds=timeout,
126 )