diff --git a/Editor/DeNA.Anjin.Editor.asmdef b/Editor/DeNA.Anjin.Editor.asmdef index bf7a167..498fee2 100644 --- a/Editor/DeNA.Anjin.Editor.asmdef +++ b/Editor/DeNA.Anjin.Editor.asmdef @@ -2,6 +2,7 @@ "name": "DeNA.Anjin.Editor", "rootNamespace": "DeNA.Anjin", "references": [ + "UniTask", "DeNA.Anjin", "TestHelper.Monkey.Annotations", "TestHelper.Monkey" diff --git a/Editor/UI/Settings/AutopilotSettingsEditor.cs b/Editor/UI/Settings/AutopilotSettingsEditor.cs index 7612f15..86a127a 100644 --- a/Editor/UI/Settings/AutopilotSettingsEditor.cs +++ b/Editor/UI/Settings/AutopilotSettingsEditor.cs @@ -2,6 +2,7 @@ // This software is released under the MIT License. using System.Diagnostics.CodeAnalysis; +using Cysharp.Threading.Tasks; using DeNA.Anjin.Settings; using UnityEditor; using UnityEngine; @@ -137,7 +138,7 @@ public override void OnInspectorGUI() { if (GUILayout.Button(s_stopButton)) { - Stop(); + Stop().Forget(); } } else @@ -157,10 +158,10 @@ private static void DrawHeader(string label) } [SuppressMessage("ApiDesign", "RS0030")] - internal void Stop() + internal async UniTask Stop() { var autopilot = FindObjectOfType(); - autopilot.Terminate(Autopilot.ExitCode.Normally); + await autopilot.TerminateAsync(Autopilot.ExitCode.Normally); } internal void Launch(AutopilotState state) diff --git a/Runtime/AgentDispatcher.cs b/Runtime/AgentDispatcher.cs index 3046046..1352ae3 100644 --- a/Runtime/AgentDispatcher.cs +++ b/Runtime/AgentDispatcher.cs @@ -1,6 +1,7 @@ // Copyright (c) 2023 DeNA Co., Ltd. // This software is released under the MIT License. +using System; using DeNA.Anjin.Agents; using DeNA.Anjin.Settings; using DeNA.Anjin.Utilities; @@ -106,9 +107,17 @@ private void DispatchAgent(AbstractAgent agent) agent.Logger = _logger; agent.Random = _randomFactory.CreateRandom(); - agent.Run(token).Forget(); - - _logger.Log($"Agent {agentName} dispatched!"); + + try + { + agent.Run(token).Forget(); + _logger.Log($"Agent {agentName} dispatched!"); + } + catch (Exception e) + { + Debug.LogException(e); + _logger.Log($"Agent {agentName} dispatched but immediately threw an error!"); + } } } } diff --git a/Runtime/Autopilot.cs b/Runtime/Autopilot.cs index 7bf1ac7..c5f67cf 100644 --- a/Runtime/Autopilot.cs +++ b/Runtime/Autopilot.cs @@ -3,6 +3,8 @@ using System; using System.Collections; +using System.Threading; +using Cysharp.Threading.Tasks; using DeNA.Anjin.Reporters; using DeNA.Anjin.Settings; using DeNA.Anjin.Utilities; @@ -62,15 +64,18 @@ private void Start() _randomFactory = new RandomFactory(seed); _logger.Log($"Random seed is {seed}"); + + // NOTE: Registering logMessageReceived must be placed before DispatchByScene. + // Because some agent can throw an error immediately, so reporter can miss the error if + // registering logMessageReceived is placed after DispatchByScene. + _reporter = new SlackReporter(_settings, new SlackAPI()); + _logMessageHandler = new LogMessageHandler(_settings, _reporter); + Application.logMessageReceived += _logMessageHandler.HandleLog; _dispatcher = new AgentDispatcher(_settings, _logger, _randomFactory); _dispatcher.DispatchByScene(SceneManager.GetActiveScene()); SceneManager.activeSceneChanged += _dispatcher.DispatchByScene; - _reporter = new SlackReporter(_settings, new SlackAPI()); - _logMessageHandler = new LogMessageHandler(_settings, _reporter); - Application.logMessageReceived += _logMessageHandler.HandleLog; - if (_settings.lifespanSec > 0) { StartCoroutine(Lifespan(_settings.lifespanSec)); @@ -92,7 +97,7 @@ private void Start() private IEnumerator Lifespan(int timeoutSec) { yield return new WaitForSecondsRealtime(timeoutSec); - Terminate(ExitCode.Normally); + yield return UniTask.ToCoroutine(() => TerminateAsync(ExitCode.Normally)); } /// @@ -101,7 +106,8 @@ private IEnumerator Lifespan(int timeoutSec) /// Exit code for Unity Editor /// Log message string when terminate by the log message /// Stack trace when terminate by the log message - public void Terminate(ExitCode exitCode, string logString = null, string stackTrace = null) + /// A task awaits termination get completed + public async UniTask TerminateAsync(ExitCode exitCode, string logString = null, string stackTrace = null, CancellationToken token = default) { if (_dispatcher != null) { @@ -132,12 +138,29 @@ public void Terminate(ExitCode exitCode, string logString = null, string stackTr // Terminate when ran specified time. _logger.Log("Stop playing by autopilot"); _state.exitCode = exitCode; + // XXX: Avoid a problem that Editor stay playing despite isPlaying get assigned false. + // SEE: https://github.com/DeNA/Anjin/issues/20 + await UniTask.DelayFrame(1, cancellationToken: token); UnityEditor.EditorApplication.isPlaying = false; // Call Launcher.OnChangePlayModeState() so terminates Unity editor, when launch from CLI. #else _logger.Log($"Exit Unity-editor by autopilot, exit code={exitCode}"); Application.Quit((int)exitCode); + await UniTask.CompletedTask; #endif } + + + /// + /// Terminate autopilot + /// + /// Exit code for Unity Editor + /// Log message string when terminate by the log message + /// Stack trace when terminate by the log message + [Obsolete("Use " + nameof(TerminateAsync))] + public void Terminate(ExitCode exitCode, string logString = null, string stackTrace = null) + { + TerminateAsync(exitCode, logString, stackTrace).Forget(); + } } } diff --git a/Runtime/Utilities/LogMessageHandler.cs b/Runtime/Utilities/LogMessageHandler.cs index ae777df..6da59d9 100644 --- a/Runtime/Utilities/LogMessageHandler.cs +++ b/Runtime/Utilities/LogMessageHandler.cs @@ -44,7 +44,7 @@ public async void HandleLog(string logString, string stackTrace, LogType type) var autopilot = Object.FindObjectOfType(); if (autopilot != null) { - autopilot.Terminate(Autopilot.ExitCode.AutopilotFailed, logString, stackTrace); + await autopilot.TerminateAsync(Autopilot.ExitCode.AutopilotFailed, logString, stackTrace); } } diff --git a/Tests/Runtime/LauncherTest.cs b/Tests/Runtime/LauncherTest.cs index 68129ce..09ece5b 100644 --- a/Tests/Runtime/LauncherTest.cs +++ b/Tests/Runtime/LauncherTest.cs @@ -39,7 +39,7 @@ public async Task Launch_InPlayMode_RunAutopilot() var autopilot = Object.FindObjectOfType(); Assert.That((bool)autopilot, Is.True, "Autopilot object is alive"); - autopilot.Terminate(Autopilot.ExitCode.Normally); + await autopilot.TerminateAsync(Autopilot.ExitCode.Normally); } [Test] @@ -66,7 +66,7 @@ public async Task Stop_TerminateAutopilotAndKeepPlayMode() var state = AutopilotState.Instance; editor.Launch(state); await Task.Delay(2000); - editor.Stop(); // Note: If Autopilot stops for life before Stop, a NullReference exception is raised here. + await editor.Stop(); // Note: If Autopilot stops for life before Stop, a NullReference exception is raised here. Assert.That(state.IsRunning, Is.False, "AutopilotState is terminated"); Assert.That(EditorApplication.isPlaying, Is.True, "Keep play mode"); diff --git a/Tests/Runtime/TestDoubles/StubClickAgent.cs b/Tests/Runtime/TestDoubles/StubClickAgent.cs index d384def..e03b57e 100644 --- a/Tests/Runtime/TestDoubles/StubClickAgent.cs +++ b/Tests/Runtime/TestDoubles/StubClickAgent.cs @@ -9,10 +9,18 @@ namespace DeNA.Anjin.TestDoubles { + /// + /// A test double for agent. This agent immediately click game objects that have the name specified + /// public class StubClickAgent : AbstractAgent { + /// + /// A name of game objects to click + /// [SerializeField] public string targetName; + + /// public override UniTask Run(CancellationToken token) { foreach (var obj in FindObjectsByType(FindObjectsSortMode.None)) @@ -33,6 +41,7 @@ public override UniTask Run(CancellationToken token) handler.OnPointerClick(new PointerEventData(EventSystem.current)); } + return UniTask.CompletedTask; } }