unity-mcp/Server/tests/test_cli.py

1345 lines
55 KiB
Python

"""Unit tests for Unity MCP CLI."""
import json
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from click.testing import CliRunner
from cli.main import cli
from cli.utils.config import CLIConfig, get_config, set_config
from cli.utils.output import format_output, format_as_json, format_as_text, format_as_table
from cli.utils.connection import (
send_command,
check_connection,
list_unity_instances,
UnityConnectionError,
)
# =============================================================================
# Fixtures
# =============================================================================
@pytest.fixture
def runner():
"""Create a CLI test runner."""
return CliRunner()
@pytest.fixture
def mock_config():
"""Create a mock CLI configuration."""
return CLIConfig(
host="127.0.0.1",
port=8080,
timeout=30,
format="text",
unity_instance=None,
)
@pytest.fixture
def mock_unity_response():
"""Standard successful Unity response."""
return {
"success": True,
"message": "Operation successful",
"data": {"test": "data"}
}
@pytest.fixture
def mock_instances_response():
"""Mock Unity instances response."""
return {
"success": True,
"instances": [
{
"session_id": "test-session-123",
"project": "TestProject",
"hash": "abc123def456",
"unity_version": "2022.3.10f1",
"connected_at": "2024-01-01T00:00:00Z",
}
]
}
@pytest.fixture
def mock_sessions_response():
"""Mock plugin sessions response (legacy format)."""
return {
"sessions": {
"test-session-123": {
"project": "TestProject",
"hash": "abc123def456",
"unity_version": "2022.3.10f1",
"connected_at": "2024-01-01T00:00:00Z",
}
}
}
# =============================================================================
# Config Tests
# =============================================================================
class TestConfig:
"""Tests for CLI configuration."""
def test_default_config(self):
"""Test default configuration values."""
config = CLIConfig()
assert config.host == "127.0.0.1"
assert config.port == 8080
assert config.timeout == 30
assert config.format == "text"
assert config.unity_instance is None
def test_config_from_env(self, monkeypatch):
"""Test configuration from environment variables."""
monkeypatch.setenv("UNITY_MCP_HOST", "192.168.1.100")
monkeypatch.setenv("UNITY_MCP_HTTP_PORT", "9090")
monkeypatch.setenv("UNITY_MCP_TIMEOUT", "60")
monkeypatch.setenv("UNITY_MCP_FORMAT", "json")
monkeypatch.setenv("UNITY_MCP_INSTANCE", "MyProject")
config = CLIConfig.from_env()
assert config.host == "192.168.1.100"
assert config.port == 9090
assert config.timeout == 60
assert config.format == "json"
assert config.unity_instance == "MyProject"
def test_set_and_get_config(self, mock_config):
"""Test setting and getting global config."""
set_config(mock_config)
retrieved = get_config()
assert retrieved.host == mock_config.host
assert retrieved.port == mock_config.port
# =============================================================================
# Output Formatting Tests
# =============================================================================
class TestOutputFormatting:
"""Tests for output formatting utilities."""
def test_format_as_json(self):
"""Test JSON formatting."""
data = {"key": "value", "number": 42}
result = format_as_json(data)
parsed = json.loads(result)
assert parsed == data
def test_format_as_json_with_complex_types(self):
"""Test JSON formatting with complex types."""
from datetime import datetime
data = {"timestamp": datetime(2024, 1, 1)}
result = format_as_json(data)
assert "2024" in result
def test_format_as_text_success_response(self):
"""Test text formatting for success response."""
data = {
"success": True,
"message": "OK",
"data": {"name": "Player", "id": 123}
}
result = format_as_text(data)
assert "name" in result
assert "Player" in result
def test_format_as_text_error_response(self):
"""Test text formatting for error response."""
data = {"success": False, "error": "Something went wrong"}
result = format_as_text(data)
assert "Error" in result
assert "Something went wrong" in result
def test_format_as_text_list(self):
"""Test text formatting for lists."""
data = [{"name": "Item1"}, {"name": "Item2"}]
result = format_as_text(data)
assert "2 items" in result
def test_format_as_table(self):
"""Test table formatting."""
data = [
{"name": "Player", "id": 1},
{"name": "Enemy", "id": 2},
]
result = format_as_table(data)
assert "name" in result
assert "Player" in result
assert "Enemy" in result
def test_format_output_dispatch(self):
"""Test format_output dispatches correctly."""
data = {"key": "value"}
json_result = format_output(data, "json")
assert json.loads(json_result) == data
text_result = format_output(data, "text")
assert "key" in text_result
table_result = format_output(data, "table")
assert "key" in table_result.lower() or "Key" in table_result
# =============================================================================
# Connection Tests
# =============================================================================
class TestConnection:
"""Tests for connection utilities."""
@pytest.mark.asyncio
async def test_check_connection_success(self):
"""Test successful connection check."""
mock_response = MagicMock()
mock_response.status_code = 200
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
return_value=mock_response
)
result = await check_connection()
assert result is True
@pytest.mark.asyncio
async def test_check_connection_failure(self):
"""Test failed connection check."""
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
side_effect=Exception("Connection refused")
)
result = await check_connection()
assert result is False
@pytest.mark.asyncio
async def test_send_command_success(self, mock_unity_response):
"""Test successful command sending."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = mock_unity_response
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
return_value=mock_response
)
mock_response.raise_for_status = MagicMock()
result = await send_command("test_command", {"param": "value"})
assert result == mock_unity_response
@pytest.mark.asyncio
async def test_send_command_connection_error(self):
"""Test command sending with connection error."""
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
side_effect=Exception("Connection refused")
)
with pytest.raises(UnityConnectionError):
await send_command("test_command", {})
@pytest.mark.asyncio
async def test_list_instances_from_sessions(self, mock_sessions_response):
"""Test listing instances from /plugin/sessions endpoint."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = mock_sessions_response
with patch("httpx.AsyncClient") as mock_client:
# First call (api/instances) returns 404, second (plugin/sessions) succeeds
mock_get = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value.get = mock_get
result = await list_unity_instances()
assert result["success"] is True
assert len(result["instances"]) == 1
assert result["instances"][0]["project"] == "TestProject"
# =============================================================================
# CLI Command Tests
# =============================================================================
class TestCLICommands:
"""Tests for CLI commands."""
def test_cli_help(self, runner):
"""Test CLI help command."""
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "Unity MCP Command Line Interface" in result.output
def test_cli_version(self, runner):
"""Test CLI version command."""
result = runner.invoke(cli, ["--version"])
assert result.exit_code == 0
def test_status_connected(self, runner, mock_instances_response):
"""Test status command when connected."""
with patch("cli.main.run_check_connection", return_value=True):
with patch("cli.main.run_list_instances", return_value=mock_instances_response):
result = runner.invoke(cli, ["status"])
assert result.exit_code == 0
assert "Connected" in result.output
def test_status_disconnected(self, runner):
"""Test status command when disconnected."""
with patch("cli.main.run_check_connection", return_value=False):
result = runner.invoke(cli, ["status"])
assert result.exit_code == 1
assert "Cannot connect" in result.output
def test_instances_command(self, runner, mock_instances_response):
"""Test instances command."""
with patch("cli.main.run_list_instances", return_value=mock_instances_response):
result = runner.invoke(cli, ["instances"])
assert result.exit_code == 0
def test_raw_command(self, runner, mock_unity_response):
"""Test raw command."""
with patch("cli.main.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["raw", "test_command", '{"param": "value"}'])
assert result.exit_code == 0
def test_raw_command_invalid_json(self, runner):
"""Test raw command with invalid JSON."""
result = runner.invoke(cli, ["raw", "test_command", "invalid json"])
assert result.exit_code == 1
assert "Invalid JSON" in result.output
# =============================================================================
# GameObject Command Tests
# =============================================================================
class TestGameObjectCommands:
"""Tests for GameObject CLI commands."""
def test_gameobject_find(self, runner, mock_unity_response):
"""Test gameobject find command."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["gameobject", "find", "Player"])
assert result.exit_code == 0
def test_gameobject_find_with_options(self, runner, mock_unity_response):
"""Test gameobject find with options."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"gameobject", "find", "Enemy",
"--method", "by_tag",
"--include-inactive",
"--limit", "100"
])
assert result.exit_code == 0
def test_gameobject_create(self, runner, mock_unity_response):
"""Test gameobject create command."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["gameobject", "create", "NewObject"])
assert result.exit_code == 0
def test_gameobject_create_with_primitive(self, runner, mock_unity_response):
"""Test gameobject create with primitive."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"gameobject", "create", "MyCube",
"--primitive", "Cube",
"--position", "0", "1", "0"
])
assert result.exit_code == 0
def test_gameobject_modify(self, runner, mock_unity_response):
"""Test gameobject modify command."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"gameobject", "modify", "Player",
"--position", "0", "5", "0"
])
assert result.exit_code == 0
def test_gameobject_delete(self, runner, mock_unity_response):
"""Test gameobject delete command."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["gameobject", "delete", "OldObject", "--force"])
assert result.exit_code == 0
def test_gameobject_delete_confirmation(self, runner, mock_unity_response):
"""Test gameobject delete with confirmation prompt."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["gameobject", "delete", "OldObject"], input="y\n")
assert result.exit_code == 0
def test_gameobject_duplicate(self, runner, mock_unity_response):
"""Test gameobject duplicate command."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"gameobject", "duplicate", "Player",
"--name", "Player2",
"--offset", "5", "0", "0"
])
assert result.exit_code == 0
def test_gameobject_move(self, runner, mock_unity_response):
"""Test gameobject move command."""
with patch("cli.commands.gameobject.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"gameobject", "move", "Chair",
"--reference", "Table",
"--direction", "right",
"--distance", "2"
])
assert result.exit_code == 0
# =============================================================================
# Component Command Tests
# =============================================================================
class TestComponentCommands:
"""Tests for Component CLI commands."""
def test_component_add(self, runner, mock_unity_response):
"""Test component add command."""
with patch("cli.commands.component.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["component", "add", "Player", "Rigidbody"])
assert result.exit_code == 0
def test_component_remove(self, runner, mock_unity_response):
"""Test component remove command."""
with patch("cli.commands.component.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["component", "remove", "Player", "Rigidbody", "--force"])
assert result.exit_code == 0
def test_component_set(self, runner, mock_unity_response):
"""Test component set command."""
with patch("cli.commands.component.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["component", "set", "Player", "Rigidbody", "mass", "5.0"])
assert result.exit_code == 0
def test_component_modify(self, runner, mock_unity_response):
"""Test component modify command."""
with patch("cli.commands.component.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"component", "modify", "Player", "Rigidbody",
"--properties", '{"mass": 5.0, "useGravity": false}'
])
assert result.exit_code == 0
# =============================================================================
# Scene Command Tests
# =============================================================================
class TestSceneCommands:
"""Tests for Scene CLI commands."""
def test_scene_hierarchy(self, runner, mock_unity_response):
"""Test scene hierarchy command."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["scene", "hierarchy"])
assert result.exit_code == 0
def test_scene_hierarchy_with_options(self, runner, mock_unity_response):
"""Test scene hierarchy with options."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"scene", "hierarchy",
"--max-depth", "5",
"--include-transform"
])
assert result.exit_code == 0
def test_scene_active(self, runner, mock_unity_response):
"""Test scene active command."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["scene", "active"])
assert result.exit_code == 0
def test_scene_load(self, runner, mock_unity_response):
"""Test scene load command."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["scene", "load", "Assets/Scenes/Main.unity"])
assert result.exit_code == 0
def test_scene_save(self, runner, mock_unity_response):
"""Test scene save command."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["scene", "save"])
assert result.exit_code == 0
def test_scene_create(self, runner, mock_unity_response):
"""Test scene create command."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["scene", "create", "NewLevel"])
assert result.exit_code == 0
def test_scene_screenshot(self, runner, mock_unity_response):
"""Test scene screenshot command."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["scene", "screenshot", "--filename", "test"])
assert result.exit_code == 0
# =============================================================================
# Asset Command Tests
# =============================================================================
class TestAssetCommands:
"""Tests for Asset CLI commands."""
def test_asset_search(self, runner, mock_unity_response):
"""Test asset search command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["asset", "search", "*.prefab"])
assert result.exit_code == 0
def test_asset_info(self, runner, mock_unity_response):
"""Test asset info command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["asset", "info", "Assets/Materials/Red.mat"])
assert result.exit_code == 0
def test_asset_create(self, runner, mock_unity_response):
"""Test asset create command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["asset", "create", "Assets/Materials/New.mat", "Material"])
assert result.exit_code == 0
def test_asset_delete(self, runner, mock_unity_response):
"""Test asset delete command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["asset", "delete", "Assets/Old.mat", "--force"])
assert result.exit_code == 0
def test_asset_duplicate(self, runner, mock_unity_response):
"""Test asset duplicate command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"asset", "duplicate",
"Assets/Materials/Red.mat",
"Assets/Materials/RedCopy.mat"
])
assert result.exit_code == 0
def test_asset_move(self, runner, mock_unity_response):
"""Test asset move command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"asset", "move",
"Assets/Old/Mat.mat",
"Assets/New/Mat.mat"
])
assert result.exit_code == 0
def test_asset_mkdir(self, runner, mock_unity_response):
"""Test asset mkdir command."""
with patch("cli.commands.asset.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["asset", "mkdir", "Assets/NewFolder"])
assert result.exit_code == 0
# =============================================================================
# Editor Command Tests
# =============================================================================
class TestEditorCommands:
"""Tests for Editor CLI commands."""
def test_editor_play(self, runner, mock_unity_response):
"""Test editor play command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "play"])
assert result.exit_code == 0
def test_editor_pause(self, runner, mock_unity_response):
"""Test editor pause command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "pause"])
assert result.exit_code == 0
def test_editor_stop(self, runner, mock_unity_response):
"""Test editor stop command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "stop"])
assert result.exit_code == 0
def test_editor_console(self, runner, mock_unity_response):
"""Test editor console command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "console"])
assert result.exit_code == 0
def test_editor_console_clear(self, runner, mock_unity_response):
"""Test editor console clear command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "console", "--clear"])
assert result.exit_code == 0
def test_editor_add_tag(self, runner, mock_unity_response):
"""Test editor add-tag command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "add-tag", "Enemy"])
assert result.exit_code == 0
def test_editor_add_layer(self, runner, mock_unity_response):
"""Test editor add-layer command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["editor", "add-layer", "Interactable"])
assert result.exit_code == 0
def test_editor_menu(self, runner, mock_unity_response):
"""Test editor menu command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "menu", "File/Save"])
assert result.exit_code == 0
def test_editor_tests(self, runner, mock_unity_response):
"""Test editor tests command."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["editor", "tests", "--mode", "EditMode"])
assert result.exit_code == 0
# =============================================================================
# Prefab Command Tests
# =============================================================================
class TestPrefabCommands:
"""Tests for Prefab CLI commands."""
def test_prefab_open(self, runner, mock_unity_response):
"""Test prefab open command."""
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["prefab", "open", "Assets/Prefabs/Player.prefab"])
assert result.exit_code == 0
def test_prefab_close(self, runner, mock_unity_response):
"""Test prefab close command."""
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["prefab", "close"])
assert result.exit_code == 0
def test_prefab_save(self, runner, mock_unity_response):
"""Test prefab save command."""
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["prefab", "save"])
assert result.exit_code == 0
def test_prefab_create(self, runner, mock_unity_response):
"""Test prefab create command."""
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"prefab", "create", "Player", "Assets/Prefabs/Player.prefab"
])
assert result.exit_code == 0
# =============================================================================
# Material Command Tests
# =============================================================================
class TestMaterialCommands:
"""Tests for Material CLI commands."""
def test_material_info(self, runner, mock_unity_response):
"""Test material info command."""
with patch("cli.commands.material.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["material", "info", "Assets/Materials/Red.mat"])
assert result.exit_code == 0
def test_material_create(self, runner, mock_unity_response):
"""Test material create command."""
with patch("cli.commands.material.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["material", "create", "Assets/Materials/New.mat"])
assert result.exit_code == 0
def test_material_set_color(self, runner, mock_unity_response):
"""Test material set-color command."""
with patch("cli.commands.material.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"material", "set-color", "Assets/Materials/Red.mat",
"1", "0", "0"
])
assert result.exit_code == 0
def test_material_set_property(self, runner, mock_unity_response):
"""Test material set-property command."""
with patch("cli.commands.material.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"material", "set-property", "Assets/Materials/Mat.mat",
"_Metallic", "0.5"
])
assert result.exit_code == 0
def test_material_assign(self, runner, mock_unity_response):
"""Test material assign command."""
with patch("cli.commands.material.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"material", "assign", "Assets/Materials/Red.mat", "Cube"
])
assert result.exit_code == 0
# =============================================================================
# Script Command Tests
# =============================================================================
class TestScriptCommands:
"""Tests for Script CLI commands."""
def test_script_create(self, runner, mock_unity_response):
"""Test script create command."""
with patch("cli.commands.script.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["script", "create", "PlayerController"])
assert result.exit_code == 0
def test_script_create_with_options(self, runner, mock_unity_response):
"""Test script create with options."""
with patch("cli.commands.script.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"script", "create", "EnemyData",
"--type", "ScriptableObject",
"--namespace", "MyGame"
])
assert result.exit_code == 0
def test_script_read(self, runner):
"""Test script read command."""
mock_response = {
"success": True,
"data": {"content": "using UnityEngine;\n\npublic class Test {}"}
}
with patch("cli.commands.script.run_command", return_value=mock_response):
result = runner.invoke(
cli, ["script", "read", "Assets/Scripts/Test.cs"])
assert result.exit_code == 0
def test_script_delete(self, runner, mock_unity_response):
"""Test script delete command."""
with patch("cli.commands.script.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["script", "delete", "Assets/Scripts/Old.cs", "--force"])
assert result.exit_code == 0
# =============================================================================
# Global Options Tests
# =============================================================================
class TestGlobalOptions:
"""Tests for global CLI options."""
def test_custom_host(self, runner, mock_unity_response):
"""Test custom host option."""
with patch("cli.main.run_check_connection", return_value=True):
with patch("cli.main.run_list_instances", return_value={"instances": []}):
result = runner.invoke(
cli, ["--host", "192.168.1.100", "status"])
assert result.exit_code == 0
def test_custom_port(self, runner, mock_unity_response):
"""Test custom port option."""
with patch("cli.main.run_check_connection", return_value=True):
with patch("cli.main.run_list_instances", return_value={"instances": []}):
result = runner.invoke(cli, ["--port", "9090", "status"])
assert result.exit_code == 0
def test_json_format(self, runner, mock_unity_response):
"""Test JSON output format."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["--format", "json", "scene", "active"])
assert result.exit_code == 0
def test_table_format(self, runner, mock_unity_response):
"""Test table output format."""
with patch("cli.commands.scene.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["--format", "table", "scene", "active"])
assert result.exit_code == 0
def test_timeout_option(self, runner, mock_unity_response):
"""Test timeout option."""
with patch("cli.main.run_check_connection", return_value=True):
with patch("cli.main.run_list_instances", return_value={"instances": []}):
result = runner.invoke(cli, ["--timeout", "60", "status"])
assert result.exit_code == 0
# =============================================================================
# Error Handling Tests
# =============================================================================
class TestErrorHandling:
"""Tests for error handling."""
def test_connection_error_handling(self, runner):
"""Test connection error is handled gracefully."""
with patch("cli.commands.scene.run_command", side_effect=UnityConnectionError("Connection failed")):
result = runner.invoke(cli, ["scene", "hierarchy"])
assert result.exit_code == 1
assert "Connection failed" in result.output or "Error" in result.output
def test_invalid_json_params(self, runner):
"""Test invalid JSON parameters are handled."""
result = runner.invoke(cli, [
"component", "modify", "Player", "Rigidbody",
"--properties", "not valid json"
])
assert result.exit_code == 1
assert "Invalid JSON" in result.output
def test_missing_required_argument(self, runner):
"""Test missing required argument."""
result = runner.invoke(cli, ["gameobject", "find"])
assert result.exit_code != 0
assert "Missing argument" in result.output
# =============================================================================
# Integration-style Tests (with mocked responses)
# =============================================================================
class TestIntegration:
"""Integration-style tests with realistic response data."""
def test_full_gameobject_workflow(self, runner):
"""Test a full GameObject workflow."""
create_response = {
"success": True,
"message": "GameObject created",
"data": {"instanceID": -12345, "name": "TestObject"}
}
modify_response = {
"success": True,
"message": "GameObject modified"
}
delete_response = {
"success": True,
"message": "GameObject deleted"
}
# Create
with patch("cli.commands.gameobject.run_command", return_value=create_response):
result = runner.invoke(
cli, ["gameobject", "create", "TestObject", "--primitive", "Cube"])
assert result.exit_code == 0
assert "Created" in result.output
# Modify
with patch("cli.commands.gameobject.run_command", return_value=modify_response):
result = runner.invoke(
cli, ["gameobject", "modify", "TestObject", "--position", "0", "5", "0"])
assert result.exit_code == 0
# Delete
with patch("cli.commands.gameobject.run_command", return_value=delete_response):
result = runner.invoke(
cli, ["gameobject", "delete", "TestObject", "--force"])
assert result.exit_code == 0
assert "Deleted" in result.output
def test_scene_hierarchy_with_data(self, runner):
"""Test scene hierarchy with realistic data."""
hierarchy_response = {
"success": True,
"data": {
"nodes": [
{"name": "Main Camera", "instanceID": -100, "childCount": 0},
{"name": "Directional Light",
"instanceID": -200, "childCount": 0},
{"name": "Player", "instanceID": -300, "childCount": 2},
]
}
}
with patch("cli.commands.scene.run_command", return_value=hierarchy_response):
result = runner.invoke(cli, ["scene", "hierarchy"])
assert result.exit_code == 0
def test_find_gameobjects_with_results(self, runner):
"""Test finding GameObjects with results."""
find_response = {
"success": True,
"message": "Found 3 GameObjects",
"data": {
"instanceIDs": [-100, -200, -300],
"count": 3,
"hasMore": False
}
}
with patch("cli.commands.gameobject.run_command", return_value=find_response):
result = runner.invoke(cli, ["gameobject", "find", "Camera"])
assert result.exit_code == 0
# =============================================================================
# Instance Command Tests
# =============================================================================
class TestInstanceCommands:
"""Tests for instance management commands."""
def test_instance_list(self, runner):
"""Test listing Unity instances."""
mock_instances = {
"instances": [
{"project": "TestProject", "hash": "abc123",
"unity_version": "2022.3.10f1", "session_id": "sess-1"}
]
}
with patch("cli.commands.instance.run_list_instances", return_value=mock_instances):
result = runner.invoke(cli, ["instance", "list"])
assert result.exit_code == 0
assert "TestProject" in result.output
def test_instance_set(self, runner, mock_unity_response):
"""Test setting active instance."""
with patch("cli.commands.instance.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["instance", "set", "TestProject@abc123"])
assert result.exit_code == 0
def test_instance_current(self, runner):
"""Test showing current instance."""
result = runner.invoke(cli, ["instance", "current"])
assert result.exit_code == 0
# Should show info message about no instance set
assert "instance" in result.output.lower()
# =============================================================================
# Shader Command Tests
# =============================================================================
class TestShaderCommands:
"""Tests for shader commands."""
def test_shader_read(self, runner):
"""Test reading a shader."""
read_response = {
"success": True,
"data": {"contents": "Shader \"Custom/Test\" { ... }"}
}
with patch("cli.commands.shader.run_command", return_value=read_response):
result = runner.invoke(
cli, ["shader", "read", "Assets/Shaders/Test.shader"])
assert result.exit_code == 0
def test_shader_create(self, runner, mock_unity_response):
"""Test creating a shader."""
with patch("cli.commands.shader.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["shader", "create", "NewShader", "--path", "Assets/Shaders"])
assert result.exit_code == 0
def test_shader_delete(self, runner, mock_unity_response):
"""Test deleting a shader."""
with patch("cli.commands.shader.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["shader", "delete", "Assets/Shaders/Old.shader", "--force"])
assert result.exit_code == 0
# =============================================================================
# VFX Command Tests
# =============================================================================
class TestVfxCommands:
"""Tests for VFX commands."""
def test_vfx_particle_info(self, runner, mock_unity_response):
"""Test getting particle system info."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["vfx", "particle", "info", "Fire"])
assert result.exit_code == 0
def test_vfx_particle_play(self, runner, mock_unity_response):
"""Test playing a particle system."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["vfx", "particle", "play", "Fire"])
assert result.exit_code == 0
def test_vfx_particle_stop(self, runner, mock_unity_response):
"""Test stopping a particle system."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["vfx", "particle", "stop", "Fire"])
assert result.exit_code == 0
def test_vfx_line_info(self, runner, mock_unity_response):
"""Test getting line renderer info."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["vfx", "line", "info", "LaserBeam"])
assert result.exit_code == 0
def test_vfx_line_create_line(self, runner, mock_unity_response):
"""Test creating a line."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["vfx", "line", "create-line", "Line", "--start", "0", "0", "0", "--end", "10", "5", "0"])
assert result.exit_code == 0
def test_vfx_line_create_circle(self, runner, mock_unity_response):
"""Test creating a circle."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["vfx", "line", "create-circle", "Circle", "--radius", "5"])
assert result.exit_code == 0
def test_vfx_trail_info(self, runner, mock_unity_response):
"""Test getting trail renderer info."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["vfx", "trail", "info", "Trail"])
assert result.exit_code == 0
def test_vfx_trail_set_time(self, runner, mock_unity_response):
"""Test setting trail time."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["vfx", "trail", "set-time", "Trail", "2.0"])
assert result.exit_code == 0
def test_vfx_raw(self, runner, mock_unity_response):
"""Test raw VFX action."""
with patch("cli.commands.vfx.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["vfx", "raw", "particle_set_main", "Fire", "--params", '{"duration": 5}'])
assert result.exit_code == 0
def test_vfx_raw_invalid_json(self, runner):
"""Test raw VFX action with invalid JSON."""
result = runner.invoke(
cli, ["vfx", "raw", "particle_set_main", "Fire", "--params", "invalid json"])
assert result.exit_code == 1
assert "Invalid JSON" in result.output
# =============================================================================
# Batch Command Tests
# =============================================================================
class TestBatchCommands:
"""Tests for batch commands."""
def test_batch_inline(self, runner, mock_unity_response):
"""Test inline batch execution."""
batch_response = {
"success": True,
"data": {"results": [{"success": True}]}
}
with patch("cli.commands.batch.run_command", return_value=batch_response):
result = runner.invoke(
cli, ["batch", "inline", '[{"tool": "manage_scene", "params": {"action": "get_active"}}]'])
assert result.exit_code == 0
def test_batch_inline_invalid_json(self, runner):
"""Test inline batch with invalid JSON."""
result = runner.invoke(cli, ["batch", "inline", "not valid json"])
assert result.exit_code == 1
assert "Invalid JSON" in result.output
def test_batch_template(self, runner):
"""Test generating batch template."""
result = runner.invoke(cli, ["batch", "template"])
assert result.exit_code == 0
# Template should be valid JSON
import json
template = json.loads(result.output)
assert isinstance(template, list)
assert len(template) > 0
assert "tool" in template[0]
def test_batch_run_file(self, runner, tmp_path, mock_unity_response):
"""Test running batch from file."""
# Create a temp batch file
batch_file = tmp_path / "commands.json"
batch_file.write_text(
'[{"tool": "manage_scene", "params": {"action": "get_active"}}]')
batch_response = {
"success": True,
"data": {"results": [{"success": True}]}
}
with patch("cli.commands.batch.run_command", return_value=batch_response):
result = runner.invoke(cli, ["batch", "run", str(batch_file)])
assert result.exit_code == 0
# =============================================================================
# Enhanced Editor Command Tests
# =============================================================================
class TestEditorEnhancedCommands:
"""Tests for new editor subcommands."""
def test_editor_refresh(self, runner, mock_unity_response):
"""Test editor refresh."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "refresh"])
assert result.exit_code == 0
def test_editor_refresh_with_compile(self, runner, mock_unity_response):
"""Test editor refresh with compile flag."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "refresh", "--compile"])
assert result.exit_code == 0
def test_editor_custom_tool(self, runner, mock_unity_response):
"""Test executing custom tool."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, ["editor", "custom-tool", "MyTool"])
assert result.exit_code == 0
def test_editor_custom_tool_with_params(self, runner, mock_unity_response):
"""Test executing custom tool with parameters."""
with patch("cli.commands.editor.run_command", return_value=mock_unity_response):
result = runner.invoke(
cli, ["editor", "custom-tool", "BuildTool", "--params", '{"target": "Android"}'])
assert result.exit_code == 0
def test_editor_custom_tool_invalid_json(self, runner):
"""Test custom tool with invalid JSON params."""
result = runner.invoke(
cli, ["editor", "custom-tool", "MyTool", "--params", "bad json"])
assert result.exit_code == 1
assert "Invalid JSON" in result.output
def test_editor_tests_async(self, runner):
"""Test async test execution."""
async_response = {
"success": True,
"data": {"job_id": "test-job-123", "status": "running"}
}
with patch("cli.commands.editor.run_command", return_value=async_response):
result = runner.invoke(cli, ["editor", "tests", "--async"])
assert result.exit_code == 0
assert "test-job-123" in result.output
def test_editor_poll_test(self, runner):
"""Test polling test job."""
poll_response = {
"success": True,
"data": {
"job_id": "test-job-123",
"status": "succeeded",
"result": {"summary": {"total": 10, "passed": 10, "failed": 0}}
}
}
with patch("cli.commands.editor.run_command", return_value=poll_response):
result = runner.invoke(
cli, ["editor", "poll-test", "test-job-123"])
assert result.exit_code == 0
# =============================================================================
# Code Search Tests
# =============================================================================
class TestCodeSearchCommand:
"""Tests for code search command."""
def test_code_search(self, runner):
"""Test code search."""
# Mock manage_script response with file contents
read_response = {
"status": "success",
"result": {
"success": True,
"data": {
"contents": "using UnityEngine;\n\npublic class Player : MonoBehaviour\n{\n void Start() {}\n}\n",
"contentsEncoded": False,
}
}
}
with patch("cli.commands.code.run_command", return_value=read_response):
result = runner.invoke(
cli, ["code", "search", "class.*Player", "Assets/Scripts/Player.cs"])
assert result.exit_code == 0
assert "Line 3" in result.output
assert "class Player" in result.output
def test_code_search_no_matches(self, runner):
"""Test code search with no matches."""
read_response = {
"status": "success",
"result": {
"success": True,
"data": {
"contents": "using UnityEngine;\n\npublic class Test : MonoBehaviour {}\n",
"contentsEncoded": False,
}
}
}
with patch("cli.commands.code.run_command", return_value=read_response):
result = runner.invoke(
cli, ["code", "search", "nonexistent", "Assets/Scripts/Test.cs"])
assert result.exit_code == 0
assert "No matches" in result.output
def test_code_search_with_options(self, runner):
"""Test code search with options."""
read_response = {
"status": "success",
"result": {
"success": True,
"data": {
"contents": "// TODO: implement this\n// FIXME: bug here\nclass Test {}\n",
"contentsEncoded": False,
}
}
}
with patch("cli.commands.code.run_command", return_value=read_response):
result = runner.invoke(
cli, ["code", "search", "TODO", "Assets/Utils.cs", "--max-results", "100", "--case-sensitive"])
assert result.exit_code == 0
assert "Line 1" in result.output
# =============================================================================
# Texture Command Tests
# =============================================================================
class TestTextureCommands:
"""Tests for Texture CLI commands."""
def test_texture_create_basic(self, runner, mock_unity_response):
"""Test basic texture create command."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "create", "Assets/Textures/Red.png",
"--color", "[255,0,0,255]"
])
assert result.exit_code == 0
def test_texture_create_with_hex_color(self, runner, mock_unity_response):
"""Test texture create with hex color."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "create", "Assets/Textures/Blue.png",
"--color", "#0000FF"
])
assert result.exit_code == 0
def test_texture_create_with_pattern(self, runner, mock_unity_response):
"""Test texture create with pattern."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "create", "Assets/Textures/Checker.png",
"--pattern", "checkerboard",
"--width", "128",
"--height", "128"
])
assert result.exit_code == 0
def test_texture_create_with_import_settings(self, runner, mock_unity_response):
"""Test texture create with import settings."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "create", "Assets/Textures/Sprite.png",
"--import-settings", '{"texture_type": "sprite", "filter_mode": "point"}'
])
assert result.exit_code == 0
def test_texture_sprite_basic(self, runner, mock_unity_response):
"""Test sprite create command."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "sprite", "Assets/Sprites/Player.png"
])
assert result.exit_code == 0
def test_texture_sprite_with_color(self, runner, mock_unity_response):
"""Test sprite create with solid color."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "sprite", "Assets/Sprites/Green.png",
"--color", "[0,255,0,255]"
])
assert result.exit_code == 0
def test_texture_sprite_with_pattern(self, runner, mock_unity_response):
"""Test sprite create with pattern."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "sprite", "Assets/Sprites/Dots.png",
"--pattern", "dots",
"--ppu", "50"
])
assert result.exit_code == 0
def test_texture_sprite_with_custom_pivot(self, runner, mock_unity_response):
"""Test sprite create with custom pivot."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "sprite", "Assets/Sprites/Custom.png",
"--pivot", "[0.25,0.75]"
])
assert result.exit_code == 0
def test_texture_modify(self, runner, mock_unity_response):
"""Test texture modify command."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "modify", "Assets/Textures/Test.png",
"--set-pixels", '{"x":0,"y":0,"width":10,"height":10,"color":[255,0,0,255]}'
])
assert result.exit_code == 0
def test_texture_delete(self, runner, mock_unity_response):
"""Test texture delete command."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "delete", "Assets/Textures/Old.png", "--force"
])
assert result.exit_code == 0
def test_texture_create_invalid_json(self, runner):
"""Test texture create with invalid JSON."""
result = runner.invoke(cli, [
"texture", "create", "Assets/Test.png",
"--import-settings", "not valid json"
])
assert result.exit_code == 1
assert "Invalid JSON" in result.output
def test_texture_sprite_color_and_pattern_precedence(self, runner, mock_unity_response):
"""Test that color takes precedence over default pattern in sprite command."""
with patch("cli.commands.texture.run_command", return_value=mock_unity_response):
result = runner.invoke(cli, [
"texture", "sprite", "Assets/Sprites/Solid.png",
"--color", "[255,0,0,255]"
])
assert result.exit_code == 0
if __name__ == "__main__":
pytest.main([__file__, "-v"])