chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class AIPropertyMatchingTests
|
||||
{
|
||||
private List<string> sampleProperties;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
sampleProperties = new List<string>
|
||||
{
|
||||
"maxReachDistance",
|
||||
"maxHorizontalDistance",
|
||||
"maxVerticalDistance",
|
||||
"moveSpeed",
|
||||
"healthPoints",
|
||||
"playerName",
|
||||
"isEnabled",
|
||||
"mass",
|
||||
"velocity",
|
||||
"transform"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllComponentProperties_ReturnsValidProperties_ForTransform()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
|
||||
|
||||
Assert.IsNotEmpty(properties, "Transform should have properties");
|
||||
Assert.Contains("position", properties, "Transform should have position property");
|
||||
Assert.Contains("rotation", properties, "Transform should have rotation property");
|
||||
Assert.Contains("localScale", properties, "Transform should have localScale property");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllComponentProperties_ReturnsEmpty_ForNullType()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(null);
|
||||
|
||||
Assert.IsEmpty(properties, "Null type should return empty list");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_ReturnsEmpty_ForNullInput()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(null, sampleProperties);
|
||||
|
||||
Assert.IsEmpty(suggestions, "Null input should return no suggestions");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_ReturnsEmpty_ForEmptyInput()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("", sampleProperties);
|
||||
|
||||
Assert.IsEmpty(suggestions, "Empty input should return no suggestions");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("test", new List<string>());
|
||||
|
||||
Assert.IsEmpty(suggestions, "Empty property list should return no suggestions");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_FindsExactMatch_AfterCleaning()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("Max Reach Distance", sampleProperties);
|
||||
|
||||
Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces");
|
||||
Assert.GreaterOrEqual(suggestions.Count, 1, "Should return at least one match for exact match");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_FindsMultipleWordMatches()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("max distance", sampleProperties);
|
||||
|
||||
Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance");
|
||||
Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance");
|
||||
Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_FindsSimilarStrings_WithTypos()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("movespeed", sampleProperties); // missing capital S
|
||||
|
||||
Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_FindsSemanticMatches_ForCommonTerms()
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("weight", sampleProperties);
|
||||
|
||||
// Note: Current algorithm might not find "mass" but should handle it gracefully
|
||||
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_LimitsResults_ToReasonableNumber()
|
||||
{
|
||||
// Test with input that might match many properties
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("m", sampleProperties);
|
||||
|
||||
Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_CachesResults()
|
||||
{
|
||||
var input = "Max Reach Distance";
|
||||
|
||||
// First call
|
||||
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions(input, sampleProperties);
|
||||
|
||||
// Second call should use cache (tested indirectly by ensuring consistency)
|
||||
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions(input, sampleProperties);
|
||||
|
||||
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent");
|
||||
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_HandlesUnityNamingConventions()
|
||||
{
|
||||
var unityStyleProperties = new List<string> { "isKinematic", "useGravity", "maxLinearVelocity" };
|
||||
|
||||
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions("is kinematic", unityStyleProperties);
|
||||
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions("use gravity", unityStyleProperties);
|
||||
var suggestions3 = ComponentResolver.GetFuzzyPropertySuggestions("max linear velocity", unityStyleProperties);
|
||||
|
||||
Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention");
|
||||
Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention");
|
||||
Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_PrioritizesExactMatches()
|
||||
{
|
||||
var properties = new List<string> { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" };
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("speed", properties);
|
||||
|
||||
Assert.IsNotEmpty(suggestions, "Should find suggestions");
|
||||
Assert.Contains("speed", suggestions, "Exact match should be included in results");
|
||||
// Note: Implementation may or may not prioritize exact matches first
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFuzzyPropertySuggestions_HandlesCaseInsensitive()
|
||||
{
|
||||
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions("MAXREACHDISTANCE", sampleProperties);
|
||||
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions("maxreachdistance", sampleProperties);
|
||||
|
||||
Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input");
|
||||
Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e4468da1a15349029e52570b84ec4b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEditor;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using TestNamespace;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that batch_execute only normalizes top-level parameter keys (snake_case → camelCase)
|
||||
/// and preserves nested value keys (e.g. Unity serialized property paths like m_PersistentCalls).
|
||||
/// </summary>
|
||||
public class BatchExecuteKeyPreservationTests
|
||||
{
|
||||
private GameObject testGo;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void OneTimeSetUp()
|
||||
{
|
||||
CommandRegistry.Initialize();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
testGo = new GameObject("BatchKeyTestGO");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (testGo != null)
|
||||
Object.DestroyImmediate(testGo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NestedValueKeys_WithUnderscores_ArePreservedThroughBatch()
|
||||
{
|
||||
testGo.AddComponent<UnityEventTestComponent>();
|
||||
int targetId = testGo.GetInstanceID();
|
||||
|
||||
var batchParams = new JObject
|
||||
{
|
||||
["commands"] = new JArray
|
||||
{
|
||||
new JObject
|
||||
{
|
||||
["tool"] = "manage_components",
|
||||
["params"] = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGo.name,
|
||||
["search_method"] = "by_name",
|
||||
["component_type"] = "UnityEventTestComponent",
|
||||
["property"] = "onSimpleEvent",
|
||||
["value"] = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": [
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": { ""m_BoolArgument"": true },
|
||||
""m_CallState"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult();
|
||||
var resultObj = JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), $"Batch should succeed: {resultObj}");
|
||||
|
||||
// Verify the nested m_PersistentCalls keys were preserved (not mangled to mPersistentCalls)
|
||||
var comp = testGo.GetComponent<UnityEventTestComponent>();
|
||||
var so = new SerializedObject(comp);
|
||||
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
|
||||
Assert.IsNotNull(callsProp, "m_Calls property should exist");
|
||||
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call");
|
||||
Assert.AreEqual("SetActive",
|
||||
callsProp.GetArrayElementAtIndex(0).FindPropertyRelative("m_MethodName").stringValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TopLevelParameterKeys_AreStillNormalized()
|
||||
{
|
||||
testGo.AddComponent<AudioSource>();
|
||||
|
||||
// Use snake_case top-level keys: search_method, component_type
|
||||
var batchParams = new JObject
|
||||
{
|
||||
["commands"] = new JArray
|
||||
{
|
||||
new JObject
|
||||
{
|
||||
["tool"] = "manage_components",
|
||||
["params"] = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGo.name,
|
||||
["search_method"] = "by_name",
|
||||
["component_type"] = "AudioSource",
|
||||
["property"] = "volume",
|
||||
["value"] = 0.42f
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult();
|
||||
var resultObj = JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"),
|
||||
$"Batch with snake_case top-level keys should succeed: {resultObj}");
|
||||
Assert.AreEqual(0.42f, testGo.GetComponent<AudioSource>().volume, 0.001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Regression_CreateGameObject_StillWorksViaBatch()
|
||||
{
|
||||
string goName = "BatchCreatedGO_" + System.Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
GameObject created = null;
|
||||
|
||||
try
|
||||
{
|
||||
var batchParams = new JObject
|
||||
{
|
||||
["commands"] = new JArray
|
||||
{
|
||||
new JObject
|
||||
{
|
||||
["tool"] = "manage_gameobject",
|
||||
["params"] = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = goName,
|
||||
["primitive_type"] = "Cube"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult();
|
||||
var resultObj = JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), $"Batch create GO should succeed: {resultObj}");
|
||||
|
||||
created = GameObject.Find(goName);
|
||||
Assert.IsNotNull(created, $"GameObject '{goName}' should exist in scene");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (created != null)
|
||||
Object.DestroyImmediate(created);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d612089c86a754594916e29bb33b340d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using MCPForUnity.Editor.Tools.Build;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Tests.EditMode.Tools
|
||||
{
|
||||
[TestFixture]
|
||||
public class BuildTargetMappingTests
|
||||
{
|
||||
[TestCase("windows64", BuildTarget.StandaloneWindows64)]
|
||||
[TestCase("macos", BuildTarget.StandaloneOSX)]
|
||||
[TestCase("linux", BuildTarget.StandaloneLinux64)]
|
||||
[TestCase("tvos", BuildTarget.tvOS)]
|
||||
public void TryResolveBuildTarget_KnownAliasesResolve(string name, BuildTarget expected)
|
||||
{
|
||||
Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget(name, out var target));
|
||||
Assert.AreEqual(expected, target);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolveBuildTarget_NumericInputDoesNotResolve()
|
||||
{
|
||||
Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("5", out _));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolveNamedBuildTarget_UnknownTargetListsOnlyAvailableTargets()
|
||||
{
|
||||
string error = BuildTargetMapping.TryResolveNamedBuildTarget("not-a-target", out _);
|
||||
|
||||
Assert.IsNotNull(error);
|
||||
StringAssert.Contains("windows64", error);
|
||||
|
||||
bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _);
|
||||
if (visionOSAvailable)
|
||||
{
|
||||
StringAssert.Contains("visionos", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsFalse(error.Contains("visionos"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolveNamedBuildTarget_VisionOSUnavailableReturnsHelpfulError()
|
||||
{
|
||||
bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _);
|
||||
string error = BuildTargetMapping.TryResolveNamedBuildTarget("visionos", out _);
|
||||
|
||||
if (visionOSAvailable)
|
||||
{
|
||||
Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget("visionos", out _));
|
||||
Assert.IsTrue(
|
||||
error == null || error.Contains("VisionOS"),
|
||||
$"Expected no error or a VisionOS-specific error, got: {error}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("visionos", out _));
|
||||
Assert.IsNotNull(error);
|
||||
StringAssert.Contains("VisionOS build target is not available", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94419cfeebb59e84ea16331766eaad52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c3f1a2b4d5e6f7089012345abcdef01
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+594
@@ -0,0 +1,594 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Tools.Prefabs;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools.Characterization
|
||||
{
|
||||
/// <summary>
|
||||
/// Characterization tests for Editor Tools domain.
|
||||
/// These tests capture CURRENT behavior without refactoring.
|
||||
/// They serve as a regression baseline for future refactoring work.
|
||||
///
|
||||
/// Based on analysis in: MCPForUnity/Editor/Tools/Tests/CHARACTERIZATION_SUMMARY.md
|
||||
///
|
||||
/// Sampled tools: ManageEditor, ManageMaterial, FindGameObjects, ManagePrefabs, ExecuteMenuItem
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class EditorToolsCharacterizationTests
|
||||
{
|
||||
private static JObject ToJO(object o) => JObject.FromObject(o);
|
||||
|
||||
#region Section 1: HandleCommand Entry Point and Null/Empty Parameter Handling
|
||||
|
||||
/// <summary>
|
||||
/// Current behavior: All tools have a single public HandleCommand(JObject) entry point.
|
||||
/// This is the standard pattern - tests verify all sampled tools follow it.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_WithNullParams_ReturnsErrorResponse()
|
||||
{
|
||||
// FIXED BEHAVIOR (P1-1 ToolParams refactoring): ManageEditor now handles null params gracefully
|
||||
// Returns ErrorResponse instead of throwing NullReferenceException
|
||||
var result = ManageEditor.HandleCommand(null);
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should return error for null params");
|
||||
Assert.IsNotNull(jo["error"], "Should have error message");
|
||||
Assert.That((string)jo["error"], Does.Contain("cannot be null"), "Should indicate parameters are null");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_WithNullParams_ReturnsErrorResponse()
|
||||
{
|
||||
// CURRENT BEHAVIOR: FindGameObjects DOES handle null params gracefully - returns ErrorResponse
|
||||
// This is good design and should be preserved during refactoring.
|
||||
var result = FindGameObjects.HandleCommand(null);
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should return error for null params");
|
||||
Assert.IsNotNull(jo["error"], "Should have error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_WithoutActionParameter_ReturnsError()
|
||||
{
|
||||
// Current behavior: Action parameter is required for dispatch
|
||||
var result = ManageEditor.HandleCommand(new JObject());
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require action parameter");
|
||||
Assert.IsNotNull(jo["error"], "Should have error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ActionNormalization_CaseInsensitive()
|
||||
{
|
||||
// Current behavior: Actions are normalized to lowercase for comparison
|
||||
// Using telemetry_status (read-only) instead of play to avoid mutating editor state
|
||||
var upperResult = ManageEditor.HandleCommand(new JObject { ["action"] = "TELEMETRY_STATUS" });
|
||||
var lowerResult = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
|
||||
|
||||
var upperJo = ToJO(upperResult);
|
||||
var lowerJo = ToJO(lowerResult);
|
||||
|
||||
// Both should succeed or both should fail in the same way (action recognized)
|
||||
Assert.AreEqual((bool)upperJo["success"], (bool)lowerJo["success"],
|
||||
"Case normalization should make both behave identically");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 2: Parameter Extraction and Validation
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_WithCamelCaseSearchMethod_Succeeds()
|
||||
{
|
||||
// Current behavior: Tools accept camelCase parameter names
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "TestObject",
|
||||
["searchMethod"] = "by_name"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
// FindGameObjects should accept the parameter (may return empty results)
|
||||
Assert.IsTrue((bool)jo["success"], "Should accept camelCase parameter");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_WithSnakeCaseSearchMethod_Succeeds()
|
||||
{
|
||||
// Current behavior: Tools also accept snake_case parameter names
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "TestObject",
|
||||
["search_method"] = "by_name"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsTrue((bool)jo["success"], "Should accept snake_case parameter");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_WithoutSearchMethod_UsesDefault()
|
||||
{
|
||||
// Current behavior: searchMethod defaults to "by_name"
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "TestObject"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsTrue((bool)jo["success"], "Should use default search method");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_ClampsPageSizeToValidRange()
|
||||
{
|
||||
// Current behavior: pageSize is clamped to 1-500 range
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "TestObject",
|
||||
["pageSize"] = 1000 // Exceeds max
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsTrue((bool)jo["success"], "Should clamp and succeed");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_SetActiveTool_RequiresToolNameParameter()
|
||||
{
|
||||
// Current behavior: set_active_tool requires tool_name parameter
|
||||
var result = ManageEditor.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_active_tool"
|
||||
// Missing tool_name
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require tool_name");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_ActionsRecognized()
|
||||
{
|
||||
// Current behavior: Valid actions are recognized and return response objects
|
||||
// Using telemetry_status (read-only) to avoid mutating editor state
|
||||
var result = ManageEditor.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "telemetry_status"
|
||||
});
|
||||
// Action should be recognized and return valid response
|
||||
var jo = ToJO(result);
|
||||
Assert.IsNotNull(jo, "Should return a response object");
|
||||
Assert.IsTrue(jo.ContainsKey("success"), "Response should have success field");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 3: Action Switch Dispatch
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_WithUnknownAction_ReturnsError()
|
||||
{
|
||||
// Current behavior: Unknown actions return error with descriptive message
|
||||
var result = ManageEditor.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "nonexistent_action_xyz"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should fail for unknown action");
|
||||
StringAssert.Contains("nonexistent_action_xyz", jo["error"]?.ToString() ?? "",
|
||||
"Error should mention the unknown action");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_DifferentActionsDispatchToDifferentHandlers()
|
||||
{
|
||||
// Current behavior: Different actions dispatch to different handlers
|
||||
// Using read-only actions to avoid mutating editor state
|
||||
var statusResult = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
|
||||
var pingResult = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_ping" });
|
||||
|
||||
// Both should return responses
|
||||
Assert.IsNotNull(statusResult, "Status should return response");
|
||||
Assert.IsNotNull(pingResult, "Ping should return response");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageMaterial_WithUnknownAction_ReturnsError()
|
||||
{
|
||||
// Current behavior: Material tool also returns error for unknown actions
|
||||
var result = ManageMaterial.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "unknown_material_action"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should fail for unknown action");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 4: Error Handling and Logging
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManagePrefabs_WithInvalidParameters_ReturnsError()
|
||||
{
|
||||
// Current behavior: Invalid parameters caught and returned as ErrorResponse
|
||||
var result = ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_from_gameobject"
|
||||
// Missing required parameters
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should fail with invalid params");
|
||||
Assert.IsNotNull(jo["error"], "Should have error description");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_ReturnsResponseObject()
|
||||
{
|
||||
// Current behavior: All responses are either SuccessResponse or ErrorResponse
|
||||
// Using telemetry_status (read-only) to avoid mutating editor state
|
||||
var result = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
|
||||
var jo = ToJO(result);
|
||||
// Verify response has expected shape
|
||||
Assert.IsTrue(jo.ContainsKey("success"), "Response should have 'success' field");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ErrorMessages_AreContextSpecific()
|
||||
{
|
||||
// Current behavior: Error messages include context about what went wrong
|
||||
var result = ManageEditor.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add_tag"
|
||||
// Missing tag_name
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
var error = jo["error"]?.ToString() ?? "";
|
||||
// Error should mention what's missing or wrong
|
||||
Assert.IsTrue(error.Length > 0, "Should have descriptive error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_SafelyHandlesNullTokens()
|
||||
{
|
||||
// Current behavior: Null-safe token access pattern prevents NullReferenceException
|
||||
// This test verifies ManageEditor doesn't crash on partial params
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
ManageEditor.HandleCommand(new JObject { ["action"] = null });
|
||||
}, "Should handle null action token without exception");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 5: Inline Parameter Validation and Coercion
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_AddTag_RequiresTagName()
|
||||
{
|
||||
// Current behavior: add_tag validates tag_name is present before mutation
|
||||
var result = ManageEditor.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add_tag"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require tag_name parameter");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManagePrefabs_WithoutRequiredPath_ReturnsError()
|
||||
{
|
||||
// Current behavior: Required path parameter validated before operation
|
||||
var result = ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_info"
|
||||
// Missing path parameter
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require path parameter");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageMaterial_Create_RequiresNameParameter()
|
||||
{
|
||||
// Current behavior: create action requires name parameter
|
||||
var result = ManageMaterial.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create"
|
||||
// Missing name
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require name parameter");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ValidationOccursBeforeStateMutation()
|
||||
{
|
||||
// Current behavior: Parameters are validated before any state changes
|
||||
// This is verified by checking that invalid params don't cause side effects
|
||||
var result = ManageEditor.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add_layer"
|
||||
// Missing layer_name - should fail before attempting to add
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should validate before mutation");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 6: State Mutation and Side Effects
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_ReadOnlyActionsDoNotMutateState()
|
||||
{
|
||||
// Current behavior: Read-only actions like telemetry_status don't mutate editor state
|
||||
// Verify isPlaying remains false after calling telemetry_status
|
||||
var wasPlayingBefore = UnityEditor.EditorApplication.isPlaying;
|
||||
var result = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
|
||||
var isPlayingAfter = UnityEditor.EditorApplication.isPlaying;
|
||||
|
||||
Assert.AreEqual(wasPlayingBefore, isPlayingAfter, "Read-only actions should not change play mode state");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageMaterial_CreateAction_RequiresValidParams()
|
||||
{
|
||||
// Current behavior: Asset creation requires valid parameters
|
||||
// This documents that side effects only occur with valid params
|
||||
var result = ManageMaterial.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "" // Empty name should fail
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
// Either fails validation or succeeds (behavior may vary)
|
||||
Assert.IsTrue(jo.ContainsKey("success"), "Should return response");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 7: Complex Parameter Handling and Object Resolution
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_ReturnsPaginationMetadata()
|
||||
{
|
||||
// Current behavior: FindGameObjects returns pagination info
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "*",
|
||||
["pageSize"] = 10
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
if ((bool)jo["success"])
|
||||
{
|
||||
var data = jo["data"];
|
||||
// Pagination metadata should be present
|
||||
Assert.IsNotNull(data, "Should have data field");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_SearchMethodOptions()
|
||||
{
|
||||
// Current behavior: Supports multiple search methods
|
||||
string[] methods = { "by_name", "by_path", "by_tag", "by_layer", "by_component" };
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "TestQuery",
|
||||
["searchMethod"] = method
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
// All methods should be recognized and succeed
|
||||
Assert.IsTrue((bool)jo["success"], $"Method {method} should be recognized and succeed");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_PageSizeRange()
|
||||
{
|
||||
// Current behavior: pageSize clamped to 1-500
|
||||
// Test with boundary values
|
||||
var minResult = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "Test",
|
||||
["pageSize"] = 0 // Should clamp to 1
|
||||
});
|
||||
var maxResult = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "Test",
|
||||
["pageSize"] = 1000 // Should clamp to 500
|
||||
});
|
||||
|
||||
Assert.IsNotNull(ToJO(minResult), "Should handle min boundary");
|
||||
Assert.IsNotNull(ToJO(maxResult), "Should handle max boundary");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 8: Security and Filtering
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ExecuteMenuItem_BlacklistsQuit()
|
||||
{
|
||||
// Current behavior: File/Quit is blacklisted for safety
|
||||
var result = ExecuteMenuItem.HandleCommand(new JObject
|
||||
{
|
||||
["menuPath"] = "File/Quit"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Quit should be blocked");
|
||||
StringAssert.Contains("blocked", jo["error"]?.ToString()?.ToLower() ?? "",
|
||||
"Error should mention blocking");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ExecuteMenuItem_RequiresMenuPath()
|
||||
{
|
||||
// Current behavior: menu_path/menuPath parameter is required
|
||||
var result = ExecuteMenuItem.HandleCommand(new JObject());
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require menuPath");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 9: Response Objects and Data Structures
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ResponsesHaveConsistentShape()
|
||||
{
|
||||
// Current behavior: All responses have success field
|
||||
var tools = new Func<JObject, object>[]
|
||||
{
|
||||
p => ManageEditor.HandleCommand(p),
|
||||
p => FindGameObjects.HandleCommand(p),
|
||||
p => ManagePrefabs.HandleCommand(p),
|
||||
p => ManageMaterial.HandleCommand(p),
|
||||
p => ExecuteMenuItem.HandleCommand(p)
|
||||
};
|
||||
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
var result = tool(new JObject { ["action"] = "ping" });
|
||||
var jo = ToJO(result);
|
||||
Assert.IsTrue(jo.ContainsKey("success"), "All responses should have success field");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_SuccessResponse_HasMessageField()
|
||||
{
|
||||
// Current behavior: Success responses typically have message field
|
||||
var result = ManageMaterial.HandleCommand(new JObject { ["action"] = "ping" });
|
||||
var jo = ToJO(result);
|
||||
if ((bool)jo["success"])
|
||||
{
|
||||
Assert.IsTrue(jo.ContainsKey("message") || jo.ContainsKey("data"),
|
||||
"Success should have message or data");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ErrorResponse_HasErrorField()
|
||||
{
|
||||
// Current behavior: Error responses have error field with description
|
||||
var result = ManageEditor.HandleCommand(new JObject { ["action"] = "invalid" });
|
||||
var jo = ToJO(result);
|
||||
if (!(bool)jo["success"])
|
||||
{
|
||||
Assert.IsTrue(jo.ContainsKey("error"), "Error response should have error field");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 10: Tool Registration
|
||||
|
||||
[Test]
|
||||
public void AllSampledTools_HaveMcpForUnityToolAttribute()
|
||||
{
|
||||
// Current behavior: Tools are registered via McpForUnityTool attribute
|
||||
// This verifies the sampled tools have the attribute
|
||||
var toolTypes = new[]
|
||||
{
|
||||
typeof(ManageEditor),
|
||||
typeof(FindGameObjects),
|
||||
typeof(ManagePrefabs),
|
||||
typeof(ManageMaterial),
|
||||
typeof(ExecuteMenuItem)
|
||||
};
|
||||
|
||||
foreach (var type in toolTypes)
|
||||
{
|
||||
var attr = Attribute.GetCustomAttribute(type, typeof(McpForUnityToolAttribute));
|
||||
Assert.IsNotNull(attr, $"{type.Name} should have McpForUnityTool attribute");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Section 11: Tool-Specific Behaviors
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageEditor_PlayPauseStopStateMachine()
|
||||
{
|
||||
// Current behavior: play/pause/stop form a state machine
|
||||
// pause only works when playing
|
||||
var pauseResult = ManageEditor.HandleCommand(new JObject { ["action"] = "pause" });
|
||||
var jo = ToJO(pauseResult);
|
||||
// Pause behavior depends on current play state
|
||||
Assert.IsNotNull(jo, "Should return response");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManageMaterial_ColorCoercion()
|
||||
{
|
||||
// Current behavior: Colors can be specified in multiple formats
|
||||
var result = ManageMaterial.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_material_color",
|
||||
["path"] = "NonExistent/Material",
|
||||
["color"] = new JArray(1.0f, 0.5f, 0.5f, 1.0f)
|
||||
});
|
||||
// Even if material doesn't exist, the color parsing should not throw
|
||||
var jo = ToJO(result);
|
||||
Assert.IsNotNull(jo, "Should handle color array format");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_FindGameObjects_EmptyResultsAreValid()
|
||||
{
|
||||
// Current behavior: Finding no objects is a valid success case
|
||||
var result = FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "DEFINITELY_NONEXISTENT_OBJECT_NAME_12345"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsTrue((bool)jo["success"], "Empty results should still be success");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManagePrefabs_GetInfo_RequiresPath()
|
||||
{
|
||||
// Current behavior: get_info needs path to prefab
|
||||
var result = ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_info"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require path");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ManagePrefabs_CreateFromGameObject_RequiresTargetAndPath()
|
||||
{
|
||||
// Current behavior: create_from_gameobject needs both target and path
|
||||
var result = ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_from_gameobject"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
Assert.IsFalse((bool)jo["success"], "Should require target and path");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit("Opens Console window - steals focus")]
|
||||
public void HandleCommand_ExecuteMenuItem_ExecutesNonBlacklistedItems()
|
||||
{
|
||||
// Current behavior: Non-blacklisted items are executed
|
||||
// NOTE: This test opens the Console window which steals focus from the terminal
|
||||
var result = ExecuteMenuItem.HandleCommand(new JObject
|
||||
{
|
||||
["menuPath"] = "Window/General/Console"
|
||||
});
|
||||
var jo = ToJO(result);
|
||||
// Should attempt execution (success depends on menu existence)
|
||||
Assert.IsTrue((bool)jo["success"], "Non-blacklisted item should be attempted");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d4e2b3c5f6a7801234567890abcdef2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class CommandRegistryTests
|
||||
{
|
||||
[OneTimeSetUp]
|
||||
public void OneTimeSetUp()
|
||||
{
|
||||
// Ensure CommandRegistry is initialized before tests run
|
||||
CommandRegistry.Initialize();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetHandler_ThrowsException_ForUnknownCommand()
|
||||
{
|
||||
var unknown = "nonexistent_command_that_should_not_exist";
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
CommandRegistry.GetHandler(unknown);
|
||||
}, "Should throw InvalidOperationException for unknown handler");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoDiscovery_RegistersAllBuiltInTools()
|
||||
{
|
||||
// Verify that all expected built-in tools are registered by trying to get their handlers
|
||||
var expectedTools = new[]
|
||||
{
|
||||
"manage_asset",
|
||||
"manage_editor",
|
||||
"manage_gameobject",
|
||||
"manage_scene",
|
||||
"manage_script",
|
||||
"manage_shader",
|
||||
"read_console",
|
||||
"execute_menu_item",
|
||||
"manage_prefabs"
|
||||
};
|
||||
|
||||
foreach (var toolName in expectedTools)
|
||||
{
|
||||
var handler = CommandRegistry.GetHandler(toolName);
|
||||
Assert.IsNotNull(handler, $"Handler for '{toolName}' should not be null");
|
||||
|
||||
// Verify the handler is actually callable (returns a result, not throws)
|
||||
var emptyParams = new Newtonsoft.Json.Linq.JObject();
|
||||
var result = handler(emptyParams);
|
||||
Assert.IsNotNull(result, $"Handler for '{toolName}' should return a result even for empty params");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed0df02ef7d99451f9f3b2bc3e3aba28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEditor;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using TestNamespace;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ComponentOpsUnityEventTests
|
||||
{
|
||||
private GameObject testGo;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
testGo = new GameObject("UnityEventTestGO");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (testGo != null)
|
||||
Object.DestroyImmediate(testGo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperty_UnityEvent_SinglePersistentCall_PersistsViaSerialization()
|
||||
{
|
||||
var comp = testGo.AddComponent<UnityEventTestComponent>();
|
||||
int targetId = testGo.GetInstanceID();
|
||||
|
||||
var value = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": [
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": {
|
||||
""m_BoolArgument"": true
|
||||
},
|
||||
""m_CallState"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}");
|
||||
|
||||
bool ok = ComponentOps.SetProperty(comp, "onSimpleEvent", value, out string error);
|
||||
|
||||
Assert.IsTrue(ok, $"SetProperty should succeed, got error: {error}");
|
||||
|
||||
// Verify via SerializedObject readback
|
||||
var so = new SerializedObject(comp);
|
||||
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
|
||||
Assert.IsNotNull(callsProp, "m_Calls property should exist");
|
||||
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call");
|
||||
|
||||
var call0 = callsProp.GetArrayElementAtIndex(0);
|
||||
Assert.AreEqual("SetActive", call0.FindPropertyRelative("m_MethodName").stringValue);
|
||||
Assert.AreEqual(testGo, call0.FindPropertyRelative("m_Target").objectReferenceValue);
|
||||
Assert.AreEqual(6, call0.FindPropertyRelative("m_Mode").enumValueIndex);
|
||||
Assert.AreEqual(2, call0.FindPropertyRelative("m_CallState").enumValueIndex);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperty_UnityEvent_MultiplePersistentCalls_AllPersist()
|
||||
{
|
||||
var comp = testGo.AddComponent<UnityEventTestComponent>();
|
||||
int targetId = testGo.GetInstanceID();
|
||||
|
||||
var value = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": [
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": { ""m_BoolArgument"": true },
|
||||
""m_CallState"": 2
|
||||
},
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": { ""m_BoolArgument"": false },
|
||||
""m_CallState"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}");
|
||||
|
||||
bool ok = ComponentOps.SetProperty(comp, "onSimpleEvent", value, out string error);
|
||||
|
||||
Assert.IsTrue(ok, $"SetProperty should succeed, got error: {error}");
|
||||
|
||||
var so = new SerializedObject(comp);
|
||||
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
|
||||
Assert.AreEqual(2, callsProp.arraySize, "Should have 2 persistent calls");
|
||||
|
||||
Assert.AreEqual("SetActive", callsProp.GetArrayElementAtIndex(0).FindPropertyRelative("m_MethodName").stringValue);
|
||||
Assert.AreEqual("SetActive", callsProp.GetArrayElementAtIndex(1).FindPropertyRelative("m_MethodName").stringValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperty_UnityEvent_EmptyCalls_ClearsEvent()
|
||||
{
|
||||
var comp = testGo.AddComponent<UnityEventTestComponent>();
|
||||
|
||||
// First set a call
|
||||
int targetId = testGo.GetInstanceID();
|
||||
var withCall = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": [
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": { ""m_BoolArgument"": true },
|
||||
""m_CallState"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}");
|
||||
ComponentOps.SetProperty(comp, "onSimpleEvent", withCall, out _);
|
||||
|
||||
// Now clear it
|
||||
var empty = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": []
|
||||
}
|
||||
}");
|
||||
|
||||
bool ok = ComponentOps.SetProperty(comp, "onSimpleEvent", empty, out string error);
|
||||
|
||||
Assert.IsTrue(ok, $"SetProperty should succeed, got error: {error}");
|
||||
|
||||
var so = new SerializedObject(comp);
|
||||
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
|
||||
Assert.AreEqual(0, callsProp.arraySize, "Should have 0 persistent calls after clearing");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperty_PrivateSerializedUnityEvent_RoutesViaSerialization()
|
||||
{
|
||||
var comp = testGo.AddComponent<UnityEventTestComponent>();
|
||||
int targetId = testGo.GetInstanceID();
|
||||
|
||||
var value = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": [
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": { ""m_BoolArgument"": true },
|
||||
""m_CallState"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}");
|
||||
|
||||
bool ok = ComponentOps.SetProperty(comp, "_onPrivateEvent", value, out string error);
|
||||
|
||||
Assert.IsTrue(ok, $"SetProperty on private [SerializeField] UnityEvent should succeed, got error: {error}");
|
||||
|
||||
var so = new SerializedObject(comp);
|
||||
var callsProp = so.FindProperty("_onPrivateEvent.m_PersistentCalls.m_Calls");
|
||||
Assert.IsNotNull(callsProp, "Private event m_Calls should exist");
|
||||
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperty_SimpleFloat_StillWorksViaReflection()
|
||||
{
|
||||
var audioSource = testGo.AddComponent<AudioSource>();
|
||||
|
||||
bool ok = ComponentOps.SetProperty(audioSource, "volume", new JValue(0.5f), out string error);
|
||||
|
||||
Assert.IsTrue(ok, $"SetProperty for float should succeed, got error: {error}");
|
||||
Assert.AreEqual(0.5f, audioSource.volume, 0.001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_EndToEnd_UnityEventWiring()
|
||||
{
|
||||
testGo.AddComponent<UnityEventTestComponent>();
|
||||
int targetId = testGo.GetInstanceID();
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGo.name,
|
||||
["search_method"] = "by_name",
|
||||
["component_type"] = "UnityEventTestComponent",
|
||||
["property"] = "onSimpleEvent",
|
||||
["value"] = JObject.Parse(@"{
|
||||
""m_PersistentCalls"": {
|
||||
""m_Calls"": [
|
||||
{
|
||||
""m_Target"": { ""instanceID"": " + targetId + @" },
|
||||
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
|
||||
""m_MethodName"": ""SetActive"",
|
||||
""m_Mode"": 6,
|
||||
""m_Arguments"": { ""m_BoolArgument"": true },
|
||||
""m_CallState"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}")
|
||||
};
|
||||
|
||||
var result = ManageComponents.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), $"HandleCommand should succeed: {resultObj}");
|
||||
|
||||
// Verify via SerializedObject
|
||||
var comp = testGo.GetComponent<UnityEventTestComponent>();
|
||||
var so = new SerializedObject(comp);
|
||||
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
|
||||
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call after end-to-end");
|
||||
Assert.AreEqual("SetActive", callsProp.GetArrayElementAtIndex(0).FindPropertyRelative("m_MethodName").stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aaea4d5f23d824f45a6f48cf7ca57407
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ComponentResolverTests
|
||||
{
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve Transform component");
|
||||
Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component");
|
||||
Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForCustomComponentShortName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve CustomComponent");
|
||||
Assert.IsNotNull(type, "Should return valid type");
|
||||
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
|
||||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent");
|
||||
Assert.IsNotNull(type, "Should return valid type");
|
||||
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
|
||||
Assert.AreEqual("TestNamespace.CustomComponent", type.FullName, "Should have correct full name");
|
||||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
|
||||
Assert.IsEmpty(error, "Should have no error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsFalse_ForNonExistentComponent()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsFalse(result, "Should not resolve non-existent component");
|
||||
Assert.IsNull(type, "Should return null type");
|
||||
Assert.IsNotEmpty(error, "Should have error message");
|
||||
Assert.That(error, Does.Contain("not found"), "Error should mention component not found");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsFalse_ForEmptyString()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("", out Type type, out string error);
|
||||
|
||||
Assert.IsFalse(result, "Should not resolve empty string");
|
||||
Assert.IsNull(type, "Should return null type");
|
||||
Assert.IsNotEmpty(error, "Should have error message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_ReturnsFalse_ForNullString()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve(null, out Type type, out string error);
|
||||
|
||||
Assert.IsFalse(result, "Should not resolve null string");
|
||||
Assert.IsNull(type, "Should return null type");
|
||||
Assert.IsNotEmpty(error, "Should have error message");
|
||||
Assert.That(error, Does.Contain("null or empty"), "Error should mention null or empty");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_CachesResolvedTypes()
|
||||
{
|
||||
// First call
|
||||
bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1);
|
||||
|
||||
// Second call should use cache
|
||||
bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2);
|
||||
|
||||
Assert.IsTrue(result1, "First call should succeed");
|
||||
Assert.IsTrue(result2, "Second call should succeed");
|
||||
Assert.AreSame(type1, type2, "Should return same type instance (cached)");
|
||||
Assert.IsEmpty(error1, "First call should have no error");
|
||||
Assert.IsEmpty(error2, "Second call should have no error");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_PrefersPlayerAssemblies()
|
||||
{
|
||||
// Test that custom user scripts (in Player assemblies) are found
|
||||
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve user script from Player assembly");
|
||||
Assert.IsNotNull(type, "Should return valid type");
|
||||
|
||||
// Verify it's not from an Editor assembly by checking the assembly name
|
||||
string assemblyName = type.Assembly.GetName().Name;
|
||||
Assert.That(assemblyName, Does.Not.Contain("Editor"),
|
||||
"User script should come from Player assembly, not Editor assembly");
|
||||
|
||||
// Verify it's from the TestAsmdef assembly (which is a Player assembly)
|
||||
Assert.AreEqual("TestAsmdef", assemblyName,
|
||||
"CustomComponent should be resolved from TestAsmdef assembly");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryResolve_HandlesDuplicateNames_WithAmbiguityError()
|
||||
{
|
||||
// This test would need duplicate component names to be meaningful
|
||||
// For now, test with a built-in component that should not have duplicates
|
||||
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Transform should resolve uniquely");
|
||||
Assert.AreEqual(typeof(Transform), type, "Should return correct type");
|
||||
Assert.IsEmpty(error, "Should have no ambiguity error");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolvedType_IsValidComponent()
|
||||
{
|
||||
bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error);
|
||||
|
||||
Assert.IsTrue(result, "Should resolve Rigidbody");
|
||||
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component");
|
||||
Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) ||
|
||||
typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c15ba6502927e4901a43826c43debd7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using UnityEditor;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for domain reload resilience - ensuring MCP requests succeed even during Unity domain reloads.
|
||||
///
|
||||
/// These tests trigger script compilation which can cause test timing issues when Unity is
|
||||
/// backgrounded, but the MCP workflow itself is unaffected - socket messages provide external
|
||||
/// stimulus that keeps Unity responsive.
|
||||
///
|
||||
/// Note: Focus nudge improvements (P2-9) should help with background test reliability.
|
||||
/// </summary>
|
||||
[Category("domain_reload")]
|
||||
[Explicit("Domain reload stress tests; run manually when needed.")]
|
||||
public class DomainReloadResilienceTests
|
||||
{
|
||||
private const string TempDir = "Assets/Temp/DomainReloadTests";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Ensure temp directory exists
|
||||
if (!AssetDatabase.IsValidFolder(TempDir))
|
||||
{
|
||||
Directory.CreateDirectory(TempDir);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up temp directory - this deletes any scripts we created
|
||||
if (AssetDatabase.IsValidFolder(TempDir))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempDir);
|
||||
}
|
||||
|
||||
// Remove parent temp folder if nothing else is inside
|
||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
var remainingDirs = Directory.GetDirectories("Assets/Temp");
|
||||
var remainingFiles = Directory.GetFiles("Assets/Temp");
|
||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
||||
{
|
||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Force a synchronous refresh and wait for any pending compilation to finish.
|
||||
// This prevents leaving compilation running that could stall subsequent tests.
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test simulates the stress test scenario:
|
||||
/// 1. Create a script (triggers domain reload)
|
||||
/// 2. Make multiple rapid read_console calls
|
||||
/// 3. Verify all calls succeed (no "No Unity plugins are currently connected" errors)
|
||||
///
|
||||
/// Note: This test uses UnityTest coroutine to handle the async nature of domain reloads.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
public IEnumerator StressTest_CreateScriptAndReadConsoleMultipleTimes()
|
||||
{
|
||||
// Step 1: Create a script to trigger domain reload
|
||||
var scriptPath = Path.Combine(TempDir, "StressTestScript.cs").Replace("\\", "/");
|
||||
var scriptContent = @"using UnityEngine;
|
||||
|
||||
public class StressTestScript : MonoBehaviour
|
||||
{
|
||||
void Start() { }
|
||||
}";
|
||||
|
||||
// Write script file
|
||||
File.WriteAllText(scriptPath, scriptContent);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
Debug.Log("[DomainReloadTest] Script created, domain reload triggered");
|
||||
|
||||
// Wait a frame for the domain reload to start
|
||||
yield return null;
|
||||
|
||||
// Step 2: Make multiple rapid read_console calls
|
||||
// These should succeed even during the reload window
|
||||
int successCount = 0;
|
||||
int totalCalls = 5;
|
||||
|
||||
for (int i = 0; i < totalCalls; i++)
|
||||
{
|
||||
var request = new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray { "all" },
|
||||
["count"] = 50,
|
||||
["format"] = "plain",
|
||||
["includeStacktrace"] = false
|
||||
};
|
||||
|
||||
var result = ExecuteReadConsole(request);
|
||||
|
||||
// Check if the call succeeded
|
||||
if (result != null && result["success"]?.Value<bool>() == true)
|
||||
{
|
||||
successCount++;
|
||||
Debug.Log($"[DomainReloadTest] read_console call {i+1}/{totalCalls} succeeded");
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = result?["error"]?.ToString() ?? "Unknown error";
|
||||
Debug.LogError($"[DomainReloadTest] read_console call {i+1}/{totalCalls} failed: {error}");
|
||||
}
|
||||
|
||||
// Small delay between calls to simulate rapid-fire scenario
|
||||
yield return WaitFrames(6);
|
||||
}
|
||||
|
||||
// Step 3: Verify all calls succeeded
|
||||
Debug.Log($"[DomainReloadTest] {successCount}/{totalCalls} read_console calls succeeded");
|
||||
Assert.AreEqual(totalCalls, successCount,
|
||||
$"Expected all {totalCalls} read_console calls to succeed during domain reload, but only {successCount} succeeded");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that read_console works reliably after a domain reload completes.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ReadConsole_AfterDomainReload_Succeeds()
|
||||
{
|
||||
// This test assumes domain reload has already completed
|
||||
// (Unity tests run after domain reload)
|
||||
|
||||
var request = new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray { "error", "warning", "log" },
|
||||
["count"] = 10,
|
||||
["format"] = "plain"
|
||||
};
|
||||
|
||||
var result = ExecuteReadConsole(request);
|
||||
|
||||
Assert.IsNotNull(result, "read_console should return a result");
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false,
|
||||
$"read_console should succeed: {result["error"]}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test creating a script and immediately querying console logs.
|
||||
/// This simulates a common AI workflow pattern.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
public IEnumerator CreateScript_ThenQueryConsole_Succeeds()
|
||||
{
|
||||
// Create a simple script
|
||||
var scriptPath = Path.Combine(TempDir, "TestScript1.cs").Replace("\\", "/");
|
||||
var scriptContent = @"using UnityEngine;
|
||||
|
||||
public class TestScript1 : MonoBehaviour
|
||||
{
|
||||
void Start()
|
||||
{
|
||||
Debug.Log(""TestScript1 initialized"");
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(scriptPath, scriptContent);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
Debug.Log("[DomainReloadTest] Script created");
|
||||
|
||||
// Wait a frame
|
||||
yield return null;
|
||||
|
||||
// Immediately try to read console
|
||||
var request = new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray { "all" },
|
||||
["count"] = 50,
|
||||
["format"] = "plain"
|
||||
};
|
||||
|
||||
var result = ExecuteReadConsole(request);
|
||||
|
||||
// Should succeed even if domain reload is happening
|
||||
Assert.IsNotNull(result, "read_console should return a result");
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false,
|
||||
$"read_console should succeed even during/after script creation: {result["error"]}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test rapid script creation followed by console reads.
|
||||
/// This is an even more aggressive stress test.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
public IEnumerator RapidScriptCreation_WithConsoleReads_AllSucceed()
|
||||
{
|
||||
int scriptCount = 3;
|
||||
int consoleReadsPerScript = 2;
|
||||
int successCount = 0;
|
||||
int totalExpectedReads = scriptCount * consoleReadsPerScript;
|
||||
|
||||
for (int i = 0; i < scriptCount; i++)
|
||||
{
|
||||
// Create script
|
||||
var scriptPath = Path.Combine(TempDir, $"RapidScript{i}.cs").Replace("\\", "/");
|
||||
var scriptContent = $@"using UnityEngine;
|
||||
|
||||
public class RapidScript{i} : MonoBehaviour
|
||||
{{
|
||||
void Start() {{ Debug.Log(""RapidScript{i}""); }}
|
||||
}}";
|
||||
|
||||
File.WriteAllText(scriptPath, scriptContent);
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
Debug.Log($"[DomainReloadTest] Created script {i+1}/{scriptCount}");
|
||||
|
||||
// Immediately try console reads
|
||||
for (int j = 0; j < consoleReadsPerScript; j++)
|
||||
{
|
||||
var request = new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray { "all" },
|
||||
["count"] = 20,
|
||||
["format"] = "plain"
|
||||
};
|
||||
|
||||
var result = ExecuteReadConsole(request);
|
||||
|
||||
if (result != null && result["success"]?.Value<bool>() == true)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = result?["error"]?.ToString() ?? "Unknown error";
|
||||
Debug.LogError($"[DomainReloadTest] Console read failed: {error}");
|
||||
}
|
||||
|
||||
yield return WaitFrames(3);
|
||||
}
|
||||
|
||||
// Brief wait between script creations
|
||||
yield return WaitFrames(12);
|
||||
}
|
||||
|
||||
Debug.Log($"[DomainReloadTest] {successCount}/{totalExpectedReads} console reads succeeded");
|
||||
|
||||
// We expect at least 80% success rate (some may fail due to timing, but resilience should help most)
|
||||
int minExpectedSuccess = (int)(totalExpectedReads * 0.8f);
|
||||
Assert.GreaterOrEqual(successCount, minExpectedSuccess,
|
||||
$"Expected at least {minExpectedSuccess} console reads to succeed, but only {successCount} succeeded");
|
||||
}
|
||||
|
||||
private static JObject ExecuteReadConsole(JObject request)
|
||||
{
|
||||
var raw = ReadConsole.HandleCommand(request);
|
||||
if (raw == null)
|
||||
{
|
||||
return new JObject
|
||||
{
|
||||
["success"] = false,
|
||||
["error"] = "ReadConsole returned null"
|
||||
};
|
||||
}
|
||||
|
||||
return JObject.FromObject(raw);
|
||||
}
|
||||
|
||||
private static IEnumerator WaitFrames(int frameCount)
|
||||
{
|
||||
for (int i = 0; i < frameCount; i++)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3d4e0a7c0814f75a3cf7d1eee7bcdd1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,382 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ExecuteCodeTests
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
ExecuteCode.HandleCommand(new JObject { ["action"] = "clear_history" });
|
||||
}
|
||||
|
||||
// ──────────────────── Execute: success cases ────────────────────
|
||||
|
||||
[Test]
|
||||
public void Execute_ReturnString_ReturnsSuccess()
|
||||
{
|
||||
var result = Execute("return \"hello\";");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual("hello", result["data"]["result"].Value<string>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_ReturnInt_ReturnsSuccess()
|
||||
{
|
||||
var result = Execute("return 42;");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(42, result["data"]["result"].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_ReturnNull_NoResultValue()
|
||||
{
|
||||
var result = Execute("int x = 1; return null;");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
// data may contain compiler info but should not have a "result" key
|
||||
var data = result["data"] as JObject;
|
||||
if (data != null)
|
||||
Assert.IsNull(data["result"], "Expected no 'result' key when code returns null");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_VoidReturn_Succeeds()
|
||||
{
|
||||
var result = Execute("UnityEngine.Debug.Log(\"test\"); return null;");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_UnityAPI_CanAccessSceneManager()
|
||||
{
|
||||
var result = Execute(
|
||||
"var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();\n" +
|
||||
"return scene.name;");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["result"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_Generics_ListOfString()
|
||||
{
|
||||
var result = Execute(
|
||||
"var list = new System.Collections.Generic.List<string>();\n" +
|
||||
"list.Add(\"a\"); list.Add(\"b\");\n" +
|
||||
"return list;");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var arr = result["data"]["result"] as JArray;
|
||||
Assert.IsNotNull(arr, "Expected array result");
|
||||
Assert.AreEqual(2, arr.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_LINQ_SelectWorks()
|
||||
{
|
||||
var result = Execute(
|
||||
"var nums = new int[] { 1, 2, 3 };\n" +
|
||||
"return nums.Select(n => n * 2).ToList();");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var arr = result["data"]["result"] as JArray;
|
||||
Assert.IsNotNull(arr);
|
||||
Assert.AreEqual(3, arr.Count);
|
||||
Assert.AreEqual(2, arr[0].Value<int>());
|
||||
Assert.AreEqual(6, arr[2].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_Dictionary_ReturnsStructured()
|
||||
{
|
||||
var result = Execute(
|
||||
"var dict = new Dictionary<string, int> { { \"a\", 1 }, { \"b\", 2 } };\n" +
|
||||
"return dict;");
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["result"]);
|
||||
}
|
||||
|
||||
// ──────────────────── Execute: error cases ────────────────────
|
||||
|
||||
[Test]
|
||||
public void Execute_CompilationError_ReturnsErrors()
|
||||
{
|
||||
var result = Execute("int x = \"not an int\";");
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("Compilation failed", result.Value<string>("error"));
|
||||
Assert.IsNotNull(result["data"]["errors"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_RuntimeException_ReturnsError()
|
||||
{
|
||||
var result = Execute("throw new System.Exception(\"boom\");");
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("boom", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_MissingCode_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "execute"
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("code", result.Value<string>("error").ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_EmptyCode_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "execute",
|
||||
["code"] = " "
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
// ──────────────────── Safety checks ────────────────────
|
||||
|
||||
[Test]
|
||||
public void Execute_SafetyChecks_BlocksFileDelete()
|
||||
{
|
||||
var result = Execute("System.IO.File.Delete(\"x\");");
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("Blocked pattern", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_SafetyChecks_BlocksProcessStart()
|
||||
{
|
||||
var result = Execute("Process.Start(\"cmd\");");
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("Blocked pattern", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_SafetyChecks_BlocksInfiniteLoop()
|
||||
{
|
||||
var result = Execute("while (true) { }");
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("Blocked pattern", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_SafetyChecksDisabled_AllowsBlockedPattern()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "execute",
|
||||
["code"] = "while (true) { break; } return null;",
|
||||
["safety_checks"] = false
|
||||
}));
|
||||
|
||||
if (!result.Value<bool>("success"))
|
||||
{
|
||||
var error = result.Value<string>("error") ?? "";
|
||||
Assert.IsFalse(error.Contains("Blocked pattern"),
|
||||
"Safety checks should be disabled but still blocked");
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── History ────────────────────
|
||||
|
||||
[Test]
|
||||
public void GetHistory_Empty_ReturnsZero()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_history"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(0, result["data"]["total"].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetHistory_AfterExecution_RecordsEntry()
|
||||
{
|
||||
Execute("return 1;");
|
||||
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_history"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(1, result["data"]["total"].Value<int>());
|
||||
var entries = result["data"]["entries"] as JArray;
|
||||
Assert.IsNotNull(entries);
|
||||
Assert.AreEqual(1, entries.Count);
|
||||
Assert.IsTrue(entries[0]["success"].Value<bool>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetHistory_Limit_RespectsParameter()
|
||||
{
|
||||
Execute("return 1;");
|
||||
Execute("return 2;");
|
||||
Execute("return 3;");
|
||||
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_history",
|
||||
["limit"] = 2
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(3, result["data"]["total"].Value<int>());
|
||||
var entries = result["data"]["entries"] as JArray;
|
||||
Assert.AreEqual(2, entries.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClearHistory_RemovesAll()
|
||||
{
|
||||
Execute("return 1;");
|
||||
Execute("return 2;");
|
||||
|
||||
var clearResult = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "clear_history"
|
||||
}));
|
||||
Assert.IsTrue(clearResult.Value<bool>("success"), clearResult.ToString());
|
||||
|
||||
var historyResult = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_history"
|
||||
}));
|
||||
Assert.AreEqual(0, historyResult["data"]["total"].Value<int>());
|
||||
}
|
||||
|
||||
// ──────────────────── Replay ────────────────────
|
||||
|
||||
[Test]
|
||||
public void Replay_ValidIndex_ReExecutes()
|
||||
{
|
||||
Execute("return 42;");
|
||||
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "replay",
|
||||
["index"] = 0
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(42, result["data"]["result"].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replay_InvalidIndex_ReturnsError()
|
||||
{
|
||||
Execute("return 1;");
|
||||
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "replay",
|
||||
["index"] = 99
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("Invalid history index", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replay_EmptyHistory_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "replay",
|
||||
["index"] = 0
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
// ──────────────────── Action validation ────────────────────
|
||||
|
||||
[Test]
|
||||
public void UnknownAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "invalid_action"
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
StringAssert.Contains("Unknown action", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NullParams_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(null));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
// ──────────────────── CodeDom backend ────────────────────
|
||||
|
||||
// Regression for CoplayDev/unity-mcp#1144: large projects (~100+ asmdefs) blew past the
|
||||
// Windows 32 KB CreateProcess limit because every reference became an inline /r: flag.
|
||||
// The fix routes references through a @responsefile, so this just verifies that the
|
||||
// codedom path still compiles and runs end-to-end.
|
||||
[Test]
|
||||
public void Execute_CodedomBackend_CompilesAndRuns()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "execute",
|
||||
["code"] = "return 1 + 1;",
|
||||
["compiler"] = "codedom"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(2, result["data"]["result"].Value<int>());
|
||||
Assert.AreEqual("codedom", result["data"]["compiler"].Value<string>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_CodedomBackend_ResolvesUnityTypes()
|
||||
{
|
||||
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "execute",
|
||||
["code"] = "return UnityEngine.Application.unityVersion;",
|
||||
["compiler"] = "codedom"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["result"]);
|
||||
}
|
||||
|
||||
// ──────────────────── Helpers ────────────────────
|
||||
|
||||
private static JObject Execute(string code)
|
||||
{
|
||||
return ToJObject(ExecuteCode.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "execute",
|
||||
["code"] = code
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ada2677bb6004017b54f9dea47e403a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ExecuteMenuItemTests
|
||||
{
|
||||
private static JObject ToJO(object o) => JObject.FromObject(o);
|
||||
|
||||
[Test]
|
||||
public void Execute_MissingParam_ReturnsError()
|
||||
{
|
||||
var res = ExecuteMenuItem.HandleCommand(new JObject());
|
||||
var jo = ToJO(res);
|
||||
Assert.IsFalse((bool)jo["success"], "Expected success false");
|
||||
StringAssert.Contains("Required parameter", (string)jo["error"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_Blacklisted_ReturnsError()
|
||||
{
|
||||
var res = ExecuteMenuItem.HandleCommand(new JObject { ["menuPath"] = "File/Quit" });
|
||||
var jo = ToJO(res);
|
||||
Assert.IsFalse((bool)jo["success"], "Expected success false for blacklisted menu");
|
||||
StringAssert.Contains("blocked for safety", (string)jo["error"], "Expected blacklist message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_NonBlacklisted_ReturnsImmediateSuccess()
|
||||
{
|
||||
// We don't rely on the menu actually existing; execution is delayed and we only check the immediate response shape
|
||||
var res = ExecuteMenuItem.HandleCommand(new JObject { ["menuPath"] = "File/Save Project" });
|
||||
var jo = ToJO(res);
|
||||
Assert.IsTrue((bool)jo["success"], "Expected immediate success response");
|
||||
StringAssert.Contains("Attempted to execute menu item", (string)jo["message"], "Expected attempt message");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae694b6ac48824768a319eb378e7fb63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbad1c3ddb00a48918a2b59a2a1714cb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools.Fixtures
|
||||
{
|
||||
[Serializable]
|
||||
public struct ManageScriptableObjectNestedData
|
||||
{
|
||||
public string note;
|
||||
}
|
||||
|
||||
// NOTE: File name matches class name so Unity can resolve a MonoScript asset for this ScriptableObject type.
|
||||
public class ManageScriptableObjectTestDefinition : ManageScriptableObjectTestDefinitionBase
|
||||
{
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField] private List<Material> materials = new();
|
||||
[SerializeField] private ManageScriptableObjectNestedData nested;
|
||||
|
||||
public string DisplayName => displayName;
|
||||
public IReadOnlyList<Material> Materials => materials;
|
||||
public string NestedNote => nested.note;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba28697c0d65145a1ad753ef73c53185
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools.Fixtures
|
||||
{
|
||||
// NOTE: File name matches class name so Unity can resolve a MonoScript asset for this ScriptableObject type.
|
||||
public class ManageScriptableObjectTestDefinitionBase : ScriptableObject
|
||||
{
|
||||
[SerializeField] private int baseNumber = 1;
|
||||
public int BaseNumber => baseNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a757068d676ee47dba1045bfb8b8fb12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8f17bd366ad941fc95a0b60a727a90d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "ArrayStressSO", menuName = "StressTests/ArrayStressSO")]
|
||||
public class ArrayStressSO : ScriptableObject
|
||||
{
|
||||
public float[] floatArray = new float[3];
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9eec250e4deee48c69c12acfde8c2adc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[System.Serializable]
|
||||
public struct NestedData
|
||||
{
|
||||
public string id;
|
||||
public float value;
|
||||
public Vector3 position;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class ComplexSubClass
|
||||
{
|
||||
public string name;
|
||||
public int level;
|
||||
public List<float> scores;
|
||||
}
|
||||
|
||||
public enum TestEnum
|
||||
{
|
||||
Alpha,
|
||||
Beta,
|
||||
Gamma
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "ComplexStressSO", menuName = "StressTests/ComplexStressSO")]
|
||||
public class ComplexStressSO : ScriptableObject
|
||||
{
|
||||
[Header("Basic Types")]
|
||||
public int intValue;
|
||||
public float floatValue;
|
||||
public string stringValue;
|
||||
public bool boolValue;
|
||||
public Vector3 vectorValue;
|
||||
public Color colorValue;
|
||||
public TestEnum enumValue;
|
||||
|
||||
[Header("Arrays & Lists")]
|
||||
public int[] intArray;
|
||||
public List<string> stringList;
|
||||
public Vector3[] vectorArray;
|
||||
|
||||
[Header("Complex Types")]
|
||||
public NestedData nestedStruct;
|
||||
public ComplexSubClass nestedClass;
|
||||
public List<NestedData> nestedDataList;
|
||||
|
||||
[Header("Extended Types (Phase 6)")]
|
||||
public AnimationCurve animCurve;
|
||||
public Quaternion rotation;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a73b1ff3fe1fa4b3fadb70c7c257d5a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[System.Serializable]
|
||||
public class Level3
|
||||
{
|
||||
public string detail;
|
||||
public Vector3 pos;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class Level2
|
||||
{
|
||||
public string midName;
|
||||
public Level3 deep;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class Level1
|
||||
{
|
||||
public string topName;
|
||||
public Level2 mid;
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "DeepStressSO", menuName = "StressTests/DeepStressSO")]
|
||||
public class DeepStressSO : ScriptableObject
|
||||
{
|
||||
public Level1 level1;
|
||||
public Color overtone;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc8fe16aef3ae4cbc949300f5fed2187
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Fusion
|
||||
{
|
||||
public struct NetworkBehaviourBuffer
|
||||
{
|
||||
public int Value;
|
||||
}
|
||||
|
||||
public struct Changed<T>
|
||||
{
|
||||
public T Value;
|
||||
}
|
||||
}
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class FusionUnsafeTypeSerializationTests
|
||||
{
|
||||
[Test]
|
||||
public void GetComponentData_SkipsFusionUnsafeTypesInsideContainers()
|
||||
{
|
||||
var testObject = new GameObject("FusionUnsafeTypeTestObject");
|
||||
|
||||
try
|
||||
{
|
||||
var component = testObject.AddComponent<FusionUnsafeTypeComponent>();
|
||||
component.bufferList.Add(new Fusion.NetworkBehaviourBuffer { Value = 1 });
|
||||
component.nestedChangedLookup["changed"] = new List<Fusion.Changed<int>>
|
||||
{
|
||||
new Fusion.Changed<int> { Value = 2 }
|
||||
};
|
||||
component.ChangedListProperty.Add(new Fusion.Changed<int> { Value = 3 });
|
||||
|
||||
var result = GameObjectSerializer.GetComponentData(component) as Dictionary<string, object>;
|
||||
|
||||
Assert.IsNotNull(result, "GetComponentData should return dictionary data.");
|
||||
Assert.IsTrue(result.TryGetValue("properties", out object propertiesObject), "Serialized data should contain properties.");
|
||||
|
||||
var properties = propertiesObject as Dictionary<string, object>;
|
||||
Assert.IsNotNull(properties, "Serialized properties should be a dictionary.");
|
||||
|
||||
Assert.IsTrue(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.safeValue)), "Safe fields should still serialize.");
|
||||
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.directBuffer)), "Direct Fusion buffer fields should be skipped.");
|
||||
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.bufferList)), "Collections containing Fusion buffer types should be skipped.");
|
||||
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.nestedChangedLookup)), "Nested generic containers containing Fusion Changed<T> should be skipped.");
|
||||
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.ChangedListProperty)), "Properties returning collections of Fusion Changed<T> should be skipped.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FusionUnsafeTypeComponent : MonoBehaviour
|
||||
{
|
||||
public string safeValue = "kept";
|
||||
public Fusion.NetworkBehaviourBuffer directBuffer;
|
||||
public List<Fusion.NetworkBehaviourBuffer> bufferList = new List<Fusion.NetworkBehaviourBuffer>();
|
||||
public Dictionary<string, List<Fusion.Changed<int>>> nestedChangedLookup = new Dictionary<string, List<Fusion.Changed<int>>>();
|
||||
|
||||
public List<Fusion.Changed<int>> ChangedListProperty { get; } = new List<Fusion.Changed<int>>();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a2e39a143bd4d46a03d27cc30468d2e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,570 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Resources.Scene;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Stress tests for the GameObject API redesign.
|
||||
/// Tests volume operations, pagination, and performance with large datasets.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class GameObjectAPIStressTests
|
||||
{
|
||||
private List<GameObject> _createdObjects = new List<GameObject>();
|
||||
private const int SMALL_BATCH = 10;
|
||||
private const int MEDIUM_BATCH = 50;
|
||||
private const int LARGE_BATCH = 100;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_createdObjects.Clear();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in _createdObjects)
|
||||
{
|
||||
if (go != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
_createdObjects.Clear();
|
||||
}
|
||||
|
||||
private GameObject CreateTestObject(string name)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
_createdObjects.Add(go);
|
||||
return go;
|
||||
}
|
||||
|
||||
#region Bulk GameObject Creation
|
||||
|
||||
[Test]
|
||||
public void BulkCreate_SmallBatch_AllSucceed()
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (int i = 0; i < SMALL_BATCH; i++)
|
||||
{
|
||||
var result = ToJObject(ManageGameObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = $"BulkTest_{i}"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Failed to create object {i}");
|
||||
|
||||
// Track for cleanup
|
||||
int instanceId = result["data"]?["instanceID"]?.Value<int>() ?? 0;
|
||||
if (instanceId != 0)
|
||||
{
|
||||
var go = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
|
||||
if (go != null) _createdObjects.Add(go);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Debug.Log($"[BulkCreate] Created {SMALL_BATCH} objects in {sw.ElapsedMilliseconds}ms");
|
||||
// Use generous threshold for CI variability
|
||||
Assert.Less(sw.ElapsedMilliseconds, 10000, "Bulk create took too long (CI threshold)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BulkCreate_MediumBatch_AllSucceed()
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (int i = 0; i < MEDIUM_BATCH; i++)
|
||||
{
|
||||
var result = ToJObject(ManageGameObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = $"MediumBulk_{i}"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Failed to create object {i}");
|
||||
|
||||
int instanceId = result["data"]?["instanceID"]?.Value<int>() ?? 0;
|
||||
if (instanceId != 0)
|
||||
{
|
||||
var go = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
|
||||
if (go != null) _createdObjects.Add(go);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Debug.Log($"[BulkCreate] Created {MEDIUM_BATCH} objects in {sw.ElapsedMilliseconds}ms");
|
||||
Assert.Less(sw.ElapsedMilliseconds, 15000, "Medium batch create took too long");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Find GameObjects Pagination
|
||||
|
||||
[Test]
|
||||
public void FindGameObjects_LargeBatch_PaginatesCorrectly()
|
||||
{
|
||||
// Create many objects with a unique marker component for reliable search
|
||||
for (int i = 0; i < LARGE_BATCH; i++)
|
||||
{
|
||||
var go = CreateTestObject($"Searchable_{i:D3}");
|
||||
go.AddComponent<GameObjectAPIStressTestMarker>();
|
||||
}
|
||||
|
||||
// Find by searching for a specific object first
|
||||
var firstResult = ToJObject(FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "Searchable_000",
|
||||
["searchMethod"] = "by_name",
|
||||
["pageSize"] = 10
|
||||
}));
|
||||
|
||||
Assert.IsTrue(firstResult["success"]?.Value<bool>() ?? false, "Should find specific named object");
|
||||
var firstData = firstResult["data"] as JObject;
|
||||
var firstIds = firstData?["instanceIDs"] as JArray;
|
||||
Assert.IsNotNull(firstIds);
|
||||
Assert.AreEqual(1, firstIds.Count, "Should find exactly one object with exact name match");
|
||||
|
||||
Debug.Log($"[FindGameObjects] Found object by exact name. Testing pagination with a unique marker component.");
|
||||
|
||||
// Now test pagination by searching for only the objects created by this test
|
||||
var result = ToJObject(FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = typeof(GameObjectAPIStressTestMarker).FullName,
|
||||
["searchMethod"] = "by_component",
|
||||
["pageSize"] = 25
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
|
||||
var instanceIds = data["instanceIDs"] as JArray;
|
||||
Assert.IsNotNull(instanceIds);
|
||||
Assert.AreEqual(25, instanceIds.Count, "First page should have 25 items");
|
||||
|
||||
int totalCount = data["totalCount"]?.Value<int>() ?? 0;
|
||||
Assert.AreEqual(LARGE_BATCH, totalCount, $"Should find exactly {LARGE_BATCH} objects created by this test");
|
||||
|
||||
bool hasMore = data["hasMore"]?.Value<bool>() ?? false;
|
||||
Assert.IsTrue(hasMore, "Should have more pages");
|
||||
|
||||
Debug.Log($"[FindGameObjects] Found {totalCount} objects, first page has {instanceIds.Count}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindGameObjects_PaginateThroughAll()
|
||||
{
|
||||
// Create objects - all will have a unique marker component
|
||||
for (int i = 0; i < MEDIUM_BATCH; i++)
|
||||
{
|
||||
var go = CreateTestObject($"Paginate_{i:D3}");
|
||||
go.AddComponent<GameObjectAPIStressTestMarker>();
|
||||
}
|
||||
|
||||
// Track IDs we've created for verification
|
||||
var createdIds = new HashSet<int>();
|
||||
foreach (var go in _createdObjects)
|
||||
{
|
||||
if (go != null && go.name.StartsWith("Paginate_"))
|
||||
{
|
||||
createdIds.Add(go.GetInstanceID());
|
||||
}
|
||||
}
|
||||
|
||||
int pageSize = 10;
|
||||
int cursor = 0;
|
||||
int foundFromCreated = 0;
|
||||
int pageCount = 0;
|
||||
|
||||
// Search by the unique marker component and check our created objects
|
||||
while (true)
|
||||
{
|
||||
var result = ToJObject(FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = typeof(GameObjectAPIStressTestMarker).FullName,
|
||||
["searchMethod"] = "by_component",
|
||||
["pageSize"] = pageSize,
|
||||
["cursor"] = cursor
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
|
||||
var data = result["data"] as JObject;
|
||||
var instanceIds = data["instanceIDs"] as JArray;
|
||||
|
||||
// Count how many of our created objects are in this page
|
||||
foreach (var id in instanceIds)
|
||||
{
|
||||
if (createdIds.Contains(id.Value<int>()))
|
||||
{
|
||||
foundFromCreated++;
|
||||
}
|
||||
}
|
||||
pageCount++;
|
||||
|
||||
bool hasMore = data["hasMore"]?.Value<bool>() ?? false;
|
||||
if (!hasMore) break;
|
||||
|
||||
cursor = data["nextCursor"]?.Value<int>() ?? cursor + pageSize;
|
||||
|
||||
// Safety limit
|
||||
if (pageCount > 50) break;
|
||||
}
|
||||
|
||||
Assert.AreEqual(MEDIUM_BATCH, foundFromCreated, $"Should find all {MEDIUM_BATCH} created objects across pages");
|
||||
Debug.Log($"[Pagination] Found {foundFromCreated} created objects across {pageCount} pages");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Operations at Scale
|
||||
|
||||
[Test]
|
||||
public void AddComponents_MultipleToSingleObject()
|
||||
{
|
||||
var go = CreateTestObject("ComponentHost");
|
||||
|
||||
string[] componentTypeNames = new[]
|
||||
{
|
||||
"BoxCollider",
|
||||
"Rigidbody",
|
||||
"Light",
|
||||
"Camera"
|
||||
};
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
foreach (var compType in componentTypeNames)
|
||||
{
|
||||
var result = ToJObject(ManageComponents.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add",
|
||||
["target"] = go.GetInstanceID().ToString(),
|
||||
["searchMethod"] = "by_id",
|
||||
["componentType"] = compType // Correct parameter name
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Failed to add {compType}: {result["message"]}");
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Debug.Log($"[AddComponents] Added {componentTypeNames.Length} components in {sw.ElapsedMilliseconds}ms");
|
||||
|
||||
// Verify all components present
|
||||
Assert.AreEqual(componentTypeNames.Length + 1, go.GetComponents<Component>().Length); // +1 for Transform
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponents_ObjectWithManyComponents()
|
||||
{
|
||||
var go = CreateTestObject("HeavyComponents");
|
||||
|
||||
// Add many components - but skip AudioSource as it triggers deprecated API warnings
|
||||
go.AddComponent<BoxCollider>();
|
||||
go.AddComponent<SphereCollider>();
|
||||
go.AddComponent<CapsuleCollider>();
|
||||
go.AddComponent<MeshCollider>();
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<Light>();
|
||||
go.AddComponent<Camera>();
|
||||
go.AddComponent<AudioListener>();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// Use the resource handler for getting components
|
||||
var result = ToJObject(GameObjectComponentsResource.HandleCommand(new JObject
|
||||
{
|
||||
["instanceID"] = go.GetInstanceID(),
|
||||
["includeProperties"] = true,
|
||||
["pageSize"] = 50
|
||||
}));
|
||||
|
||||
sw.Stop();
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"GetComponents failed: {result["message"]}");
|
||||
var data = result["data"] as JObject;
|
||||
var components = data?["components"] as JArray;
|
||||
|
||||
Assert.IsNotNull(components);
|
||||
Assert.AreEqual(9, components.Count); // 8 added + Transform
|
||||
|
||||
Debug.Log($"[GetComponents] Retrieved {components.Count} components with properties in {sw.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_ComplexRigidbody()
|
||||
{
|
||||
var go = CreateTestObject("RigidbodyTest");
|
||||
go.AddComponent<Rigidbody>();
|
||||
|
||||
var result = ToJObject(ManageComponents.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = go.GetInstanceID().ToString(),
|
||||
["searchMethod"] = "by_id",
|
||||
["componentType"] = "Rigidbody", // Correct parameter name
|
||||
["properties"] = new JObject // Correct parameter name
|
||||
{
|
||||
["mass"] = 10.5f,
|
||||
["drag"] = 0.5f,
|
||||
["angularDrag"] = 0.1f,
|
||||
["useGravity"] = false,
|
||||
["isKinematic"] = true
|
||||
}
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Set property failed: {result["message"]}");
|
||||
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
Assert.AreEqual(10.5f, rb.mass, 0.01f);
|
||||
Assert.AreEqual(0.5f, rb.drag, 0.01f);
|
||||
Assert.AreEqual(0.1f, rb.angularDrag, 0.01f);
|
||||
Assert.IsFalse(rb.useGravity);
|
||||
Assert.IsTrue(rb.isKinematic);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deep Hierarchy Operations
|
||||
|
||||
[Test]
|
||||
public void CreateDeepHierarchy_FindByPath()
|
||||
{
|
||||
// Create a deep hierarchy: Root/Level1/Level2/Level3/Target
|
||||
var root = CreateTestObject("DeepRoot");
|
||||
var current = root;
|
||||
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
var child = CreateTestObject($"Level{i}");
|
||||
child.transform.SetParent(current.transform);
|
||||
current = child;
|
||||
}
|
||||
|
||||
var target = CreateTestObject("DeepTarget");
|
||||
target.transform.SetParent(current.transform);
|
||||
|
||||
// Find by path
|
||||
var result = ToJObject(FindGameObjects.HandleCommand(new JObject
|
||||
{
|
||||
["searchTerm"] = "DeepRoot/Level1/Level2/Level3/Level4/Level5/DeepTarget",
|
||||
["searchMethod"] = "by_path"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
|
||||
var data = result["data"] as JObject;
|
||||
var ids = data?["instanceIDs"] as JArray;
|
||||
|
||||
Assert.IsNotNull(ids);
|
||||
Assert.AreEqual(1, ids.Count);
|
||||
Assert.AreEqual(target.GetInstanceID(), ids[0].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetHierarchy_LargeScene_Paginated()
|
||||
{
|
||||
// Create flat hierarchy with many objects
|
||||
for (int i = 0; i < MEDIUM_BATCH; i++)
|
||||
{
|
||||
CreateTestObject($"HierarchyItem_{i:D3}");
|
||||
}
|
||||
|
||||
var result = ToJObject(ManageScene.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_hierarchy",
|
||||
["pageSize"] = 20,
|
||||
["maxNodes"] = 100
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
|
||||
var data = result["data"] as JObject;
|
||||
var items = data?["items"] as JArray;
|
||||
|
||||
Assert.IsNotNull(items);
|
||||
Assert.GreaterOrEqual(items.Count, 1);
|
||||
|
||||
// Verify componentTypes is included
|
||||
var firstItem = items[0] as JObject;
|
||||
Assert.IsNotNull(firstItem?["componentTypes"], "Should include componentTypes");
|
||||
|
||||
Debug.Log($"[GetHierarchy] Retrieved {items.Count} items from hierarchy");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resource Read Performance
|
||||
|
||||
[Test]
|
||||
public void GameObjectResource_ReadComplexObject()
|
||||
{
|
||||
var go = CreateTestObject("ComplexObject");
|
||||
go.tag = "Player";
|
||||
go.layer = 8;
|
||||
go.isStatic = true;
|
||||
|
||||
// Add components - AudioSource is OK here since we're only reading component types, not serializing properties
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<BoxCollider>();
|
||||
go.AddComponent<AudioSource>();
|
||||
|
||||
// Add children
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var child = CreateTestObject($"Child_{i}");
|
||||
child.transform.SetParent(go.transform);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// Call the resource directly (no action param needed)
|
||||
var result = ToJObject(GameObjectResource.HandleCommand(new JObject
|
||||
{
|
||||
["instanceID"] = go.GetInstanceID()
|
||||
}));
|
||||
|
||||
sw.Stop();
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
|
||||
var data = result["data"] as JObject;
|
||||
|
||||
Assert.AreEqual("ComplexObject", data?["name"]?.Value<string>());
|
||||
Assert.AreEqual("Player", data?["tag"]?.Value<string>());
|
||||
Assert.AreEqual(8, data?["layer"]?.Value<int>());
|
||||
|
||||
var componentTypes = data?["componentTypes"] as JArray;
|
||||
Assert.IsNotNull(componentTypes);
|
||||
Assert.AreEqual(4, componentTypes.Count); // Transform + 3 added
|
||||
|
||||
var children = data?["children"] as JArray;
|
||||
Assert.IsNotNull(children);
|
||||
Assert.AreEqual(5, children.Count);
|
||||
|
||||
Debug.Log($"[GameObjectResource] Read complex object in {sw.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComponentsResource_ReadAllWithFullSerialization()
|
||||
{
|
||||
var go = CreateTestObject("FullSerialize");
|
||||
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
rb.mass = 5.5f;
|
||||
rb.drag = 1.2f;
|
||||
|
||||
var col = go.AddComponent<BoxCollider>();
|
||||
col.size = new Vector3(2, 3, 4);
|
||||
col.center = new Vector3(0.5f, 0.5f, 0.5f);
|
||||
|
||||
// Skip AudioSource to avoid deprecated API warnings
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// Use the components resource handler
|
||||
var result = ToJObject(GameObjectComponentsResource.HandleCommand(new JObject
|
||||
{
|
||||
["instanceID"] = go.GetInstanceID(),
|
||||
["includeProperties"] = true
|
||||
}));
|
||||
|
||||
sw.Stop();
|
||||
|
||||
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
|
||||
var data = result["data"] as JObject;
|
||||
var components = data?["components"] as JArray;
|
||||
|
||||
Assert.IsNotNull(components);
|
||||
Assert.AreEqual(3, components.Count); // Transform + Rigidbody + BoxCollider
|
||||
|
||||
Debug.Log($"[ComponentsResource] Full serialization of {components.Count} components in {sw.ElapsedMilliseconds}ms");
|
||||
|
||||
// Verify serialized data includes properties
|
||||
bool foundRigidbody = false;
|
||||
foreach (JObject comp in components)
|
||||
{
|
||||
var typeName = comp["typeName"]?.Value<string>();
|
||||
if (typeName != null && typeName.Contains("Rigidbody"))
|
||||
{
|
||||
foundRigidbody = true;
|
||||
// GameObjectSerializer puts properties inside a "properties" nested object
|
||||
var props = comp["properties"] as JObject;
|
||||
Assert.IsNotNull(props, $"Rigidbody should have properties. Component data: {comp}");
|
||||
float massValue = props["mass"]?.Value<float>() ?? 0;
|
||||
Assert.AreEqual(5.5f, massValue, 0.01f, $"Mass should be 5.5");
|
||||
}
|
||||
}
|
||||
Assert.IsTrue(foundRigidbody, "Should find Rigidbody with serialized properties");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Concurrent-Like Operations
|
||||
|
||||
[Test]
|
||||
public void RapidFireOperations_CreateModifyDelete()
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (int i = 0; i < SMALL_BATCH; i++)
|
||||
{
|
||||
// Create
|
||||
var createResult = ToJObject(ManageGameObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = $"RapidFire_{i}"
|
||||
}));
|
||||
Assert.IsTrue(createResult["success"]?.Value<bool>() ?? false, $"Create failed: {createResult["message"]}");
|
||||
|
||||
int instanceId = createResult["data"]?["instanceID"]?.Value<int>() ?? 0;
|
||||
Assert.AreNotEqual(0, instanceId, "Instance ID should not be 0");
|
||||
|
||||
// Modify - use layer 0 (Default) to avoid layer name issues
|
||||
var modifyResult = ToJObject(ManageGameObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = instanceId.ToString(),
|
||||
["searchMethod"] = "by_id",
|
||||
["name"] = $"RapidFire_Modified_{i}", // Use name modification instead
|
||||
["setActive"] = true
|
||||
}));
|
||||
Assert.IsTrue(modifyResult["success"]?.Value<bool>() ?? false, $"Modify failed: {modifyResult["message"]}");
|
||||
|
||||
// Delete
|
||||
var deleteResult = ToJObject(ManageGameObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = instanceId.ToString(),
|
||||
["searchMethod"] = "by_id"
|
||||
}));
|
||||
Assert.IsTrue(deleteResult["success"]?.Value<bool>() ?? false, $"Delete failed: {deleteResult["message"]}");
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Debug.Log($"[RapidFire] {SMALL_BATCH} create-modify-delete cycles in {sw.ElapsedMilliseconds}ms");
|
||||
Assert.Less(sw.ElapsedMilliseconds, 10000, "Rapid fire operations took too long");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marker component used for isolating component-based searches to objects created by this test fixture.
|
||||
/// </summary>
|
||||
public sealed class GameObjectAPIStressTestMarker : MonoBehaviour { }
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7392c46e26c4649479cce9912fa94c1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for GameObjectComponentHelpers.SetComponentPropertiesInternal error reporting.
|
||||
/// Reproduces issue #765: conversion failures incorrectly reported as "Property not found".
|
||||
/// </summary>
|
||||
public class GameObjectComponentHelpersErrorTests
|
||||
{
|
||||
private GameObject testGo;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
testGo = new GameObject("ErrorTestGO");
|
||||
CommandRegistry.Initialize();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (testGo != null)
|
||||
Object.DestroyImmediate(testGo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a property exists but conversion fails, the error should say
|
||||
/// "Failed to convert" rather than "Property not found. Did you mean: X?"
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SetComponentProperties_ConversionFailure_ReportsConversionError_NotPropertyNotFound()
|
||||
{
|
||||
// Expect conversion error log from PropertyConversion (ComponentOps reflection attempt)
|
||||
LogAssert.Expect(LogType.Error, new Regex("Error converting token"));
|
||||
// Expect the warning log from SetComponentPropertiesInternal
|
||||
LogAssert.Expect(LogType.Warning, new Regex("Failed to set"));
|
||||
|
||||
var audioSource = testGo.AddComponent<AudioSource>();
|
||||
|
||||
// spatialBlend is a float property — passing an array triggers conversion failure
|
||||
var props = new JObject { ["spatialBlend"] = JArray.Parse("[1, 2, 3]") };
|
||||
|
||||
var result = GameObjectComponentHelpers.SetComponentPropertiesInternal(
|
||||
testGo, "AudioSource", props, audioSource);
|
||||
|
||||
Assert.IsNotNull(result, "Should return an error response");
|
||||
Assert.IsInstanceOf<ErrorResponse>(result);
|
||||
|
||||
var errorResponse = (ErrorResponse)result;
|
||||
|
||||
// The error message must NOT say "not found" for a property that exists
|
||||
Assert.IsFalse(
|
||||
errorResponse.Error.Contains("not found"),
|
||||
$"Error should report conversion failure, not 'not found'. Got: {errorResponse.Error}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a property genuinely doesn't exist, the error should still say "not found" with suggestions.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SetComponentProperties_NonexistentProperty_ReportsNotFound()
|
||||
{
|
||||
// Expect the "not found" warning
|
||||
LogAssert.Expect(LogType.Warning, new Regex("not found"));
|
||||
|
||||
var audioSource = testGo.AddComponent<AudioSource>();
|
||||
|
||||
var props = new JObject { ["totallyFakeProperty"] = 42 };
|
||||
|
||||
var result = GameObjectComponentHelpers.SetComponentPropertiesInternal(
|
||||
testGo, "AudioSource", props, audioSource);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsInstanceOf<ErrorResponse>(result);
|
||||
|
||||
var errorResponse = (ErrorResponse)result;
|
||||
|
||||
Assert.IsTrue(
|
||||
errorResponse.Error.Contains("not found") || errorResponse.Error.Contains("failed"),
|
||||
$"Error for nonexistent property should say 'not found'. Got: {errorResponse.Error}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Valid property setting should still succeed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SetComponentProperties_ValidProperty_Succeeds()
|
||||
{
|
||||
var audioSource = testGo.AddComponent<AudioSource>();
|
||||
|
||||
var props = new JObject { ["volume"] = 0.42f };
|
||||
|
||||
var result = GameObjectComponentHelpers.SetComponentPropertiesInternal(
|
||||
testGo, "AudioSource", props, audioSource);
|
||||
|
||||
Assert.IsNull(result, "Should return null on success (no errors)");
|
||||
Assert.AreEqual(0.42f, audioSource.volume, 0.001f);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51225acc846dc412b82750041eb0ee61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,381 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using UnityEditor;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for MCP tool parameter handling - JSON parsing in manage_asset and manage_gameobject tools.
|
||||
/// Consolidated from multiple redundant tests into focused, non-overlapping test cases.
|
||||
/// </summary>
|
||||
public class MCPToolParameterTests
|
||||
{
|
||||
private const string TempDir = "Assets/Temp/MCPToolParameterTests";
|
||||
private const string TempLiveDir = "Assets/Temp/LiveTests";
|
||||
|
||||
private static void AssertColorsEqual(Color expected, Color actual, string message)
|
||||
{
|
||||
const float tolerance = 0.001f;
|
||||
Assert.AreEqual(expected.r, actual.r, tolerance, $"{message} - Red component mismatch");
|
||||
Assert.AreEqual(expected.g, actual.g, tolerance, $"{message} - Green component mismatch");
|
||||
Assert.AreEqual(expected.b, actual.b, tolerance, $"{message} - Blue component mismatch");
|
||||
Assert.AreEqual(expected.a, actual.a, tolerance, $"{message} - Alpha component mismatch");
|
||||
}
|
||||
|
||||
private static void AssertShaderIsSupported(Shader s)
|
||||
{
|
||||
Assert.IsNotNull(s, "Shader should not be null");
|
||||
var name = s.name;
|
||||
bool ok = name == "Universal Render Pipeline/Lit"
|
||||
|| name == "HDRP/Lit"
|
||||
|| name == "Standard"
|
||||
|| name == "Unlit/Color";
|
||||
Assert.IsTrue(ok, $"Unexpected shader: {name}");
|
||||
}
|
||||
|
||||
private static void EnsureTempFolders()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
if (!AssetDatabase.IsValidFolder(TempDir))
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
|
||||
if (!AssetDatabase.IsValidFolder(TempLiveDir))
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(TempDir))
|
||||
AssetDatabase.DeleteAsset(TempDir);
|
||||
if (AssetDatabase.IsValidFolder(TempLiveDir))
|
||||
AssetDatabase.DeleteAsset(TempLiveDir);
|
||||
|
||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
var remainingDirs = Directory.GetDirectories("Assets/Temp");
|
||||
var remainingFiles = Directory.GetFiles("Assets/Temp");
|
||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests GameObject componentProperties JSON string coercion path.
|
||||
/// Verifies material assignment via JSON string works correctly.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ManageGameObject_JSONComponentProperties_AssignsMaterial()
|
||||
{
|
||||
EnsureTempFolders();
|
||||
var matPath = $"{TempDir}/Mat_{Guid.NewGuid():N}.mat";
|
||||
|
||||
// Create material with object-typed properties
|
||||
var createMat = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = matPath,
|
||||
["assetType"] = "Material",
|
||||
["properties"] = new JObject { ["shader"] = "Universal Render Pipeline/Lit", ["color"] = new JArray(0, 0, 1, 1) }
|
||||
};
|
||||
var createMatRes = ManageAsset.HandleCommand(createMat);
|
||||
var createMatObj = createMatRes as JObject ?? JObject.FromObject(createMatRes);
|
||||
Assert.IsTrue(createMatObj.Value<bool>("success"), createMatObj.ToString());
|
||||
|
||||
// Create a sphere
|
||||
var createGo = new JObject { ["action"] = "create", ["name"] = "MCPParamTestSphere", ["primitiveType"] = "Sphere" };
|
||||
var createGoRes = ManageGameObject.HandleCommand(createGo);
|
||||
var createGoObj = createGoRes as JObject ?? JObject.FromObject(createGoRes);
|
||||
Assert.IsTrue(createGoObj.Value<bool>("success"), createGoObj.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
// Assign material via JSON string componentProperties (coercion path)
|
||||
var compJsonObj = new JObject { ["MeshRenderer"] = new JObject { ["sharedMaterial"] = matPath } };
|
||||
var compJson = compJsonObj.ToString(Newtonsoft.Json.Formatting.None);
|
||||
var modify = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "MCPParamTestSphere",
|
||||
["searchMethod"] = "by_name",
|
||||
["componentProperties"] = compJson
|
||||
};
|
||||
var raw = ManageGameObject.HandleCommand(modify);
|
||||
var result = raw as JObject ?? JObject.FromObject(raw);
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Verify material assignment
|
||||
var goVerify = GameObject.Find("MCPParamTestSphere");
|
||||
Assert.IsNotNull(goVerify, "GameObject should exist");
|
||||
var renderer = goVerify.GetComponent<MeshRenderer>();
|
||||
Assert.IsNotNull(renderer, "MeshRenderer should exist");
|
||||
var assignedMat = renderer.sharedMaterial;
|
||||
Assert.IsNotNull(assignedMat, "sharedMaterial should be assigned");
|
||||
AssertShaderIsSupported(assignedMat.shader);
|
||||
var createdMat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
Assert.AreEqual(createdMat, assignedMat, "Assigned material should match created material");
|
||||
}
|
||||
finally
|
||||
{
|
||||
var go = GameObject.Find("MCPParamTestSphere");
|
||||
if (go != null) UnityEngine.Object.DestroyImmediate(go);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(matPath) != null)
|
||||
AssetDatabase.DeleteAsset(matPath);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive end-to-end test covering all 10 property handling scenarios:
|
||||
/// 1. Create material via JSON string
|
||||
/// 2. Modify color and metallic (friendly names)
|
||||
/// 3. Modify using structured float block
|
||||
/// 4. Assign texture via direct prop alias
|
||||
/// 5. Assign texture via structured block
|
||||
/// 6. Create sphere and assign material via componentProperties JSON
|
||||
/// 7. Use URP color alias key
|
||||
/// 8. Invalid JSON handling (graceful degradation)
|
||||
/// 9. Switch shader pipeline dynamically
|
||||
/// 10. Mixed friendly and alias keys
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void EndToEnd_PropertyHandling_AllScenarios()
|
||||
{
|
||||
EnsureTempFolders();
|
||||
|
||||
string guidSuffix = Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
string matPath = $"{TempLiveDir}/Mat_{guidSuffix}.mat";
|
||||
string texPath = $"{TempLiveDir}/TempBaseTex_{guidSuffix}.asset";
|
||||
string sphereName = $"LiveSphere_{guidSuffix}";
|
||||
string badJsonPath = $"{TempLiveDir}/BadJson_{guidSuffix}.mat";
|
||||
|
||||
// Ensure clean state
|
||||
var preSphere = GameObject.Find(sphereName);
|
||||
if (preSphere != null) UnityEngine.Object.DestroyImmediate(preSphere);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(matPath) != null)
|
||||
AssetDatabase.DeleteAsset(matPath);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(badJsonPath) != null)
|
||||
AssetDatabase.DeleteAsset(badJsonPath);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(texPath) != null)
|
||||
AssetDatabase.DeleteAsset(texPath);
|
||||
|
||||
// Create test texture for texture-dependent scenarios (4, 5, 10)
|
||||
var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false);
|
||||
var pixels = new Color[16];
|
||||
for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.white;
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
AssetDatabase.CreateAsset(tex, texPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Create material via JSON string
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = matPath,
|
||||
["assetType"] = "Material",
|
||||
["properties"] = "{\"shader\":\"Universal Render Pipeline/Lit\",\"color\":[1,0,0,1]}"
|
||||
};
|
||||
var createRaw = ManageAsset.HandleCommand(createParams);
|
||||
var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), $"Test 1 failed: {createResult}");
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
Assert.IsNotNull(mat, "Material should be created");
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
Assert.AreEqual(Color.red, mat.GetColor("_BaseColor"), "Test 1: _BaseColor should be red");
|
||||
else if (mat.HasProperty("_Color"))
|
||||
Assert.AreEqual(Color.red, mat.GetColor("_Color"), "Test 1: _Color should be red");
|
||||
else
|
||||
Assert.Inconclusive("Material has neither _BaseColor nor _Color");
|
||||
|
||||
// 2. Modify color and metallic (friendly names)
|
||||
var modify1 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = "{\"color\":[0,0.5,1,1],\"metallic\":0.6}"
|
||||
};
|
||||
var modifyRaw1 = ManageAsset.HandleCommand(modify1);
|
||||
var modifyResult1 = modifyRaw1 as JObject ?? JObject.FromObject(modifyRaw1);
|
||||
Assert.IsTrue(modifyResult1.Value<bool>("success"), $"Test 2 failed: {modifyResult1}");
|
||||
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
var expectedCyan = new Color(0, 0.5f, 1, 1);
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
Assert.AreEqual(expectedCyan, mat.GetColor("_BaseColor"), "Test 2: _BaseColor should be cyan");
|
||||
else if (mat.HasProperty("_Color"))
|
||||
Assert.AreEqual(expectedCyan, mat.GetColor("_Color"), "Test 2: _Color should be cyan");
|
||||
Assert.AreEqual(0.6f, mat.GetFloat("_Metallic"), 0.001f, "Test 2: Metallic should be 0.6");
|
||||
|
||||
// 3. Modify using structured float block
|
||||
var modify2 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["float"] = new JObject { ["name"] = "_Metallic", ["value"] = 0.1 }
|
||||
}
|
||||
};
|
||||
var modifyRaw2 = ManageAsset.HandleCommand(modify2);
|
||||
var modifyResult2 = modifyRaw2 as JObject ?? JObject.FromObject(modifyRaw2);
|
||||
Assert.IsTrue(modifyResult2.Value<bool>("success"), $"Test 3 failed: {modifyResult2}");
|
||||
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
Assert.AreEqual(0.1f, mat.GetFloat("_Metallic"), 0.001f, "Test 3: Metallic should be 0.1");
|
||||
|
||||
// 4. Assign texture via direct prop alias
|
||||
var modify3 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = "{\"_BaseMap\":\"" + texPath + "\"}"
|
||||
};
|
||||
var modifyRaw3 = ManageAsset.HandleCommand(modify3);
|
||||
var modifyResult3 = modifyRaw3 as JObject ?? JObject.FromObject(modifyRaw3);
|
||||
Assert.IsTrue(modifyResult3.Value<bool>("success"), $"Test 4 failed: {modifyResult3}");
|
||||
|
||||
// 5. Assign texture via structured block
|
||||
var modify4 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["texture"] = new JObject { ["name"] = "_MainTex", ["path"] = texPath }
|
||||
}
|
||||
};
|
||||
var modifyRaw4 = ManageAsset.HandleCommand(modify4);
|
||||
var modifyResult4 = modifyRaw4 as JObject ?? JObject.FromObject(modifyRaw4);
|
||||
Assert.IsTrue(modifyResult4.Value<bool>("success"), $"Test 5 failed: {modifyResult4}");
|
||||
|
||||
// 6. Create sphere and assign material via componentProperties JSON string
|
||||
var createSphere = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = sphereName,
|
||||
["primitiveType"] = "Sphere"
|
||||
};
|
||||
var sphereRaw = ManageGameObject.HandleCommand(createSphere);
|
||||
var sphereResult = sphereRaw as JObject ?? JObject.FromObject(sphereRaw);
|
||||
Assert.IsTrue(sphereResult.Value<bool>("success"), $"Test 6 - Create sphere failed: {sphereResult}");
|
||||
|
||||
var modifySphere = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = sphereName,
|
||||
["searchMethod"] = "by_name",
|
||||
["componentProperties"] = "{\"MeshRenderer\":{\"sharedMaterial\":\"" + matPath + "\"}}"
|
||||
};
|
||||
var sphereModifyRaw = ManageGameObject.HandleCommand(modifySphere);
|
||||
var sphereModifyResult = sphereModifyRaw as JObject ?? JObject.FromObject(sphereModifyRaw);
|
||||
Assert.IsTrue(sphereModifyResult.Value<bool>("success"), $"Test 6 - Assign material failed: {sphereModifyResult}");
|
||||
var sphere = GameObject.Find(sphereName);
|
||||
Assert.IsNotNull(sphere, "Test 6: Sphere should exist");
|
||||
var renderer = sphere.GetComponent<MeshRenderer>();
|
||||
Assert.IsNotNull(renderer.sharedMaterial, "Test 6: Material should be assigned");
|
||||
|
||||
// 7. Use URP color alias key
|
||||
var modify5 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["_BaseColor"] = new JArray(0.2, 0.8, 0.3, 1)
|
||||
}
|
||||
};
|
||||
var modifyRaw5 = ManageAsset.HandleCommand(modify5);
|
||||
var modifyResult5 = modifyRaw5 as JObject ?? JObject.FromObject(modifyRaw5);
|
||||
Assert.IsTrue(modifyResult5.Value<bool>("success"), $"Test 7 failed: {modifyResult5}");
|
||||
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
Color expectedColor = new Color(0.2f, 0.8f, 0.3f, 1f);
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
AssertColorsEqual(expectedColor, mat.GetColor("_BaseColor"), "Test 7: _BaseColor should be set");
|
||||
else if (mat.HasProperty("_Color"))
|
||||
AssertColorsEqual(expectedColor, mat.GetColor("_Color"), "Test 7: Fallback _Color should be set");
|
||||
|
||||
// 8. Invalid JSON should warn (don't fail)
|
||||
var invalidJson = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = badJsonPath,
|
||||
["assetType"] = "Material",
|
||||
["properties"] = "{\"invalid\": json, \"missing\": quotes}"
|
||||
};
|
||||
LogAssert.Expect(LogType.Warning, new Regex("(failed to parse)|(Could not parse 'properties' JSON string)", RegexOptions.IgnoreCase));
|
||||
var invalidRaw = ManageAsset.HandleCommand(invalidJson);
|
||||
var invalidResult = invalidRaw as JObject ?? JObject.FromObject(invalidRaw);
|
||||
Assert.IsNotNull(invalidResult, "Test 8: Should return a result");
|
||||
|
||||
// 9. Switch shader pipeline dynamically
|
||||
var modify6 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = "{\"shader\":\"Standard\",\"color\":[1,1,0,1]}"
|
||||
};
|
||||
var modifyRaw6 = ManageAsset.HandleCommand(modify6);
|
||||
var modifyResult6 = modifyRaw6 as JObject ?? JObject.FromObject(modifyRaw6);
|
||||
Assert.IsTrue(modifyResult6.Value<bool>("success"), $"Test 9 failed: {modifyResult6}");
|
||||
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
// The "Standard" shader request is aliased to the active pipeline's lit shader.
|
||||
var pipeline9 = RenderPipelineUtility.GetActivePipeline();
|
||||
string expectedShader9 = pipeline9 switch
|
||||
{
|
||||
RenderPipelineUtility.PipelineKind.Universal => "Universal Render Pipeline/Lit",
|
||||
RenderPipelineUtility.PipelineKind.HighDefinition => "HDRP/Lit",
|
||||
_ => "Standard"
|
||||
};
|
||||
Assert.AreEqual(expectedShader9, mat.shader.name, $"Test 9: Shader should be {expectedShader9}");
|
||||
string colorProp9 = mat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
|
||||
var c9 = mat.GetColor(colorProp9);
|
||||
// Looser tolerance (0.02) for shader-switched colors due to color space conversion differences
|
||||
Assert.IsTrue(Mathf.Abs(c9.r - 1f) < 0.02f && Mathf.Abs(c9.g - 1f) < 0.02f && Mathf.Abs(c9.b - 0f) < 0.02f,
|
||||
"Test 9: Color should be near yellow");
|
||||
|
||||
// 10. Mixed friendly and alias keys in one go
|
||||
var modify7 = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = matPath,
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["metallic"] = 0.8,
|
||||
["smoothness"] = 0.3,
|
||||
["albedo"] = texPath
|
||||
}
|
||||
};
|
||||
var modifyRaw7 = ManageAsset.HandleCommand(modify7);
|
||||
var modifyResult7 = modifyRaw7 as JObject ?? JObject.FromObject(modifyRaw7);
|
||||
Assert.IsTrue(modifyResult7.Value<bool>("success"), $"Test 10 failed: {modifyResult7}");
|
||||
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
Assert.AreEqual(0.8f, mat.GetFloat("_Metallic"), 0.001f, "Test 10: Metallic should be 0.8");
|
||||
// Built-in Standard stores smoothness in "_Glossiness"; URP/HDRP Lit uses "_Smoothness".
|
||||
string smoothnessProp10 = mat.HasProperty("_Smoothness") ? "_Smoothness" : "_Glossiness";
|
||||
Assert.AreEqual(0.3f, mat.GetFloat(smoothnessProp10), 0.001f, "Test 10: Smoothness should be 0.3");
|
||||
}
|
||||
finally
|
||||
{
|
||||
var sphere = GameObject.Find(sphereName);
|
||||
if (sphere != null) UnityEngine.Object.DestroyImmediate(sphere);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(matPath) != null)
|
||||
AssetDatabase.DeleteAsset(matPath);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(badJsonPath) != null)
|
||||
AssetDatabase.DeleteAsset(badJsonPath);
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(texPath) != null)
|
||||
AssetDatabase.DeleteAsset(texPath);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80144860477bb4293acf4669566b27b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 486fc2e6daa8426383a93b7fafaa3800
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Tests.EditMode.Tools
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManageEditorUndoTests
|
||||
{
|
||||
[Test]
|
||||
public void Undo_ReturnsSuccess()
|
||||
{
|
||||
var p = new JObject { ["action"] = "undo" };
|
||||
var result = ManageEditor.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Redo_ReturnsSuccess()
|
||||
{
|
||||
var p = new JObject { ["action"] = "redo" };
|
||||
var result = ManageEditor.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Undo_AfterRecordedChange_RevertsChange()
|
||||
{
|
||||
var go = new GameObject("UndoTestGO");
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(go, "Rename UndoTestGO");
|
||||
go.name = "RenamedGO";
|
||||
Undo.FlushUndoRecordObjects();
|
||||
|
||||
var p = new JObject { ["action"] = "undo" };
|
||||
ManageEditor.HandleCommand(p);
|
||||
Assert.AreEqual("UndoTestGO", go.name, "Name should revert after undo");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9beec84be6a1148dd97618c11bd70104
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEditorInternal;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Comprehensive baseline tests for ManageGameObject "create" action.
|
||||
/// These tests capture existing behavior before API redesign.
|
||||
/// </summary>
|
||||
public class ManageGameObjectCreateTests
|
||||
{
|
||||
private List<GameObject> createdObjects = new List<GameObject>();
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in createdObjects)
|
||||
{
|
||||
if (go != null)
|
||||
{
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
createdObjects.Clear();
|
||||
}
|
||||
|
||||
private GameObject FindAndTrack(string name)
|
||||
{
|
||||
var go = GameObject.Find(name);
|
||||
if (go != null && !createdObjects.Contains(go))
|
||||
{
|
||||
createdObjects.Add(go);
|
||||
}
|
||||
return go;
|
||||
}
|
||||
|
||||
#region Basic Create Tests
|
||||
|
||||
[Test]
|
||||
public void Create_WithNameOnly_CreatesEmptyGameObject()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestEmptyObject"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestEmptyObject");
|
||||
Assert.IsNotNull(created, "GameObject should be created");
|
||||
Assert.AreEqual("TestEmptyObject", created.name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithoutName_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail without name");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithEmptyName_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = ""
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail with empty name");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Primitive Type Tests
|
||||
|
||||
[Test]
|
||||
public void Create_PrimitiveCube_CreatesCubeWithComponents()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestCube",
|
||||
["primitiveType"] = "Cube"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestCube");
|
||||
Assert.IsNotNull(created, "Cube should be created");
|
||||
Assert.IsNotNull(created.GetComponent<MeshFilter>(), "Cube should have MeshFilter");
|
||||
Assert.IsNotNull(created.GetComponent<MeshRenderer>(), "Cube should have MeshRenderer");
|
||||
Assert.IsNotNull(created.GetComponent<BoxCollider>(), "Cube should have BoxCollider");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_PrimitiveSphere_CreatesSphereWithComponents()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestSphere",
|
||||
["primitiveType"] = "Sphere"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestSphere");
|
||||
Assert.IsNotNull(created, "Sphere should be created");
|
||||
Assert.IsNotNull(created.GetComponent<SphereCollider>(), "Sphere should have SphereCollider");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_PrimitiveCapsule_CreatesCapsule()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestCapsule",
|
||||
["primitiveType"] = "Capsule"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestCapsule");
|
||||
Assert.IsNotNull(created, "Capsule should be created");
|
||||
Assert.IsNotNull(created.GetComponent<CapsuleCollider>(), "Capsule should have CapsuleCollider");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_PrimitivePlane_CreatesPlane()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestPlane",
|
||||
["primitiveType"] = "Plane"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestPlane");
|
||||
Assert.IsNotNull(created, "Plane should be created");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_PrimitiveCylinder_CreatesCylinder()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestCylinder",
|
||||
["primitiveType"] = "Cylinder"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestCylinder");
|
||||
Assert.IsNotNull(created, "Cylinder should be created");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_PrimitiveQuad_CreatesQuad()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestQuad",
|
||||
["primitiveType"] = "Quad"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestQuad");
|
||||
Assert.IsNotNull(created, "Quad should be created");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_InvalidPrimitiveType_HandlesGracefully()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestInvalidPrimitive",
|
||||
["primitiveType"] = "InvalidType"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Should either fail or create empty object - capture current behavior
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Transform Tests
|
||||
|
||||
[Test]
|
||||
public void Create_WithPosition_SetsPosition()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestPositioned",
|
||||
["position"] = new JArray { 1.0f, 2.0f, 3.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestPositioned");
|
||||
Assert.IsNotNull(created);
|
||||
Assert.AreEqual(new Vector3(1f, 2f, 3f), created.transform.position);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithRotation_SetsRotation()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestRotated",
|
||||
["rotation"] = new JArray { 0.0f, 90.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestRotated");
|
||||
Assert.IsNotNull(created);
|
||||
// Check Y rotation is approximately 90 degrees
|
||||
Assert.AreEqual(90f, created.transform.eulerAngles.y, 0.1f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithScale_SetsScale()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestScaled",
|
||||
["scale"] = new JArray { 2.0f, 3.0f, 4.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestScaled");
|
||||
Assert.IsNotNull(created);
|
||||
Assert.AreEqual(new Vector3(2f, 3f, 4f), created.transform.localScale);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithAllTransformProperties_SetsAll()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestFullTransform",
|
||||
["position"] = new JArray { 5.0f, 6.0f, 7.0f },
|
||||
["rotation"] = new JArray { 45.0f, 90.0f, 0.0f },
|
||||
["scale"] = new JArray { 1.5f, 1.5f, 1.5f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestFullTransform");
|
||||
Assert.IsNotNull(created);
|
||||
Assert.AreEqual(new Vector3(5f, 6f, 7f), created.transform.position);
|
||||
Assert.AreEqual(new Vector3(1.5f, 1.5f, 1.5f), created.transform.localScale);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parenting Tests
|
||||
|
||||
[Test]
|
||||
public void Create_WithParentByName_SetsParent()
|
||||
{
|
||||
// Create parent first
|
||||
var parent = new GameObject("TestParent");
|
||||
createdObjects.Add(parent);
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestChild",
|
||||
["parent"] = "TestParent"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var child = FindAndTrack("TestChild");
|
||||
Assert.IsNotNull(child);
|
||||
Assert.AreEqual(parent.transform, child.transform.parent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithNonExistentParent_HandlesGracefully()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestOrphan",
|
||||
["parent"] = "NonExistentParent"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Should either fail or create without parent - capture current behavior
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tag and Layer Tests
|
||||
|
||||
[Test]
|
||||
public void Create_WithTag_SetsTag()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestTagged",
|
||||
["tag"] = "MainCamera" // Use built-in tag
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestTagged");
|
||||
Assert.IsNotNull(created);
|
||||
Assert.AreEqual("MainCamera", created.tag);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithLayer_SetsLayer()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestLayered",
|
||||
["layer"] = "UI" // Use built-in layer
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestLayered");
|
||||
Assert.IsNotNull(created);
|
||||
Assert.AreEqual(LayerMask.NameToLayer("UI"), created.layer);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithNewTag_AutoCreatesTag()
|
||||
{
|
||||
const string testTag = "AutoCreatedTag12345";
|
||||
|
||||
// Tags that don't exist are now auto-created
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestAutoTag",
|
||||
["tag"] = testTag
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var created = FindAndTrack("TestAutoTag");
|
||||
Assert.IsNotNull(created, "Object should be created");
|
||||
Assert.AreEqual(testTag, created.tag, "Tag should be auto-created and assigned");
|
||||
|
||||
// Verify tag was actually added to the tag manager
|
||||
Assert.That(UnityEditorInternal.InternalEditorUtility.tags, Does.Contain(testTag),
|
||||
"Tag should exist in Unity's tag manager");
|
||||
|
||||
// Clean up the created tag
|
||||
try { UnityEditorInternal.InternalEditorUtility.RemoveTag(testTag); } catch { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Response Structure Tests
|
||||
|
||||
[Test]
|
||||
public void Create_Success_ReturnsInstanceID()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestInstanceID"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var data = resultObj["data"];
|
||||
Assert.IsNotNull(data, "Response should include data");
|
||||
|
||||
// Check that instanceID is returned (case-insensitive check)
|
||||
var instanceID = data["instanceID"]?.Value<int>() ?? data["InstanceID"]?.Value<int>();
|
||||
Assert.IsTrue(instanceID.HasValue && instanceID.Value != 0,
|
||||
$"Response should include a non-zero instanceID. Data: {data}");
|
||||
|
||||
FindAndTrack("TestInstanceID");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_Success_ReturnsName()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestReturnedName"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
var data = resultObj["data"];
|
||||
Assert.IsNotNull(data, "Response should include data");
|
||||
|
||||
// Check name is in response
|
||||
var nameValue = data["name"]?.ToString() ?? data["Name"]?.ToString();
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(nameValue) || data.ToString().Contains("TestReturnedName"),
|
||||
"Response should include name");
|
||||
|
||||
FindAndTrack("TestReturnedName");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec38858ff125347778a30792e4bb1c3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Comprehensive baseline tests for ManageGameObject "delete" action.
|
||||
/// These tests capture existing behavior before API redesign.
|
||||
/// </summary>
|
||||
public class ManageGameObjectDeleteTests
|
||||
{
|
||||
private List<GameObject> testObjects = new List<GameObject>();
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in testObjects)
|
||||
{
|
||||
if (go != null)
|
||||
{
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
testObjects.Clear();
|
||||
}
|
||||
|
||||
private GameObject CreateTestObject(string name)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
testObjects.Add(go);
|
||||
return go;
|
||||
}
|
||||
|
||||
#region Basic Delete Tests
|
||||
|
||||
[Test]
|
||||
public void Delete_ByName_DeletesObject()
|
||||
{
|
||||
var target = CreateTestObject("DeleteTargetByName");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "DeleteTargetByName",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Verify object is deleted
|
||||
var found = GameObject.Find("DeleteTargetByName");
|
||||
Assert.IsNull(found, "Object should be deleted");
|
||||
|
||||
// Remove from our tracking list since it's deleted
|
||||
testObjects.Remove(target);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_ByInstanceID_DeletesObject()
|
||||
{
|
||||
var target = CreateTestObject("DeleteTargetByID");
|
||||
int instanceID = target.GetInstanceID();
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = instanceID,
|
||||
["searchMethod"] = "by_id"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Verify object is deleted
|
||||
var found = GameObject.Find("DeleteTargetByID");
|
||||
Assert.IsNull(found, "Object should be deleted");
|
||||
|
||||
testObjects.Remove(target);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_NonExistentObject_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "NonExistentObject12345",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail for non-existent object");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_WithoutTarget_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail without target");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Search Method Tests
|
||||
|
||||
[Test]
|
||||
public void Delete_ByTag_DeletesMatchingObjects()
|
||||
{
|
||||
// Current behavior: delete action finds first matching object and deletes it.
|
||||
// This test verifies at least one tagged object is deleted.
|
||||
var target1 = CreateTestObject("DeleteByTag1");
|
||||
var target2 = CreateTestObject("DeleteByTag2");
|
||||
|
||||
// Use built-in tag
|
||||
target1.tag = "MainCamera";
|
||||
target2.tag = "MainCamera";
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "MainCamera",
|
||||
["searchMethod"] = "by_tag"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Verify at least one object was deleted (current behavior deletes first match)
|
||||
bool target1Deleted = target1 == null; // Unity Object == null check
|
||||
bool target2Deleted = target2 == null;
|
||||
Assert.IsTrue(target1Deleted || target2Deleted, "At least one tagged object should be deleted");
|
||||
|
||||
// Check response data for deletion info
|
||||
var data = resultObj["data"];
|
||||
Assert.IsNotNull(data, "Response should include data");
|
||||
|
||||
// Clean up only surviving objects from tracking
|
||||
if (!target1Deleted) testObjects.Remove(target1);
|
||||
if (!target2Deleted) testObjects.Remove(target2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_ByLayer_DeletesMatchingObjects()
|
||||
{
|
||||
var target = CreateTestObject("DeleteByLayer");
|
||||
target.layer = LayerMask.NameToLayer("UI");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "UI",
|
||||
["searchMethod"] = "by_layer"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Verify the object was actually deleted
|
||||
bool targetDeleted = target == null; // Unity Object == null check
|
||||
Assert.IsTrue(targetDeleted, "Object on UI layer should be deleted");
|
||||
Assert.IsFalse(testObjects.Contains(target) && target != null, "Deleted object should not be findable");
|
||||
|
||||
// Only remove from tracking if not already destroyed
|
||||
if (!targetDeleted) testObjects.Remove(target);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_ByPath_DeletesObject()
|
||||
{
|
||||
var parent = CreateTestObject("DeleteParent");
|
||||
var child = CreateTestObject("DeleteChild");
|
||||
child.transform.SetParent(parent.transform);
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "DeleteParent/DeleteChild",
|
||||
["searchMethod"] = "by_path"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Capture current behavior
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
|
||||
testObjects.Remove(child);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hierarchy Tests
|
||||
|
||||
[Test]
|
||||
public void Delete_Parent_DeletesChildren()
|
||||
{
|
||||
var parent = CreateTestObject("DeleteParentWithChildren");
|
||||
var child1 = CreateTestObject("Child1");
|
||||
var child2 = CreateTestObject("Child2");
|
||||
var grandchild = CreateTestObject("Grandchild");
|
||||
|
||||
child1.transform.SetParent(parent.transform);
|
||||
child2.transform.SetParent(parent.transform);
|
||||
grandchild.transform.SetParent(child1.transform);
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "DeleteParentWithChildren",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// All should be deleted
|
||||
Assert.IsNull(GameObject.Find("DeleteParentWithChildren"), "Parent should be deleted");
|
||||
Assert.IsNull(GameObject.Find("Child1"), "Child1 should be deleted");
|
||||
Assert.IsNull(GameObject.Find("Child2"), "Child2 should be deleted");
|
||||
Assert.IsNull(GameObject.Find("Grandchild"), "Grandchild should be deleted");
|
||||
|
||||
testObjects.Remove(parent);
|
||||
testObjects.Remove(child1);
|
||||
testObjects.Remove(child2);
|
||||
testObjects.Remove(grandchild);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_Child_DoesNotDeleteParent()
|
||||
{
|
||||
var parent = CreateTestObject("ParentShouldSurvive");
|
||||
var child = CreateTestObject("ChildToDelete");
|
||||
child.transform.SetParent(parent.transform);
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "ChildToDelete",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Child deleted, parent survives
|
||||
Assert.IsNull(GameObject.Find("ChildToDelete"), "Child should be deleted");
|
||||
Assert.IsNotNull(GameObject.Find("ParentShouldSurvive"), "Parent should survive");
|
||||
|
||||
testObjects.Remove(child);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Response Structure Tests
|
||||
|
||||
[Test]
|
||||
public void Delete_Success_ReturnsDeletedCount()
|
||||
{
|
||||
var target = CreateTestObject("DeleteCountTest");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "DeleteCountTest",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Verify object was actually deleted
|
||||
bool targetDeleted = target == null;
|
||||
Assert.IsTrue(targetDeleted, "Object should be deleted");
|
||||
|
||||
// Check for deleted count in response
|
||||
var data = resultObj["data"];
|
||||
Assert.IsNotNull(data, "Response should include data");
|
||||
|
||||
// Verify the actual count if present
|
||||
if (data is JObject dataObj && dataObj.ContainsKey("deletedCount"))
|
||||
{
|
||||
Assert.AreEqual(1, dataObj.Value<int>("deletedCount"), "Should report 1 deleted object");
|
||||
}
|
||||
|
||||
// Only remove from tracking if not already destroyed
|
||||
if (!targetDeleted) testObjects.Remove(target);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Test]
|
||||
public void Delete_InactiveObject_StillDeletes()
|
||||
{
|
||||
var target = CreateTestObject("InactiveDeleteTarget");
|
||||
target.SetActive(false);
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "InactiveDeleteTarget",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Capture current behavior for inactive objects
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
|
||||
testObjects.Remove(target);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_MultipleObjectsSameName_DeletesCorrectly()
|
||||
{
|
||||
// Expected behavior: delete action with by_name finds the FIRST matching object
|
||||
// and deletes only that one. This is consistent with Unity's GameObject.Find behavior.
|
||||
var target1 = CreateTestObject("DuplicateName");
|
||||
var target2 = CreateTestObject("DuplicateName");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "DuplicateName",
|
||||
["searchMethod"] = "by_name"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
|
||||
// Verify deletion occurred - at least one should be deleted
|
||||
bool target1Deleted = target1 == null;
|
||||
bool target2Deleted = target2 == null;
|
||||
Assert.IsTrue(target1Deleted || target2Deleted, "At least one object should be deleted");
|
||||
|
||||
// Count remaining objects with the name to verify behavior
|
||||
int remainingCount = 0;
|
||||
if (!target1Deleted) remainingCount++;
|
||||
if (!target2Deleted) remainingCount++;
|
||||
|
||||
// Document the actual behavior: first match is deleted, second survives
|
||||
// If both are deleted, that's also acceptable (bulk delete mode)
|
||||
Assert.IsTrue(remainingCount <= 1, $"Expected at most 1 remaining, got {remainingCount}");
|
||||
|
||||
// Clean up only survivors from tracking
|
||||
if (!target1Deleted) testObjects.Remove(target1);
|
||||
if (!target2Deleted) testObjects.Remove(target2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e74a6d8990a344fd6a1e4b175d411be1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEditorInternal;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Comprehensive baseline tests for ManageGameObject "modify" action.
|
||||
/// These tests capture existing behavior before API redesign.
|
||||
/// </summary>
|
||||
public class ManageGameObjectModifyTests
|
||||
{
|
||||
private List<GameObject> testObjects = new List<GameObject>();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Create a standard test object for each test
|
||||
var go = new GameObject("ModifyTestObject");
|
||||
go.transform.position = Vector3.zero;
|
||||
go.transform.rotation = Quaternion.identity;
|
||||
go.transform.localScale = Vector3.one;
|
||||
testObjects.Add(go);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in testObjects)
|
||||
{
|
||||
if (go != null)
|
||||
{
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
testObjects.Clear();
|
||||
}
|
||||
|
||||
private GameObject CreateTestObject(string name)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
testObjects.Add(go);
|
||||
return go;
|
||||
}
|
||||
|
||||
#region Target Resolution Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_ByName_FindsAndModifiesObject()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["searchMethod"] = "by_name",
|
||||
["position"] = new JArray { 10.0f, 0.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(new Vector3(10f, 0f, 0f), testObjects[0].transform.position);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_ByInstanceID_FindsAndModifiesObject()
|
||||
{
|
||||
int instanceID = testObjects[0].GetInstanceID();
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = instanceID,
|
||||
["searchMethod"] = "by_id",
|
||||
["position"] = new JArray { 20.0f, 0.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(new Vector3(20f, 0f, 0f), testObjects[0].transform.position);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_WithNameAlias_UsesNameAsTarget()
|
||||
{
|
||||
// When target is missing but name is provided, should use name as target
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["name"] = "ModifyTestObject",
|
||||
["position"] = new JArray { 30.0f, 0.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(new Vector3(30f, 0f, 0f), testObjects[0].transform.position);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_NonExistentTarget_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "NonExistentObject12345",
|
||||
["searchMethod"] = "by_name",
|
||||
["position"] = new JArray { 0.0f, 0.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail for non-existent object");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_WithoutTarget_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["position"] = new JArray { 0.0f, 0.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail without target");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Transform Modification Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_Position_SetsNewPosition()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["position"] = new JArray { 1.0f, 2.0f, 3.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(new Vector3(1f, 2f, 3f), testObjects[0].transform.position);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_Rotation_SetsNewRotation()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["rotation"] = new JArray { 0.0f, 90.0f, 0.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(90f, testObjects[0].transform.eulerAngles.y, 0.1f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_Scale_SetsNewScale()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["scale"] = new JArray { 2.0f, 3.0f, 4.0f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(new Vector3(2f, 3f, 4f), testObjects[0].transform.localScale);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_AllTransformProperties_SetsAll()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["position"] = new JArray { 5.0f, 6.0f, 7.0f },
|
||||
["rotation"] = new JArray { 45.0f, 45.0f, 45.0f },
|
||||
["scale"] = new JArray { 0.5f, 0.5f, 0.5f }
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(new Vector3(5f, 6f, 7f), testObjects[0].transform.position);
|
||||
Assert.AreEqual(new Vector3(0.5f, 0.5f, 0.5f), testObjects[0].transform.localScale);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rename Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_Name_RenamesObject()
|
||||
{
|
||||
// Get instanceID first since name will change
|
||||
int instanceID = testObjects[0].GetInstanceID();
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = instanceID,
|
||||
["searchMethod"] = "by_id",
|
||||
["name"] = "RenamedObject" // Uses 'name' parameter, not 'newName'
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual("RenamedObject", testObjects[0].name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_NameToEmpty_HandlesGracefully()
|
||||
{
|
||||
int instanceID = testObjects[0].GetInstanceID();
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = instanceID,
|
||||
["searchMethod"] = "by_id",
|
||||
["name"] = "" // Empty name
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Capture current behavior - may reject or allow empty name
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reparenting Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_Parent_ReparentsObject()
|
||||
{
|
||||
var parent = CreateTestObject("NewParent");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["parent"] = "NewParent"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(parent.transform, testObjects[0].transform.parent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_ParentToNull_UnparentsObject()
|
||||
{
|
||||
// First parent the object
|
||||
var parent = CreateTestObject("TempParent");
|
||||
testObjects[0].transform.SetParent(parent.transform);
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["parent"] = JValue.CreateNull()
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Capture current behavior for null parent
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_ParentToNonExistent_HandlesGracefully()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["parent"] = "NonExistentParent12345"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
// Should fail or handle gracefully
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Active State Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_SetActive_DeactivatesObject()
|
||||
{
|
||||
Assert.IsTrue(testObjects[0].activeSelf, "Object should start active");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["setActive"] = false
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.IsFalse(testObjects[0].activeSelf, "Object should be deactivated");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_SetActive_ActivatesObject()
|
||||
{
|
||||
testObjects[0].SetActive(false);
|
||||
Assert.IsFalse(testObjects[0].activeSelf, "Object should start inactive");
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["setActive"] = true
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.IsTrue(testObjects[0].activeSelf, "Object should be activated");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tag and Layer Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_Tag_SetsNewTag()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["tag"] = "MainCamera"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual("MainCamera", testObjects[0].tag);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_Layer_SetsNewLayer()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["layer"] = "UI"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(LayerMask.NameToLayer("UI"), testObjects[0].layer);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_NewTag_AutoCreatesTag()
|
||||
{
|
||||
const string testTag = "AutoModifyTag12345";
|
||||
|
||||
// Tags that don't exist are now auto-created
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "ModifyTestObject",
|
||||
["tag"] = testTag
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual(testTag, testObjects[0].tag, "Tag should be auto-created and assigned");
|
||||
|
||||
// Verify tag was actually added to the tag manager
|
||||
Assert.That(UnityEditorInternal.InternalEditorUtility.tags, Does.Contain(testTag),
|
||||
"Tag should exist in Unity's tag manager");
|
||||
|
||||
// Clean up the created tag
|
||||
try { UnityEditorInternal.InternalEditorUtility.RemoveTag(testTag); } catch { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multiple Modifications Tests
|
||||
|
||||
[Test]
|
||||
public void Modify_MultipleProperties_AppliesAll()
|
||||
{
|
||||
var parent = CreateTestObject("MultiModifyParent");
|
||||
int instanceID = testObjects[0].GetInstanceID();
|
||||
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = instanceID,
|
||||
["searchMethod"] = "by_id",
|
||||
["name"] = "MultiModifiedObject", // Uses 'name' not 'newName'
|
||||
["position"] = new JArray { 100.0f, 200.0f, 300.0f },
|
||||
["scale"] = new JArray { 5.0f, 5.0f, 5.0f },
|
||||
["parent"] = "MultiModifyParent",
|
||||
["tag"] = "MainCamera"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(p);
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
|
||||
Assert.AreEqual("MultiModifiedObject", testObjects[0].name);
|
||||
Assert.AreEqual(parent.transform, testObjects[0].transform.parent);
|
||||
Assert.AreEqual("MainCamera", testObjects[0].tag);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 042aca01b843348a3bc9ac86475e6293
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,645 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageGameObjectTests
|
||||
{
|
||||
private GameObject testGameObject;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Create a test GameObject for each test
|
||||
testGameObject = new GameObject("TestObject");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up test GameObject
|
||||
if (testGameObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testGameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ReturnsError_ForNullParams()
|
||||
{
|
||||
var result = ManageGameObject.HandleCommand(null);
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result object");
|
||||
// Verify the result indicates an error state
|
||||
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
|
||||
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse for null params");
|
||||
Assert.IsFalse(errorResponse.Success, "Success should be false for null params");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ReturnsError_ForEmptyParams()
|
||||
{
|
||||
var emptyParams = new JObject();
|
||||
var result = ManageGameObject.HandleCommand(emptyParams);
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result object for empty params");
|
||||
// Verify the result indicates an error state (missing required action)
|
||||
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
|
||||
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse for empty params");
|
||||
Assert.IsFalse(errorResponse.Success, "Success should be false for empty params");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_ProcessesValidCreateAction()
|
||||
{
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TestCreateObject"
|
||||
};
|
||||
|
||||
var result = ManageGameObject.HandleCommand(createParams);
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result for valid create action");
|
||||
|
||||
// Clean up - find and destroy the created object
|
||||
var createdObject = GameObject.Find("TestCreateObject");
|
||||
if (createdObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(createdObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComponentResolver_Integration_WorksWithRealComponents()
|
||||
{
|
||||
// Test that our ComponentResolver works with actual Unity components
|
||||
var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error);
|
||||
|
||||
Assert.IsTrue(transformResult, "Should resolve Transform component");
|
||||
Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type");
|
||||
Assert.IsEmpty(error, "Should have no error for valid component");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComponentResolver_Integration_WorksWithBuiltInComponents()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
("Rigidbody", typeof(Rigidbody)),
|
||||
("Collider", typeof(Collider)),
|
||||
("Renderer", typeof(Renderer)),
|
||||
("Camera", typeof(Camera)),
|
||||
("Light", typeof(Light))
|
||||
};
|
||||
|
||||
foreach (var (componentName, expectedType) in components)
|
||||
{
|
||||
var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error);
|
||||
|
||||
// Some components might not resolve (abstract classes), but the method should handle gracefully
|
||||
if (result)
|
||||
{
|
||||
Assert.IsTrue(expectedType.IsAssignableFrom(actualType),
|
||||
$"{componentName} should resolve to assignable type");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotEmpty(error, $"Should have error message for {componentName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PropertyMatching_Integration_WorksWithRealGameObject()
|
||||
{
|
||||
// Add a Rigidbody to test real property matching
|
||||
var rigidbody = testGameObject.AddComponent<Rigidbody>();
|
||||
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody));
|
||||
|
||||
Assert.IsNotEmpty(properties, "Rigidbody should have properties");
|
||||
Assert.Contains("mass", properties, "Rigidbody should have mass property");
|
||||
Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property");
|
||||
|
||||
// Test AI suggestions
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("Use Gravity", properties);
|
||||
Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PropertyMatching_HandlesMonoBehaviourProperties()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour));
|
||||
|
||||
Assert.IsNotEmpty(properties, "MonoBehaviour should have properties");
|
||||
Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property");
|
||||
Assert.Contains("name", properties, "MonoBehaviour should have name property");
|
||||
Assert.Contains("tag", properties, "MonoBehaviour should have tag property");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PropertyMatching_HandlesCaseVariations()
|
||||
{
|
||||
var testProperties = new List<string> { "maxReachDistance", "playerHealth", "movementSpeed" };
|
||||
|
||||
var testCases = new[]
|
||||
{
|
||||
("max reach distance", "maxReachDistance"),
|
||||
("Max Reach Distance", "maxReachDistance"),
|
||||
("MAX_REACH_DISTANCE", "maxReachDistance"),
|
||||
("player health", "playerHealth"),
|
||||
("movement speed", "movementSpeed")
|
||||
};
|
||||
|
||||
foreach (var (input, expected) in testCases)
|
||||
{
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(input, testProperties);
|
||||
Assert.Contains(expected, suggestions, $"Should suggest {expected} for input '{input}'");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ErrorHandling_ReturnsHelpfulMessages()
|
||||
{
|
||||
// This test verifies that error messages are helpful and contain suggestions
|
||||
var testProperties = new List<string> { "mass", "velocity", "drag", "useGravity" };
|
||||
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("weight", testProperties);
|
||||
|
||||
// Even if no perfect match, should return valid list
|
||||
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
|
||||
|
||||
// Test with completely invalid input
|
||||
var badSuggestions = ComponentResolver.GetFuzzyPropertySuggestions("xyz123invalid", testProperties);
|
||||
Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerformanceTest_CachingWorks()
|
||||
{
|
||||
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
|
||||
var input = "Test Property Name";
|
||||
|
||||
// First call - populate cache
|
||||
var startTime = System.DateTime.UtcNow;
|
||||
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions(input, properties);
|
||||
var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Second call - should use cache
|
||||
startTime = System.DateTime.UtcNow;
|
||||
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions(input, properties);
|
||||
var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical");
|
||||
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly");
|
||||
|
||||
// Second call should be faster (though this test might be flaky)
|
||||
Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes()
|
||||
{
|
||||
// Arrange - add Transform and Rigidbody components to test with
|
||||
var transform = testGameObject.transform;
|
||||
var rigidbody = testGameObject.AddComponent<Rigidbody>();
|
||||
|
||||
// Create a params object with mixed valid and invalid properties
|
||||
var setPropertiesParams = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = testGameObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentProperties"] = new JObject
|
||||
{
|
||||
["Transform"] = new JObject
|
||||
{
|
||||
["localPosition"] = new JObject { ["x"] = 1.0f, ["y"] = 2.0f, ["z"] = 3.0f }, // Valid
|
||||
["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation)
|
||||
["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f } // Valid
|
||||
},
|
||||
["Rigidbody"] = new JObject
|
||||
{
|
||||
["mass"] = 5.0f, // Valid
|
||||
["invalidProp"] = "test", // Invalid - doesn't exist
|
||||
["useGravity"] = true // Valid
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Store original values to verify changes
|
||||
var originalLocalPosition = transform.localPosition;
|
||||
var originalLocalScale = transform.localScale;
|
||||
var originalMass = rigidbody.mass;
|
||||
var originalUseGravity = rigidbody.useGravity;
|
||||
|
||||
Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
|
||||
|
||||
// Expect the warning logs from the invalid properties
|
||||
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'rotatoin' not found"));
|
||||
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'invalidProp' not found"));
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(setPropertiesParams);
|
||||
|
||||
Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
|
||||
Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}");
|
||||
Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}");
|
||||
|
||||
// Assert - verify that valid properties were set despite invalid ones
|
||||
Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition,
|
||||
"Valid localPosition should be set even with other invalid properties");
|
||||
Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale,
|
||||
"Valid localScale should be set even with other invalid properties");
|
||||
Assert.AreEqual(5.0f, rigidbody.mass, 0.001f,
|
||||
"Valid mass should be set even with other invalid properties");
|
||||
Assert.AreEqual(true, rigidbody.useGravity,
|
||||
"Valid useGravity should be set even with other invalid properties");
|
||||
|
||||
// Verify the result indicates errors (since we had invalid properties)
|
||||
Assert.IsNotNull(result, "Should return a result object");
|
||||
|
||||
// The collect-and-continue behavior means we should get an error response
|
||||
// that contains info about the failed properties, but valid ones were still applied
|
||||
// This proves the collect-and-continue behavior is working
|
||||
|
||||
// Harden: verify structured error response with failures list contains both invalid fields
|
||||
var successProp = result.GetType().GetProperty("success");
|
||||
Assert.IsNotNull(successProp, "Result should expose 'success' property");
|
||||
Assert.IsFalse((bool)successProp.GetValue(result), "Result.success should be false for partial failure");
|
||||
|
||||
var dataProp = result.GetType().GetProperty("data");
|
||||
Assert.IsNotNull(dataProp, "Result should include 'data' with errors");
|
||||
var dataVal = dataProp.GetValue(result);
|
||||
Assert.IsNotNull(dataVal, "Result.data should not be null");
|
||||
var errorsProp = dataVal.GetType().GetProperty("errors");
|
||||
Assert.IsNotNull(errorsProp, "Result.data should include 'errors' list");
|
||||
var errorsEnum = errorsProp.GetValue(dataVal) as System.Collections.IEnumerable;
|
||||
Assert.IsNotNull(errorsEnum, "errors should be enumerable");
|
||||
|
||||
bool foundRotatoin = false;
|
||||
bool foundInvalidProp = false;
|
||||
foreach (var err in errorsEnum)
|
||||
{
|
||||
string s = err?.ToString() ?? string.Empty;
|
||||
if (s.Contains("rotatoin")) foundRotatoin = true;
|
||||
if (s.Contains("invalidProp")) foundInvalidProp = true;
|
||||
}
|
||||
Assert.IsTrue(foundRotatoin, "errors should mention the misspelled 'rotatoin' property");
|
||||
Assert.IsTrue(foundInvalidProp, "errors should mention the 'invalidProp' property");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_ContinuesAfterException()
|
||||
{
|
||||
// Arrange - create scenario that might cause exceptions
|
||||
var rigidbody = testGameObject.AddComponent<Rigidbody>();
|
||||
|
||||
// Set initial values that we'll change
|
||||
rigidbody.mass = 1.0f;
|
||||
rigidbody.useGravity = true;
|
||||
|
||||
var setPropertiesParams = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = testGameObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentProperties"] = new JObject
|
||||
{
|
||||
["Rigidbody"] = new JObject
|
||||
{
|
||||
["mass"] = 2.5f, // Valid - should be set
|
||||
["velocity"] = "invalid_type", // Invalid type - will cause exception
|
||||
["useGravity"] = false // Valid - should still be set after exception
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expect the error logs from the invalid property
|
||||
// Note: PropertyConversion logs "Error converting token to..." when conversion fails,
|
||||
// then ComponentOps catches the exception and returns an error string (no second Error log).
|
||||
// GameObjectComponentHelpers logs the failure as a warning.
|
||||
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Error converting token to UnityEngine.Vector3"));
|
||||
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex(@"\[ManageGameObject\].*Failed to set property 'velocity'"));
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(setPropertiesParams);
|
||||
|
||||
// Assert - verify that valid properties before AND after the exception were still set
|
||||
Assert.AreEqual(2.5f, rigidbody.mass, 0.001f,
|
||||
"Mass should be set even if later property causes exception");
|
||||
Assert.AreEqual(false, rigidbody.useGravity,
|
||||
"UseGravity should be set even if previous property caused exception");
|
||||
|
||||
Assert.IsNotNull(result, "Should return a result even with exceptions");
|
||||
|
||||
// The key test: processing continued after the exception and set useGravity
|
||||
// This proves the collect-and-continue behavior works even with exceptions
|
||||
|
||||
// Harden: verify structured error response contains velocity failure
|
||||
var successProp2 = result.GetType().GetProperty("success");
|
||||
Assert.IsNotNull(successProp2, "Result should expose 'success' property");
|
||||
Assert.IsFalse((bool)successProp2.GetValue(result), "Result.success should be false when an exception occurs for a property");
|
||||
|
||||
var dataProp2 = result.GetType().GetProperty("data");
|
||||
Assert.IsNotNull(dataProp2, "Result should include 'data' with errors");
|
||||
var dataVal2 = dataProp2.GetValue(result);
|
||||
Assert.IsNotNull(dataVal2, "Result.data should not be null");
|
||||
var errorsProp2 = dataVal2.GetType().GetProperty("errors");
|
||||
Assert.IsNotNull(errorsProp2, "Result.data should include 'errors' list");
|
||||
var errorsEnum2 = errorsProp2.GetValue(dataVal2) as System.Collections.IEnumerable;
|
||||
Assert.IsNotNull(errorsEnum2, "errors should be enumerable");
|
||||
|
||||
bool foundVelocityError = false;
|
||||
foreach (var err in errorsEnum2)
|
||||
{
|
||||
string s = err?.ToString() ?? string.Empty;
|
||||
if (s.Contains("velocity")) { foundVelocityError = true; break; }
|
||||
}
|
||||
Assert.IsTrue(foundVelocityError, "errors should include a message referencing 'velocity'");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_DoesNotInstantiateMaterialsInEditMode()
|
||||
{
|
||||
// Arrange - Create a GameObject with MeshRenderer and MeshFilter components
|
||||
var testObject = new GameObject("MaterialMeshTestObject");
|
||||
var meshRenderer = testObject.AddComponent<MeshRenderer>();
|
||||
var meshFilter = testObject.AddComponent<MeshFilter>();
|
||||
|
||||
// Create a simple material and mesh for testing
|
||||
var testMaterial = new Material(Shader.Find("Standard"));
|
||||
var tempCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
var testMesh = tempCube.GetComponent<MeshFilter>().sharedMesh;
|
||||
UnityEngine.Object.DestroyImmediate(tempCube);
|
||||
|
||||
// Set the shared material and mesh (these should be used in edit mode)
|
||||
meshRenderer.sharedMaterial = testMaterial;
|
||||
meshFilter.sharedMesh = testMesh;
|
||||
|
||||
// Act - Get component data which should trigger material/mesh property access
|
||||
var prevIgnore = LogAssert.ignoreFailingMessages;
|
||||
LogAssert.ignoreFailingMessages = true; // Avoid failing due to incidental editor logs during reflection
|
||||
object result;
|
||||
try
|
||||
{
|
||||
result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = prevIgnore;
|
||||
}
|
||||
|
||||
// Assert - Basic success and shape tolerance
|
||||
Assert.IsNotNull(result, "GetComponentData should return a result");
|
||||
if (result is Dictionary<string, object> dict &&
|
||||
dict.TryGetValue("properties", out var propsObj) &&
|
||||
propsObj is Dictionary<string, object> properties)
|
||||
{
|
||||
Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial") || properties.ContainsKey("materials") || properties.ContainsKey("sharedMaterials"),
|
||||
"Serialized data should include a material-related key when present.");
|
||||
}
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testMaterial);
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_DoesNotInstantiateMeshesInEditMode()
|
||||
{
|
||||
// Arrange - Create a GameObject with MeshFilter component
|
||||
var testObject = new GameObject("MeshTestObject");
|
||||
var meshFilter = testObject.AddComponent<MeshFilter>();
|
||||
|
||||
// Create a simple mesh for testing
|
||||
var tempSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||||
var testMesh = tempSphere.GetComponent<MeshFilter>().sharedMesh;
|
||||
UnityEngine.Object.DestroyImmediate(tempSphere);
|
||||
meshFilter.sharedMesh = testMesh;
|
||||
|
||||
// Act - Get component data which should trigger mesh property access
|
||||
var prevIgnore2 = LogAssert.ignoreFailingMessages;
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
object result;
|
||||
try
|
||||
{
|
||||
result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = prevIgnore2;
|
||||
}
|
||||
|
||||
// Assert - Basic success and shape tolerance
|
||||
Assert.IsNotNull(result, "GetComponentData should return a result");
|
||||
if (result is Dictionary<string, object> dict2 &&
|
||||
dict2.TryGetValue("properties", out var propsObj2) &&
|
||||
propsObj2 is Dictionary<string, object> properties2)
|
||||
{
|
||||
Assert.IsTrue(properties2.ContainsKey("mesh") || properties2.ContainsKey("sharedMesh"),
|
||||
"Serialized data should include a mesh-related key when present.");
|
||||
}
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_UsesSharedMaterialInEditMode()
|
||||
{
|
||||
// Arrange - Create a GameObject with MeshRenderer
|
||||
var testObject = new GameObject("SharedMaterialTestObject");
|
||||
var meshRenderer = testObject.AddComponent<MeshRenderer>();
|
||||
|
||||
// Create a test material
|
||||
var testMaterial = new Material(Shader.Find("Standard"));
|
||||
testMaterial.name = "TestMaterial";
|
||||
meshRenderer.sharedMaterial = testMaterial;
|
||||
|
||||
// Act - Get component data in edit mode
|
||||
var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
|
||||
// Assert - Verify that the material property was accessed without instantiation
|
||||
Assert.IsNotNull(result, "GetComponentData should return a result");
|
||||
|
||||
// Check that result is a dictionary with properties key
|
||||
if (result is Dictionary<string, object> resultDict &&
|
||||
resultDict.TryGetValue("properties", out var propertiesObj) &&
|
||||
propertiesObj is Dictionary<string, object> properties)
|
||||
{
|
||||
Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial"),
|
||||
"Serialized data should include 'material' or 'sharedMaterial' when present.");
|
||||
}
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testMaterial);
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_UsesSharedMeshInEditMode()
|
||||
{
|
||||
// Arrange - Create a GameObject with MeshFilter
|
||||
var testObject = new GameObject("SharedMeshTestObject");
|
||||
var meshFilter = testObject.AddComponent<MeshFilter>();
|
||||
|
||||
// Create a test mesh
|
||||
var tempCylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||
var testMesh = tempCylinder.GetComponent<MeshFilter>().sharedMesh;
|
||||
UnityEngine.Object.DestroyImmediate(tempCylinder);
|
||||
testMesh.name = "TestMesh";
|
||||
meshFilter.sharedMesh = testMesh;
|
||||
|
||||
// Act - Get component data in edit mode
|
||||
var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
|
||||
|
||||
// Assert - Verify that the mesh property was accessed without instantiation
|
||||
Assert.IsNotNull(result, "GetComponentData should return a result");
|
||||
|
||||
// Check that result is a dictionary with properties key
|
||||
if (result is Dictionary<string, object> resultDict &&
|
||||
resultDict.TryGetValue("properties", out var propertiesObj) &&
|
||||
propertiesObj is Dictionary<string, object> properties)
|
||||
{
|
||||
Assert.IsTrue(properties.ContainsKey("mesh") || properties.ContainsKey("sharedMesh"),
|
||||
"Serialized data should include 'mesh' or 'sharedMesh' when present.");
|
||||
}
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_HandlesNullMaterialsAndMeshes()
|
||||
{
|
||||
// Arrange - Create a GameObject with MeshRenderer and MeshFilter but no materials/meshes
|
||||
var testObject = new GameObject("NullMaterialMeshTestObject");
|
||||
var meshRenderer = testObject.AddComponent<MeshRenderer>();
|
||||
var meshFilter = testObject.AddComponent<MeshFilter>();
|
||||
|
||||
// Don't set any materials or meshes - they should be null
|
||||
|
||||
// Act - Get component data
|
||||
var rendererResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
var meshFilterResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
|
||||
|
||||
// Assert - Verify that the operations succeeded even with null materials/meshes
|
||||
Assert.IsNotNull(rendererResult, "GetComponentData should handle null materials");
|
||||
Assert.IsNotNull(meshFilterResult, "GetComponentData should handle null meshes");
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_WorksWithMultipleMaterials()
|
||||
{
|
||||
// Arrange - Create a GameObject with MeshRenderer that has multiple materials
|
||||
var testObject = new GameObject("MultiMaterialTestObject");
|
||||
var meshRenderer = testObject.AddComponent<MeshRenderer>();
|
||||
|
||||
// Create multiple test materials
|
||||
var material1 = new Material(Shader.Find("Standard"));
|
||||
material1.name = "TestMaterial1";
|
||||
var material2 = new Material(Shader.Find("Standard"));
|
||||
material2.name = "TestMaterial2";
|
||||
|
||||
meshRenderer.sharedMaterials = new Material[] { material1, material2 };
|
||||
|
||||
// Act - Get component data
|
||||
var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
|
||||
// Assert - Verify that the operation succeeded with multiple materials
|
||||
Assert.IsNotNull(result, "GetComponentData should handle multiple materials");
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(material1);
|
||||
UnityEngine.Object.DestroyImmediate(material2);
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
#region Prefab Asset Handling Tests
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_WithPrefabPath_ReturnsGuidanceError_ForModifyAction()
|
||||
{
|
||||
// Arrange - Attempt to modify a prefab asset directly
|
||||
var modifyParams = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = "Assets/Prefabs/MyPrefab.prefab"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(modifyParams);
|
||||
|
||||
// Assert - Should return an error with guidance to use correct tools
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
|
||||
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse");
|
||||
Assert.IsFalse(errorResponse.Success, "Should indicate failure");
|
||||
Assert.That(errorResponse.Error, Does.Contain("prefab asset"), "Error should mention prefab asset");
|
||||
Assert.That(errorResponse.Error, Does.Contain("manage_asset"), "Error should guide to manage_asset");
|
||||
Assert.That(errorResponse.Error, Does.Contain("manage_prefabs"), "Error should guide to manage_prefabs");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_WithPrefabPath_ReturnsGuidanceError_ForDeleteAction()
|
||||
{
|
||||
// Arrange - Attempt to delete a prefab asset directly
|
||||
var deleteParams = new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["target"] = "Assets/Prefabs/SomePrefab.prefab"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(deleteParams);
|
||||
|
||||
// Assert - Should return an error with guidance
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
|
||||
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse");
|
||||
Assert.IsFalse(errorResponse.Success, "Should indicate failure");
|
||||
Assert.That(errorResponse.Error, Does.Contain("prefab asset"), "Error should mention prefab asset");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_WithPrefabPath_AllowsCreateAction()
|
||||
{
|
||||
// Arrange - Create (instantiate) from a prefab should be allowed
|
||||
// Note: This will fail because the prefab doesn't exist, but the error should NOT be
|
||||
// the prefab redirection error - it should be a "prefab not found" type error
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["prefab_path"] = "Assets/Prefabs/NonExistent.prefab",
|
||||
["name"] = "TestInstance"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(createParams);
|
||||
|
||||
// Assert - Should NOT return the prefab redirection error
|
||||
// (It may fail for other reasons like prefab not found, but not due to redirection)
|
||||
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
|
||||
if (errorResponse != null)
|
||||
{
|
||||
// If there's an error, it should NOT be the prefab asset guidance error
|
||||
Assert.That(errorResponse.Error, Does.Not.Contain("Use 'manage_asset'"),
|
||||
"Create action should not be blocked by prefab check");
|
||||
}
|
||||
// If it's not an error, that's also fine (means create was allowed)
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5931268353eab4ea5baa054e6231e824
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,697 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools.Graphics;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageGraphicsTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageGraphicsTests";
|
||||
private bool _hasVolumeSystem;
|
||||
private bool _hasURP;
|
||||
private bool _hasHDRP;
|
||||
private bool _hasSceneView;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
EnsureFolder(TempRoot);
|
||||
|
||||
var pingResult = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "ping" }));
|
||||
if (pingResult.Value<bool>("success"))
|
||||
{
|
||||
var data = pingResult["data"];
|
||||
_hasVolumeSystem = data?.Value<bool>("hasVolumeSystem") ?? false;
|
||||
_hasURP = data?.Value<bool>("hasURP") ?? false;
|
||||
_hasHDRP = data?.Value<bool>("hasHDRP") ?? false;
|
||||
}
|
||||
|
||||
_hasSceneView = UnityEditor.SceneView.lastActiveSceneView != null;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
|
||||
#else
|
||||
foreach (var go in UnityEngine.Object.FindObjectsOfType<GameObject>())
|
||||
#endif
|
||||
{
|
||||
if (go.name.StartsWith("GfxTest_"))
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
|
||||
// Reset scene debug mode
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "stats_set_scene_debug",
|
||||
["mode"] = "Textured"
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Dispatch / Error Handling
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_NullParams_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(null));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_MissingAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject()));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("action"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_UnknownAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "bogus_action" }));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Unknown action"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ping_ReturnsPipelineInfo()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "ping" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
Assert.That(result["message"].ToString(), Does.Contain("Pipeline"));
|
||||
var data = result["data"];
|
||||
Assert.IsNotNull(data);
|
||||
Assert.IsNotNull(data["pipeline"]);
|
||||
Assert.IsNotNull(data["pipelineName"]);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Volume Actions
|
||||
// =====================================================================
|
||||
|
||||
private void AssumeVolumeSystem()
|
||||
{
|
||||
Assume.That(_hasVolumeSystem, "Volume system not available — skipping.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeCreate_Global_CreatesVolume()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_create",
|
||||
["name"] = "GfxTest_Volume",
|
||||
["is_global"] = true,
|
||||
["priority"] = 10
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsTrue(result["data"]["isGlobal"].Value<bool>());
|
||||
Assert.AreEqual(10, result["data"]["priority"].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeCreate_WithEffects_AddsEffects()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_create",
|
||||
["name"] = "GfxTest_VolumeEffects",
|
||||
["effects"] = new JArray
|
||||
{
|
||||
new JObject { ["type"] = "Bloom", ["intensity"] = 2 },
|
||||
new JObject { ["type"] = "Vignette", ["intensity"] = 0.5 }
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var effects = result["data"]["effects"] as JArray;
|
||||
Assert.IsNotNull(effects);
|
||||
Assert.AreEqual(2, effects.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeCreate_Local_CreatesNonGlobal()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_create",
|
||||
["name"] = "GfxTest_LocalVol",
|
||||
["is_global"] = false
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsFalse(result["data"]["isGlobal"].Value<bool>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeAddEffect_AddsEffect()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_AddFx");
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_AddFx",
|
||||
["effect"] = "Bloom",
|
||||
["parameters"] = new JObject { ["intensity"] = 3 }
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual("Bloom", result["data"]["effect"].ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeAddEffect_Duplicate_ReturnsError()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_DupFx");
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_DupFx",
|
||||
["effect"] = "Bloom"
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_DupFx",
|
||||
["effect"] = "Bloom"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("already exists"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeAddEffect_InvalidEffect_ReturnsError()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_BadFx");
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_BadFx",
|
||||
["effect"] = "FakeEffect"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("not found"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeSetEffect_SetsParameters()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_SetFx");
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_SetFx",
|
||||
["effect"] = "Bloom"
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_set_effect",
|
||||
["target"] = "GfxTest_SetFx",
|
||||
["effect"] = "Bloom",
|
||||
["parameters"] = new JObject { ["intensity"] = 5, ["scatter"] = 0.8 }
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var setParams = result["data"]["set"] as JArray;
|
||||
Assert.IsNotNull(setParams);
|
||||
Assert.That(setParams.Select(t => t.ToString()), Contains.Item("intensity"));
|
||||
Assert.That(setParams.Select(t => t.ToString()), Contains.Item("scatter"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeSetEffect_InvalidParam_ReportsFailed()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_BadParam");
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_BadParam",
|
||||
["effect"] = "Bloom"
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_set_effect",
|
||||
["target"] = "GfxTest_BadParam",
|
||||
["effect"] = "Bloom",
|
||||
["parameters"] = new JObject { ["nonExistent"] = 42 }
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
var failed = result["data"]["failed"] as JArray;
|
||||
Assert.IsNotNull(failed);
|
||||
Assert.That(failed.Select(t => t.ToString()), Contains.Item("nonExistent"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeRemoveEffect_RemovesEffect()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_RmFx");
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_RmFx",
|
||||
["effect"] = "Vignette"
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_remove_effect",
|
||||
["target"] = "GfxTest_RmFx",
|
||||
["effect"] = "Vignette"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Verify it's gone
|
||||
var info = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_get_info",
|
||||
["target"] = "GfxTest_RmFx"
|
||||
}));
|
||||
var effects = info["data"]["effects"] as JArray;
|
||||
Assert.IsNotNull(effects);
|
||||
Assert.AreEqual(0, effects.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeRemoveEffect_NonExistent_ReturnsError()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_RmMissing");
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_remove_effect",
|
||||
["target"] = "GfxTest_RmMissing",
|
||||
["effect"] = "DepthOfField"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("not found"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeGetInfo_ReturnsEffectList()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_Info");
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_add_effect",
|
||||
["target"] = "GfxTest_Info",
|
||||
["effect"] = "Bloom"
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_get_info",
|
||||
["target"] = "GfxTest_Info"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"];
|
||||
Assert.AreEqual("GfxTest_Info", data["name"].ToString());
|
||||
var effects = data["effects"] as JArray;
|
||||
Assert.IsNotNull(effects);
|
||||
Assert.AreEqual(1, effects.Count);
|
||||
Assert.AreEqual("Bloom", effects[0]["type"].ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeGetInfo_NonExistentTarget_ReturnsError()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_get_info",
|
||||
["target"] = "NonExistentVolume"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeSetProperties_UpdatesWeightAndPriority()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
CreateTestVolume("GfxTest_Props");
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_set_properties",
|
||||
["target"] = "GfxTest_Props",
|
||||
["properties"] = new JObject { ["weight"] = 0.5, ["priority"] = 20 }
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var changed = result["data"]["changed"] as JArray;
|
||||
Assert.IsNotNull(changed);
|
||||
Assert.That(changed.Select(t => t.ToString()), Contains.Item("weight"));
|
||||
Assert.That(changed.Select(t => t.ToString()), Contains.Item("priority"));
|
||||
|
||||
// Verify via get_info
|
||||
var info = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_get_info",
|
||||
["target"] = "GfxTest_Props"
|
||||
}));
|
||||
Assert.AreEqual(0.5f, info["data"]["weight"].Value<float>(), 0.01f);
|
||||
Assert.AreEqual(20f, info["data"]["priority"].Value<float>(), 0.01f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeListEffects_ReturnsAvailableTypes()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "volume_list_effects" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var effects = result["data"]["effects"] as JArray;
|
||||
Assert.IsNotNull(effects);
|
||||
Assert.Greater(effects.Count, 0);
|
||||
Assert.IsNotNull(effects[0]["name"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VolumeCreateProfile_CreatesAsset()
|
||||
{
|
||||
AssumeVolumeSystem();
|
||||
string path = $"{TempRoot}/TestProfile";
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_create_profile",
|
||||
["path"] = path
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
string fullPath = result["data"]["path"].ToString();
|
||||
Assert.IsTrue(fullPath.EndsWith(".asset"));
|
||||
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Bake Actions
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void BakeGetSettings_ReturnsLightmapperInfo()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "bake_get_settings" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"];
|
||||
Assert.IsNotNull(data["lightmapper"]);
|
||||
Assert.IsNotNull(data["lightmapResolution"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeSetSettings_ChangesAndRestores()
|
||||
{
|
||||
// Read original
|
||||
var original = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "bake_get_settings" }));
|
||||
int origResolution = original["data"]["lightmapResolution"].Value<int>();
|
||||
|
||||
// Change
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_set_settings",
|
||||
["settings"] = new JObject { ["lightmapResolution"] = 20 }
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var changed = result["data"]["changed"] as JArray;
|
||||
Assert.That(changed.Select(t => t.ToString()), Contains.Item("lightmapResolution"));
|
||||
|
||||
// Verify
|
||||
var verify = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "bake_get_settings" }));
|
||||
Assert.AreEqual(20, verify["data"]["lightmapResolution"].Value<int>());
|
||||
|
||||
// Restore
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_set_settings",
|
||||
["settings"] = new JObject { ["lightmapResolution"] = origResolution }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeStatus_ReportsNotRunning()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "bake_status" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["isRunning"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeClear_Succeeds()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "bake_clear" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeCreateReflectionProbe_CreatesProbe()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_create_reflection_probe",
|
||||
["name"] = "GfxTest_ReflProbe",
|
||||
["position"] = new JArray(0, 1, 0),
|
||||
["size"] = new JArray(10, 10, 10),
|
||||
["resolution"] = 128
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var go = GameObject.Find("GfxTest_ReflProbe");
|
||||
Assert.IsNotNull(go);
|
||||
Assert.IsNotNull(go.GetComponent<ReflectionProbe>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeCreateLightProbeGroup_CreatesGrid()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_create_light_probe_group",
|
||||
["name"] = "GfxTest_LPGroup",
|
||||
["position"] = new JArray(0, 0, 0),
|
||||
["grid_size"] = new JArray(2, 2, 2),
|
||||
["spacing"] = 2
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(8, result["data"]["probeCount"].Value<int>());
|
||||
var go = GameObject.Find("GfxTest_LPGroup");
|
||||
Assert.IsNotNull(go);
|
||||
Assert.IsNotNull(go.GetComponent<LightProbeGroup>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeSetProbePositions_SetsPositions()
|
||||
{
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_create_light_probe_group",
|
||||
["name"] = "GfxTest_LPPos",
|
||||
["grid_size"] = new JArray(1, 1, 1)
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_set_probe_positions",
|
||||
["target"] = "GfxTest_LPPos",
|
||||
["positions"] = new JArray
|
||||
{
|
||||
new JArray(0, 0, 0),
|
||||
new JArray(1, 0, 0),
|
||||
new JArray(0, 1, 0)
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(3, result["data"]["probeCount"].Value<int>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BakeSetProbePositions_WrongComponent_ReturnsError()
|
||||
{
|
||||
var go = new GameObject("GfxTest_NoProbe");
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "bake_set_probe_positions",
|
||||
["target"] = "GfxTest_NoProbe",
|
||||
["positions"] = new JArray { new JArray(0, 0, 0) }
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("LightProbeGroup"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Stats Actions
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void StatsGet_ReturnsCounters()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "stats_get" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["draw_calls"]);
|
||||
Assert.IsNotNull(result["data"]["batches"]);
|
||||
Assert.IsNotNull(result["data"]["triangles"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatsListCounters_ReturnsList()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "stats_list_counters" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var counters = result["data"]["counters"] as JArray;
|
||||
Assert.IsNotNull(counters);
|
||||
Assert.Greater(counters.Count, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatsGetMemory_ReturnsMemoryInfo()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "stats_get_memory" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["totalAllocatedMB"]);
|
||||
Assert.IsNotNull(result["data"]["graphicsDriverMB"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatsSetSceneDebug_ValidMode_Succeeds()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "stats_set_scene_debug",
|
||||
["mode"] = "Wireframe"
|
||||
}));
|
||||
if (_hasSceneView)
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
else
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatsSetSceneDebug_InvalidMode_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "stats_set_scene_debug",
|
||||
["mode"] = "InvalidMode"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Valid:"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Pipeline Actions
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void PipelineGetInfo_ReturnsPipelineName()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "pipeline_get_info" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["pipelineName"]);
|
||||
Assert.IsNotNull(result["data"]["qualityLevelName"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PipelineGetSettings_ReturnsSettings()
|
||||
{
|
||||
Assume.That(_hasURP || _hasHDRP, "Built-in pipeline has no settings asset — skipping.");
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "pipeline_get_settings" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var settings = result["data"]["settings"];
|
||||
Assert.IsNotNull(settings);
|
||||
Assert.IsNotNull(settings["renderScale"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PipelineSetQuality_InvalidLevel_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "pipeline_set_quality",
|
||||
["level"] = "NonExistentLevel"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Available:"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Renderer Feature Actions (URP only)
|
||||
// =====================================================================
|
||||
|
||||
private void AssumeURP()
|
||||
{
|
||||
Assume.That(_hasURP, "URP not available — skipping.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FeatureList_ReturnsFeatures()
|
||||
{
|
||||
AssumeURP();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(
|
||||
new JObject { ["action"] = "feature_list" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNotNull(result["data"]["features"]);
|
||||
Assert.IsNotNull(result["data"]["rendererDataName"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FeatureAdd_InvalidType_ReturnsError()
|
||||
{
|
||||
AssumeURP();
|
||||
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "feature_add",
|
||||
["type"] = "NonExistentFeature"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("not found"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Available:"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Helpers
|
||||
// =====================================================================
|
||||
|
||||
private void CreateTestVolume(string name)
|
||||
{
|
||||
ManageGraphics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "volume_create",
|
||||
["name"] = name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f7dd666649bd48049711444cb73ab7f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageMaterialPropertiesTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageMaterialPropertiesTests";
|
||||
private string _matPath;
|
||||
|
||||
// Built-in's lit shader (Standard) uses "_Color" as its main color property,
|
||||
// while URP/HDRP lit shaders use "_BaseColor". The tool aliases shader "Standard"
|
||||
// to the pipeline-appropriate lit shader, so the color property name must match.
|
||||
private static string MainColorProperty()
|
||||
{
|
||||
return RenderPipelineUtility.GetActivePipeline() == RenderPipelineUtility.PipelineKind.BuiltIn
|
||||
? "_Color"
|
||||
: "_BaseColor";
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
}
|
||||
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialPropertiesTests");
|
||||
}
|
||||
_matPath = $"{TempRoot}/PropTest.mat";
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithValidJsonStringArray_SetsProperty()
|
||||
{
|
||||
string colorProp = MainColorProperty();
|
||||
string jsonProps = "{\"" + colorProp + "\": [1.0, 0.0, 0.0, 1.0]}";
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["materialPath"] = _matPath,
|
||||
["shader"] = "Standard",
|
||||
["properties"] = jsonProps
|
||||
};
|
||||
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
Assert.AreEqual(Color.red, mat.GetColor(colorProp));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithJObjectArray_SetsProperty()
|
||||
{
|
||||
string colorProp = MainColorProperty();
|
||||
var props = new JObject();
|
||||
props[colorProp] = new JArray(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["materialPath"] = _matPath,
|
||||
["shader"] = "Standard",
|
||||
["properties"] = props
|
||||
};
|
||||
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
Assert.AreEqual(Color.green, mat.GetColor(colorProp));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithEmptyProperties_Succeeds()
|
||||
{
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["materialPath"] = _matPath,
|
||||
["shader"] = "Standard",
|
||||
["properties"] = new JObject()
|
||||
};
|
||||
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithInvalidJsonSyntax_ReturnsDetailedError()
|
||||
{
|
||||
// Missing closing brace
|
||||
string invalidJson = "{\"_Color\": [1,0,0,1]";
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["materialPath"] = _matPath,
|
||||
["shader"] = "Standard",
|
||||
["properties"] = invalidJson
|
||||
};
|
||||
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
string msg = result.Value<string>("error");
|
||||
|
||||
// Verify we get exception details
|
||||
Assert.IsTrue(msg.Contains("Invalid JSON"), "Should mention Invalid JSON");
|
||||
// Verify the message contains more than just the prefix (has exception details)
|
||||
Assert.IsTrue(msg.Length > "Invalid JSON".Length,
|
||||
$"Message should contain exception details. Got: {msg}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithNullProperty_HandlesGracefully()
|
||||
{
|
||||
var props = new JObject();
|
||||
props["_Color"] = null;
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["materialPath"] = _matPath,
|
||||
["shader"] = "Standard",
|
||||
["properties"] = props
|
||||
};
|
||||
|
||||
// Should probably succeed but warn or ignore, or fail gracefully
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// We accept either success (ignored) or specific error, but not crash
|
||||
// The new response format uses a bool "success" field
|
||||
var success = result.Value<bool?>("success");
|
||||
Assert.IsNotNull(success, "Response should have success field");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca019b5c6c1ee4e13b77574f2ae53583
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageMaterialReproTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageMaterialReproTests";
|
||||
private string _matPath;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
}
|
||||
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialReproTests");
|
||||
}
|
||||
|
||||
string guid = Guid.NewGuid().ToString("N");
|
||||
_matPath = $"{TempRoot}/ReproMat_{guid}.mat";
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithInvalidJsonString_ReturnsGenericError()
|
||||
{
|
||||
// Arrange
|
||||
// Malformed JSON string (missing closing brace)
|
||||
string invalidJson = "{\"_Color\": [1,0,0,1]";
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["materialPath"] = _matPath,
|
||||
["shader"] = "Standard",
|
||||
["properties"] = invalidJson
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
|
||||
// We expect more detailed error message after fix
|
||||
var message = result.Value<string>("error");
|
||||
Assert.IsTrue(message.StartsWith("Invalid JSON in properties"), "Message should start with prefix");
|
||||
Assert.AreNotEqual("Invalid JSON in properties", message, "Message should contain exception details");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c967207bf78c344178484efe6d87dea7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageMaterialStressTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageMaterialStressTests";
|
||||
private string _matPath;
|
||||
private GameObject _cube;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
}
|
||||
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialStressTests");
|
||||
}
|
||||
|
||||
string guid = Guid.NewGuid().ToString("N");
|
||||
_matPath = $"{TempRoot}/StressMat_{guid}.mat";
|
||||
|
||||
var material = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
|
||||
material.color = Color.white;
|
||||
AssetDatabase.CreateAsset(material, _matPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
_cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
_cube.name = "StressCube";
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_cube != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(_cube);
|
||||
}
|
||||
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleInvalidInputs_ReturnsError_NotException()
|
||||
{
|
||||
// 1. Bad path
|
||||
var paramsBadPath = new JObject
|
||||
{
|
||||
["action"] = "set_material_color",
|
||||
["materialPath"] = "Assets/NonExistent/Ghost.mat",
|
||||
["color"] = new JArray(1f, 0f, 0f, 1f)
|
||||
};
|
||||
var resultBadPath = ToJObject(ManageMaterial.HandleCommand(paramsBadPath));
|
||||
Assert.IsFalse(resultBadPath.Value<bool>("success"));
|
||||
StringAssert.Contains("Could not find material", resultBadPath.Value<string>("error"));
|
||||
|
||||
// 2. Bad color array (too short)
|
||||
var paramsBadColor = new JObject
|
||||
{
|
||||
["action"] = "set_material_color",
|
||||
["materialPath"] = _matPath,
|
||||
["color"] = new JArray(1f) // Invalid
|
||||
};
|
||||
var resultBadColor = ToJObject(ManageMaterial.HandleCommand(paramsBadColor));
|
||||
Assert.IsFalse(resultBadColor.Value<bool>("success"));
|
||||
StringAssert.Contains("Invalid color format", resultBadColor.Value<string>("error"));
|
||||
|
||||
// 3. Bad slot index
|
||||
// Assign material first
|
||||
var renderer = _cube.GetComponent<Renderer>();
|
||||
renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
|
||||
var paramsBadSlot = new JObject
|
||||
{
|
||||
["action"] = "assign_material_to_renderer",
|
||||
["target"] = "StressCube",
|
||||
["searchMethod"] = "by_name",
|
||||
["materialPath"] = _matPath,
|
||||
["slot"] = 99
|
||||
};
|
||||
var resultBadSlot = ToJObject(ManageMaterial.HandleCommand(paramsBadSlot));
|
||||
Assert.IsFalse(resultBadSlot.Value<bool>("success"));
|
||||
StringAssert.Contains("out of bounds", resultBadSlot.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StateIsolation_PropertyBlockDoesNotLeakToSharedMaterial()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = _cube.GetComponent<Renderer>();
|
||||
var sharedMat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
renderer.sharedMaterial = sharedMat;
|
||||
|
||||
// Initial color
|
||||
var initialColor = Color.white;
|
||||
if (sharedMat.HasProperty("_BaseColor")) sharedMat.SetColor("_BaseColor", initialColor);
|
||||
else if (sharedMat.HasProperty("_Color")) sharedMat.SetColor("_Color", initialColor);
|
||||
|
||||
// Act - Set Property Block Color
|
||||
var blockColor = Color.red;
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "set_renderer_color",
|
||||
["target"] = "StressCube",
|
||||
["searchMethod"] = "by_name",
|
||||
["color"] = new JArray(blockColor.r, blockColor.g, blockColor.b, blockColor.a),
|
||||
["mode"] = "property_block"
|
||||
};
|
||||
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Assert
|
||||
// 1. Renderer has property block with Red
|
||||
var block = new MaterialPropertyBlock();
|
||||
renderer.GetPropertyBlock(block, 0);
|
||||
var propName = sharedMat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
|
||||
Assert.AreEqual(blockColor, block.GetColor(propName));
|
||||
|
||||
// 2. Shared material remains White
|
||||
var sharedColor = sharedMat.GetColor(propName);
|
||||
Assert.AreEqual(initialColor, sharedColor, "Shared material color should NOT change when using PropertyBlock");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Integration_PureManageMaterial_AssignsMaterialAndModifies()
|
||||
{
|
||||
// This simulates a workflow where we create a GO, assign a mat, then tweak it.
|
||||
|
||||
// 1. Create GO (already done in Setup, but let's verify)
|
||||
Assert.IsNotNull(_cube);
|
||||
|
||||
// 2. Assign Material using ManageMaterial
|
||||
var assignParams = new JObject
|
||||
{
|
||||
["action"] = "assign_material_to_renderer",
|
||||
["target"] = "StressCube",
|
||||
["searchMethod"] = "by_name",
|
||||
["materialPath"] = _matPath
|
||||
};
|
||||
var assignResult = ToJObject(ManageMaterial.HandleCommand(assignParams));
|
||||
Assert.IsTrue(assignResult.Value<bool>("success"), assignResult.ToString());
|
||||
|
||||
// Verify assignment
|
||||
var renderer = _cube.GetComponent<Renderer>();
|
||||
Assert.AreEqual(Path.GetFileNameWithoutExtension(_matPath), renderer.sharedMaterial.name);
|
||||
|
||||
// 3. Modify Shared Material Color using ManageMaterial
|
||||
var newColor = Color.blue;
|
||||
var colorParams = new JObject
|
||||
{
|
||||
["action"] = "set_material_color",
|
||||
["materialPath"] = _matPath,
|
||||
["color"] = new JArray(newColor.r, newColor.g, newColor.b, newColor.a)
|
||||
};
|
||||
var colorResult = ToJObject(ManageMaterial.HandleCommand(colorParams));
|
||||
Assert.IsTrue(colorResult.Value<bool>("success"), colorResult.ToString());
|
||||
|
||||
// Verify color changed on renderer (because it's shared)
|
||||
var propName = renderer.sharedMaterial.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
|
||||
Assert.AreEqual(newColor, renderer.sharedMaterial.GetColor(propName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49ecdd3f43cf54deea7508f317efcb45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageMaterialTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageMaterialTests";
|
||||
private string _matPath;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
}
|
||||
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialTests");
|
||||
}
|
||||
|
||||
string guid = Guid.NewGuid().ToString("N");
|
||||
_matPath = $"{TempRoot}/TestMat_{guid}.mat";
|
||||
|
||||
// Create a basic material
|
||||
var material = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
|
||||
AssetDatabase.CreateAsset(material, _matPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetMaterialShaderProperty_SetsColor()
|
||||
{
|
||||
// Arrange
|
||||
var color = new Color(1f, 1f, 0f, 1f); // Yellow
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "set_material_shader_property",
|
||||
["materialPath"] = _matPath,
|
||||
["property"] = "_BaseColor", // URP
|
||||
["value"] = new JArray(color.r, color.g, color.b, color.a)
|
||||
};
|
||||
|
||||
// Check if using Standard shader (fallback)
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
if (mat.shader.name == "Standard")
|
||||
{
|
||||
paramsObj["property"] = "_Color";
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath); // Reload
|
||||
var prop = mat.shader.name == "Standard" ? "_Color" : "_BaseColor";
|
||||
|
||||
Assert.IsTrue(mat.HasProperty(prop), $"Material should have property {prop}");
|
||||
Assert.AreEqual(color, mat.GetColor(prop));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetMaterialColor_SetsColorWithFallback()
|
||||
{
|
||||
// Arrange
|
||||
var color = new Color(0f, 1f, 0f, 1f); // Green
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "set_material_color",
|
||||
["materialPath"] = _matPath,
|
||||
["color"] = new JArray(color.r, color.g, color.b, color.a)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
var prop = mat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
|
||||
|
||||
Assert.IsTrue(mat.HasProperty(prop), $"Material should have property {prop}");
|
||||
Assert.AreEqual(color, mat.GetColor(prop));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssignMaterialToRenderer_Works()
|
||||
{
|
||||
// Arrange
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
go.name = "AssignTestCube";
|
||||
|
||||
try
|
||||
{
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "assign_material_to_renderer",
|
||||
["target"] = "AssignTestCube",
|
||||
["searchMethod"] = "by_name",
|
||||
["materialPath"] = _matPath,
|
||||
["slot"] = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var renderer = go.GetComponent<Renderer>();
|
||||
Assert.IsNotNull(renderer.sharedMaterial);
|
||||
// Compare names because objects might be different instances (loaded vs scene)
|
||||
var matName = Path.GetFileNameWithoutExtension(_matPath);
|
||||
Assert.AreEqual(matName, renderer.sharedMaterial.name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetRendererColor_PropertyBlock_Works()
|
||||
{
|
||||
// Arrange
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
go.name = "BlockTestCube";
|
||||
|
||||
// Assign the material first so we have something valid
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
go.GetComponent<Renderer>().sharedMaterial = mat;
|
||||
|
||||
try
|
||||
{
|
||||
var color = new Color(1f, 0f, 0f, 1f); // Red
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "set_renderer_color",
|
||||
["target"] = "BlockTestCube",
|
||||
["searchMethod"] = "by_name",
|
||||
["color"] = new JArray(color.r, color.g, color.b, color.a),
|
||||
["mode"] = "property_block"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var renderer = go.GetComponent<Renderer>();
|
||||
var block = new MaterialPropertyBlock();
|
||||
renderer.GetPropertyBlock(block, 0);
|
||||
|
||||
var prop = mat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
|
||||
Assert.AreEqual(color, block.GetColor(prop));
|
||||
|
||||
// Verify material asset didn't change (it was originally white/gray from setup?)
|
||||
// We didn't check original color, but property block shouldn't affect shared material
|
||||
// We can check that sharedMaterial color is NOT red if we set it to something else first
|
||||
// But assuming test isolation, we can just verify the block is set.
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetMaterialInfo_ReturnsProperties()
|
||||
{
|
||||
// Arrange
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "get_material_info",
|
||||
["materialPath"] = _matPath
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data, "Response should have data object");
|
||||
Assert.IsNotNull(data["properties"]);
|
||||
Assert.IsInstanceOf<JArray>(data["properties"]);
|
||||
var props = data["properties"] as JArray;
|
||||
Assert.IsTrue(props.Count > 0);
|
||||
|
||||
// Check for standard properties
|
||||
bool foundColor = false;
|
||||
foreach(var p in props)
|
||||
{
|
||||
var name = p["name"]?.ToString();
|
||||
if (name == "_Color" || name == "_BaseColor") foundColor = true;
|
||||
}
|
||||
Assert.IsTrue(foundColor, "Should find color property");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f96e01f904e044608d97842c3a3cb43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,974 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools.Physics;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManagePhysicsTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManagePhysicsTests";
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
EnsureFolder(TempRoot);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
|
||||
#else
|
||||
foreach (var go in UnityEngine.Object.FindObjectsOfType<GameObject>())
|
||||
#endif
|
||||
{
|
||||
if (go.name.StartsWith("PhysTest_"))
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Dispatch / Error Handling
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_NullParams_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(null));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_MissingAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject()));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("action"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_UnknownAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(
|
||||
new JObject { ["action"] = "bogus_action" }));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Unknown action"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Ping
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void Ping_ReturnsPhysicsStatus()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(
|
||||
new JObject { ["action"] = "ping" }));
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
Assert.That(result["message"].ToString(), Does.Contain("Physics"));
|
||||
var data = result["data"];
|
||||
Assert.IsNotNull(data);
|
||||
Assert.IsNotNull(data["gravity3d"]);
|
||||
Assert.IsNotNull(data["gravity2d"]);
|
||||
Assert.IsNotNull(data["simulationMode"]);
|
||||
Assert.IsNotNull(data["defaultSolverIterations"]);
|
||||
Assert.IsNotNull(data["bounceThreshold"]);
|
||||
Assert.IsNotNull(data["queriesHitTriggers"]);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// GetSettings
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void GetSettings_3D_ReturnsGravity()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_settings",
|
||||
["dimension"] = "3d"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"];
|
||||
Assert.AreEqual("3d", data["dimension"].ToString());
|
||||
var gravity = data["gravity"] as JArray;
|
||||
Assert.IsNotNull(gravity);
|
||||
Assert.AreEqual(3, gravity.Count);
|
||||
Assert.IsNotNull(data["defaultContactOffset"]);
|
||||
Assert.IsNotNull(data["sleepThreshold"]);
|
||||
Assert.IsNotNull(data["defaultSolverIterations"]);
|
||||
Assert.IsNotNull(data["simulationMode"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSettings_DefaultDimension_Returns3D()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_settings"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual("3d", result["data"]["dimension"].ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSettings_2D_ReturnsGravity()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_settings",
|
||||
["dimension"] = "2d"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"];
|
||||
Assert.AreEqual("2d", data["dimension"].ToString());
|
||||
var gravity = data["gravity"] as JArray;
|
||||
Assert.IsNotNull(gravity);
|
||||
Assert.AreEqual(2, gravity.Count);
|
||||
Assert.IsNotNull(data["velocityIterations"]);
|
||||
Assert.IsNotNull(data["positionIterations"]);
|
||||
Assert.IsNotNull(data["queriesHitTriggers"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSettings_InvalidDimension_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_settings",
|
||||
["dimension"] = "4d"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Invalid dimension"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// SetSettings 3D
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void SetSettings_3D_ChangesGravity()
|
||||
{
|
||||
// Save original
|
||||
var originalGravity = UnityEngine.Physics.gravity;
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "3d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["gravity"] = new JArray(0, -20, 0)
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var changed = result["data"]["changed"] as JArray;
|
||||
Assert.IsNotNull(changed);
|
||||
Assert.That(changed.ToString(), Does.Contain("gravity"));
|
||||
|
||||
// Verify the change took effect
|
||||
Assert.AreEqual(-20f, UnityEngine.Physics.gravity.y, 0.01f);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Restore original
|
||||
UnityEngine.Physics.gravity = originalGravity;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_3D_ChangesSolverIterations()
|
||||
{
|
||||
var original = UnityEngine.Physics.defaultSolverIterations;
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "3d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["defaultSolverIterations"] = 12
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(12, UnityEngine.Physics.defaultSolverIterations);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Physics.defaultSolverIterations = original;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_3D_UnknownKey_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "3d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["nonExistentSetting"] = 42
|
||||
}
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Unknown"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("nonExistentSetting"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_3D_InvalidGravityArray_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "3d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["gravity"] = new JArray(0, -10) // Only 2 elements for 3D
|
||||
}
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("[x, y, z]"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_EmptySettings_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "3d",
|
||||
["settings"] = new JObject()
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("settings"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_MissingSettings_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "3d"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("settings"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// SetSettings 2D
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void SetSettings_2D_ChangesGravity()
|
||||
{
|
||||
var originalGravity = Physics2D.gravity;
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "2d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["gravity"] = new JArray(0, -20)
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var changed = result["data"]["changed"] as JArray;
|
||||
Assert.IsNotNull(changed);
|
||||
Assert.That(changed.ToString(), Does.Contain("gravity"));
|
||||
|
||||
Assert.AreEqual(-20f, Physics2D.gravity.y, 0.01f);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Physics2D.gravity = originalGravity;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_2D_UnknownKey_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "2d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["fakeSetting"] = true
|
||||
}
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Unknown"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("fakeSetting"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSettings_2D_InvalidGravityArray_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_settings",
|
||||
["dimension"] = "2d",
|
||||
["settings"] = new JObject
|
||||
{
|
||||
["gravity"] = new JArray(0) // Only 1 element for 2D
|
||||
}
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("[x, y]"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// GetCollisionMatrix
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void CreatePhysicsMaterial_3D_CreatesAsset()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_physics_material",
|
||||
["name"] = "TestMat3D",
|
||||
["path"] = TempRoot,
|
||||
["dimension"] = "3d"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
string expectedPath = TempRoot + "/TestMat3D.physicMaterial";
|
||||
Assert.AreEqual(expectedPath, result["data"]["path"].ToString());
|
||||
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(expectedPath),
|
||||
"Asset should exist on disk.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreatePhysicsMaterial_2D_CreatesAsset()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_physics_material",
|
||||
["name"] = "TestMat2D",
|
||||
["path"] = TempRoot,
|
||||
["dimension"] = "2d"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
string expectedPath = TempRoot + "/TestMat2D.physicsMaterial2D";
|
||||
Assert.AreEqual(expectedPath, result["data"]["path"].ToString());
|
||||
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(expectedPath),
|
||||
"2D physics material asset should exist on disk.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreatePhysicsMaterial_MissingName_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_physics_material",
|
||||
["path"] = TempRoot,
|
||||
["dimension"] = "3d"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("name"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ConfigurePhysicsMaterial
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void ConfigurePhysicsMaterial_3D_UpdatesProperties()
|
||||
{
|
||||
// Create material first
|
||||
ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_physics_material",
|
||||
["name"] = "ConfigTest3D",
|
||||
["path"] = TempRoot,
|
||||
["dimension"] = "3d",
|
||||
["bounciness"] = 0
|
||||
});
|
||||
|
||||
string matPath = TempRoot + "/ConfigTest3D.physicMaterial";
|
||||
|
||||
// Configure it
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "configure_physics_material",
|
||||
["path"] = matPath,
|
||||
["dimension"] = "3d",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["bounciness"] = 0.75
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Verify the property changed
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var mat = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(matPath);
|
||||
#else
|
||||
var mat = AssetDatabase.LoadAssetAtPath<PhysicMaterial>(matPath);
|
||||
#endif
|
||||
Assert.IsNotNull(mat);
|
||||
Assert.AreEqual(0.75f, mat.bounciness, 0.01f);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// AssignPhysicsMaterial
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void AssignPhysicsMaterial_ToBoxCollider()
|
||||
{
|
||||
// Create a physics material
|
||||
ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_physics_material",
|
||||
["name"] = "AssignTest3D",
|
||||
["path"] = TempRoot,
|
||||
["dimension"] = "3d"
|
||||
});
|
||||
|
||||
string matPath = TempRoot + "/AssignTest3D.physicMaterial";
|
||||
|
||||
// Create a GameObject with a BoxCollider
|
||||
var go = new GameObject("PhysTest_BoxCollider");
|
||||
go.AddComponent<BoxCollider>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "assign_physics_material",
|
||||
["target"] = "PhysTest_BoxCollider",
|
||||
["material_path"] = matPath,
|
||||
["search_method"] = "by_name"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var collider = go.GetComponent<BoxCollider>();
|
||||
Assert.IsNotNull(collider.sharedMaterial,
|
||||
"BoxCollider.sharedMaterial should be set after assignment.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssignPhysicsMaterial_NoCollider_ReturnsError()
|
||||
{
|
||||
// Create a physics material
|
||||
ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_physics_material",
|
||||
["name"] = "AssignNoCollider",
|
||||
["path"] = TempRoot,
|
||||
["dimension"] = "3d"
|
||||
});
|
||||
|
||||
string matPath = TempRoot + "/AssignNoCollider.physicMaterial";
|
||||
|
||||
// Create a GameObject WITHOUT any collider
|
||||
var go = new GameObject("PhysTest_NoCollider");
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "assign_physics_material",
|
||||
["target"] = "PhysTest_NoCollider",
|
||||
["material_path"] = matPath,
|
||||
["search_method"] = "by_name"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("collider").IgnoreCase);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// AddJoint
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void AddJoint_Hinge_RequiresRigidbody()
|
||||
{
|
||||
var go = new GameObject("PhysTest_NoRB");
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add_joint",
|
||||
["target"] = "PhysTest_NoRB",
|
||||
["joint_type"] = "hinge"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Rigidbody"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddJoint_Hinge_Success()
|
||||
{
|
||||
var go = new GameObject("PhysTest_HingeAdd");
|
||||
go.AddComponent<Rigidbody>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add_joint",
|
||||
["target"] = "PhysTest_HingeAdd",
|
||||
["joint_type"] = "hinge"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.That(result["data"]["jointType"].ToString(), Is.EqualTo("HingeJoint"));
|
||||
|
||||
var hinge = go.GetComponent<HingeJoint>();
|
||||
Assert.IsNotNull(hinge, "HingeJoint should be present on the GameObject.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddJoint_UnknownType_ReturnsError()
|
||||
{
|
||||
var go = new GameObject("PhysTest_UnknownJoint");
|
||||
go.AddComponent<Rigidbody>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "add_joint",
|
||||
["target"] = "PhysTest_UnknownJoint",
|
||||
["joint_type"] = "rubber_band"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Unknown"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("rubber_band"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ConfigureJoint
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void ConfigureJoint_Hinge_Motor()
|
||||
{
|
||||
var go = new GameObject("PhysTest_HingeMotor");
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<HingeJoint>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "configure_joint",
|
||||
["target"] = "PhysTest_HingeMotor",
|
||||
["joint_type"] = "hinge",
|
||||
["motor"] = new JObject
|
||||
{
|
||||
["targetVelocity"] = 90f,
|
||||
["force"] = 50f,
|
||||
["freeSpin"] = false
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var hinge = go.GetComponent<HingeJoint>();
|
||||
Assert.IsTrue(hinge.useMotor, "useMotor should be true after motor config.");
|
||||
Assert.AreEqual(90f, hinge.motor.targetVelocity, 0.01f);
|
||||
Assert.AreEqual(50f, hinge.motor.force, 0.01f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConfigureJoint_Hinge_Limits()
|
||||
{
|
||||
var go = new GameObject("PhysTest_HingeLimits");
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<HingeJoint>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "configure_joint",
|
||||
["target"] = "PhysTest_HingeLimits",
|
||||
["joint_type"] = "hinge",
|
||||
["limits"] = new JObject
|
||||
{
|
||||
["min"] = -45f,
|
||||
["max"] = 45f,
|
||||
["bounciness"] = 0.5f
|
||||
}
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var hinge = go.GetComponent<HingeJoint>();
|
||||
Assert.IsTrue(hinge.useLimits, "useLimits should be true after limits config.");
|
||||
Assert.AreEqual(-45f, hinge.limits.min, 0.01f);
|
||||
Assert.AreEqual(45f, hinge.limits.max, 0.01f);
|
||||
Assert.AreEqual(0.5f, hinge.limits.bounciness, 0.01f);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// RemoveJoint
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void RemoveJoint_Specific_Success()
|
||||
{
|
||||
var go = new GameObject("PhysTest_RemoveHinge");
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<HingeJoint>();
|
||||
Assert.IsNotNull(go.GetComponent<HingeJoint>());
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "remove_joint",
|
||||
["target"] = "PhysTest_RemoveHinge",
|
||||
["joint_type"] = "hinge"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(1, result["data"]["removedCount"].Value<int>());
|
||||
Assert.IsNull(go.GetComponent<HingeJoint>(),
|
||||
"HingeJoint should be removed.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemoveJoint_All()
|
||||
{
|
||||
var go = new GameObject("PhysTest_RemoveAll");
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<HingeJoint>();
|
||||
go.AddComponent<FixedJoint>();
|
||||
Assert.IsNotNull(go.GetComponent<HingeJoint>());
|
||||
Assert.IsNotNull(go.GetComponent<FixedJoint>());
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "remove_joint",
|
||||
["target"] = "PhysTest_RemoveAll"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(2, result["data"]["removedCount"].Value<int>());
|
||||
Assert.IsNull(go.GetComponent<HingeJoint>(),
|
||||
"HingeJoint should be removed.");
|
||||
Assert.IsNull(go.GetComponent<FixedJoint>(),
|
||||
"FixedJoint should be removed.");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// GetCollisionMatrix
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void GetCollisionMatrix_3D_ReturnsMatrix()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_collision_matrix",
|
||||
["dimension"] = "3d"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"];
|
||||
Assert.IsNotNull(data["layers"]);
|
||||
Assert.IsTrue(data["layers"] is JArray);
|
||||
Assert.IsTrue(((JArray)data["layers"]).Count > 0);
|
||||
Assert.IsNotNull(data["matrix"]);
|
||||
Assert.IsTrue(data["matrix"].Type == JTokenType.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetCollisionMatrix_2D_ReturnsMatrix()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_collision_matrix",
|
||||
["dimension"] = "2d"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"];
|
||||
Assert.IsNotNull(data["layers"]);
|
||||
Assert.IsTrue(data["layers"] is JArray);
|
||||
Assert.IsTrue(((JArray)data["layers"]).Count > 0);
|
||||
Assert.IsNotNull(data["matrix"]);
|
||||
Assert.IsTrue(data["matrix"].Type == JTokenType.Object);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// SetCollisionMatrix
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void SetCollisionMatrix_3D_ChangesAndRestores()
|
||||
{
|
||||
bool original = !UnityEngine.Physics.GetIgnoreLayerCollision(0, 0);
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_collision_matrix",
|
||||
["dimension"] = "3d",
|
||||
["layer_a"] = "Default",
|
||||
["layer_b"] = "Default",
|
||||
["collide"] = false
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsTrue(UnityEngine.Physics.GetIgnoreLayerCollision(0, 0),
|
||||
"Layers should be ignoring collision after setting collide=false");
|
||||
|
||||
result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_collision_matrix",
|
||||
["dimension"] = "3d",
|
||||
["layer_a"] = "Default",
|
||||
["layer_b"] = "Default",
|
||||
["collide"] = true
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsFalse(UnityEngine.Physics.GetIgnoreLayerCollision(0, 0),
|
||||
"Layers should NOT be ignoring collision after setting collide=true");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Physics.IgnoreLayerCollision(0, 0, !original);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetCollisionMatrix_InvalidLayer_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_collision_matrix",
|
||||
["dimension"] = "3d",
|
||||
["layer_a"] = "NonExistentLayerXYZ",
|
||||
["layer_b"] = "Default"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("layer_a"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetCollisionMatrix_MissingParams_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "set_collision_matrix",
|
||||
["dimension"] = "3d"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("layer_a"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Raycast
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void Raycast_HitsCollider()
|
||||
{
|
||||
// Use an offset origin/direction to avoid hitting unrelated scene objects
|
||||
var go = new GameObject("PhysTest_RayTarget");
|
||||
go.transform.position = new Vector3(500, 500, 5);
|
||||
var col = go.AddComponent<BoxCollider>();
|
||||
col.size = new Vector3(10, 10, 1);
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "raycast",
|
||||
["origin"] = new JArray(500, 500, 0),
|
||||
["direction"] = new JArray(0, 0, 1),
|
||||
["max_distance"] = 100
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsTrue(result["data"].Value<bool>("hit"));
|
||||
Assert.AreEqual("PhysTest_RayTarget", result["data"]["gameObject"].ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Raycast_MissesWhenNoTarget()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "raycast",
|
||||
["origin"] = new JArray(0, 1000, 0),
|
||||
["direction"] = new JArray(0, 1, 0),
|
||||
["max_distance"] = 1
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsFalse(result["data"].Value<bool>("hit"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Raycast_MissingOrigin_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "raycast",
|
||||
["direction"] = new JArray(0, 0, 1)
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("origin"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Overlap
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void Overlap_Sphere_FindsColliders()
|
||||
{
|
||||
var go = new GameObject("PhysTest_OverlapTarget");
|
||||
go.transform.position = Vector3.zero;
|
||||
go.AddComponent<SphereCollider>();
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "overlap",
|
||||
["shape"] = "sphere",
|
||||
["position"] = new JArray(0, 0, 0),
|
||||
["size"] = 10
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var colliders = result["data"]["colliders"] as JArray;
|
||||
Assert.IsNotNull(colliders);
|
||||
Assert.IsTrue(colliders.Count > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Overlap_MissingShape_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "overlap",
|
||||
["position"] = new JArray(0, 0, 0),
|
||||
["size"] = 10
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("shape"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Validate
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void Validate_DetectsMeshColliderWithoutConvex()
|
||||
{
|
||||
var go = new GameObject("PhysTest_BadMesh");
|
||||
go.AddComponent<Rigidbody>();
|
||||
var mc = go.AddComponent<MeshCollider>();
|
||||
mc.convex = false;
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "validate",
|
||||
["target"] = "PhysTest_BadMesh"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var warnings = result["data"]["warnings"] as JArray;
|
||||
Assert.IsNotNull(warnings);
|
||||
Assert.IsTrue(warnings.Count > 0);
|
||||
bool foundConvex = false;
|
||||
foreach (var w in warnings)
|
||||
{
|
||||
if (w.ToString().Contains("Convex"))
|
||||
{ foundConvex = true; break; }
|
||||
}
|
||||
Assert.IsTrue(foundConvex, "Should detect MeshCollider without Convex.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validate_DetectsColliderWithoutRigidbody()
|
||||
{
|
||||
var go = new GameObject("PhysTest_NoRigidbody");
|
||||
go.isStatic = false;
|
||||
go.AddComponent<BoxCollider>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "validate",
|
||||
["target"] = "PhysTest_NoRigidbody"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var warnings = result["data"]["warnings"] as JArray;
|
||||
Assert.IsNotNull(warnings);
|
||||
bool foundWarning = false;
|
||||
foreach (var w in warnings)
|
||||
{
|
||||
if (w.ToString().Contains("Collider") && w.ToString().Contains("Rigidbody"))
|
||||
{ foundWarning = true; break; }
|
||||
}
|
||||
Assert.IsTrue(foundWarning, "Should detect Collider without Rigidbody on non-static object.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validate_CleanObject_FewWarnings()
|
||||
{
|
||||
var go = new GameObject("PhysTest_Clean");
|
||||
go.AddComponent<Rigidbody>();
|
||||
go.AddComponent<BoxCollider>();
|
||||
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "validate",
|
||||
["target"] = "PhysTest_Clean"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(1, result["data"].Value<int>("objects_scanned"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validate_TargetNotFound_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "validate",
|
||||
["target"] = "PhysTest_DoesNotExistXYZ"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// SimulateStep
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void SimulateStep_ExecutesWithoutError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "simulate_step",
|
||||
["steps"] = 1
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(1, result["data"].Value<int>("steps_executed"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimulateStep_ClampsMax()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "simulate_step",
|
||||
["steps"] = 999
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.AreEqual(100, result["data"].Value<int>("steps_executed"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimulateStep_InvalidDimension_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "simulate_step",
|
||||
["dimension"] = "4d"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f48e1b7be1ed4a29b698127aac0e55e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a8d9f0e1b2c3d4e5f6a7b8c9d0e1f2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,251 @@
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Tools.Prefabs;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManagePrefabsStageTests
|
||||
{
|
||||
private const string TempDirectory = "Assets/Temp/ManagePrefabsStageTests";
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
EnsureFolder(TempDirectory);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
|
||||
if (AssetDatabase.IsValidFolder(TempDirectory))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempDirectory);
|
||||
}
|
||||
|
||||
CleanupEmptyParentFolders(TempDirectory);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OpenPrefabStage_RequiresPrefabPath()
|
||||
{
|
||||
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "open_prefab_stage"
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
StringAssert.Contains("prefabPath", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OpenPrefabStage_RejectsNonPrefabPath()
|
||||
{
|
||||
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "open_prefab_stage",
|
||||
["prefabPath"] = "Assets/Temp/NotPrefab.txt"
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
StringAssert.Contains(".prefab", result.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OpenPrefabStage_OpensPrefabStageAndReturnsStageData()
|
||||
{
|
||||
string prefabPath = CreateTestPrefab("OpenStageRoot");
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "open_prefab_stage",
|
||||
["prefabPath"] = prefabPath
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
Assert.AreEqual(prefabPath, result["data"].Value<string>("prefabPath"));
|
||||
Assert.AreEqual(prefabPath, result["data"].Value<string>("openedPrefabPath"));
|
||||
Assert.AreEqual("OpenStageRoot", result["data"].Value<string>("rootName"));
|
||||
|
||||
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
Assert.IsNotNull(stage);
|
||||
Assert.AreEqual(prefabPath, stage.assetPath);
|
||||
|
||||
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "close_prefab_stage"
|
||||
}));
|
||||
Assert.IsTrue(closeResult.Value<bool>("success"));
|
||||
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
SafeDeleteAsset(prefabPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OpenPrefabStage_AcceptsPathAlias()
|
||||
{
|
||||
string prefabPath = CreateTestPrefab("AliasRoot");
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "open_prefab_stage",
|
||||
["path"] = prefabPath
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
Assert.AreEqual(prefabPath, result["data"].Value<string>("openedPrefabPath"));
|
||||
Assert.IsNotNull(PrefabStageUtility.GetCurrentPrefabStage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
SafeDeleteAsset(prefabPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OpenPrefabStage_PrefabPathTakesPrecedenceOverPath()
|
||||
{
|
||||
string prefabPath = CreateTestPrefab("PrefabPathRoot");
|
||||
string aliasPath = CreateTestPrefab("AliasPathRoot");
|
||||
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "open_prefab_stage",
|
||||
["prefabPath"] = prefabPath,
|
||||
["path"] = aliasPath
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
|
||||
var currentStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
Assert.IsNotNull(currentStage, "Expected a prefab stage to be open.");
|
||||
Assert.AreEqual(
|
||||
prefabPath,
|
||||
currentStage.assetPath,
|
||||
"prefabPath should take precedence over path when both are provided."
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
SafeDeleteAsset(prefabPath);
|
||||
SafeDeleteAsset(aliasPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SavePrefabStage_PersistsPrefabContentChanges()
|
||||
{
|
||||
string prefabPath = CreateTestPrefab("SaveStageRoot");
|
||||
|
||||
try
|
||||
{
|
||||
AssertOpenPrefabStage(prefabPath);
|
||||
|
||||
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
Assert.IsNotNull(stage, "Expected prefab stage to be open.");
|
||||
|
||||
var child = new GameObject("SavedChild");
|
||||
child.transform.SetParent(stage.prefabContentsRoot.transform, false);
|
||||
|
||||
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "save_prefab_stage"
|
||||
}));
|
||||
|
||||
Assert.IsTrue(saveResult.Value<bool>("success"), $"Expected save to succeed but got: {saveResult}");
|
||||
Assert.AreEqual(prefabPath, saveResult["data"].Value<string>("prefabPath"));
|
||||
|
||||
StageUtility.GoToMainStage();
|
||||
|
||||
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
Assert.IsNotNull(reloaded.transform.Find("SavedChild"), "Saved prefab should contain the new child after save_prefab_stage.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
SafeDeleteAsset(prefabPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClosePrefabStage_SaveBeforeClose_PersistsChanges()
|
||||
{
|
||||
string prefabPath = CreateTestPrefab("CloseSaveRoot");
|
||||
|
||||
try
|
||||
{
|
||||
AssertOpenPrefabStage(prefabPath);
|
||||
|
||||
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
Assert.IsNotNull(stage, "Expected prefab stage to be open.");
|
||||
|
||||
var child = new GameObject("CloseSavedChild");
|
||||
child.transform.SetParent(stage.prefabContentsRoot.transform, false);
|
||||
|
||||
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "close_prefab_stage",
|
||||
["saveBeforeClose"] = true
|
||||
}));
|
||||
|
||||
Assert.IsTrue(closeResult.Value<bool>("success"), $"Expected close with save to succeed but got: {closeResult}");
|
||||
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage(), "Prefab stage should be closed after close_prefab_stage.");
|
||||
|
||||
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
Assert.IsNotNull(reloaded.transform.Find("CloseSavedChild"), "Saved prefab should contain the new child after close_prefab_stage(saveBeforeClose: true).");
|
||||
}
|
||||
finally
|
||||
{
|
||||
StageUtility.GoToMainStage();
|
||||
SafeDeleteAsset(prefabPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertOpenPrefabStage(string prefabPath)
|
||||
{
|
||||
var openResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "open_prefab_stage",
|
||||
["prefabPath"] = prefabPath
|
||||
}));
|
||||
|
||||
Assert.IsTrue(openResult.Value<bool>("success"), $"Expected open to succeed but got: {openResult}");
|
||||
}
|
||||
|
||||
private static string CreateTestPrefab(string rootName)
|
||||
{
|
||||
string prefabPath = Path.Combine(TempDirectory, $"{rootName}.prefab").Replace('\\', '/');
|
||||
var root = new GameObject(rootName);
|
||||
|
||||
try
|
||||
{
|
||||
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(root, true);
|
||||
}
|
||||
|
||||
return prefabPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 421e522d607414e7ba1339af74dab899
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,861 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools.ProBuilder;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageProBuilderTests
|
||||
{
|
||||
private readonly List<GameObject> _createdObjects = new List<GameObject>();
|
||||
private bool _proBuilderInstalled;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void OneTimeSetUp()
|
||||
{
|
||||
_proBuilderInstalled = Type.GetType(
|
||||
"UnityEngine.ProBuilder.ProBuilderMesh, Unity.ProBuilder"
|
||||
) != null;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in _createdObjects)
|
||||
{
|
||||
if (go != null)
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
_createdObjects.Clear();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Basic action validation (works regardless of ProBuilder installation)
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_MissingAction_ReturnsError()
|
||||
{
|
||||
var paramsObj = new JObject();
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_UnknownAction_ReturnsError()
|
||||
{
|
||||
var paramsObj = new JObject { ["action"] = "nonexistent_action" };
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
var errorMsg = result["error"]?.ToString() ?? result["message"]?.ToString();
|
||||
Assert.That(errorMsg, Does.Contain("Unknown action").Or.Contain("not installed"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_Ping_ReturnsSuccess()
|
||||
{
|
||||
var paramsObj = new JObject { ["action"] = "ping" };
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
// Without ProBuilder, should return error about missing package
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
Assert.That(result["error"]?.ToString(),
|
||||
Does.Contain("ProBuilder").IgnoreCase);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Shape creation (requires ProBuilder)
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void CreateShape_MissingShapeType_ReturnsError()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateShape_InvalidShapeType_ReturnsError()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject { ["shapeType"] = "InvalidShape" },
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateShape_Cube_CreatesGameObject()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestCube",
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data, "Result should contain data");
|
||||
Assert.AreEqual("PBTestCube", data.Value<string>("gameObjectName"));
|
||||
Assert.Greater(data.Value<int>("faceCount"), 0);
|
||||
Assert.Greater(data.Value<int>("vertexCount"), 0);
|
||||
|
||||
// Track for cleanup
|
||||
var go = GameObject.Find("PBTestCube");
|
||||
if (go != null) _createdObjects.Add(go);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateShape_WithPosition_SetsTransform()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestCubePos",
|
||||
["position"] = new JArray(5f, 10f, 15f),
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestCubePos");
|
||||
Assert.IsNotNull(go, "Created GameObject should exist");
|
||||
Assert.AreEqual(new Vector3(5f, 10f, 15f), go.transform.position);
|
||||
|
||||
_createdObjects.Add(go);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreatePolyShape_CreatesFromPoints()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "create_poly_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["points"] = new JArray(
|
||||
new JArray(0f, 0f, 0f),
|
||||
new JArray(5f, 0f, 0f),
|
||||
new JArray(5f, 0f, 5f),
|
||||
new JArray(0f, 0f, 5f)
|
||||
),
|
||||
["extrudeHeight"] = 3f,
|
||||
["name"] = "PBTestPoly",
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestPoly");
|
||||
if (go != null) _createdObjects.Add(go);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Mesh editing (requires ProBuilder + created shape)
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void GetMeshInfo_ReturnsDetails()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// First create a shape
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestInfoCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestInfoCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
// Now get mesh info
|
||||
var infoParams = new JObject
|
||||
{
|
||||
["action"] = "get_mesh_info",
|
||||
["target"] = "PBTestInfoCube",
|
||||
["properties"] = new JObject { ["include"] = "faces" },
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
Assert.Greater(data.Value<int>("faceCount"), 0);
|
||||
Assert.Greater(data.Value<int>("vertexCount"), 0);
|
||||
Assert.IsNotNull(data["bounds"]);
|
||||
Assert.IsNotNull(data["faces"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExtrudeFaces_IncreaseFaceCount()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a cube
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestExtrudeCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestExtrudeCube");
|
||||
_createdObjects.Add(go);
|
||||
|
||||
int initialFaceCount = createResult["data"].Value<int>("faceCount");
|
||||
|
||||
// Extrude face 0
|
||||
var extrudeParams = new JObject
|
||||
{
|
||||
["action"] = "extrude_faces",
|
||||
["target"] = "PBTestExtrudeCube",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["faceIndices"] = new JArray(0),
|
||||
["distance"] = 1.0f,
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(extrudeParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
int newFaceCount = result["data"].Value<int>("faceCount");
|
||||
Assert.Greater(newFaceCount, initialFaceCount,
|
||||
"Face count should increase after extrusion");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeleteFaces_DecreasesFaceCount()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestDeleteCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestDeleteCube");
|
||||
_createdObjects.Add(go);
|
||||
|
||||
int initialFaceCount = createResult["data"].Value<int>("faceCount");
|
||||
|
||||
var deleteParams = new JObject
|
||||
{
|
||||
["action"] = "delete_faces",
|
||||
["target"] = "PBTestDeleteCube",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["faceIndices"] = new JArray(0),
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(deleteParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
int newFaceCount = result["data"].Value<int>("faceCount");
|
||||
Assert.Less(newFaceCount, initialFaceCount,
|
||||
"Face count should decrease after deletion");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetFaceMaterial_WithMissingTarget_ReturnsError()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "set_face_material",
|
||||
["target"] = "NonExistentObject999",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["faceIndices"] = new JArray(0),
|
||||
["materialPath"] = "Assets/Materials/Test.mat",
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FlipNormals_SucceedsOnValidMesh()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestFlipCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestFlipCube");
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var flipParams = new JObject
|
||||
{
|
||||
["action"] = "flip_normals",
|
||||
["target"] = "PBTestFlipCube",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["faceIndices"] = new JArray(0, 1),
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(flipParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Enhanced get_mesh_info with include parameter
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void GetMeshInfo_DefaultInclude_ReturnsSummaryOnly()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestSummaryCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestSummaryCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var infoParams = new JObject
|
||||
{
|
||||
["action"] = "get_mesh_info",
|
||||
["target"] = "PBTestSummaryCube",
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
Assert.Greater(data.Value<int>("faceCount"), 0);
|
||||
// Default "summary" should NOT include faces array
|
||||
Assert.IsNull(data["faces"], "Summary mode should not include faces array");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetMeshInfo_IncludeFaces_ReturnsFaceNormalsAndDirections()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestFacesCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestFacesCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var infoParams = new JObject
|
||||
{
|
||||
["action"] = "get_mesh_info",
|
||||
["target"] = "PBTestFacesCube",
|
||||
["properties"] = new JObject { ["include"] = "faces" },
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
var faces = data["faces"] as JArray;
|
||||
Assert.IsNotNull(faces, "Faces mode should include faces array");
|
||||
Assert.Greater(faces.Count, 0);
|
||||
|
||||
// Each face should have normal, center, direction
|
||||
var firstFace = faces[0] as JObject;
|
||||
Assert.IsNotNull(firstFace);
|
||||
Assert.IsNotNull(firstFace["normal"], "Face should have normal");
|
||||
Assert.IsNotNull(firstFace["center"], "Face should have center");
|
||||
// direction may be null for angled faces, but should exist as key
|
||||
Assert.IsTrue(firstFace.ContainsKey("direction"), "Face should have direction key");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetMeshInfo_IncludeEdges_ReturnsEdgeData()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestEdgesCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestEdgesCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var infoParams = new JObject
|
||||
{
|
||||
["action"] = "get_mesh_info",
|
||||
["target"] = "PBTestEdgesCube",
|
||||
["properties"] = new JObject { ["include"] = "edges" },
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
var edges = data["edges"] as JArray;
|
||||
Assert.IsNotNull(edges, "Edges mode should include edges array");
|
||||
Assert.Greater(edges.Count, 0);
|
||||
|
||||
var firstEdge = edges[0] as JObject;
|
||||
Assert.IsNotNull(firstEdge);
|
||||
Assert.IsTrue(firstEdge.ContainsKey("vertexA"), "Edge should have vertexA");
|
||||
Assert.IsTrue(firstEdge.ContainsKey("vertexB"), "Edge should have vertexB");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetMeshInfo_CubeTopFace_HasUpNormal()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestTopNormalCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestTopNormalCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var infoParams = new JObject
|
||||
{
|
||||
["action"] = "get_mesh_info",
|
||||
["target"] = "PBTestTopNormalCube",
|
||||
["properties"] = new JObject { ["include"] = "faces" },
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var faces = result["data"]["faces"] as JArray;
|
||||
Assert.IsNotNull(faces);
|
||||
|
||||
// At least one face should have direction "top"
|
||||
bool hasTop = false;
|
||||
foreach (JObject face in faces)
|
||||
{
|
||||
if (face["direction"]?.ToString() == "top")
|
||||
{
|
||||
hasTop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.IsTrue(hasTop, "A cube should have at least one face with direction 'top'");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Smoothing
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void AutoSmooth_DefaultAngle_AssignsGroups()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestAutoSmoothCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestAutoSmoothCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var smoothParams = new JObject
|
||||
{
|
||||
["action"] = "auto_smooth",
|
||||
["target"] = "PBTestAutoSmoothCube",
|
||||
["properties"] = new JObject { ["angleThreshold"] = 30 },
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(smoothParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetSmoothing_OnSpecificFaces_SetsGroup()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestSetSmoothCube",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestSetSmoothCube");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var smoothParams = new JObject
|
||||
{
|
||||
["action"] = "set_smoothing",
|
||||
["target"] = "PBTestSetSmoothCube",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["faceIndices"] = new JArray(0, 1),
|
||||
["smoothingGroup"] = 1,
|
||||
},
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(smoothParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
Assert.AreEqual(2, data.Value<int>("facesModified"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Mesh Utilities
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void CenterPivot_MovesPivotToCenter()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestCenterPivot",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestCenterPivot");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var pivotParams = new JObject
|
||||
{
|
||||
["action"] = "center_pivot",
|
||||
["target"] = "PBTestCenterPivot",
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(pivotParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FreezeTransform_ResetsTransformKeepsShape()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestFreeze",
|
||||
["position"] = new JArray(5f, 3f, 2f),
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestFreeze");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var freezeParams = new JObject
|
||||
{
|
||||
["action"] = "freeze_transform",
|
||||
["target"] = "PBTestFreeze",
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(freezeParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Transform should be reset to identity
|
||||
Assert.AreEqual(Vector3.zero, go.transform.position);
|
||||
Assert.AreEqual(Quaternion.identity, go.transform.rotation);
|
||||
Assert.AreEqual(Vector3.one, go.transform.localScale);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateMesh_CleanMesh_ReturnsNoIssues()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestValidate",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestValidate");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var validateParams = new JObject
|
||||
{
|
||||
["action"] = "validate_mesh",
|
||||
["target"] = "PBTestValidate",
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(validateParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
Assert.IsTrue(data.Value<bool>("healthy"), "Fresh cube should be healthy");
|
||||
Assert.AreEqual(0, data.Value<int>("degenerateTriangles"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RepairMesh_OnCleanMesh_ReportsNoChanges()
|
||||
{
|
||||
if (!_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder not installed - skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create_shape",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shapeType"] = "Cube",
|
||||
["name"] = "PBTestRepair",
|
||||
},
|
||||
};
|
||||
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
|
||||
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
|
||||
|
||||
var go = GameObject.Find("PBTestRepair");
|
||||
Assert.IsNotNull(go);
|
||||
_createdObjects.Add(go);
|
||||
|
||||
var repairParams = new JObject
|
||||
{
|
||||
["action"] = "repair_mesh",
|
||||
["target"] = "PBTestRepair",
|
||||
};
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(repairParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
Assert.AreEqual(0, data.Value<int>("degenerateTrianglesRemoved"));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ProBuilder not installed fallback
|
||||
// =====================================================================
|
||||
|
||||
[Test]
|
||||
public void AllActions_WithoutProBuilder_ReturnPackageError()
|
||||
{
|
||||
if (_proBuilderInstalled)
|
||||
{
|
||||
Assert.Pass("ProBuilder IS installed - this test verifies the not-installed path.");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] testActions = {
|
||||
"ping", "create_shape", "get_mesh_info", "extrude_faces",
|
||||
"auto_smooth", "set_smoothing", "center_pivot", "validate_mesh",
|
||||
};
|
||||
foreach (var action in testActions)
|
||||
{
|
||||
var paramsObj = new JObject { ["action"] = action };
|
||||
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
|
||||
Assert.IsFalse(result.Value<bool>("success"),
|
||||
$"Action '{action}' should fail without ProBuilder: {result}");
|
||||
Assert.That(result["error"]?.ToString(),
|
||||
Does.Contain("ProBuilder").IgnoreCase,
|
||||
$"Error for '{action}' should mention ProBuilder");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2a1efebc27c48258b6ce356fed59a35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
using NUnit.Framework;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageSceneHierarchyPagingTests
|
||||
{
|
||||
private GameObject _root;
|
||||
private readonly System.Collections.Generic.List<GameObject> _created = new System.Collections.Generic.List<GameObject>();
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
for (int i = 0; i < _created.Count; i++)
|
||||
{
|
||||
if (_created[i] != null) Object.DestroyImmediate(_created[i]);
|
||||
}
|
||||
_created.Clear();
|
||||
|
||||
if (_root != null)
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
_root = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetHierarchy_PaginatesRoots_AndSupportsChildrenPaging()
|
||||
{
|
||||
// Arrange: create many roots so paging must occur.
|
||||
// Note: Keep counts modest to avoid slowing EditMode tests.
|
||||
const int rootCount = 40;
|
||||
for (int i = 0; i < rootCount; i++)
|
||||
{
|
||||
_created.Add(new GameObject($"HS_Root_{i:D2}"));
|
||||
}
|
||||
|
||||
_root = new GameObject("HS_Parent");
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var child = new GameObject($"HS_Child_{i:D2}");
|
||||
child.transform.SetParent(_root.transform);
|
||||
}
|
||||
|
||||
// Act: request a small page to force truncation
|
||||
var p1 = new JObject
|
||||
{
|
||||
["action"] = "get_hierarchy",
|
||||
["pageSize"] = 10,
|
||||
};
|
||||
var raw1 = ManageScene.HandleCommand(p1);
|
||||
var res1 = raw1 as JObject ?? JObject.FromObject(raw1);
|
||||
|
||||
// Assert: envelope success + payload shape
|
||||
Assert.IsTrue(res1.Value<bool>("success"), res1.ToString());
|
||||
var data1 = res1["data"] as JObject;
|
||||
Assert.IsNotNull(data1, "Expected data payload to be an object.");
|
||||
Assert.AreEqual("roots", data1.Value<string>("scope"));
|
||||
Assert.AreEqual(true, data1.Value<bool>("truncated"), "Expected truncation when pageSize < root count.");
|
||||
Assert.IsNotNull(data1["next_cursor"], "Expected next_cursor when truncated.");
|
||||
|
||||
var items1 = data1["items"] as JArray;
|
||||
Assert.IsNotNull(items1, "Expected items array.");
|
||||
Assert.AreEqual(10, items1.Count, "Expected exactly pageSize items returned.");
|
||||
|
||||
// Act: fetch next page of roots using next_cursor
|
||||
var cursor = data1.Value<string>("next_cursor");
|
||||
var p2 = new JObject
|
||||
{
|
||||
["action"] = "get_hierarchy",
|
||||
["pageSize"] = 10,
|
||||
["cursor"] = cursor,
|
||||
};
|
||||
var raw2 = ManageScene.HandleCommand(p2);
|
||||
var res2 = raw2 as JObject ?? JObject.FromObject(raw2);
|
||||
Assert.IsTrue(res2.Value<bool>("success"), res2.ToString());
|
||||
var data2 = res2["data"] as JObject;
|
||||
Assert.IsNotNull(data2);
|
||||
var items2 = data2["items"] as JArray;
|
||||
Assert.IsNotNull(items2);
|
||||
Assert.AreEqual(10, items2.Count);
|
||||
|
||||
// Act: page children of a specific parent via 'parent' param (instance ID)
|
||||
var pChildren = new JObject
|
||||
{
|
||||
["action"] = "get_hierarchy",
|
||||
["parent"] = _root.GetInstanceID(),
|
||||
["pageSize"] = 7,
|
||||
};
|
||||
var rawChildren = ManageScene.HandleCommand(pChildren);
|
||||
var resChildren = rawChildren as JObject ?? JObject.FromObject(rawChildren);
|
||||
Assert.IsTrue(resChildren.Value<bool>("success"), resChildren.ToString());
|
||||
var dataChildren = resChildren["data"] as JObject;
|
||||
Assert.IsNotNull(dataChildren);
|
||||
Assert.AreEqual("children", dataChildren.Value<string>("scope"));
|
||||
Assert.AreEqual(true, dataChildren.Value<bool>("truncated"));
|
||||
Assert.IsNotNull(dataChildren["next_cursor"]);
|
||||
var childItems = dataChildren["items"] as JArray;
|
||||
Assert.IsNotNull(childItems);
|
||||
Assert.AreEqual(7, childItems.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Screenshot_SceneViewRejectsSupersizeAboveOne()
|
||||
{
|
||||
var raw = ManageScene.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "screenshot",
|
||||
["captureSource"] = "scene_view",
|
||||
["superSize"] = 2,
|
||||
});
|
||||
var response = raw as JObject ?? JObject.FromObject(raw);
|
||||
|
||||
Assert.IsFalse(response.Value<bool>("success"), response.ToString());
|
||||
StringAssert.Contains("does not support super_size above 1", response.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EditorWindowScreenshotUtility_SanitizesFileName()
|
||||
{
|
||||
var helperType = typeof(ManageScene).Assembly.GetType("MCPForUnity.Editor.Helpers.EditorWindowScreenshotUtility");
|
||||
Assert.IsNotNull(helperType, "Expected EditorWindowScreenshotUtility type.");
|
||||
|
||||
var sanitizeMethod = helperType.GetMethod("SanitizeFileName", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(sanitizeMethod, "Expected SanitizeFileName helper.");
|
||||
|
||||
string sanitized = (string)sanitizeMethod.Invoke(null, new object[] { "../evil/path/shot" });
|
||||
Assert.AreEqual("shot", sanitized);
|
||||
Assert.IsFalse(sanitized.Contains("/"));
|
||||
Assert.IsFalse(sanitized.Contains("\\"));
|
||||
Assert.IsFalse(sanitized.Contains(".."));
|
||||
|
||||
string[] reservedInputs = { "CON", "NUL", "PRN", "AUX", "../CON.txt", "folder/COM1.log", "nested\\LPT9", "CON ", "NUL." };
|
||||
foreach (string input in reservedInputs)
|
||||
{
|
||||
sanitized = (string)sanitizeMethod.Invoke(null, new object[] { input });
|
||||
string sanitizedStem = System.IO.Path.GetFileNameWithoutExtension(sanitized);
|
||||
Assert.IsFalse(
|
||||
string.Equals(sanitizedStem, "CON", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(sanitizedStem, "NUL", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(sanitizedStem, "PRN", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(sanitizedStem, "AUX", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(sanitizedStem, "COM1", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(sanitizedStem, "LPT9", System.StringComparison.OrdinalIgnoreCase),
|
||||
$"Expected reserved device name to be sanitized for input '{input}', got '{sanitized}'.");
|
||||
Assert.IsFalse(sanitized.Contains("/"));
|
||||
Assert.IsFalse(sanitized.Contains("\\"));
|
||||
Assert.IsFalse(sanitized.Contains(".."));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EditorWindowScreenshotUtility_ClampsSceneViewSupersizeToOne()
|
||||
{
|
||||
var helperType = typeof(ManageScene).Assembly.GetType("MCPForUnity.Editor.Helpers.EditorWindowScreenshotUtility");
|
||||
Assert.IsNotNull(helperType, "Expected EditorWindowScreenshotUtility type.");
|
||||
|
||||
var normalizeMethod = helperType.GetMethod("NormalizeSceneViewSuperSize", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(normalizeMethod, "Expected NormalizeSceneViewSuperSize helper.");
|
||||
|
||||
int normalized = (int)normalizeMethod.Invoke(null, new object[] { 4 });
|
||||
Assert.AreEqual(1, normalized);
|
||||
|
||||
normalized = (int)normalizeMethod.Invoke(null, new object[] { 0 });
|
||||
Assert.AreEqual(1, normalized);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Screenshot_ViewTargetAcceptedForGameView()
|
||||
{
|
||||
// view_target should be accepted for game_view (positioned capture path).
|
||||
// It will fail to resolve a non-existent GO, but should NOT reject the parameter itself.
|
||||
var raw = ManageScene.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "screenshot",
|
||||
["viewTarget"] = "NonExistentObject",
|
||||
});
|
||||
var response = raw as JObject ?? JObject.FromObject(raw);
|
||||
|
||||
// Should attempt positioned capture and fail to resolve the GO — not reject the param
|
||||
Assert.IsFalse(response.Value<bool>("success"), response.ToString());
|
||||
StringAssert.Contains("not found", response.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CalculateFrameBounds_UsesCollider2D()
|
||||
{
|
||||
var helperType = typeof(ManageScene).GetMethod("CalculateFrameBounds", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(helperType, "Expected CalculateFrameBounds helper.");
|
||||
|
||||
var root = new GameObject("HS_2D");
|
||||
_created.Add(root);
|
||||
var collider = root.AddComponent<BoxCollider2D>();
|
||||
collider.size = new Vector2(4f, 2f);
|
||||
collider.offset = new Vector2(1f, -1f);
|
||||
|
||||
Bounds bounds = (Bounds)helperType.Invoke(null, new object[] { root });
|
||||
Assert.Greater(bounds.size.x, 0.1f);
|
||||
Assert.Greater(bounds.size.y, 0.1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95a76a6de453c48abaee108f9029cb65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MCPForUnity.Tests.EditMode.Tools
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManageSceneMultiSceneTests
|
||||
{
|
||||
[Test]
|
||||
public void GetLoadedScenes_ReturnsAtLeastOne()
|
||||
{
|
||||
var p = new JObject { ["action"] = "get_loaded_scenes" };
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
|
||||
var scenes = r["data"]?["scenes"] as JArray;
|
||||
Assert.IsNotNull(scenes);
|
||||
Assert.GreaterOrEqual(scenes.Count, 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CloseScene_LastScene_ReturnsError()
|
||||
{
|
||||
if (SceneManager.sceneCount > 1)
|
||||
{
|
||||
Assert.Ignore("Test requires a single scene; editor has additive scenes open.");
|
||||
return;
|
||||
}
|
||||
var active = SceneManager.GetActiveScene();
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "close_scene",
|
||||
["sceneName"] = active.name
|
||||
};
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsFalse(r.Value<bool>("success"), "Should fail to close last scene");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MoveToScene_MissingTarget_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "move_to_scene",
|
||||
["sceneName"] = "SomeScene"
|
||||
};
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsFalse(r.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MoveToScene_NonExistentGO_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "move_to_scene",
|
||||
["target"] = "NonExistentGO_99999",
|
||||
["sceneName"] = "SomeScene"
|
||||
};
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsFalse(r.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModifyBuildSettings_RedirectsToManageBuild()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "modify_build_settings",
|
||||
["scenePath"] = "Assets/Scenes/Test.unity",
|
||||
["operation"] = "add"
|
||||
};
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsFalse(r.Value<bool>("success"));
|
||||
Assert.IsTrue(r.Value<string>("error").Contains("manage_build"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetActiveScene_NotFound_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "set_active_scene",
|
||||
["sceneName"] = "NonExistentScene_99999"
|
||||
};
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsFalse(r.Value<bool>("success"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71e2a65280e7c4c7da84b774b639f70a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnity.Tests.EditMode.Tools
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManageSceneTemplateTests
|
||||
{
|
||||
[Test]
|
||||
public void Create_UnknownTemplate_ReturnsError()
|
||||
{
|
||||
var p = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "TemplateTest",
|
||||
["path"] = "Assets/Scenes",
|
||||
["template"] = "nonexistent_template"
|
||||
};
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsFalse(r.Value<bool>("success"), "Unknown template should fail");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 575ba801f927b4ad2933d8eb86c8571b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnity.Tests.EditMode.Tools
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManageSceneValidationTests
|
||||
{
|
||||
[Test]
|
||||
public void Validate_CleanScene_ReturnsNoIssues()
|
||||
{
|
||||
var p = new JObject { ["action"] = "validate" };
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
|
||||
var data = r["data"];
|
||||
Assert.IsNotNull(data);
|
||||
Assert.AreEqual(0, data.Value<int>("totalIssues"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validate_WithAutoRepair_CleanScene_RepairsNothing()
|
||||
{
|
||||
var p = new JObject { ["action"] = "validate", ["autoRepair"] = true };
|
||||
var result = ManageScene.HandleCommand(p);
|
||||
var r = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
|
||||
Assert.AreEqual(0, r["data"].Value<int>("repaired"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 929d6112822864391ae2dd7cf289049c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for ManageScript delimiter-checking and token-finding logic,
|
||||
/// specifically covering C# string variants that the old lexer missed:
|
||||
/// verbatim strings, interpolated strings, raw string literals.
|
||||
/// </summary>
|
||||
public class ManageScriptDelimiterTests
|
||||
{
|
||||
// ── CheckBalancedDelimiters ──────────────────────────────────────
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_VerbatimString_WithBackslashes()
|
||||
{
|
||||
// @"C:\Users\file" — backslashes are NOT escape chars in verbatim strings
|
||||
string code = "class C { string s = @\"C:\\Users\\file\"; }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Verbatim string with backslashes should not break delimiter balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_VerbatimString_DoubledQuotes()
|
||||
{
|
||||
// @"He said ""hello""" — doubled quotes are the escape in verbatim strings
|
||||
string code = "class C { string s = @\"He said \"\"hello\"\"\"; }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Verbatim string with doubled quotes should not break delimiter balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_InterpolatedString_WithBraces()
|
||||
{
|
||||
// $"Value: {x}" — the { } are interpolation holes, not real braces
|
||||
string code = "class C { void M() { int x = 1; string s = $\"Value: {x}\"; } }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Interpolated string braces should not be counted as delimiters");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_InterpolatedVerbatim_Combined()
|
||||
{
|
||||
// $@"Path: {dir}\file" — interpolated + verbatim combined
|
||||
string code = "class C { void M() { string dir = \"d\"; string s = $@\"Path: {dir}\\file\"; } }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Interpolated verbatim string should not break delimiter balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_NestedInterpolation()
|
||||
{
|
||||
// $"Outer {$"Inner {x}"}" — nested interpolated strings
|
||||
string code = "class C { void M() { int x = 1; string s = $\"Outer {$\"Inner {x}\"}\"; } }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Nested interpolated strings should not break delimiter balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_RawStringLiteral()
|
||||
{
|
||||
// C# 11 raw string literal: """{ }"""
|
||||
string code = "class C { string s = \"\"\"\n{ }\n\"\"\"; }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Raw string literal braces should not be counted as delimiters");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_MultilineVerbatimString()
|
||||
{
|
||||
// Verbatim string spanning multiple lines with braces
|
||||
string code = "class C { string s = @\"line1\n{ }\"; }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Multiline verbatim string with braces should not break balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_InterpolatedEscapedBraces()
|
||||
{
|
||||
// $"literal {{braces}}" — escaped braces in interpolated string
|
||||
string code = "class C { string s = $\"literal {{braces}}\"; }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Escaped braces in interpolated strings should not break balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_InterpolatedRawString()
|
||||
{
|
||||
// $"""...{expr}...""" — interpolated raw string literal (C# 11)
|
||||
string code = "class C { void M() { int x = 1; string s = $\"\"\"\n Hello {x}\n \"\"\"; } }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Interpolated raw string should not break delimiter balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_MultiDollarRawString()
|
||||
{
|
||||
// $$"""...{{expr}}...""" — multi-dollar interpolated raw string
|
||||
string code = "class C { void M() { int x = 1; string s = $$\"\"\"\n {literal} {{x}}\n \"\"\"; } }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Multi-dollar raw string should not break delimiter balance");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_BracesInComments_Ignored()
|
||||
{
|
||||
string code = "class C {\n// {\n/* { */\nvoid M() { }\n}";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Braces in comments should be ignored");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_BracesInRegularStrings_Ignored()
|
||||
{
|
||||
string code = "class C { string s = \"{ }\"; }";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Braces in regular strings should be ignored");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_ActuallyUnbalanced_ReturnsFalse()
|
||||
{
|
||||
string code = "class C { void M() { }";
|
||||
Assert.IsFalse(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Actually unbalanced code should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_ExtraClosingBrace_ReturnsFalse()
|
||||
{
|
||||
string code = "class C { } }";
|
||||
Assert.IsFalse(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Extra closing brace should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_RealWorldUnityScript()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
|
||||
public class PlayerHUD : MonoBehaviour
|
||||
{
|
||||
private int score;
|
||||
private string playerName;
|
||||
|
||||
void Start()
|
||||
{
|
||||
score = 0;
|
||||
playerName = ""Player"";
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
string label = $""Score: {score}"";
|
||||
string path = @""C:\Games\SaveData"";
|
||||
string msg = $@""Player {playerName} at path {path}"";
|
||||
Debug.Log($""HUD initialized for {playerName} with score {score}"");
|
||||
Debug.Log(""Literal {{braces}}"");
|
||||
}
|
||||
}";
|
||||
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
|
||||
"Real-world Unity script with interpolated/verbatim strings should pass");
|
||||
}
|
||||
|
||||
// ── IndexOfClassToken ────────────────────────────────────────────
|
||||
|
||||
[Test]
|
||||
public void IndexOfClassToken_FindsClass_NormalCode()
|
||||
{
|
||||
string code = "public class Foo { }";
|
||||
int idx = CallIndexOfClassToken(code, "Foo");
|
||||
Assert.GreaterOrEqual(idx, 0, "Should find class Foo in normal code");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IndexOfClassToken_SkipsClassInComment()
|
||||
{
|
||||
string code = "// class Foo\npublic class Real { }";
|
||||
int idx = CallIndexOfClassToken(code, "Foo");
|
||||
Assert.AreEqual(-1, idx, "Should not find 'class Foo' inside a comment");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IndexOfClassToken_SkipsClassInString()
|
||||
{
|
||||
string code = "class Real { string s = \"class Foo { }\"; }";
|
||||
int idx = CallIndexOfClassToken(code, "Foo");
|
||||
Assert.AreEqual(-1, idx, "Should not find 'class Foo' inside a string literal");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IndexOfClassToken_FindsSecondClass_WhenFirstInComment()
|
||||
{
|
||||
string code = "// class Fake\npublic class Real { }";
|
||||
int idx = CallIndexOfClassToken(code, "Real");
|
||||
Assert.GreaterOrEqual(idx, 0, "Should find class Real even when a commented class precedes it");
|
||||
}
|
||||
|
||||
// ── Reflection helpers ───────────────────────────────────────────
|
||||
|
||||
private static bool CallCheckBalancedDelimiters(string text, out int line, out char expected)
|
||||
{
|
||||
line = 0;
|
||||
expected = '\0';
|
||||
|
||||
var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(method, "CheckBalancedDelimiters method should exist");
|
||||
|
||||
var parameters = new object[] { text, 0, '\0' };
|
||||
var result = (bool)method.Invoke(null, parameters);
|
||||
line = (int)parameters[1];
|
||||
expected = (char)parameters[2];
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int CallIndexOfClassToken(string source, string className)
|
||||
{
|
||||
var method = typeof(ManageScript).GetMethod("IndexOfClassToken",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.IsNotNull(method, "IndexOfClassToken method should exist");
|
||||
|
||||
return (int)method.Invoke(null, new object[] { source, className });
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cbe58d767a3d4ddf995332459d66830
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// In-memory tests for ManageScript validation logic.
|
||||
/// These tests focus on the validation methods directly without creating files.
|
||||
/// </summary>
|
||||
public class ManageScriptValidationTests
|
||||
{
|
||||
[Test]
|
||||
public void HandleCommand_NullParams_ReturnsError()
|
||||
{
|
||||
var result = ManageScript.HandleCommand(null);
|
||||
Assert.IsNotNull(result, "Should handle null parameters gracefully");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_InvalidAction_ReturnsError()
|
||||
{
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "invalid_action",
|
||||
["name"] = "TestScript",
|
||||
["path"] = "Assets/Scripts"
|
||||
};
|
||||
|
||||
var result = ManageScript.HandleCommand(paramsObj);
|
||||
Assert.IsNotNull(result, "Should return error result for invalid action");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_ValidCode_ReturnsTrue()
|
||||
{
|
||||
string validCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n }\n}";
|
||||
|
||||
bool result = CallCheckBalancedDelimiters(validCode, out int line, out char expected);
|
||||
Assert.IsTrue(result, "Valid C# code should pass balance check");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_UnbalancedBraces_ReturnsFalse()
|
||||
{
|
||||
string unbalancedCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n // Missing closing brace";
|
||||
|
||||
bool result = CallCheckBalancedDelimiters(unbalancedCode, out int line, out char expected);
|
||||
Assert.IsFalse(result, "Unbalanced code should fail balance check");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CheckBalancedDelimiters_StringWithBraces_ReturnsTrue()
|
||||
{
|
||||
string codeWithStringBraces = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n public string json = \"{key: value}\";\n void Start() { Debug.Log(json); }\n}";
|
||||
|
||||
bool result = CallCheckBalancedDelimiters(codeWithStringBraces, out int line, out char expected);
|
||||
Assert.IsTrue(result, "Code with braces in strings should pass balance check");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TicTacToe3D_ValidationScenario_DoesNotCrash()
|
||||
{
|
||||
// Test the scenario that was causing issues without file I/O
|
||||
string ticTacToeCode = "using UnityEngine;\n\npublic class TicTacToe3D : MonoBehaviour\n{\n public string gameState = \"active\";\n void Start() { Debug.Log(\"Game started\"); }\n public void MakeMove(int position) { if (gameState == \"active\") Debug.Log($\"Move {position}\"); }\n}";
|
||||
|
||||
// Test that the validation methods don't crash on this code
|
||||
bool balanceResult = CallCheckBalancedDelimiters(ticTacToeCode, out int line, out char expected);
|
||||
|
||||
Assert.IsTrue(balanceResult, "TicTacToe3D code should pass balance validation");
|
||||
}
|
||||
|
||||
// Helper methods to access private ManageScript methods via reflection
|
||||
private bool CallCheckBalancedDelimiters(string contents, out int line, out char expected)
|
||||
{
|
||||
line = 0;
|
||||
expected = ' ';
|
||||
|
||||
try
|
||||
{
|
||||
var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
if (method != null)
|
||||
{
|
||||
var parameters = new object[] { contents, line, expected };
|
||||
var result = (bool)method.Invoke(null, parameters);
|
||||
line = (int)parameters[1];
|
||||
expected = (char)parameters[2];
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not test CheckBalancedDelimiters directly: {ex.Message}");
|
||||
}
|
||||
|
||||
// Fallback: basic structural check
|
||||
return BasicBalanceCheck(contents);
|
||||
}
|
||||
|
||||
private bool BasicBalanceCheck(string contents)
|
||||
{
|
||||
// Simple fallback balance check
|
||||
int braceCount = 0;
|
||||
bool inString = false;
|
||||
bool escaped = false;
|
||||
|
||||
for (int i = 0; i < contents.Length; i++)
|
||||
{
|
||||
char c = contents[i];
|
||||
|
||||
if (escaped)
|
||||
{
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString)
|
||||
{
|
||||
if (c == '\\') escaped = true;
|
||||
else if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"') inString = true;
|
||||
else if (c == '{') braceCount++;
|
||||
else if (c == '}') braceCount--;
|
||||
|
||||
if (braceCount < 0) return false;
|
||||
}
|
||||
|
||||
return braceCount == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls ValidateScriptSyntax via reflection and returns the error list.
|
||||
/// This exercises CheckDuplicateMethodSignatures (called from ValidateScriptSyntax).
|
||||
/// </summary>
|
||||
private List<string> CallValidateScriptSyntaxUnity(string contents)
|
||||
{
|
||||
var validationLevelType = typeof(ManageScript).GetNestedType("ValidationLevel",
|
||||
BindingFlags.NonPublic);
|
||||
Assert.IsNotNull(validationLevelType, "ValidationLevel enum must exist");
|
||||
var basicLevel = Enum.ToObject(validationLevelType, 0); // ValidationLevel.Basic
|
||||
|
||||
var method = typeof(ManageScript).GetMethod("ValidateScriptSyntax",
|
||||
BindingFlags.NonPublic | BindingFlags.Static, null,
|
||||
new[] { typeof(string), validationLevelType, typeof(string[]).MakeByRefType() }, null);
|
||||
Assert.IsNotNull(method, "ValidateScriptSyntax method must exist");
|
||||
|
||||
var args = new object[] { contents, basicLevel, null };
|
||||
method.Invoke(null, args);
|
||||
var errArray = (string[])args[2];
|
||||
return errArray != null ? errArray.ToList() : new List<string>();
|
||||
}
|
||||
|
||||
private bool HasDuplicateMethodError(List<string> errors)
|
||||
{
|
||||
return errors.Any(e => e.Contains("Duplicate method signature detected"));
|
||||
}
|
||||
|
||||
// --- Duplicate method detection: false positive tests ---
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_LineCommentedMethod_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void DoStuff(int x) { }
|
||||
// public void DoStuff(int x) { }
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"A method in a line comment should not be flagged as duplicate");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_BlockCommentedMethod_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void DoStuff(int x) { }
|
||||
/* public void DoStuff(int x) { } */
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"A method in a block comment should not be flagged as duplicate");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_InnerClassSameMethod_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Outer : MonoBehaviour
|
||||
{
|
||||
public void Init(int x) { }
|
||||
|
||||
private class Inner
|
||||
{
|
||||
public void Init(int x) { }
|
||||
}
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"Same method name in outer and inner class should not be flagged");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_DifferentTypeOverloads_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void Process(int x) { }
|
||||
public void Process(string x) { }
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"Overloads with different param types but same count should not be flagged");
|
||||
}
|
||||
|
||||
// --- Duplicate method detection: true positive tests ---
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_ExpressionBodiedDuplicate_Flagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public int GetValue(int x) => x * 2;
|
||||
public int GetValue(int x) => x * 3;
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsTrue(HasDuplicateMethodError(errors),
|
||||
"Expression-bodied duplicate methods should be flagged");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_ExactDuplicate_Flagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void DoStuff(int x) { }
|
||||
public void DoStuff(int x) { }
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsTrue(HasDuplicateMethodError(errors),
|
||||
"Exact duplicate methods should be flagged");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_SameTypeDifferentParamName_Flagged()
|
||||
{
|
||||
// This is the real anchor_replace corruption pattern
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void Initialize(string name) { }
|
||||
public void Initialize(string label) { }
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsTrue(HasDuplicateMethodError(errors),
|
||||
"Same-type different-name duplicates (corruption pattern) should be flagged");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_GenericParamDuplicate_Flagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void Process(Dictionary<string, int> data) { }
|
||||
public void Process(Dictionary<string, int> other) { }
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsTrue(HasDuplicateMethodError(errors),
|
||||
"Generic param duplicates with different names should be flagged");
|
||||
}
|
||||
|
||||
// --- Keyword false positive tests ---
|
||||
|
||||
[Test]
|
||||
public void DuplicateDetection_CSharpKeywords_NotMatchedAsMethods()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Foo : MonoBehaviour
|
||||
{
|
||||
public void Update()
|
||||
{
|
||||
if (true) { }
|
||||
if (true) { }
|
||||
for (int i = 0; i < 10; i++) { }
|
||||
for (int j = 0; j < 5; j++) { }
|
||||
while (true) { break; }
|
||||
while (false) { break; }
|
||||
foreach (var x in new int[0]) { }
|
||||
foreach (var y in new int[0]) { }
|
||||
switch (0) { default: break; }
|
||||
switch (1) { default: break; }
|
||||
lock (this) { }
|
||||
lock (this) { }
|
||||
using (var d = new System.IO.MemoryStream()) { }
|
||||
using (var e = new System.IO.MemoryStream()) { }
|
||||
typeof(int);
|
||||
typeof(string);
|
||||
}
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"C# keywords (if, for, while, etc.) should not be matched as duplicate methods");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateMethodCheck_ConstructorInvocations_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Test : MonoBehaviour
|
||||
{
|
||||
void Start()
|
||||
{
|
||||
GameObject a = new GameObject(""A"");
|
||||
GameObject b = new GameObject(""B"");
|
||||
}
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"Constructor invocations (new Type(...)) should not be flagged as duplicate methods");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateMethodCheck_MultipleDistinctConstructors_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Test : MonoBehaviour
|
||||
{
|
||||
void Start()
|
||||
{
|
||||
var mpb1 = new MaterialPropertyBlock();
|
||||
var mpb2 = new MaterialPropertyBlock();
|
||||
var go1 = new GameObject(""A"");
|
||||
var go2 = new GameObject(""B"");
|
||||
}
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"Multiple constructor invocations of different types should not be flagged");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateMethodCheck_NewModifierWithConstructors_CorrectBehavior()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Base : MonoBehaviour
|
||||
{
|
||||
public virtual void Init() { }
|
||||
}
|
||||
public class Derived : Base
|
||||
{
|
||||
public new void Init() { }
|
||||
void Start()
|
||||
{
|
||||
var a = new GameObject(""A"");
|
||||
var b = new GameObject(""B"");
|
||||
}
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"new modifier on method should not interfere with constructor invocation filtering");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateMethodCheck_LocalFunctionsInDifferentMethods_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Test : MonoBehaviour
|
||||
{
|
||||
void Start()
|
||||
{
|
||||
int ClampScore(int value) { return Mathf.Clamp(value, 0, 100); }
|
||||
Debug.Log(ClampScore(10));
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
int ClampScore(int value) { return Mathf.Clamp(value, 0, 100); }
|
||||
Debug.Log(ClampScore(0));
|
||||
}
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"Same-signature local functions in different methods are valid C# and should not be flagged as duplicate class methods");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateMethodCheck_RepeatedMethodInvocations_NotFlagged()
|
||||
{
|
||||
string code = @"using UnityEngine;
|
||||
public class Test : MonoBehaviour
|
||||
{
|
||||
int First()
|
||||
{
|
||||
return Compute();
|
||||
}
|
||||
|
||||
int Second()
|
||||
{
|
||||
return Compute();
|
||||
}
|
||||
|
||||
int Compute() { return 1; }
|
||||
}";
|
||||
var errors = CallValidateScriptSyntaxUnity(code);
|
||||
Assert.IsFalse(HasDuplicateMethodError(errors),
|
||||
"Repeated method invocations inside method bodies should not be treated as duplicate method declarations");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_PathWithCsExtension_StripsFilename()
|
||||
{
|
||||
// When path ends with .cs (full file path instead of directory),
|
||||
// HandleCommand should strip the filename to avoid doubled paths
|
||||
// like "Assets/Scripts/Foo.cs/Foo.cs".
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "read",
|
||||
["name"] = "TestScript",
|
||||
["path"] = "Assets/Scripts/TestScript.cs"
|
||||
};
|
||||
|
||||
var result = ManageScript.HandleCommand(paramsObj);
|
||||
// The script won't exist, but the error path should NOT contain doubled filename
|
||||
string json = Newtonsoft.Json.JsonConvert.SerializeObject(result);
|
||||
Assert.IsFalse(json.Contains("TestScript.cs/TestScript.cs"),
|
||||
"Path ending in .cs should be treated as directory, not produce doubled filename");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8f7e3d1c4a2b5f8e9d6c3a7b1e4f7d2
|
||||
+1389
File diff suppressed because it is too large
Load Diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b53f7e50a801437e83e646cf00effed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnityTests.Editor.Tools.Fixtures;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageScriptableObjectTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageScriptableObjectTests";
|
||||
private const double UnityReadyTimeoutSeconds = 180.0;
|
||||
|
||||
private string _runRoot;
|
||||
private string _nestedFolder;
|
||||
private string _createdAssetPath;
|
||||
private string _createdGuid;
|
||||
private string _matAPath;
|
||||
private string _matBPath;
|
||||
|
||||
[UnitySetUp]
|
||||
public IEnumerator SetUp()
|
||||
{
|
||||
yield return WaitForUnityReady(UnityReadyTimeoutSeconds);
|
||||
EnsureFolder("Assets/Temp");
|
||||
// Avoid deleting/recreating the entire TempRoot each test (can trigger heavy reimport churn).
|
||||
// Instead, isolate each test in its own unique subfolder under TempRoot.
|
||||
EnsureFolder(TempRoot);
|
||||
_runRoot = $"{TempRoot}/Run_{Guid.NewGuid():N}";
|
||||
EnsureFolder(_runRoot);
|
||||
_nestedFolder = _runRoot + "/Nested/Deeper";
|
||||
|
||||
_createdAssetPath = null;
|
||||
_createdGuid = null;
|
||||
|
||||
// Create two Materials we can reference by guid/path.
|
||||
_matAPath = $"{TempRoot}/MatA_{Guid.NewGuid():N}.mat";
|
||||
_matBPath = $"{TempRoot}/MatB_{Guid.NewGuid():N}.mat";
|
||||
var shader = FindFallbackShader();
|
||||
Assert.IsNotNull(shader, "A fallback shader must be available for creating Material assets in tests.");
|
||||
AssetDatabase.CreateAsset(new Material(shader), _matAPath);
|
||||
AssetDatabase.CreateAsset(new Material(shader), _matBPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
yield return WaitForUnityReady(UnityReadyTimeoutSeconds);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Best-effort cleanup
|
||||
if (!string.IsNullOrEmpty(_createdAssetPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_createdAssetPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_createdAssetPath);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(_matAPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matAPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_matAPath);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(_matBPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matBPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_matBPath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_runRoot) && AssetDatabase.IsValidFolder(_runRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_runRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_CreatesNestedFolders_PlacesAssetCorrectly()
|
||||
{
|
||||
var create = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
["folderPath"] = _nestedFolder,
|
||||
["assetName"] = "My_Test_Def_Placement",
|
||||
["overwrite"] = true,
|
||||
};
|
||||
|
||||
var raw = ManageScriptableObject.HandleCommand(create);
|
||||
var result = raw as JObject ?? JObject.FromObject(raw);
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data, "Expected data payload");
|
||||
|
||||
_createdGuid = data!["guid"]?.ToString();
|
||||
_createdAssetPath = data["path"]?.ToString();
|
||||
|
||||
Assert.IsTrue(AssetDatabase.IsValidFolder(_nestedFolder), "Nested folder should be created.");
|
||||
Assert.IsTrue(_createdAssetPath!.StartsWith(_nestedFolder, StringComparison.Ordinal), $"Asset should be created under {_nestedFolder}: {_createdAssetPath}");
|
||||
Assert.IsTrue(_createdAssetPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase), "Asset should have .asset extension.");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response.");
|
||||
|
||||
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
|
||||
Assert.IsNotNull(asset, "Created asset should load as TestDefinition.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_AppliesPatches_ToCreatedAsset()
|
||||
{
|
||||
var create = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
// Patching correctness does not depend on nested folder creation; keep this lightweight.
|
||||
["folderPath"] = _runRoot,
|
||||
["assetName"] = "My_Test_Def_Patches",
|
||||
["overwrite"] = true,
|
||||
["patches"] = new JArray
|
||||
{
|
||||
new JObject { ["propertyPath"] = "displayName", ["op"] = "set", ["value"] = "Hello" },
|
||||
new JObject { ["propertyPath"] = "baseNumber", ["op"] = "set", ["value"] = 42 },
|
||||
new JObject { ["propertyPath"] = "nested.note", ["op"] = "set", ["value"] = "note!" }
|
||||
}
|
||||
};
|
||||
|
||||
var raw = ManageScriptableObject.HandleCommand(create);
|
||||
var result = raw as JObject ?? JObject.FromObject(raw);
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data, "Expected data payload");
|
||||
|
||||
_createdGuid = data!["guid"]?.ToString();
|
||||
_createdAssetPath = data["path"]?.ToString();
|
||||
|
||||
Assert.IsTrue(_createdAssetPath!.StartsWith(_runRoot, StringComparison.Ordinal), $"Asset should be created under {_runRoot}: {_createdAssetPath}");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response.");
|
||||
|
||||
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
|
||||
Assert.IsNotNull(asset, "Created asset should load as TestDefinition.");
|
||||
Assert.AreEqual("Hello", asset!.DisplayName, "Private [SerializeField] string should be set via SerializedProperty.");
|
||||
Assert.AreEqual(42, asset.BaseNumber, "Inherited serialized field should be set via SerializedProperty.");
|
||||
Assert.AreEqual("note!", asset.NestedNote, "Nested struct field should be set via SerializedProperty path.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Modify_ArrayResize_ThenAssignObjectRefs_ByGuidAndByPath()
|
||||
{
|
||||
// Create base asset first with no patches.
|
||||
var create = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
["folderPath"] = _runRoot,
|
||||
["assetName"] = "Modify_Target",
|
||||
["overwrite"] = true
|
||||
};
|
||||
var createRes = ToJObject(ManageScriptableObject.HandleCommand(create));
|
||||
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
|
||||
_createdGuid = createRes["data"]?["guid"]?.ToString();
|
||||
_createdAssetPath = createRes["data"]?["path"]?.ToString();
|
||||
|
||||
var matAGuid = AssetDatabase.AssetPathToGUID(_matAPath);
|
||||
|
||||
var modify = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = new JObject { ["guid"] = _createdGuid },
|
||||
["patches"] = new JArray
|
||||
{
|
||||
// Resize list to 2
|
||||
new JObject { ["propertyPath"] = "materials.Array.size", ["op"] = "array_resize", ["value"] = 2 },
|
||||
// Assign element 0 by guid
|
||||
new JObject
|
||||
{
|
||||
["propertyPath"] = "materials.Array.data[0]",
|
||||
["op"] = "set",
|
||||
["ref"] = new JObject { ["guid"] = matAGuid }
|
||||
},
|
||||
// Assign element 1 by path
|
||||
new JObject
|
||||
{
|
||||
["propertyPath"] = "materials.Array.data[1]",
|
||||
["op"] = "set",
|
||||
["ref"] = new JObject { ["path"] = _matBPath }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var modRes = ToJObject(ManageScriptableObject.HandleCommand(modify));
|
||||
Assert.IsTrue(modRes.Value<bool>("success"), modRes.ToString());
|
||||
|
||||
// Assert patch results are ok so failures are visible even if the tool returns success.
|
||||
var results = modRes["data"]?["results"] as JArray;
|
||||
Assert.IsNotNull(results, "Expected per-patch results in response.");
|
||||
foreach (var r in results!)
|
||||
{
|
||||
Assert.IsTrue(r.Value<bool>("ok"), $"Patch failed: {r}");
|
||||
}
|
||||
|
||||
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
|
||||
Assert.IsNotNull(asset);
|
||||
Assert.AreEqual(2, asset!.Materials.Count, "List should be resized to 2.");
|
||||
|
||||
var matA = AssetDatabase.LoadAssetAtPath<Material>(_matAPath);
|
||||
var matB = AssetDatabase.LoadAssetAtPath<Material>(_matBPath);
|
||||
Assert.AreEqual(matA, asset.Materials[0], "Element 0 should be set by GUID ref.");
|
||||
Assert.AreEqual(matB, asset.Materials[1], "Element 1 should be set by path ref.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Errors_InvalidAction_TypeNotFound_TargetNotFound()
|
||||
{
|
||||
// invalid action
|
||||
var badAction = ToJObject(ManageScriptableObject.HandleCommand(new JObject { ["action"] = "nope" }));
|
||||
Assert.IsFalse(badAction.Value<bool>("success"));
|
||||
Assert.AreEqual("invalid_params", badAction.Value<string>("error"));
|
||||
|
||||
// type not found
|
||||
var badType = ToJObject(ManageScriptableObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = "Nope.MissingType",
|
||||
["folderPath"] = TempRoot,
|
||||
["assetName"] = "X",
|
||||
}));
|
||||
Assert.IsFalse(badType.Value<bool>("success"));
|
||||
Assert.AreEqual("type_not_found", badType.Value<string>("error"));
|
||||
|
||||
// target not found
|
||||
var badTarget = ToJObject(ManageScriptableObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["target"] = new JObject { ["guid"] = "00000000000000000000000000000000" },
|
||||
["patches"] = new JArray(),
|
||||
}));
|
||||
Assert.IsFalse(badTarget.Value<bool>("success"));
|
||||
Assert.AreEqual("target_not_found", badTarget.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_RejectsNonAssetsRootFolders()
|
||||
{
|
||||
var badPackages = ToJObject(ManageScriptableObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
["folderPath"] = "Packages/NotAllowed",
|
||||
["assetName"] = "BadFolder",
|
||||
["overwrite"] = true,
|
||||
}));
|
||||
Assert.IsFalse(badPackages.Value<bool>("success"));
|
||||
Assert.AreEqual("invalid_folder_path", badPackages.Value<string>("error"));
|
||||
|
||||
var badAbsolute = ToJObject(ManageScriptableObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
["folderPath"] = "/tmp/not_allowed",
|
||||
["assetName"] = "BadFolder2",
|
||||
["overwrite"] = true,
|
||||
}));
|
||||
Assert.IsFalse(badAbsolute.Value<bool>("success"));
|
||||
Assert.AreEqual("invalid_folder_path", badAbsolute.Value<string>("error"));
|
||||
|
||||
var badFileUri = ToJObject(ManageScriptableObject.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
["folderPath"] = "file:///tmp/not_allowed",
|
||||
["assetName"] = "BadFolder3",
|
||||
["overwrite"] = true,
|
||||
}));
|
||||
Assert.IsFalse(badFileUri.Value<bool>("success"));
|
||||
Assert.AreEqual("invalid_folder_path", badFileUri.Value<string>("error"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_NormalizesRelativeAndBackslashPaths_AndAvoidsDoubleSlashesInResult()
|
||||
{
|
||||
var create = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
|
||||
["folderPath"] = @"Temp\ManageScriptableObjectTests\SlashProbe\\Deep",
|
||||
["assetName"] = "SlashProbe",
|
||||
["overwrite"] = true,
|
||||
};
|
||||
|
||||
var res = ToJObject(ManageScriptableObject.HandleCommand(create));
|
||||
Assert.IsTrue(res.Value<bool>("success"), res.ToString());
|
||||
|
||||
var path = res["data"]?["path"]?.ToString();
|
||||
Assert.IsNotNull(path, "Expected path in response.");
|
||||
Assert.IsTrue(path!.StartsWith("Assets/Temp/ManageScriptableObjectTests/SlashProbe/Deep", StringComparison.Ordinal),
|
||||
$"Expected sanitized Assets-rooted path, got: {path}");
|
||||
Assert.IsFalse(path.Contains("//", StringComparison.Ordinal), $"Path should not contain double slashes: {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1aac13ba83f134fc2ae264408d048c9b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,903 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using UnityEngine.UIElements;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ManageUITests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/ManageUITests";
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
EnsureFolder(TempRoot);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
}
|
||||
|
||||
// ---- Action validation ----
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_MissingAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject()));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_UnknownAction_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "explode"
|
||||
}));
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Unknown action"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ping_ReturnsPong()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "ping"
|
||||
}));
|
||||
Assert.IsTrue(result.Value<bool>("success"));
|
||||
Assert.AreEqual("pong", result.Value<string>("message"));
|
||||
}
|
||||
|
||||
// ---- Create file ----
|
||||
|
||||
[Test]
|
||||
public void Create_Uxml_CreatesFile()
|
||||
{
|
||||
string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Hi\" /></ui:UXML>";
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Verify file was created on disk
|
||||
string fullPath = Path.Combine(Application.dataPath,
|
||||
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
|
||||
Assert.IsTrue(File.Exists(fullPath), $"File should exist at {fullPath}");
|
||||
|
||||
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
|
||||
string actual = File.ReadAllText(fullPath);
|
||||
Assert.That(actual, Does.Contain("ui:UXML"));
|
||||
Assert.That(actual, Does.Contain("ui:Label"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_Uss_CreatesFile()
|
||||
{
|
||||
string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uss";
|
||||
string content = ".root { background-color: red; }";
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_InvalidExtension_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = $"{TempRoot}/Test.txt",
|
||||
["contents"] = "hello",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain(".uxml or .uss"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_MissingContents_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = $"{TempRoot}/Test.uxml",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("contents"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_AlreadyExists_ReturnsError()
|
||||
{
|
||||
string path = $"{TempRoot}/Exists_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
||||
|
||||
// Create first time
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
});
|
||||
|
||||
// Try to create again
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("already exists"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WithBase64EncodedContents_Decodes()
|
||||
{
|
||||
string path = $"{TempRoot}/Encoded_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
||||
string encoded = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content));
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["encodedContents"] = encoded,
|
||||
["contentsEncoded"] = true,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
string fullPath = Path.Combine(Application.dataPath,
|
||||
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
|
||||
string actual = File.ReadAllText(fullPath);
|
||||
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
|
||||
Assert.That(actual, Does.Contain("ui:UXML"));
|
||||
Assert.That(actual, Does.Contain("UnityEngine.UIElements"));
|
||||
}
|
||||
|
||||
// ---- Read file ----
|
||||
|
||||
[Test]
|
||||
public void Read_ExistingFile_ReturnsContents()
|
||||
{
|
||||
string path = $"{TempRoot}/ReadTest_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
||||
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "read",
|
||||
["path"] = path,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
|
||||
Assert.That(data.Value<string>("contents"), Does.Contain("ui:UXML"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Read_NonExistentFile_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "read",
|
||||
["path"] = $"{TempRoot}/DoesNotExist.uxml",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("not found"));
|
||||
}
|
||||
|
||||
// ---- Update file ----
|
||||
|
||||
[Test]
|
||||
public void Update_ExistingFile_OverwritesContents()
|
||||
{
|
||||
string path = $"{TempRoot}/UpdateTest_{Guid.NewGuid():N}.uss";
|
||||
string original = ".root { color: red; }";
|
||||
string updated = ".root { color: blue; font-size: 20px; }";
|
||||
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = original,
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "update",
|
||||
["path"] = path,
|
||||
["contents"] = updated,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Verify content was updated
|
||||
var readResult = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "read",
|
||||
["path"] = path,
|
||||
}));
|
||||
Assert.AreEqual(updated, readResult["data"].Value<string>("contents"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_NonExistentFile_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "update",
|
||||
["path"] = $"{TempRoot}/Missing.uxml",
|
||||
["contents"] = "<ui:UXML />",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("not found"));
|
||||
}
|
||||
|
||||
// ---- Create PanelSettings ----
|
||||
|
||||
[Test]
|
||||
public void CreatePanelSettings_CreatesAsset()
|
||||
{
|
||||
string path = $"{TempRoot}/TestPanel_{Guid.NewGuid():N}.asset";
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_panel_settings",
|
||||
["path"] = path,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(path);
|
||||
Assert.IsNotNull(ps, "PanelSettings should exist at the path");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreatePanelSettings_AlreadyExists_ReturnsError()
|
||||
{
|
||||
string path = $"{TempRoot}/ExistingPanel_{Guid.NewGuid():N}.asset";
|
||||
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_panel_settings",
|
||||
["path"] = path,
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create_panel_settings",
|
||||
["path"] = path,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("already exists"));
|
||||
}
|
||||
|
||||
// ---- Attach UIDocument ----
|
||||
|
||||
[Test]
|
||||
public void AttachUIDocument_AddsComponent()
|
||||
{
|
||||
// Create a UXML file first
|
||||
string uxmlPath = $"{TempRoot}/Attach_{Guid.NewGuid():N}.uxml";
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = uxmlPath,
|
||||
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Test\" /></ui:UXML>",
|
||||
});
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
// Create a test GameObject
|
||||
var go = new GameObject("UITestObject_Attach");
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "attach_ui_document",
|
||||
["target"] = go.name,
|
||||
["source_asset"] = uxmlPath,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
var uiDoc = go.GetComponent<UIDocument>();
|
||||
Assert.IsNotNull(uiDoc, "UIDocument component should be attached");
|
||||
Assert.IsNotNull(uiDoc.visualTreeAsset, "VisualTreeAsset should be assigned");
|
||||
Assert.IsNotNull(uiDoc.panelSettings, "PanelSettings should be assigned (auto-created)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AttachUIDocument_MissingTarget_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "attach_ui_document",
|
||||
["source_asset"] = "Assets/UI/Test.uxml",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AttachUIDocument_MissingSourceAsset_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "attach_ui_document",
|
||||
["target"] = "SomeObject",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
// ---- Get Visual Tree ----
|
||||
|
||||
[Test]
|
||||
public void GetVisualTree_MissingTarget_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_visual_tree",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetVisualTree_NoUIDocument_ReturnsError()
|
||||
{
|
||||
var go = new GameObject("UITestObject_NoDoc");
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get_visual_tree",
|
||||
["target"] = go.name,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("UIDocument"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Delete file ----
|
||||
|
||||
[Test]
|
||||
public void Delete_ExistingFile_DeletesFile()
|
||||
{
|
||||
string path = $"{TempRoot}/Delete_{Guid.NewGuid():N}.uss";
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = ".root { color: red; }",
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["path"] = path,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
string fullPath = Path.Combine(Application.dataPath,
|
||||
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
|
||||
Assert.IsFalse(File.Exists(fullPath), "File should be deleted");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_NonExistentFile_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["path"] = $"{TempRoot}/Missing.uxml",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("not found"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_InvalidExtension_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["path"] = $"{TempRoot}/File.txt",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain(".uxml or .uss"));
|
||||
}
|
||||
|
||||
// ---- List UI assets ----
|
||||
|
||||
[Test]
|
||||
public void List_ReturnsUIAssets()
|
||||
{
|
||||
string uxmlPath = $"{TempRoot}/ListTest_{Guid.NewGuid():N}.uxml";
|
||||
string ussPath = $"{TempRoot}/ListTest_{Guid.NewGuid():N}.uss";
|
||||
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = uxmlPath,
|
||||
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />",
|
||||
});
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = ussPath,
|
||||
["contents"] = ".root { }",
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "list",
|
||||
["path"] = TempRoot,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
int total = data.Value<int>("total");
|
||||
Assert.GreaterOrEqual(total, 2, "Should find at least 2 UI assets");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void List_WithFilterType_FiltersResults()
|
||||
{
|
||||
string uxmlPath = $"{TempRoot}/FilterTest_{Guid.NewGuid():N}.uxml";
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = uxmlPath,
|
||||
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />",
|
||||
});
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "list",
|
||||
["path"] = TempRoot,
|
||||
["filterType"] = "uxml",
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var assets = result["data"]["assets"] as JArray;
|
||||
Assert.IsNotNull(assets);
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
Assert.AreEqual("uxml", asset.Value<string>("type"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Detach UIDocument ----
|
||||
|
||||
[Test]
|
||||
public void DetachUIDocument_RemovesComponent()
|
||||
{
|
||||
string uxmlPath = $"{TempRoot}/Detach_{Guid.NewGuid():N}.uxml";
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = uxmlPath,
|
||||
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Test\" /></ui:UXML>",
|
||||
});
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var go = new GameObject("UITestObject_Detach");
|
||||
try
|
||||
{
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "attach_ui_document",
|
||||
["target"] = go.name,
|
||||
["source_asset"] = uxmlPath,
|
||||
});
|
||||
Assert.IsNotNull(go.GetComponent<UIDocument>(), "UIDocument should be attached");
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "detach_ui_document",
|
||||
["target"] = go.name,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
Assert.IsNull(go.GetComponent<UIDocument>(), "UIDocument should be removed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DetachUIDocument_NoUIDocument_ReturnsError()
|
||||
{
|
||||
var go = new GameObject("UITestObject_DetachNoDoc");
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "detach_ui_document",
|
||||
["target"] = go.name,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("UIDocument"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DetachUIDocument_MissingTarget_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "detach_ui_document",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
// ---- Modify visual element ----
|
||||
|
||||
[Test]
|
||||
public void ModifyVisualElement_MissingTarget_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "modify_visual_element",
|
||||
["elementName"] = "test",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModifyVisualElement_MissingElementName_ReturnsError()
|
||||
{
|
||||
var go = new GameObject("UITestObject_ModifyNoName");
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "modify_visual_element",
|
||||
["target"] = go.name,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("element_name"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModifyVisualElement_NoUIDocument_ReturnsError()
|
||||
{
|
||||
var go = new GameObject("UITestObject_ModifyNoDoc");
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "modify_visual_element",
|
||||
["target"] = go.name,
|
||||
["elementName"] = "test",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("UIDocument"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- UXML validation ----
|
||||
|
||||
[Test]
|
||||
public void Create_MalformedXml_ReturnsError_FileNotWritten()
|
||||
{
|
||||
string path = $"{TempRoot}/Malformed_{Guid.NewGuid():N}.uxml";
|
||||
string badContent = "<ui:UXML><ui:Label text=\"unclosed\">";
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = badContent,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Malformed XML"));
|
||||
|
||||
// Verify file was NOT written
|
||||
string fullPath = Path.Combine(Application.dataPath,
|
||||
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
|
||||
Assert.IsFalse(File.Exists(fullPath), "Malformed UXML should not be written to disk");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_MissingNamespace_WritesWithWarning()
|
||||
{
|
||||
string path = $"{TempRoot}/NoNs_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<ui:UXML><ui:Label text=\"hi\" /></ui:UXML>";
|
||||
|
||||
// Unity's UXML importer logs an error for undeclared 'ui' prefix
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNotNull(data);
|
||||
var warnings = data["validationWarnings"] as JArray;
|
||||
Assert.IsNotNull(warnings, "Should have validationWarnings");
|
||||
Assert.That(warnings.ToString(), Does.Contain("Missing namespace"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = false;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_ValidUxml_NoWarnings()
|
||||
{
|
||||
string path = $"{TempRoot}/Valid_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"ok\" /></ui:UXML>";
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
Assert.IsNull(data?["validationWarnings"],
|
||||
"Fully valid UXML should not have validationWarnings");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_WrongRootElement_WritesWithWarning()
|
||||
{
|
||||
string path = $"{TempRoot}/WrongRoot_{Guid.NewGuid():N}.uxml";
|
||||
string content = "<div xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"hi\" /></div>";
|
||||
|
||||
// Unity's UXML importer logs an error about expected root element
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JObject;
|
||||
var warnings = data["validationWarnings"] as JArray;
|
||||
Assert.IsNotNull(warnings, "Should have validationWarnings");
|
||||
Assert.That(warnings.ToString(), Does.Contain("Root element"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = false;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_EmptyContent_ReturnsError()
|
||||
{
|
||||
string path = $"{TempRoot}/Empty_{Guid.NewGuid():N}.uxml";
|
||||
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = " ",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("empty"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_MalformedXml_ReturnsError_FileNotChanged()
|
||||
{
|
||||
string path = $"{TempRoot}/UpdateMalformed_{Guid.NewGuid():N}.uxml";
|
||||
string original = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
||||
|
||||
ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = original,
|
||||
});
|
||||
|
||||
string badContent = "<ui:UXML><broken>";
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "update",
|
||||
["path"] = path,
|
||||
["contents"] = badContent,
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("Malformed XML"));
|
||||
|
||||
// Verify original content was preserved (EnsureEditorExtensionMode may have injected attribute)
|
||||
string fullPath = Path.Combine(Application.dataPath,
|
||||
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
|
||||
string actual = File.ReadAllText(fullPath);
|
||||
Assert.That(actual, Does.Contain("ui:UXML"), "Original file content should be preserved");
|
||||
Assert.That(actual, Does.Not.Contain("<broken>"), "Malformed content should not be written");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_Uss_SkipsUxmlValidation()
|
||||
{
|
||||
string path = $"{TempRoot}/NoValidation_{Guid.NewGuid():N}.uss";
|
||||
// USS is CSS-like, not XML — validation should be skipped
|
||||
string content = "This is not valid XML <broken>";
|
||||
|
||||
// Unity 6000.4+'s USS importer logs an error on this content; the test only
|
||||
// verifies that ManageUI itself doesn't pre-validate .uss as UXML, not the
|
||||
// downstream importer's behavior.
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
try
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = path,
|
||||
["contents"] = content,
|
||||
}));
|
||||
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Path traversal validation ----
|
||||
|
||||
[Test]
|
||||
public void Create_TraversalPath_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = "Assets/../etc/evil.uxml",
|
||||
["contents"] = "<ui:UXML />",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_DotDotInMiddle_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = "Assets/UI/../../secret.uxml",
|
||||
["contents"] = "<ui:UXML />",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Read_TraversalPath_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "read",
|
||||
["path"] = "Assets/../secret.uxml",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_TraversalPath_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "update",
|
||||
["path"] = "Assets/../../etc/passwd.uxml",
|
||||
["contents"] = "overwrite",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_TraversalPath_ReturnsError()
|
||||
{
|
||||
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "delete",
|
||||
["path"] = "Assets/../outside.uxml",
|
||||
}));
|
||||
|
||||
Assert.IsFalse(result.Value<bool>("success"));
|
||||
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b16ad7850014de1afb1696eb1d09bd1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class MaterialDirectPropertiesTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/MaterialDirectPropertiesTests";
|
||||
private string _matPath;
|
||||
private string _baseMapPath;
|
||||
private string _normalMapPath;
|
||||
private string _occlusionMapPath;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
}
|
||||
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "MaterialDirectPropertiesTests");
|
||||
}
|
||||
|
||||
string guid = Guid.NewGuid().ToString("N");
|
||||
_matPath = $"{TempRoot}/DirectProps_{guid}.mat";
|
||||
_baseMapPath = $"{TempRoot}/TexBase_{guid}.asset";
|
||||
_normalMapPath = $"{TempRoot}/TexNormal_{guid}.asset";
|
||||
_occlusionMapPath = $"{TempRoot}/TexOcc_{guid}.asset";
|
||||
|
||||
// Clean any leftovers just in case
|
||||
TryDeleteAsset(_matPath);
|
||||
TryDeleteAsset(_baseMapPath);
|
||||
TryDeleteAsset(_normalMapPath);
|
||||
TryDeleteAsset(_occlusionMapPath);
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
TryDeleteAsset(_matPath);
|
||||
TryDeleteAsset(_baseMapPath);
|
||||
TryDeleteAsset(_normalMapPath);
|
||||
TryDeleteAsset(_occlusionMapPath);
|
||||
|
||||
// Clean up temp directory after each test
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
private static void TryDeleteAsset(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
}
|
||||
var abs = Path.Combine(Directory.GetCurrentDirectory(), path);
|
||||
try
|
||||
{
|
||||
if (File.Exists(abs)) File.Delete(abs);
|
||||
if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static Texture2D CreateSolidTextureAsset(string path, Color color)
|
||||
{
|
||||
var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false);
|
||||
var pixels = new Color[16];
|
||||
for (int i = 0; i < pixels.Length; i++) pixels[i] = color;
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
AssetDatabase.CreateAsset(tex, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
return tex;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
|
||||
{
|
||||
// Arrange: create textures as assets
|
||||
CreateSolidTextureAsset(_baseMapPath, Color.white);
|
||||
CreateSolidTextureAsset(_normalMapPath, new Color(0.5f, 0.5f, 1f));
|
||||
CreateSolidTextureAsset(_occlusionMapPath, Color.gray);
|
||||
|
||||
// Create material using direct keys via JSON string
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = _matPath,
|
||||
["assetType"] = "Material",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shader"] = "Universal Render Pipeline/Lit",
|
||||
["_Color"] = new JArray(0f, 1f, 0f, 1f),
|
||||
["_Glossiness"] = 0.25f
|
||||
}
|
||||
};
|
||||
var createRes = ToJObject(ManageAsset.HandleCommand(createParams));
|
||||
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
|
||||
|
||||
// Modify with aliases and textures
|
||||
var modifyParams = new JObject
|
||||
{
|
||||
["action"] = "modify",
|
||||
["path"] = _matPath,
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["_BaseColor"] = new JArray(0f, 0f, 1f, 1f),
|
||||
["_Smoothness"] = 0.5f,
|
||||
["_BaseMap"] = _baseMapPath,
|
||||
["_BumpMap"] = _normalMapPath,
|
||||
["_OcclusionMap"] = _occlusionMapPath
|
||||
}
|
||||
};
|
||||
var modifyRes = ToJObject(ManageAsset.HandleCommand(modifyParams));
|
||||
Assert.IsTrue(modifyRes.Value<bool>("success"), modifyRes.ToString());
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
Assert.IsNotNull(mat, "Material should exist at path.");
|
||||
|
||||
// Verify color alias applied
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
{
|
||||
Assert.AreEqual(Color.blue, mat.GetColor("_BaseColor"));
|
||||
}
|
||||
else if (mat.HasProperty("_Color"))
|
||||
{
|
||||
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
|
||||
}
|
||||
|
||||
// Verify float
|
||||
string smoothProp = mat.HasProperty("_Smoothness") ? "_Smoothness" : (mat.HasProperty("_Glossiness") ? "_Glossiness" : null);
|
||||
Assert.IsNotNull(smoothProp, "Material should expose Smoothness/Glossiness.");
|
||||
Assert.That(Mathf.Abs(mat.GetFloat(smoothProp) - 0.5f) < 1e-4f);
|
||||
|
||||
// Verify textures
|
||||
string baseMapProp = mat.HasProperty("_BaseMap") ? "_BaseMap" : (mat.HasProperty("_MainTex") ? "_MainTex" : null);
|
||||
Assert.IsNotNull(baseMapProp, "Material should expose BaseMap/MainTex.");
|
||||
Assert.IsNotNull(mat.GetTexture(baseMapProp), "BaseMap/MainTex should be assigned.");
|
||||
if (mat.HasProperty("_BumpMap")) Assert.IsNotNull(mat.GetTexture("_BumpMap"));
|
||||
if (mat.HasProperty("_OcclusionMap")) Assert.IsNotNull(mat.GetTexture("_OcclusionMap"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3623619548eef40568ac5e3cef4c22a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests specifically for the material and mesh instantiation warnings fix.
|
||||
/// These tests verify that the GameObjectSerializer uses sharedMaterial/sharedMesh
|
||||
/// in edit mode to prevent Unity's instantiation warnings.
|
||||
/// </summary>
|
||||
public class MaterialMeshInstantiationTests
|
||||
{
|
||||
private GameObject testGameObject;
|
||||
private Material testMaterial;
|
||||
private Mesh testMesh;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Create a test GameObject for each test
|
||||
testGameObject = new GameObject("MaterialMeshTestObject");
|
||||
|
||||
// Create test material and mesh
|
||||
testMaterial = new Material(Shader.Find("Standard"));
|
||||
testMaterial.name = "TestMaterial";
|
||||
|
||||
var temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
testMesh = temp.GetComponent<MeshFilter>().sharedMesh;
|
||||
UnityEngine.Object.DestroyImmediate(temp);
|
||||
testMesh.name = "TestMesh";
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up test objects
|
||||
if (testMaterial != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testMaterial);
|
||||
}
|
||||
|
||||
if (testGameObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testGameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_UsesSharedMaterialInsteadOfMaterial()
|
||||
{
|
||||
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
|
||||
meshRenderer.sharedMaterial = testMaterial;
|
||||
int beforeId = meshRenderer.sharedMaterial != null ? meshRenderer.sharedMaterial.GetInstanceID() : 0;
|
||||
var result = GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
int afterId = meshRenderer.sharedMaterial != null ? meshRenderer.sharedMaterial.GetInstanceID() : 0;
|
||||
Assert.AreEqual(beforeId, afterId, "sharedMaterial instanceID must not change during edit-mode serialization (no instantiation)");
|
||||
Assert.IsNotNull(result, "GetComponentData should return a result");
|
||||
var propsObj = (result as Dictionary<string, object>) != null && ((Dictionary<string, object>)result).TryGetValue("properties", out var p)
|
||||
? p as Dictionary<string, object>
|
||||
: null;
|
||||
if (propsObj != null)
|
||||
{
|
||||
long? foundInstanceId = null;
|
||||
if (propsObj.TryGetValue("material", out var materialObj) && materialObj is Dictionary<string, object> matDict && matDict.TryGetValue("instanceID", out var idObj1) && idObj1 is long id1)
|
||||
{
|
||||
foundInstanceId = id1;
|
||||
}
|
||||
else if (propsObj.TryGetValue("sharedMaterial", out var sharedMatObj) && sharedMatObj is Dictionary<string, object> sharedMatDict && sharedMatDict.TryGetValue("instanceID", out var idObj2) && idObj2 is long id2)
|
||||
{
|
||||
foundInstanceId = id2;
|
||||
}
|
||||
else if (propsObj.TryGetValue("materials", out var materialsObj) && materialsObj is List<object> mats && mats.Count > 0 && mats[0] is Dictionary<string, object> firstMat && firstMat.TryGetValue("instanceID", out var idObj3) && idObj3 is long id3)
|
||||
{
|
||||
foundInstanceId = id3;
|
||||
}
|
||||
else if (propsObj.TryGetValue("sharedMaterials", out var sharedMaterialsObj) && sharedMaterialsObj is List<object> smats && smats.Count > 0 && smats[0] is Dictionary<string, object> firstSMat && firstSMat.TryGetValue("instanceID", out var idObj4) && idObj4 is long id4)
|
||||
{
|
||||
foundInstanceId = id4;
|
||||
}
|
||||
if (foundInstanceId.HasValue)
|
||||
{
|
||||
Assert.AreEqual(beforeId, (int)foundInstanceId.Value, "Serialized material must reference the sharedMaterial instance");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_UsesSharedMeshInsteadOfMesh()
|
||||
{
|
||||
var meshFilter = testGameObject.AddComponent<MeshFilter>();
|
||||
var uniqueMesh = UnityEngine.Object.Instantiate(testMesh);
|
||||
meshFilter.sharedMesh = uniqueMesh;
|
||||
int beforeId = meshFilter.sharedMesh != null ? meshFilter.sharedMesh.GetInstanceID() : 0;
|
||||
var result = GameObjectSerializer.GetComponentData(meshFilter);
|
||||
int afterId = meshFilter.sharedMesh != null ? meshFilter.sharedMesh.GetInstanceID() : 0;
|
||||
Assert.AreEqual(beforeId, afterId, "sharedMesh instanceID must not change during edit-mode serialization (no instantiation)");
|
||||
Assert.IsNotNull(result, "GetComponentData should return a result");
|
||||
var propsObj = (result as Dictionary<string, object>) != null && ((Dictionary<string, object>)result).TryGetValue("properties", out var p)
|
||||
? p as Dictionary<string, object>
|
||||
: null;
|
||||
if (propsObj != null)
|
||||
{
|
||||
long? foundInstanceId = null;
|
||||
if (propsObj.TryGetValue("mesh", out var meshObj) && meshObj is Dictionary<string, object> meshDict && meshDict.TryGetValue("instanceID", out var idObj1) && idObj1 is long id1)
|
||||
{
|
||||
foundInstanceId = id1;
|
||||
}
|
||||
else if (propsObj.TryGetValue("sharedMesh", out var sharedMeshObj) && sharedMeshObj is Dictionary<string, object> sharedMeshDict && sharedMeshDict.TryGetValue("instanceID", out var idObj2) && idObj2 is long id2)
|
||||
{
|
||||
foundInstanceId = id2;
|
||||
}
|
||||
if (foundInstanceId.HasValue)
|
||||
{
|
||||
Assert.AreEqual(beforeId, (int)foundInstanceId.Value, "Serialized mesh must reference the sharedMesh instance");
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the instantiated mesh
|
||||
UnityEngine.Object.DestroyImmediate(uniqueMesh);
|
||||
}
|
||||
|
||||
// (The two strong tests above replace the prior lighter-weight versions.)
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_HandlesNullSharedMaterial()
|
||||
{
|
||||
// Arrange - Create MeshRenderer without setting shared material
|
||||
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
|
||||
// Don't set sharedMaterial - it should be null
|
||||
|
||||
// Act - Get component data
|
||||
var result = GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
|
||||
// Assert - Should handle null shared material gracefully
|
||||
Assert.IsNotNull(result, "GetComponentData should handle null shared material");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_HandlesNullSharedMesh()
|
||||
{
|
||||
// Arrange - Create MeshFilter without setting shared mesh
|
||||
var meshFilter = testGameObject.AddComponent<MeshFilter>();
|
||||
// Don't set sharedMesh - it should be null
|
||||
|
||||
// Act - Get component data
|
||||
var result = GameObjectSerializer.GetComponentData(meshFilter);
|
||||
|
||||
// Assert - Should handle null shared mesh gracefully
|
||||
Assert.IsNotNull(result, "GetComponentData should handle null shared mesh");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_WorksWithMultipleSharedMaterials()
|
||||
{
|
||||
// Arrange - Create MeshRenderer with multiple shared materials
|
||||
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
|
||||
|
||||
var material1 = new Material(Shader.Find("Standard"));
|
||||
material1.name = "TestMaterial1";
|
||||
var material2 = new Material(Shader.Find("Standard"));
|
||||
material2.name = "TestMaterial2";
|
||||
|
||||
meshRenderer.sharedMaterials = new Material[] { material1, material2 };
|
||||
|
||||
// Act - Get component data
|
||||
var result = GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
|
||||
// Assert - Should handle multiple shared materials
|
||||
Assert.IsNotNull(result, "GetComponentData should handle multiple shared materials");
|
||||
|
||||
// Clean up additional materials
|
||||
UnityEngine.Object.DestroyImmediate(material1);
|
||||
UnityEngine.Object.DestroyImmediate(material2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetComponentData_EditModeDetectionWorks()
|
||||
{
|
||||
// This test verifies that our edit mode detection is working
|
||||
// We can't easily test Application.isPlaying directly, but we can verify
|
||||
// that the behavior is consistent with edit mode expectations
|
||||
|
||||
// Arrange - Create components that would trigger warnings in edit mode
|
||||
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
|
||||
var meshFilter = testGameObject.AddComponent<MeshFilter>();
|
||||
|
||||
meshRenderer.sharedMaterial = testMaterial;
|
||||
meshFilter.sharedMesh = testMesh;
|
||||
|
||||
// Act - Get component data multiple times
|
||||
var rendererResult = GameObjectSerializer.GetComponentData(meshRenderer);
|
||||
var meshFilterResult = GameObjectSerializer.GetComponentData(meshFilter);
|
||||
|
||||
// Assert - Both operations should succeed without warnings
|
||||
Assert.IsNotNull(rendererResult, "MeshRenderer serialization should work in edit mode");
|
||||
Assert.IsNotNull(meshFilterResult, "MeshFilter serialization should work in edit mode");
|
||||
}
|
||||
// Removed low-value property-presence tests; the instanceID tests are the authoritative guardrails.
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f67ba1d248b564c97b1afa12caae0196
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Tools.GameObjects;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class MaterialParameterToolTests
|
||||
{
|
||||
private const string TempRoot = "Assets/Temp/MaterialParameterToolTests";
|
||||
private string _matPath; // unique per test run
|
||||
private GameObject _sphere;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_matPath = $"{TempRoot}/BlueURP_{Guid.NewGuid().ToString("N")}.mat";
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||
}
|
||||
if (!AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.CreateFolder("Assets/Temp", "MaterialParameterToolTests");
|
||||
}
|
||||
// Ensure any leftover material from previous runs is removed
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_matPath);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
// Hard-delete any stray files on disk (in case GUID lookup fails)
|
||||
var abs = Path.Combine(Directory.GetCurrentDirectory(), _matPath);
|
||||
try
|
||||
{
|
||||
if (File.Exists(abs)) File.Delete(abs);
|
||||
if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
|
||||
}
|
||||
catch { /* best-effort cleanup */ }
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_sphere != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(_sphere);
|
||||
_sphere = null;
|
||||
}
|
||||
if (AssetDatabase.LoadAssetAtPath<Material>(_matPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_matPath);
|
||||
}
|
||||
|
||||
// Clean up temp directory after each test
|
||||
if (AssetDatabase.IsValidFolder(TempRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TempRoot);
|
||||
}
|
||||
|
||||
// Clean up empty parent folders to avoid debris
|
||||
CleanupEmptyParentFolders(TempRoot);
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateMaterial_WithObjectProperties_SucceedsAndSetsColor()
|
||||
{
|
||||
// Ensure a clean state if a previous run left the asset behind (uses _matPath now)
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(_matPath);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
var createParams = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["path"] = _matPath,
|
||||
["assetType"] = "Material",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["shader"] = "Universal Render Pipeline/Lit",
|
||||
["color"] = new JArray(0f, 0f, 1f, 1f)
|
||||
}
|
||||
};
|
||||
|
||||
var result = ToJObject(ManageAsset.HandleCommand(createParams));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.Value<string>("error"));
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
|
||||
Assert.IsNotNull(mat, "Material should exist at path.");
|
||||
// Verify color if shader exposes _Color
|
||||
if (mat.HasProperty("_Color"))
|
||||
{
|
||||
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssignMaterial_ToSphere_UsingManageMaterial_Succeeds()
|
||||
{
|
||||
// Ensure material exists first
|
||||
CreateMaterial_WithObjectProperties_SucceedsAndSetsColor();
|
||||
|
||||
// Create a sphere via handler
|
||||
var createGo = new JObject
|
||||
{
|
||||
["action"] = "create",
|
||||
["name"] = "ToolTestSphere",
|
||||
["primitiveType"] = "Sphere"
|
||||
};
|
||||
var createGoResult = ToJObject(ManageGameObject.HandleCommand(createGo));
|
||||
Assert.IsTrue(createGoResult.Value<bool>("success"), createGoResult.Value<string>("error"));
|
||||
|
||||
_sphere = GameObject.Find("ToolTestSphere");
|
||||
Assert.IsNotNull(_sphere, "Sphere should be created.");
|
||||
|
||||
// Assign material via ManageMaterial tool
|
||||
var assignParams = new JObject
|
||||
{
|
||||
["action"] = "assign_material_to_renderer",
|
||||
["target"] = "ToolTestSphere",
|
||||
["searchMethod"] = "by_name",
|
||||
["materialPath"] = _matPath,
|
||||
["slot"] = 0
|
||||
};
|
||||
|
||||
var assignResult = ToJObject(ManageMaterial.HandleCommand(assignParams));
|
||||
Assert.IsTrue(assignResult.Value<bool>("success"), assignResult.ToString());
|
||||
|
||||
var renderer = _sphere.GetComponent<MeshRenderer>();
|
||||
Assert.IsNotNull(renderer, "Sphere should have MeshRenderer.");
|
||||
Assert.IsNotNull(renderer.sharedMaterial, "sharedMaterial should be assigned.");
|
||||
StringAssert.StartsWith("BlueURP_", renderer.sharedMaterial.name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadRendererData_DoesNotInstantiateMaterial_AndIncludesSharedMaterial()
|
||||
{
|
||||
// Prepare object and assignment
|
||||
AssignMaterial_ToSphere_UsingManageMaterial_Succeeds();
|
||||
|
||||
var renderer = _sphere.GetComponent<MeshRenderer>();
|
||||
int beforeId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
|
||||
|
||||
var data = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(renderer) as System.Collections.Generic.Dictionary<string, object>;
|
||||
Assert.IsNotNull(data, "Serializer should return data.");
|
||||
|
||||
int afterId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
|
||||
Assert.AreEqual(beforeId, afterId, "sharedMaterial instance must not change (no instantiation in EditMode).");
|
||||
|
||||
if (data.TryGetValue("properties", out var propsObj) && propsObj is System.Collections.Generic.Dictionary<string, object> props)
|
||||
{
|
||||
Assert.IsTrue(
|
||||
props.ContainsKey("sharedMaterial") || props.ContainsKey("material") || props.ContainsKey("sharedMaterials") || props.ContainsKey("materials"),
|
||||
"Serialized data should include material info.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd76b616a816c47a79c4a3da4c307cff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests to reproduce issue #654: PropertyConversion crash causing dispatcher unavailability
|
||||
/// while telemetry continues reporting success.
|
||||
/// </summary>
|
||||
public class PropertyConversionErrorHandlingTests
|
||||
{
|
||||
private GameObject testGameObject;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
testGameObject = new GameObject("PropertyConversionTestObject");
|
||||
CommandRegistry.Initialize();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (testGameObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testGameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 1: Integer value for object reference property (AudioClip on AudioSource)
|
||||
/// Should return graceful error, not crash dispatcher
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ManageComponents_SetProperty_IntegerForObjectReference_ReturnsGracefulError()
|
||||
{
|
||||
// Add AudioSource component
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
|
||||
// Try to set AudioClip (object reference) to integer 12345
|
||||
var setPropertyParams = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "clip",
|
||||
["value"] = 12345 // INCOMPATIBLE: int for AudioClip
|
||||
};
|
||||
|
||||
var result = ManageComponents.HandleCommand(setPropertyParams);
|
||||
|
||||
// Main test: should return a result without crashing
|
||||
Assert.IsNotNull(result, "Should return a result, not crash dispatcher");
|
||||
|
||||
// If it's an ErrorResponse, verify it properly reports failure
|
||||
if (result is ErrorResponse errorResp)
|
||||
{
|
||||
Assert.IsFalse(errorResp.Success, "Should report failure for incompatible type");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 2: Array format for float property (spatialBlend expects float, not array)
|
||||
/// Mirrors the "Array format [0, 0] for Vector2 properties" from issue #654
|
||||
/// This test documents that the error is caught and doesn't crash the dispatcher
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ManageComponents_SetProperty_ArrayForFloatProperty_DoesNotCrashDispatcher()
|
||||
{
|
||||
// Expect the error log that will be generated
|
||||
LogAssert.Expect(LogType.Error, new Regex("Error converting token to System.Single"));
|
||||
|
||||
// Add AudioSource component
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
|
||||
// Try to set spatialBlend (float) to array [0, 0]
|
||||
// This triggers: "Error converting token to System.Single: Error reading double. Unexpected token: StartArray"
|
||||
var setPropertyParams = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "spatialBlend",
|
||||
["value"] = JArray.Parse("[0, 0]") // INCOMPATIBLE: array for float
|
||||
};
|
||||
|
||||
var result = ManageComponents.HandleCommand(setPropertyParams);
|
||||
|
||||
// Main test: dispatcher should remain responsive and return a result
|
||||
Assert.IsNotNull(result, "Should return a result, not crash dispatcher");
|
||||
|
||||
// Verify subsequent commands still work
|
||||
var followupParams = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "volume",
|
||||
["value"] = 0.5f
|
||||
};
|
||||
|
||||
var followupResult = ManageComponents.HandleCommand(followupParams);
|
||||
Assert.IsNotNull(followupResult, "Dispatcher should still be responsive after conversion error");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 3: Multiple property conversion failures in sequence
|
||||
/// Tests if dispatcher remains responsive after multiple errors
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ManageComponents_MultipleSetPropertyFailures_DispatcherStaysResponsive()
|
||||
{
|
||||
// Expect the error log for the invalid string conversion
|
||||
LogAssert.Expect(LogType.Error, new Regex("Error converting token to System.Single"));
|
||||
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
|
||||
// First bad conversion attempt - int for AudioClip doesn't generate an error log
|
||||
var badParam1 = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "clip",
|
||||
["value"] = 999 // bad: int for AudioClip
|
||||
};
|
||||
|
||||
var result1 = ManageComponents.HandleCommand(badParam1);
|
||||
Assert.IsNotNull(result1, "First call should return result");
|
||||
|
||||
// Second bad conversion attempt - generates error log
|
||||
var badParam2 = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "rolloffFactor",
|
||||
["value"] = "invalid_string" // bad: string for float
|
||||
};
|
||||
|
||||
var result2 = ManageComponents.HandleCommand(badParam2);
|
||||
Assert.IsNotNull(result2, "Second call should return result");
|
||||
|
||||
// Third attempt - valid conversion
|
||||
var badParam3 = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "volume",
|
||||
["value"] = 0.5f // good: float for float - dispatcher should still work
|
||||
};
|
||||
|
||||
var result3 = ManageComponents.HandleCommand(badParam3);
|
||||
Assert.IsNotNull(result3, "Third call should return result (dispatcher should still be responsive)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 4: After property conversion failures, other commands still work
|
||||
/// Tests dispatcher responsiveness
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ManageComponents_AfterConversionFailure_OtherOperationsWork()
|
||||
{
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
|
||||
// Trigger a conversion failure
|
||||
var failParam = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "clip",
|
||||
["value"] = 12345 // bad
|
||||
};
|
||||
|
||||
var failResult = ManageComponents.HandleCommand(failParam);
|
||||
Assert.IsNotNull(failResult, "Should return result for failed conversion");
|
||||
|
||||
// Now try a valid operation on the same component
|
||||
var validParam = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "volume",
|
||||
["value"] = 0.5f // valid: float for float
|
||||
};
|
||||
|
||||
var validResult = ManageComponents.HandleCommand(validParam);
|
||||
Assert.IsNotNull(validResult, "Should still be able to execute valid commands after conversion failure");
|
||||
|
||||
// Verify the property was actually set
|
||||
Assert.AreEqual(0.5f, audioSource.volume, "Volume should have been set to 0.5");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 5: Telemetry continues reporting success even after conversion errors
|
||||
/// This is the core of issue #654: telemetry should accurately reflect dispatcher health
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ManageEditor_TelemetryStatus_ReportsAccurateHealth()
|
||||
{
|
||||
// Trigger multiple conversion failures first
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var badParam = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "clip",
|
||||
["value"] = i * 1000 // bad
|
||||
};
|
||||
ManageComponents.HandleCommand(badParam);
|
||||
}
|
||||
|
||||
// Now check telemetry
|
||||
var telemetryParams = new JObject { ["action"] = "telemetry_status" };
|
||||
var telemetryResult = ManageEditor.HandleCommand(telemetryParams);
|
||||
|
||||
Assert.IsNotNull(telemetryResult, "Telemetry should return result");
|
||||
|
||||
// NOTE: Issue #654 noted that telemetry returns success even when dispatcher is dead.
|
||||
// If telemetry returns success, that's the actual current behavior (which may be a problem).
|
||||
// This test just documents what happens.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 6: Direct PropertyConversion error handling
|
||||
/// Tests if PropertyConversion.ConvertToType properly handles exceptions
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PropertyConversion_ConvertToType_HandlesIncompatibleTypes()
|
||||
{
|
||||
// Try to convert integer to AudioClip type
|
||||
var token = JToken.FromObject(12345);
|
||||
|
||||
// PropertyConversion.ConvertToType should either:
|
||||
// 1. Return a valid converted value
|
||||
// 2. Throw an exception that can be caught
|
||||
// 3. Return null
|
||||
|
||||
Exception thrownException = null;
|
||||
object result = null;
|
||||
|
||||
try
|
||||
{
|
||||
result = PropertyConversion.ConvertToType(token, typeof(AudioClip));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
thrownException = ex;
|
||||
}
|
||||
|
||||
// Document what actually happens
|
||||
if (thrownException != null)
|
||||
{
|
||||
Debug.Log($"PropertyConversion threw exception: {thrownException.GetType().Name}: {thrownException.Message}");
|
||||
Assert.Pass($"PropertyConversion threw {thrownException.GetType().Name} - exception is being raised, not swallowed");
|
||||
}
|
||||
else if (result == null)
|
||||
{
|
||||
Debug.Log("PropertyConversion returned null for incompatible type");
|
||||
Assert.Pass("PropertyConversion returned null for incompatible type");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"PropertyConversion returned unexpected result: {result}");
|
||||
Assert.Pass("PropertyConversion produced some result");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 7: TryConvertToType should never throw
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PropertyConversion_TryConvertToType_NeverThrows()
|
||||
{
|
||||
var token = JToken.FromObject(12345);
|
||||
|
||||
// This should never throw, only return null
|
||||
object result = null;
|
||||
Exception thrownException = null;
|
||||
|
||||
try
|
||||
{
|
||||
result = PropertyConversion.TryConvertToType(token, typeof(AudioClip));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
thrownException = ex;
|
||||
}
|
||||
|
||||
Assert.IsNull(thrownException, "TryConvertToType should never throw");
|
||||
// Result can be null or a value, but shouldn't throw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case 8: ComponentOps error handling
|
||||
/// Tests if ComponentOps.SetProperty properly catches exceptions
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ComponentOps_SetProperty_HandlesConversionErrors()
|
||||
{
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
var token = JToken.FromObject(12345);
|
||||
|
||||
// Try to set clip (AudioClip) to integer value
|
||||
bool success = ComponentOps.SetProperty(audioSource, "clip", token, out string error);
|
||||
|
||||
Assert.IsFalse(success, "Should fail to set incompatible type");
|
||||
Assert.IsNotEmpty(error, "Should provide error message");
|
||||
|
||||
// Verify the object is still in a valid state
|
||||
Assert.IsNotNull(audioSource, "AudioSource should still exist");
|
||||
Assert.IsNull(audioSource.clip, "Clip should remain null (not corrupted)");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 815e2cf338526014d93708b9070b7fc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// ISOLATED TEST: Array format [0, 0] for float property
|
||||
/// Issue #654 - Test 2 of 8
|
||||
/// This is the test that triggers "Error converting token to System.Single"
|
||||
/// </summary>
|
||||
public class PropertyConversion_ArrayForFloat_Test
|
||||
{
|
||||
private GameObject testGameObject;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
testGameObject = new GameObject("PropertyConversion_ArrayForFloat_Test");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (testGameObject != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(testGameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperty_ArrayForFloat_ReturnsError()
|
||||
{
|
||||
// Expect the error log that will be generated
|
||||
LogAssert.Expect(LogType.Error, new Regex("Error converting token to System.Single"));
|
||||
|
||||
var audioSource = testGameObject.AddComponent<AudioSource>();
|
||||
|
||||
// This is the exact error from issue #654
|
||||
var setPropertyParams = new JObject
|
||||
{
|
||||
["action"] = "set_property",
|
||||
["target"] = testGameObject.name,
|
||||
["componentType"] = "AudioSource",
|
||||
["property"] = "spatialBlend",
|
||||
["value"] = JArray.Parse("[0, 0]") // Array for float = error
|
||||
};
|
||||
|
||||
var result = ManageComponents.HandleCommand(setPropertyParams);
|
||||
Assert.IsNotNull(result, "Should return a result");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4b99eb57f53db948bd5e8a0a08dfec8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using static MCPForUnityTests.Editor.TestUtilities;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Tools
|
||||
{
|
||||
public class ReadConsoleTests
|
||||
{
|
||||
[Test]
|
||||
public void HandleCommand_Clear_Works()
|
||||
{
|
||||
// Arrange
|
||||
// Ensure there's something to clear
|
||||
Debug.Log("Log to clear");
|
||||
|
||||
// Verify content exists before clear
|
||||
var getBefore = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["types"] = new JArray { "error", "warning", "log" }, ["count"] = 10 }));
|
||||
Assert.IsTrue(getBefore.Value<bool>("success"), getBefore.ToString());
|
||||
var entriesBefore = getBefore["data"] as JArray;
|
||||
|
||||
// Ideally we'd assert count > 0, but other tests/system logs might affect this.
|
||||
// Just ensuring the call doesn't fail is a baseline, but let's try to be stricter if possible.
|
||||
// Since we just logged, there should be at least one entry.
|
||||
Assert.IsTrue(entriesBefore != null && entriesBefore.Count > 0, "Setup failed: console should have logs.");
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "clear" }));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
|
||||
// Verify clear effect
|
||||
var getAfter = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["types"] = new JArray { "error", "warning", "log" }, ["count"] = 10 }));
|
||||
Assert.IsTrue(getAfter.Value<bool>("success"), getAfter.ToString());
|
||||
var entriesAfter = getAfter["data"] as JArray;
|
||||
Assert.IsTrue(entriesAfter == null || entriesAfter.Count == 0, "Console should be empty after clear.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_Get_Works()
|
||||
{
|
||||
// Arrange
|
||||
string uniqueMessage = $"Test Log Message {Guid.NewGuid()}";
|
||||
Debug.Log(uniqueMessage);
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray { "error", "warning", "log" },
|
||||
["format"] = "detailed",
|
||||
["count"] = 1000 // Fetch enough to likely catch our message
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ToJObject(ReadConsole.HandleCommand(paramsObj));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JArray;
|
||||
Assert.IsNotNull(data, "Data array should not be null.");
|
||||
Assert.IsTrue(data.Count > 0, "Should retrieve at least one log entry.");
|
||||
|
||||
// Verify content
|
||||
bool found = false;
|
||||
foreach (var entry in data)
|
||||
{
|
||||
if (entry["message"]?.ToString().Contains(uniqueMessage) == true)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.IsTrue(found, $"The unique log message '{uniqueMessage}' was not found in retrieved logs.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandleCommand_Get_PreservesMultilineMessageBody()
|
||||
{
|
||||
string id = Guid.NewGuid().ToString();
|
||||
string firstLine = $"First line {id}";
|
||||
string secondLine = $"Second line {id}";
|
||||
Debug.Log($"{firstLine}\n\n{secondLine}");
|
||||
|
||||
var paramsObj = new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray { "error", "warning", "log" },
|
||||
["format"] = "detailed",
|
||||
["count"] = 1000
|
||||
};
|
||||
|
||||
var result = ToJObject(ReadConsole.HandleCommand(paramsObj));
|
||||
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
||||
var data = result["data"] as JArray;
|
||||
Assert.IsNotNull(data, "Data array should not be null.");
|
||||
|
||||
string message = null;
|
||||
foreach (var entry in data)
|
||||
{
|
||||
string candidate = entry["message"]?.ToString();
|
||||
if (candidate != null && candidate.Contains(firstLine))
|
||||
{
|
||||
message = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.IsNotNull(message, "Multi-line log entry was not found.");
|
||||
StringAssert.Contains($"{firstLine}\n\n{secondLine}", message);
|
||||
StringAssert.DoesNotContain("UnityEngine.Debug", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user