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

151 lines
6.0 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
Fix PlayMode tests stalling when unfocused (python refresh utility), improve domain reload recovery and refresh tool (#554) * Fix test job state management after domain reload - TestRunnerService.RunFinished: Always clean up job state even when _runCompletionSource is null (happens after PlayMode domain reload) - TestJobManager: Detect and clear stale jobs (5+ min without updates) on startup to recover from stuck state after domain reload - refresh_unity.py: Add "could not connect" to retryable errors when wait_for_ready=True, so connection failures during domain reload trigger waiting instead of immediate failure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add focus nudge to handle OS-level throttling during PlayMode tests When Unity is unfocused, macOS App Nap (and similar OS features) can throttle the process, causing PlayMode tests to stall even with Unity No Throttling mode enabled. Changes: - Add ApplyNoThrottlingPreemptive() to TestRunnerNoThrottle for early throttle prevention before PlayMode Execute() - Add focus_nudge.py utility that temporarily focuses Unity and returns focus to the original app (supports macOS, Windows, Linux) - Integrate focus nudge into get_test_job polling - when tests appear stalled (unfocused + no progress for 10s), automatically nudge Unity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix code review issues in focus_nudge.py - Remove redundant time import (already imported at module level) - Escape window titles in PowerShell script to prevent injection - Remove unused Callable import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Improve focus nudge logging and fix skipped tests - Improve logging in focus_nudge.py: rate limit skip and focus return at INFO level - Improve logging in run_tests.py: show nudge completion status - Fix path resolution in test_logging_stdout.py and test_transport_framing.py - Add PlayMode tests to UnityMCPTests project for testing PlayMode runner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add troubleshooting note about focus permission requests When running PlayMode tests with Unity in the background, the focus nudge feature may trigger OS permission prompts (especially on macOS for accessibility/automation). Document this expected behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:02:40 +08:00
/// <summary>
/// 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).
/// </summary>
public static void ApplyNoThrottlingPreemptive()
{
SetTestRunActive(true);
ApplyNoThrottling();
}
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
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) { }
}
}
}