fastmcp/tests/cli/test_run.py
Jeremiah Lowin 24ed957f3a Update tests
2025-05-17 18:02:23 -04:00

262 lines
9.1 KiB
Python

"""Tests for the CLI module."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from typer.testing import CliRunner
import fastmcp.cli.run
from fastmcp.cli import cli
# Set up test runner
runner = CliRunner()
@pytest.fixture
def mock_console():
"""Mock the rich console to test output."""
with patch("fastmcp.cli.cli.console") as mock_console:
yield mock_console
@pytest.fixture
def mock_logger():
"""Mock the logger to test logging."""
with patch("fastmcp.cli.cli.logger") as mock_logger:
yield mock_logger
@pytest.fixture
def mock_exit():
"""Mock sys.exit to prevent tests from exiting."""
with patch("sys.exit") as mock_exit:
yield mock_exit
@pytest.fixture
def temp_python_file(tmp_path):
"""Create a temporary Python file with a test server."""
server_code = """
from mcp import Server
class TestServer(Server):
name = "test_server"
dependencies = ["package1", "package2"]
def run(self, **kwargs):
print("Running server with", kwargs)
mcp = TestServer()
server = TestServer()
app = TestServer()
custom_server = TestServer()
"""
file_path = tmp_path / "test_server.py"
file_path.write_text(server_code)
return file_path
@pytest.fixture
def temp_env_file(tmp_path):
"""Create a temporary .env file."""
env_content = """
TEST_VAR1=value1
TEST_VAR2=value2
"""
env_path = tmp_path / ".env"
env_path.write_text(env_content)
return env_path
class TestHelperFunctions:
def test_parse_file_path_simple(self):
"""Test parsing simple file path."""
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = True
mock_expanduser.return_value = Path("file.py")
mock_resolve.return_value = Path("file.py")
path, obj = fastmcp.cli.run.parse_file_path("file.py")
assert path == Path("file.py")
assert obj is None
def test_parse_file_path_with_object(self):
"""Test parsing file path with object."""
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = True
mock_expanduser.return_value = Path("file.py")
mock_resolve.return_value = Path("file.py")
path, obj = fastmcp.cli.run.parse_file_path("file.py:server")
assert path == Path("file.py")
assert obj == "server"
def test_parse_file_path_windows(self):
"""Test parsing Windows file path."""
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = True
mock_expanduser.return_value = Path("C:/path/file.py")
mock_resolve.return_value = Path("C:/path/file.py")
path, obj = fastmcp.cli.run.parse_file_path("C:/path/file.py:server")
assert path == Path("C:/path/file.py")
assert obj == "server"
def test_parse_file_path_not_file(self, mock_exit):
"""Test parsing path that is not a file."""
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
patch("fastmcp.cli.run.logger") as mock_logger,
):
mock_exists.return_value = True
mock_is_file.return_value = False
mock_expanduser.return_value = Path("directory")
mock_resolve.return_value = Path("directory")
fastmcp.cli.run.parse_file_path("directory")
mock_logger.error.assert_called_once()
mock_exit.assert_called_once_with(1)
class TestRunCommand:
"""Tests for the run command."""
def test_run_command_success(self, temp_python_file):
"""Test successful run command execution."""
with (
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
patch("fastmcp.cli.run.import_server") as mock_import,
patch("fastmcp.cli.run.logger") as mock_logger,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, ["run", str(temp_python_file)])
assert result.exit_code == 0
mock_server.run.assert_called_once_with()
mock_logger.debug.assert_called_with(
f'Found server "test_server" in {temp_python_file}'
)
def test_run_command_with_transport(self, temp_python_file):
"""Test run command with transport option."""
with (
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
patch("fastmcp.cli.run.import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--transport", "sse"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(transport="sse")
def test_run_command_with_host(self, temp_python_file):
"""Test run command with host option."""
with (
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
patch("fastmcp.cli.run.import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--host", "0.0.0.0"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(host="0.0.0.0")
def test_run_command_with_port(self, temp_python_file):
"""Test run command with port option."""
with (
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
patch("fastmcp.cli.run.import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--port", "8080"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(port=8080)
def test_run_command_with_log_level(self, temp_python_file):
"""Test run command with log level option."""
with (
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
patch("fastmcp.cli.run.import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--log-level", "DEBUG"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(log_level="DEBUG")
def test_run_command_with_multiple_options(self, temp_python_file):
"""Test run command with multiple options."""
with (
patch("fastmcp.cli.run.parse_file_path") as mock_parse,
patch("fastmcp.cli.run.import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(
cli.app,
[
"run",
str(temp_python_file),
"--transport",
"sse",
"--host",
"0.0.0.0",
"--port",
"8080",
"--log-level",
"DEBUG",
],
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(
transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
)