Jeremiah Lowin commited on
Commit
6755eff
·
1 Parent(s): d26a0ac

Reduce mocks

Browse files
Files changed (1) hide show
  1. 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
- @patch("fastmcp.cli.cli.sys.exit")
98
- @patch("fastmcp.cli.cli.console.print")
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
- # Verify it printed something and exited with 0
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
- @patch("fastmcp.cli.cli.run_module.run_command")
142
- def test_run_command_basic(self, mock_run_command):
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
- @patch("fastmcp.cli.cli.run_module.run_command")
158
- def test_run_command_with_options(self, mock_run_command):
159
- """Test run command with various options."""
 
 
 
 
 
 
 
 
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
- @patch("fastmcp.cli.cli.run_module.run_command")
188
- def test_run_command_failure(self, mock_run_command):
189
- """Test run command handling failures."""
190
- mock_run_command.side_effect = Exception("Test error")
191
-
192
- with pytest.raises(SystemExit) as exc_info:
193
- command, bound, _ = app.parse_args(["run", "server.py"])
194
- command(**bound.arguments)
 
 
 
 
 
 
 
 
 
 
 
195
 
196
- assert exc_info.value.code == 1
 
 
 
 
 
 
 
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
- # We can't test actual Windows paths on non-Windows systems,
286
- # but we can test the logic with mock paths
287
- with patch("pathlib.Path.exists") as mock_exists:
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
- file_path, obj = parse_file_path("C:\\path\\file.py")
297
- assert obj is None
 
298
 
299
- # Test C:\path\file.py:object parsing
300
- with patch("pathlib.Path.resolve") as mock_resolve:
301
- mock_resolve.return_value = Path("C:/path/file.py")
302
 
303
- file_path, obj = parse_file_path("C:\\path\\file.py:myapp")
304
- assert obj == "myapp"
305
 
306
 
307
  class TestInspectCommand:
308
  """Test the inspect command."""
309
 
310
- @patch("fastmcp.cli.cli.run_module.parse_file_path")
311
- @patch("fastmcp.cli.cli.run_module.import_server")
312
- @patch("fastmcp.cli.cli.inspect_fastmcp")
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
- await command(**bound.arguments)
 
 
 
347
 
348
- # Verify the output file was created
349
- assert output_file.exists()
350
- assert output_file.read_text() == '{"name": "TestServer"}'
351
 
352
- @patch("fastmcp.cli.cli.run_module.import_server")
353
- async def test_inspect_command_failure(self, mock_import_server):
354
- """Test inspect command handling failures."""
355
- mock_import_server.side_effect = Exception("Import failed")
 
 
 
 
356
 
357
- with pytest.raises(SystemExit) as exc_info:
358
- command, bound, _ = app.parse_args(["inspect", "server.py"])
359
- await command(**bound.arguments)
 
360
 
361
- assert exc_info.value.code == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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