1052 lines
44 KiB
Python
1052 lines
44 KiB
Python
|
|
"""
|
||
|
|
Characterization tests for CLI Commands domain (Server-Side Tools).
|
||
|
|
|
||
|
|
This test suite captures CURRENT behavior of CLI command modules without refactoring.
|
||
|
|
Tests are designed to identify common patterns and boilerplate across command implementations.
|
||
|
|
|
||
|
|
Domain: /Server/src/cli/commands/
|
||
|
|
Modules sampled:
|
||
|
|
- prefab.py (216 lines) - Stage/hierarchy operations, create from GameObject
|
||
|
|
- component.py (213 lines) - Add, remove, set properties on components
|
||
|
|
- material.py (269 lines) - Material info, creation, property assignment
|
||
|
|
- asset.py (partial) - Asset search, info, create
|
||
|
|
- animation.py (partial) - Animation state/parameter control
|
||
|
|
|
||
|
|
Common patterns identified:
|
||
|
|
1. All commands follow try/except with UnityConnectionError handling
|
||
|
|
2. All commands call run_command() with params dict and config
|
||
|
|
3. All commands build params dict with "action" key first
|
||
|
|
4. All commands use format_output() for results
|
||
|
|
5. Success messages use print_success() when result.get("success")
|
||
|
|
6. JSON parsing appears 5+ times across modules (inline try/except blocks)
|
||
|
|
7. Search method parameter uses click.Choice() - repeated across 4+ modules
|
||
|
|
8. Confirmation dialogs use click.confirm() directly (not extracted)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import patch, MagicMock, call
|
||
|
|
from click.testing import CliRunner
|
||
|
|
|
||
|
|
from cli.commands.prefab import prefab
|
||
|
|
from cli.commands.component import component
|
||
|
|
from cli.commands.material import material
|
||
|
|
from cli.commands.asset import asset
|
||
|
|
from cli.utils.connection import UnityConnectionError
|
||
|
|
from cli.utils.config import CLIConfig
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Fixtures - Shared Test Setup
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def runner():
|
||
|
|
"""CLI test runner for all command tests."""
|
||
|
|
return CliRunner()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_config():
|
||
|
|
"""Standard mock configuration for CLI commands."""
|
||
|
|
return CLIConfig(
|
||
|
|
host="127.0.0.1",
|
||
|
|
port=8080,
|
||
|
|
timeout=30,
|
||
|
|
format="text",
|
||
|
|
unity_instance=None,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_success_response():
|
||
|
|
"""Standard successful command response."""
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"message": "Operation successful",
|
||
|
|
"data": {"result": "ok"}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_failure_response():
|
||
|
|
"""Standard failure command response."""
|
||
|
|
return {
|
||
|
|
"success": False,
|
||
|
|
"error": "Operation failed",
|
||
|
|
"message": "Something went wrong"
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Command Structure and Parameter Building
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestCommandParameterBuilding:
|
||
|
|
"""Verify how commands build parameter dictionaries.
|
||
|
|
|
||
|
|
Current behavior: All commands build params with 'action' key first,
|
||
|
|
then conditionally add optional parameters.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_open_builds_action_and_path_params(self, runner, mock_config):
|
||
|
|
"""Test prefab open command parameter construction.
|
||
|
|
|
||
|
|
Captures: All prefab commands use 'action' key with camelCase param names
|
||
|
|
like 'prefabPath', 'saveBeforeClose', 'force'.
|
||
|
|
"""
|
||
|
|
mock_response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=mock_response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["open", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
# Verify run_command was called with correct structure
|
||
|
|
mock_run.assert_called_once()
|
||
|
|
args = mock_run.call_args
|
||
|
|
assert args[0][0] == "manage_prefabs"
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["action"] == "open_stage"
|
||
|
|
assert params["prefabPath"] == "Assets/Prefabs/Test.prefab"
|
||
|
|
|
||
|
|
def test_component_add_with_optional_properties(self, runner, mock_config):
|
||
|
|
"""Test component add builds action, required, and optional params.
|
||
|
|
|
||
|
|
Captures: Conditional parameter inclusion pattern - if search_method
|
||
|
|
is provided, add to params; if properties JSON provided, parse and add.
|
||
|
|
"""
|
||
|
|
mock_response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=mock_response) as mock_run:
|
||
|
|
# Without optional params
|
||
|
|
runner.invoke(component, ["add", "Player", "Rigidbody"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert "searchMethod" not in params
|
||
|
|
assert "properties" not in params
|
||
|
|
assert params["action"] == "add"
|
||
|
|
assert params["target"] == "Player"
|
||
|
|
assert params["componentType"] == "Rigidbody"
|
||
|
|
|
||
|
|
def test_material_set_color_converts_floats_to_list(self, runner, mock_config):
|
||
|
|
"""Test material command converts multiple float args to color array.
|
||
|
|
|
||
|
|
Captures: Material commands convert 4 float args into [r, g, b, a] list
|
||
|
|
in params dict before calling run_command.
|
||
|
|
"""
|
||
|
|
mock_response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=mock_response) as mock_run:
|
||
|
|
runner.invoke(material, ["set-color", "Assets/Mat.mat", "1.0", "0.5", "0.25", "1.0"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["color"] == [1.0, 0.5, 0.25, 1.0]
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: JSON Parsing and Type Coercion
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestJSONParsingPattern:
|
||
|
|
"""Verify how commands parse JSON parameters.
|
||
|
|
|
||
|
|
Current behavior: Each module has inline try/except blocks for JSON parsing,
|
||
|
|
duplicated across component.py, material.py, and asset.py modules.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_component_add_parses_json_properties(self, runner, mock_config):
|
||
|
|
"""Test component add parses JSON properties parameter.
|
||
|
|
|
||
|
|
Captures: Try json.loads() on properties string, catch JSONDecodeError,
|
||
|
|
print_error and sys.exit(1) on failure.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command") as mock_run:
|
||
|
|
# Valid JSON
|
||
|
|
runner.invoke(component, ["add", "Player", "Rigidbody", "-p", '{"mass": 5.0}'])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["properties"] == {"mass": 5.0}
|
||
|
|
|
||
|
|
def test_component_add_rejects_invalid_json(self, runner, mock_config):
|
||
|
|
"""Test component add exits with error on invalid JSON.
|
||
|
|
|
||
|
|
Captures: When json.loads() fails, print_error is called and sys.exit(1)
|
||
|
|
is invoked, resulting in exit_code != 0.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
result = runner.invoke(component, ["add", "Player", "Rigidbody", "-p", "not json"])
|
||
|
|
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert "Invalid JSON" in result.output
|
||
|
|
|
||
|
|
def test_material_set_property_tries_json_then_float_then_string(self, runner, mock_config):
|
||
|
|
"""Test material set-property uses fallback parsing strategy.
|
||
|
|
|
||
|
|
Captures: Attempt json.loads() first, then float(), then keep as string.
|
||
|
|
This pattern appears in component.py and material.py independently.
|
||
|
|
"""
|
||
|
|
mock_response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=mock_response) as mock_run:
|
||
|
|
# Test float value
|
||
|
|
runner.invoke(material, ["set-property", "Mat.mat", "_Metallic", "0.5"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["value"] == 0.5
|
||
|
|
assert isinstance(params["value"], float)
|
||
|
|
|
||
|
|
def test_material_set_property_parses_json_value(self, runner, mock_config):
|
||
|
|
"""Test material set-property accepts JSON values for complex types.
|
||
|
|
|
||
|
|
Captures: JSON parsing tries first, enabling complex types like vectors.
|
||
|
|
"""
|
||
|
|
mock_response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=mock_response) as mock_run:
|
||
|
|
runner.invoke(material, ["set-property", "Mat.mat", "_Color", "[1, 0, 0, 1]"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["value"] == [1, 0, 0, 1]
|
||
|
|
|
||
|
|
def test_material_set_property_keeps_string_fallback(self, runner, mock_config):
|
||
|
|
"""Test material set-property falls back to string for non-numeric values.
|
||
|
|
|
||
|
|
Captures: If json.loads() and float() both fail, use original string.
|
||
|
|
"""
|
||
|
|
mock_response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=mock_response) as mock_run:
|
||
|
|
runner.invoke(material, ["set-property", "Mat.mat", "_MainTex", "Assets/Tex.png"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["value"] == "Assets/Tex.png"
|
||
|
|
assert isinstance(params["value"], str)
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Error Handling and Exit Codes
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestErrorHandlingPattern:
|
||
|
|
"""Verify consistent error handling across command modules.
|
||
|
|
|
||
|
|
Current behavior: All commands have try/except wrapping run_command(),
|
||
|
|
catch UnityConnectionError, print_error(), and sys.exit(1).
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_open_catches_unity_connection_error(self, runner, mock_config):
|
||
|
|
"""Test prefab open handles connection errors gracefully.
|
||
|
|
|
||
|
|
Captures: try/except around run_command(), catches UnityConnectionError,
|
||
|
|
calls print_error(str(e)), then sys.exit(1).
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", side_effect=UnityConnectionError("Connection failed")):
|
||
|
|
result = runner.invoke(prefab, ["open", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
assert result.exit_code == 1
|
||
|
|
assert "Connection failed" in result.output
|
||
|
|
|
||
|
|
def test_component_add_catches_unity_connection_error(self, runner, mock_config):
|
||
|
|
"""Test component add handles connection errors.
|
||
|
|
|
||
|
|
Captures: Same error handling pattern repeated in component.py.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", side_effect=UnityConnectionError("Connection lost")):
|
||
|
|
result = runner.invoke(component, ["add", "Player", "Rigidbody"])
|
||
|
|
|
||
|
|
assert result.exit_code == 1
|
||
|
|
|
||
|
|
def test_material_info_handles_connection_failure(self, runner, mock_config):
|
||
|
|
"""Test material info handles connection errors.
|
||
|
|
|
||
|
|
Captures: Pattern repeats across all material commands.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", side_effect=UnityConnectionError("Disconnected")):
|
||
|
|
result = runner.invoke(material, ["info", "Assets/Mat.mat"])
|
||
|
|
|
||
|
|
assert result.exit_code == 1
|
||
|
|
|
||
|
|
def test_asset_search_handles_connection_error(self, runner, mock_config):
|
||
|
|
"""Test asset search handles connection errors.
|
||
|
|
|
||
|
|
Captures: Pattern found in asset.py as well.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.asset.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.asset.run_command", side_effect=UnityConnectionError("Timeout")):
|
||
|
|
result = runner.invoke(asset, ["search", "*.prefab"])
|
||
|
|
|
||
|
|
assert result.exit_code == 1
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Success Response Handling
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestSuccessResponseHandling:
|
||
|
|
"""Verify how commands handle successful responses.
|
||
|
|
|
||
|
|
Current behavior: Commands check result.get("success"), then call
|
||
|
|
print_success() with context-specific message.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_open_shows_success_message_on_success(self, runner, mock_config):
|
||
|
|
"""Test prefab open shows success message when result succeeds.
|
||
|
|
|
||
|
|
Captures: if result.get("success"): print_success(formatted message)
|
||
|
|
"""
|
||
|
|
response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response):
|
||
|
|
result = runner.invoke(prefab, ["open", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "Opened prefab" in result.output
|
||
|
|
assert "Assets/Prefabs/Test.prefab" in result.output
|
||
|
|
|
||
|
|
def test_prefab_close_shows_context_appropriate_success_message(self, runner, mock_config):
|
||
|
|
"""Test prefab close shows appropriate success message.
|
||
|
|
|
||
|
|
Captures: Different commands show different success messages based on action.
|
||
|
|
"""
|
||
|
|
response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response):
|
||
|
|
result = runner.invoke(prefab, ["close"])
|
||
|
|
|
||
|
|
assert "Closed prefab stage" in result.output
|
||
|
|
|
||
|
|
def test_component_add_shows_action_context_in_success(self, runner, mock_config):
|
||
|
|
"""Test component add includes component type and target in success message.
|
||
|
|
|
||
|
|
Captures: Success messages include relevant context (component type, target).
|
||
|
|
"""
|
||
|
|
response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response):
|
||
|
|
result = runner.invoke(component, ["add", "Player", "Rigidbody"])
|
||
|
|
|
||
|
|
assert "Added Rigidbody" in result.output
|
||
|
|
assert "Player" in result.output
|
||
|
|
|
||
|
|
def test_material_create_includes_path_in_success_message(self, runner, mock_config):
|
||
|
|
"""Test material create includes asset path in success message.
|
||
|
|
|
||
|
|
Captures: Path parameters are echoed back in success messages.
|
||
|
|
"""
|
||
|
|
response = {"success": True, "data": {}}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response):
|
||
|
|
result = runner.invoke(material, ["create", "Assets/Materials/New.mat"])
|
||
|
|
|
||
|
|
assert "Created material" in result.output
|
||
|
|
assert "Assets/Materials/New.mat" in result.output
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Output Formatting
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestOutputFormattingPattern:
|
||
|
|
"""Verify how commands format and display responses.
|
||
|
|
|
||
|
|
Current behavior: All commands call format_output(result, config.format)
|
||
|
|
and echo result with click.echo().
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_command_uses_format_output_with_config_format(self, runner, mock_config):
|
||
|
|
"""Test command passes config.format to format_output.
|
||
|
|
|
||
|
|
Captures: All commands call format_output(result, config.format) where
|
||
|
|
config comes from get_config().
|
||
|
|
"""
|
||
|
|
response = {"success": True, "data": {"info": "value"}}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response):
|
||
|
|
with patch("cli.commands.prefab.format_output", return_value="formatted") as mock_format:
|
||
|
|
runner.invoke(prefab, ["open", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
mock_format.assert_called()
|
||
|
|
call_args = mock_format.call_args
|
||
|
|
assert call_args[0][1] == "text" # config.format
|
||
|
|
|
||
|
|
def test_prefab_info_formats_output_from_wrapped_result(self, runner, mock_config):
|
||
|
|
"""Test prefab info handles wrapped response structure.
|
||
|
|
|
||
|
|
Captures: Some commands unwrap response data with result.get("result", result)
|
||
|
|
before accessing .get("success") and .get("data").
|
||
|
|
"""
|
||
|
|
response = {
|
||
|
|
"result": {
|
||
|
|
"success": True,
|
||
|
|
"data": {
|
||
|
|
"assetPath": "Assets/Prefabs/Test.prefab",
|
||
|
|
"prefabType": "Regular",
|
||
|
|
"guid": "abc123"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response):
|
||
|
|
result = runner.invoke(prefab, ["info", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
# Compact output shows parsed data
|
||
|
|
assert "Prefab:" in result.output or "Assets/Prefabs/Test.prefab" in result.output
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Search Method Parameter
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestSearchMethodParameter:
|
||
|
|
"""Verify repeated search method parameter implementation.
|
||
|
|
|
||
|
|
Current behavior: search_method parameter appears in component.py, material.py,
|
||
|
|
and other modules with identical click.Choice() definitions.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_component_add_accepts_search_method_parameter(self, runner, mock_config):
|
||
|
|
"""Test component add supports search_method choices.
|
||
|
|
|
||
|
|
Captures: click.Choice(["by_id", "by_name", "by_path"]) appears in
|
||
|
|
component.py add() and remove() and multiple material commands.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(component, ["add", "Player", "Rigidbody", "--search-method", "by_id"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["searchMethod"] == "by_id"
|
||
|
|
|
||
|
|
def test_component_add_rejects_invalid_search_method(self, runner, mock_config):
|
||
|
|
"""Test component add validates search_method choices.
|
||
|
|
|
||
|
|
Captures: Click automatically validates against Choice() options.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
result = runner.invoke(component, ["add", "Player", "Rigidbody", "--search-method", "invalid"])
|
||
|
|
|
||
|
|
# Click exits with code 2 for usage errors
|
||
|
|
assert result.exit_code == 2
|
||
|
|
assert "invalid" in result.output.lower()
|
||
|
|
|
||
|
|
def test_material_assign_has_extended_search_methods(self, runner, mock_config):
|
||
|
|
"""Test material assign supports additional search methods.
|
||
|
|
|
||
|
|
Captures: material.py has ["by_name", "by_path", "by_tag", "by_layer", "by_component"]
|
||
|
|
which is different from component.py's list - DUPLICATION with variation.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(material, ["assign", "Assets/Mat.mat", "Cube", "--search-method", "by_tag"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["searchMethod"] == "by_tag"
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Confirmation Dialogs
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestConfirmationDialogPattern:
|
||
|
|
"""Verify confirmation dialog usage across commands.
|
||
|
|
|
||
|
|
Current behavior: Some destructive commands use click.confirm() directly,
|
||
|
|
with --force flag to skip confirmation.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_component_remove_shows_confirmation_by_default(self, runner, mock_config):
|
||
|
|
"""Test component remove shows confirmation prompt.
|
||
|
|
|
||
|
|
Captures: click.confirm() is called directly in the command function
|
||
|
|
when force=False.
|
||
|
|
"""
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
# Simulate user declining confirmation
|
||
|
|
result = runner.invoke(component, ["remove", "Player", "Rigidbody"], input="n\n")
|
||
|
|
|
||
|
|
assert result.exit_code == 1
|
||
|
|
assert "Aborted" in result.output or "cancelled" in result.output.lower()
|
||
|
|
|
||
|
|
def test_component_remove_skips_confirmation_with_force_flag(self, runner, mock_config):
|
||
|
|
"""Test component remove skips confirmation when --force is set.
|
||
|
|
|
||
|
|
Captures: When force=True, no confirmation is prompted and run_command is called.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response):
|
||
|
|
result = runner.invoke(component, ["remove", "Player", "Rigidbody", "--force"])
|
||
|
|
|
||
|
|
# Command should proceed without confirmation
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Optional Parameter Handling
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestOptionalParameterHandling:
|
||
|
|
"""Verify how commands handle optional parameters.
|
||
|
|
|
||
|
|
Current behavior: Commands check each optional parameter and conditionally
|
||
|
|
add to params dict only if provided.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_close_includes_save_param_when_flag_set(self, runner, mock_config):
|
||
|
|
"""Test prefab close includes saveBeforeClose when --save is set.
|
||
|
|
|
||
|
|
Captures: Optional parameters are only added to params dict if flag is True.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["close", "--save"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["saveBeforeClose"] is True
|
||
|
|
|
||
|
|
def test_prefab_close_omits_save_param_when_flag_not_set(self, runner, mock_config):
|
||
|
|
"""Test prefab close omits saveBeforeClose when --save is not set.
|
||
|
|
|
||
|
|
Captures: Optional parameters are NOT included in params if not provided.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["close"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert "saveBeforeClose" not in params
|
||
|
|
|
||
|
|
def test_material_create_only_adds_properties_if_provided(self, runner, mock_config):
|
||
|
|
"""Test material create only includes properties parameter if specified.
|
||
|
|
|
||
|
|
Captures: Same optional parameter pattern as prefab commands.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(material, ["create", "Assets/Mat.mat"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert "properties" not in params
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Command Tool Name Resolution
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestCommandToolNameResolution:
|
||
|
|
"""Verify how commands resolve target tool names.
|
||
|
|
|
||
|
|
Current behavior: Each command module hardcodes the tool name passed to
|
||
|
|
run_command() - e.g., "manage_prefabs", "manage_components", "manage_material".
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_commands_use_manage_prefabs_tool(self, runner, mock_config):
|
||
|
|
"""Test all prefab commands call run_command with 'manage_prefabs'.
|
||
|
|
|
||
|
|
Captures: Tool name is hardcoded in each command function.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["open", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
assert args[0][0] == "manage_prefabs"
|
||
|
|
|
||
|
|
def test_component_commands_use_manage_components_tool(self, runner, mock_config):
|
||
|
|
"""Test component commands use 'manage_components' tool name.
|
||
|
|
|
||
|
|
Captures: Hardcoded tool name per module.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(component, ["add", "Player", "Rigidbody"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
assert args[0][0] == "manage_components"
|
||
|
|
|
||
|
|
def test_material_commands_use_manage_material_tool(self, runner, mock_config):
|
||
|
|
"""Test material commands use 'manage_material' tool name.
|
||
|
|
|
||
|
|
Captures: Material uses singular 'manage_material' while others use plural.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(material, ["info", "Assets/Mat.mat"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
assert args[0][0] == "manage_material"
|
||
|
|
|
||
|
|
def test_asset_commands_use_manage_asset_tool(self, runner, mock_config):
|
||
|
|
"""Test asset commands use 'manage_asset' tool name.
|
||
|
|
|
||
|
|
Captures: Hardcoded tool name in asset.py.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.asset.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.asset.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(asset, ["search", "*.prefab"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
assert args[0][0] == "manage_asset"
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Config Access
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestConfigAccessPattern:
|
||
|
|
"""Verify how commands access CLI configuration.
|
||
|
|
|
||
|
|
Current behavior: All commands call get_config() at the beginning of the
|
||
|
|
command function and use config throughout for formatting and connection.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_every_command_calls_get_config(self, runner, mock_config):
|
||
|
|
"""Test that commands retrieve config via get_config().
|
||
|
|
|
||
|
|
Captures: Pattern is consistent across all command modules - first line
|
||
|
|
is always config = get_config().
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config) as mock_get:
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response):
|
||
|
|
runner.invoke(prefab, ["open", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
mock_get.assert_called_once()
|
||
|
|
|
||
|
|
def test_config_is_passed_to_run_command(self, runner, mock_config):
|
||
|
|
"""Test config is passed to run_command as third argument.
|
||
|
|
|
||
|
|
Captures: run_command(tool_name, params, config) signature.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["info", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
assert len(args[0]) >= 3
|
||
|
|
assert args[0][2] == mock_config
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Wrapped Response Structure
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestWrappedResponseHandling:
|
||
|
|
"""Verify handling of wrapped response data.
|
||
|
|
|
||
|
|
Current behavior: Some commands (prefab.py) handle wrapped responses using
|
||
|
|
result.get("result", result) fallback pattern.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_info_handles_wrapped_response_structure(self, runner, mock_config):
|
||
|
|
"""Test prefab info unwraps nested response structure.
|
||
|
|
|
||
|
|
Captures: result.get("result", result) pattern allows handling both:
|
||
|
|
- Direct success responses: {"success": True, "data": {...}}
|
||
|
|
- Wrapped responses: {"result": {"success": True, "data": {...}}}
|
||
|
|
"""
|
||
|
|
wrapped_response = {
|
||
|
|
"result": {
|
||
|
|
"success": True,
|
||
|
|
"data": {
|
||
|
|
"assetPath": "Test.prefab",
|
||
|
|
"prefabType": "Regular"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=wrapped_response):
|
||
|
|
result = runner.invoke(prefab, ["info", "Assets/Prefabs/Test.prefab"])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
def test_prefab_hierarchy_compact_mode_extracts_data(self, runner, mock_config):
|
||
|
|
"""Test prefab hierarchy extracts and formats data from wrapped response.
|
||
|
|
|
||
|
|
Captures: Compact mode custom formatting uses response_data.get("data")
|
||
|
|
after unwrapping.
|
||
|
|
"""
|
||
|
|
wrapped_response = {
|
||
|
|
"result": {
|
||
|
|
"success": True,
|
||
|
|
"data": {
|
||
|
|
"items": [
|
||
|
|
{"name": "Root", "path": ""},
|
||
|
|
{"name": "Child", "path": "Root/Child"}
|
||
|
|
],
|
||
|
|
"total": 2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=wrapped_response):
|
||
|
|
result = runner.invoke(prefab, ["hierarchy", "Assets/Prefabs/Test.prefab", "--compact"])
|
||
|
|
|
||
|
|
assert "Total: 2 objects" in result.output
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Pattern: Prefab Creation with Optional Flags
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestPrefabCreateFlags:
|
||
|
|
"""Verify prefab create command's optional flags.
|
||
|
|
|
||
|
|
Current behavior: Multiple boolean flags control prefab creation behavior.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_create_includes_all_optional_flags_when_set(self, runner, mock_config):
|
||
|
|
"""Test prefab create includes optional flags in params.
|
||
|
|
|
||
|
|
Captures: --overwrite, --include-inactive, --unlink-if-instance flags
|
||
|
|
map to allowOverwrite, searchInactive, unlinkIfInstance params.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["create", "Player", "Assets/Prefabs/Player.prefab",
|
||
|
|
"--overwrite", "--include-inactive", "--unlink-if-instance"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["allowOverwrite"] is True
|
||
|
|
assert params["searchInactive"] is True
|
||
|
|
assert params["unlinkIfInstance"] is True
|
||
|
|
|
||
|
|
def test_prefab_create_omits_unset_optional_flags(self, runner, mock_config):
|
||
|
|
"""Test prefab create omits flags when not provided.
|
||
|
|
|
||
|
|
Captures: Unset flags are not included in params dict.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["create", "Player", "Assets/Prefabs/Player.prefab"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert "allowOverwrite" not in params
|
||
|
|
assert "searchInactive" not in params
|
||
|
|
assert "unlinkIfInstance" not in params
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Integration Tests - Multi-Step Command Flows
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestMultiStepCommandFlows:
|
||
|
|
"""Verify realistic workflows using multiple commands.
|
||
|
|
|
||
|
|
Captures: How commands work together in typical usage patterns.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_workflow_open_modify_save(self, runner, mock_config):
|
||
|
|
"""Test realistic prefab editing workflow.
|
||
|
|
|
||
|
|
Captures: Sequential commands that work together.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response):
|
||
|
|
# Open
|
||
|
|
result = runner.invoke(prefab, ["open", "Assets/Prefabs/Player.prefab"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Save
|
||
|
|
result = runner.invoke(prefab, ["save", "--force"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Close
|
||
|
|
result = runner.invoke(prefab, ["close", "--save"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
def test_component_workflow_add_modify_remove(self, runner, mock_config):
|
||
|
|
"""Test component add, modify, remove workflow.
|
||
|
|
|
||
|
|
Captures: Multiple component operations in sequence.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response):
|
||
|
|
# Add
|
||
|
|
result = runner.invoke(component, ["add", "Player", "Rigidbody"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Modify
|
||
|
|
result = runner.invoke(component, ["modify", "Player", "Rigidbody",
|
||
|
|
"-p", '{"mass": 5.0, "useGravity": false}'])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Remove
|
||
|
|
result = runner.invoke(component, ["remove", "Player", "Rigidbody", "--force"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
def test_material_workflow_create_assign_modify(self, runner, mock_config):
|
||
|
|
"""Test material create, assign, and modify workflow.
|
||
|
|
|
||
|
|
Captures: Material-related commands work together.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response):
|
||
|
|
# Create
|
||
|
|
result = runner.invoke(material, ["create", "Assets/Materials/New.mat"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Set color
|
||
|
|
result = runner.invoke(material, ["set-color", "Assets/Materials/New.mat", "1", "0", "0"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Assign
|
||
|
|
result = runner.invoke(material, ["assign", "Assets/Materials/New.mat", "Cube"])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Edge Cases and Boundary Conditions
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestEdgeCases:
|
||
|
|
"""Verify handling of edge cases and unusual inputs.
|
||
|
|
|
||
|
|
Captures: How commands behave with unexpected inputs or boundary conditions.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_prefab_path_with_spaces_and_special_chars(self, runner, mock_config):
|
||
|
|
"""Test prefab commands handle paths with spaces and special characters.
|
||
|
|
|
||
|
|
Captures: Path arguments are passed as-is to run_command.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
path = "Assets/Prefabs/My Special Prefab [v2].prefab"
|
||
|
|
|
||
|
|
with patch("cli.commands.prefab.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.prefab.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(prefab, ["open", path])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["prefabPath"] == path
|
||
|
|
|
||
|
|
def test_material_color_with_extreme_values(self, runner, mock_config):
|
||
|
|
"""Test material color command with out-of-range values.
|
||
|
|
|
||
|
|
Captures: Click validates floats but doesn't clamp values; they're passed
|
||
|
|
through to Unity which handles validation.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response) as mock_run:
|
||
|
|
# Out of typical 0-1 range but valid float values
|
||
|
|
result = runner.invoke(material, ["set-color", "Mat.mat", "2.5", "0.5", "0.25", "1.0"])
|
||
|
|
|
||
|
|
# Verify command executed successfully
|
||
|
|
if mock_run.called:
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["color"] == [2.5, 0.5, 0.25, 1.0]
|
||
|
|
else:
|
||
|
|
# Command may fail on validation, which is ok for characterization test
|
||
|
|
assert result.exit_code != 0 or mock_run.called
|
||
|
|
|
||
|
|
def test_component_with_long_component_type_name(self, runner, mock_config):
|
||
|
|
"""Test component command with long/qualified component type name.
|
||
|
|
|
||
|
|
Captures: Component type is passed as string; no validation on command side.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(component, ["add", "Player", "MyNamespace.CustomComponent"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["componentType"] == "MyNamespace.CustomComponent"
|
||
|
|
|
||
|
|
def test_json_properties_with_nested_structure(self, runner, mock_config):
|
||
|
|
"""Test JSON properties with nested objects.
|
||
|
|
|
||
|
|
Captures: json.loads() successfully parses nested structures.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
nested_json = '{"outer": {"inner": {"value": 42}}}'
|
||
|
|
|
||
|
|
with patch("cli.commands.component.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.component.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(component, ["add", "Player", "Rigidbody", "-p", nested_json])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert params["properties"] == {"outer": {"inner": {"value": 42}}}
|
||
|
|
|
||
|
|
def test_search_method_default_when_not_specified(self, runner, mock_config):
|
||
|
|
"""Test that search_method is omitted when not specified (defaults in Unity).
|
||
|
|
|
||
|
|
Captures: Optional search_method parameter is None by default, not included in params.
|
||
|
|
"""
|
||
|
|
response = {"success": True}
|
||
|
|
|
||
|
|
with patch("cli.commands.material.get_config", return_value=mock_config):
|
||
|
|
with patch("cli.commands.material.run_command", return_value=response) as mock_run:
|
||
|
|
runner.invoke(material, ["assign", "Assets/Mat.mat", "Cube"])
|
||
|
|
|
||
|
|
args = mock_run.call_args
|
||
|
|
params = args[0][1]
|
||
|
|
assert "searchMethod" not in params
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Identified Boilerplate Patterns for Refactoring
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class TestBoilerplatePatterns:
|
||
|
|
"""Document boilerplate patterns identified for refactoring.
|
||
|
|
|
||
|
|
These tests serve as specification for P2-1 refactoring (Command Wrapper Decorator).
|
||
|
|
"""
|
||
|
|
|
||
|
|
def test_every_command_has_identical_try_except_pattern(self):
|
||
|
|
"""Document that all commands have identical error handling.
|
||
|
|
|
||
|
|
BOILERPLATE PATTERN:
|
||
|
|
try:
|
||
|
|
result = run_command(tool, params, config)
|
||
|
|
click.echo(format_output(result, config.format))
|
||
|
|
if result.get("success"):
|
||
|
|
print_success(message)
|
||
|
|
except UnityConnectionError as e:
|
||
|
|
print_error(str(e))
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
Identified in: prefab.py, component.py, material.py, asset.py, and all other modules.
|
||
|
|
Refactoring opportunity: Extract as @standard_command decorator (P2-1).
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def test_json_parsing_appears_5_times_independently(self):
|
||
|
|
"""Document JSON parsing duplication.
|
||
|
|
|
||
|
|
BOILERPLATE PATTERN - appears in:
|
||
|
|
1. component.py:54-57 (properties)
|
||
|
|
2. component.py:138-142 (value with fallback)
|
||
|
|
3. material.py:71-75 (properties)
|
||
|
|
4. material.py:142-149 (value with fallback)
|
||
|
|
5. asset.py:132-136 (properties)
|
||
|
|
|
||
|
|
Three variants:
|
||
|
|
- Simple json.loads() with error handling
|
||
|
|
- json.loads() then float() fallback (component, material)
|
||
|
|
- json.loads() then float() then string fallback (material)
|
||
|
|
|
||
|
|
Refactoring opportunity: Extract as QW-2 utility (cli/utils/parsers.py).
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def test_search_method_parameter_repeated_4_times(self):
|
||
|
|
"""Document search_method parameter duplication.
|
||
|
|
|
||
|
|
BOILERPLATE PATTERN - appears in:
|
||
|
|
1. component.py:add(), remove(), set_property(), modify()
|
||
|
|
2. material.py:assign(), set_renderer_color()
|
||
|
|
3. asset.py - potentially (not examined)
|
||
|
|
|
||
|
|
Each uses click.Choice() but with slight variations:
|
||
|
|
- component: ["by_id", "by_name", "by_path"]
|
||
|
|
- material: ["by_name", "by_path", "by_tag", "by_layer", "by_component"]
|
||
|
|
|
||
|
|
Refactoring opportunity: Extract as QW-4 constant (cli/utils/constants.py).
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def test_confirmation_dialog_pattern_could_be_extracted(self):
|
||
|
|
"""Document confirmation dialog pattern.
|
||
|
|
|
||
|
|
BOILERPLATE PATTERN:
|
||
|
|
if not force:
|
||
|
|
click.confirm(f"Remove {item}?", abort=True)
|
||
|
|
|
||
|
|
Identified in: component.py:94
|
||
|
|
|
||
|
|
Refactoring opportunity: Extract as QW-5 utility function
|
||
|
|
(cli/utils/confirmation.py).
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def test_wrapped_response_handling_in_prefab_module(self):
|
||
|
|
"""Document wrapped response handling pattern.
|
||
|
|
|
||
|
|
BOILERPLATE PATTERN in prefab.py:
|
||
|
|
response_data = result.get("result", result)
|
||
|
|
if response_data.get("success") and response_data.get("data"):
|
||
|
|
data = response_data["data"]
|
||
|
|
# access data fields
|
||
|
|
|
||
|
|
Identified in: prefab.py:133, 182, 195
|
||
|
|
|
||
|
|
This pattern appears unique to prefab.py; may indicate inconsistent
|
||
|
|
response wrapping behavior that should be standardized.
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
pytest.main([__file__, "-v"])
|