Harden `manage_scriptable_object` Tool (#522)
* feat(manage_scriptable_object): harden tool with path normalization, auto-resize, bulk mapping
Phase 1: Path Syntax & Auto-Resizing
- Add NormalizePropertyPath() to convert field[index] to Array.data format
- Add EnsureArrayCapacity() to auto-grow arrays when targeting out-of-bounds indices
Phase 2: Consolidation
- Replace duplicate TryGet* helpers with ParamCoercion/VectorParsing shared utilities
- Add Vector4 parsing support to VectorParsing.cs
Phase 3: Bulk Data Mapping
- Handle JArray values for list/array properties (recursive element setting)
- Handle JObject values for nested struct/class properties
Phase 4: Enhanced Reference Resolution
- Support plain 32-char GUID strings for ObjectReference fields
Phase 5: Validation & Dry-Run
- Add ValidatePatches() for pre-validation of all patches
- Add dry_run parameter to validate without mutating
Includes comprehensive stress test suite covering:
- Big Bang (large nested arrays), Out of Bounds, Friendly Path Syntax
- Deep Nesting, Mixed References, Rapid Fire, Type Mismatch
- Bulk Array Mapping, GUID Shorthand, Dry Run validation
* feat: Add AnimationCurve and Quaternion support to manage_scriptable_object tool
- Implement TrySetAnimationCurve() supporting both {'keys': [...]} and direct [...] formats
* Support keyframe properties: time, value, inSlope, outSlope, weightedMode, inWeight, outWeight
* Gracefully default missing optional fields to 0
* Clear error messages for malformed structures
- Implement TrySetQuaternion() with 4 input formats:
* Euler array [x, y, z] - 3 elements interpreted as degrees
* Raw array [x, y, z, w] - 4 components
* Object format {x, y, z, w} - explicit components
* Explicit euler {euler: [x, y, z]} - labeled format
- Improve error handling:
* Null values: AnimationCurve→empty, Quaternion→identity
* Invalid inputs rejected with specific, actionable error messages
* Validate keyframe objects and array sizes
- Add comprehensive test coverage in ManageScriptableObjectStressTests.cs:
* AnimationCurve with keyframe array format
* AnimationCurve with direct array (no wrapper)
* Quaternion via Euler angles
* Quaternion via raw components
* Quaternion via object format
* Quaternion via explicit euler property
- Fix test file compilation issues:
* Replace undefined TestFolder with _runRoot
* Add System.IO using statement
* refactor: consolidate test utilities to eliminate duplication
- Add TestUtilities.cs with shared helpers:
- ToJObject() - consolidates 11 duplicates across test files
- EnsureFolder() - consolidates 2 duplicates
- WaitForUnityReady() - consolidates 2 duplicates
- FindFallbackShader() - consolidates shader chain duplicates
- SafeDeleteAsset() - helper for asset cleanup
- CleanupEmptyParentFolders() - standardizes TearDown cleanup
- Update 11 test files to use shared TestUtilities via 'using static'
- Standardize TearDown cleanup patterns across all test files
- Net reduction of ~40 lines while improving maintainability
* fix: add missing animCurve and rotation fields to ComplexStressSO
Add AnimationCurve and Quaternion fields required by Phase 6 stress tests.
main
parent
3090a014fb
commit
552b2d3aae
|
|
@ -97,6 +97,49 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a JToken (array or object) into a Vector4.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The JSON token to parse</param>
|
||||||
|
/// <returns>The parsed Vector4 or null if parsing fails</returns>
|
||||||
|
public static Vector4? ParseVector4(JToken token)
|
||||||
|
{
|
||||||
|
if (token == null || token.Type == JTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Array format: [x, y, z, w]
|
||||||
|
if (token is JArray array && array.Count >= 4)
|
||||||
|
{
|
||||||
|
return new Vector4(
|
||||||
|
array[0].ToObject<float>(),
|
||||||
|
array[1].ToObject<float>(),
|
||||||
|
array[2].ToObject<float>(),
|
||||||
|
array[3].ToObject<float>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object format: {x: 1, y: 2, z: 3, w: 4}
|
||||||
|
if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") &&
|
||||||
|
obj.ContainsKey("z") && obj.ContainsKey("w"))
|
||||||
|
{
|
||||||
|
return new Vector4(
|
||||||
|
obj["x"].ToObject<float>(),
|
||||||
|
obj["y"].ToObject<float>(),
|
||||||
|
obj["z"].ToObject<float>(),
|
||||||
|
obj["w"].ToObject<float>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[VectorParsing] Failed to parse Vector4 from '{token}': {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a JToken (array or object) into a Quaternion.
|
/// Parses a JToken (array or object) into a Quaternion.
|
||||||
/// Supports both euler angles [x, y, z] and quaternion components [x, y, z, w].
|
/// Supports both euler angles [x, y, z] and quaternion components [x, y, z, w].
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -41,6 +41,8 @@ async def manage_scriptable_object(
|
||||||
target: Annotated[dict[str, Any] | str | None, "Target asset reference {guid|path} (for modify)."] = None,
|
target: Annotated[dict[str, Any] | str | None, "Target asset reference {guid|path} (for modify)."] = None,
|
||||||
# --- shared ---
|
# --- shared ---
|
||||||
patches: Annotated[list[dict[str, Any]] | str | None, "Patch list (or JSON string) to apply."] = None,
|
patches: Annotated[list[dict[str, Any]] | str | None, "Patch list (or JSON string) to apply."] = None,
|
||||||
|
# --- validation ---
|
||||||
|
dry_run: Annotated[bool | str | None, "If true, validate patches without applying (modify only)."] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
unity_instance = get_unity_instance_from_context(ctx)
|
unity_instance = get_unity_instance_from_context(ctx)
|
||||||
|
|
||||||
|
|
@ -62,6 +64,7 @@ async def manage_scriptable_object(
|
||||||
"overwrite": coerce_bool(overwrite, default=None),
|
"overwrite": coerce_bool(overwrite, default=None),
|
||||||
"target": parsed_target,
|
"target": parsed_target,
|
||||||
"patches": parsed_patches,
|
"patches": parsed_patches,
|
||||||
|
"dryRun": coerce_bool(dry_run, default=None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove None values to keep Unity handler simpler
|
# Remove None values to keep Unity handler simpler
|
||||||
|
|
|
||||||
|
|
@ -69,4 +69,62 @@ def test_manage_scriptable_object_forwards_modify_params(monkeypatch):
|
||||||
assert captured["params"]["patches"][0]["op"] == "array_resize"
|
assert captured["params"]["patches"][0]["op"] == "array_resize"
|
||||||
|
|
||||||
|
|
||||||
|
def test_manage_scriptable_object_forwards_dry_run_param(monkeypatch):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
async def fake_async_send(cmd, params, **kwargs):
|
||||||
|
captured["cmd"] = cmd
|
||||||
|
captured["params"] = params
|
||||||
|
return {"success": True, "data": {"dryRun": True, "validationResults": []}}
|
||||||
|
|
||||||
|
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
|
||||||
|
|
||||||
|
ctx = DummyContext()
|
||||||
|
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
mod.manage_scriptable_object(
|
||||||
|
ctx=ctx,
|
||||||
|
action="modify",
|
||||||
|
target='{"guid":"abc123"}',
|
||||||
|
patches=[{"propertyPath": "intValue", "op": "set", "value": 42}],
|
||||||
|
dry_run=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert captured["cmd"] == "manage_scriptable_object"
|
||||||
|
assert captured["params"]["action"] == "modify"
|
||||||
|
assert captured["params"]["dryRun"] is True
|
||||||
|
assert captured["params"]["target"] == {"guid": "abc123"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_manage_scriptable_object_dry_run_string_coercion(monkeypatch):
|
||||||
|
"""Test that dry_run accepts string 'true' and coerces to boolean."""
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
async def fake_async_send(cmd, params, **kwargs):
|
||||||
|
captured["cmd"] = cmd
|
||||||
|
captured["params"] = params
|
||||||
|
return {"success": True, "data": {"dryRun": True}}
|
||||||
|
|
||||||
|
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
|
||||||
|
|
||||||
|
ctx = DummyContext()
|
||||||
|
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
mod.manage_scriptable_object(
|
||||||
|
ctx=ctx,
|
||||||
|
action="modify",
|
||||||
|
target={"guid": "xyz"},
|
||||||
|
patches=[],
|
||||||
|
dry_run="true", # String instead of bool
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert captured["params"]["dryRun"] is True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MCPForUnityTests.Editor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared test utilities for EditMode tests across the MCP for Unity test suite.
|
||||||
|
/// Consolidates common patterns to avoid duplication across test files.
|
||||||
|
/// </summary>
|
||||||
|
public static class TestUtilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Safely converts a command result to JObject, handling both JSON objects and other types.
|
||||||
|
/// Returns an empty JObject if result is null.
|
||||||
|
/// </summary>
|
||||||
|
public static JObject ToJObject(object result)
|
||||||
|
{
|
||||||
|
if (result == null) return new JObject();
|
||||||
|
return result as JObject ?? JObject.FromObject(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates all parent directories for the given asset path if they don't exist.
|
||||||
|
/// Handles normalization and validates against dangerous patterns.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderPath">An Assets-relative folder path (e.g., "Assets/Temp/MyFolder")</param>
|
||||||
|
public static void EnsureFolder(string folderPath)
|
||||||
|
{
|
||||||
|
if (AssetDatabase.IsValidFolder(folderPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sanitized = MCPForUnity.Editor.Helpers.AssetPathUtility.SanitizeAssetPath(folderPath);
|
||||||
|
if (string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parts = sanitized.Split('/');
|
||||||
|
string current = "Assets";
|
||||||
|
for (int i = 1; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
var next = current + "/" + parts[i];
|
||||||
|
if (!AssetDatabase.IsValidFolder(next))
|
||||||
|
{
|
||||||
|
AssetDatabase.CreateFolder(current, parts[i]);
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for Unity to finish compiling and updating, with a configurable timeout.
|
||||||
|
/// Some EditMode tests trigger script compilation/domain reload.
|
||||||
|
/// Tools intentionally return "compiling_or_reloading" during these windows.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeoutSeconds">Maximum time to wait before failing the test.</param>
|
||||||
|
public static IEnumerator WaitForUnityReady(double timeoutSeconds = 30.0)
|
||||||
|
{
|
||||||
|
double start = EditorApplication.timeSinceStartup;
|
||||||
|
while (EditorApplication.isCompiling || EditorApplication.isUpdating)
|
||||||
|
{
|
||||||
|
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
|
||||||
|
{
|
||||||
|
Assert.Fail($"Timed out waiting for Unity to finish compiling/updating (>{timeoutSeconds:0.0}s).");
|
||||||
|
}
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a fallback shader for creating materials in tests.
|
||||||
|
/// Tries modern pipelines first, then falls back to Standard/Unlit.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A shader suitable for test materials, or null if none found.</returns>
|
||||||
|
public static Shader FindFallbackShader()
|
||||||
|
{
|
||||||
|
return Shader.Find("Universal Render Pipeline/Lit")
|
||||||
|
?? Shader.Find("HDRP/Lit")
|
||||||
|
?? Shader.Find("Standard")
|
||||||
|
?? Shader.Find("Unlit/Color");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safely deletes an asset if it exists.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The asset path to delete.</param>
|
||||||
|
public static void SafeDeleteAsset(string path)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(path) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
|
||||||
|
{
|
||||||
|
AssetDatabase.DeleteAsset(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up empty parent folders recursively up to but not including "Assets".
|
||||||
|
/// Useful in TearDown to avoid leaving folder debris.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderPath">The starting folder path to check.</param>
|
||||||
|
public static void CleanupEmptyParentFolders(string folderPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(folderPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
|
||||||
|
while (!string.IsNullOrEmpty(parent) && parent != "Assets")
|
||||||
|
{
|
||||||
|
if (AssetDatabase.IsValidFolder(parent))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dirs = Directory.GetDirectories(parent);
|
||||||
|
var files = Directory.GetFiles(parent);
|
||||||
|
if (dirs.Length == 0 && files.Length == 0)
|
||||||
|
{
|
||||||
|
AssetDatabase.DeleteAsset(parent);
|
||||||
|
parent = Path.GetDirectoryName(parent)?.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4bf7029284fff48b6a5e003e262ab5c8
|
||||||
|
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:
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
[CreateAssetMenu(fileName = "ArrayStressSO", menuName = "StressTests/ArrayStressSO")]
|
||||||
|
public class ArrayStressSO : ScriptableObject
|
||||||
|
{
|
||||||
|
public float[] floatArray = new float[3];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9eec250e4deee48c69c12acfde8c2adc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a73b1ff3fe1fa4b3fadb70c7c257d5a8
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cc8fe16aef3ae4cbc949300f5fed2187
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -6,10 +6,10 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using MCPForUnity.Editor.Resources.Scene;
|
using MCPForUnity.Editor.Resources.Scene;
|
||||||
using UnityEngine.TestTools;
|
using UnityEngine.TestTools;
|
||||||
using Debug = UnityEngine.Debug;
|
using Debug = UnityEngine.Debug;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -51,21 +51,6 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
return go;
|
return go;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
if (result == null) return new JObject();
|
|
||||||
if (result is JObject jobj) return jobj;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JObject.FromObject(result);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[ToJObject] Failed to convert result: {ex.Message}");
|
|
||||||
return new JObject { ["error"] = ex.Message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Bulk GameObject Creation
|
#region Bulk GameObject Creation
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -33,11 +34,9 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
AssetDatabase.DeleteAsset(TempRoot);
|
AssetDatabase.DeleteAsset(TempRoot);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
// Clean up empty parent folders to avoid debris
|
||||||
{
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -35,11 +36,9 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
AssetDatabase.DeleteAsset(TempRoot);
|
AssetDatabase.DeleteAsset(TempRoot);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
// Clean up empty parent folders to avoid debris
|
||||||
{
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -52,21 +52,8 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.DeleteAsset(TempRoot);
|
AssetDatabase.DeleteAsset(TempRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up parent Temp folder if it's empty
|
// Clean up empty parent folders to avoid debris
|
||||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
{
|
|
||||||
var remainingDirs = Directory.GetDirectories("Assets/Temp");
|
|
||||||
var remainingFiles = Directory.GetFiles("Assets/Temp");
|
|
||||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
|
||||||
{
|
|
||||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -43,21 +43,8 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.DeleteAsset(TempRoot);
|
AssetDatabase.DeleteAsset(TempRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up parent Temp folder if it's empty
|
// Clean up empty parent folders to avoid debris
|
||||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
{
|
|
||||||
// Only delete if empty
|
|
||||||
var subFolders = AssetDatabase.GetSubFolders("Assets/Temp");
|
|
||||||
if (subFolders.Length == 0)
|
|
||||||
{
|
|
||||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using UnityEditor;
|
||||||
using UnityEditor.SceneManagement;
|
using UnityEditor.SceneManagement;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools.Prefabs;
|
using MCPForUnity.Editor.Tools.Prefabs;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -30,16 +31,8 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.DeleteAsset(TempDirectory);
|
AssetDatabase.DeleteAsset(TempDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up parent Temp folder if it's empty
|
// Clean up empty parent folders to avoid debris
|
||||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
CleanupEmptyParentFolders(TempDirectory);
|
||||||
{
|
|
||||||
var remainingDirs = Directory.GetDirectories("Assets/Temp");
|
|
||||||
var remainingFiles = Directory.GetFiles("Assets/Temp");
|
|
||||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
|
||||||
{
|
|
||||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -252,10 +245,5 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests");
|
AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2b53f7e50a801437e83e646cf00effed
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -5,9 +5,9 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.TestTools;
|
using UnityEngine.TestTools;
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using MCPForUnityTests.Editor.Tools.Fixtures;
|
using MCPForUnityTests.Editor.Tools.Fixtures;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -41,10 +41,7 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
// Create two Materials we can reference by guid/path.
|
// Create two Materials we can reference by guid/path.
|
||||||
_matAPath = $"{TempRoot}/MatA_{Guid.NewGuid():N}.mat";
|
_matAPath = $"{TempRoot}/MatA_{Guid.NewGuid():N}.mat";
|
||||||
_matBPath = $"{TempRoot}/MatB_{Guid.NewGuid():N}.mat";
|
_matBPath = $"{TempRoot}/MatB_{Guid.NewGuid():N}.mat";
|
||||||
var shader = Shader.Find("Universal Render Pipeline/Lit")
|
var shader = FindFallbackShader();
|
||||||
?? Shader.Find("HDRP/Lit")
|
|
||||||
?? Shader.Find("Standard")
|
|
||||||
?? Shader.Find("Unlit/Color");
|
|
||||||
Assert.IsNotNull(shader, "A fallback shader must be available for creating Material assets in tests.");
|
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), _matAPath);
|
||||||
AssetDatabase.CreateAsset(new Material(shader), _matBPath);
|
AssetDatabase.CreateAsset(new Material(shader), _matBPath);
|
||||||
|
|
@ -75,16 +72,8 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.DeleteAsset(_runRoot);
|
AssetDatabase.DeleteAsset(_runRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up parent Temp folder if empty
|
// Clean up empty parent folders to avoid debris
|
||||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
{
|
|
||||||
var remainingDirs = System.IO.Directory.GetDirectories("Assets/Temp");
|
|
||||||
var remainingFiles = System.IO.Directory.GetFiles("Assets/Temp");
|
|
||||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
|
||||||
{
|
|
||||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetDatabase.Refresh();
|
AssetDatabase.Refresh();
|
||||||
}
|
}
|
||||||
|
|
@ -312,50 +301,6 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
$"Expected sanitized Assets-rooted path, got: {path}");
|
$"Expected sanitized Assets-rooted path, got: {path}");
|
||||||
Assert.IsFalse(path.Contains("//", StringComparison.Ordinal), $"Path should not contain double slashes: {path}");
|
Assert.IsFalse(path.Contains("//", StringComparison.Ordinal), $"Path should not contain double slashes: {path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureFolder(string folderPath)
|
|
||||||
{
|
|
||||||
if (AssetDatabase.IsValidFolder(folderPath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Only used for Assets/... paths in tests.
|
|
||||||
var sanitized = AssetPathUtility.SanitizeAssetPath(folderPath);
|
|
||||||
if (string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parts = sanitized.Split('/');
|
|
||||||
string current = "Assets";
|
|
||||||
for (int i = 1; i < parts.Length; i++)
|
|
||||||
{
|
|
||||||
var next = current + "/" + parts[i];
|
|
||||||
if (!AssetDatabase.IsValidFolder(next))
|
|
||||||
{
|
|
||||||
AssetDatabase.CreateFolder(current, parts[i]);
|
|
||||||
}
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerator WaitForUnityReady(double timeoutSeconds = 30.0)
|
|
||||||
{
|
|
||||||
// Some EditMode tests trigger script compilation/domain reload. Tools like ManageScriptableObject
|
|
||||||
// intentionally return "compiling_or_reloading" during these windows. Wait until Unity is stable
|
|
||||||
// to make tests deterministic.
|
|
||||||
double start = EditorApplication.timeSinceStartup;
|
|
||||||
while (EditorApplication.isCompiling || EditorApplication.isUpdating)
|
|
||||||
{
|
|
||||||
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
|
|
||||||
{
|
|
||||||
Assert.Fail($"Timed out waiting for Unity to finish compiling/updating (>{timeoutSeconds:0.0}s).");
|
|
||||||
}
|
|
||||||
yield return null; // yield to the editor loop so importing/compiling can actually progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -57,16 +58,8 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.DeleteAsset(TempRoot);
|
AssetDatabase.DeleteAsset(TempRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up parent Temp folder if it's empty
|
// Clean up empty parent folders to avoid debris
|
||||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
{
|
|
||||||
var remainingDirs = Directory.GetDirectories("Assets/Temp");
|
|
||||||
var remainingFiles = Directory.GetFiles("Assets/Temp");
|
|
||||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
|
||||||
{
|
|
||||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetDatabase.Refresh();
|
AssetDatabase.Refresh();
|
||||||
}
|
}
|
||||||
|
|
@ -99,11 +92,6 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
|
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -62,25 +63,12 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
AssetDatabase.DeleteAsset(TempRoot);
|
AssetDatabase.DeleteAsset(TempRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up parent Temp folder if it's empty
|
// Clean up empty parent folders to avoid debris
|
||||||
if (AssetDatabase.IsValidFolder("Assets/Temp"))
|
CleanupEmptyParentFolders(TempRoot);
|
||||||
{
|
|
||||||
var remainingDirs = Directory.GetDirectories("Assets/Temp");
|
|
||||||
var remainingFiles = Directory.GetFiles("Assets/Temp");
|
|
||||||
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
|
|
||||||
{
|
|
||||||
AssetDatabase.DeleteAsset("Assets/Temp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetDatabase.Refresh();
|
AssetDatabase.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void CreateMaterial_WithObjectProperties_SucceedsAndSetsColor()
|
public void CreateMaterial_WithObjectProperties_SucceedsAndSetsColor()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using static MCPForUnityTests.Editor.TestUtilities;
|
||||||
|
|
||||||
namespace MCPForUnityTests.Editor.Tools
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -77,16 +75,5 @@ namespace MCPForUnityTests.Editor.Tools
|
||||||
}
|
}
|
||||||
Assert.IsTrue(found, $"The unique log message '{uniqueMessage}' was not found in retrieved logs.");
|
Assert.IsTrue(found, $"The unique log message '{uniqueMessage}' was not found in retrieved logs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JObject ToJObject(object result)
|
|
||||||
{
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
Assert.Fail("ReadConsole.HandleCommand returned null.");
|
|
||||||
return new JObject(); // Unreachable, but satisfies return type.
|
|
||||||
}
|
|
||||||
|
|
||||||
return result as JObject ?? JObject.FromObject(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue