Skip to content

Commit 3e86edf

Browse files
maxisbeyclaude
andauthored
fix: Replace arbitrary sleeps with active server readiness checks in tests (#1527)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f97f7c4 commit 3e86edf

File tree

7 files changed

+45
-98
lines changed

7 files changed

+45
-98
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ venv = ".venv"
9898
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
9999
# and refactor code that is not public.
100100
executionEnvironments = [
101-
{ root = "tests", reportUnusedFunction = false, reportPrivateUsage = false },
101+
{ root = "tests", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false },
102102
{ root = "examples/servers", reportUnusedFunction = false },
103103
]
104104

tests/server/test_sse_security.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import logging
44
import multiprocessing
55
import socket
6-
import time
76

87
import httpx
98
import pytest
@@ -17,6 +16,7 @@
1716
from mcp.server.sse import SseServerTransport
1817
from mcp.server.transport_security import TransportSecuritySettings
1918
from mcp.types import Tool
19+
from tests.test_helpers import wait_for_server
2020

2121
logger = logging.getLogger(__name__)
2222
SERVER_NAME = "test_sse_security_server"
@@ -66,26 +66,6 @@ async def handle_sse(request: Request):
6666
uvicorn.run(starlette_app, host="127.0.0.1", port=port, log_level="error")
6767

6868

69-
def wait_for_server(port: int, timeout: float = 5.0) -> None:
70-
"""Wait for server to be ready to accept connections.
71-
72-
Polls the server port until it accepts connections or timeout is reached.
73-
This eliminates race conditions without arbitrary sleeps.
74-
"""
75-
start_time = time.time()
76-
while time.time() - start_time < timeout:
77-
try:
78-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
79-
s.settimeout(0.1)
80-
s.connect(("127.0.0.1", port))
81-
# Server is ready
82-
return
83-
except (ConnectionRefusedError, OSError):
84-
# Server not ready yet, retry quickly
85-
time.sleep(0.01)
86-
raise TimeoutError(f"Server on port {port} did not start within {timeout} seconds")
87-
88-
8969
def start_server_process(port: int, security_settings: TransportSecuritySettings | None = None):
9070
"""Start server in a separate process."""
9171
process = multiprocessing.Process(target=run_server_with_settings, args=(port, security_settings))

tests/server/test_streamable_http_security.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import logging
44
import multiprocessing
55
import socket
6-
import time
76
from collections.abc import AsyncGenerator
87
from contextlib import asynccontextmanager
98

@@ -18,6 +17,7 @@
1817
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
1918
from mcp.server.transport_security import TransportSecuritySettings
2019
from mcp.types import Tool
20+
from tests.test_helpers import wait_for_server
2121

2222
logger = logging.getLogger(__name__)
2323
SERVER_NAME = "test_streamable_http_security_server"
@@ -77,8 +77,8 @@ def start_server_process(port: int, security_settings: TransportSecuritySettings
7777
"""Start server in a separate process."""
7878
process = multiprocessing.Process(target=run_server_with_settings, args=(port, security_settings))
7979
process.start()
80-
# Give server time to start
81-
time.sleep(1)
80+
# Wait for server to be ready to accept connections
81+
wait_for_server(port)
8282
return process
8383

8484

tests/shared/test_sse.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
TextResourceContents,
3333
Tool,
3434
)
35+
from tests.test_helpers import wait_for_server
3536

3637
SERVER_NAME = "test_server_for_SSE"
3738

@@ -123,19 +124,8 @@ def server(server_port: int) -> Generator[None, None, None]:
123124
proc.start()
124125

125126
# Wait for server to be running
126-
max_attempts = 20
127-
attempt = 0
128127
print("waiting for server to start")
129-
while attempt < max_attempts:
130-
try:
131-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
132-
s.connect(("127.0.0.1", server_port))
133-
break
134-
except ConnectionRefusedError:
135-
time.sleep(0.1)
136-
attempt += 1
137-
else:
138-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
128+
wait_for_server(server_port)
139129

140130
yield
141131

tests/shared/test_streamable_http.py

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import json
88
import multiprocessing
99
import socket
10-
import time
1110
from collections.abc import Generator
1211
from typing import Any
1312

@@ -43,6 +42,7 @@
4342
from mcp.shared.message import ClientMessageMetadata
4443
from mcp.shared.session import RequestResponder
4544
from mcp.types import InitializeResult, TextContent, TextResourceContents, Tool
45+
from tests.test_helpers import wait_for_server
4646

4747
# Test constants
4848
SERVER_NAME = "test_streamable_http_server"
@@ -344,18 +344,7 @@ def basic_server(basic_server_port: int) -> Generator[None, None, None]:
344344
proc.start()
345345

346346
# Wait for server to be running
347-
max_attempts = 20
348-
attempt = 0
349-
while attempt < max_attempts:
350-
try:
351-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
352-
s.connect(("127.0.0.1", basic_server_port))
353-
break
354-
except ConnectionRefusedError:
355-
time.sleep(0.1)
356-
attempt += 1
357-
else:
358-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
347+
wait_for_server(basic_server_port)
359348

360349
yield
361350

@@ -391,18 +380,7 @@ def event_server(
391380
proc.start()
392381

393382
# Wait for server to be running
394-
max_attempts = 20
395-
attempt = 0
396-
while attempt < max_attempts:
397-
try:
398-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
399-
s.connect(("127.0.0.1", event_server_port))
400-
break
401-
except ConnectionRefusedError:
402-
time.sleep(0.1)
403-
attempt += 1
404-
else:
405-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
383+
wait_for_server(event_server_port)
406384

407385
yield event_store, f"http://127.0.0.1:{event_server_port}"
408386

@@ -422,18 +400,7 @@ def json_response_server(json_server_port: int) -> Generator[None, None, None]:
422400
proc.start()
423401

424402
# Wait for server to be running
425-
max_attempts = 20
426-
attempt = 0
427-
while attempt < max_attempts:
428-
try:
429-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
430-
s.connect(("127.0.0.1", json_server_port))
431-
break
432-
except ConnectionRefusedError:
433-
time.sleep(0.1)
434-
attempt += 1
435-
else:
436-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
403+
wait_for_server(json_server_port)
437404

438405
yield
439406

@@ -1407,18 +1374,7 @@ def context_aware_server(basic_server_port: int) -> Generator[None, None, None]:
14071374
proc.start()
14081375

14091376
# Wait for server to be running
1410-
max_attempts = 20
1411-
attempt = 0
1412-
while attempt < max_attempts:
1413-
try:
1414-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1415-
s.connect(("127.0.0.1", basic_server_port))
1416-
break
1417-
except ConnectionRefusedError:
1418-
time.sleep(0.1)
1419-
attempt += 1
1420-
else:
1421-
raise RuntimeError(f"Context-aware server failed to start after {max_attempts} attempts")
1377+
wait_for_server(basic_server_port)
14221378

14231379
yield
14241380

tests/shared/test_ws.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
TextResourceContents,
2727
Tool,
2828
)
29+
from tests.test_helpers import wait_for_server
2930

3031
SERVER_NAME = "test_server_for_WS"
3132

@@ -110,19 +111,8 @@ def server(server_port: int) -> Generator[None, None, None]:
110111
proc.start()
111112

112113
# Wait for server to be running
113-
max_attempts = 20
114-
attempt = 0
115114
print("waiting for server to start")
116-
while attempt < max_attempts:
117-
try:
118-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
119-
s.connect(("127.0.0.1", server_port))
120-
break
121-
except ConnectionRefusedError:
122-
time.sleep(0.1)
123-
attempt += 1
124-
else:
125-
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
115+
wait_for_server(server_port)
126116

127117
yield
128118

tests/test_helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Common test utilities for MCP server tests."""
2+
3+
import socket
4+
import time
5+
6+
7+
def wait_for_server(port: int, timeout: float = 5.0) -> None:
8+
"""Wait for server to be ready to accept connections.
9+
10+
Polls the server port until it accepts connections or timeout is reached.
11+
This eliminates race conditions without arbitrary sleeps.
12+
13+
Args:
14+
port: The port number to check
15+
timeout: Maximum time to wait in seconds (default 5.0)
16+
17+
Raises:
18+
TimeoutError: If server doesn't start within the timeout period
19+
"""
20+
start_time = time.time()
21+
while time.time() - start_time < timeout:
22+
try:
23+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24+
s.settimeout(0.1)
25+
s.connect(("127.0.0.1", port))
26+
# Server is ready
27+
return
28+
except (ConnectionRefusedError, OSError):
29+
# Server not ready yet, retry quickly
30+
time.sleep(0.01)
31+
raise TimeoutError(f"Server on port {port} did not start within {timeout} seconds")

0 commit comments

Comments
 (0)