Skip to content

Commit 4e27059

Browse files
committed
Merge branch 'refactor/mock-process-tests' into refactor/shell-executor
Resolve conflicts in: - tests/test_shell_executor.py - tests/test_shell_executor_redirections.py Adopt improvements from mock-process-tests: - Enhanced test stability with mocks - Better error handling coverage - More robust async testing
2 parents 4b50b18 + 1fbd4da commit 4e27059

14 files changed

+982
-986
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ filterwarnings = [
4545
"ignore::RuntimeWarning:selectors:",
4646
"ignore::pytest.PytestUnhandledCoroutineWarning:",
4747
"ignore::pytest.PytestUnraisableExceptionWarning:",
48+
"ignore::DeprecationWarning:pytest_asyncio.plugin:",
4849
]
4950

5051
[tool.ruff]

src/mcp_shell_server/process_manager.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
import logging
55
import os
6-
from typing import IO, Any, Dict, List, Optional, Tuple
6+
from typing import IO, Any, Dict, List, Optional, Tuple, Union
77

88

99
class ProcessManager:
@@ -29,14 +29,19 @@ async def create_process(
2929
Returns:
3030
asyncio.subprocess.Process: Created process
3131
"""
32-
return await asyncio.create_subprocess_shell(
33-
shell_cmd,
34-
stdin=asyncio.subprocess.PIPE,
35-
stdout=stdout_handle,
36-
stderr=asyncio.subprocess.PIPE,
37-
env={**os.environ, **(envs or {})},
38-
cwd=directory,
39-
)
32+
try:
33+
return await asyncio.create_subprocess_shell(
34+
shell_cmd,
35+
stdin=asyncio.subprocess.PIPE,
36+
stdout=stdout_handle,
37+
stderr=asyncio.subprocess.PIPE,
38+
env={**os.environ, **(envs or {})},
39+
cwd=directory,
40+
)
41+
except OSError as e:
42+
raise ValueError(f"Failed to create process: {str(e)}") from e
43+
except Exception as e:
44+
raise ValueError(f"Unexpected error: {str(e)}") from e
4045

4146
async def execute_with_timeout(
4247
self,
@@ -62,6 +67,9 @@ async def execute_with_timeout(
6267
async def communicate_with_timeout():
6368
try:
6469
return await process.communicate(input=stdin_bytes)
70+
except asyncio.TimeoutError:
71+
process.kill() # Kill process on timeout
72+
raise
6573
except Exception as e:
6674
try:
6775
await process.wait()
@@ -77,7 +85,7 @@ async def execute_pipeline(
7785
self,
7886
commands: List[str],
7987
first_stdin: Optional[bytes] = None,
80-
last_stdout: Optional[IO[Any]] = None,
88+
last_stdout: Union[IO[Any], int, None] = None,
8189
directory: Optional[str] = None,
8290
timeout: Optional[int] = None,
8391
envs: Optional[Dict[str, str]] = None,
@@ -94,7 +102,13 @@ async def execute_pipeline(
94102
95103
Returns:
96104
Tuple[bytes, bytes, int]: Tuple of (stdout, stderr, return_code)
105+
106+
Raises:
107+
ValueError: If no commands provided or command execution fails
97108
"""
109+
if not commands:
110+
raise ValueError("No commands provided")
111+
98112
processes: List[asyncio.subprocess.Process] = []
99113
try:
100114
prev_stdout: Optional[bytes] = first_stdin
@@ -166,10 +180,20 @@ async def cleanup_processes(
166180
Args:
167181
processes: List of processes to clean up
168182
"""
183+
cleanup_tasks = []
169184
for process in processes:
170185
if process.returncode is None:
171186
try:
172187
process.kill()
173-
await process.wait()
188+
cleanup_tasks.append(asyncio.create_task(process.wait()))
174189
except Exception as e:
175190
logging.warning(f"Error cleaning up process: {e}")
191+
192+
if cleanup_tasks:
193+
try:
194+
# Set a timeout for cleanup to prevent hanging
195+
await asyncio.wait_for(asyncio.gather(*cleanup_tasks), timeout=5)
196+
except asyncio.TimeoutError:
197+
logging.warning("Process cleanup timed out")
198+
except Exception as e:
199+
logging.warning(f"Error during process cleanup: {e}")

0 commit comments

Comments
 (0)