diff --git a/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs b/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs index c30e590bd..20b3f1c3a 100644 --- a/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs +++ b/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs @@ -30,7 +30,7 @@ public static async void TestCompileChecker() CompileResult result = await compileController.TryCompileAsync( forceRecompile: false, - pausePointWarning: null, + playModeStopWarning: null, ct: CancellationToken.None); CompilerMessage[] err = result.Errors; CompilerMessage[] warning = result.Warnings; @@ -72,7 +72,7 @@ public static async void TestForceCompileChecker() // Example of forced re-compilation CompileResult result = await compileController.TryCompileAsync( forceRecompile: true, - pausePointWarning: null, + playModeStopWarning: null, ct: CancellationToken.None); CompilerMessage[] err = result.Errors; CompilerMessage[] warning = result.Warnings; diff --git a/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs b/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs index db97f157a..5baf28e70 100644 --- a/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs +++ b/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs @@ -135,7 +135,7 @@ private async Task ExecuteCompileAsync() return; } - CompileResult result = await _compileController.TryCompileAsync(_forceRecompile, pausePointWarning: null, CancellationToken.None); + CompileResult result = await _compileController.TryCompileAsync(_forceRecompile, playModeStopWarning: null, CancellationToken.None); if (ShouldRunExecuteDynamicCodeReadinessAfterCompile(result)) { _isPostCompileReadinessRunning = true; diff --git a/Assets/Tests/Editor/CompileApiUpdaterConsentPropagationTests.cs b/Assets/Tests/Editor/CompileApiUpdaterConsentPropagationTests.cs index 110b27c4a..9062cbb16 100644 --- a/Assets/Tests/Editor/CompileApiUpdaterConsentPropagationTests.cs +++ b/Assets/Tests/Editor/CompileApiUpdaterConsentPropagationTests.cs @@ -95,7 +95,7 @@ public async Task CompileAsync_WhenConsentWasDeclined_ReturnsDisclosure() compileResultSessionRepository, pendingCompileSessionRepository); useCase.SetCompilationStateValidationForTesting(() => ValidationResult.Success()); - useCase.SetCompilationExecutionForTesting((compileRequest, pausePointWarning, ct) => + useCase.SetCompilationExecutionForTesting((compileRequest, playModeStopWarning, ct) => { ct.ThrowIfCancellationRequested(); return Task.FromResult(executionResult); @@ -131,7 +131,7 @@ public void CreateResponse_WhenDeclinedWithExistingWarning_AppendsFixedWarning() CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: "Play Mode was active with 2 enabled pause point(s)."); + playModeStopWarning: "Play Mode was active with 2 enabled pause point(s)."); Assert.That( response.Warning, @@ -160,7 +160,7 @@ public void CreateResponse_WhenDeclinedForceCompile_AppendsFixedNextAction() CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: true, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.Warning, Is.EqualTo(WarningText)); Assert.That( diff --git a/Assets/Tests/Editor/CompileControllerPlayModeStopWarningTests.cs b/Assets/Tests/Editor/CompileControllerPlayModeStopWarningTests.cs new file mode 100644 index 000000000..0da5e18fb --- /dev/null +++ b/Assets/Tests/Editor/CompileControllerPlayModeStopWarningTests.cs @@ -0,0 +1,74 @@ +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies CompileController keeps the Play-stop Warning on the delayed status-polling path + /// when external Scene changes abort compile before Unity starts compiling. + /// + [TestFixture] + public sealed class CompileControllerPlayModeStopWarningTests + { + /// + /// What: an external Scene-change refusal stores the received Play-stop Warning for status polling. + /// + [Test] + public async Task TryCompileAsync_WhenExternalSceneChangeBlocks_PersistsReceivedPlayModeStopWarning() + { + const string expectedWarning = + "Play Mode was active when this compile was requested. The compile stops Play Mode and the domain reload discards the Play session state — re-establish your runtime state before continuing verification."; + UnityCliLoopCompileResultSessionRepository compileResultSessionRepository = + UnityCliLoopEditorSessionStateTestFactory.CreateCompileResultSessionRepository(); + UnityCliLoopPendingCompileSessionRepository pendingCompileSessionRepository = + UnityCliLoopEditorSessionStateTestFactory.CreatePendingCompileSessionRepository(); + UnityCliLoopEditorSessionStateSnapshot originalSnapshot = + UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(); + UnityCliLoopEditorSessionStateTestFactory.ClearAll(); + + try + { + using CompileController controller = new( + compileResultSessionRepository, + pendingCompileSessionRepository); + controller.SetResultRecordingContext( + CompileResultRecordingContext.Create( + new CompileSchema + { + WaitForDomainReload = true, + RequestId = "compile_scene_change_play_stop_warning", + ForceRecompile = false + })); + controller.SetExternalSceneChangeResolutionForTesting(_ => ( + false, + "Open Scene files have changed externally and compile stopped.", + new[] { "Assets/Scenes/Sample.unity" })); + + await controller.TryCompileAsync( + forceRecompile: false, + expectedWarning, + CancellationToken.None); + + UnityCliLoopStoredCompileResult storedResult = + compileResultSessionRepository.GetCompileResult("compile_scene_change_play_stop_warning"); + CompileResponse storedResponse = JsonConvert.DeserializeObject( + storedResult.ResultJson, + UnityCliLoopJsonResponseSerializerSettings.Settings); + + Assert.That(storedResult.HasResult, Is.True); + Assert.That(storedResponse.Warning, Is.EqualTo(expectedWarning)); + } + finally + { + originalSnapshot.Restore(); + } + } + } +} diff --git a/Assets/Tests/Editor/CompileControllerPlayModeStopWarningTests.cs.meta b/Assets/Tests/Editor/CompileControllerPlayModeStopWarningTests.cs.meta new file mode 100644 index 000000000..239a5922b --- /dev/null +++ b/Assets/Tests/Editor/CompileControllerPlayModeStopWarningTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f0b1a1490f524f51a8d10747ad2c6fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/CompileErrorNextActionsComposerTests.cs b/Assets/Tests/Editor/CompileErrorNextActionsComposerTests.cs index 4640a5667..c07862227 100644 --- a/Assets/Tests/Editor/CompileErrorNextActionsComposerTests.cs +++ b/Assets/Tests/Editor/CompileErrorNextActionsComposerTests.cs @@ -246,7 +246,7 @@ public void CreateResponse_WhenLanguageVersionError_ReturnsExactNextActions() CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.NextActions, Is.EqualTo(new[] { FileScopedNamespaceNextAction })); } @@ -271,7 +271,7 @@ public void CreateResponse_WhenForceCompileWithLanguageVersionError_DoesNotAddRe CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: true, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.NextActions, Is.EqualTo(new[] { ExistingNextAction })); } @@ -296,7 +296,7 @@ public void CreateResponse_WhenIndeterminateWithLanguageVersionError_DoesNotAddR CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.NextActions, Is.Null); } @@ -320,7 +320,7 @@ public void CreateResponse_WhenLanguageVersionErrorAndConsentDeclined_AppendsAft CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That( response.NextActions, @@ -480,7 +480,7 @@ public void CreateResponse_WhenCs0234ForNUnitFramework_IncludesNunitFrameworkAss CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.NextActions, Is.EqualTo(new[] { NUnitFrameworkNextAction })); } @@ -496,7 +496,7 @@ public void CreateResponse_WhenCs0234HasNoDeclaringAssembly_ReturnsNoNextActions CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.NextActions, Is.Null); } @@ -512,7 +512,7 @@ public void CreateResponse_WhenCs0246Error_ReturnsNoNextActions() CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That(response.NextActions, Is.Null); } @@ -536,7 +536,7 @@ public void CreateResponse_WhenCs0234AndConsentDeclined_AppendsAfterExistingNext CompileResponse response = CompileResponseFactory.CreateResponse( result, forceRecompile: false, - pausePointWarning: null); + playModeStopWarning: null); Assert.That( response.NextActions, diff --git a/Assets/Tests/Editor/CompilePausePointWarningBuilderTests.cs b/Assets/Tests/Editor/CompilePausePointWarningBuilderTests.cs deleted file mode 100644 index 02c62a15d..000000000 --- a/Assets/Tests/Editor/CompilePausePointWarningBuilderTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using NUnit.Framework; - -using io.github.hatayama.UnityCliLoop.FirstPartyTools; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor -{ - /// - /// Verifies the compile Warning text only appears when Play Mode was active with at least - /// one enabled pause point, and states the domain-reload loss when it does. - /// - [TestFixture] - public sealed class CompilePausePointWarningBuilderTests - { - [Test] - public void BuildWarning_WhenNotPlayingAtRequestStart_ReturnsNull() - { - // Verifies no warning is produced when Play Mode was not active, regardless of marker count. - string warning = CompilePausePointWarningBuilder.BuildWarning( - wasPlayingAtRequestStart: false, - activePausePointCount: 3); - - Assert.That(warning, Is.Null); - } - - [Test] - public void BuildWarning_WhenPlayingButNoActivePausePoints_ReturnsNull() - { - // Verifies no warning is produced when Play Mode was active but no pause point is enabled. - string warning = CompilePausePointWarningBuilder.BuildWarning( - wasPlayingAtRequestStart: true, - activePausePointCount: 0); - - Assert.That(warning, Is.Null); - } - - [Test] - public void BuildWarning_WhenPlayingWithActivePausePoints_MentionsCountAndDomainReloadLoss() - { - // Verifies the warning names the active count and explains what the domain reload discards. - string warning = CompilePausePointWarningBuilder.BuildWarning( - wasPlayingAtRequestStart: true, - activePausePointCount: 2); - - Assert.That(warning, Does.Contain("2 enabled pause point")); - Assert.That(warning, Does.Contain("Play session state")); - Assert.That(warning, Does.Contain("pause point patches")); - } - } -} diff --git a/Assets/Tests/Editor/CompilePlayModeStopWarningBuilderTests.cs b/Assets/Tests/Editor/CompilePlayModeStopWarningBuilderTests.cs new file mode 100644 index 000000000..3e3a11f2d --- /dev/null +++ b/Assets/Tests/Editor/CompilePlayModeStopWarningBuilderTests.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies compile Warning text for each Play-at-request-start branch: none, Play without + /// pause points, and Play with enabled pause points. + /// + [TestFixture] + public sealed class CompilePlayModeStopWarningBuilderTests + { + /// + /// What: no warning when Play Mode was not active, regardless of marker count. + /// + [Test] + public void BuildWarning_WhenNotPlayingAtRequestStart_ReturnsNull() + { + string warning = CompilePlayModeStopWarningBuilder.BuildWarning( + wasPlayingAtRequestStart: false, + activePausePointCount: 3); + + Assert.That(warning, Is.Null); + } + + /// + /// What: Play without enabled pause points warns that compile stops Play and discards session state. + /// + [Test] + public void BuildWarning_WhenPlayingButNoActivePausePoints_ReturnsPlaySessionDiscardWarning() + { + string warning = CompilePlayModeStopWarningBuilder.BuildWarning( + wasPlayingAtRequestStart: true, + activePausePointCount: 0); + + Assert.That( + warning, + Is.EqualTo( + "Play Mode was active when this compile was requested. The compile stops Play Mode and the domain reload discards the Play session state — re-establish your runtime state before continuing verification.")); + } + + /// + /// What: Play with enabled pause points keeps the existing count-and-patch-loss wording exactly. + /// + [Test] + public void BuildWarning_WhenPlayingWithActivePausePoints_ReturnsExistingPausePointWording() + { + string warning = CompilePlayModeStopWarningBuilder.BuildWarning( + wasPlayingAtRequestStart: true, + activePausePointCount: 2); + + Assert.That( + warning, + Is.EqualTo( + "Play Mode was active with 2 enabled pause point(s). The compile stops Play Mode and the domain reload discards the Play session state and all pause point patches — re-enable pause points after the compile completes.")); + } + } +} diff --git a/Assets/Tests/Editor/CompilePausePointWarningBuilderTests.cs.meta b/Assets/Tests/Editor/CompilePlayModeStopWarningBuilderTests.cs.meta similarity index 100% rename from Assets/Tests/Editor/CompilePausePointWarningBuilderTests.cs.meta rename to Assets/Tests/Editor/CompilePlayModeStopWarningBuilderTests.cs.meta diff --git a/Assets/Tests/Editor/CompileResponseFactoryTests.cs b/Assets/Tests/Editor/CompileResponseFactoryTests.cs index 01d2677ba..eb68b2bdd 100644 --- a/Assets/Tests/Editor/CompileResponseFactoryTests.cs +++ b/Assets/Tests/Editor/CompileResponseFactoryTests.cs @@ -42,7 +42,7 @@ public void CreateResponse_WhenNormalCompileCompletes_MapsDetailedIssues() warnings: new[] { warning }); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); Assert.That(response.Success, Is.False); Assert.That(response.ErrorCount, Is.EqualTo(1)); @@ -75,7 +75,7 @@ public void CreateResponse_WhenForceCompileIsUnknown_ExplainsNullDetails() message: null); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: true, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: true, playModeStopWarning: null); Assert.That(response.Success, Is.False); Assert.That(response.ErrorCount, Is.Null); @@ -102,7 +102,7 @@ public void CreateResponse_WhenForceCompileHasOutcome_ExplainsNullDetails() message: "Internal force compile status message."); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: true, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: true, playModeStopWarning: null); Assert.That(response.Success, Is.False); Assert.That(response.ErrorCount, Is.Null); @@ -144,7 +144,7 @@ public void CreateResponse_WhenIndeterminateNonForceCompileHasCounts_PreservesCo message: null); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); Assert.That(response.Success, Is.False); Assert.That(response.ErrorCount, Is.EqualTo(1)); @@ -179,7 +179,7 @@ public void CreateResponse_WhenForceCompileHasPreservedFailure_MapsDetailedIssue preserveDetailsWhenForceRecompile: true); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: true, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: true, playModeStopWarning: null); Assert.That(response.Success, Is.False); Assert.That(response.ErrorCount, Is.EqualTo(1)); @@ -210,7 +210,7 @@ public void CreateResponse_WhenExternalSceneCannotBeResolved_AddsNextActions() message: error.message); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); Assert.That(response.NextActions, Is.Not.Null); Assert.That(response.NextActions, Has.Length.EqualTo(2)); @@ -245,7 +245,7 @@ public void CreateResponse_WhenExternalSceneCannotBeReloaded_AddsNextActions() message: error.message); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); Assert.That(response.NextActions, Is.Not.Null); Assert.That(response.NextActions, Has.Length.EqualTo(2)); @@ -275,7 +275,7 @@ public void CreateResponse_WhenExternalSceneStopMessageUsesDifferentWording_Does message: error.message); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); Assert.That(response.NextActions, Is.Null); } @@ -301,7 +301,7 @@ public void CreateResponse_WhenUnityTestFrameworkSymbolIsMissing_AddsTestAsmdefH warnings: Array.Empty()); CompileResponse response = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); Assert.That(response.Message, Does.Contain("TestAssemblies")); Assert.That(response.Message, Does.Contain("com.unity.test-framework")); diff --git a/Assets/Tests/Editor/CompileSessionResultStoreTests.cs b/Assets/Tests/Editor/CompileSessionResultStoreTests.cs index e3c775032..7394d694b 100644 --- a/Assets/Tests/Editor/CompileSessionResultStoreTests.cs +++ b/Assets/Tests/Editor/CompileSessionResultStoreTests.cs @@ -97,9 +97,9 @@ public void StoreCompileResult_WhenEquivalentNormalResponsesAreStoredTwice_Write errors: Array.Empty(), warnings: new[] { warning }); CompileResponse firstResponse = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); CompileResponse secondResponse = - CompileResponseFactory.CreateResponse(result, forceRecompile: false, pausePointWarning: null); + CompileResponseFactory.CreateResponse(result, forceRecompile: false, playModeStopWarning: null); CompileSessionResultStore.StoreCompileResult( compileResultSessionRepository, diff --git a/Assets/Tests/Editor/CompileUseCaseTests.cs b/Assets/Tests/Editor/CompileUseCaseTests.cs index f7f4d4ab3..f48b892b5 100644 --- a/Assets/Tests/Editor/CompileUseCaseTests.cs +++ b/Assets/Tests/Editor/CompileUseCaseTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using NUnit.Framework; using UnityEditor.Compilation; @@ -50,7 +51,7 @@ public async Task CompileAsync_WhenExecutionLayerStoresDelayedSuccess_DoesNotSto compileSessionLifecycleService, compileResultSessionRepository, pendingCompileSessionRepository); - useCase.SetCompilationExecutionForTesting((compileRequest, pausePointWarning, ct) => + useCase.SetCompilationExecutionForTesting((compileRequest, playModeStopWarning, ct) => { ct.ThrowIfCancellationRequested(); CompileResultSessionRecorder.RecordCompileResult( @@ -68,7 +69,7 @@ public async Task CompileAsync_WhenExecutionLayerStoresDelayedSuccess_DoesNotSto Assert.That(compileResultSessionRepository.StoreCount, Is.EqualTo(1)); Assert.That(response.Success, Is.True); Assert.That(response.ProjectRoot, Is.Not.Empty); - // Verifies no pause-point Warning appears outside Play Mode (the only state an EditMode test can exercise). + // Verifies no Play-stop Warning appears outside Play Mode (the only state an EditMode test can exercise). Assert.That(response.Warning, Is.Null); UnityCliLoopStoredCompileResult storedResult = compileResultSessionRepository.GetCompileResult("compile_test_request"); @@ -109,7 +110,7 @@ public async Task CompileAsync_WhenValidationFailsBecauseCompiling_SetsAlreadyIn ValidationResult.FailureWithErrorCode( "Compilation is already in progress. Please wait for the current compilation to finish.", CompileStateValidationErrorCodes.AlreadyInProgressErrorCodeText)); - useCase.SetCompilationExecutionForTesting((compileRequest, pausePointWarning, ct) => + useCase.SetCompilationExecutionForTesting((compileRequest, playModeStopWarning, ct) => { throw new InvalidOperationException("validation failure must not start compilation"); }); @@ -135,6 +136,71 @@ public async Task CompileAsync_WhenValidationFailsBecauseCompiling_SetsAlreadyIn } } + /// + /// What: a validation failure after Play was active still returns and stores the Play-stop Warning. + /// + [Test] + public async Task CompileAsync_WhenValidationFailsAfterPlayWasActive_SetsPlayModeStopWarningOnImmediateAndStoredResponses() + { + const string expectedWarning = + "Play Mode was active when this compile was requested. The compile stops Play Mode and the domain reload discards the Play session state — re-establish your runtime state before continuing verification."; + UnityCliLoopCompileResultSessionRepository compileResultSessionRepository = + UnityCliLoopEditorSessionStateTestFactory.CreateCompileResultSessionRepository(); + UnityCliLoopPendingCompileSessionRepository pendingCompileSessionRepository = + UnityCliLoopEditorSessionStateTestFactory.CreatePendingCompileSessionRepository(); + UnityCliLoopCompileSessionLifecycleService compileSessionLifecycleService = + new( + UnityCliLoopEditorSessionStateTestFactory.CreateSessionFlagsRepository(), + compileResultSessionRepository, + pendingCompileSessionRepository); + UnityCliLoopEditorSessionStateSnapshot originalSnapshot = + UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(); + UnityCliLoopEditorSessionStateTestFactory.ClearAll(); + + try + { + CompileUseCase useCase = new( + compileSessionLifecycleService, + compileResultSessionRepository, + pendingCompileSessionRepository); + useCase.SetPlayModeStopWarningInputsForTesting( + wasPlayingAtRequestStart: true, + activePausePointCount: 0); + useCase.SetCompilationStateValidationForTesting(() => + ValidationResult.FailureWithErrorCode( + "Compilation is already in progress. Please wait for the current compilation to finish.", + CompileStateValidationErrorCodes.AlreadyInProgressErrorCodeText)); + useCase.SetCompilationExecutionForTesting((compileRequest, playModeStopWarning, ct) => + { + throw new InvalidOperationException("validation failure must not start compilation"); + }); + + CompileResponse response = await useCase.CompileAsync( + new CompileSchema + { + WaitForDomainReload = true, + RequestId = "compile_validation_play_stop_warning", + ForceRecompile = false, + ReloadExternalSceneChanges = true + }, + CancellationToken.None); + + UnityCliLoopStoredCompileResult storedResult = + compileResultSessionRepository.GetCompileResult("compile_validation_play_stop_warning"); + CompileResponse storedResponse = JsonConvert.DeserializeObject( + storedResult.ResultJson, + UnityCliLoopJsonResponseSerializerSettings.Settings); + + Assert.That(response.Warning, Is.EqualTo(expectedWarning)); + Assert.That(storedResult.HasResult, Is.True); + Assert.That(storedResponse.Warning, Is.EqualTo(expectedWarning)); + } + finally + { + originalSnapshot.Restore(); + } + } + private static CompileResult CreateSuccessfulCompileResult() { CompilerMessage warning = new() diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompilationExecutionService.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompilationExecutionService.cs index ada340008..df1e78cf4 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompilationExecutionService.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompilationExecutionService.cs @@ -34,9 +34,9 @@ public CompilationExecutionService( /// Execute compilation asynchronously /// /// Compile request with force and delayed-result settings. - /// Optional Warning to carry onto the shaped response, e.g. when Play Mode was active with enabled pause points. + /// Optional Warning to carry onto the shaped response when compile was requested during Play Mode. /// Compilation result - public async Task ExecuteCompilationAsync(CompileSchema request, string pausePointWarning, CancellationToken ct) + public async Task ExecuteCompilationAsync(CompileSchema request, string playModeStopWarning, CancellationToken ct) { if (request == null) { @@ -48,7 +48,7 @@ public async Task ExecuteCompilationAsync(CompileSchema request, _pendingCompileSessionRepository); compileController.SetResultRecordingContext(CompileResultRecordingContext.Create(request)); compileController.SetExternalSceneChangePolicy(request.ReloadExternalSceneChanges); - return await compileController.TryCompileAsync(request.ForceRecompile, pausePointWarning, ct).ConfigureAwait(false); + return await compileController.TryCompileAsync(request.ForceRecompile, playModeStopWarning, ct).ConfigureAwait(false); } } } diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs index 0c143176e..283b305b1 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileController.cs @@ -22,8 +22,9 @@ public class CompileController : IDisposable private List _compileMessages = new(); private TaskCompletionSource _currentCompileTask; private bool _isForceCompile = false; - private string _pendingPausePointWarning; + private string _pendingPlayModeStopWarning; private bool _reloadExternalSceneChanges = true; + private Func _resolveExternalSceneChangesForTesting; private CompileResultRecordingContext _resultRecordingContext = CompileResultRecordingContext.Disabled(); private DateTime _compileStartedAtUtc = DateTime.MinValue; private int _assemblyFinishedCount; @@ -98,11 +99,33 @@ internal void SetExternalSceneChangePolicy(bool reloadExternalSceneChanges) _reloadExternalSceneChanges = reloadExternalSceneChanges; } + /// + /// Replaces external Scene-change resolution so tests can exercise the early-return + /// recording path without mutating open Scenes. + /// + internal void SetExternalSceneChangeResolutionForTesting( + Func resolveExternalSceneChanges) + { + UnityEngine.Debug.Assert(resolveExternalSceneChanges != null, "resolveExternalSceneChanges must not be null"); + _resolveExternalSceneChangesForTesting = resolveExternalSceneChanges ?? + throw new ArgumentNullException(nameof(resolveExternalSceneChanges)); + } + + private (bool CanProceed, string Message, string[] ScenePaths) ResolveExternalSceneChanges() + { + if (_resolveExternalSceneChangesForTesting != null) + { + return _resolveExternalSceneChangesForTesting(_reloadExternalSceneChanges); + } + + return ExternalSceneChangeTracker.ResolveForCompile(_reloadExternalSceneChanges); + } + /// /// Executes compilation asynchronously. /// /// Whether to force a recompile. - /// Optional Warning to carry onto the shaped response, e.g. when Play Mode was active with enabled pause points. + /// Optional Warning to carry onto the shaped response when compile was requested during Play Mode. /// Cancellation token for the compile execution. /// The compilation result. /// Thrown when the task is not found during compilation. @@ -110,7 +133,7 @@ internal void SetExternalSceneChangePolicy(bool reloadExternalSceneChanges) /// Callers must validate editor compilation state before invoking compile execution; /// the production pipeline does this in CompileUseCase. /// - public async Task TryCompileAsync(bool forceRecompile, string pausePointWarning, CancellationToken ct) + public async Task TryCompileAsync(bool forceRecompile, string playModeStopWarning, CancellationToken ct) { if (_isCompiling) { @@ -130,7 +153,7 @@ public async Task TryCompileAsync(bool forceRecompile, string pau } (bool CanProceed, string Message, string[] ScenePaths) sceneChangeResult = - ExternalSceneChangeTracker.ResolveForCompile(_reloadExternalSceneChanges); + ResolveExternalSceneChanges(); if (!sceneChangeResult.CanProceed) { VibeLogger.LogWarning( @@ -143,11 +166,11 @@ public async Task TryCompileAsync(bool forceRecompile, string pau }); CompileResult result = CompileResultFactory.CreateExternalSceneChangeFailureResult(sceneChangeResult); - RecordCompileResultIfNeeded(result, pausePointWarning: null); + RecordCompileResultIfNeeded(result, playModeStopWarning); return result; } - _pendingPausePointWarning = pausePointWarning; + _pendingPlayModeStopWarning = playModeStopWarning; _isCompiling = true; _compileMessages.Clear(); _assemblyFinishedCount = 0; @@ -325,7 +348,7 @@ private void CompleteCompileRequest(CompileResult result, bool unregisterEvents) // Completion subscribers are outside this controller, so state cleanup cannot depend on them returning. try { - RecordCompileResultIfNeeded(resultToComplete, _pendingPausePointWarning); + RecordCompileResultIfNeeded(resultToComplete, _pendingPlayModeStopWarning); if (unregisterEvents) { @@ -345,7 +368,7 @@ private void CompleteCompileRequest(CompileResult result, bool unregisterEvents) _isForceCompile = false; _resultRecordingContext = CompileResultRecordingContext.Disabled(); _compileStartedAtUtc = DateTime.MinValue; - _pendingPausePointWarning = null; + _pendingPlayModeStopWarning = null; CompileApiUpdaterConsentState.EndCliCompile(); } @@ -367,7 +390,7 @@ private void ClearUntransferredCompileState( _isForceCompile = false; _resultRecordingContext = CompileResultRecordingContext.Disabled(); _compileStartedAtUtc = DateTime.MinValue; - _pendingPausePointWarning = null; + _pendingPlayModeStopWarning = null; CompileApiUpdaterConsentState.EndCliCompile(); compileTask.TrySetCanceled(); } @@ -392,7 +415,7 @@ internal void ClearUntransferredCompileStateForTesting() ClearUntransferredCompileState(compileTask, eventsRegistered: false); } - private void RecordCompileResultIfNeeded(CompileResult result, string pausePointWarning) + private void RecordCompileResultIfNeeded(CompileResult result, string playModeStopWarning) { UnityEngine.Debug.Assert(result != null, "result must not be null"); @@ -408,7 +431,7 @@ private void RecordCompileResultIfNeeded(CompileResult result, string pausePoint _resultRecordingContext.ForceRecompile, result, _resultRecordingContext.RequestId, - pausePointWarning); + playModeStopWarning); } /// @@ -519,7 +542,7 @@ public void Cleanup() _reloadExternalSceneChanges = true; _resultRecordingContext = CompileResultRecordingContext.Disabled(); _compileStartedAtUtc = DateTime.MinValue; - _pendingPausePointWarning = null; + _pendingPlayModeStopWarning = null; CompileApiUpdaterConsentState.EndCliCompile(); } diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompilePausePointWarningBuilder.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompilePausePointWarningBuilder.cs deleted file mode 100644 index 6825e0e28..000000000 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompilePausePointWarningBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace io.github.hatayama.UnityCliLoop.FirstPartyTools -{ - /// - /// Builds the compile Warning text for the case where Play Mode was active with enabled - /// pause points at the moment compile was requested: the domain reload that follows discards - /// both the Play session state and every pause-point Harmony patch. - /// - internal static class CompilePausePointWarningBuilder - { - public static string BuildWarning(bool wasPlayingAtRequestStart, int activePausePointCount) - { - if (!wasPlayingAtRequestStart || activePausePointCount <= 0) - { - return null; - } - - return "Play Mode was active with " + activePausePointCount + " enabled pause point(s). " - + "The compile stops Play Mode and the domain reload discards the Play session state " - + "and all pause point patches — re-enable pause points after the compile completes."; - } - } -} diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompilePlayModeStopWarningBuilder.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompilePlayModeStopWarningBuilder.cs new file mode 100644 index 000000000..4c6355a6f --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompilePlayModeStopWarningBuilder.cs @@ -0,0 +1,27 @@ +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Builds the compile Warning text when Play Mode was active at the moment compile was + /// requested: the compile stops Play Mode and the following domain reload discards the + /// Play session state, and also every pause-point Harmony patch when any are enabled. + /// + internal static class CompilePlayModeStopWarningBuilder + { + public static string BuildWarning(bool wasPlayingAtRequestStart, int activePausePointCount) + { + if (!wasPlayingAtRequestStart) + { + return null; + } + + if (activePausePointCount > 0) + { + return "Play Mode was active with " + activePausePointCount + " enabled pause point(s). " + + "The compile stops Play Mode and the domain reload discards the Play session state " + + "and all pause point patches — re-enable pause points after the compile completes."; + } + + return "Play Mode was active when this compile was requested. The compile stops Play Mode and the domain reload discards the Play session state — re-establish your runtime state before continuing verification."; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompilePausePointWarningBuilder.cs.meta b/Packages/src/Editor/FirstPartyTools/Compile/CompilePlayModeStopWarningBuilder.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/Compile/CompilePausePointWarningBuilder.cs.meta rename to Packages/src/Editor/FirstPartyTools/Compile/CompilePlayModeStopWarningBuilder.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileResponse.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileResponse.cs index 953296759..8488f6dd1 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileResponse.cs @@ -75,7 +75,7 @@ public class CompileResponse : UnityCliLoopToolResponse /// /// Optional warning about a condition compile does not block on, e.g. Play Mode being - /// active with enabled pause points whose patches the domain reload will discard. + /// active when compile was requested so the domain reload discards Play session state. /// public string Warning { get; set; } diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileResponseFactory.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileResponseFactory.cs index 4d3b65667..757aa63b2 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileResponseFactory.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileResponseFactory.cs @@ -28,14 +28,14 @@ internal static class CompileResponseFactory internal static CompileResponse CreateResponse( CompileResult result, bool forceRecompile, - string pausePointWarning) + string playModeStopWarning) { Debug.Assert(result != null, "result must not be null"); CompileResponse response = CreateResponseWithoutApiUpdaterConsent( result, forceRecompile, - pausePointWarning); + playModeStopWarning); CompileApiUpdaterConsentResponseComposer.Apply(response, result.ApiUpdaterConsentDeclined); if (ShouldApplyErrorNextActions(result, forceRecompile)) { @@ -67,11 +67,11 @@ private static bool ShouldApplyErrorNextActions(CompileResult result, bool force private static CompileResponse CreateResponseWithoutApiUpdaterConsent( CompileResult result, bool forceRecompile, - string pausePointWarning) + string playModeStopWarning) { if (forceRecompile && !result.PreserveDetailsWhenForceRecompile) { - return CreateForceCompileResult(result, pausePointWarning); + return CreateForceCompileResult(result, playModeStopWarning); } if (result.IsIndeterminate) @@ -83,7 +83,7 @@ private static CompileResponse CreateResponseWithoutApiUpdaterConsent( errors: null, warnings: null, message: result.Message ?? "Compilation status is unknown. Use get-logs to inspect the compiler output."); - indeterminateResponse.Warning = pausePointWarning; + indeterminateResponse.Warning = playModeStopWarning; return indeterminateResponse; } @@ -95,7 +95,7 @@ private static CompileResponse CreateResponseWithoutApiUpdaterConsent( warnings: ToIssues(result.Warnings), message: AddMissingTestFrameworkReferenceHint(result.Message, result.Errors)); response.NextActions = CreateExternalSceneChangeNextActions(result.Message); - response.Warning = pausePointWarning; + response.Warning = playModeStopWarning; return response; } @@ -114,7 +114,7 @@ private static string[] CreateExternalSceneChangeNextActions(string message) return ExternalSceneChangeNextActions; } - private static CompileResponse CreateForceCompileResult(CompileResult result, string pausePointWarning) + private static CompileResponse CreateForceCompileResult(CompileResult result, string playModeStopWarning) { ForceCompileUnknownResult unknownResult = ForceCompileUnknownResult.Create(); CompileResponse response = new CompileResponse( @@ -126,7 +126,7 @@ private static CompileResponse CreateForceCompileResult(CompileResult result, st message: unknownResult.Message); response.ErrorCode = ForceCompileUnknownResult.ErrorCodeText; response.NextActions = new[] { ForceCompileUnknownResult.NextActionText }; - response.Warning = pausePointWarning; + response.Warning = playModeStopWarning; return response; } diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileResultSessionRecorder.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileResultSessionRecorder.cs index 4b6c4a447..325c9bc01 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileResultSessionRecorder.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileResultSessionRecorder.cs @@ -17,14 +17,14 @@ internal static CompileResponse RecordCompileResult( bool forceRecompile, CompileResult result, string correlationId, - string pausePointWarning = null) + string playModeStopWarning = null) { Debug.Assert(compileResultSessionRepository != null, "compileResultSessionRepository must not be null"); Debug.Assert(pendingCompileSessionRepository != null, "pendingCompileSessionRepository must not be null"); Debug.Assert(!string.IsNullOrWhiteSpace(requestId), "requestId must not be null or whitespace"); Debug.Assert(result != null, "result must not be null"); - CompileResponse response = CompileResponseFactory.CreateResponse(result, forceRecompile, pausePointWarning); + CompileResponse response = CompileResponseFactory.CreateResponse(result, forceRecompile, playModeStopWarning); return RecordCompileResponse( compileResultSessionRepository, pendingCompileSessionRepository, diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs index 1951e8465..22b01fa20 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs @@ -24,6 +24,7 @@ public class CompileUseCase private readonly IPendingCompileSessionRepository _pendingCompileSessionRepository; private Func> _executeCompilationAsync; private Func _validateCompilationState; + private Func<(bool WasPlayingAtRequestStart, int ActivePausePointCount)> _capturePlayModeStopWarningInputs; public CompileUseCase( UnityCliLoopCompileSessionLifecycleService compileSessionLifecycleService, @@ -42,6 +43,7 @@ public CompileUseCase( throw new ArgumentNullException(nameof(pendingCompileSessionRepository)); _executeCompilationAsync = ExecuteCompilationWithDefaultServiceAsync; _validateCompilationState = () => new CompilationStateValidationService().ValidateCompilationState(); + _capturePlayModeStopWarningInputs = CaptureLivePlayModeStopWarningInputs; } /// @@ -63,6 +65,15 @@ internal void SetCompilationStateValidationForTesting(Func val throw new ArgumentNullException(nameof(validateCompilationState)); } + /// + /// Replaces Play-at-request-start capture so tests can exercise the Play-stop Warning + /// without entering Play Mode. + /// + internal void SetPlayModeStopWarningInputsForTesting(bool wasPlayingAtRequestStart, int activePausePointCount) + { + _capturePlayModeStopWarningInputs = () => (wasPlayingAtRequestStart, activePausePointCount); + } + /// /// Executes compilation processing /// @@ -82,8 +93,10 @@ public async Task CompileAsync(CompileSchema request, Cancellat // Captured before PlayMode preparation can stop Play Mode, so the warning reflects // the state compile was actually requested in, not the state after this method mutates it. - bool wasPlayingAtRequestStart = EditorApplication.isPlaying; - int activePausePointCountAtRequestStart = UloopPausePointRegistry.GetActiveCount(); + (bool WasPlayingAtRequestStart, int ActivePausePointCount) playModeStopWarningInputs = + _capturePlayModeStopWarningInputs(); + bool wasPlayingAtRequestStart = playModeStopWarningInputs.WasPlayingAtRequestStart; + int activePausePointCountAtRequestStart = playModeStopWarningInputs.ActivePausePointCount; DateTime utcNow = DateTime.UtcNow; _compileSessionLifecycleService.ClearExpiredCompileResult(utcNow); @@ -147,6 +160,12 @@ public async Task CompileAsync(CompileSchema request, Cancellat } } + // Built after a successful Play stop (or when stop was unnecessary). The did-not-exit + // path above must not carry this Warning: Play is still running, so "discarded" would be false. + string playModeStopWarning = CompilePlayModeStopWarningBuilder.BuildWarning( + wasPlayingAtRequestStart, + activePausePointCountAtRequestStart); + // 2. Compilation state validation ValidationResult validation = _validateCompilationState(); @@ -164,24 +183,22 @@ public async Task CompileAsync(CompileSchema request, Cancellat errors: new[] { new CompileIssue(validation.ErrorMessage, "", 0) }, warnings: Array.Empty()); response.ErrorCode = validation.ErrorCode; + response.Warning = playModeStopWarning; CompileResponse persistedResponse = StorePreControllerResponseIfNeeded(request, response, correlationId); return persistedResponse; } // 3. Compilation execution - string pausePointWarning = CompilePausePointWarningBuilder.BuildWarning( - wasPlayingAtRequestStart, - activePausePointCountAtRequestStart); ct.ThrowIfCancellationRequested(); - CompileResult result = await _executeCompilationAsync(request, pausePointWarning, ct).ConfigureAwait(false); + CompileResult result = await _executeCompilationAsync(request, playModeStopWarning, ct).ConfigureAwait(false); // Why: CreateResponse may query TypeCache for missing-reference NextActions, and // TypeCache is a Unity Editor API that must run on the main thread. await MainThreadSwitcher.SwitchToMainThread(ct); // 4. Result formatting CompileResponse successResponse = - CompileResponseFactory.CreateResponse(result, request.ForceRecompile, pausePointWarning); + CompileResponseFactory.CreateResponse(result, request.ForceRecompile, playModeStopWarning); StampProjectRootForDelayedResponseIfNeeded(request, successResponse); return successResponse; } @@ -338,6 +355,11 @@ private static void LogCompileRequestReceived( correlationId); } + private static (bool WasPlayingAtRequestStart, int ActivePausePointCount) CaptureLivePlayModeStopWarningInputs() + { + return (EditorApplication.isPlaying, UloopPausePointRegistry.GetActiveCount()); + } + private static string ResolveCorrelationId(CompileSchema request) { Debug.Assert(request != null, "request must not be null"); @@ -370,14 +392,14 @@ private static string CreateRequestId() return $"compile_{unixTimeMilliseconds}_{correlationId}"; } - private Task ExecuteCompilationWithDefaultServiceAsync(CompileSchema request, string pausePointWarning, CancellationToken ct) + private Task ExecuteCompilationWithDefaultServiceAsync(CompileSchema request, string playModeStopWarning, CancellationToken ct) { Debug.Assert(request != null, "request must not be null"); CompilationExecutionService executionService = new( _compileResultSessionRepository, _pendingCompileSessionRepository); - return executionService.ExecuteCompilationAsync(request, pausePointWarning, ct); + return executionService.ExecuteCompilationAsync(request, playModeStopWarning, ct); } } }