From 932b2a6a48dd85413b394825f79688457fa5fd13 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Tue, 25 Nov 2025 16:28:55 -0400 Subject: [PATCH 1/2] Harden PlayMode test runs - Guard against starting tests while already in Play Mode. - Pre-save dirty scenes before PlayMode runs to avoid SaveModifiedSceneTask failures. - Temporarily disable domain reload during PlayMode tests to keep the MCP bridge alive; restore settings afterward. - Avoid runSynchronously because it can freeze Unity --- .../Editor/Services/TestRunnerService.cs | 96 ++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs b/MCPForUnity/Editor/Services/TestRunnerService.cs index 60411278..f1ec64fb 100644 --- a/MCPForUnity/Editor/Services/TestRunnerService.cs +++ b/MCPForUnity/Editor/Services/TestRunnerService.cs @@ -5,8 +5,10 @@ using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using UnityEditor; +using UnityEditor.SceneManagement; using UnityEditor.TestTools.TestRunner.Api; using UnityEngine; +using UnityEngine.SceneManagement; namespace MCPForUnity.Editor.Services { @@ -31,7 +33,7 @@ public TestRunnerService() public async Task>> GetTestsAsync(TestMode? mode) { - await _operationLock.WaitAsync().ConfigureAwait(false); + await _operationLock.WaitAsync().ConfigureAwait(true); try { var modes = mode.HasValue ? new[] { mode.Value } : AllModes; @@ -58,8 +60,11 @@ public async Task>> GetTestsAsync(TestM public async Task RunTestsAsync(TestMode mode) { - await _operationLock.WaitAsync().ConfigureAwait(false); + await _operationLock.WaitAsync().ConfigureAwait(true); Task runTask; + bool adjustedPlayModeOptions = false; + bool originalEnterPlayModeOptionsEnabled = false; + EnterPlayModeOptions originalEnterPlayModeOptions = EnterPlayModeOptions.None; try { if (_runCompletionSource != null && !_runCompletionSource.Task.IsCompleted) @@ -67,16 +72,47 @@ public async Task RunTestsAsync(TestMode mode) throw new InvalidOperationException("A Unity test run is already in progress."); } + if (EditorApplication.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode) + { + throw new InvalidOperationException("Cannot start a test run while the Editor is in or entering Play Mode. Stop Play Mode and try again."); + } + + if (mode == TestMode.PlayMode) + { + // PlayMode runs transition the editor into play across multiple update ticks. Unity's + // built-in pipeline schedules SaveModifiedSceneTask early, but that task uses + // EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo which throws once play mode is + // active. To minimize that window we pre-save dirty scenes and disable domain reload (so the + // MCP bridge stays alive). We do NOT force runSynchronously here because that can freeze the + // editor in some projects. If the TestRunner still hits the save task after entering play, the + // run can fail; in that case, rerun from a clean Edit Mode state. + adjustedPlayModeOptions = EnsurePlayModeRunsWithoutDomainReload( + out originalEnterPlayModeOptionsEnabled, + out originalEnterPlayModeOptions); + } + _leafResults.Clear(); _runCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var filter = new Filter { testMode = mode }; - _testRunnerApi.Execute(new ExecutionSettings(filter)); + var settings = new ExecutionSettings(filter); + + if (mode == TestMode.PlayMode) + { + SaveDirtyScenesIfNeeded(); + } + + _testRunnerApi.Execute(settings); runTask = _runCompletionSource.Task; } catch { + if (adjustedPlayModeOptions) + { + RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions); + } + _operationLock.Release(); throw; } @@ -87,6 +123,11 @@ public async Task RunTestsAsync(TestMode mode) } finally { + if (adjustedPlayModeOptions) + { + RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions); + } + _operationLock.Release(); } } @@ -149,6 +190,55 @@ public void TestFinished(ITestResultAdaptor result) #endregion + private static bool EnsurePlayModeRunsWithoutDomainReload( + out bool originalEnterPlayModeOptionsEnabled, + out EnterPlayModeOptions originalEnterPlayModeOptions) + { + originalEnterPlayModeOptionsEnabled = EditorSettings.enterPlayModeOptionsEnabled; + originalEnterPlayModeOptions = EditorSettings.enterPlayModeOptions; + + // When Play Mode triggers a domain reload, the MCP connection is torn down and the pending + // test run response never makes it back to the caller. To keep the bridge alive for this + // invocation, temporarily enable Enter Play Mode Options with domain reload disabled. + bool domainReloadDisabled = (originalEnterPlayModeOptions & EnterPlayModeOptions.DisableDomainReload) != 0; + bool needsChange = !originalEnterPlayModeOptionsEnabled || !domainReloadDisabled; + if (!needsChange) + { + return false; + } + + var desired = originalEnterPlayModeOptions | EnterPlayModeOptions.DisableDomainReload; + EditorSettings.enterPlayModeOptionsEnabled = true; + EditorSettings.enterPlayModeOptions = desired; + return true; + } + + private static void RestoreEnterPlayModeOptions(bool originalEnabled, EnterPlayModeOptions originalOptions) + { + EditorSettings.enterPlayModeOptions = originalOptions; + EditorSettings.enterPlayModeOptionsEnabled = originalEnabled; + } + + private static void SaveDirtyScenesIfNeeded() + { + int sceneCount = SceneManager.sceneCount; + for (int i = 0; i < sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + if (scene.isDirty) + { + try + { + EditorSceneManager.SaveScene(scene); + } + catch (Exception ex) + { + McpLog.Warn($"[TestRunnerService] Failed to save dirty scene '{scene.name}': {ex.Message}"); + } + } + } + } + #region Test list helpers private async Task RetrieveTestRootAsync(TestMode mode) From 2ff151f7719d151fba6c26494002fe09bf15032f Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Tue, 25 Nov 2025 16:46:27 -0400 Subject: [PATCH 2/2] Handle the not too uncommon case where we have an empty scene --- MCPForUnity/Editor/Services/TestRunnerService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs b/MCPForUnity/Editor/Services/TestRunnerService.cs index f1ec64fb..f8cbd13f 100644 --- a/MCPForUnity/Editor/Services/TestRunnerService.cs +++ b/MCPForUnity/Editor/Services/TestRunnerService.cs @@ -227,6 +227,11 @@ private static void SaveDirtyScenesIfNeeded() var scene = SceneManager.GetSceneAt(i); if (scene.isDirty) { + if (string.IsNullOrEmpty(scene.path)) + { + McpLog.Warn($"[TestRunnerService] Skipping unsaved scene '{scene.name}': save it manually before running PlayMode tests."); + continue; + } try { EditorSceneManager.SaveScene(scene);