unity-mcp/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs

140 lines
5.6 KiB
C#
Raw Normal View History

Async Test Infrastructure & Editor Readiness Status + new refresh_unity tool (#507) * Add editor readiness v2, refresh tool, and preflight guards * Detect external package changes and harden refresh retry * feat: add TestRunnerNoThrottle and async test running with background stall prevention - Add TestRunnerNoThrottle.cs: Sets editor to 'No Throttling' mode during test runs with SessionState persistence across domain reload - Add run_tests_async and get_test_job tools for non-blocking test execution - Add TestJobManager for async test job tracking with progress monitoring - Add ForceSynchronousImport to all AssetDatabase.Refresh() calls to prevent stalls - Mark DomainReloadResilienceTests as [Explicit] with documentation explaining the test infrastructure limitation (internal coroutine waits vs MCP socket polling) - MCP workflow is unaffected - socket messages provide external stimulus that keeps Unity responsive even when backgrounded * refactor: simplify and clean up code - Remove unused Newtonsoft.Json.Linq import from TestJobManager - Add throttling to SessionState persistence (once per second) to reduce overhead - Critical job state changes (start/finish) still persist immediately - Fix duplicate XML summary tag in DomainReloadResilienceTests * docs: add async test tools to README, document domain reload limitation - Add run_tests_async and get_test_job to main README tools list - Document background stall limitation for domain reload tests in DEV readme * ci: add separate job for domain reload tests Run [Explicit] domain_reload tests in their own job using -testCategory * ci: run domain reload tests in same job as regular tests Combines into single job with two test steps to reuse cached Library * fix: address coderabbit review issues - Fix TOCTOU race in TestJobManager.StartJob (single lock scope for check-and-set) - Store TestRunnerApi reference with HideAndDontSave to prevent GC/serialization issues * docs: update tool descriptions to prefer run_tests_async - run_tests_async is now marked as preferred for long-running suites - run_tests description notes it blocks and suggests async alternative * docs: update README screenshot to v8.6 UI * docs: add v8.6 UI screenshot * Update README for MCP version and instructions for v8.7 * fix: handle preflight busy signals and derive job status from test results - manage_asset, manage_gameobject, manage_scene now check preflight return value and propagate busy/retry signals to clients (fixes Sourcery #1) - TestJobManager.FinalizeCurrentJobFromRunFinished now sets job status to Failed when resultPayload.Failed > 0, not always Succeeded (fixes Sourcery #2) * fix: increase HTTP server startup timeout for dev mode When 'Force fresh server install' is enabled, uvx uses --no-cache --refresh which rebuilds the package and takes significantly longer to start. - Increase timeout from 10s to 45s when dev mode is enabled - Add informative log message explaining the longer startup time - Show actual timeout value in warning message * fix: derive job status from test results in FinalizeFromTask fallback Apply same logic as FinalizeCurrentJobFromRunFinished: check result.Failed > 0 to correctly mark jobs as Failed when tests fail, even in the fallback path when RunFinished callback is not delivered.
2026-01-04 04:42:32 +08:00
// 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
{
/// <summary>
/// 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.
/// </summary>
[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<TestRunnerApi>();
_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
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) { }
}
}
}