Harden PlayMode test runs (#396)
* 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 * Handle the not too uncommon case where we have an empty scenemain
parent
be6c387327
commit
2f50962b58
|
|
@ -5,8 +5,10 @@ using System.Threading;
|
|||
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 @@ namespace MCPForUnity.Editor.Services
|
|||
|
||||
public async Task<IReadOnlyList<Dictionary<string, string>>> 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 @@ namespace MCPForUnity.Editor.Services
|
|||
|
||||
public async Task<TestRunResult> RunTestsAsync(TestMode mode)
|
||||
{
|
||||
await _operationLock.WaitAsync().ConfigureAwait(false);
|
||||
await _operationLock.WaitAsync().ConfigureAwait(true);
|
||||
Task<TestRunResult> runTask;
|
||||
bool adjustedPlayModeOptions = false;
|
||||
bool originalEnterPlayModeOptionsEnabled = false;
|
||||
EnterPlayModeOptions originalEnterPlayModeOptions = EnterPlayModeOptions.None;
|
||||
try
|
||||
{
|
||||
if (_runCompletionSource != null && !_runCompletionSource.Task.IsCompleted)
|
||||
|
|
@ -67,16 +72,47 @@ namespace MCPForUnity.Editor.Services
|
|||
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<TestRunResult>(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 @@ namespace MCPForUnity.Editor.Services
|
|||
}
|
||||
finally
|
||||
{
|
||||
if (adjustedPlayModeOptions)
|
||||
{
|
||||
RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions);
|
||||
}
|
||||
|
||||
_operationLock.Release();
|
||||
}
|
||||
}
|
||||
|
|
@ -149,6 +190,60 @@ namespace MCPForUnity.Editor.Services
|
|||
|
||||
#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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[TestRunnerService] Failed to save dirty scene '{scene.name}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Test list helpers
|
||||
|
||||
private async Task<ITestAdaptor> RetrieveTestRootAsync(TestMode mode)
|
||||
|
|
|
|||
Loading…
Reference in New Issue