Get baseline 100% clean coverage (#1553)
This commit is contained in:
parent
da4fce2195
commit
89e9c43acf
62 changed files with 1032 additions and 1084 deletions
|
|
@ -82,7 +82,7 @@ def test_get_npx_windows(monkeypatch: pytest.MonkeyPatch):
|
|||
def fake_run(cmd: list[str], **kw: Any) -> subprocess.CompletedProcess[bytes]:
|
||||
if cmd[0] in candidates:
|
||||
return subprocess.CompletedProcess(cmd, 0)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
raise subprocess.CalledProcessError(1, cmd[0])
|
||||
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ class MockTokenStorage:
|
|||
self._tokens: OAuthToken | None = None
|
||||
self._client_info: OAuthClientInformationFull | None = None
|
||||
|
||||
async def get_tokens(self) -> OAuthToken | None:
|
||||
async def get_tokens(self) -> OAuthToken | None: # pragma: no cover
|
||||
return self._tokens
|
||||
|
||||
async def set_tokens(self, tokens: OAuthToken) -> None:
|
||||
async def set_tokens(self, tokens: OAuthToken) -> None: # pragma: no cover
|
||||
self._tokens = tokens
|
||||
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None: # pragma: no cover
|
||||
return self._client_info
|
||||
|
||||
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
||||
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: # pragma: no cover
|
||||
self._client_info = client_info
|
||||
|
||||
|
||||
|
|
@ -45,11 +45,11 @@ def client_metadata():
|
|||
|
||||
@pytest.fixture
|
||||
def rfc7523_oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage):
|
||||
async def redirect_handler(url: str) -> None:
|
||||
async def redirect_handler(url: str) -> None: # pragma: no cover
|
||||
"""Mock redirect handler."""
|
||||
pass
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
async def callback_handler() -> tuple[str, str | None]: # pragma: no cover
|
||||
"""Mock callback handler."""
|
||||
return "test_auth_code", "test_state"
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class StreamSpyCollection:
|
|||
self.client.sent_messages.clear()
|
||||
self.server.sent_messages.clear()
|
||||
|
||||
def get_client_requests(self, method: str | None = None) -> list[JSONRPCRequest]:
|
||||
def get_client_requests(self, method: str | None = None) -> list[JSONRPCRequest]: # pragma: no cover
|
||||
"""Get client-sent requests, optionally filtered by method."""
|
||||
return [
|
||||
req.message.root
|
||||
|
|
@ -48,15 +48,15 @@ class StreamSpyCollection:
|
|||
if isinstance(req.message.root, JSONRPCRequest) and (method is None or req.message.root.method == method)
|
||||
]
|
||||
|
||||
def get_server_requests(self, method: str | None = None) -> list[JSONRPCRequest]:
|
||||
def get_server_requests(self, method: str | None = None) -> list[JSONRPCRequest]: # pragma: no cover
|
||||
"""Get server-sent requests, optionally filtered by method."""
|
||||
return [
|
||||
return [ # pragma: no cover
|
||||
req.message.root
|
||||
for req in self.server.sent_messages
|
||||
if isinstance(req.message.root, JSONRPCRequest) and (method is None or req.message.root.method == method)
|
||||
]
|
||||
|
||||
def get_client_notifications(self, method: str | None = None) -> list[JSONRPCNotification]:
|
||||
def get_client_notifications(self, method: str | None = None) -> list[JSONRPCNotification]: # pragma: no cover
|
||||
"""Get client-sent notifications, optionally filtered by method."""
|
||||
return [
|
||||
notif.message.root
|
||||
|
|
@ -65,7 +65,7 @@ class StreamSpyCollection:
|
|||
and (method is None or notif.message.root.method == method)
|
||||
]
|
||||
|
||||
def get_server_notifications(self, method: str | None = None) -> list[JSONRPCNotification]:
|
||||
def get_server_notifications(self, method: str | None = None) -> list[JSONRPCNotification]: # pragma: no cover
|
||||
"""Get server-sent notifications, optionally filtered by method."""
|
||||
return [
|
||||
notif.message.root
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ class MockTokenStorage:
|
|||
self._client_info: OAuthClientInformationFull | None = None
|
||||
|
||||
async def get_tokens(self) -> OAuthToken | None:
|
||||
return self._tokens
|
||||
return self._tokens # pragma: no cover
|
||||
|
||||
async def set_tokens(self, tokens: OAuthToken) -> None:
|
||||
self._tokens = tokens
|
||||
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
||||
return self._client_info
|
||||
return self._client_info # pragma: no cover
|
||||
|
||||
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
||||
self._client_info = client_info
|
||||
|
|
@ -64,11 +64,11 @@ def valid_tokens():
|
|||
def oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage):
|
||||
async def redirect_handler(url: str) -> None:
|
||||
"""Mock redirect handler."""
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
"""Mock callback handler."""
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
return OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
|
|
@ -247,10 +247,10 @@ class TestOAuthFlow:
|
|||
"""Test protected resource metadata discovery URL building with fallback."""
|
||||
|
||||
async def redirect_handler(url: str) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
provider = OAuthClientProvider(
|
||||
server_url="https://api.example.com",
|
||||
|
|
@ -663,7 +663,7 @@ class TestRegistrationResponse:
|
|||
|
||||
@property
|
||||
def text(self):
|
||||
if not self._aread_called:
|
||||
if not self._aread_called: # pragma: no cover
|
||||
raise RuntimeError("Response.text accessed before response.aread()")
|
||||
return self._text
|
||||
|
||||
|
|
@ -846,10 +846,10 @@ class TestAuthFlow:
|
|||
# In the buggy version, this would yield the request AGAIN unconditionally
|
||||
# In the fixed version, this should end the generator
|
||||
try:
|
||||
await auth_flow.asend(response) # extra request
|
||||
request_yields += 1
|
||||
# If we reach here, the bug is present
|
||||
pytest.fail(
|
||||
await auth_flow.asend(response) # extra request # pragma: no cover
|
||||
request_yields += 1 # pragma: no cover
|
||||
# If we reach here, the bug is present # pragma: no cover
|
||||
pytest.fail( # pragma: no cover
|
||||
f"Unnecessary retry detected! Request was yielded {request_yields} times. "
|
||||
f"This indicates the retry logic bug that caused 2x performance degradation. "
|
||||
f"The request should only be yielded once for successful responses."
|
||||
|
|
@ -949,7 +949,7 @@ class TestAuthFlow:
|
|||
success_response = httpx.Response(200, request=final_request)
|
||||
try:
|
||||
await auth_flow.asend(success_response)
|
||||
pytest.fail("Should have stopped after successful response")
|
||||
pytest.fail("Should have stopped after successful response") # pragma: no cover
|
||||
except StopAsyncIteration:
|
||||
pass # Expected
|
||||
|
||||
|
|
@ -1043,10 +1043,10 @@ class TestSEP985Discovery:
|
|||
"""Test that client falls back to path-based well-known URI when WWW-Authenticate is absent."""
|
||||
|
||||
async def redirect_handler(url: str) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
provider = OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
|
|
@ -1076,10 +1076,10 @@ class TestSEP985Discovery:
|
|||
"""Test that client falls back to root-based URI when path-based returns 404."""
|
||||
|
||||
async def redirect_handler(url: str) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
provider = OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
|
|
@ -1177,10 +1177,10 @@ class TestSEP985Discovery:
|
|||
"""Test that WWW-Authenticate header resource_metadata takes priority over well-known URIs."""
|
||||
|
||||
async def redirect_handler(url: str) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
provider = OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
|
|
@ -1269,10 +1269,10 @@ class TestWWWAuthenticate:
|
|||
"""Test extraction of various fields from valid WWW-Authenticate headers."""
|
||||
|
||||
async def redirect_handler(url: str) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
provider = OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
|
|
@ -1317,10 +1317,10 @@ class TestWWWAuthenticate:
|
|||
"""Test extraction returns None for invalid cases."""
|
||||
|
||||
async def redirect_handler(url: str) -> None:
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
async def callback_handler() -> tuple[str, str | None]:
|
||||
return "test_auth_code", "test_state"
|
||||
return "test_auth_code", "test_state" # pragma: no cover
|
||||
|
||||
provider = OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ def test_command_execution(mock_config_path: Path):
|
|||
|
||||
test_args = [command] + args + ["--help"]
|
||||
|
||||
result = subprocess.run(test_args, capture_output=True, text=True, timeout=5, check=False)
|
||||
result = subprocess.run(test_args, capture_output=True, text=True, timeout=20, check=False)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "usage" in result.stdout.lower()
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ UNICODE_TEST_STRINGS = {
|
|||
}
|
||||
|
||||
|
||||
def run_unicode_server(port: int) -> None:
|
||||
def run_unicode_server(port: int) -> None: # pragma: no cover
|
||||
"""Run the Unicode test server in a separate process."""
|
||||
# Import inside the function since this runs in a separate process
|
||||
from collections.abc import AsyncGenerator
|
||||
|
|
@ -167,7 +167,7 @@ def running_unicode_server(unicode_server_port: int) -> Generator[str, None, Non
|
|||
# Clean up - try graceful termination first
|
||||
proc.terminate()
|
||||
proc.join(timeout=2)
|
||||
if proc.is_alive():
|
||||
if proc.is_alive(): # pragma: no cover
|
||||
proc.kill()
|
||||
proc.join(timeout=1)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,27 +19,27 @@ async def full_featured_server():
|
|||
server = FastMCP("test")
|
||||
|
||||
@server.tool(name="test_tool_1")
|
||||
async def test_tool_1() -> str:
|
||||
async def test_tool_1() -> str: # pragma: no cover
|
||||
"""First test tool"""
|
||||
return "Result 1"
|
||||
|
||||
@server.tool(name="test_tool_2")
|
||||
async def test_tool_2() -> str:
|
||||
async def test_tool_2() -> str: # pragma: no cover
|
||||
"""Second test tool"""
|
||||
return "Result 2"
|
||||
|
||||
@server.resource("resource://test/data")
|
||||
async def test_resource() -> str:
|
||||
async def test_resource() -> str: # pragma: no cover
|
||||
"""Test resource"""
|
||||
return "Test data"
|
||||
|
||||
@server.prompt()
|
||||
async def test_prompt(name: str) -> str:
|
||||
async def test_prompt(name: str) -> str: # pragma: no cover
|
||||
"""Test prompt"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@server.resource("resource://test/{name}")
|
||||
async def test_template(name: str) -> str:
|
||||
async def test_template(name: str) -> str: # pragma: no cover
|
||||
"""Test resource template"""
|
||||
return f"Data for {name}"
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ async def test_list_tools_with_strict_server_validation():
|
|||
server = Server("strict_server")
|
||||
|
||||
@server.list_tools()
|
||||
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:
|
||||
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult: # pragma: no cover
|
||||
"""Strict handler that validates params field exists"""
|
||||
|
||||
# Simulate strict server validation
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ async def test_logging_callback():
|
|||
async def message_handler(
|
||||
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
|
||||
) -> None:
|
||||
if isinstance(message, Exception):
|
||||
if isinstance(message, Exception): # pragma: no cover
|
||||
raise message
|
||||
|
||||
async with create_session(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from mcp.types import ClientNotification, RootsListChangedNotification
|
|||
from tests.test_helpers import wait_for_server
|
||||
|
||||
|
||||
def create_non_sdk_server_app() -> Starlette:
|
||||
def create_non_sdk_server_app() -> Starlette: # pragma: no cover
|
||||
"""Create a minimal server that doesn't follow SDK conventions."""
|
||||
|
||||
async def handle_mcp_request(request: Request) -> Response:
|
||||
|
|
@ -67,7 +67,7 @@ def create_non_sdk_server_app() -> Starlette:
|
|||
return app
|
||||
|
||||
|
||||
def run_non_sdk_server(port: int) -> None:
|
||||
def run_non_sdk_server(port: int) -> None: # pragma: no cover
|
||||
"""Run the non-SDK server in a separate process."""
|
||||
app = create_non_sdk_server_app()
|
||||
config = uvicorn.Config(
|
||||
|
|
@ -95,9 +95,9 @@ def non_sdk_server(non_sdk_server_port: int) -> Generator[None, None, None]:
|
|||
proc.start()
|
||||
|
||||
# Wait for server to be ready
|
||||
try:
|
||||
try: # pragma: no cover
|
||||
wait_for_server(non_sdk_server_port, timeout=10.0)
|
||||
except TimeoutError:
|
||||
except TimeoutError: # pragma: no cover
|
||||
proc.kill()
|
||||
proc.join(timeout=2)
|
||||
pytest.fail("Server failed to start within 10 seconds")
|
||||
|
|
@ -120,7 +120,7 @@ async def test_non_compliant_notification_response(non_sdk_server: None, non_sdk
|
|||
server_url = f"http://127.0.0.1:{non_sdk_server_port}/mcp"
|
||||
returned_exception = None
|
||||
|
||||
async def message_handler(
|
||||
async def message_handler( # pragma: no cover
|
||||
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
|
||||
):
|
||||
nonlocal returned_exception
|
||||
|
|
@ -141,5 +141,5 @@ async def test_non_compliant_notification_response(non_sdk_server: None, non_sdk
|
|||
ClientNotification(RootsListChangedNotification(method="notifications/roots/list_changed"))
|
||||
)
|
||||
|
||||
if returned_exception:
|
||||
if returned_exception: # pragma: no cover
|
||||
pytest.fail(f"Server encountered an exception: {returned_exception}")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ async def test_send_request_stream_cleanup():
|
|||
|
||||
# Create a mock session with the minimal required functionality
|
||||
class TestSession(BaseSession[ClientRequest, ClientNotification, ClientResult, Any, Any]):
|
||||
async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
|
||||
async def _send_response(
|
||||
self, request_id: RequestId, response: SendResultT | ErrorData
|
||||
) -> None: # pragma: no cover
|
||||
pass
|
||||
|
||||
# Create streams
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ async def test_client_session_initialize():
|
|||
)
|
||||
|
||||
# Create a message handler to catch exceptions
|
||||
async def message_handler(
|
||||
async def message_handler( # pragma: no cover
|
||||
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
|
||||
) -> None:
|
||||
if isinstance(message, Exception):
|
||||
|
|
@ -426,7 +426,7 @@ async def test_client_capabilities_with_custom_callbacks():
|
|||
|
||||
received_capabilities = None
|
||||
|
||||
async def custom_sampling_callback(
|
||||
async def custom_sampling_callback( # pragma: no cover
|
||||
context: RequestContext["ClientSession", Any],
|
||||
params: types.CreateMessageRequestParams,
|
||||
) -> types.CreateMessageResult | types.ErrorData:
|
||||
|
|
@ -436,7 +436,7 @@ async def test_client_capabilities_with_custom_callbacks():
|
|||
model="test-model",
|
||||
)
|
||||
|
||||
async def custom_list_roots_callback(
|
||||
async def custom_list_roots_callback( # pragma: no cover
|
||||
context: RequestContext["ClientSession", Any],
|
||||
) -> types.ListRootsResult | types.ErrorData:
|
||||
return types.ListRootsResult(roots=[])
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class TestClientSessionGroup:
|
|||
mock_session = mock.AsyncMock()
|
||||
|
||||
# --- Prepare Session Group ---
|
||||
def hook(name: str, server_info: types.Implementation) -> str:
|
||||
def hook(name: str, server_info: types.Implementation) -> str: # pragma: no cover
|
||||
return f"{(server_info.name)}-{name}"
|
||||
|
||||
mcp_session_group = ClientSessionGroup(component_name_hook=hook)
|
||||
|
|
@ -344,7 +344,7 @@ class TestClientSessionGroup:
|
|||
timeout=server_params_instance.timeout,
|
||||
sse_read_timeout=server_params_instance.sse_read_timeout,
|
||||
)
|
||||
elif client_type_name == "streamablehttp":
|
||||
elif client_type_name == "streamablehttp": # pragma: no branch
|
||||
assert isinstance(server_params_instance, StreamableHttpParameters)
|
||||
mock_specific_client_func.assert_called_once_with(
|
||||
url=server_params_instance.url,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ async def test_stdio_client():
|
|||
read_messages: list[JSONRPCMessage] = []
|
||||
async with read_stream:
|
||||
async for message in read_stream:
|
||||
if isinstance(message, Exception):
|
||||
if isinstance(message, Exception): # pragma: no cover
|
||||
raise message
|
||||
|
||||
read_messages.append(message.message)
|
||||
|
|
@ -93,7 +93,7 @@ async def test_stdio_client_nonexistent_command():
|
|||
# Should raise an error when trying to start the process
|
||||
with pytest.raises(OSError) as exc_info:
|
||||
async with stdio_client(server_params) as (_, _):
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
||||
# The error should indicate the command was not found (ENOENT: No such file or directory)
|
||||
assert exc_info.value.errno == errno.ENOENT
|
||||
|
|
@ -144,7 +144,7 @@ async def test_stdio_client_universal_cleanup():
|
|||
)
|
||||
|
||||
# Check if we timed out
|
||||
if cancel_scope.cancelled_caught:
|
||||
if cancel_scope.cancelled_caught: # pragma: no cover
|
||||
pytest.fail(
|
||||
"stdio_client cleanup timed out after 8.0 seconds. "
|
||||
"This indicates the cleanup mechanism is hanging and needs fixing."
|
||||
|
|
@ -153,7 +153,7 @@ async def test_stdio_client_universal_cleanup():
|
|||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="Windows signal handling is different")
|
||||
async def test_stdio_client_sigint_only_process():
|
||||
async def test_stdio_client_sigint_only_process(): # pragma: no cover
|
||||
"""
|
||||
Test cleanup with a process that ignores SIGTERM but responds to SIGINT.
|
||||
"""
|
||||
|
|
@ -195,7 +195,7 @@ async def test_stdio_client_sigint_only_process():
|
|||
# Exit context triggers cleanup - this should not hang
|
||||
pass
|
||||
|
||||
if cancel_scope.cancelled_caught:
|
||||
if cancel_scope.cancelled_caught: # pragma: no cover
|
||||
raise TimeoutError("Test timed out")
|
||||
|
||||
end_time = time.time()
|
||||
|
|
@ -208,7 +208,7 @@ async def test_stdio_client_sigint_only_process():
|
|||
f"Expected < {SIGTERM_IGNORING_PROCESS_TIMEOUT} seconds. "
|
||||
"This suggests the cleanup needs SIGINT/SIGKILL fallback."
|
||||
)
|
||||
except (TimeoutError, Exception) as e:
|
||||
except (TimeoutError, Exception) as e: # pragma: no cover
|
||||
if isinstance(e, TimeoutError) or "timed out" in str(e):
|
||||
pytest.fail(
|
||||
f"stdio_client cleanup timed out after {SIGTERM_IGNORING_PROCESS_TIMEOUT} seconds "
|
||||
|
|
@ -303,7 +303,7 @@ class TestChildProcessCleanup:
|
|||
assert os.path.exists(parent_marker), "Parent process didn't start"
|
||||
|
||||
# Verify child is writing
|
||||
if os.path.exists(marker_file):
|
||||
if os.path.exists(marker_file): # pragma: no branch
|
||||
initial_size = os.path.getsize(marker_file)
|
||||
await anyio.sleep(0.3)
|
||||
size_after_wait = os.path.getsize(marker_file)
|
||||
|
|
@ -318,7 +318,7 @@ class TestChildProcessCleanup:
|
|||
|
||||
# Verify processes stopped
|
||||
await anyio.sleep(0.5)
|
||||
if os.path.exists(marker_file):
|
||||
if os.path.exists(marker_file): # pragma: no branch
|
||||
size_after_cleanup = os.path.getsize(marker_file)
|
||||
await anyio.sleep(0.5)
|
||||
final_size = os.path.getsize(marker_file)
|
||||
|
|
@ -335,7 +335,7 @@ class TestChildProcessCleanup:
|
|||
for f in [marker_file, parent_marker]:
|
||||
try:
|
||||
os.unlink(f)
|
||||
except OSError:
|
||||
except OSError: # pragma: no cover
|
||||
pass
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -406,7 +406,7 @@ class TestChildProcessCleanup:
|
|||
|
||||
# Verify all are writing
|
||||
for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]:
|
||||
if os.path.exists(file_path):
|
||||
if os.path.exists(file_path): # pragma: no branch
|
||||
initial_size = os.path.getsize(file_path)
|
||||
await anyio.sleep(0.3)
|
||||
new_size = os.path.getsize(file_path)
|
||||
|
|
@ -420,7 +420,7 @@ class TestChildProcessCleanup:
|
|||
# Verify all stopped
|
||||
await anyio.sleep(0.5)
|
||||
for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]:
|
||||
if os.path.exists(file_path):
|
||||
if os.path.exists(file_path): # pragma: no branch
|
||||
size1 = os.path.getsize(file_path)
|
||||
await anyio.sleep(0.3)
|
||||
size2 = os.path.getsize(file_path)
|
||||
|
|
@ -433,7 +433,7 @@ class TestChildProcessCleanup:
|
|||
for f in [parent_file, child_file, grandchild_file]:
|
||||
try:
|
||||
os.unlink(f)
|
||||
except OSError:
|
||||
except OSError: # pragma: no cover
|
||||
pass
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -487,7 +487,7 @@ class TestChildProcessCleanup:
|
|||
await anyio.sleep(0.5)
|
||||
|
||||
# Verify child is writing
|
||||
if os.path.exists(marker_file):
|
||||
if os.path.exists(marker_file): # pragma: no cover
|
||||
size1 = os.path.getsize(marker_file)
|
||||
await anyio.sleep(0.3)
|
||||
size2 = os.path.getsize(marker_file)
|
||||
|
|
@ -500,7 +500,7 @@ class TestChildProcessCleanup:
|
|||
|
||||
# Verify child stopped
|
||||
await anyio.sleep(0.5)
|
||||
if os.path.exists(marker_file):
|
||||
if os.path.exists(marker_file): # pragma: no branch
|
||||
size3 = os.path.getsize(marker_file)
|
||||
await anyio.sleep(0.3)
|
||||
size4 = os.path.getsize(marker_file)
|
||||
|
|
@ -512,7 +512,7 @@ class TestChildProcessCleanup:
|
|||
# Clean up marker file
|
||||
try:
|
||||
os.unlink(marker_file)
|
||||
except OSError:
|
||||
except OSError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -560,7 +560,7 @@ async def test_stdio_client_graceful_stdin_exit():
|
|||
pytest.fail(
|
||||
"stdio_client cleanup timed out after 5.0 seconds. "
|
||||
"Process should have exited gracefully when stdin was closed."
|
||||
)
|
||||
) # pragma: no cover
|
||||
|
||||
end_time = time.time()
|
||||
elapsed = end_time - start_time
|
||||
|
|
@ -619,7 +619,7 @@ async def test_stdio_client_stdin_close_ignored():
|
|||
pytest.fail(
|
||||
"stdio_client cleanup timed out after 7.0 seconds. "
|
||||
"Process should have been terminated via SIGTERM escalation."
|
||||
)
|
||||
) # pragma: no cover
|
||||
|
||||
end_time = time.time()
|
||||
elapsed = end_time - start_time
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue