Repository navigation
Plain awaits in CompileUseCase/RunTestsUseCase can hang if Play Mode pauses mid-command #1686
Description
Summary
CompileUseCase and RunTestsUseCase still have plain (non-ConfigureAwait(false)) awaits that violate the invariant documented on UnityCliLoopTool.ExecuteAsync (added in #1685): every await in a tool's execution path must avoid capturing Unity's SynchronizationContext, because a continuation posted to it is never drained while Play Mode is paused.
This was flagged by CodeRabbit during review of #1685 and confirmed as a real (but out of scope for that PR) latent gap.
Affected locations
Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:106—await WaitForPlayModeExitAsync(ct)Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:154—await _executeCompilationAsync(request, ct)Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:170—await TimerDelay.Wait(POLL_INTERVAL_MS, ct)(insideWaitForPlayModeExitAsync's poll loop)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:95—await _executionService.ExecutePlayModeTestAsync(filter, ct)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:99—await _executionService.ExecuteEditModeTestAsync(filter, ct)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:102—await _waitForTestRunnerCleanupAsync(ct)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:154—await TimerDelay.Wait(TestRunnerCleanupFallbackDelayMilliseconds, ct)
(ExecuteDynamicCodeUseCase was also audited during #1685's review and is not affected — every await there already uses ConfigureAwait(false).)
Mechanism
Same second-layer bug class fixed for simulate-mouse-ui/simulate-keyboard/simulate-mouse-input in #1685: if one of these awaits is still suspended (not yet complete) when Play Mode pauses, and it later completes off the captured UnitySynchronizationContext, the resumed continuation is posted back to that context. That context's pump does not run while EditorApplication.isPaused is true, so the tool call hangs forever instead of returning.
run-tests --test-mode PlayMode is the most plausible trigger, since it genuinely executes inside Play Mode where a pause point could fire while a test is running. compile's exposure depends on whether StopPlayMode() itself resolves the pause as a side effect before the poll loop's await would otherwise hang — this needs to be measured, not assumed.
Why a mechanical fix is not safe here
Unlike the simulate-mouse-ui/simulate-keyboard wait loops (which pair every ConfigureAwait(false) with an explicit MainThreadSwitcher/InputSystemUpdateHelper.SwitchToMainThreadIfNeeded call before touching Unity APIs again), these awaits currently rely on the implicit context-capture to resume on the main thread. Code immediately after them reads main-thread-only Unity APIs (e.g. EditorApplication.isPlaying at CompileUseCase.cs:167,174).
Blindly adding ConfigureAwait(false) here would remove that implicit resume-on-main-thread guarantee, trading a rare pause-triggered hang for a frequent threadpool-touches-Unity-API bug. Do not "just add ConfigureAwait(false)" to these awaits.
Candidate fixes
- Port the
MouseUiEditorFrameWaiterpattern:ConfigureAwait(false)paired with an explicit main-thread switch-back before the next Unity API read. - Or, for
run-testsspecifically, reject execution via a preflight check if Play Mode is paused when the tool starts (may be the more appropriate design for test execution — this is Unity's highest-risk area per the Unity Freeze Prevention guidance inCLAUDE.md, so any behavioral change here needs to be validated very carefully, ideally starting as a non-EditMode/pure-unit-test-covered refactor before any Play Mode E2E repro is attempted).
Verification requirements before closing
- A real, non-mocked repro proving the current hang exists (or does not, if
compile'sStopPlayMode()genuinely self-resolves the pause first) — do not assume the mechanism transfers 1:1 from the mouse-ui case without measuring it. - After the fix, a real repro showing the tool call now returns promptly instead of hanging when a pause point fires while the awaited operation is in flight.
- Full
run-testssuite green, with special attention to the Unity Freeze Prevention guardrails inCLAUDE.md(no infinite waits, no blocking the main thread, no risky EditMode test patterns).
Context
Found during E2E verification for #1685 (source-based pause points, uloop-wait-for-pause-point skill).
Activity
hatayama commentedon Jul 11, 2026
Measurement results (2026-07-11)
compile during pause: self-resolving (no code change needed)
uloop compilePlayModeCompilationPreparationService.StopPlayMode()clearsisPaused = falsebeforeisPlaying = false, so by the timeWaitForPlayModeExitAsync'sTimerDelay.Waitcontinuation is posted toUnitySynchronizationContext, the pause is already lifted and the queue drains normally.WaitForPlayModeExitAsyncdocumenting the self-resolution mechanism and measurement date.run-tests during pause: fixed with entry preflight
uloop run-tests --test-mode PlayModeExecutePlayModeTestAsync/WaitForTestRunnerCleanupAsyncIsPausedcheck toTestExecutionStateValidationService.Validate()for PlayMode testsOut of scope: mid-run pause point hit
If a pause point fires during an already-running test, the Test Runner itself stalls. This is not solvable at the tool layer — documented with a why-comment on the plain awaits in
RunTestsUseCase.hatayama commentedon Jul 12, 2026
Resolved by #1687 (merged as 998ce19 into v3-beta) with real measurements: compile self-resolves because StopPlayMode clears isPaused before the poll loop's awaits (5.089s, no change needed, documented with a why-comment), EditMode run-tests was already rejected by existing validation, and PlayMode run-tests — the one real hang path — now fails fast with a preflight error (0.162s) when the Editor is paused. The remaining mid-test trigger (a pause point firing while a test runs) was closed by #1690, which clears active pause points before running tests. The issue stayed open only because the auto-close from "Closes #1686" does not fire for merges into non-default branches (v3-beta).