// TestRunnerNoThrottle.cs // Sets Unity Editor to "No Throttling" mode during test runs. // This helps tests that don't trigger compilation run smoothly in the background. // Note: Tests that trigger mid-run compilation may still stall due to OS-level throttling. using System; using System.Reflection; using MCPForUnity.Editor.Helpers; using UnityEditor; using UnityEditor.TestTools.TestRunner.Api; using UnityEngine; namespace MCPForUnity.Editor.Services { /// /// Automatically sets the editor to "No Throttling" mode during test runs. /// /// This helps prevent background stalls for normal tests. However, tests that trigger /// script compilation mid-run may still stall because: /// - Internal Unity coroutine waits rely on editor ticks /// - OS-level throttling affects the main thread when Unity is backgrounded /// - No amount of internal nudging can overcome OS thread scheduling /// /// The MCP workflow is unaffected because socket messages provide external stimulus /// that wakes Unity's main thread. /// [InitializeOnLoad] public static class TestRunnerNoThrottle { private const string ApplicationIdleTimeKey = "ApplicationIdleTime"; private const string InteractionModeKey = "InteractionMode"; // SessionState keys to persist across domain reload private const string SessionKey_TestRunActive = "TestRunnerNoThrottle_TestRunActive"; private const string SessionKey_PrevIdleTime = "TestRunnerNoThrottle_PrevIdleTime"; private const string SessionKey_PrevInteractionMode = "TestRunnerNoThrottle_PrevInteractionMode"; private const string SessionKey_SettingsCaptured = "TestRunnerNoThrottle_SettingsCaptured"; // Keep reference to avoid GC and set HideFlags to avoid serialization issues private static TestRunnerApi _api; static TestRunnerNoThrottle() { try { _api = ScriptableObject.CreateInstance(); _api.hideFlags = HideFlags.HideAndDontSave; _api.RegisterCallbacks(new TestCallbacks()); // Check if recovering from domain reload during an active test run if (IsTestRunActive()) { McpLog.Info("[TestRunnerNoThrottle] Recovered from domain reload - reapplying No Throttling."); ApplyNoThrottling(); } } catch (Exception e) { McpLog.Warn($"[TestRunnerNoThrottle] Failed to register callbacks: {e}"); } } #region State Persistence private static bool IsTestRunActive() => SessionState.GetBool(SessionKey_TestRunActive, false); private static void SetTestRunActive(bool active) => SessionState.SetBool(SessionKey_TestRunActive, active); private static bool AreSettingsCaptured() => SessionState.GetBool(SessionKey_SettingsCaptured, false); private static void SetSettingsCaptured(bool captured) => SessionState.SetBool(SessionKey_SettingsCaptured, captured); private static int GetPrevIdleTime() => SessionState.GetInt(SessionKey_PrevIdleTime, 4); private static void SetPrevIdleTime(int value) => SessionState.SetInt(SessionKey_PrevIdleTime, value); private static int GetPrevInteractionMode() => SessionState.GetInt(SessionKey_PrevInteractionMode, 0); private static void SetPrevInteractionMode(int value) => SessionState.SetInt(SessionKey_PrevInteractionMode, value); #endregion /// /// Apply no-throttling preemptively before tests start. /// Call this before Execute() for PlayMode tests to ensure Unity isn't throttled /// during the Play mode transition (before RunStarted fires). /// public static void ApplyNoThrottlingPreemptive() { SetTestRunActive(true); ApplyNoThrottling(); } private static void ApplyNoThrottling() { if (!AreSettingsCaptured()) { SetPrevIdleTime(EditorPrefs.GetInt(ApplicationIdleTimeKey, 4)); SetPrevInteractionMode(EditorPrefs.GetInt(InteractionModeKey, 0)); SetSettingsCaptured(true); } // 0ms idle + InteractionMode=1 (No Throttling) EditorPrefs.SetInt(ApplicationIdleTimeKey, 0); EditorPrefs.SetInt(InteractionModeKey, 1); ForceEditorToApplyInteractionPrefs(); McpLog.Info("[TestRunnerNoThrottle] Applied No Throttling for test run."); } private static void RestoreThrottling() { if (!AreSettingsCaptured()) return; EditorPrefs.SetInt(ApplicationIdleTimeKey, GetPrevIdleTime()); EditorPrefs.SetInt(InteractionModeKey, GetPrevInteractionMode()); ForceEditorToApplyInteractionPrefs(); SetSettingsCaptured(false); SetTestRunActive(false); McpLog.Info("[TestRunnerNoThrottle] Restored Interaction Mode after test run."); } private static void ForceEditorToApplyInteractionPrefs() { try { var method = typeof(EditorApplication).GetMethod( "UpdateInteractionModeSettings", BindingFlags.Static | BindingFlags.NonPublic ); method?.Invoke(null, null); } catch { // Ignore reflection errors } } private sealed class TestCallbacks : ICallbacks { public void RunStarted(ITestAdaptor testsToRun) { SetTestRunActive(true); ApplyNoThrottling(); } public void RunFinished(ITestResultAdaptor result) { RestoreThrottling(); } public void TestStarted(ITestAdaptor test) { } public void TestFinished(ITestResultAdaptor result) { } } } }