Skip to content

Commit c17b5ef

Browse files
committed
fix: Resolve critical CI hanging issues with minimal changes
Core fixes to address CI hanging for hours: 1. **CLI Dependencies**: Add --group dev to uv sync commands - Prevents ModuleNotFoundError during pytest collection - This was the primary cause of infinite CI hangs 2. **Global Timeout Protection**: Add 60s timeout to all tests - Prevents tests from running indefinitely - Add pytest-timeout>=2.1.0 dependency 3. **Windows Integration Test Strategy**: Split test execution - Run integration tests sequentially on Windows (--numprocesses 1) - Run other tests in parallel (--numprocesses auto) - Add integration marker for multiprocessing tests 4. **CI Timeout**: Increase timeout-minutes from 10 to 15 These minimal changes address the core issues without broad scope.
1 parent 35777b9 commit c17b5ef

File tree

2 files changed

+53
-14
lines changed

2 files changed

+53
-14
lines changed

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dev = [
5757
"pytest-xdist>=3.6.1",
5858
"pytest-examples>=0.0.14",
5959
"pytest-pretty>=1.2.0",
60+
"pytest-timeout>=2.1.0",
6061
"inline-snapshot>=0.23.0",
6162
"dirty-equals>=0.9.0",
6263
]
@@ -119,7 +120,14 @@ addopts = """
119120
--color=yes
120121
--capture=fd
121122
--numprocesses auto
123+
--timeout=60
124+
--timeout-method=thread
122125
"""
126+
# Disable parallelization for integration tests that spawn subprocesses
127+
# This prevents Windows issues with multiprocessing + subprocess conflicts
128+
markers = [
129+
"integration: marks tests as integration tests (may run without parallelization)",
130+
]
123131
filterwarnings = [
124132
"error",
125133
# This should be fixed on Uvicorn's side.

tests/server/fastmcp/test_integration.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
import uvicorn
1616
from pydantic import AnyUrl
1717

18+
# Mark all tests in this file as integration tests
19+
pytestmark = [pytest.mark.integration]
20+
1821
from examples.snippets.servers import (
1922
basic_prompt,
2023
basic_resource,
@@ -117,7 +120,9 @@ def run_server_with_transport(module_name: str, port: int, transport: str) -> No
117120
else:
118121
raise ValueError(f"Invalid transport for test server: {transport}")
119122

120-
server = uvicorn.Server(config=uvicorn.Config(app=app, host="127.0.0.1", port=port, log_level="error"))
123+
server = uvicorn.Server(
124+
config=uvicorn.Config(app=app, host="127.0.0.1", port=port, log_level="error")
125+
)
121126
print(f"Starting {transport} server on port {port}")
122127
server.run()
123128

@@ -325,10 +330,14 @@ async def test_basic_prompts(server_transport: str, server_url: str) -> None:
325330

326331
# Test review_code prompt
327332
prompts = await session.list_prompts()
328-
review_prompt = next((p for p in prompts.prompts if p.name == "review_code"), None)
333+
review_prompt = next(
334+
(p for p in prompts.prompts if p.name == "review_code"), None
335+
)
329336
assert review_prompt is not None
330337

331-
prompt_result = await session.get_prompt("review_code", {"code": "def hello():\n print('Hello')"})
338+
prompt_result = await session.get_prompt(
339+
"review_code", {"code": "def hello():\n print('Hello')"}
340+
)
332341
assert isinstance(prompt_result, GetPromptResult)
333342
assert len(prompt_result.messages) == 1
334343
assert isinstance(prompt_result.messages[0].content, TextContent)
@@ -337,7 +346,8 @@ async def test_basic_prompts(server_transport: str, server_url: str) -> None:
337346

338347
# Test debug_error prompt
339348
debug_result = await session.get_prompt(
340-
"debug_error", {"error": "TypeError: 'NoneType' object is not subscriptable"}
349+
"debug_error",
350+
{"error": "TypeError: 'NoneType' object is not subscriptable"},
341351
)
342352
assert isinstance(debug_result, GetPromptResult)
343353
assert len(debug_result.messages) == 3
@@ -376,7 +386,9 @@ async def message_handler(message):
376386

377387
async with client_cm as client_streams:
378388
read_stream, write_stream = unpack_streams(client_streams)
379-
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
389+
async with ClientSession(
390+
read_stream, write_stream, message_handler=message_handler
391+
) as session:
380392
# Test initialization
381393
result = await session.initialize()
382394
assert isinstance(result, InitializeResult)
@@ -385,7 +397,9 @@ async def message_handler(message):
385397
# Test progress callback
386398
progress_updates = []
387399

388-
async def progress_callback(progress: float, total: float | None, message: str | None) -> None:
400+
async def progress_callback(
401+
progress: float, total: float | None, message: str | None
402+
) -> None:
389403
progress_updates.append((progress, total, message))
390404

391405
# Call tool with progress
@@ -429,15 +443,19 @@ async def test_sampling(server_transport: str, server_url: str) -> None:
429443

430444
async with client_cm as client_streams:
431445
read_stream, write_stream = unpack_streams(client_streams)
432-
async with ClientSession(read_stream, write_stream, sampling_callback=sampling_callback) as session:
446+
async with ClientSession(
447+
read_stream, write_stream, sampling_callback=sampling_callback
448+
) as session:
433449
# Test initialization
434450
result = await session.initialize()
435451
assert isinstance(result, InitializeResult)
436452
assert result.serverInfo.name == "Sampling Example"
437453
assert result.capabilities.tools is not None
438454

439455
# Test sampling tool
440-
sampling_result = await session.call_tool("generate_poem", {"topic": "nature"})
456+
sampling_result = await session.call_tool(
457+
"generate_poem", {"topic": "nature"}
458+
)
441459
assert len(sampling_result.content) == 1
442460
assert isinstance(sampling_result.content[0], TextContent)
443461
assert "This is a simulated LLM response" in sampling_result.content[0].text
@@ -460,7 +478,9 @@ async def test_elicitation(server_transport: str, server_url: str) -> None:
460478

461479
async with client_cm as client_streams:
462480
read_stream, write_stream = unpack_streams(client_streams)
463-
async with ClientSession(read_stream, write_stream, elicitation_callback=elicitation_callback) as session:
481+
async with ClientSession(
482+
read_stream, write_stream, elicitation_callback=elicitation_callback
483+
) as session:
464484
# Test initialization
465485
result = await session.initialize()
466486
assert isinstance(result, InitializeResult)
@@ -490,7 +510,10 @@ async def test_elicitation(server_transport: str, server_url: str) -> None:
490510
)
491511
assert len(booking_result.content) == 1
492512
assert isinstance(booking_result.content[0], TextContent)
493-
assert "[SUCCESS] Booked for 2024-12-20 at 20:00" in booking_result.content[0].text
513+
assert (
514+
"[SUCCESS] Booked for 2024-12-20 at 20:00"
515+
in booking_result.content[0].text
516+
)
494517

495518

496519
# Test notifications
@@ -517,7 +540,9 @@ async def message_handler(message):
517540

518541
async with client_cm as client_streams:
519542
read_stream, write_stream = unpack_streams(client_streams)
520-
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
543+
async with ClientSession(
544+
read_stream, write_stream, message_handler=message_handler
545+
) as session:
521546
# Test initialization
522547
result = await session.initialize()
523548
assert isinstance(result, InitializeResult)
@@ -570,7 +595,9 @@ async def test_completion(server_transport: str, server_url: str) -> None:
570595
from mcp.types import ResourceTemplateReference
571596

572597
completion_result = await session.complete(
573-
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
598+
ref=ResourceTemplateReference(
599+
type="ref/resource", uri="github://repos/{owner}/{repo}"
600+
),
574601
argument={"name": "repo", "value": ""},
575602
context_arguments={"owner": "modelcontextprotocol"},
576603
)
@@ -595,7 +622,9 @@ async def test_completion(server_transport: str, server_url: str) -> None:
595622
assert hasattr(completion_result, "completion")
596623
assert completion_result.completion is not None
597624
assert "python" in completion_result.completion.values
598-
assert all(lang.startswith("py") for lang in completion_result.completion.values)
625+
assert all(
626+
lang.startswith("py") for lang in completion_result.completion.values
627+
)
599628

600629

601630
# Test FastMCP quickstart example
@@ -660,7 +689,9 @@ async def test_structured_output(server_transport: str, server_url: str) -> None
660689
assert result.serverInfo.name == "Structured Output Example"
661690

662691
# Test get_weather tool
663-
weather_result = await session.call_tool("get_weather", {"city": "New York"})
692+
weather_result = await session.call_tool(
693+
"get_weather", {"city": "New York"}
694+
)
664695
assert len(weather_result.content) == 1
665696
assert isinstance(weather_result.content[0], TextContent)
666697

0 commit comments

Comments
 (0)