|
1 | 1 | # tests/test_process_manager_macos.py
|
2 |
| -import pytest |
| 2 | +import asyncio |
3 | 3 | import platform
|
4 |
| -import signal |
5 |
| -import time |
6 |
| -import os |
7 | 4 | import subprocess
|
8 |
| -from typing import Generator |
| 5 | + |
| 6 | +import pytest |
9 | 7 |
|
10 | 8 | pytestmark = [
|
11 | 9 | pytest.mark.skipif(
|
12 |
| - platform.system() != "Darwin", |
13 |
| - reason="These tests only run on macOS" |
| 10 | + platform.system() != "Darwin", reason="These tests only run on macOS" |
14 | 11 | ),
|
15 | 12 | pytest.mark.macos,
|
16 |
| - pytest.mark.slow |
| 13 | + pytest.mark.slow, |
17 | 14 | ]
|
18 | 15 |
|
| 16 | + |
19 | 17 | @pytest.fixture
|
20 | 18 | def process_manager():
|
21 | 19 | from mcp_shell_server.process_manager import ProcessManager
|
| 20 | + |
22 | 21 | pm = ProcessManager()
|
23 |
| - yield pm |
24 |
| - pm.cleanup_all() |
| 22 | + try: |
| 23 | + yield pm |
| 24 | + finally: |
| 25 | + asyncio.run(pm.cleanup_all()) |
| 26 | + |
25 | 27 |
|
26 | 28 | def get_process_status(pid: int) -> str:
|
27 | 29 | """Get process status using ps command."""
|
28 | 30 | try:
|
29 | 31 | ps = subprocess.run(
|
30 |
| - ['ps', '-o', 'stat=', '-p', str(pid)], |
31 |
| - capture_output=True, |
32 |
| - text=True |
| 32 | + ["ps", "-o", "stat=", "-p", str(pid)], capture_output=True, text=True |
33 | 33 | )
|
34 | 34 | return ps.stdout.strip()
|
35 | 35 | except subprocess.CalledProcessError:
|
36 | 36 | return ""
|
37 | 37 |
|
38 |
| -def test_zombie_process_cleanup(process_manager): |
| 38 | + |
| 39 | +@pytest.mark.asyncio |
| 40 | +async def test_zombie_process_cleanup(process_manager): |
39 | 41 | """Test that background processes don't become zombies."""
|
40 | 42 | cmd = ["sh", "-c", "sleep 0.5 & wait"]
|
41 |
| - process = process_manager.start_process(cmd) |
42 |
| - |
| 43 | + process = await process_manager.start_process(cmd) |
| 44 | + |
43 | 45 | # Wait for the background process to finish
|
44 |
| - time.sleep(1) |
45 |
| - |
| 46 | + await asyncio.sleep(1) |
| 47 | + |
46 | 48 | # Get process status
|
47 | 49 | status = get_process_status(process.pid)
|
48 |
| - |
| 50 | + |
49 | 51 | # Verify process is either gone or not zombie (Z state)
|
50 |
| - assert 'Z' not in status, f"Process {process.pid} is zombie (status: {status})" |
| 52 | + assert "Z" not in status, f"Process {process.pid} is zombie (status: {status})" |
51 | 53 |
|
52 |
| -def test_process_timeout(process_manager): |
| 54 | + |
| 55 | +@pytest.mark.asyncio |
| 56 | +async def test_process_timeout(process_manager): |
53 | 57 | """Test process timeout functionality."""
|
54 | 58 | # Start a process that should timeout
|
55 | 59 | cmd = ["sleep", "10"]
|
56 |
| - process = process_manager.start_process(cmd, timeout=1) |
57 |
| - |
58 |
| - # Wait slightly longer than the timeout |
59 |
| - time.sleep(1.5) |
60 |
| - |
61 |
| - # Verify process was terminated |
62 |
| - assert not process.is_running() |
63 |
| - assert process.returncode is not None |
64 |
| - |
65 |
| -def test_multiple_process_cleanup(process_manager): |
| 60 | + process = await process_manager.start_process(cmd) |
| 61 | + |
| 62 | + try: |
| 63 | + # Communicate with timeout |
| 64 | + with pytest.raises(TimeoutError): |
| 65 | + _, _ = await process_manager.execute_with_timeout(process, timeout=1) |
| 66 | + |
| 67 | + # プロセスが終了するまで待つ |
| 68 | + try: |
| 69 | + await asyncio.wait_for(process.wait(), timeout=1.0) |
| 70 | + except asyncio.TimeoutError: |
| 71 | + process.kill() # Force kill |
| 72 | + |
| 73 | + # Wait for termination |
| 74 | + await asyncio.wait_for(process.wait(), timeout=0.5) |
| 75 | + |
| 76 | + # Verify process was terminated |
| 77 | + assert process.returncode is not None |
| 78 | + assert not process.is_running() |
| 79 | + finally: |
| 80 | + if process.returncode is None: |
| 81 | + try: |
| 82 | + process.kill() |
| 83 | + await asyncio.wait_for(process.wait(), timeout=0.5) |
| 84 | + except (ProcessLookupError, asyncio.TimeoutError): |
| 85 | + pass |
| 86 | + |
| 87 | + |
| 88 | +@pytest.mark.asyncio |
| 89 | +async def test_multiple_process_cleanup(process_manager): |
66 | 90 | """Test cleanup of multiple processes."""
|
67 | 91 | # Start multiple background processes
|
68 |
| - processes = [ |
69 |
| - process_manager.start_process(["sleep", "2"]) |
70 |
| - for _ in range(3) |
71 |
| - ] |
72 |
| - |
| 92 | + # Start multiple processes in parallel |
| 93 | + processes = await asyncio.gather( |
| 94 | + *[process_manager.start_process(["sleep", "2"]) for _ in range(3)] |
| 95 | + ) |
| 96 | + |
73 | 97 | # Give them a moment to start
|
74 |
| - time.sleep(0.1) |
75 |
| - |
76 |
| - # Verify they're all running |
77 |
| - assert all(p.is_running() for p in processes) |
78 |
| - |
79 |
| - # Cleanup |
80 |
| - process_manager.cleanup_all() |
81 |
| - |
82 |
| - # Give cleanup a moment to complete |
83 |
| - time.sleep(0.1) |
84 |
| - |
85 |
| - # Verify all processes are gone |
86 |
| - for p in processes: |
87 |
| - status = get_process_status(p.pid) |
88 |
| - assert status == "", f"Process {p.pid} still exists with status: {status}" |
89 |
| - |
90 |
| -def test_process_group_termination(process_manager): |
| 98 | + await asyncio.sleep(0.1) |
| 99 | + |
| 100 | + try: |
| 101 | + # Verify they're all running |
| 102 | + assert all(p.is_running() for p in processes) |
| 103 | + |
| 104 | + # Cleanup |
| 105 | + await process_manager.cleanup_all() |
| 106 | + |
| 107 | + # Give cleanup a moment to complete |
| 108 | + await asyncio.sleep(0.1) |
| 109 | + |
| 110 | + # Verify all processes are gone |
| 111 | + for p in processes: |
| 112 | + status = get_process_status(p.pid) |
| 113 | + assert status == "", f"Process {p.pid} still exists with status: {status}" |
| 114 | + finally: |
| 115 | + # Ensure cleanup in case of test failure |
| 116 | + for p in processes: |
| 117 | + if p.returncode is None: |
| 118 | + try: |
| 119 | + p.kill() |
| 120 | + except ProcessLookupError: |
| 121 | + pass |
| 122 | + |
| 123 | + |
| 124 | +@pytest.mark.asyncio |
| 125 | +async def test_process_group_termination(process_manager): |
91 | 126 | """Test that entire process group is terminated."""
|
92 | 127 | # Create a process that spawns children
|
93 | 128 | cmd = ["sh", "-c", "sleep 10 & sleep 10 & sleep 10 & wait"]
|
94 |
| - process = process_manager.start_process(cmd) |
95 |
| - |
96 |
| - # Give processes time to start |
97 |
| - time.sleep(0.5) |
98 |
| - |
99 |
| - # Kill the main process |
100 |
| - process.kill() |
101 |
| - |
102 |
| - # Wait a moment for cleanup |
103 |
| - time.sleep(0.5) |
104 |
| - |
105 |
| - # Check if any processes from the group remain |
106 |
| - ps = subprocess.run( |
107 |
| - ["pgrep", "-g", str(process.pid)], |
108 |
| - capture_output=True |
109 |
| - ) |
110 |
| - assert ps.returncode != 0, "Process group still exists" |
| 129 | + process = await process_manager.start_process(cmd) |
| 130 | + |
| 131 | + try: |
| 132 | + # Give processes time to start |
| 133 | + await asyncio.sleep(0.5) |
| 134 | + |
| 135 | + # Kill the main process |
| 136 | + process.kill() |
| 137 | + |
| 138 | + # Wait a moment for cleanup |
| 139 | + await asyncio.sleep(0.5) |
| 140 | + |
| 141 | + # Check if any processes from the group remain |
| 142 | + ps = subprocess.run(["pgrep", "-g", str(process.pid)], capture_output=True) |
| 143 | + assert ps.returncode != 0, "Process group still exists" |
| 144 | + finally: |
| 145 | + if process.returncode is None: |
| 146 | + try: |
| 147 | + process.kill() |
| 148 | + except ProcessLookupError: |
| 149 | + pass |
0 commit comments