diff --git a/.editorconfig b/.editorconfig index 24a7136..8c560ab 100644 --- a/.editorconfig +++ b/.editorconfig @@ -145,7 +145,7 @@ dotnet_naming_symbols.all_members.applicable_kinds = * dotnet_naming_style.pascal_case_style.capitalization = pascal_case -file_header_template = Copyright (c) ${CurrentDate.Year} DeNA Co., Ltd.\nThis software is released under the MIT License. +file_header_template = Copyright (c) 2023-${CurrentDate.Year} DeNA Co., Ltd.\nThis software is released under the MIT License. # RS0016: Only enable if API files are present dotnet_public_api_analyzer.require_api_files = true diff --git a/Editor/Commandline.cs b/Editor/Commandline.cs index 998dcd6..8b02a5b 100644 --- a/Editor/Commandline.cs +++ b/Editor/Commandline.cs @@ -61,7 +61,7 @@ private static void Bootstrap() // Activate autopilot and enter play mode var state = AutopilotState.Instance; - state.launchFrom = AutopilotState.LaunchType.Commandline; + state.launchFrom = LaunchType.Commandline; state.settings = settings; EditorApplication.isPlaying = true; diff --git a/Editor/UI/Settings/AutopilotSettingsEditor.cs b/Editor/UI/Settings/AutopilotSettingsEditor.cs index abd5f44..734c508 100644 --- a/Editor/UI/Settings/AutopilotSettingsEditor.cs +++ b/Editor/UI/Settings/AutopilotSettingsEditor.cs @@ -170,7 +170,7 @@ private static void DrawHeader(string label) internal async UniTask Stop() { var autopilot = FindObjectOfType(); - await autopilot.TerminateAsync(Autopilot.ExitCode.Normally); + await autopilot.TerminateAsync(ExitCode.Normally); } internal void Launch(AutopilotState state) @@ -178,12 +178,12 @@ internal void Launch(AutopilotState state) state.settings = _settings; if (EditorApplication.isPlaying) { - state.launchFrom = AutopilotState.LaunchType.EditorPlayMode; + state.launchFrom = LaunchType.EditorPlayMode; Launcher.Run(); } else { - state.launchFrom = AutopilotState.LaunchType.EditorEditMode; + state.launchFrom = LaunchType.EditorEditMode; EditorApplication.isPlaying = true; } } diff --git a/README.md b/README.md index 43ab29b..4a3136e 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,10 @@ You can choose from two typical installation methods. ![](Documentation~/PackageManager_Dark.png/#gh-dark-mode-only) ![](Documentation~/PackageManager_Light.png/#gh-light-mode-only) -> **Note** +> [!NOTE] > Do not forget to add `com.cysharp` and `com.nowsprinting` into scopes. These are used within Anjin. -> **Note** +> [!NOTE] > Required install [Unity Test Framework](https://docs.unity3d.com/Packages/com.unity.test-framework@latest) package v1.3 or later for running tests (when adding to the `testables` in package.json). ### Install via OpenUPM-CLI @@ -77,7 +77,7 @@ After installing the UPM package in the game title project, configure and implem - Implement a custom Agent if necessary - Implement initialization process as needed -> **Note** +> [!NOTE] > Set `UNITY_EDITOR || DENA_AUTOPILOT_ENABLE` in the Define Constraints of the Assembly Definition File to which they belong to exclude them from release builds. @@ -169,7 +169,27 @@ Open the AutopilotSettings file you wish to run in the inspector and click the * After the set run time has elapsed, or as in normal play mode, clicking the Play button will stop the program. -### 2. Run from commandline +### 2. Run from Play Mode test + +Autopilot works within your test code using the async method `LauncherFromTest.AutopilotAsync(string)`. +Specify the `AutopilotSettings` file path as the argument. + +``` +[Test] +public async Task LaunchAutopilotFromTest() +{ + await LauncherFromTest.AutopilotAsync("Assets/Path/To/AutopilotSettings.asset"); +} +``` + +> [!NOTE] +> If an error is detected in running, it will be output to `LogError` and the test will fail. + +> [!WARNING] +> The default timeout for tests is 3 minutes. If the autopilot execution time exceeds 3 minutes, please specify the timeout time with the `Timeout` attribute. + + +### 3. Run from commandline To execute from the commandline, specify the following arguments. @@ -241,7 +261,7 @@ See **Anjin Annotations** below for more information. This is an Agent that playback uGUI operations with the Recorded Playback feature of the [Automated QA](https://docs.unity3d.com/Packages/com.unity.automated-testing@latest) package. -> **Note** +> [!NOTE] > The Automated QA package is in the preview stage. Please note that destructive changes may occur, and the package itself may be discontinued or withdrawn. The following can be set in an instance (.asset file) of this Agent. diff --git a/README_ja.md b/README_ja.md index 850b12a..6b145b5 100644 --- a/README_ja.md +++ b/README_ja.md @@ -40,10 +40,10 @@ Click [English](./README.md) for English page if you need. ![](Documentation~/PackageManager_Dark.png/#gh-dark-mode-only) ![](Documentation~/PackageManager_Light.png/#gh-light-mode-only) -> **Note** +> [!NOTE] > scopesに `com.cysharp` と `com.nowsprinting` を忘れず追加してください。Anjin内で使用しています。 -> **Note** +> [!NOTE] > Anjinパッケージ内のテストを実行する場合(package.jsonの `testables` に追加するとき)は、[Unity Test Framework](https://docs.unity3d.com/Packages/com.unity.test-framework@latest) パッケージ v1.3以上が必要です。 ### openupm-cli を使用する場合 @@ -76,7 +76,7 @@ Anjinを起動すると、次のファイルが自動生成されます。 - 必要に応じて専用Agentの実装 - 必要に応じて初期化処理の実装 -> **Note** +> [!NOTE] > ゲームタイトル固有のオートパイロット向けコードは、属するAssembly Definition FileのDefine Constraintsに `UNITY_EDITOR || DENA_AUTOPILOT_ENABLE` を設定することで、リリースビルドから除外できます @@ -168,7 +168,27 @@ Slack通知に付与するメンションを設定します。 設定された実行時間が経過するか、通常の再生モードと同じく再生ボタンクリックで停止します。 -### 2. コマンドラインから実行 +### 2. Play Modeテストから実行 + +非同期メソッド `LauncherFromTest.AutopilotAsync(string)` を使用することで、テストコード内でオートパイロットが動作します。 +引数には `AutopilotSettings` ファイルパスを指定します。 + +``` +[Test] +public async Task LaunchAutopilotFromTest() +{ + await LauncherFromTest.AutopilotAsync("Assets/Path/To/AutopilotSettings.asset"); +} +``` + +> [!NOTE] +> 実行中にエラーを検知すると `LogError` が出力されるため、そのテストは失敗と判定されます。 + +> [!WARNING] +> テストのデフォルトタイムアウトは3分です。オートパイロットの実行時間が3分を超える場合は `Timeout` 属性でタイムアウト時間を指定してください。 + + +### 3. コマンドラインから実行 コマンドラインから実行する場合、以下の引数を指定します。 @@ -242,7 +262,7 @@ uGUIのコンポーネントをランダムに操作するAgentです。 [Automated QA](https://docs.unity3d.com/Packages/com.unity.automated-testing@latest)パッケージのRecorded Playback機能でレコーディングしたuGUI操作を再生するAgentです。 -> **Note** +> [!NOTE] > Automated QAパッケージはプレビュー段階のため、破壊的変更や、パッケージ自体の開発中止・廃止もありえる点、ご注意ください。 このAgentのインスタンス(.assetファイル)には以下を設定できます。 diff --git a/Runtime/Autopilot.cs b/Runtime/Autopilot.cs index f6d931e..561f5b0 100644 --- a/Runtime/Autopilot.cs +++ b/Runtime/Autopilot.cs @@ -19,27 +19,6 @@ namespace DeNA.Anjin /// public class Autopilot : MonoBehaviour { - /// - /// Exit code for autopilot running - /// - public enum ExitCode - { - /// - /// Normally exit - /// - Normally = 0, - - /// - /// Exit by un catch Exceptions - /// - UnCatchExceptions = 1, - - /// - /// Exit by fault in log message - /// - AutopilotFailed = 2 - } - private ILogger _logger; private RandomFactory _randomFactory; private IAgentDispatcher _dispatcher; @@ -114,7 +93,8 @@ private IEnumerator Lifespan(int timeoutSec) /// Log message string when terminate by the log message /// Stack trace when terminate by the log message /// A task awaits termination get completed - public async UniTask TerminateAsync(ExitCode exitCode, string logString = null, string stackTrace = null, CancellationToken token = default) + public async UniTask TerminateAsync(ExitCode exitCode, string logString = null, string stackTrace = null, + CancellationToken token = default) { if (_dispatcher != null) { @@ -134,10 +114,16 @@ public async UniTask TerminateAsync(ExitCode exitCode, string logString = null, Destroy(this.gameObject); - if (_state.launchFrom == AutopilotState.LaunchType.EditorPlayMode) + if (_state.IsLaunchFromPlayMode) { _logger.Log("Terminate autopilot"); _state.Reset(); +#if UNITY_INCLUDE_TESTS + if (_state.launchFrom == LaunchType.PlayModeTests && exitCode != ExitCode.Normally) + { + throw new NUnit.Framework.AssertionException($"Autopilot failed with exit code {exitCode}"); + } +#endif return; // Only terminate autopilot run if starting from play mode. } diff --git a/Runtime/ExitCode.cs b/Runtime/ExitCode.cs new file mode 100644 index 0000000..e9ea4a8 --- /dev/null +++ b/Runtime/ExitCode.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +namespace DeNA.Anjin +{ + /// + /// Exit code for autopilot running + /// + public enum ExitCode + { + /// + /// Normally exit + /// + Normally = 0, + + /// + /// Exit by un catch Exceptions + /// + UnCatchExceptions = 1, + + /// + /// Exit by fault in log message + /// + AutopilotFailed = 2 + } +} diff --git a/Runtime/ExitCode.cs.meta b/Runtime/ExitCode.cs.meta new file mode 100644 index 0000000..7063461 --- /dev/null +++ b/Runtime/ExitCode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5238adde16cc4df8bd68d61826fba98c +timeCreated: 1710279505 \ No newline at end of file diff --git a/Runtime/LaunchType.cs b/Runtime/LaunchType.cs new file mode 100644 index 0000000..4095c2e --- /dev/null +++ b/Runtime/LaunchType.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +namespace DeNA.Anjin +{ + /// + /// Define of what autopilot was launched by + /// + public enum LaunchType + { + /// + /// Not launch yet + /// + NotSet = 0, + + /// + /// Launch via Edit mode + /// + EditorEditMode, + + /// + /// Launch via Play mode + /// + EditorPlayMode, + + /// + /// Launch via Play mode tests + /// + PlayModeTests, + + /// + /// Launch from commandline interface + /// When autopilot is finished, Unity editor is also exit. + /// + Commandline, + + /// + /// Launch on standalone platform player build (not support yet) + /// + Runtime, + } +} diff --git a/Runtime/LaunchType.cs.meta b/Runtime/LaunchType.cs.meta new file mode 100644 index 0000000..625f3d5 --- /dev/null +++ b/Runtime/LaunchType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3e29989dd48141fdba0ba0f253fd90a8 +timeCreated: 1710279555 \ No newline at end of file diff --git a/Runtime/Launcher.cs b/Runtime/Launcher.cs index 4920eca..e694e96 100644 --- a/Runtime/Launcher.cs +++ b/Runtime/Launcher.cs @@ -41,7 +41,7 @@ public static void Run() return; // Normally play mode (not run autopilot) } - if (state.launchFrom != AutopilotState.LaunchType.EditorPlayMode) + if (!state.IsLaunchFromPlayMode) { EditorApplication.playModeStateChanged += OnChangePlayModeState; } @@ -83,12 +83,12 @@ private static void OnChangePlayModeState(PlayModeStateChange playModeStateChang var state = AutopilotState.Instance; switch (state.launchFrom) { - case AutopilotState.LaunchType.EditorEditMode: + case LaunchType.EditorEditMode: Debug.Log("Exit play mode"); state.Reset(); break; - case AutopilotState.LaunchType.Commandline: + case LaunchType.Commandline: // Exit Unity when returning from play mode to edit mode. // Because it may freeze when exiting without going through edit mode. var exitCode = (int)state.exitCode; diff --git a/Runtime/LauncherFromTest.cs b/Runtime/LauncherFromTest.cs new file mode 100644 index 0000000..2628d11 --- /dev/null +++ b/Runtime/LauncherFromTest.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +using Cysharp.Threading.Tasks; +using DeNA.Anjin.Settings; +using NUnit.Framework; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace DeNA.Anjin +{ + /// + /// Launch from Play Mode test interface. + /// + public static class LauncherFromTest + { + /// + /// Run autopilot from Play Mode test. + /// If an error is detected in running, it will be output to `LogError` and the test will fail. + /// + /// Autopilot settings + public static async UniTask AutopilotAsync(AutopilotSettings settings) + { +#if UNITY_EDITOR + if (!EditorApplication.isPlaying) + { + throw new AssertionException("Not support run on Edit Mode tests"); + } +#endif + var state = AutopilotState.Instance; + if (state.IsRunning) + { + throw new AssertionException("Autopilot is already running"); + } + + state.launchFrom = LaunchType.PlayModeTests; + state.settings = settings; + Launcher.Run(); + + await UniTask.WaitUntil(() => !state.IsRunning); + } + + /// + /// Run autopilot from Play Mode test. + /// If an error is detected in running, it will be output to `LogError` and the test will fail. + /// + /// Asset file path for autopilot settings + public static async UniTask AutopilotAsync(string autopilotSettingsPath) + { + var settings = AssetDatabase.LoadAssetAtPath(autopilotSettingsPath); + await AutopilotAsync(settings); + } + } +} diff --git a/Runtime/LauncherFromTest.cs.meta b/Runtime/LauncherFromTest.cs.meta new file mode 100644 index 0000000..2471d25 --- /dev/null +++ b/Runtime/LauncherFromTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 67daa96e39994bae9c5a2e9382d2d1f5 +timeCreated: 1710574470 \ No newline at end of file diff --git a/Runtime/Settings/AutopilotState.cs b/Runtime/Settings/AutopilotState.cs index 94faedd..5e917ba 100644 --- a/Runtime/Settings/AutopilotState.cs +++ b/Runtime/Settings/AutopilotState.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023 DeNA Co., Ltd. +// Copyright (c) 2023-2024 DeNA Co., Ltd. // This software is released under the MIT License. using System; @@ -16,38 +16,6 @@ namespace DeNA.Anjin.Settings /// public class AutopilotState : ScriptableObject { - /// - /// Define of what autopilot was launched by - /// - public enum LaunchType - { - /// - /// Not launch yet - /// - NotSet = 0, - - /// - /// Launch via Edit mode - /// - EditorEditMode, - - /// - /// Launch via Play mode - /// - EditorPlayMode, - - /// - /// Launch from commandline interface - /// When autopilot is finished, Unity editor is also exit. - /// - Commandline, - - /// - /// Launch on standalone platform player build (not support yet) - /// - Runtime, - } - /// /// Launch type /// @@ -61,7 +29,7 @@ public enum LaunchType /// /// Exit code when terminate autopilot from commandline interface /// - [HideInInspector] public Autopilot.ExitCode exitCode; + [HideInInspector] public ExitCode exitCode; /// /// Reset run state @@ -70,7 +38,7 @@ public void Reset() { launchFrom = LaunchType.NotSet; settings = null; - exitCode = Autopilot.ExitCode.Normally; + exitCode = ExitCode.Normally; } /// @@ -88,6 +56,17 @@ public bool IsRunning } } + /// + /// Is launch from play mode (Editor play mode or Play Mode tests) + /// + public bool IsLaunchFromPlayMode + { + get + { + return launchFrom == LaunchType.EditorPlayMode || launchFrom == LaunchType.PlayModeTests; + } + } + [NonSerialized] private static AutopilotState s_instance; /// diff --git a/Runtime/Utilities/LogMessageHandler.cs b/Runtime/Utilities/LogMessageHandler.cs index 6563470..ab3b54c 100644 --- a/Runtime/Utilities/LogMessageHandler.cs +++ b/Runtime/Utilities/LogMessageHandler.cs @@ -51,7 +51,7 @@ public async void HandleLog(string logString, string stackTrace, LogType type) var autopilot = Object.FindObjectOfType(); if (autopilot != null) { - await autopilot.TerminateAsync(Autopilot.ExitCode.AutopilotFailed, logString, stackTrace); + await autopilot.TerminateAsync(ExitCode.AutopilotFailed, logString, stackTrace); } } diff --git a/Tests/Runtime/LauncherFromTestTest.cs b/Tests/Runtime/LauncherFromTestTest.cs new file mode 100644 index 0000000..272935a --- /dev/null +++ b/Tests/Runtime/LauncherFromTestTest.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using DeNA.Anjin.Agents; +using DeNA.Anjin.Settings; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +namespace DeNA.Anjin +{ + [TestFixture] + public class LauncherFromTestTest + { + [Test] + public async Task AutopilotAsync_RunAutopilot() + { + var agent = ScriptableObject.CreateInstance(typeof(DoNothingAgent)) as DoNothingAgent; + agent.lifespanSec = 1; + + var settings = ScriptableObject.CreateInstance(typeof(AutopilotSettings)) as AutopilotSettings; + settings.sceneAgentMaps = new List(); + settings.fallbackAgent = agent; + settings.lifespanSec = 2; + + var beforeTimestamp = Time.time; + await LauncherFromTest.AutopilotAsync(settings); + + var afterTimestamp = Time.time; + Assert.That(afterTimestamp - beforeTimestamp, Is.GreaterThan(2f), "Autopilot is running for 2 seconds"); + + var state = AutopilotState.Instance; + Assert.That(state.IsRunning, Is.False, "AutopilotState is terminated"); + Assert.That(state.launchFrom, Is.EqualTo(LaunchType.NotSet), "Launch from is reset"); + Assert.That(state.exitCode, Is.EqualTo(ExitCode.Normally), "Exit code is reset"); + Assert.That(EditorApplication.isPlaying, Is.True, "Keep play mode"); + } + + [Test] + public async Task AutopilotAsync_WithAssetFile_RunAutopilot() + { + const string AssetPath = "Packages/com.dena.anjin/Tests/TestAssets/AutopilotSettingsForTests.asset"; + + var beforeTimestamp = Time.time; + await LauncherFromTest.AutopilotAsync(AssetPath); + + var afterTimestamp = Time.time; + Assert.That(afterTimestamp - beforeTimestamp, Is.GreaterThan(2f), "Autopilot is running for 2 seconds"); + + var state = AutopilotState.Instance; + Assert.That(state.IsRunning, Is.False, "AutopilotState is terminated"); + Assert.That(state.launchFrom, Is.EqualTo(LaunchType.NotSet), "Launch from is reset"); + Assert.That(state.exitCode, Is.EqualTo(ExitCode.Normally), "Exit code is reset"); + Assert.That(EditorApplication.isPlaying, Is.True, "Keep play mode"); + } + } +} diff --git a/Tests/Runtime/LauncherFromTestTest.cs.meta b/Tests/Runtime/LauncherFromTestTest.cs.meta new file mode 100644 index 0000000..6438141 --- /dev/null +++ b/Tests/Runtime/LauncherFromTestTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 54bc6c4002634baa983e79cb35ea447f +timeCreated: 1710578007 \ No newline at end of file diff --git a/Tests/Runtime/LauncherTest.cs b/Tests/Runtime/LauncherTest.cs index 09ece5b..2c9a053 100644 --- a/Tests/Runtime/LauncherTest.cs +++ b/Tests/Runtime/LauncherTest.cs @@ -33,13 +33,13 @@ public async Task Launch_InPlayMode_RunAutopilot() await Task.Delay(200); - Assert.That(state.launchFrom, Is.EqualTo(AutopilotState.LaunchType.EditorPlayMode)); + Assert.That(state.launchFrom, Is.EqualTo(LaunchType.EditorPlayMode)); Assert.That(state.IsRunning, Is.True, "AutopilotState is running"); var autopilot = Object.FindObjectOfType(); Assert.That((bool)autopilot, Is.True, "Autopilot object is alive"); - await autopilot.TerminateAsync(Autopilot.ExitCode.Normally); + await autopilot.TerminateAsync(ExitCode.Normally); } [Test]