Get baseline 100% clean coverage (#1553)
This commit is contained in:
parent
da4fce2195
commit
89e9c43acf
10 changed files with 1032 additions and 1084 deletions
|
|
@ -13,7 +13,7 @@ async def test_list_tools_returns_all_tools():
|
|||
for i in range(num_tools):
|
||||
|
||||
@mcp.tool(name=f"tool_{i}")
|
||||
def dummy_tool_func():
|
||||
def dummy_tool_func(): # pragma: no cover
|
||||
f"""Tool number {i}"""
|
||||
return i
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ async def test_lifespan_cleanup_executed():
|
|||
|
||||
# Give server a moment to complete cleanup
|
||||
with anyio.move_on_after(5.0):
|
||||
while not Path(cleanup_marker).exists():
|
||||
while not Path(cleanup_marker).exists(): # pragma: no cover
|
||||
await anyio.sleep(0.1)
|
||||
|
||||
# Verify cleanup marker was created - this works now that stdio_client
|
||||
|
|
@ -121,9 +121,9 @@ async def test_lifespan_cleanup_executed():
|
|||
finally:
|
||||
# Clean up files
|
||||
for path in [server_script, startup_marker, cleanup_marker]:
|
||||
try:
|
||||
try: # pragma: no cover
|
||||
Path(path).unlink()
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -213,27 +213,27 @@ async def test_stdin_close_triggers_cleanup():
|
|||
await anyio.sleep(0.1)
|
||||
|
||||
# Check if process is still running
|
||||
if hasattr(process, "returncode") and process.returncode is not None:
|
||||
if hasattr(process, "returncode") and process.returncode is not None: # pragma: no cover
|
||||
pytest.fail(f"Server process exited with code {process.returncode}")
|
||||
|
||||
assert Path(startup_marker).exists(), "Server startup marker not created"
|
||||
|
||||
# Close stdin to signal shutdown
|
||||
if process.stdin:
|
||||
if process.stdin: # pragma: no branch
|
||||
await process.stdin.aclose()
|
||||
|
||||
# Wait for process to exit gracefully
|
||||
try:
|
||||
with anyio.fail_after(5.0): # Increased from 2.0 to 5.0
|
||||
await process.wait()
|
||||
except TimeoutError:
|
||||
except TimeoutError: # pragma: no cover
|
||||
# If it doesn't exit after stdin close, terminate it
|
||||
process.terminate()
|
||||
await process.wait()
|
||||
|
||||
# Check if cleanup ran
|
||||
with anyio.move_on_after(5.0):
|
||||
while not Path(cleanup_marker).exists():
|
||||
while not Path(cleanup_marker).exists(): # pragma: no cover
|
||||
await anyio.sleep(0.1)
|
||||
|
||||
# Verify the cleanup ran - stdin closure enables graceful shutdown
|
||||
|
|
@ -243,7 +243,7 @@ async def test_stdin_close_triggers_cleanup():
|
|||
finally:
|
||||
# Clean up files
|
||||
for path in [server_script, startup_marker, cleanup_marker]:
|
||||
try:
|
||||
try: # pragma: no cover
|
||||
Path(path).unlink()
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ async def test_resource_templates():
|
|||
|
||||
# Add a dynamic greeting resource
|
||||
@mcp.resource("greeting://{name}")
|
||||
def get_greeting(name: str) -> str:
|
||||
def get_greeting(name: str) -> str: # pragma: no cover
|
||||
"""Get a personalized greeting"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@mcp.resource("users://{user_id}/profile")
|
||||
def get_user_profile(user_id: str) -> str:
|
||||
def get_user_profile(user_id: str) -> str: # pragma: no cover
|
||||
"""Dynamic user data"""
|
||||
return f"Profile data for user {user_id}"
|
||||
|
||||
|
|
@ -33,10 +33,10 @@ async def test_resource_templates():
|
|||
assert len(templates) == 2
|
||||
|
||||
# Verify template details
|
||||
greeting_template = next(t for t in templates if t.name == "get_greeting")
|
||||
greeting_template = next(t for t in templates if t.name == "get_greeting") # pragma: no cover
|
||||
assert greeting_template.uriTemplate == "greeting://{name}"
|
||||
assert greeting_template.description == "Get a personalized greeting"
|
||||
|
||||
profile_template = next(t for t in templates if t.name == "get_user_profile")
|
||||
profile_template = next(t for t in templates if t.name == "get_user_profile") # pragma: no cover
|
||||
assert profile_template.uriTemplate == "users://{user_id}/profile"
|
||||
assert profile_template.description == "Dynamic user data"
|
||||
|
|
|
|||
|
|
@ -23,25 +23,25 @@ async def test_icons_and_website_url():
|
|||
|
||||
# Create tool with icon
|
||||
@mcp.tool(icons=[test_icon])
|
||||
def test_tool(message: str) -> str:
|
||||
def test_tool(message: str) -> str: # pragma: no cover
|
||||
"""A test tool with an icon."""
|
||||
return message
|
||||
|
||||
# Create resource with icon
|
||||
@mcp.resource("test://resource", icons=[test_icon])
|
||||
def test_resource() -> str:
|
||||
def test_resource() -> str: # pragma: no cover
|
||||
"""A test resource with an icon."""
|
||||
return "test content"
|
||||
|
||||
# Create prompt with icon
|
||||
@mcp.prompt("test_prompt", icons=[test_icon])
|
||||
def test_prompt(text: str) -> str:
|
||||
def test_prompt(text: str) -> str: # pragma: no cover
|
||||
"""A test prompt with an icon."""
|
||||
return text
|
||||
|
||||
# Create resource template with icon
|
||||
@mcp.resource("test://weather/{city}", icons=[test_icon])
|
||||
def test_resource_template(city: str) -> str:
|
||||
def test_resource_template(city: str) -> str: # pragma: no cover
|
||||
"""Get weather for a city."""
|
||||
return f"Weather for {city}"
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ async def test_multiple_icons():
|
|||
|
||||
# Create tool with multiple icons
|
||||
@mcp.tool(icons=[icon1, icon2, icon3])
|
||||
def multi_icon_tool() -> str:
|
||||
def multi_icon_tool() -> str: # pragma: no cover
|
||||
"""A tool with multiple icons."""
|
||||
return "success"
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ async def test_no_icons_or_website():
|
|||
mcp = FastMCP("BasicServer")
|
||||
|
||||
@mcp.tool()
|
||||
def basic_tool() -> str:
|
||||
def basic_tool() -> str: # pragma: no cover
|
||||
"""A basic tool without icons."""
|
||||
return "success"
|
||||
|
||||
|
|
|
|||
|
|
@ -25,28 +25,28 @@ async def test_resource_template_edge_cases():
|
|||
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
|
||||
|
||||
@mcp.resource("resource://users/{user_id}/profile")
|
||||
def get_user_profile(user_id: str, optional_param: str | None = None) -> str:
|
||||
def get_user_profile(user_id: str, optional_param: str | None = None) -> str: # pragma: no cover
|
||||
return f"Profile for user {user_id}"
|
||||
|
||||
# Test case 3: Template with mismatched parameters
|
||||
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
|
||||
|
||||
@mcp.resource("resource://users/{user_id}/profile")
|
||||
def get_user_profile_mismatch(different_param: str) -> str:
|
||||
def get_user_profile_mismatch(different_param: str) -> str: # pragma: no cover
|
||||
return f"Profile for user {different_param}"
|
||||
|
||||
# Test case 4: Template with extra function parameters
|
||||
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
|
||||
|
||||
@mcp.resource("resource://users/{user_id}/profile")
|
||||
def get_user_profile_extra(user_id: str, extra_param: str) -> str:
|
||||
def get_user_profile_extra(user_id: str, extra_param: str) -> str: # pragma: no cover
|
||||
return f"Profile for user {user_id}"
|
||||
|
||||
# Test case 5: Template with missing function parameters
|
||||
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
|
||||
|
||||
@mcp.resource("resource://users/{user_id}/profile/{section}")
|
||||
def get_user_profile_missing(user_id: str) -> str:
|
||||
def get_user_profile_missing(user_id: str) -> str: # pragma: no cover
|
||||
return f"Profile for user {user_id}"
|
||||
|
||||
# Verify valid template works
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ async def test_lowlevel_resource_mime_type():
|
|||
return [ReadResourceContents(content=base64_string, mime_type="image/png")]
|
||||
elif str(uri) == "test://image_bytes":
|
||||
return [ReadResourceContents(content=bytes(image_bytes), mime_type="image/png")]
|
||||
raise Exception(f"Resource not found: {uri}")
|
||||
raise Exception(f"Resource not found: {uri}") # pragma: no cover
|
||||
|
||||
# Test that resources are listed with correct mime type
|
||||
async with client_session(server) as client:
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ from mcp.server.session import ServerSession
|
|||
|
||||
class Database: # Replace with your actual DB type
|
||||
@classmethod
|
||||
async def connect(cls):
|
||||
async def connect(cls): # pragma: no cover
|
||||
return cls()
|
||||
|
||||
async def disconnect(self):
|
||||
async def disconnect(self): # pragma: no cover
|
||||
pass
|
||||
|
||||
def query(self):
|
||||
def query(self): # pragma: no cover
|
||||
return "Hello, World!"
|
||||
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ class AppContext:
|
|||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
|
||||
"""Manage application lifecycle with type-safe context"""
|
||||
# Initialize on startup
|
||||
db = await Database.connect()
|
||||
|
|
@ -45,7 +45,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
|
|||
|
||||
# Access type-safe lifespan context in tools
|
||||
@mcp.tool()
|
||||
def query_db(ctx: Context[ServerSession, AppContext]) -> str:
|
||||
def query_db(ctx: Context[ServerSession, AppContext]) -> str: # pragma: no cover
|
||||
"""Tool that uses initialized resources"""
|
||||
db = ctx.request_context.lifespan_context.db
|
||||
return db.query()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from mcp import ClientSession, StdioServerParameters
|
|||
from mcp.client.stdio import stdio_client
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test")
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") # pragma: no cover
|
||||
@pytest.mark.anyio
|
||||
async def test_windows_stdio_client_with_session():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ async def test_notification_validation_error(tmp_path: Path):
|
|||
return [TextContent(type="text", text=f"slow {request_count}")]
|
||||
elif name == "fast":
|
||||
return [TextContent(type="text", text=f"fast {request_count}")]
|
||||
return [TextContent(type="text", text=f"unknown {request_count}")]
|
||||
return [TextContent(type="text", text=f"unknown {request_count}")] # pragma: no cover
|
||||
|
||||
async def server_handler(
|
||||
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
|
||||
|
|
@ -107,7 +107,7 @@ async def test_notification_validation_error(tmp_path: Path):
|
|||
# proving server is still responsive
|
||||
result = await session.call_tool("fast", read_timeout_seconds=None)
|
||||
assert result.content == [TextContent(type="text", text="fast 3")]
|
||||
scope.cancel()
|
||||
scope.cancel() # pragma: no cover
|
||||
|
||||
# Run server and client in separate task groups to avoid cancellation
|
||||
server_writer, server_reader = anyio.create_memory_object_stream[SessionMessage](1)
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ async def test_malformed_initialize_request_does_not_crash_server():
|
|||
assert second_response.id == "test_id_2"
|
||||
assert second_response.error.code == INVALID_PARAMS
|
||||
|
||||
except anyio.WouldBlock:
|
||||
except anyio.WouldBlock: # pragma: no cover
|
||||
pytest.fail("No response received - server likely crashed")
|
||||
finally:
|
||||
finally: # pragma: no cover
|
||||
# Close all streams to ensure proper cleanup
|
||||
await read_send_stream.aclose()
|
||||
await write_send_stream.aclose()
|
||||
|
|
@ -154,7 +154,7 @@ async def test_multiple_concurrent_malformed_requests():
|
|||
assert isinstance(response, JSONRPCError)
|
||||
assert response.id == f"malformed_{i}"
|
||||
assert response.error.code == INVALID_PARAMS
|
||||
finally:
|
||||
finally: # pragma: no cover
|
||||
# Close all streams to ensure proper cleanup
|
||||
await read_send_stream.aclose()
|
||||
await write_send_stream.aclose()
|
||||
|
|
|
|||
Loading…
Reference in a new issue