using System; using System.Collections; using System.IO; using Newtonsoft.Json.Linq; using NUnit.Framework; using UnityEditor; using UnityEngine; namespace MCPForUnityTests.Editor { /// /// Shared test utilities for EditMode tests across the MCP for Unity test suite. /// Consolidates common patterns to avoid duplication across test files. /// public static class TestUtilities { /// /// Safely converts a command result to JObject, handling both JSON objects and other types. /// Returns an empty JObject if result is null. /// public static JObject ToJObject(object result) { if (result == null) return new JObject(); return result as JObject ?? JObject.FromObject(result); } /// /// Creates all parent directories for the given asset path if they don't exist. /// Handles normalization and validates against dangerous patterns. /// /// An Assets-relative folder path (e.g., "Assets/Temp/MyFolder") 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; } } /// /// 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. /// /// Maximum time to wait before failing the test. 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; } } /// /// Finds a fallback shader for creating materials in tests. /// Tries modern pipelines first, then falls back to Standard/Unlit. /// /// A shader suitable for test materials, or null if none found. public static Shader FindFallbackShader() { return Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("HDRP/Lit") ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color"); } /// /// Safely deletes an asset if it exists. /// /// The asset path to delete. public static void SafeDeleteAsset(string path) { if (!string.IsNullOrEmpty(path) && AssetDatabase.LoadAssetAtPath(path) != null) { AssetDatabase.DeleteAsset(path); } } /// /// Cleans up empty parent folders recursively up to but not including "Assets". /// Useful in TearDown to avoid leaving folder debris. /// /// The starting folder path to check. 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; } } } } }