Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
6755eff
1
Parent(s): d26a0ac
Reduce mocks
Browse files- tests/cli/test_cli.py +125 -111
tests/cli/test_cli.py
CHANGED
|
@@ -94,17 +94,19 @@ class TestMainCLI:
|
|
| 94 |
class TestVersionCommand:
|
| 95 |
"""Test the version command."""
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def test_version_command(self, mock_print, mock_exit):
|
| 100 |
-
"""Test that version command prints info and exits."""
|
| 101 |
-
# Parse and execute version command
|
| 102 |
command, bound, _ = app.parse_args(["version"])
|
| 103 |
-
command
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
mock_print.assert_called_once()
|
| 107 |
-
mock_exit.assert_called_once_with(0)
|
| 108 |
|
| 109 |
|
| 110 |
class TestDevCommand:
|
|
@@ -138,25 +140,21 @@ class TestDevCommand:
|
|
| 138 |
class TestRunCommand:
|
| 139 |
"""Test the run command."""
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
"""Test basic run command."""
|
| 144 |
command, bound, _ = app.parse_args(["run", "server.py"])
|
| 145 |
-
command(**bound.arguments)
|
| 146 |
-
|
| 147 |
-
mock_run_command.assert_called_once_with(
|
| 148 |
-
server_spec="server.py",
|
| 149 |
-
transport=None,
|
| 150 |
-
host=None,
|
| 151 |
-
port=None,
|
| 152 |
-
log_level=None,
|
| 153 |
-
server_args=[],
|
| 154 |
-
show_banner=True,
|
| 155 |
-
)
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
command, bound, _ = app.parse_args(
|
| 161 |
[
|
| 162 |
"run",
|
|
@@ -172,28 +170,35 @@ class TestRunCommand:
|
|
| 172 |
"--no-banner",
|
| 173 |
]
|
| 174 |
)
|
| 175 |
-
command(**bound.arguments)
|
| 176 |
-
|
| 177 |
-
mock_run_command.assert_called_once_with(
|
| 178 |
-
server_spec="server.py",
|
| 179 |
-
transport="http",
|
| 180 |
-
host="localhost",
|
| 181 |
-
port=8080,
|
| 182 |
-
log_level="DEBUG",
|
| 183 |
-
server_args=[],
|
| 184 |
-
show_banner=False,
|
| 185 |
-
)
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
""
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
|
| 199 |
class TestWindowsSpecific:
|
|
@@ -278,84 +283,93 @@ class TestWindowsSpecific:
|
|
| 278 |
assert result == "npx"
|
| 279 |
mock_run.assert_not_called()
|
| 280 |
|
| 281 |
-
def test_windows_path_parsing_with_colon(self):
|
| 282 |
"""Test parsing Windows paths with drive letters and colons."""
|
| 283 |
from fastmcp.cli.run import parse_file_path
|
| 284 |
|
| 285 |
-
#
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
with patch("pathlib.Path.is_file") as mock_is_file:
|
| 289 |
-
mock_exists.return_value = True
|
| 290 |
-
mock_is_file.return_value = True
|
| 291 |
-
|
| 292 |
-
# Test that C:\path\file.py is parsed correctly
|
| 293 |
-
with patch("pathlib.Path.resolve") as mock_resolve:
|
| 294 |
-
mock_resolve.return_value = Path("C:/path/file.py")
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
|
|
|
| 298 |
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
|
| 303 |
-
|
| 304 |
-
|
| 305 |
|
| 306 |
|
| 307 |
class TestInspectCommand:
|
| 308 |
"""Test the inspect command."""
|
| 309 |
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
async def test_inspect_command_basic(
|
| 314 |
-
self, mock_inspect, mock_import_server, mock_parse_file_path, tmp_path
|
| 315 |
-
):
|
| 316 |
-
"""Test basic inspect command functionality."""
|
| 317 |
-
# Setup mocks
|
| 318 |
-
mock_parse_file_path.return_value = (Path("server.py"), None)
|
| 319 |
-
mock_server = Mock()
|
| 320 |
-
mock_import_server.return_value = mock_server
|
| 321 |
-
|
| 322 |
-
mock_info = Mock()
|
| 323 |
-
mock_info.name = "TestServer"
|
| 324 |
-
mock_info.tools = []
|
| 325 |
-
mock_info.prompts = []
|
| 326 |
-
mock_info.resources = []
|
| 327 |
-
mock_info.templates = []
|
| 328 |
-
mock_inspect.return_value = mock_info
|
| 329 |
-
|
| 330 |
-
# Mock TypeAdapter
|
| 331 |
-
with patch("fastmcp.cli.cli.TypeAdapter") as mock_adapter:
|
| 332 |
-
mock_adapter.return_value.dump_json.return_value = b'{"name": "TestServer"}'
|
| 333 |
-
|
| 334 |
-
output_file = tmp_path / "test-output.json"
|
| 335 |
-
|
| 336 |
-
# Parse and execute
|
| 337 |
-
command, bound, _ = app.parse_args(
|
| 338 |
-
[
|
| 339 |
-
"inspect",
|
| 340 |
-
"server.py",
|
| 341 |
-
"--output",
|
| 342 |
-
str(output_file),
|
| 343 |
-
]
|
| 344 |
-
)
|
| 345 |
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
| 360 |
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
class TestVersionCommand:
|
| 95 |
"""Test the version command."""
|
| 96 |
|
| 97 |
+
def test_version_command_parsing(self):
|
| 98 |
+
"""Test that version command can be parsed."""
|
|
|
|
|
|
|
|
|
|
| 99 |
command, bound, _ = app.parse_args(["version"])
|
| 100 |
+
assert command is not None
|
| 101 |
+
|
| 102 |
+
def test_version_command_execution(self):
|
| 103 |
+
"""Test that version command executes and exits properly."""
|
| 104 |
+
# The version command should exit with code 0 when executed
|
| 105 |
+
with pytest.raises(SystemExit) as exc_info:
|
| 106 |
+
command, bound, _ = app.parse_args(["version"])
|
| 107 |
+
command()
|
| 108 |
|
| 109 |
+
assert exc_info.value.code == 0
|
|
|
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
class TestDevCommand:
|
|
|
|
| 140 |
class TestRunCommand:
|
| 141 |
"""Test the run command."""
|
| 142 |
|
| 143 |
+
def test_run_command_parsing_basic(self):
|
| 144 |
+
"""Test basic run command parsing."""
|
|
|
|
| 145 |
command, bound, _ = app.parse_args(["run", "server.py"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
+
assert command is not None
|
| 148 |
+
assert bound.arguments["server_spec"] == "server.py"
|
| 149 |
+
# Cyclopts only includes non-default values
|
| 150 |
+
assert "transport" not in bound.arguments
|
| 151 |
+
assert "host" not in bound.arguments
|
| 152 |
+
assert "port" not in bound.arguments
|
| 153 |
+
assert "log_level" not in bound.arguments
|
| 154 |
+
assert "no_banner" not in bound.arguments
|
| 155 |
+
|
| 156 |
+
def test_run_command_parsing_with_options(self):
|
| 157 |
+
"""Test run command parsing with various options."""
|
| 158 |
command, bound, _ = app.parse_args(
|
| 159 |
[
|
| 160 |
"run",
|
|
|
|
| 170 |
"--no-banner",
|
| 171 |
]
|
| 172 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
assert command is not None
|
| 175 |
+
assert bound.arguments["server_spec"] == "server.py"
|
| 176 |
+
assert bound.arguments["transport"] == "http"
|
| 177 |
+
assert bound.arguments["host"] == "localhost"
|
| 178 |
+
assert bound.arguments["port"] == 8080
|
| 179 |
+
assert bound.arguments["log_level"] == "DEBUG"
|
| 180 |
+
assert bound.arguments["no_banner"] is True
|
| 181 |
+
|
| 182 |
+
def test_run_command_parsing_partial_options(self):
|
| 183 |
+
"""Test run command parsing with only some options."""
|
| 184 |
+
command, bound, _ = app.parse_args(
|
| 185 |
+
[
|
| 186 |
+
"run",
|
| 187 |
+
"server.py",
|
| 188 |
+
"--transport",
|
| 189 |
+
"http",
|
| 190 |
+
"--no-banner",
|
| 191 |
+
]
|
| 192 |
+
)
|
| 193 |
|
| 194 |
+
assert command is not None
|
| 195 |
+
assert bound.arguments["server_spec"] == "server.py"
|
| 196 |
+
assert bound.arguments["transport"] == "http"
|
| 197 |
+
assert bound.arguments["no_banner"] is True
|
| 198 |
+
# Other options should not be present
|
| 199 |
+
assert "host" not in bound.arguments
|
| 200 |
+
assert "port" not in bound.arguments
|
| 201 |
+
assert "log_level" not in bound.arguments
|
| 202 |
|
| 203 |
|
| 204 |
class TestWindowsSpecific:
|
|
|
|
| 283 |
assert result == "npx"
|
| 284 |
mock_run.assert_not_called()
|
| 285 |
|
| 286 |
+
def test_windows_path_parsing_with_colon(self, tmp_path):
|
| 287 |
"""Test parsing Windows paths with drive letters and colons."""
|
| 288 |
from fastmcp.cli.run import parse_file_path
|
| 289 |
|
| 290 |
+
# Create a real test file to test the logic
|
| 291 |
+
test_file = tmp_path / "server.py"
|
| 292 |
+
test_file.write_text("# test server")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
+
# Test normal file parsing (works on all platforms)
|
| 295 |
+
file_path, obj = parse_file_path(str(test_file))
|
| 296 |
+
assert obj is None
|
| 297 |
|
| 298 |
+
# Test file:object parsing
|
| 299 |
+
file_path, obj = parse_file_path(f"{test_file}:myapp")
|
| 300 |
+
assert obj == "myapp"
|
| 301 |
|
| 302 |
+
# Test that the file portion resolves correctly when object is specified
|
| 303 |
+
assert file_path == test_file.resolve()
|
| 304 |
|
| 305 |
|
| 306 |
class TestInspectCommand:
|
| 307 |
"""Test the inspect command."""
|
| 308 |
|
| 309 |
+
def test_inspect_command_parsing_basic(self):
|
| 310 |
+
"""Test basic inspect command parsing."""
|
| 311 |
+
command, bound, _ = app.parse_args(["inspect", "server.py"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
+
assert command is not None
|
| 314 |
+
assert bound.arguments["server_spec"] == "server.py"
|
| 315 |
+
# Only explicitly set parameters are in bound.arguments
|
| 316 |
+
assert "output" not in bound.arguments
|
| 317 |
|
| 318 |
+
def test_inspect_command_parsing_with_output(self, tmp_path):
|
| 319 |
+
"""Test inspect command parsing with output file."""
|
| 320 |
+
output_file = tmp_path / "output.json"
|
| 321 |
|
| 322 |
+
command, bound, _ = app.parse_args(
|
| 323 |
+
[
|
| 324 |
+
"inspect",
|
| 325 |
+
"server.py",
|
| 326 |
+
"--output",
|
| 327 |
+
str(output_file),
|
| 328 |
+
]
|
| 329 |
+
)
|
| 330 |
|
| 331 |
+
assert command is not None
|
| 332 |
+
assert bound.arguments["server_spec"] == "server.py"
|
| 333 |
+
# Output is parsed as a Path object
|
| 334 |
+
assert bound.arguments["output"] == output_file
|
| 335 |
|
| 336 |
+
async def test_inspect_command_with_real_server(self, tmp_path):
|
| 337 |
+
"""Test inspect command with a real server file."""
|
| 338 |
+
# Create a real server file
|
| 339 |
+
server_file = tmp_path / "test_server.py"
|
| 340 |
+
server_file.write_text("""
|
| 341 |
+
import fastmcp
|
| 342 |
+
|
| 343 |
+
mcp = fastmcp.FastMCP("InspectTestServer")
|
| 344 |
+
|
| 345 |
+
@mcp.tool
|
| 346 |
+
def test_tool(x: int) -> int:
|
| 347 |
+
return x * 2
|
| 348 |
+
|
| 349 |
+
@mcp.prompt
|
| 350 |
+
def test_prompt(name: str) -> str:
|
| 351 |
+
return f"Hello, {name}!"
|
| 352 |
+
""")
|
| 353 |
+
|
| 354 |
+
output_file = tmp_path / "inspect_output.json"
|
| 355 |
+
|
| 356 |
+
# Parse and execute the command
|
| 357 |
+
command, bound, _ = app.parse_args(
|
| 358 |
+
[
|
| 359 |
+
"inspect",
|
| 360 |
+
str(server_file),
|
| 361 |
+
"--output",
|
| 362 |
+
str(output_file),
|
| 363 |
+
]
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
await command(**bound.arguments)
|
| 367 |
+
|
| 368 |
+
# Verify the output file was created and contains expected content
|
| 369 |
+
assert output_file.exists()
|
| 370 |
+
content = output_file.read_text()
|
| 371 |
+
|
| 372 |
+
# Basic checks that the inspection worked
|
| 373 |
+
assert "InspectTestServer" in content
|
| 374 |
+
assert "test_tool" in content
|
| 375 |
+
assert "test_prompt" in content
|