unity-mcp/TestProjects/UnityMCPTests/Assets/Tests/EditMode/TestUtilities.cs

141 lines
5.2 KiB
C#
Raw Normal View History

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.
2026-01-07 22:46:35 +08:00
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;
}
}
}
}
}