Skip to content

Commit 2f50962

Browse files
authored
Harden PlayMode test runs (CoplayDev#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 scene
1 parent be6c387 commit 2f50962

File tree

1 file changed

+98
-3
lines changed

1 file changed

+98
-3
lines changed

MCPForUnity/Editor/Services/TestRunnerService.cs

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.Threading.Tasks;
66
using MCPForUnity.Editor.Helpers;
77
using UnityEditor;
8+
using UnityEditor.SceneManagement;
89
using UnityEditor.TestTools.TestRunner.Api;
910
using UnityEngine;
11+
using UnityEngine.SceneManagement;
1012

1113
namespace MCPForUnity.Editor.Services
1214
{
@@ -31,7 +33,7 @@ public TestRunnerService()
3133

3234
public async Task<IReadOnlyList<Dictionary<string, string>>> GetTestsAsync(TestMode? mode)
3335
{
34-
await _operationLock.WaitAsync().ConfigureAwait(false);
36+
await _operationLock.WaitAsync().ConfigureAwait(true);
3537
try
3638
{
3739
var modes = mode.HasValue ? new[] { mode.Value } : AllModes;
@@ -58,25 +60,59 @@ public async Task<IReadOnlyList<Dictionary<string, string>>> GetTestsAsync(TestM
5860

5961
public async Task<TestRunResult> RunTestsAsync(TestMode mode)
6062
{
61-
await _operationLock.WaitAsync().ConfigureAwait(false);
63+
await _operationLock.WaitAsync().ConfigureAwait(true);
6264
Task<TestRunResult> runTask;
65+
bool adjustedPlayModeOptions = false;
66+
bool originalEnterPlayModeOptionsEnabled = false;
67+
EnterPlayModeOptions originalEnterPlayModeOptions = EnterPlayModeOptions.None;
6368
try
6469
{
6570
if (_runCompletionSource != null && !_runCompletionSource.Task.IsCompleted)
6671
{
6772
throw new InvalidOperationException("A Unity test run is already in progress.");
6873
}
6974

75+
if (EditorApplication.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode)
76+
{
77+
throw new InvalidOperationException("Cannot start a test run while the Editor is in or entering Play Mode. Stop Play Mode and try again.");
78+
}
79+
80+
if (mode == TestMode.PlayMode)
81+
{
82+
// PlayMode runs transition the editor into play across multiple update ticks. Unity's
83+
// built-in pipeline schedules SaveModifiedSceneTask early, but that task uses
84+
// EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo which throws once play mode is
85+
// active. To minimize that window we pre-save dirty scenes and disable domain reload (so the
86+
// MCP bridge stays alive). We do NOT force runSynchronously here because that can freeze the
87+
// editor in some projects. If the TestRunner still hits the save task after entering play, the
88+
// run can fail; in that case, rerun from a clean Edit Mode state.
89+
adjustedPlayModeOptions = EnsurePlayModeRunsWithoutDomainReload(
90+
out originalEnterPlayModeOptionsEnabled,
91+
out originalEnterPlayModeOptions);
92+
}
93+
7094
_leafResults.Clear();
7195
_runCompletionSource = new TaskCompletionSource<TestRunResult>(TaskCreationOptions.RunContinuationsAsynchronously);
7296

7397
var filter = new Filter { testMode = mode };
74-
_testRunnerApi.Execute(new ExecutionSettings(filter));
98+
var settings = new ExecutionSettings(filter);
99+
100+
if (mode == TestMode.PlayMode)
101+
{
102+
SaveDirtyScenesIfNeeded();
103+
}
104+
105+
_testRunnerApi.Execute(settings);
75106

76107
runTask = _runCompletionSource.Task;
77108
}
78109
catch
79110
{
111+
if (adjustedPlayModeOptions)
112+
{
113+
RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions);
114+
}
115+
80116
_operationLock.Release();
81117
throw;
82118
}
@@ -87,6 +123,11 @@ public async Task<TestRunResult> RunTestsAsync(TestMode mode)
87123
}
88124
finally
89125
{
126+
if (adjustedPlayModeOptions)
127+
{
128+
RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions);
129+
}
130+
90131
_operationLock.Release();
91132
}
92133
}
@@ -149,6 +190,60 @@ public void TestFinished(ITestResultAdaptor result)
149190

150191
#endregion
151192

193+
private static bool EnsurePlayModeRunsWithoutDomainReload(
194+
out bool originalEnterPlayModeOptionsEnabled,
195+
out EnterPlayModeOptions originalEnterPlayModeOptions)
196+
{
197+
originalEnterPlayModeOptionsEnabled = EditorSettings.enterPlayModeOptionsEnabled;
198+
originalEnterPlayModeOptions = EditorSettings.enterPlayModeOptions;
199+
200+
// When Play Mode triggers a domain reload, the MCP connection is torn down and the pending
201+
// test run response never makes it back to the caller. To keep the bridge alive for this
202+
// invocation, temporarily enable Enter Play Mode Options with domain reload disabled.
203+
bool domainReloadDisabled = (originalEnterPlayModeOptions & EnterPlayModeOptions.DisableDomainReload) != 0;
204+
bool needsChange = !originalEnterPlayModeOptionsEnabled || !domainReloadDisabled;
205+
if (!needsChange)
206+
{
207+
return false;
208+
}
209+
210+
var desired = originalEnterPlayModeOptions | EnterPlayModeOptions.DisableDomainReload;
211+
EditorSettings.enterPlayModeOptionsEnabled = true;
212+
EditorSettings.enterPlayModeOptions = desired;
213+
return true;
214+
}
215+
216+
private static void RestoreEnterPlayModeOptions(bool originalEnabled, EnterPlayModeOptions originalOptions)
217+
{
218+
EditorSettings.enterPlayModeOptions = originalOptions;
219+
EditorSettings.enterPlayModeOptionsEnabled = originalEnabled;
220+
}
221+
222+
private static void SaveDirtyScenesIfNeeded()
223+
{
224+
int sceneCount = SceneManager.sceneCount;
225+
for (int i = 0; i < sceneCount; i++)
226+
{
227+
var scene = SceneManager.GetSceneAt(i);
228+
if (scene.isDirty)
229+
{
230+
if (string.IsNullOrEmpty(scene.path))
231+
{
232+
McpLog.Warn($"[TestRunnerService] Skipping unsaved scene '{scene.name}': save it manually before running PlayMode tests.");
233+
continue;
234+
}
235+
try
236+
{
237+
EditorSceneManager.SaveScene(scene);
238+
}
239+
catch (Exception ex)
240+
{
241+
McpLog.Warn($"[TestRunnerService] Failed to save dirty scene '{scene.name}': {ex.Message}");
242+
}
243+
}
244+
}
245+
}
246+
152247
#region Test list helpers
153248

154249
private async Task<ITestAdaptor> RetrieveTestRootAsync(TestMode mode)

0 commit comments

Comments
 (0)