diff --git a/Editor/Localization/ja.po b/Editor/Localization/ja.po index dcebe08..051f51b 100644 --- a/Editor/Localization/ja.po +++ b/Editor/Localization/ja.po @@ -103,50 +103,6 @@ msgstr "Reporters" msgid "List of Reporters to be called on Autopilot terminate." msgstr "オートパイロット終了時に通知を行なうReporterを指定します" -# Header: Error Handling Settings -msgid "Error Handling Settings" -msgstr "エラーハンドリング設定" - -# handleException -msgid "Handle Exception" -msgstr "例外を報告する" - -# handleException tooltip -msgid "Notify when Exception detected in log" -msgstr "例外ログを検知したらSlack通知します" - -# handleError -msgid "Handle Error" -msgstr "エラーを報告する" - -# handleError tooltip -msgid "Notify when Error detected in log" -msgstr "エラーログを検知したらSlack通知します" - -# handleAssert -msgid "Handle Assert" -msgstr "アサートを報告する" - -# handleAssert tooltip -msgid "Notify when Assert detected in log" -msgstr "アサートログを検知したらSlack通知します" - -# handleWarning -msgid "Handle Warning" -msgstr "警告を報告する" - -# handleWarning tooltip -msgid "Notify when Warning detected in log" -msgstr "警告ログを検知したらSlack通知します" - -# ignoreMessages -msgid "Ignore Messages" -msgstr "無視するメッセージ" - -# ignoreMessages tooltip -msgid "Do not send notifications when log messages contain this string" -msgstr "ログメッセージの中にこの文字列が含まれていたらSlack通知を行ないません" - # Autopilot実行ボタン msgid "Run" msgstr "実行" @@ -384,6 +340,7 @@ msgstr "ステレオキャプチャモード" msgid "The eye texture to capture when stereo rendering is enabled. Neither this nor Resolution Factor can be specified" msgstr "ステレオレンダリングが有効な場合にどちらのカメラを使用するかを指定できます。拡大係数と同時には設定できません" + #: Editor/UI/Agents/UGUIPlaybackAgentEditor.cs # recordedJson @@ -395,6 +352,49 @@ msgid "JSON file recorded by AutomatedQA package" msgstr "Automated QAパッケージのRecorded Playbackウィンドウで記録したjsonファイルを設定します" +#: Editor/UI/Agents/ErrorHandlerAgent.cs + +# handleException +msgid "Handle Exception" +msgstr "例外を捕捉" + +# handleException tooltip +msgid "Specify an Autopilot terminates or only reports when an Exception is detected in the log" +msgstr "例外ログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します" + +# handleError +msgid "Handle Error" +msgstr "エラーを検知" + +# handleError tooltip +msgid "Specify an Autopilot terminates or only reports when an Error is detected in the log" +msgstr "エラーログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します" + +# handleAssert +msgid "Handle Assert" +msgstr "アサートを検知" + +# handleAssert tooltip +msgid "Specify an Autopilot terminates or only reports when an Assert is detected in the log" +msgstr "アサートログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します" + +# handleWarning +msgid "Handle Warning" +msgstr "警告を検知" + +# handleWarning tooltip +msgid "Specify an Autopilot terminates or only reports when an Warning is detected in the log" +msgstr "警告ログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します" + +# ignoreMessages +msgid "Ignore Messages" +msgstr "無視するメッセージ" + +# ignoreMessages tooltip +msgid "Log messages containing the specified strings will be ignored from the stop condition. Regex is also available; escape is a single backslash (\)." +msgstr "指定された文字列を含むログメッセージは停止条件から無視されます。正規表現も使用できます。エスケープは単一のバックスラッシュ (\) です" + + #: Editor/UI/Loggers/ 共通 # description (same as AutopilotSettingsEditor.cs) diff --git a/Editor/UI/Agents/ErrorHandlerAgentEditor.cs b/Editor/UI/Agents/ErrorHandlerAgentEditor.cs new file mode 100644 index 0000000..f38ca22 --- /dev/null +++ b/Editor/UI/Agents/ErrorHandlerAgentEditor.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +using DeNA.Anjin.Agents; +using UnityEditor; +using UnityEngine; + +namespace DeNA.Anjin.Editor.UI.Agents +{ + /// + /// Editor GUI for ErrorHandlerAgent + /// + [CustomEditor(typeof(ErrorHandlerAgent))] + public class ErrorHandlerAgentEditor : UnityEditor.Editor + { + private const float SpacerPixels = 10f; + + // @formatter:off + private static readonly string s_description = L10n.Tr("Description"); + private static readonly string s_descriptionTooltip = L10n.Tr("Description about this Agent instance"); + private SerializedProperty _descriptionProp; + private GUIContent _descriptionLabel; + + private static readonly string s_handleException = L10n.Tr("Handle Exception"); + private static readonly string s_handleExceptionTooltip = L10n.Tr("Specify an Autopilot terminates or only reports when an Exception is detected in the log"); + private SerializedProperty _handleExceptionProp; + private GUIContent _handleExceptionLabel; + + private static readonly string s_handleError = L10n.Tr("Handle Error"); + private static readonly string s_handleErrorTooltip = L10n.Tr("Specify an Autopilot terminates or only reports when an Error is detected in the log"); + private SerializedProperty _handleErrorProp; + private GUIContent _handleErrorLabel; + + private static readonly string s_handleAssert = L10n.Tr("Handle Assert"); + private static readonly string s_handleAssertTooltip = L10n.Tr("Specify an Autopilot terminates or only reports when an Assert is detected in the log"); + private SerializedProperty _handleAssertProp; + private GUIContent _handleAssertLabel; + + private static readonly string s_handleWarning = L10n.Tr("Handle Warning"); + private static readonly string s_handleWarningTooltip = L10n.Tr("Specify an Autopilot terminates or only reports when an Warning is detected in the log"); + private SerializedProperty _handleWarningProp; + private GUIContent _handleWarningLabel; + + private static readonly string s_ignoreMessages = L10n.Tr("Ignore Messages"); + private static readonly string s_ignoreMessagesTooltip = L10n.Tr("Log messages containing the specified strings will be ignored from the stop condition. Regex is also available; escape is a single backslash (\\)."); + private SerializedProperty _ignoreMessagesProp; + private GUIContent _ignoreMessagesLabel; + // @formatter:on + + private void OnEnable() + { + Initialize(); + } + + private void Initialize() + { + _descriptionProp = serializedObject.FindProperty(nameof(ErrorHandlerAgent.description)); + _descriptionLabel = new GUIContent(s_description, s_descriptionTooltip); + + _handleExceptionProp = serializedObject.FindProperty(nameof(ErrorHandlerAgent.handleException)); + _handleExceptionLabel = new GUIContent(s_handleException, s_handleExceptionTooltip); + + _handleErrorProp = serializedObject.FindProperty(nameof(ErrorHandlerAgent.handleError)); + _handleErrorLabel = new GUIContent(s_handleError, s_handleErrorTooltip); + + _handleAssertProp = serializedObject.FindProperty(nameof(ErrorHandlerAgent.handleAssert)); + _handleAssertLabel = new GUIContent(s_handleAssert, s_handleAssertTooltip); + + _handleWarningProp = serializedObject.FindProperty(nameof(ErrorHandlerAgent.handleWarning)); + _handleWarningLabel = new GUIContent(s_handleWarning, s_handleWarningTooltip); + + _ignoreMessagesProp = serializedObject.FindProperty(nameof(ErrorHandlerAgent.ignoreMessages)); + _ignoreMessagesLabel = new GUIContent(s_ignoreMessages, s_ignoreMessagesTooltip); + } + + /// + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_descriptionProp, _descriptionLabel); + GUILayout.Space(SpacerPixels); + + EditorGUILayout.PropertyField(_handleExceptionProp, _handleExceptionLabel); + EditorGUILayout.PropertyField(_handleErrorProp, _handleErrorLabel); + EditorGUILayout.PropertyField(_handleAssertProp, _handleAssertLabel); + EditorGUILayout.PropertyField(_handleWarningProp, _handleWarningLabel); + GUILayout.Space(SpacerPixels); + + EditorGUILayout.PropertyField(_ignoreMessagesProp, _ignoreMessagesLabel); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/UI/Agents/ErrorHandlerAgentEditor.cs.meta b/Editor/UI/Agents/ErrorHandlerAgentEditor.cs.meta new file mode 100644 index 0000000..ea057a6 --- /dev/null +++ b/Editor/UI/Agents/ErrorHandlerAgentEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ae3db68592ab4098ae95f3bc1a4f8146 +timeCreated: 1730767312 \ No newline at end of file diff --git a/Editor/UI/Settings/AutopilotSettingsEditor.cs b/Editor/UI/Settings/AutopilotSettingsEditor.cs index 85c3c7c..4df6549 100644 --- a/Editor/UI/Settings/AutopilotSettingsEditor.cs +++ b/Editor/UI/Settings/AutopilotSettingsEditor.cs @@ -45,18 +45,6 @@ public class AutopilotSettingsEditor : UnityEditor.Editor private static readonly string s_reporters = L10n.Tr("Reporters"); private static readonly string s_reportersTooltip = L10n.Tr("List of Reporters to be called on Autopilot terminate."); - private static readonly string s_errorHandlingSettingsHeader = L10n.Tr("Error Handling Settings"); - private static readonly string s_handleException = L10n.Tr("Handle Exception"); - private static readonly string s_handleExceptionTooltip = L10n.Tr("Notify when Exception detected in log"); - private static readonly string s_handleError = L10n.Tr("Handle Error"); - private static readonly string s_handleErrorTooltip = L10n.Tr("Notify when Error detected in log"); - private static readonly string s_handleAssert = L10n.Tr("Handle Assert"); - private static readonly string s_handleAssertTooltip = L10n.Tr("Notify when Assert detected in log"); - private static readonly string s_handleWarning = L10n.Tr("Handle Warning"); - private static readonly string s_handleWarningTooltip = L10n.Tr("Notify when Warning detected in log"); - private static readonly string s_ignoreMessages = L10n.Tr("Ignore Messages"); - private static readonly string s_ignoreMessagesTooltip = L10n.Tr("Do not send notifications when log messages contain this string"); - private static readonly string s_runButton = L10n.Tr("Run"); private static readonly string s_stopButton = L10n.Tr("Stop"); private const float SpacerPixels = 10f; @@ -93,18 +81,6 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.reporters)), new GUIContent(s_reporters, s_reportersTooltip)); - DrawHeader(s_errorHandlingSettingsHeader); - EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.handleException)), - new GUIContent(s_handleException, s_handleExceptionTooltip)); - EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.handleError)), - new GUIContent(s_handleError, s_handleErrorTooltip)); - EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.handleAssert)), - new GUIContent(s_handleAssert, s_handleAssertTooltip)); - EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.handleWarning)), - new GUIContent(s_handleWarning, s_handleWarningTooltip)); - EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.ignoreMessages)), - new GUIContent(s_ignoreMessages, s_ignoreMessagesTooltip), true); - serializedObject.ApplyModifiedProperties(); GUILayout.Space(SpacerPixels); @@ -136,11 +112,7 @@ private static void DrawHeader(string label) // ReSharper disable once MemberCanBeMadeStatic.Global internal void Stop() { - var autopilot = FindObjectOfType(); - if (autopilot) - { - autopilot.TerminateAsync(ExitCode.Normally, reporting: false).Forget(); - } + Autopilot.Instance.TerminateAsync(ExitCode.Normally, reporting: false).Forget(); } internal void Launch() diff --git a/README.md b/README.md index 081ba11..207cebb 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,9 @@ Set the Agents to run by scene crossing, independent of `Scene Agent Maps` and ` The specified agents will have the same lifespan as Autopilot (i.e., use `DontDestroyOnLoad`) for specifying, e.g., `ErrorHandlerAgent` and `UGUIEmergencyExitAgent`. +> [!WARNING] +> Recommend set an [ErrorHandlerAgent](#ErrorHandlerAgent) to interrupt Autopilot if an exception occurs during execution. + #### Autopilot Run Settings This item can also be overridden from the commandline (see below). @@ -122,18 +125,6 @@ This item can also be overridden from the commandline (see below).
Reporters
Reporter to be called on Autopilot terminate.
-#### Error Handling Settings - -Set up a filter to catch abnormal log messages and notify using Reporter. - -
-
Handle Exception
Report when exception is detected in log
-
Handle Error
Report when error message is detected in log
-
Handle Assert
Report when assert message is detected in log
-
Handle Warning
Report when warning message is detected in log
-
Ignore Messages
Log messages containing this string will not be reported. Regex is also available, and escape is a single backslash (`\`).
-
- ### Generate and configure the Agent setting file (.asset) @@ -200,10 +191,6 @@ For details on each argument, see the entry of the same name in the "Generate an
LIFESPAN_SEC
Specifies the execution time limit in seconds
RANDOM_SEED
Specifies when you want to fix the seed given to the pseudo-random number generator
TIME_SCALE
Specifies the Time.timeScale. Default is 1.0
-
HANDLE_EXCEPTION
Overwrites whether to report when an exception occurs with TRUE/FALSE
-
HANDLE_ERROR
Overwrites whether to report when an error message is detected with TRUE/FALSE
-
HANDLE_ASSERT
Overwrites whether to report when an assert message is detected with TRUE/FALSE
-
HANDLE_WARNING
Overwrites whether to report when an warning message is detected with TRUE/FALSE
In both cases, the key should be prefixed with `-` and specified as `-LIFESPAN_SEC 60`. @@ -276,9 +263,6 @@ 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] -> 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.
@@ -296,6 +280,14 @@ A screenshot of the operation by Automated QA is stored under `Application.persi The `Application.persistentDataPath` for each platform can be found in the Unity manual at [Scripting API: Application.persistentDataPath](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html). +> [!WARNING] +> The Automated QA package outputs `LogType.Error` to the console when playback fails (e.g., the target Button cannot be found). The following setting is required to detect this and terminate the autopilot. +> 1. Add [ErrorHandlerAgent](#ErrorHandlerAgent) and set `Handle Error` to `Terminate Autopilot`. +> 2. Add [ConsoleLogger](#ConsoleLogger) and set `Filter LogType` to `Error` or higher. + +> [!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. + ### DoNothingAgent @@ -375,6 +367,32 @@ This Agent instance (.asset file) can contain the following.
+### ErrorHandlerAgent + +An Agent that detects abnormal log messages and terminates the Autopilot running. + +It should always be started at the same time as other Agents (that actually perform game operations). +Generally, you add to `Scene Crossing Agents` in `AutopilotSettings`. +If you want to change the settings for each Scene, use with `ParallelCompositeAgent`. + +This Agent instance (.asset file) can contain the following. + +
+
Handle Exception
Specify an Autopilot terminates or only reports when an Exception is detected in the log. + It can be overwritten with the command line argument -HANDLE_EXCEPTION, but be careful if multiple ErrorHandlerAgents are defined, they will all be overwritten with the same value.
+
Handle Error
Specify an Autopilot terminates or only reports when an Error is detected in the log. + It can be overwritten with the command line argument -HANDLE_ERROR, but be careful if multiple ErrorHandlerAgents are defined, they will all be overwritten with the same value.
+
Handle Assert
Specify an Autopilot terminates or only reports when an Assert is detected in the log. + It can be overwritten with the command line argument -HANDLE_ASSERT, but be careful if multiple ErrorHandlerAgents are defined, they will all be overwritten with the same value.
+
Handle Warning
Specify an Autopilot terminates or only reports when an Warning is detected in the log. + It can be overwritten with the command line argument -HANDLE_WARNING, but be careful if multiple ErrorHandlerAgents are defined, they will all be overwritten with the same value.
+
Ignore Messages
Log messages containing the specified strings will be ignored from the stop condition. Regex is also available; escape is a single backslash (`\`).
+
+ +> [!NOTE] +> Recommend enabling a `Handle Exception` to interrupt Autopilot if an exception occurs during execution. + + ### EmergencyExitAgent An Agent that monitors the appearance of the `EmergencyExitAnnotations` component in the `DeNA.Anjin.Annotations` assembly and clicks on it as soon as it appears. @@ -409,7 +427,7 @@ The instance of this Logger (.asset file) can have the following settings.
Output File Path
Log output file path. Specify relative path from project root or absolute path. When run on player, it will be the Application.persistentDataPath. - This setting can be overwritten with the command line argument -FILE_LOGGER_OUTPUT_PATH, but if multiple File Loggers are defined, they will all be overwritten with the same path.
+ It can be overwritten with the command line argument -FILE_LOGGER_OUTPUT_PATH, but be careful if multiple FileLoggers are defined, they will all be overwritten with the same value.
Filter LogType
To selective enable debug log message
Timestamp
Output timestamp to log entities
@@ -442,15 +460,15 @@ The instance of this Reporter (.asset file) can have the following settings.
Slack Token
OAuth token of Slack Bot used for notifications. If omitted, no notifications will be sent. - This setting can be overwritten with the command line argument -SLACK_TOKEN.
+ It can be overwritten with the command line argument -SLACK_TOKEN, but be careful if multiple SlackReporters are defined, they will all be overwritten with the same value.
Slack Channels
Channels to send notifications. If omitted, not notified. Multiple channels can be specified by separating them with commas. - This setting can be overwritten with the command line argument -SLACK_CHANNELS. - The bot must be invited to the channel.
+ Note that the bot must be invited to the channel. + It can be overwritten with the command line argument -SLACK_CHANNELS, but be careful if multiple SlackReporters are defined, they will all be overwritten with the same value.
Mention Sub Team IDs
Comma-separated sub team IDs to mention when posting error reports.
Add @here
Add @here to the post when posting error reports.
Lead Text
Lead text for error reports. It is used in OS notifications. You can specify placeholders like a "{message}".
Message
Message body template for error reports. You can specify placeholders like a "{message}".
-
Color
Attachments color for error reports.
+
Color
Attachments color for error reports. Default color is same as Slack's "danger" red.
Screenshot
Take a screenshot for error reports. Default is on.
Post if completes
Also post a report if completed autopilot normally. Default is off.
@@ -728,7 +746,7 @@ git submodule add https://github.com/dena/Anjin.git Packages/com.dena.anjin > [!WARNING] > Required install packages for running tests (when adding to the `testables` in package.json), as follows: > - [Unity Test Framework](https://docs.unity3d.com/Packages/com.unity.test-framework@latest) package v1.3.4 or later -> - [Test Helper](https://github.com/nowsprinting/test-helper) package v1.0.0 or later +> - [Test Helper](https://github.com/nowsprinting/test-helper) package v0.7.2 or later Generate a temporary project and run tests on each Unity version from the command line. diff --git a/README_ja.md b/README_ja.md index 2ff25ef..2e4e8c1 100644 --- a/README_ja.md +++ b/README_ja.md @@ -108,6 +108,9 @@ Sceneごとに自動実行を行なうAgent設定ファイル(.asset)の対 指定されたAgentの寿命はオートパイロット本体と同じになります(つまり、`DontDestroyOnLoad` を使用します)。 たとえば、`ErrorHandlerAgent` や `UGUIEmergencyExitAgent` などを指定します。 +> [!WARNING] +> 実行中に例外が発生した時点でオートパイロットを中断するために、[ErrorHandlerAgent](#ErrorHandlerAgent) の設定をお勧めします。 + #### オートパイロット実行設定 この項目は、コマンドラインから上書きもできます(後述)。 @@ -120,18 +123,6 @@ Sceneごとに自動実行を行なうAgent設定ファイル(.asset)の対
Reporters
オートパイロット終了時に通知を行なうReporterを指定します
-#### エラーハンドリング設定 - -異常系ログメッセージを捕捉してReporterで通知するフィルタを設定します。 - -
-
handle Exception
例外を検知したらReporterで通知します
-
handle Error
エラーを検知したらReporterで通知します
-
handle Assert
アサート違反を検知したらReporterで通知します
-
handle Warning
警告を検知したらReporterで通知します
-
Ignore Messages
ここに設定した文字列を含むメッセージはReporterで通知しません。正規表現も使用可能で、エスケープはバックスラッシュ1文字(`\`)です
-
- ### Agent設定ファイル(.asset)の生成 @@ -203,10 +194,6 @@ $(UNITY) \
LIFESPAN_SEC
実行時間上限を秒で指定します
RANDOM_SEED
疑似乱数発生器に与えるシードを固定したいときに指定します
TIME_SCALE
Time.timeScaleを指定します。デフォルトは1.0
-
HANDLE_EXCEPTION
例外を検知したときに通知を行なうかを TRUE/ FALSEで上書きします
-
HANDLE_ERROR
エラーを検知したときに通知を行なうかを TRUE/ FALSEで上書きします
-
HANDLE_ASSERT
アサート違反を検知したときに通知を行なうかを TRUE/ FALSEで上書きします
-
HANDLE_WARNING
警告を検知したときに通知を行なうかを TRUE/ FALSEで上書きします
いずれも、キーの先頭に`-`を付けて`-LIFESPAN_SEC 60`のように指定してください。 @@ -279,9 +266,6 @@ uGUIのコンポーネントをランダムに操作するAgentです。 [Automated QA](https://docs.unity3d.com/Packages/com.unity.automated-testing@latest)パッケージのRecorded Playback機能でレコーディングしたuGUI操作を再生するAgentです。 -> [!NOTE] -> Automated QAパッケージはプレビュー段階のため、破壊的変更や、パッケージ自体の開発中止・廃止もありえる点、ご注意ください。 - このAgentのインスタンス(.assetファイル)には以下を設定できます。
@@ -301,6 +285,14 @@ Automated QAによる操作のレコーディングは、Unityエディターの [Scripting API: Application.persistentDataPath](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html) を参照してください。 +> [!WARNING] +> Automated QAパッケージは、再生に失敗(対象のボタンが見つからないなど)したときコンソールに `LogType.Error` を出力します。これを検知してオートパイロットを停止するには、次の設定が必要です。 +> 1. [ErrorHandlerAgent](#ErrorHandlerAgent) を追加し、`Handle Error` を `Terminate Autopilot` に設定します +> 2. [ConsoleLogger](#ConsoleLogger) を追加し、`Filter LogType` に `Error` 以上を設定します + +> [!NOTE] +> Automated QAパッケージはプレビュー段階のため、破壊的変更や、パッケージ自体の開発中止・廃止もありえる点、ご注意ください。 + ### DoNothingAgent @@ -380,6 +372,32 @@ SerialCompositeAgentと組み合わせることで、一連の操作を何周も
+### ErrorHandlerAgent + +異常系のログメッセージを捕捉してオートパイロットの実行を停止するAgentです。 + +常に、他の(実際にゲーム操作を行なう)Agentと同時に起動しておく必要があります。 +一般的には、`AutopilotSettings` の `Scene Crossing Agents` に追加します。 +Sceneごとに設定を変えたい場合は、`ParallelCompositeAgent` と合わせて使用します。 + +このAgentのインスタンス(.assetファイル)には以下を設定できます。 + +
+
Handle Exception
例外ログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します。 + コマンドライン引数 -HANDLE_EXCEPTION で上書きできますが、複数のErrorHandlerAgentを定義しているとき、すべて同じ値で上書きされますので注意してください。
+
Handle Error
エラーログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します。 + コマンドライン引数 -HANDLE_ERROR で上書きできますが、複数のErrorHandlerAgentを定義しているとき、すべて同じ値で上書きされますので注意してください。
+
Handle Assert
アサートログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します。 + コマンドライン引数 -HANDLE_ASSERT で上書きできますが、複数のErrorHandlerAgentを定義しているとき、すべて同じ値で上書きされますので注意してください。
+
Handle Warning
警告ログを検知したとき、オートパイロットを停止するか、レポート送信のみ行なうかを指定します。 + コマンドライン引数 -HANDLE_WARNING で上書きできますが、複数のErrorHandlerAgentを定義しているとき、すべて同じ値で上書きされますので注意してください。
+
Ignore Messages
指定された文字列を含むログメッセージは停止条件から無視されます。正規表現も使用できます。エスケープは単一のバックスラッシュ (`\`) です。
+
+ +> [!NOTE] +> 実行中に例外が発生した時点でオートパイロットを中断するために、`Handle Exception` の有効化をお勧めします。 + + ### EmergencyExitAgent `DeNA.Anjin.Annotations` アセンブリに含まれる `EmergencyExitAnnotations` コンポーネントの出現を監視し、表示されたら即クリックするAgentです。 @@ -414,7 +432,7 @@ SerialCompositeAgentと組み合わせることで、一連の操作を何周も
出力ファイルパス
ログ出力ファイルのパス。プロジェクトルートからの相対パスまたは絶対パスを指定します。プレイヤー実行では相対パスの起点は Application.persistentDataPath になります。 - コマンドライン引数 -FILE_LOGGER_OUTPUT_PATH で上書きできますが、複数のFileLoggerを定義しているときに指定すると、すべての出力パスが上書きされますので注意してください。
+ コマンドライン引数 -FILE_LOGGER_OUTPUT_PATH で上書きできますが、複数のFileLoggerを定義しているとき、すべて同じ値で上書きされますので注意してください。
フィルタリングLogType
選択したLogType以上のログ出力のみを有効にします
タイムスタンプを出力
ログエンティティにタイムスタンプを出力します
@@ -447,15 +465,15 @@ Slackにレポート送信するReporterです。
Slack Token
通知に使用するSlack BotのOAuthトークン(省略時は通知されない)。 - コマンドライン引数 -SLACK_TOKEN で上書きできます。
+ コマンドライン引数 -SLACK_TOKEN で上書きできますが、複数のSlackReporterを定義しているとき、すべて同じ値で上書きされますので注意してください。
Slack Channels
通知を送るチャンネル(省略時は通知されない)。カンマ区切りで複数指定できます。 - コマンドライン引数 -SLACK_CHANNELS で上書きできます。 - チャンネルにはBotを招待しておく必要があります。
+ なお、チャンネルにはBotを招待しておく必要があります。 + コマンドライン引数 -SLACK_CHANNELS で上書きできますが、複数のSlackReporterを定義しているとき、すべて同じ値で上書きされますので注意してください。
Mention Sub Team IDs
エラー終了時に送信する通知をメンションするチームのIDをカンマ区切りで指定します
Add @here
エラー終了時に送信する通知に@hereを付けます
Lead Text
エラー終了時に送信する通知のリード文。OSの通知に使用されます。"{message}" のようなプレースホルダーを指定できます
Message
エラー終了時に送信するメッセージ本文のテンプレート。"{message}" のようなプレースホルダーを指定できます
-
Color
エラー終了時に送信するメッセージのアタッチメントに指定する色
+
Color
エラー終了時に送信するメッセージのアタッチメントに指定する色(デフォルト:Slackの "danger" と同じ赤色)
Screenshot
エラー終了時にスクリーンショットを撮影します(デフォルト: on)
Normally terminated report
正常終了時にもレポートを送信します(デフォルト: off)
@@ -740,7 +758,7 @@ git submodule add https://github.com/dena/Anjin.git Packages/com.dena.anjin > [!WARNING] > Anjinパッケージ内のテストを実行するためには、次のパッケージのインストールが必要です。 > - [Unity Test Framework](https://docs.unity3d.com/Packages/com.unity.test-framework@latest) package v1.3.4 or later -> - [Test Helper](https://github.com/nowsprinting/test-helper) package v1.0.0 or later +> - [Test Helper](https://github.com/nowsprinting/test-helper) package v0.7.2 or later テスト専用のUnityプロジェクトを生成し、Unityバージョンを指定してテストを実行するには、次のコマンドを実行します。 diff --git a/Runtime/Agents/ErrorHandlerAgent.cs b/Runtime/Agents/ErrorHandlerAgent.cs new file mode 100644 index 0000000..98fb981 --- /dev/null +++ b/Runtime/Agents/ErrorHandlerAgent.cs @@ -0,0 +1,219 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Cysharp.Threading.Tasks; +using DeNA.Anjin.Reporters.Slack; +using DeNA.Anjin.Settings; +using UnityEngine; + +namespace DeNA.Anjin.Agents +{ + public enum HandlingBehavior + { + /// + /// Ignore this log type. + /// + Ignore, + + /// + /// Reporting only when handle this log type. + /// + ReportOnly, + + /// + /// Terminate Autopilot when handle this log type. + /// + TerminateAutopilot, + } + + /// + /// Error log handling using Application.logMessageReceived. + /// + [CreateAssetMenu(fileName = "New ErrorHandlerAgent", menuName = "Anjin/Error Handler Agent", order = 50)] + public class ErrorHandlerAgent : AbstractAgent + { + /// + /// Autopilot terminates and/or reports when an Exception is detected in the log. + /// + public HandlingBehavior handleException = HandlingBehavior.TerminateAutopilot; + + /// + /// Autopilot terminates and/or reports when an Error is detected in the log. + /// + public HandlingBehavior handleError = HandlingBehavior.TerminateAutopilot; + + /// + /// Autopilot terminates and/or reports when an Assert is detected in the log. + /// + public HandlingBehavior handleAssert = HandlingBehavior.TerminateAutopilot; + + /// + /// Autopilot terminates and/or reports when an Warning is detected in the log. + /// + public HandlingBehavior handleWarning = HandlingBehavior.Ignore; + + /// + /// Do not send Slack notifications when log messages contain this string + /// + public string[] ignoreMessages = new string[] { }; + + internal ITerminatable _autopilot; // can inject for testing + private List _ignoreMessagesRegexes; + private CancellationToken _token; + + public override async UniTask Run(CancellationToken token) + { + this._token = token; + + try + { + Logger.Log($"Enter {this.name}.Run()"); + + OverwriteByCommandLineArguments(new Arguments()); + + Application.logMessageReceivedThreaded += this.HandleLog; + + await UniTask.WaitWhile(() => true, cancellationToken: token); + } + finally + { + Application.logMessageReceivedThreaded -= this.HandleLog; + Logger.Log($"Exit {this.name}.Run()"); + } + } + + /// + /// Handle log message and post notification to Reporter (if necessary). + /// + /// Log message string + /// Stack trace + /// Log message type + internal async void HandleLog(string logString, string stackTrace, LogType type) + { + var handlingBehavior = JudgeHandlingBehavior(logString, stackTrace, type); + if (handlingBehavior == HandlingBehavior.Ignore) + { + return; + } + + // NOTE: HandleLog may called by non-main thread because it subscribe Application.logMessageReceivedThreaded + await UniTask.SwitchToMainThread(); + + Logger.Log(type, logString, stackTrace); + + var exitCode = type == LogType.Exception ? ExitCode.UnCatchExceptions : ExitCode.DetectErrorsInLog; + if (handlingBehavior == HandlingBehavior.ReportOnly) + { + var settings = AutopilotState.Instance.settings; + if (settings == null) + { + throw new InvalidOperationException("Autopilot is not running"); + } + + await settings.Reporter.PostReportAsync(logString, stackTrace, exitCode, this._token); + } + else + { + _autopilot = _autopilot ?? Autopilot.Instance; + await _autopilot.TerminateAsync(exitCode, logString, stackTrace, token: this._token); + } + } + + private HandlingBehavior JudgeHandlingBehavior(string logString, string stackTrace, LogType type) + { + if (type == LogType.Log) + { + return HandlingBehavior.Ignore; + } + + if (stackTrace.Contains(nameof(ErrorHandlerAgent)) || stackTrace.Contains(nameof(SlackAPI))) + { + Debug.Log($"Ignore looped message: {logString}"); + return HandlingBehavior.Ignore; + } + + if (IsIgnoreMessage(logString)) + { + return HandlingBehavior.Ignore; + } + + switch (type) + { + case LogType.Exception: + return this.handleException; + case LogType.Error: + return this.handleError; + case LogType.Assert: + return this.handleAssert; + case LogType.Warning: + return this.handleWarning; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + private bool IsIgnoreMessage(string logString) + { + if (this._ignoreMessagesRegexes == null) + { + this._ignoreMessagesRegexes = CreateIgnoreMessageRegexes(); + } + + return this._ignoreMessagesRegexes.Exists(regex => regex.IsMatch(logString)); + } + + private List CreateIgnoreMessageRegexes() + { + var regexs = new List(); + foreach (var message in this.ignoreMessages) + { + Regex pattern; + try + { + pattern = new Regex(message); + } + catch (ArgumentException e) + { + Debug.LogException(e); + continue; + } + + regexs.Add(pattern); + } + + return regexs; + } + + internal void OverwriteByCommandLineArguments(Arguments args) + { + if (args.HandleException.IsCaptured()) + { + this.handleException = HandlingBehaviorFrom(args.HandleException.Value()); + } + + if (args.HandleError.IsCaptured()) + { + this.handleError = HandlingBehaviorFrom(args.HandleError.Value()); + } + + if (args.HandleAssert.IsCaptured()) + { + this.handleAssert = HandlingBehaviorFrom(args.HandleAssert.Value()); + } + + if (args.HandleWarning.IsCaptured()) + { + this.handleWarning = HandlingBehaviorFrom(args.HandleWarning.Value()); + } + } + + internal static HandlingBehavior HandlingBehaviorFrom(bool value) + { + return value ? HandlingBehavior.TerminateAutopilot : HandlingBehavior.Ignore; + } + } +} diff --git a/Runtime/Utilities/LogMessageHandler.cs.meta b/Runtime/Agents/ErrorHandlerAgent.cs.meta similarity index 72% rename from Runtime/Utilities/LogMessageHandler.cs.meta rename to Runtime/Agents/ErrorHandlerAgent.cs.meta index 52049a7..2480cff 100644 --- a/Runtime/Utilities/LogMessageHandler.cs.meta +++ b/Runtime/Agents/ErrorHandlerAgent.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 3b75b13d94254371b057c5539f7caf4c timeCreated: 1620884310 \ No newline at end of file diff --git a/Runtime/Agents/TimeBombAgent.cs b/Runtime/Agents/TimeBombAgent.cs index 0b0a09a..828ca05 100644 --- a/Runtime/Agents/TimeBombAgent.cs +++ b/Runtime/Agents/TimeBombAgent.cs @@ -2,6 +2,7 @@ // This software is released under the MIT License. using System; +using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading; using Cysharp.Threading.Tasks; @@ -30,6 +31,7 @@ public class TimeBombAgent : AbstractAgent /// public string defuseMessage; + internal ITerminatable _autopilot; // can inject for testing private Regex DefuseMessageRegex => new Regex(defuseMessage); private CancellationTokenSource _cts; @@ -51,7 +53,7 @@ private void HandleDefuseMessage(string logString, string stackTrace, LogType ty { _cts?.Cancel(); } - catch (ObjectDisposedException e) + catch (ObjectDisposedException) { // ignored } @@ -78,10 +80,17 @@ public override async UniTask Run(CancellationToken token) // Note: This Agent does not consume pseudo-random numbers, so passed on as is. await agent.Run(linkedCts.Token); - throw new TimeoutException( - $"Could not receive defuse message `{defuseMessage}` before the agent terminated."); + // If the working Agent exits first, the TimeBombAgent will fail. + var message = + $"Could not receive defuse message `{defuseMessage}` before the {agent.name} terminated."; + Logger.Log(message); + _autopilot = _autopilot ?? Autopilot.Instance; + // ReSharper disable once MethodSupportsCancellation + _autopilot.TerminateAsync(ExitCode.AutopilotFailed, message, + new StackTrace(true).ToString()) + .Forget(); // Note: Do not use this Agent's CancellationToken. } - catch (OperationCanceledException e) + catch (OperationCanceledException) { if (token.IsCancellationRequested) // The parent was cancelled. { diff --git a/Runtime/Agents/UGUIMonkeyAgent.cs b/Runtime/Agents/UGUIMonkeyAgent.cs index 96e82db..92881f8 100644 --- a/Runtime/Agents/UGUIMonkeyAgent.cs +++ b/Runtime/Agents/UGUIMonkeyAgent.cs @@ -95,6 +95,8 @@ public class UGUIMonkeyAgent : AbstractAgent public ScreenCapture.StereoScreenCaptureMode screenshotStereoCaptureMode = ScreenCapture.StereoScreenCaptureMode.LeftEye; + internal ITerminatable _autopilot; // can inject for testing + /// public override async UniTask Run(CancellationToken token) { @@ -134,6 +136,15 @@ public override async UniTask Run(CancellationToken token) { await Monkey.Run(config, token); } + catch (TimeoutException e) + { + var message = $"{e.GetType().Name}: {e.Message}"; + Logger.Log(message); + _autopilot = _autopilot ?? Autopilot.Instance; + // ReSharper disable once MethodSupportsCancellation + _autopilot.TerminateAsync(ExitCode.AutopilotFailed, message, e.StackTrace) + .Forget(); // Note: Do not use this Agent's CancellationToken. + } finally { Logger.Log($"Exit {this.name}.Run()"); diff --git a/Runtime/Agents/UGUIPlaybackAgent.cs b/Runtime/Agents/UGUIPlaybackAgent.cs index 16bd11d..4cf2ea1 100644 --- a/Runtime/Agents/UGUIPlaybackAgent.cs +++ b/Runtime/Agents/UGUIPlaybackAgent.cs @@ -41,6 +41,7 @@ public override async UniTask Run(CancellationToken token) try { await Play(recordedJson, token); + // Note: If playback is not possible, AQA will output a LogError and exit. You must handle LogError with the ErrorHandlerAgent. } finally { diff --git a/Runtime/Autopilot.cs b/Runtime/Autopilot.cs index 03104e7..31891e7 100644 --- a/Runtime/Autopilot.cs +++ b/Runtime/Autopilot.cs @@ -7,9 +7,9 @@ using Cysharp.Threading.Tasks; using DeNA.Anjin.Settings; using DeNA.Anjin.Utilities; +using JetBrains.Annotations; using UnityEngine; using UnityEngine.SceneManagement; -using Assert = UnityEngine.Assertions.Assert; namespace DeNA.Anjin { @@ -39,16 +39,37 @@ public class Autopilot : MonoBehaviour, ITerminatable private ILogger _logger; private RandomFactory _randomFactory; private IAgentDispatcher _dispatcher; - private LogMessageHandler _logMessageHandler; private AutopilotState _state; private AutopilotSettings _settings; private bool _isTerminating; + /// + /// Returns the running Autopilot instance. + /// No caching. + /// + [NotNull] + public static Autopilot Instance + { + get + { + var autopilot = FindObjectOfType(); + if (autopilot == null) + { + throw new InvalidOperationException("Autopilot instance not found"); + } + + return autopilot; + } + } + private void Start() { _state = AutopilotState.Instance; _settings = _state.settings; - Assert.IsNotNull(_settings); + if (_settings == null) + { + throw new InvalidOperationException("Autopilot is not running"); + } _logger = _settings.LoggerAsset.Logger; // Note: Set a default logger if no logger settings. see: AutopilotSettings.Initialize method. @@ -61,11 +82,6 @@ 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. - _logMessageHandler = new LogMessageHandler(_settings, this); - _dispatcher = new AgentDispatcher(_settings, _logger, _randomFactory); _dispatcher.DispatchSceneCrossingAgents(); var dispatched = _dispatcher.DispatchByScene(SceneManager.GetActiveScene(), false); @@ -118,7 +134,6 @@ private void OnDestroy() _logger?.Log("Destroy Autopilot object"); _dispatcher?.Dispose(); - _logMessageHandler?.Dispose(); _settings.LoggerAsset?.Dispose(); } diff --git a/Runtime/ExitCode.cs b/Runtime/ExitCode.cs index dc4b3f6..8c518a2 100644 --- a/Runtime/ExitCode.cs +++ b/Runtime/ExitCode.cs @@ -9,23 +9,28 @@ namespace DeNA.Anjin public enum ExitCode { /// - /// Normally exit. + /// Normally terminate. /// Normally = 0, /// - /// Uncaught exceptions. + /// Terminate by uncaught exceptions. /// UnCatchExceptions = 1, /// - /// Autopilot running failed. + /// Terminate by ErrorHandlerAgent. /// - AutopilotFailed = 2, + DetectErrorsInLog, /// - /// Autopilot launching failed. + /// Terminate by Autopilot launching failure. /// - AutopilotLaunchingFailed + AutopilotLaunchingFailed, + + /// + /// Terminate by Autopilot scenario running failure. + /// + AutopilotFailed, } } diff --git a/Runtime/Launcher.cs b/Runtime/Launcher.cs index 75f5f7f..493b3a8 100644 --- a/Runtime/Launcher.cs +++ b/Runtime/Launcher.cs @@ -12,7 +12,6 @@ using DeNA.Anjin.Settings; using DeNA.Anjin.Utilities; using UnityEngine; -using Assert = UnityEngine.Assertions.Assert; using Object = UnityEngine.Object; #if UNITY_INCLUDE_TESTS using NUnit.Framework; @@ -67,7 +66,10 @@ public static async UniTask LaunchAutopilotAsync(string settingsPath, Cancellati #else var settings = Resources.Load(settingsPath); #endif - Assert.IsNotNull(settings, $"Autopilot settings not found: {settingsPath}"); + if (settings == null) + { + throw new ArgumentException($"Autopilot settings not found: {settingsPath}"); + } await LaunchAutopilotAsync(settings, token); } diff --git a/Runtime/Reporters/JUnitXmlReporter.cs b/Runtime/Reporters/JUnitXmlReporter.cs index 6c0d485..5be5af3 100644 --- a/Runtime/Reporters/JUnitXmlReporter.cs +++ b/Runtime/Reporters/JUnitXmlReporter.cs @@ -10,7 +10,6 @@ using DeNA.Anjin.Settings; using DeNA.Anjin.Utilities; using UnityEngine; -using UnityEngine.Assertions; namespace DeNA.Anjin.Reporters { @@ -43,7 +42,10 @@ public override async UniTask PostReportAsync(string message, string stackTrace, CancellationToken cancellationToken = default) { var settings = AutopilotState.Instance.settings; - Assert.IsNotNull(settings, "Autopilot is not running"); + if (settings == null) + { + throw new InvalidOperationException("Autopilot is not running"); + } var path = GetOutputPath(this.outputPath, new Arguments()); if (string.IsNullOrEmpty(path)) @@ -106,16 +108,17 @@ internal static XElement CreateTestCase(string name, float time, string message, case ExitCode.Normally: break; case ExitCode.UnCatchExceptions: + case ExitCode.DetectErrorsInLog: + case ExitCode.AutopilotLaunchingFailed: element.Add(new XElement("error", new XAttribute("message", message), - new XAttribute("type", ""), + new XAttribute("type", exitCode.ToString()), new XCData(stackTrace))); break; case ExitCode.AutopilotFailed: - case ExitCode.AutopilotLaunchingFailed: element.Add(new XElement("failure", new XAttribute("message", message), - new XAttribute("type", ""), + new XAttribute("type", exitCode.ToString()), new XCData(stackTrace))); break; default: diff --git a/Runtime/Settings/AutopilotSettings.cs b/Runtime/Settings/AutopilotSettings.cs index cce74f2..f8c15c6 100644 --- a/Runtime/Settings/AutopilotSettings.cs +++ b/Runtime/Settings/AutopilotSettings.cs @@ -10,7 +10,6 @@ using DeNA.Anjin.Loggers; using DeNA.Anjin.Reporters; using UnityEngine; -using UnityEngine.Assertions; using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; @@ -135,26 +134,31 @@ public class AutopilotSettings : ScriptableObject /// /// Slack notification when Exception detected in log /// - public bool handleException = true; + [Obsolete("Use ErrorHandlerAgent instead")] + public bool handleException; /// /// Slack notification when Error detected in log /// - public bool handleError = true; + [Obsolete("Use ErrorHandlerAgent instead")] + public bool handleError; /// /// Slack notification when Assert detected in log /// - public bool handleAssert = true; + [Obsolete("Use ErrorHandlerAgent instead")] + public bool handleAssert; /// /// Slack notification when Warning detected in log /// + [Obsolete("Use ErrorHandlerAgent instead")] public bool handleWarning; /// /// Do not send Slack notifications when log messages contain this string /// + [Obsolete("Use ErrorHandlerAgent instead")] public string[] ignoreMessages; /// @@ -238,33 +242,16 @@ internal void OverwriteByCommandLineArguments(Arguments args) { timeScale = args.TimeScale.Value(); } - - if (args.HandleException.IsCaptured()) - { - handleException = args.HandleException.Value(); - } - - if (args.HandleError.IsCaptured()) - { - handleError = args.HandleError.Value(); - } - - if (args.HandleAssert.IsCaptured()) - { - handleAssert = args.HandleAssert.Value(); - } - - if (args.HandleWarning.IsCaptured()) - { - handleWarning = args.HandleWarning.Value(); - } } [InitializeOnLaunchAutopilot(InitializeOnLaunchAutopilotAttribute.InitializeSettings)] private static void Initialize() { var settings = AutopilotState.Instance.settings; - Assert.IsNotNull(settings); + if (settings == null) + { + throw new InvalidOperationException("Autopilot is not running"); + } settings.OverwriteByCommandLineArguments(new Arguments()); @@ -276,7 +263,8 @@ private static void Initialize() settings.ConvertSlackReporterFromObsoleteSlackSettings(logger); settings.ConvertJUnitXmlReporterFromObsoleteJUnitReportPath(logger); - settings.ConvertSceneCrossingAgentsFromObsoleteObserverAgent(logger); // Note: before convert other Agents. + settings.ConvertSceneCrossingAgentsFromObsoleteObserverAgent(logger); // Note: before convert other Agents. + settings.ConvertErrorHandlerAgentFromObsoleteSettings(logger); } private void CreateDefaultLoggerIfNeeded() @@ -409,5 +397,38 @@ internal void ConvertSceneCrossingAgentsFromObsoleteObserverAgent(ILogger logger this.sceneCrossingAgents.Add(this.observerAgent); } + + [Obsolete("Remove this method when bump major version")] + internal void ConvertErrorHandlerAgentFromObsoleteSettings(ILogger logger) + { + if ((!this.handleException && !this.handleError && !this.handleAssert && !this.handleWarning) || + this.sceneCrossingAgents.Any(x => x.GetType() == typeof(ErrorHandlerAgent))) + { + return; + // Note: + // If all are off, no conversion will occur. + // No conversion will be performed when creating a new AutopilotSettings because all default values are false. + } + + const string AutoConvertingMessage = @"Error handling settings in AutopilotSettings has been obsolete. +Please delete the value using Debug Mode in the Inspector window. And create an ErrorHandlerAgent asset file. +This time, temporarily generate and use ErrorHandlerAgent instance."; + logger.Log(LogType.Warning, AutoConvertingMessage); + + var convertedAgent = CreateInstance(); + convertedAgent.handleException = ErrorHandlerAgent.HandlingBehaviorFrom(this.handleException); + convertedAgent.handleError = ErrorHandlerAgent.HandlingBehaviorFrom(this.handleError); + convertedAgent.handleAssert = ErrorHandlerAgent.HandlingBehaviorFrom(this.handleAssert); + convertedAgent.handleWarning = ErrorHandlerAgent.HandlingBehaviorFrom(this.handleWarning); + convertedAgent.ignoreMessages = this.ignoreMessages; +#if UNITY_EDITOR + convertedAgent.description = AutoConvertingMessage; + SaveConvertedObject(convertedAgent); +#endif + this.sceneCrossingAgents.Add(convertedAgent); +#if UNITY_EDITOR + EditorUtility.SetDirty(this); +#endif + } } } diff --git a/Runtime/Utilities/LogMessageHandler.cs b/Runtime/Utilities/LogMessageHandler.cs deleted file mode 100644 index f0885c3..0000000 --- a/Runtime/Utilities/LogMessageHandler.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2023 DeNA Co., Ltd. -// This software is released under the MIT License. - -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Cysharp.Threading.Tasks; -using DeNA.Anjin.Reporters; -using DeNA.Anjin.Reporters.Slack; -using DeNA.Anjin.Settings; -using UnityEngine; - -namespace DeNA.Anjin.Utilities -{ - /// - /// Log message handling using Application.logMessageReceived. - /// - public class LogMessageHandler : IDisposable - { - private readonly AutopilotSettings _settings; - private readonly ITerminatable _autopilot; - - private List _ignoreMessagesRegexes; - - /// - /// Constructor - /// - /// Autopilot settings - /// Autopilot instance that termination target - public LogMessageHandler(AutopilotSettings settings, ITerminatable autopilot) - { - _settings = settings; - _autopilot = autopilot; - - Application.logMessageReceivedThreaded += this.HandleLog; - } - - public void Dispose() - { - Application.logMessageReceivedThreaded -= this.HandleLog; - } - - /// - /// Handle log message and post notification to Reporter (if necessary). - /// - /// Log message string - /// Stack trace - /// Log message type - public async void HandleLog(string logString, string stackTrace, LogType type) - { - if (_settings == null) - { - return; - } - - if (IsIgnoreMessage(logString, stackTrace, type)) - { - return; - } - - // NOTE: HandleLog may called by non-main thread because it subscribe Application.logMessageReceivedThreaded - await UniTask.SwitchToMainThread(); - - if (_settings.LoggerAsset != null) - { - _settings.LoggerAsset.Logger.Log(type, logString, stackTrace); - } - - if (type == LogType.Exception) - { - await _autopilot.TerminateAsync(ExitCode.UnCatchExceptions, logString, stackTrace); - } - else - { - await _autopilot.TerminateAsync(ExitCode.AutopilotFailed, logString, stackTrace); - } - } - - private bool IsIgnoreMessage(string logString, string stackTrace, LogType type) - { - if (type == LogType.Log) - { - return true; - } - - if (type == LogType.Exception && !_settings.handleException) - { - return true; - } - - if (type == LogType.Assert && !_settings.handleAssert) - { - return true; - } - - if (type == LogType.Error && !_settings.handleError) - { - return true; - } - - if (type == LogType.Warning && !_settings.handleWarning) - { - return true; - } - - if (_ignoreMessagesRegexes == null) - { - _ignoreMessagesRegexes = CreateIgnoreMessageRegexes(_settings); - } - - if (_ignoreMessagesRegexes.Exists(regex => regex.IsMatch(logString))) - { - return true; - } - - if (stackTrace.Contains(nameof(LogMessageHandler)) || stackTrace.Contains(nameof(SlackAPI))) - { - Debug.Log($"Ignore looped message: {logString}"); - return true; - } - - return false; - } - - private static List CreateIgnoreMessageRegexes(AutopilotSettings settings) - { - var regexs = new List(); - foreach (var message in settings.ignoreMessages) - { - Regex pattern; - try - { - pattern = new Regex(message); - } - catch (ArgumentException e) - { - Debug.LogException(e); - continue; - } - - regexs.Add(pattern); - } - - return regexs; - } - } -} diff --git a/Tests/Runtime/Agents/ErrorHandlerAgentTest.cs b/Tests/Runtime/Agents/ErrorHandlerAgentTest.cs new file mode 100644 index 0000000..17336be --- /dev/null +++ b/Tests/Runtime/Agents/ErrorHandlerAgentTest.cs @@ -0,0 +1,266 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Cysharp.Threading.Tasks; +using DeNA.Anjin.Settings; +using DeNA.Anjin.TestDoubles; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace DeNA.Anjin.Agents +{ + [TestFixture] + public class ErrorHandlerAgentTest + { + private SpyTerminatable _spyTerminatable; + private SpyLoggerAsset _spyLoggerAsset; + private SpyReporter _spyReporter; + private ErrorHandlerAgent _sut; + + private const string Message = "MESSAGE"; + private const string StackTrace = "STACK TRACE"; + + [SetUp] + public void SetUp() + { + _spyTerminatable = new SpyTerminatable(); + _spyLoggerAsset = ScriptableObject.CreateInstance(); + + _sut = ScriptableObject.CreateInstance(); + _sut._autopilot = _spyTerminatable; + _sut.Logger = _spyLoggerAsset.Logger; + + _spyReporter = ScriptableObject.CreateInstance(); + var settings = ScriptableObject.CreateInstance(); + settings.Reporter.reporters.Add(_spyReporter); + AutopilotState.Instance.settings = settings; + } + + [TearDown] + public void TearDown() + { + AutopilotState.Instance.Reset(); + } + + [Test] + public async Task HandleLog_LogTypeLog_NotCallTerminate() + { + _sut.HandleLog(Message, StackTrace, LogType.Log); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); + } + + private static IEnumerable LogTypeTestCase() + { + yield return new TestCaseData(LogType.Exception, ExitCode.UnCatchExceptions); + yield return new TestCaseData(LogType.Error, ExitCode.DetectErrorsInLog); + yield return new TestCaseData(LogType.Assert, ExitCode.DetectErrorsInLog); + yield return new TestCaseData(LogType.Warning, ExitCode.DetectErrorsInLog); + } + + private void SetHandlingBehavior(LogType logType, HandlingBehavior handlingBehavior) + { + _sut.handleException = HandlingBehavior.Ignore; + _sut.handleError = HandlingBehavior.Ignore; + _sut.handleAssert = HandlingBehavior.Ignore; + _sut.handleWarning = HandlingBehavior.Ignore; + + switch (logType) + { + case LogType.Exception: + _sut.handleException = handlingBehavior; + break; + case LogType.Error: + _sut.handleError = handlingBehavior; + break; + case LogType.Assert: + _sut.handleAssert = handlingBehavior; + break; + case LogType.Warning: + _sut.handleWarning = handlingBehavior; + break; + case LogType.Log: + default: + throw new ArgumentOutOfRangeException(nameof(logType), logType, null); + } + } + + [TestCaseSource(nameof(LogTypeTestCase))] + public async Task HandleLog_HandlingBehaviorIsIgnore(LogType logType, ExitCode _) + { + SetHandlingBehavior(logType, HandlingBehavior.Ignore); + _sut.HandleLog(Message, StackTrace, logType); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); + Assert.That(_spyReporter.IsCalled, Is.False); + } + + [TestCaseSource(nameof(LogTypeTestCase))] + public async Task HandleLog_HandlingBehaviorIsReportOnly(LogType logType, ExitCode expectedExitCode) + { + SetHandlingBehavior(logType, HandlingBehavior.ReportOnly); + _sut.HandleLog(Message, StackTrace, logType); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); + Assert.That(_spyReporter.IsCalled, Is.True); + Assert.That(_spyReporter.Arguments["message"], Is.EqualTo(Message)); + Assert.That(_spyReporter.Arguments["stackTrace"], Is.EqualTo(StackTrace)); + Assert.That(_spyReporter.Arguments["exitCode"], Is.EqualTo(expectedExitCode.ToString())); + } + + [TestCaseSource(nameof(LogTypeTestCase))] + public async Task HandleLog_HandlingBehaviorIsTerminateAutopilot(LogType logType, ExitCode expectedExitCode) + { + SetHandlingBehavior(logType, HandlingBehavior.TerminateAutopilot); + _sut.HandleLog(Message, StackTrace, logType); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.True); + Assert.That(_spyTerminatable.CapturedExitCode, Is.EqualTo(expectedExitCode)); + Assert.That(_spyTerminatable.CapturedMessage, Is.EqualTo(Message)); + Assert.That(_spyTerminatable.CapturedStackTrace, Is.EqualTo(StackTrace)); + Assert.That(_spyTerminatable.CapturedReporting, Is.True); + } + + [Test] + public async Task HandleLog_ContainsIgnoreMessage_NotTerminate() + { + _sut.ignoreMessages = new[] { "ignore" }; + _sut.handleException = HandlingBehavior.TerminateAutopilot; + _sut.HandleLog("xxx_ignore_xxx", string.Empty, LogType.Exception); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); + } + + [Test] + public async Task HandleLog_MatchIgnoreMessagePattern_NotTerminate() + { + _sut.ignoreMessages = new[] { "ignore.+ignore" }; + _sut.handleException = HandlingBehavior.TerminateAutopilot; + _sut.HandleLog("ignore_xxx_ignore", string.Empty, LogType.Exception); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); + } + + [Test] + public async Task HandleLog_NotMatchIgnoreMessagePattern_Terminate() + { + _sut.ignoreMessages = new[] { "ignore.+ignore" }; + _sut.handleException = HandlingBehavior.TerminateAutopilot; + _sut.HandleLog("ignore", string.Empty, LogType.Exception); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.True); + } + + [Test] + public async Task HandleLog_InvalidIgnoreMessagePattern_ThrowArgumentExceptionAndTerminate() + { + _sut.ignoreMessages = new[] { "[a" }; // invalid pattern + _sut.handleException = HandlingBehavior.TerminateAutopilot; + + _sut.HandleLog("ignore", string.Empty, LogType.Exception); + await UniTask.NextFrame(); + + LogAssert.Expect(LogType.Exception, "ArgumentException: parsing \"[a\" - Unterminated [] set."); + Assert.That(_spyTerminatable.IsCalled, Is.True); + } + + [Test] + public async Task HandleLog_StackTraceContainsErrorHandlerAgent_NotTerminate() + { + _sut.handleException = HandlingBehavior.TerminateAutopilot; + _sut.HandleLog(string.Empty, "at DeNA.Anjin.Agents.ErrorHandlerAgent", LogType.Exception); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); + } + + [Test] + public void OverwriteByCommandLineArguments_NotSpecified_SameAsDefault() + { + var sut = ScriptableObject.CreateInstance(); + Assume.That(sut.handleException, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + Assume.That(sut.handleError, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + Assume.That(sut.handleAssert, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + Assume.That(sut.handleWarning, Is.EqualTo(HandlingBehavior.Ignore)); + + sut.OverwriteByCommandLineArguments(new StubArguments() + { + _handleException = new StubArgument(), // Not captured + _handleError = new StubArgument(), // Not captured + _handleAssert = new StubArgument(), // Not captured + _handleWarning = new StubArgument(), // Not captured + }); + Assert.That(sut.handleException, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + Assert.That(sut.handleError, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + Assert.That(sut.handleAssert, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + Assert.That(sut.handleWarning, Is.EqualTo(HandlingBehavior.Ignore)); + } + + [Test] + public void OverwriteByCommandLineArguments_SpecifyHandleException_Overwrite() + { + var sut = ScriptableObject.CreateInstance(); + sut.OverwriteByCommandLineArguments(new StubArguments() + { + _handleException = new StubArgument(captured: true, value: false), // flip value + _handleError = new StubArgument(), + _handleAssert = new StubArgument(), + _handleWarning = new StubArgument(), + }); + Assert.That(sut.handleException, Is.EqualTo(HandlingBehavior.Ignore)); + } + + [Test] + public void OverwriteByCommandLineArguments_SpecifyHandleError_Overwrite() + { + var sut = ScriptableObject.CreateInstance(); + sut.OverwriteByCommandLineArguments(new StubArguments() + { + _handleException = new StubArgument(), + _handleError = new StubArgument(captured: true, value: false), // flip value + _handleAssert = new StubArgument(), + _handleWarning = new StubArgument(), + }); + Assert.That(sut.handleError, Is.EqualTo(HandlingBehavior.Ignore)); + } + + [Test] + public void OverwriteByCommandLineArguments_SpecifyHandleAssert_Overwrite() + { + var sut = ScriptableObject.CreateInstance(); + sut.OverwriteByCommandLineArguments(new StubArguments() + { + _handleException = new StubArgument(), + _handleError = new StubArgument(), + _handleAssert = new StubArgument(captured: true, value: false), // flip value + _handleWarning = new StubArgument(), + }); + Assert.That(sut.handleAssert, Is.EqualTo(HandlingBehavior.Ignore)); + } + + [Test] + public void OverwriteByCommandLineArguments_SpecifyHandleWarning_Overwrite() + { + var sut = ScriptableObject.CreateInstance(); + sut.OverwriteByCommandLineArguments(new StubArguments() + { + _handleException = new StubArgument(), + _handleError = new StubArgument(), + _handleAssert = new StubArgument(), + _handleWarning = new StubArgument(captured: true, value: true), // flip value + }); + Assert.That(sut.handleWarning, Is.EqualTo(HandlingBehavior.TerminateAutopilot)); + } + } +} diff --git a/Tests/Runtime/Utilities/LogMessageHandlerTest.cs.meta b/Tests/Runtime/Agents/ErrorHandlerAgentTest.cs.meta similarity index 100% rename from Tests/Runtime/Utilities/LogMessageHandlerTest.cs.meta rename to Tests/Runtime/Agents/ErrorHandlerAgentTest.cs.meta diff --git a/Tests/Runtime/Agents/TimeBombAgentTest.cs b/Tests/Runtime/Agents/TimeBombAgentTest.cs index ae53193..5d28bca 100644 --- a/Tests/Runtime/Agents/TimeBombAgentTest.cs +++ b/Tests/Runtime/Agents/TimeBombAgentTest.cs @@ -1,10 +1,11 @@ // Copyright (c) 2023-2024 DeNA Co., Ltd. // This software is released under the MIT License. -using System; using System.Threading; using System.Threading.Tasks; using Cysharp.Threading.Tasks; +using DeNA.Anjin.TestComponents; +using DeNA.Anjin.TestDoubles; using DeNA.Anjin.Utilities; using NUnit.Framework; using TestHelper.Attributes; @@ -18,6 +19,12 @@ public class TimeBombAgentTest { private const string TestScene = "Packages/com.dena.anjin/Tests/TestScenes/OutGameTutorial.unity"; + [SetUp] + public void SetUp() + { + OutGameTutorialButton.ResetTutorialCompleted(); + } + private static UGUIMonkeyAgent CreateMonkeyAgent(long lifespanSec) { var agent = ScriptableObject.CreateInstance(); @@ -85,25 +92,27 @@ public async Task Run_Defuse_StopAgent() [Test] [LoadScene(TestScene)] - public async Task Run_NotDefuse_ThrowsTimeoutException() + public async Task Run_NotDefuse_AutopilotFailed() { var monkeyAgent = CreateMonkeyAgent(1); var agent = CreateTimeBombAgent(monkeyAgent, "^Never match!$"); + var spyTerminatable = new SpyTerminatable(); + agent._autopilot = spyTerminatable; using (var cts = new CancellationTokenSource()) { - try - { - await agent.Run(cts.Token); - Assert.Fail("Should throw TimeoutException"); - } - catch (TimeoutException e) - { - Assert.That(e.Message, Is.EqualTo( - "Could not receive defuse message `^Never match!$` before the agent terminated.")); - } + await agent.Run(cts.Token); + await UniTask.NextFrame(); } + Assert.That(spyTerminatable.IsCalled, Is.True); + Assert.That(spyTerminatable.CapturedExitCode, Is.EqualTo(ExitCode.AutopilotFailed)); + Assert.That(spyTerminatable.CapturedMessage, Is.EqualTo( + "Could not receive defuse message `^Never match!$` before the UGUIMonkeyAgent terminated.")); + Assert.That(spyTerminatable.CapturedStackTrace, Does.StartWith( + " at DeNA.Anjin.Agents.TimeBombAgent")); + Assert.That(spyTerminatable.CapturedReporting, Is.True); + LogAssert.Expect(LogType.Log, $"Enter {agent.name}.Run()"); LogAssert.Expect(LogType.Log, $"Enter {monkeyAgent.name}.Run()"); LogAssert.Expect(LogType.Log, $"Exit {monkeyAgent.name}.Run()"); diff --git a/Tests/Runtime/Agents/UGUIMonkeyAgentTest.cs b/Tests/Runtime/Agents/UGUIMonkeyAgentTest.cs index 61ba847..77c093e 100644 --- a/Tests/Runtime/Agents/UGUIMonkeyAgentTest.cs +++ b/Tests/Runtime/Agents/UGUIMonkeyAgentTest.cs @@ -6,6 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Cysharp.Threading.Tasks; +using DeNA.Anjin.Strategies; +using DeNA.Anjin.TestDoubles; using DeNA.Anjin.Utilities; using NUnit.Framework; using TestHelper.Attributes; @@ -92,9 +94,12 @@ public async Task Run_DefaultScreenshotFilenamePrefix_UseAgentName() agent.name = AgentName; agent.lifespanSec = 1; agent.delayMillis = 100; + agent.touchAndHoldDelayMillis = 100; agent.screenshotEnabled = true; agent.defaultScreenshotFilenamePrefix = true; // Use default prefix + TwoTieredCounterStrategy.ResetPrefixCounters(); + using (var cancellationTokenSource = new CancellationTokenSource()) { var token = cancellationTokenSource.Token; @@ -103,5 +108,40 @@ public async Task Run_DefaultScreenshotFilenamePrefix_UseAgentName() Assert.That(path, Does.Exist); } + + [Test] + [FocusGameView] + [LoadScene("Packages/com.dena.anjin/Tests/TestScenes/Empty.unity")] + public async Task Run_TimeoutExceptionOccurred_AutopilotFailed() + { + var agent = ScriptableObject.CreateInstance(); + agent.Logger = Debug.unityLogger; + agent.Random = new RandomFactory(0).CreateRandom(); + agent.name = TestContext.CurrentContext.Test.Name; + agent.lifespanSec = 5; + agent.delayMillis = 100; + agent.touchAndHoldDelayMillis = 100; + agent.secondsToErrorForNoInteractiveComponent = 1; // TimeoutException occurred after 1 second + + var spyTerminatable = new SpyTerminatable(); + agent._autopilot = spyTerminatable; + + using (var cts = new CancellationTokenSource()) + { + await agent.Run(cts.Token); + await UniTask.NextFrame(); + } + + Assert.That(spyTerminatable.IsCalled, Is.True); + Assert.That(spyTerminatable.CapturedExitCode, Is.EqualTo(ExitCode.AutopilotFailed)); + Assert.That(spyTerminatable.CapturedMessage, Does.StartWith( + "TimeoutException: Interactive component not found in 1 seconds")); + Assert.That(spyTerminatable.CapturedStackTrace, Does.StartWith( + " at TestHelper.Monkey.Monkey")); + Assert.That(spyTerminatable.CapturedReporting, Is.True); + + LogAssert.Expect(LogType.Log, $"Enter {agent.name}.Run()"); + LogAssert.Expect(LogType.Log, $"Exit {agent.name}.Run()"); + } } } diff --git a/Tests/Runtime/Reporters/JUnitXmlReporterTest.cs b/Tests/Runtime/Reporters/JUnitXmlReporterTest.cs index d920e3d..ea29f2e 100644 --- a/Tests/Runtime/Reporters/JUnitXmlReporterTest.cs +++ b/Tests/Runtime/Reporters/JUnitXmlReporterTest.cs @@ -77,7 +77,7 @@ public void CreateTestCase_Error() new XAttribute("status", "Failed"), new XElement("error", new XAttribute("message", message), - new XAttribute("type", ""), + new XAttribute("type", "UnCatchExceptions"), new XCData(stackTrace) ) ); @@ -102,7 +102,7 @@ public void CreateTestCase_Failure() new XAttribute("status", "Failed"), new XElement("failure", new XAttribute("message", message), - new XAttribute("type", ""), + new XAttribute("type", "AutopilotFailed"), new XCData(stackTrace) ) ); @@ -305,7 +305,7 @@ public async Task PostReportAsync_Error() status=""Failed"" > diff --git a/Tests/Runtime/Settings/AutopilotSettingsTest.cs b/Tests/Runtime/Settings/AutopilotSettingsTest.cs index cd233c1..1c3488f 100644 --- a/Tests/Runtime/Settings/AutopilotSettingsTest.cs +++ b/Tests/Runtime/Settings/AutopilotSettingsTest.cs @@ -18,10 +18,6 @@ private static AutopilotSettings CreateAutopilotSettings() sut.lifespanSec = 5; sut.randomSeed = "1"; sut.timeScale = 1.0f; - sut.handleException = true; - sut.handleError = true; - sut.handleAssert = true; - sut.handleWarning = true; return sut; } @@ -32,10 +28,6 @@ private static Arguments CreateNotCapturedArguments() _lifespanSec = new StubArgument(), // Not captured _randomSeed = new StubArgument(), // Not captured _timeScale = new StubArgument(), // Not captured - _handleException = new StubArgument(), // Not captured - _handleError = new StubArgument(), // Not captured - _handleAssert = new StubArgument(), // Not captured - _handleWarning = new StubArgument(), // Not captured }; return arguments; } @@ -49,10 +41,6 @@ public void OverwriteByCommandLineArguments_HasNotCommandlineArguments_KeepScrip Assert.That(sut.lifespanSec, Is.EqualTo(5)); Assert.That(sut.randomSeed, Is.EqualTo("1")); Assert.That(sut.timeScale, Is.EqualTo(1.0f)); - Assert.That(sut.handleException, Is.True); - Assert.That(sut.handleError, Is.True); - Assert.That(sut.handleAssert, Is.True); - Assert.That(sut.handleWarning, Is.True); } private static Arguments CreateCapturedArguments() @@ -62,10 +50,6 @@ private static Arguments CreateCapturedArguments() _lifespanSec = new StubArgument(true, 2), _randomSeed = new StubArgument(true, ""), _timeScale = new StubArgument(true, 2.5f), - _handleException = new StubArgument(true, false), - _handleError = new StubArgument(true, false), - _handleAssert = new StubArgument(true, false), - _handleWarning = new StubArgument(true, false), }; return arguments; } @@ -79,10 +63,6 @@ public void OverwriteByCommandLineArguments_HasCommandlineArguments_OverwriteVal Assert.That(sut.lifespanSec, Is.EqualTo(2)); Assert.That(sut.randomSeed, Is.EqualTo("")); Assert.That(sut.timeScale, Is.EqualTo(2.5f)); - Assert.That(sut.handleException, Is.False); - Assert.That(sut.handleError, Is.False); - Assert.That(sut.handleAssert, Is.False); - Assert.That(sut.handleWarning, Is.False); } [Test] @@ -309,5 +289,74 @@ public void ConvertJUnitXmlReporterFromObsoleteJUnitReportPath_ExistReporter_Not Assert.That(settings.Reporter.reporters.Count, Is.EqualTo(1)); Assert.That(settings.Reporter.reporters, Does.Contain(existReporter)); } + + [TestCase(true, false, false, false)] + [TestCase(false, true, false, false)] + [TestCase(false, false, true, false)] + [TestCase(false, false, false, true)] + public void ConvertErrorHandlerAgentFromObsoleteSettings_HasAnyHandlingFlag_GenerateErrorHandlerAgent( + bool handleException, + bool handleError, + bool handleAssert, + bool handleWarning) + { + var ignoreMessages = new string[] { "message1", "message2" }; + var settings = ScriptableObject.CreateInstance(); + settings.handleException = handleException; + settings.handleError = handleError; + settings.handleAssert = handleAssert; + settings.handleWarning = handleWarning; + settings.ignoreMessages = ignoreMessages; + + var spyLogger = ScriptableObject.CreateInstance(); + settings.ConvertErrorHandlerAgentFromObsoleteSettings(spyLogger.Logger); + + Assert.That(settings.sceneCrossingAgents.Count, Is.EqualTo(1)); + var errorHandlerAgent = settings.sceneCrossingAgents[0] as ErrorHandlerAgent; + Assert.That(errorHandlerAgent, Is.Not.Null); + Assert.That(errorHandlerAgent.handleException, + Is.EqualTo(handleException ? HandlingBehavior.TerminateAutopilot : HandlingBehavior.Ignore)); + Assert.That(errorHandlerAgent.handleError, + Is.EqualTo(handleError ? HandlingBehavior.TerminateAutopilot : HandlingBehavior.Ignore)); + Assert.That(errorHandlerAgent.handleAssert, + Is.EqualTo(handleAssert ? HandlingBehavior.TerminateAutopilot : HandlingBehavior.Ignore)); + Assert.That(errorHandlerAgent.handleWarning, + Is.EqualTo(handleWarning ? HandlingBehavior.TerminateAutopilot : HandlingBehavior.Ignore)); + Assert.That(errorHandlerAgent.ignoreMessages, Is.EqualTo(ignoreMessages)); + + Assert.That(spyLogger.Logs, Has.Member((LogType.Warning, + @"Error handling settings in AutopilotSettings has been obsolete. +Please delete the value using Debug Mode in the Inspector window. And create an ErrorHandlerAgent asset file. +This time, temporarily generate and use ErrorHandlerAgent instance."))); + } + + [Test] + public void ConvertErrorHandlerAgentFromObsoleteSettings_HasNotHandlingFlags_NotGenerateErrorHandlerAgent() + { + var settings = ScriptableObject.CreateInstance(); + settings.handleException = false; + settings.handleError = false; + settings.handleAssert = false; + settings.handleWarning = false; + + settings.ConvertErrorHandlerAgentFromObsoleteSettings(Debug.unityLogger); + Assert.That(settings.sceneCrossingAgents, Is.Empty); + } + + [Test] + public void ConvertErrorHandlerAgentFromObsoleteSettings_ExistErrorHandlerAgent_NotGenerateErrorHandlerAgent() + { + var existAgent = ScriptableObject.CreateInstance(); + var settings = ScriptableObject.CreateInstance(); + settings.sceneCrossingAgents.Add(existAgent); // already exists + settings.handleException = true; + settings.handleError = false; + settings.handleAssert = false; + settings.handleWarning = false; + + settings.ConvertErrorHandlerAgentFromObsoleteSettings(Debug.unityLogger); + Assert.That(settings.sceneCrossingAgents.Count, Is.EqualTo(1)); + Assert.That(settings.sceneCrossingAgents, Does.Contain(existAgent)); + } } } diff --git a/Tests/Runtime/TestComponents/OutGameTutorialButton.cs b/Tests/Runtime/TestComponents/OutGameTutorialButton.cs index 2139578..bc4db69 100644 --- a/Tests/Runtime/TestComponents/OutGameTutorialButton.cs +++ b/Tests/Runtime/TestComponents/OutGameTutorialButton.cs @@ -12,6 +12,11 @@ public class OutGameTutorialButton : MonoBehaviour { private static bool s_tutorialCompleted; + public static void ResetTutorialCompleted() + { + s_tutorialCompleted = false; + } + private void Awake() { var button = GetComponent